[
  {
    "path": ".changeset/cold-foxes-lie.md",
    "content": "---\n\"@bigcommerce/catalyst-core\": patch\n---\n\nFix cart summary Discounts row not showing manual discounts applied via the Management Checkout API\n"
  },
  {
    "path": ".changeset/config.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@3.0.0/schema.json\",\n  \"changelog\": [\n    \"@changesets/changelog-github\",\n    { \"repo\": \"bigcommerce/catalyst\" }\n  ],\n  \"commit\": false,\n  \"linked\": [],\n  \"access\": \"public\",\n  \"privatePackages\": {\n    \"version\": true,\n    \"tag\": true\n  },\n  \"baseBranch\": \"canary\",\n  \"updateInternalDependencies\": \"patch\",\n  \"ignore\": [\"@bigcommerce/catalyst\"]\n}\n"
  },
  {
    "path": ".changeset/correlation-id-header.md",
    "content": "---\n\"@bigcommerce/catalyst-core\": patch\n---\n\nAdd X-Correlation-ID header to all GraphQL requests. Each page render gets a stable UUID that persists across all fetches within the same render, enabling easier request tracing in server logs.\n"
  },
  {
    "path": ".changeset/fix-hidden-fields-d35665be.md",
    "content": "---\n\"@bigcommerce/catalyst-core\": patch\n---\n\nFix DynamicForm not rendering hidden field types, which caused `pageEntityId` to be `NaN` on contact form submission.\n"
  },
  {
    "path": ".changeset/fix-html-lang-locale.md",
    "content": "---\n\"@bigcommerce/catalyst-core\": minor\n---\n\nRestore locale-aware `lang` attribute on the root `<html>` tag. The previous root layout hardcoded `lang=\"en\"` for all locales; ownership of `<html>`/`<body>` now lives in `app/[locale]/layout.tsx` so `lang={locale}` reflects the active locale. The root `app/layout.tsx` is now a passthrough, and `app/not-found.tsx` is self-sufficient (renders its own `<html>`/`<body>`) to preserve the branded 404 for non-localized requests.\n"
  },
  {
    "path": ".changeset/translations-patch-d3abeec7.md",
    "content": "---\n\"@bigcommerce/catalyst-core\": patch\n---\n\nUpdate translations.\n"
  },
  {
    "path": ".changeset/translations-patch-e3d3b994.md",
    "content": "---\n\"@bigcommerce/catalyst-core\": patch\n---\n\nUpdate translations.\n"
  },
  {
    "path": ".claude/skills/release-catalyst/SKILL.md",
    "content": "---\nname: release-catalyst\ndescription: >\n  Cut a new release of Catalyst (`@bigcommerce/catalyst-core` and `@bigcommerce/catalyst-makeswift`).\n  Use when the user says \"/release-catalyst\", \"cut a release\", \"release catalyst\", or asks to\n  publish new versions of the Catalyst packages. This skill orchestrates the full two-stage release\n  process: merging the Version Packages PR on canary, syncing integrations/makeswift, and pushing\n  @latest tags.\n---\n\n# Release Catalyst\n\nExecute stages in order. Pause for user input where indicated.\n\n## Stage 1: Cut release from `canary`\n\n### 1a. Find and merge the Version Packages PR\n\n```bash\ngh pr list --search \"Version Packages (canary)\" --state open --json number,title,reviews,mergeable\n```\n\n- If **no open PR** exists, inform the user that there are no pending changesets on `canary` and stop.\n- If the PR is **approved and checks are passing**, merge it: `gh pr merge <number> --squash`\n- If the PR is **not approved or checks are not passing**, tell the user and wait.\n  - Bot-opened PRs often don't trigger CI. If checks aren't running, push an empty commit to trigger them:\n    ```bash\n    git checkout --track origin/changeset-release/canary\n    git commit --allow-empty -m \"chore: trigger CI\"\n    git push origin changeset-release/canary\n    git checkout canary && git branch -D changeset-release/canary\n    ```\n  - **Stop here.** Wait for the user to confirm checks pass and the PR is approved before merging.\n\n### 1b. Verify the release\n\nAfter the PR merges:\n\n```bash\ngit fetch origin --tags\n```\n\nDetermine the new `@bigcommerce/catalyst-core` version from the PR body (look for `## @bigcommerce/catalyst-core@X.Y.Z`). Then verify:\n\n```bash\ngh release view @bigcommerce/catalyst-core@<version> --json tagName,name,isDraft,isPrerelease\n```\n\nIf the release and tag don't exist yet, wait briefly and retry — the Changesets action may still be running.\n\nRecord the **version number** and **bump type** (patch/minor/major) for use in Stage 2.\n\n## Stage 2: Sync and release `integrations/makeswift`\n\n### 2a. Sync branches\n\nInvoke the `/sync-makeswift` skill, with one addition: during the sync (after merge, before pushing), also add a changeset for `@bigcommerce/catalyst-makeswift`:\n\n**Determine bump type**: Match the bump type from Stage 1 (e.g., if core went `1.4.2` → `1.5.0`, that's a `minor`).\n\n**Create changeset file** (`.changeset/sync-canary-<version>.md`, where `<version>` uses hyphens instead of dots — e.g., `1.6.0` → `sync-canary-1-6-0.md`). Changeset filenames only allow lowercase letters and hyphens; dots are invalid.\n\n```markdown\n---\n\"@bigcommerce/catalyst-makeswift\": <patch|minor|major>\n---\n\nPulls in changes from the `@bigcommerce/catalyst-core@<version>` release. For more information about what was included in the `@bigcommerce/catalyst-core@<version>` release, see the [changelog entry](https://github.com/bigcommerce/catalyst/blob/<canary-sha>/core/CHANGELOG.md#<version-anchor>).\n```\n\nWhere:\n- `<canary-sha>` is the merge commit SHA on canary (from the Version Packages merge)\n- `<version-anchor>` is the version with dots removed (e.g., `1.5.0` → `150`)\n\nInclude this changeset in the merge commit (amend if needed) alongside the normal sync work.\n\n### 2b. Merge the Version Packages (`integrations/makeswift`) PR\n\nAfter the sync lands, the Changesets action will open a \"Version Packages (`integrations/makeswift`)\" PR.\n\n```bash\ngh pr list --search \"Version Packages (integrations/makeswift)\" --state open --json number,title\n```\n\nSame flow as Stage 1a:\n- If checks aren't running (bot PR), push an empty commit to trigger CI, then **drop it before merging** by resetting to the parent and force-pushing.\n- Once approved and green, merge with `gh pr merge <number> --squash`.\n  - Note: squash merging is normally disallowed on `integrations/makeswift` to preserve merge bases for sync PRs. The user may need to temporarily enable squash merging in the branch protection rules for this step, then re-disable it after.\n\n### 2c. Verify the makeswift release\n\n```bash\ngit fetch origin --tags\ngh release view @bigcommerce/catalyst-makeswift@<version> --json tagName,name,isDraft,isPrerelease\n```\n\n## Stage 3: Push `@latest` tags\n\nUpdate both `@latest` tags to point to the new releases:\n\n```bash\ngit fetch origin --tags\ngit tag @bigcommerce/catalyst-core@latest @bigcommerce/catalyst-core@<version> -f\ngit tag @bigcommerce/catalyst-makeswift@latest @bigcommerce/catalyst-makeswift@<version> -f\ngit push origin @bigcommerce/catalyst-core@latest -f\ngit push origin @bigcommerce/catalyst-makeswift@latest -f\n```\n\nConfirm both tags were pushed successfully.\n\n## Stage 4: Cleanup\n\n```bash\ngit checkout canary\ngit pull\n```\n\nDelete any leftover local branches (`changeset-release/*`, `sync-integrations-makeswift`, `integrations/makeswift`).\n\nReport the final state: both package versions released, tags updated, branches cleaned up.\n"
  },
  {
    "path": ".claude/skills/release-catalyst-patch/SKILL.md",
    "content": "---\nname: release-catalyst-patch\ndescription: >\n  Release a single Catalyst package patch in isolation, without bundling it with\n  other queued changesets in the open Version Packages PR. Use when the user says\n  \"/release-catalyst-patch\", \"isolate a patch release\", \"publish only one package\",\n  or wants to ship a single package's changeset ahead of the normal release cadence.\n  Performs the full flow locally (`changeset version`, build, `changeset publish`,\n  push, GitHub release) so the remaining queued changesets stay untouched in the\n  Version Packages PR.\n---\n\n# Release Catalyst Patch (single-package isolation)\n\nThe Changesets GitHub Action picks one mode per push to `canary`: if any unconsumed\nchangesets exist, it opens/refreshes the Version Packages PR and **does not** publish.\nThat's why we publish locally — we keep the other changesets in `.changeset/` so the\nVersion Packages PR still tracks them, but we ship just the one we want now.\n\nExecute stages in order. Pause for user input where indicated. **Never execute the\n`changeset publish` command yourself** — provide it to the user to run.\n\n## Stage 0: Confirm scope\n\nIdentify the changeset to release and the target package + version.\n\n```bash\nls .changeset/\ngit log --oneline -10                       # find the PR/commit that added the changeset\ncat .changeset/<changeset-file>.md          # confirm the package + bump type\nnpm view <package> version dist-tags        # confirm current published state\n```\n\nConfirm with the user:\n- Which `.changeset/*.md` file is being released\n- The target package and the resulting version (e.g., `1.0.2` → `1.0.3`)\n- That **all other** changesets in `.changeset/` should remain queued for the next regular release\n\nIf the package has notes in the [Package-specific notes](#package-specific-notes) section\nbelow, review them before continuing.\n\n## Stage 1: Branch + quarantine\n\nWork on a release branch so the workflow doesn't trigger mid-flow.\n\n```bash\ngit switch canary && git pull\ngit switch -c release/<package>-<new-version>\n```\n\nMove the **other** changesets **outside** the repo. A subdirectory inside `.changeset/`\ngets parsed as a malformed changeset by the CLI and crashes `changeset version`:\n\n```bash\nmkdir -p /tmp/changeset-hold\nmv .changeset/<other-1>.md .changeset/<other-2>.md ... /tmp/changeset-hold/\nls .changeset/   # should leave only config.json + the one changeset to release\n```\n\n## Stage 2: Run `changeset version`\n\nRequires `GITHUB_TOKEN` because the repo uses `@changesets/changelog-github`. The `gh`\nCLI token works.\n\n```bash\npnpm install --frozen-lockfile\nGITHUB_TOKEN=$(gh auth token) pnpm changeset version\n```\n\nRestore the held changesets immediately:\n\n```bash\nmv /tmp/changeset-hold/*.md .changeset/\nrmdir /tmp/changeset-hold\n```\n\nVerify the diff:\n\n```bash\ngit status\ngit diff packages/<package>/package.json packages/<package>/CHANGELOG.md\n```\n\nExpect: bumped `package.json`, new CHANGELOG entry, deleted only the one changeset.\nThe other changesets should be back in `.changeset/` and untouched.\n\n## Stage 3: Build\n\nRun the root build. Turborepo handles env passthrough from `.env.local` via the\npipeline config, so package builds get the variables they need without manual sourcing.\n\n```bash\npnpm build\n```\n\nIf a package has build-time secrets that must be present, see the\n[Package-specific notes](#package-specific-notes) section for verification steps.\n\n## Stage 4: Commit\n\n```bash\ngit add -A\ngit commit -m \"Version Packages (\\`canary\\`)\"\n```\n\nMatch the message format the changesets bot uses, since this is a manual stand-in for it.\n\n## Stage 5: Hand the publish command to the user\n\n**Do not run `changeset publish` from the agent.** Publishing to npm is a destructive,\nexternally-visible action that should be performed by the user.\n\nHave the user confirm they're logged in, then run the publish themselves. The CLI will\nprompt them interactively for the npm OTP.\n\n```bash\nnpm whoami                       # expect their npm username\npnpm exec changeset publish      # prompts for OTP interactively\n```\n\n`changeset publish` is per-package version-aware: it only publishes packages whose local\n`package.json` is ahead of npm. Since only one package was bumped, only it ships. It\nalso creates a local git tag like `@bigcommerce/<package>@<version>`.\n\nWait for the user to confirm the publish completed before continuing.\n\n## Stage 6: Verify npm state\n\n```bash\nnpm view <package> version\nnpm view <package> dist-tags\n```\n\n`latest` should point to the new version. If it doesn't (rare — only happens if\n`publishConfig.tag` is set), advise the user to fix with:\n\n```bash\nnpm dist-tag add <package>@<version> latest\n```\n\n## Stage 7: Fast-forward canary and push\n\nThis requires a direct push to the protected default branch. **Pause and ask for explicit\nuser authorization** before pushing — the user's \"never push directly to main/master/production\"\nrule guards against this even though the changesets bot does the same thing during normal\nreleases.\n\n```bash\ngit switch canary\ngit fetch origin canary\ngit log --oneline origin/canary..canary    # should be 1 ahead\ngit log --oneline canary..origin/canary    # should be 0 behind\ngit merge --ff-only release/<package>-<new-version>\n```\n\nIf you're 1 ahead and 0 behind, default `git push` rejects non-fast-forward updates,\nwhich gives the same safety as `--ff-only` on the push side. Apple's git build (≤2.50.x)\ndoes not accept `--ff-only` as a push flag — `git push origin canary` is correct here.\n\nIf a hook blocks the agent push, hand these commands to the user:\n\n```bash\ngit push origin canary\ngit push origin \"@bigcommerce/<package>@<new-version>\"\n```\n\n## Stage 8: Create the GitHub release manually\n\nCI runs `changeset publish` after the canary push, finds the version already on npm,\nand no-ops. Because the action only creates GitHub releases for packages it actually\npublishes, **no release is created automatically** in this flow. Make it manually.\n\nExtract the new CHANGELOG section (everything after the `## <version>` heading up to\nthe next `## ` heading) into a notes file. Then:\n\n```bash\ngh release create \"@bigcommerce/<package>@<new-version>\" \\\n  --repo bigcommerce/catalyst \\\n  --title \"@bigcommerce/<package>@<new-version>\" \\\n  --notes-file /tmp/release-notes.md\n```\n\nMatch the body format of prior releases — just the `### Patch Changes` /\n`### Minor Changes` heading and the bullets, no version heading at the top. Compare\nagainst an existing release:\n\n```bash\ngh release view \"@bigcommerce/catalyst-core@<some-prior-version>\" --repo bigcommerce/catalyst --json body\n```\n\n## Stage 9: Final validation\n\n```bash\ngit fetch origin canary\ngit log --oneline origin/canary -3                                              # version commit on canary\ngit ls-remote --tags origin \"@bigcommerce/<package>@<new-version>\"              # tag on origin\ngh release view \"@bigcommerce/<package>@<new-version>\" --repo bigcommerce/catalyst --json tagName,isDraft,isPrerelease\ngh run list --workflow=changesets-release.yml --limit 1                         # CI run succeeded\ngh pr list --search \"Version Packages (canary)\" --state open --json number,headRefName,updatedAt   # Version Packages PR refreshed\ngit fetch origin changeset-release/canary\ngit show --stat origin/changeset-release/canary | head -20                      # confirm only the other changesets remain\nnpm view <package> version dist-tags\n```\n\nReport:\n- Published version + dist-tag\n- Canary commit SHA\n- Tag pushed\n- GitHub release URL\n- Confirmation that the Version Packages PR now contains **only** the other changesets — the released one has been dropped from its scope.\n\n## Stage 10: Cleanup\n\n```bash\ngit branch -d release/<package>-<new-version>\n```\n\n## Package-specific notes\n\n### `@bigcommerce/create-catalyst`\n\nThe CLI build (`tsup` via the package's `tsup.config.ts`) inlines `CLI_SEGMENT_WRITE_KEY`\nat build time, falling back to the placeholder `'not-a-valid-segment-write-key'` if the\nenv var is missing. After Stage 3, verify the real key was embedded:\n\n```bash\ngrep -c \"not-a-valid-segment-write-key\" packages/create-catalyst/dist/index.js\n# expect: 0\n```\n\nIf `1`, the env var wasn't loaded. Confirm `CLI_SEGMENT_WRITE_KEY` exists in `.env.local`,\nand that the turbo pipeline for `build` declares it under `env` or `passThroughEnv` in\n`turbo.json`. Re-run `pnpm build` after fixing.\n"
  },
  {
    "path": ".claude/skills/sync-makeswift/SKILL.md",
    "content": "---\nname: sync-makeswift\ndescription: >\n  Sync the `integrations/makeswift` branch with `canary` in the Catalyst monorepo.\n  Use when the user says \"/sync-makeswift\", \"sync makeswift\", \"sync integrations/makeswift\",\n  or asks to bring `integrations/makeswift` up to date with `canary`.\n---\n\n# Sync `integrations/makeswift` with `canary`\n\nExecute the following phases in order. Pause for user input where indicated.\n\n## Phase 1: Prepare and merge\n\n```bash\ngit fetch origin\ngit checkout -B sync-integrations-makeswift origin/integrations/makeswift\ngit merge origin/canary\n```\n\nIf the merge completes cleanly, skip to changeset cleanup. Otherwise, resolve conflicts.\n\n### Conflict resolution rules\n\n- `core/package.json`: the `name` field MUST stay `@bigcommerce/catalyst-makeswift`. The `version` field MUST stay at the latest published `@bigcommerce/catalyst-makeswift` version (check what's on `origin/integrations/makeswift`, not `canary`).\n- `core/CHANGELOG.md`: the latest release entry MUST match the latest published `@bigcommerce/catalyst-makeswift` version.\n- `pnpm-lock.yaml`: accept canary's version (`git checkout --theirs pnpm-lock.yaml`), then regenerate with `pnpm install --no-frozen-lockfile`.\n- For all other conflicts, prefer canary's structure/patterns while preserving makeswift-specific additions (imports, components, config).\n\nAfter resolving all conflicts, stage everything and verify no unresolved conflicts remain:\n\n```bash\ngit add <resolved files>\ngit diff --name-only --diff-filter=U  # should return empty\n```\n\n### Changeset cleanup\n\nRemove any `.changeset/*.md` files that do NOT target `@bigcommerce/catalyst-makeswift`. Read each changeset file and delete any that reference `@bigcommerce/catalyst-core` or other packages. Amend the removals into the merge commit.\n\n### Commit the merge\n\n```bash\ngit commit --no-edit\n```\n\nIf changesets were removed after the initial commit, amend them in (`git commit --amend --no-edit`) rather than creating a separate commit.\n\n## Phase 2: Push and open PR\n\n```bash\ngit push origin sync-integrations-makeswift\n```\n\nOpen a PR into `integrations/makeswift` (not `canary`):\n\n- Title: `sync \\`integrations/makeswift\\` with \\`canary\\``\n- Body: summarize what came from canary, list conflict resolutions, and include this notice:\n\n> **Do not squash or rebase-and-merge this PR.** Use a true merge commit or rebase locally to preserve the merge base between `canary` and `integrations/makeswift`.\n\n**Stop here.** Tell the user the PR is ready for review and wait for them to confirm approval before continuing.\n\n## Phase 3: Rebase and push (after PR approval)\n\n```bash\ngit fetch origin\ngit checkout -B integrations/makeswift origin/integrations/makeswift\ngit rebase sync-integrations-makeswift\ngit push origin integrations/makeswift --force-with-lease\n```\n\nThis closes the PR automatically. Confirm with the user that the push succeeded and the PR closed.\n\n## Phase 4: Cleanup\n\nSwitch back to `canary` and delete the local branches that are no longer needed:\n\n```bash\ngit checkout canary\ngit pull\ngit branch -D sync-integrations-makeswift integrations/makeswift\n```\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "* @bigcommerce/team-trac\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Have questions?\n    url: https://github.com/bigcommerce/catalyst/blob/canary/README.md\n    about: Explore the Catalyst Docs.\n  - name: Need help with Catalyst?\n    url: https://github.com/bigcommerce/catalyst/discussions/new?category=q-a\n    about: If you can't get something to work the way you expect, join us in Discussions to browse existing topics or to share a new post.\n  - name: Feature Request\n    url: https://github.com/bigcommerce/catalyst/discussions/categories/feature-requests\n    about: Join us in Discussions to share your idea on improving Catalyst. Thanks for your contribution!\n  - name: Official BigCommerce Support\n    url: https://support.bigcommerce.com/contact\n    about: To report a platform bug, outage or greater platform issue, contact our Technical Support team. If you're a partner, please do so via your Partner Portal.\n  - name: 💙 Join the BigCommerceDevs Community\n    url: https://developer.bigcommerce.com/community\n    about: Connect with other devs building on BigCommerce!\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/🐞📝-bug-report-makeswift.md",
    "content": "---\nname: \"\\U0001F41E\\U0001F4DD Makeswift Bug report\"\nabout: You're running into a reproducible error while developing with Catalyst and Makeswift.\ntitle: '[x] is not working when I [y]'\nlabels: ''\nassignees: ''\n---\n\nWe really appreciate the help making Catalyst and Makeswift better. Every issue helps!\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\nPlease link to a repo that can be used to reproduce this issue, if possible. It'll help fix the bug faster.\n\n**Previously working?**\nWas this functionality previously working? If so, please link to a commit or PR that caused it to stop working.\n\n**Any Errors?**\nWere there any errors that surfaced when merging the above PR?\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/🐞📝-bug-report.md",
    "content": "---\nname: \"\\U0001F41E\\U0001F4DD Bug report\"\nabout: You're running into a reproducible error while developing with Catalyst.\ntitle: \"[x] is not working when I [y]\"\nlabels: ''\nassignees: ''\n\n---\n\nWe really appreciate the help making Catalyst better. Every issue helps!\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\nPlease link to a repo that can be used to reproduce this issue, if possible. It'll help fix the bug faster.\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: 'npm'\n    directory: '/'\n    schedule:\n      interval: 'weekly'\n      day: 'monday'\n    open-pull-requests-limit: 1\n    groups:\n      npm-dependencies:\n        patterns:\n          - \"*\"\n    ignore:\n      - dependency-name: '@types/node'\n        update-types: ['version-update:semver-major']\n      - dependency-name: 'eslint'\n        update-types: ['version-update:semver-major']\n      # Disabling tailwind due to browser compatibility constraints.\n      - dependency-name: 'tailwindcss'\n        update-types: ['version-update:semver-major']\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## What/Why?\n<!--- \n  A description about what this pull request implements and its purpose.\n  Try to be detailed and describe any technical details to simplify the job\n  of the reviewer and the individual on production support.\n--->\n\n## Testing\n<!---\n  Provide as much information as you can about how you tested and\n  how another developer can test.\n--->\n\n## Migration\n<!---\n  If you have moved any files around, or made any breaking changes,\n  please provide a migration guide for the developers to make rebases easier.\n--->\n"
  },
  {
    "path": ".github/scripts/__tests__/audit-unlighthouse.test.mts",
    "content": "import { describe, it } from 'node:test';\nimport assert from 'node:assert/strict';\n\nimport { buildReport } from '../audit-unlighthouse.mts';\nimport type { CiResult } from '../audit-unlighthouse.mts';\n\n// ---------------------------------------------------------------------------\n// Fixtures\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_METRICS: CiResult['summary']['metrics'] = {\n  'largest-contentful-paint': { displayValue: '2.5 s' },\n  'cumulative-layout-shift': { displayValue: '0.01' },\n  'first-contentful-paint': { displayValue: '1.2 s' },\n  'total-blocking-time': { displayValue: '100 ms' },\n  'max-potential-fid': { displayValue: '200 ms' },\n  interactive: { displayValue: '3.5 s' },\n};\n\nfunction makeCiResult(overrides: {\n  score?: number;\n  performance?: number;\n  accessibility?: number;\n  'best-practices'?: number;\n  seo?: number;\n  metrics?: CiResult['summary']['metrics'];\n} = {}): CiResult {\n  return {\n    summary: {\n      score: overrides.score ?? 0.85,\n      categories: {\n        performance: { score: overrides.performance ?? 0.80 },\n        accessibility: { score: overrides.accessibility ?? 0.92 },\n        'best-practices': { score: overrides['best-practices'] ?? 1.0 },\n        seo: { score: overrides.seo ?? 0.90 },\n      },\n      metrics: overrides.metrics ?? { ...DEFAULT_METRICS },\n    },\n  };\n}\n\nconst BASE = makeCiResult();\n\n// ---------------------------------------------------------------------------\n// Report heading\n// ---------------------------------------------------------------------------\n\ndescribe('report heading', () => {\n  it('contains the audit heading', () => {\n    const markdown = buildReport(BASE, BASE);\n\n    assert.ok(markdown.includes('## Unlighthouse Audit'), 'Missing main heading');\n  });\n\n  it('appends branch label when branch is given', () => {\n    const markdown = buildReport(BASE, BASE, 'canary');\n\n    assert.ok(\n      markdown.includes('## Unlighthouse Audit — `canary`'),\n      'Missing branch label in heading',\n    );\n  });\n\n  it('handles branch names with slashes', () => {\n    const markdown = buildReport(BASE, BASE, 'integrations/makeswift');\n\n    assert.ok(markdown.includes('`integrations/makeswift`'));\n  });\n\n  it('omits branch label when none provided', () => {\n    const markdown = buildReport(BASE, BASE);\n\n    assert.ok(!markdown.includes(' — '), 'Should not contain a branch label separator');\n  });\n\n  it('contains the description text', () => {\n    const markdown = buildReport(BASE, BASE);\n\n    assert.ok(markdown.includes('Unlighthouse scores for the latest commit on this branch.'));\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Summary Score section\n// ---------------------------------------------------------------------------\n\ndescribe('Summary Score section', () => {\n  it('contains the Summary Score heading', () => {\n    const markdown = buildReport(BASE, BASE);\n\n    assert.ok(markdown.includes('### Summary Score'));\n  });\n\n  it('contains the aggregate score note', () => {\n    const markdown = buildReport(BASE, BASE);\n\n    assert.ok(markdown.includes('Aggregate score across all categories as reported by Unlighthouse.'));\n  });\n\n  it('renders scores as integers on a 1-100 scale', () => {\n    const desktop = makeCiResult({ score: 0.85 });\n    const mobile = makeCiResult({ score: 0.72 });\n    const markdown = buildReport(desktop, mobile);\n\n    assert.ok(markdown.includes('| Score | 85 | 72 |'));\n  });\n\n  it('rounds fractional scores correctly', () => {\n    const desktop = makeCiResult({ score: 0.856 }); // rounds to 86\n    const markdown = buildReport(desktop, BASE);\n\n    assert.ok(markdown.includes('86'), 'Score 0.856 should round to 86');\n  });\n\n  it('contains the two-column header', () => {\n    const markdown = buildReport(BASE, BASE);\n\n    assert.ok(markdown.includes('| | Desktop | Mobile |'));\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Category Scores section\n// ---------------------------------------------------------------------------\n\ndescribe('Category Scores section', () => {\n  it('contains the Category Scores heading', () => {\n    const markdown = buildReport(BASE, BASE);\n\n    assert.ok(markdown.includes('### Category Scores'));\n  });\n\n  it('renders all four categories', () => {\n    const markdown = buildReport(BASE, BASE);\n\n    assert.ok(markdown.includes('Performance'));\n    assert.ok(markdown.includes('Accessibility'));\n    assert.ok(markdown.includes('Best Practices'));\n    assert.ok(markdown.includes('SEO'));\n  });\n\n  it('renders desktop and mobile scores independently', () => {\n    const desktop = makeCiResult({ performance: 0.80 });\n    const mobile = makeCiResult({ performance: 0.93 });\n    const markdown = buildReport(desktop, mobile);\n\n    assert.ok(\n      markdown.includes('| Performance | 80 | 93 |'),\n      'Performance row should show desktop then mobile score',\n    );\n  });\n\n  it('shows all four categories independently', () => {\n    const desktop = makeCiResult({ seo: 0.88, accessibility: 0.75 });\n    const mobile = makeCiResult({ seo: 0.91, accessibility: 0.82 });\n    const markdown = buildReport(desktop, mobile);\n\n    assert.ok(markdown.includes('| SEO | 88 | 91 |'));\n    assert.ok(markdown.includes('| Accessibility | 75 | 82 |'));\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Core Web Vitals section\n// ---------------------------------------------------------------------------\n\ndescribe('Core Web Vitals section', () => {\n  it('contains the Core Web Vitals heading', () => {\n    const markdown = buildReport(BASE, BASE);\n\n    assert.ok(markdown.includes('### Core Web Vitals'));\n  });\n\n  it('renders all six metrics', () => {\n    const markdown = buildReport(BASE, BASE);\n\n    assert.ok(markdown.includes('LCP'));\n    assert.ok(markdown.includes('CLS'));\n    assert.ok(markdown.includes('FCP'));\n    assert.ok(markdown.includes('TBT'));\n    assert.ok(markdown.includes('Max Potential FID'));\n    assert.ok(markdown.includes('Time to Interactive'));\n  });\n\n  it('passes displayValue through unchanged', () => {\n    const desktop = makeCiResult({\n      metrics: { ...DEFAULT_METRICS, 'largest-contentful-paint': { displayValue: '4.8 s' } },\n    });\n    const markdown = buildReport(desktop, BASE);\n\n    assert.ok(markdown.includes('4.8 s'));\n  });\n\n  it('shows — for a metric missing from a result', () => {\n    const desktopMissingMetric = makeCiResult({ metrics: {} });\n    const markdown = buildReport(desktopMissingMetric, BASE);\n\n    assert.ok(markdown.includes('—'), 'Missing metric should show —');\n  });\n\n  it('shows desktop and mobile displayValues per metric row', () => {\n    const desktop = makeCiResult({ metrics: { ...DEFAULT_METRICS, 'total-blocking-time': { displayValue: '80 ms' } } });\n    const mobile = makeCiResult({ metrics: { ...DEFAULT_METRICS, 'total-blocking-time': { displayValue: '320 ms' } } });\n    const markdown = buildReport(desktop, mobile);\n\n    assert.ok(markdown.includes('| TBT | 80 ms | 320 ms |'));\n  });\n});\n"
  },
  {
    "path": ".github/scripts/__tests__/bundle-size.test.mts",
    "content": "import { describe, it, after, beforeEach } from 'node:test';\nimport assert from 'node:assert/strict';\nimport { writeFileSync, mkdirSync, rmSync, unlinkSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { tmpdir } from 'node:os';\n\nimport {\n  round1,\n  getGzipSize,\n  parseManifestEntries,\n  computeRootLayout,\n  computeRouteMetrics,\n  compareReport,\n  clearSizeCache,\n  readTurbopackEntries,\n} from '../bundle-size.mts';\n\n// ---------------------------------------------------------------------------\n// Shared temp directory with fixture chunk files.\n// Initialized at module load time so testDir is set before any test runs.\n// Files are large enough (varied content) to produce measurable gzip sizes.\n// ---------------------------------------------------------------------------\n\nconst testDir = join(tmpdir(), `bundle-size-test-${Date.now()}`);\n\nmkdirSync(testDir, { recursive: true });\n\n// Each file gets unique, varied content so gzip produces a non-trivial size.\nconst makeJs = (prefix: string) =>\n  Array.from(\n    { length: 30 },\n    (_, i) => `export const ${prefix}_${i} = ${JSON.stringify(`${prefix}_v${i}_pad${i * 37 + 13}`)};`,\n  ).join('\\n') + '\\n';\n\nconst makeCss = (prefix: string) =>\n  Array.from(\n    { length: 20 },\n    (_, i) => `.${prefix}-class-${i} { color: hsl(${i * 17}, 50%, 50%); margin: ${i}px; }`,\n  ).join('\\n') + '\\n';\n\nwriteFileSync(join(testDir, 'route.js'), makeJs('route'));\nwriteFileSync(join(testDir, 'shared.js'), makeJs('shared'));\nwriteFileSync(join(testDir, 'root-layout.js'), makeJs('root_layout'));\nwriteFileSync(join(testDir, 'product-layout.js'), makeJs('product_layout'));\nwriteFileSync(join(testDir, 'route.css'), makeCss('route'));\n\nafter(() => {\n  rmSync(testDir, { recursive: true, force: true });\n});\n\n// ---------------------------------------------------------------------------\n// round1\n// ---------------------------------------------------------------------------\n\ndescribe('round1', () => {\n  it('rounds up at .05', () => {\n    assert.equal(round1(1.25), 1.3);\n  });\n\n  it('rounds down below .05', () => {\n    assert.equal(round1(1.24), 1.2);\n  });\n\n  it('returns 0 unchanged', () => {\n    assert.equal(round1(0), 0);\n  });\n\n  it('handles negative values', () => {\n    assert.equal(round1(-1.25), -1.2);\n  });\n});\n\n// ---------------------------------------------------------------------------\n// parseManifestEntries\n// ---------------------------------------------------------------------------\n\ndescribe('parseManifestEntries', () => {\n  it('routes /layout entries to layouts', () => {\n    const { layouts, pages } = parseManifestEntries({\n      '/app/layout': ['a.js'],\n      '/app/about/page': ['b.js'],\n    });\n\n    assert.deepEqual(Object.keys(layouts), ['/app/layout']);\n    assert.deepEqual(Object.keys(pages), ['/app/about/page']);\n  });\n\n  it('routes /page entries to pages', () => {\n    const { pages } = parseManifestEntries({ '/app/contact/page': ['c.js'] });\n\n    assert.deepEqual(Object.keys(pages), ['/app/contact/page']);\n  });\n\n  it('ignores entries ending in neither /layout nor /page', () => {\n    const { layouts, pages } = parseManifestEntries({\n      '/app/route': ['d.js'],\n      '/api/handler': [],\n      '/app/loading': ['e.js'],\n    });\n\n    assert.deepEqual(Object.keys(layouts), []);\n    assert.deepEqual(Object.keys(pages), []);\n  });\n\n  it('returns empty objects for empty input', () => {\n    const { layouts, pages } = parseManifestEntries({});\n\n    assert.deepEqual(layouts, {});\n    assert.deepEqual(pages, {});\n  });\n\n  it('handles multiple layouts and pages together', () => {\n    const { layouts, pages } = parseManifestEntries({\n      '/app/layout': ['a.js'],\n      '/app/products/layout': ['b.js'],\n      '/app/page': ['c.js'],\n      '/app/products/page': ['d.js'],\n    });\n\n    assert.deepEqual(Object.keys(layouts).sort(), ['/app/layout', '/app/products/layout']);\n    assert.deepEqual(Object.keys(pages).sort(), ['/app/page', '/app/products/page']);\n  });\n});\n\n// ---------------------------------------------------------------------------\n// computeRootLayout\n// ---------------------------------------------------------------------------\n\ndescribe('computeRootLayout', () => {\n  beforeEach(() => clearSizeCache());\n\n  it('selects shortest path as root when multiple layouts exist', () => {\n    const layouts = {\n      '/[locale]/products/layout': [],\n      '/[locale]/layout': [],\n      '/[locale]/about/deep/layout': [],\n    };\n    const { rootLayoutPath } = computeRootLayout(\n      Object.keys(layouts),\n      layouts,\n      new Set(),\n      testDir,\n    );\n\n    assert.equal(rootLayoutPath, '/[locale]/layout');\n  });\n\n  it('returns null rootLayoutPath when layoutPaths is empty', () => {\n    const { rootLayoutPath, rootLayoutChunks, rootLayoutJs, rootLayoutCss } = computeRootLayout(\n      [],\n      {},\n      new Set(),\n      testDir,\n    );\n\n    assert.equal(rootLayoutPath, null);\n    assert.equal(rootLayoutChunks.size, 0);\n    assert.equal(rootLayoutJs, 0);\n    assert.equal(rootLayoutCss, 0);\n  });\n\n  it('excludes sharedChunks from rootLayoutChunks', () => {\n    const layouts = { '/layout': ['shared.js', 'root-layout.js'] };\n    const sharedChunks = new Set(['shared.js']);\n    const { rootLayoutChunks } = computeRootLayout(\n      ['/layout'],\n      layouts,\n      sharedChunks,\n      testDir,\n    );\n\n    assert.ok(!rootLayoutChunks.has('shared.js'), 'shared.js should be excluded');\n    assert.ok(rootLayoutChunks.has('root-layout.js'), 'root-layout.js should be included');\n  });\n\n  it('rootLayoutChunks contains all non-shared layout chunks', () => {\n    const layouts = { '/layout': ['root-layout.js', 'route.js'] };\n    const { rootLayoutChunks } = computeRootLayout(\n      ['/layout'],\n      layouts,\n      new Set(),\n      testDir,\n    );\n\n    assert.ok(rootLayoutChunks.has('root-layout.js'));\n    assert.ok(rootLayoutChunks.has('route.js'));\n    assert.equal(rootLayoutChunks.size, 2);\n  });\n\n  it('computes non-zero sizes when real files exist', () => {\n    const layouts = { '/layout': ['root-layout.js'] };\n    const { rootLayoutJs } = computeRootLayout(\n      ['/layout'],\n      layouts,\n      new Set(),\n      testDir,\n    );\n\n    assert.ok(rootLayoutJs > 0, `Expected rootLayoutJs > 0, got ${rootLayoutJs}`);\n  });\n});\n\n// ---------------------------------------------------------------------------\n// computeRouteMetrics\n// ---------------------------------------------------------------------------\n\ndescribe('computeRouteMetrics', () => {\n  beforeEach(() => clearSizeCache());\n\n  it('firstLoadJs equals firstLoadJs arg when all chunks are non-existent', () => {\n    const pages = { '/app/page': [] };\n    const routes = computeRouteMetrics(\n      pages,\n      {},\n      new Set(),\n      null,\n      new Set(),\n      100,\n      testDir,\n    );\n\n    assert.equal(routes['/app/page'].firstLoadJs, 100);\n  });\n\n  it('firstLoadJs is greater than firstLoadJs arg when real chunk files exist', () => {\n    const pages = { '/app/page': ['route.js', 'route.css'] };\n    const routes = computeRouteMetrics(\n      pages,\n      {},\n      new Set(),\n      null,\n      new Set(),\n      0,\n      testDir,\n    );\n\n    const { js, css, firstLoadJs } = routes['/app/page'];\n\n    assert.ok(js > 0, `js should be > 0 (real file exists), got ${js}`);\n    assert.ok(css > 0, `css should be > 0 (real file exists), got ${css}`);\n    assert.ok(firstLoadJs > 0, `firstLoadJs should be > 0, got ${firstLoadJs}`);\n  });\n\n  it('excludes sharedChunks from route chunk set', () => {\n    const pages = { '/app/page': ['shared.js', 'route.js'] };\n\n    // With both chunks in sharedChunks, routeChunks is empty -> js = 0\n    const routesAllExcluded = computeRouteMetrics(\n      pages,\n      {},\n      new Set(['shared.js', 'route.js']),\n      null,\n      new Set(),\n      0,\n      testDir,\n    );\n\n    assert.equal(routesAllExcluded['/app/page'].js, 0, 'All shared chunks excluded -> js = 0');\n\n    clearSizeCache();\n\n    // With no exclusions, real files contribute -> js > 0\n    const routesNoneExcluded = computeRouteMetrics(\n      pages,\n      {},\n      new Set(),\n      null,\n      new Set(),\n      0,\n      testDir,\n    );\n\n    assert.ok(routesNoneExcluded['/app/page'].js > 0, 'No exclusions -> js > 0');\n  });\n\n  it('excludes rootLayoutChunks from route chunk set', () => {\n    const pages = { '/app/page': ['root-layout.js', 'route.js'] };\n\n    // With both chunks in rootLayoutChunks, routeChunks is empty -> js = 0\n    const routesAllExcluded = computeRouteMetrics(\n      pages,\n      {},\n      new Set(),\n      null,\n      new Set(['root-layout.js', 'route.js']),\n      0,\n      testDir,\n    );\n\n    assert.equal(routesAllExcluded['/app/page'].js, 0, 'All rootLayout chunks excluded -> js = 0');\n\n    clearSizeCache();\n\n    // With no rootLayoutChunks excluded, real files contribute -> js > 0\n    const routesNoneExcluded = computeRouteMetrics(\n      pages,\n      {},\n      new Set(),\n      null,\n      new Set(),\n      0,\n      testDir,\n    );\n\n    assert.ok(routesNoneExcluded['/app/page'].js > 0, 'No exclusions -> js > 0');\n  });\n\n  it('includes non-root ancestor layout chunks in route size', () => {\n    // Page has no own chunks; non-root ancestor layout contributes product-layout.js\n    const pages = { '/[locale]/products/page': [] };\n    const layouts = {\n      '/[locale]/layout': ['root-layout.js'],\n      '/[locale]/products/layout': ['product-layout.js'],\n    };\n    const rootLayoutChunks = new Set(['root-layout.js']);\n\n    const routes = computeRouteMetrics(\n      pages,\n      layouts,\n      new Set(),\n      '/[locale]/layout',\n      rootLayoutChunks,\n      0,\n      testDir,\n    );\n\n    assert.ok(\n      routes['/[locale]/products/page'].js > 0,\n      'Non-root ancestor layout chunk should contribute to route js',\n    );\n  });\n\n  it('does not include root ancestor layout chunks in route size', () => {\n    // Page has no own chunks; root layout has root-layout.js (should be excluded)\n    const pages = { '/[locale]/page': [] };\n    const layouts = {\n      '/[locale]/layout': ['root-layout.js'],\n    };\n    const rootLayoutChunks = new Set(['root-layout.js']);\n\n    const routes = computeRouteMetrics(\n      pages,\n      layouts,\n      new Set(),\n      '/[locale]/layout',\n      rootLayoutChunks,\n      0,\n      testDir,\n    );\n\n    assert.equal(\n      routes['/[locale]/page'].js,\n      0,\n      'Root ancestor layout chunks should NOT contribute to route js',\n    );\n  });\n\n  it('applies round1 to all output values', () => {\n    const pages = { '/app/page': [] };\n    const routes = computeRouteMetrics(\n      pages,\n      {},\n      new Set(),\n      null,\n      new Set(),\n      1.25,\n      testDir,\n    );\n\n    // firstLoadJs = round1(1.25 + 0 + 0) = 1.3\n    assert.equal(routes['/app/page'].firstLoadJs, 1.3);\n    assert.equal(routes['/app/page'].js, 0);\n    assert.equal(routes['/app/page'].css, 0);\n  });\n});\n\n// ---------------------------------------------------------------------------\n// compareReport\n// The warning sign in the report output is U+26A0 U+FE0F (warning emoji).\n// Warning table rows end with \"| warning-emoji |\" while the footer contains\n// the same emoji in a sentence. Use \"warning-emoji |\" to match only table cells.\n// ---------------------------------------------------------------------------\n\nconst WARN_EMOJI = '\\u26a0\\ufe0f'; // ⚠️\nconst WARN_IN_ROW = `${WARN_EMOJI} |`; // appears only in warning table cells\n\ndescribe('compareReport', () => {\n  function makeReport(overrides = {}) {\n    return {\n      commitSha: 'abc123',\n      updatedAt: '2024-01-01',\n      firstLoadJs: 100,\n      totalJs: 200,\n      totalCss: 10,\n      routes: {},\n      ...overrides,\n    };\n  }\n\n  it('shows \"No bundle size changes detected.\" when nothing changed', () => {\n    const baseline = makeReport();\n    const current = makeReport();\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes('No bundle size changes detected.'));\n    assert.ok(!report.includes('_No route changes detected._'));\n    assert.ok(!report.includes('### Per-Route First Load JS'));\n  });\n\n  it('shows \"No route changes detected.\" when only global metrics changed', () => {\n    // Global metric differs (Case 2) but routes are identical → section shown, no threshold\n    const baseline = makeReport({ firstLoadJs: 100, routes: { '/app/page': { firstLoadJs: 100, js: 50, css: 5 } } });\n    const current = makeReport({ firstLoadJs: 110, routes: { '/app/page': { firstLoadJs: 100, js: 50, css: 5 } } });\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes('_No route changes detected._'));\n    assert.ok(!report.includes(`Threshold:`));\n  });\n\n  it('does not show global metrics table when global metrics are unchanged', () => {\n    const baseline = makeReport();\n    const current = makeReport();\n    const report = compareReport(baseline, current);\n\n    assert.ok(!report.includes('| Metric |'));\n  });\n\n  it('shows global metrics table only when metrics changed', () => {\n    const baseline = makeReport({ firstLoadJs: 100 });\n    const current = makeReport({ firstLoadJs: 115 });\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes('| Metric |'));\n    assert.ok(report.includes('First Load JS'));\n  });\n\n  it('shows only the changed global metrics', () => {\n    const baseline = makeReport({ firstLoadJs: 100, totalJs: 200, totalCss: 10 });\n    const current = makeReport({ firstLoadJs: 100, totalJs: 210, totalCss: 10 });\n    const report = compareReport(baseline, current);\n\n    // Use pipe-delimited patterns to match table rows only (not the section header)\n    assert.ok(report.includes('| Total JS |'));\n    assert.ok(!report.includes('| First Load JS |'));\n    assert.ok(!report.includes('| Total CSS |'));\n  });\n\n  it('shows NEW row for added route', () => {\n    const baseline = makeReport({ routes: {} });\n    const current = makeReport({\n      routes: { '/app/new/page': { firstLoadJs: 120, js: 60, css: 5 } },\n    });\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes('NEW'));\n    assert.ok(report.includes('120 kB'));\n  });\n\n  it('shows REMOVED row for deleted route', () => {\n    const baseline = makeReport({\n      routes: { '/app/old/page': { firstLoadJs: 120, js: 60, css: 5 } },\n    });\n    const current = makeReport({ routes: {} });\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes('REMOVED'));\n    assert.ok(report.includes('120 kB'));\n  });\n\n  it('does not show warning for increase under threshold', () => {\n    // delta=3kB, pct=3% < 5% threshold: no warning row\n    const baseline = makeReport({ routes: { '/app/page': { firstLoadJs: 100, js: 50, css: 5 } } });\n    const current = makeReport({ routes: { '/app/page': { firstLoadJs: 103, js: 53, css: 5 } } });\n    const report = compareReport(baseline, current);\n\n    assert.ok(!report.includes(WARN_IN_ROW), 'Should not have a warning table cell');\n  });\n\n  it('shows warning for increase over threshold (over 1kB AND over threshold percent)', () => {\n    // delta=10kB, pct=10% > 5% threshold: warning row present\n    const baseline = makeReport({ routes: { '/app/page': { firstLoadJs: 100, js: 50, css: 5 } } });\n    const current = makeReport({ routes: { '/app/page': { firstLoadJs: 110, js: 60, css: 5 } } });\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes(WARN_IN_ROW), 'Should have a warning table cell');\n  });\n\n  it('does not warn when delta is over threshold percent but 1kB or less', () => {\n    // delta=0.5kB = 50% but <=1kB: no warning\n    const baseline = makeReport({ routes: { '/app/page': { firstLoadJs: 1, js: 1, css: 0 } } });\n    const current = makeReport({ routes: { '/app/page': { firstLoadJs: 1.5, js: 1.5, css: 0 } } });\n    const report = compareReport(baseline, current);\n\n    assert.ok(!report.includes(WARN_IN_ROW));\n  });\n\n  it('does not warn when delta is over 1kB but at or under threshold percent', () => {\n    // delta=2kB = 1% < 5% threshold: no warning\n    const baseline = makeReport({ routes: { '/app/page': { firstLoadJs: 200, js: 200, css: 0 } } });\n    const current = makeReport({ routes: { '/app/page': { firstLoadJs: 202, js: 202, css: 0 } } });\n    const report = compareReport(baseline, current);\n\n    assert.ok(!report.includes(WARN_IN_ROW));\n  });\n\n  it('respects custom threshold: no warning when under', () => {\n    // delta=8kB = 8%, threshold=10: no warning\n    const baseline = makeReport({ routes: { '/app/page': { firstLoadJs: 100, js: 50, css: 5 } } });\n    const current = makeReport({ routes: { '/app/page': { firstLoadJs: 108, js: 58, css: 5 } } });\n    const report = compareReport(baseline, current, { threshold: 10 });\n\n    assert.ok(!report.includes(WARN_IN_ROW));\n  });\n\n  it('respects custom threshold: warning when over', () => {\n    // delta=8kB = 8%, threshold=3: warning\n    const baseline = makeReport({ routes: { '/app/page': { firstLoadJs: 100, js: 50, css: 5 } } });\n    const current = makeReport({ routes: { '/app/page': { firstLoadJs: 108, js: 58, css: 5 } } });\n    const report = compareReport(baseline, current, { threshold: 3 });\n\n    assert.ok(report.includes(WARN_IN_ROW));\n  });\n\n  it('uses default threshold of 5 percent when not specified', () => {\n    // delta=6kB = 6% > 5%: warning with default\n    const baseline = makeReport({ routes: { '/app/page': { firstLoadJs: 100, js: 100, css: 0 } } });\n    const current = makeReport({ routes: { '/app/page': { firstLoadJs: 106, js: 106, css: 0 } } });\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes(WARN_IN_ROW));\n    assert.ok(report.includes('Threshold: 5%'));\n  });\n\n  it('shows threshold in footer only when route changes are present', () => {\n    // Route changed: threshold callout shown\n    const baseline = makeReport({ routes: { '/app/page': { firstLoadJs: 100, js: 50, css: 5 } } });\n    const current = makeReport({ routes: { '/app/page': { firstLoadJs: 110, js: 60, css: 5 } } });\n    const report = compareReport(baseline, current, { threshold: 7 });\n\n    assert.ok(report.includes('Threshold: 7%'));\n  });\n\n  it('omits threshold footer when there are no route changes', () => {\n    // Global metrics differ but routes are identical — no threshold callout\n    const baseline = makeReport({ firstLoadJs: 100 });\n    const current = makeReport({ firstLoadJs: 115 });\n    const report = compareReport(baseline, current);\n\n    assert.ok(!report.includes('Threshold:'));\n  });\n\n  it('formats positive delta with + sign and percent', () => {\n    const baseline = makeReport({ routes: { '/app/page': { firstLoadJs: 100, js: 50, css: 5 } } });\n    const current = makeReport({ routes: { '/app/page': { firstLoadJs: 110, js: 60, css: 5 } } });\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes('+10 kB'));\n    assert.ok(report.includes('+10%'));\n  });\n\n  it('formats negative delta with minus sign and percent', () => {\n    const baseline = makeReport({ routes: { '/app/page': { firstLoadJs: 100, js: 50, css: 5 } } });\n    const current = makeReport({ routes: { '/app/page': { firstLoadJs: 90, js: 40, css: 5 } } });\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes('-10 kB'));\n    assert.ok(report.includes('-10%'));\n  });\n\n  it('sorts routes alphabetically', () => {\n    const makeRoute = (v: number) => ({ firstLoadJs: v, js: v, css: 0 });\n    const baseline = makeReport({\n      routes: {\n        '/z/page': makeRoute(100),\n        '/a/page': makeRoute(100),\n        '/m/page': makeRoute(100),\n      },\n    });\n    const current = makeReport({\n      routes: {\n        '/z/page': makeRoute(110),\n        '/a/page': makeRoute(110),\n        '/m/page': makeRoute(110),\n      },\n    });\n    const report = compareReport(baseline, current);\n\n    const aIdx = report.indexOf('/a/page');\n    const mIdx = report.indexOf('/m/page');\n    const zIdx = report.indexOf('/z/page');\n\n    assert.ok(aIdx < mIdx, '/a should appear before /m');\n    assert.ok(mIdx < zIdx, '/m should appear before /z');\n  });\n\n  it('strips the /[locale] prefix from display names', () => {\n    const baseline = makeReport({ routes: {} });\n    const current = makeReport({\n      routes: { '/[locale]/products/page': { firstLoadJs: 120, js: 60, css: 5 } },\n    });\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes('/products/page'), 'Should show /products/page (locale stripped)');\n    assert.ok(\n      !report.includes('/[locale]/products/page'),\n      'Should not show /[locale] prefix',\n    );\n  });\n\n  it('omits near-zero deltas that round to 0.0', () => {\n    // 0.04kB delta rounds to 0.0: treated as no change\n    const baseline = makeReport({\n      routes: { '/app/page': { firstLoadJs: 100.04, js: 50, css: 5 } },\n    });\n    const current = makeReport({\n      routes: { '/app/page': { firstLoadJs: 100.04, js: 50, css: 5 } },\n    });\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes('No bundle size changes detected.'));\n  });\n\n  it('shows Per-Route First Load JS section when there are route changes', () => {\n    const baseline = makeReport({ routes: { '/app/page': { firstLoadJs: 100, js: 50, css: 0 } } });\n    const current = makeReport({ routes: { '/app/page': { firstLoadJs: 110, js: 60, css: 0 } } });\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes('### Per-Route First Load JS'));\n  });\n\n  it('omits Per-Route First Load JS section when nothing changed', () => {\n    const baseline = makeReport();\n    const current = makeReport();\n    const report = compareReport(baseline, current);\n\n    assert.ok(!report.includes('### Per-Route First Load JS'));\n  });\n\n  it('shows header with baseline commitSha and updatedAt', () => {\n    const baseline = makeReport({ commitSha: 'deadbeef', updatedAt: '2024-06-15' });\n    const current = makeReport();\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes('`deadbeef`'));\n    assert.ok(report.includes('2024-06-15'));\n  });\n\n  it('shows \"No bundle size changes detected.\" for empty routes in both reports', () => {\n    const baseline = makeReport({ routes: {} });\n    const current = makeReport({ routes: {} });\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes('No bundle size changes detected.'));\n  });\n\n  it('shows table header when routes have changes', () => {\n    const baseline = makeReport({ routes: { '/app/page': { firstLoadJs: 100, js: 50, css: 0 } } });\n    const current = makeReport({ routes: { '/app/page': { firstLoadJs: 110, js: 60, css: 0 } } });\n    const report = compareReport(baseline, current);\n\n    assert.ok(report.includes('| Route |'));\n    assert.ok(report.includes('| Baseline |'));\n    assert.ok(report.includes('| Current |'));\n  });\n});\n\n// ---------------------------------------------------------------------------\n// readTurbopackEntries\n// ---------------------------------------------------------------------------\n\ndescribe('readTurbopackEntries', () => {\n  // Helper: create a minimal _client-reference-manifest.js fixture\n  function makeManifestContent(\n    routes: Record<string, Record<string, { chunks: string[] }>>,\n  ): string {\n    const manifest: Record<string, { clientModules: Record<string, { chunks: string[] }> }> = {};\n\n    for (const [routeKey, modules] of Object.entries(routes)) {\n      manifest[routeKey] = { clientModules: modules };\n    }\n\n    return `globalThis.__RSC_MANIFEST = ${JSON.stringify(manifest)};`;\n  }\n\n  it('reads chunk paths from a single manifest and normalizes /_next/ prefix', () => {\n    const dir = join(testDir, `turbopack-basic-${Date.now()}`);\n\n    mkdirSync(dir, { recursive: true });\n    writeFileSync(\n      join(dir, 'page_client-reference-manifest.js'),\n      makeManifestContent({\n        '/products/page': {\n          'mod-a': { chunks: ['/_next/static/chunks/a.js'] },\n          'mod-b': { chunks: ['/_next/static/chunks/b.js'] },\n        },\n      }),\n    );\n\n    const entries = readTurbopackEntries(dir);\n\n    assert.ok(entries['/products/page'], 'should have /products/page entry');\n    assert.ok(entries['/products/page'].includes('static/chunks/a.js'), 'should normalize /_next/ prefix');\n    assert.ok(entries['/products/page'].includes('static/chunks/b.js'));\n    assert.ok(!entries['/products/page'].some((c) => c.startsWith('/_next/')), 'no chunk should start with /_next/');\n  });\n\n  it('filters out non-/page routes (layouts, route handlers)', () => {\n    const dir = join(testDir, `turbopack-filter-${Date.now()}`);\n\n    mkdirSync(dir, { recursive: true });\n    writeFileSync(\n      join(dir, 'page_client-reference-manifest.js'),\n      makeManifestContent({\n        '/app/layout': { 'mod-a': { chunks: ['/_next/static/chunks/layout.js'] } },\n        '/app/route': { 'mod-b': { chunks: ['/_next/static/chunks/route.js'] } },\n        '/app/page': { 'mod-c': { chunks: ['/_next/static/chunks/page.js'] } },\n      }),\n    );\n\n    const entries = readTurbopackEntries(dir);\n\n    assert.ok(entries['/app/page'], 'should include /page route');\n    assert.ok(!entries['/app/layout'], 'should exclude /layout route');\n    assert.ok(!entries['/app/route'], 'should exclude /route handler');\n  });\n\n  it('deduplicates chunks appearing in multiple modules', () => {\n    const dir = join(testDir, `turbopack-dedup-${Date.now()}`);\n\n    mkdirSync(dir, { recursive: true });\n    writeFileSync(\n      join(dir, 'page_client-reference-manifest.js'),\n      makeManifestContent({\n        '/shop/page': {\n          'mod-a': { chunks: ['/_next/static/chunks/shared.js', '/_next/static/chunks/a.js'] },\n          'mod-b': { chunks: ['/_next/static/chunks/shared.js', '/_next/static/chunks/b.js'] },\n        },\n      }),\n    );\n\n    const entries = readTurbopackEntries(dir);\n    const chunks = entries['/shop/page'];\n\n    assert.ok(chunks, 'should have /shop/page entry');\n\n    const sharedCount = chunks.filter((c) => c === 'static/chunks/shared.js').length;\n\n    assert.equal(sharedCount, 1, 'shared chunk should appear exactly once');\n    assert.equal(chunks.length, 3, 'should have 3 unique chunks');\n  });\n\n  it('scans subdirectories recursively', () => {\n    const dir = join(testDir, `turbopack-recursive-${Date.now()}`);\n\n    mkdirSync(join(dir, 'nested', 'deep'), { recursive: true });\n    writeFileSync(\n      join(dir, 'nested', 'deep', 'page_client-reference-manifest.js'),\n      makeManifestContent({\n        '/nested/deep/page': { 'mod-a': { chunks: ['/_next/static/chunks/deep.js'] } },\n      }),\n    );\n\n    const entries = readTurbopackEntries(dir);\n\n    assert.ok(entries['/nested/deep/page'], 'should find manifest in nested directory');\n  });\n\n  it('skips malformed manifest files gracefully', () => {\n    const dir = join(testDir, `turbopack-malformed-${Date.now()}`);\n\n    mkdirSync(dir, { recursive: true });\n    writeFileSync(join(dir, 'bad_client-reference-manifest.js'), 'this is not valid JS {{{');\n    writeFileSync(\n      join(dir, 'good_client-reference-manifest.js'),\n      makeManifestContent({\n        '/valid/page': { 'mod-a': { chunks: ['/_next/static/chunks/valid.js'] } },\n      }),\n    );\n\n    // Should not throw, and should still return valid entries\n    assert.doesNotThrow(() => readTurbopackEntries(dir));\n\n    const entries = readTurbopackEntries(dir);\n\n    assert.ok(entries['/valid/page'], 'should return valid entries even when another file is malformed');\n  });\n\n  it('returns empty object when no manifest files exist', () => {\n    const dir = join(testDir, `turbopack-empty-${Date.now()}`);\n\n    mkdirSync(dir, { recursive: true });\n\n    const entries = readTurbopackEntries(dir);\n\n    assert.deepEqual(entries, {});\n  });\n\n  it('returns empty object when manifests have no __RSC_MANIFEST', () => {\n    const dir = join(testDir, `turbopack-no-rsc-${Date.now()}`);\n\n    mkdirSync(dir, { recursive: true });\n    writeFileSync(\n      join(dir, 'page_client-reference-manifest.js'),\n      'globalThis.somethingElse = {};',\n    );\n\n    const entries = readTurbopackEntries(dir);\n\n    assert.deepEqual(entries, {});\n  });\n});\n\n// ---------------------------------------------------------------------------\n// getGzipSize\n// ---------------------------------------------------------------------------\n\ndescribe('getGzipSize', () => {\n  beforeEach(() => clearSizeCache());\n\n  it('returns 0 when file does not exist', () => {\n    const result = getGzipSize(join(testDir, 'nonexistent-file-xyz.js'));\n\n    assert.equal(result, 0);\n  });\n\n  it('returns a positive number for an existing file', () => {\n    const result = getGzipSize(join(testDir, 'route.js'));\n\n    assert.ok(result > 0, `Expected positive size, got ${result}`);\n  });\n\n  it('caches results and returns same value on second call', () => {\n    const filePath = join(testDir, `cache-test-${Date.now()}.js`);\n\n    writeFileSync(filePath, makeJs('cached'));\n\n    const firstResult = getGzipSize(filePath);\n\n    assert.ok(firstResult > 0);\n\n    // Delete the file — the cached value should still be returned\n    unlinkSync(filePath);\n\n    const secondResult = getGzipSize(filePath);\n\n    assert.equal(secondResult, firstResult, 'Should return cached value after file deletion');\n  });\n\n  it('clearSizeCache resets the cache', () => {\n    const filePath = join(testDir, `clear-test-${Date.now()}.js`);\n\n    writeFileSync(filePath, makeJs('cleared'));\n\n    const sizeBeforeDelete = getGzipSize(filePath);\n\n    assert.ok(sizeBeforeDelete > 0);\n\n    unlinkSync(filePath);\n    clearSizeCache();\n\n    // After clearing cache, file is gone so size should be 0\n    const sizeAfterClear = getGzipSize(filePath);\n\n    assert.equal(sizeAfterClear, 0, 'Should return 0 after cache cleared and file deleted');\n  });\n});\n"
  },
  {
    "path": ".github/scripts/__tests__/compare-unlighthouse.test.mts",
    "content": "import { describe, it } from 'node:test';\nimport assert from 'node:assert/strict';\n\nimport { compareResults } from '../compare-unlighthouse.mts';\nimport type { CiResult } from '../compare-unlighthouse.mts';\n\n// ---------------------------------------------------------------------------\n// Fixtures\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_METRICS: CiResult['summary']['metrics'] = {\n  'largest-contentful-paint': { displayValue: '2.5 s' },\n  'cumulative-layout-shift': { displayValue: '0.01' },\n  'first-contentful-paint': { displayValue: '1.2 s' },\n  'total-blocking-time': { displayValue: '100 ms' },\n  'max-potential-fid': { displayValue: '200 ms' },\n  interactive: { displayValue: '3.5 s' },\n};\n\nfunction makeCiResult(overrides: {\n  score?: number;\n  performance?: number;\n  accessibility?: number;\n  'best-practices'?: number;\n  seo?: number;\n  metrics?: CiResult['summary']['metrics'];\n} = {}): CiResult {\n  return {\n    summary: {\n      score: overrides.score ?? 0.85,\n      categories: {\n        performance: { score: overrides.performance ?? 0.80 },\n        accessibility: { score: overrides.accessibility ?? 0.92 },\n        'best-practices': { score: overrides['best-practices'] ?? 1.0 },\n        seo: { score: overrides.seo ?? 0.90 },\n      },\n      metrics: overrides.metrics ?? { ...DEFAULT_METRICS },\n    },\n  };\n}\n\nconst BASE = makeCiResult();\n\n// ---------------------------------------------------------------------------\n// hasChanges\n// ---------------------------------------------------------------------------\n\ndescribe('hasChanges', () => {\n  it('is false when all four results are identical', () => {\n    const { hasChanges } = compareResults(BASE, BASE, BASE, BASE, 1);\n\n    assert.equal(hasChanges, false);\n  });\n\n  it('is true when preview desktop summary score differs by exactly 1pp', () => {\n    const preview = makeCiResult({ score: 0.84 }); // 1pp below\n    const { hasChanges } = compareResults(BASE, BASE, preview, BASE, 1);\n\n    assert.equal(hasChanges, true);\n  });\n\n  it('is false when summary score differs by less than 1pp', () => {\n    const preview = makeCiResult({ score: 0.855 }); // 0.5pp above\n    const { hasChanges } = compareResults(BASE, BASE, preview, BASE, 1);\n\n    assert.equal(hasChanges, false);\n  });\n\n  it('is true when preview mobile summary score differs by >= 1pp', () => {\n    const previewMobile = makeCiResult({ score: 0.74 });\n    const { hasChanges } = compareResults(BASE, BASE, BASE, previewMobile, 1);\n\n    assert.equal(hasChanges, true);\n  });\n\n  it('is true when a category score differs by >= 1pp', () => {\n    const preview = makeCiResult({ performance: 0.79 }); // 1pp below 0.80\n    const { hasChanges } = compareResults(BASE, BASE, preview, BASE, 1);\n\n    assert.equal(hasChanges, true);\n  });\n\n  it('is false when category score differs by less than 1pp', () => {\n    const preview = makeCiResult({ performance: 0.805 }); // 0.5pp above\n    const { hasChanges } = compareResults(BASE, BASE, preview, BASE, 1);\n\n    assert.equal(hasChanges, false);\n  });\n\n  it('respects a custom threshold', () => {\n    // 2pp delta — true at threshold=1, false at threshold=3\n    const preview = makeCiResult({ score: 0.83 });\n\n    assert.equal(compareResults(BASE, BASE, preview, BASE, 1).hasChanges, true);\n    assert.equal(compareResults(BASE, BASE, preview, BASE, 3).hasChanges, false);\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Report heading\n// ---------------------------------------------------------------------------\n\ndescribe('report heading', () => {\n  it('contains the comparison heading', () => {\n    const { markdown } = compareResults(BASE, BASE, BASE, BASE, 1);\n\n    assert.ok(\n      markdown.includes('## Unlighthouse Performance Comparison'),\n      'Missing main heading',\n    );\n  });\n\n  it('appends provider label when provider is given', () => {\n    const { markdown } = compareResults(BASE, BASE, BASE, BASE, 1, 'vercel');\n\n    assert.ok(\n      markdown.includes('## Unlighthouse Performance Comparison — Vercel'),\n      'Missing provider label in heading',\n    );\n  });\n\n  it('capitalises the provider label', () => {\n    const { markdown } = compareResults(BASE, BASE, BASE, BASE, 1, 'cloudflare');\n\n    assert.ok(markdown.includes('— Cloudflare'), 'Provider should be capitalised');\n  });\n\n  it('omits provider label when none provided', () => {\n    const { markdown } = compareResults(BASE, BASE, BASE, BASE, 1);\n\n    assert.ok(\n      !markdown.includes(' — '),\n      'Should not contain a provider label separator',\n    );\n  });\n\n  it('contains the description text', () => {\n    const { markdown } = compareResults(BASE, BASE, BASE, BASE, 1);\n\n    assert.ok(\n      markdown.includes(\n        'Comparing PR preview deployment Unlighthouse scores vs production Unlighthouse scores.',\n      ),\n    );\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Summary Score section\n// ---------------------------------------------------------------------------\n\ndescribe('Summary Score section', () => {\n  it('contains the Summary Score heading', () => {\n    const { markdown } = compareResults(BASE, BASE, BASE, BASE, 1);\n\n    assert.ok(markdown.includes('### Summary Score'));\n  });\n\n  it('contains the aggregate score note', () => {\n    const { markdown } = compareResults(BASE, BASE, BASE, BASE, 1);\n\n    assert.ok(\n      markdown.includes(\n        'Aggregate score across all categories as reported by Unlighthouse.',\n      ),\n    );\n  });\n\n  it('renders scores as integers on a 1-100 scale', () => {\n    const prod = makeCiResult({ score: 0.85 });\n    const prev = makeCiResult({ score: 0.72 });\n    const { markdown } = compareResults(prod, prod, prev, prev, 1);\n\n    assert.ok(markdown.includes('| Score | 85 | 85 | 72 | 72 |'));\n  });\n\n  it('rounds fractional scores correctly', () => {\n    const prod = makeCiResult({ score: 0.856 }); // rounds to 86\n    const { markdown } = compareResults(prod, BASE, prod, BASE, 1);\n\n    assert.ok(markdown.includes('86'), 'Score 0.856 should round to 86');\n  });\n\n  it('contains the four-column header', () => {\n    const { markdown } = compareResults(BASE, BASE, BASE, BASE, 1);\n\n    assert.ok(\n      markdown.includes('| | Prod Desktop | Prod Mobile | Preview Desktop | Preview Mobile |'),\n    );\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Category Scores section\n// ---------------------------------------------------------------------------\n\ndescribe('Category Scores section', () => {\n  it('contains the Category Scores heading', () => {\n    const { markdown } = compareResults(BASE, BASE, BASE, BASE, 1);\n\n    assert.ok(markdown.includes('### Category Scores'));\n  });\n\n  it('renders all four categories', () => {\n    const { markdown } = compareResults(BASE, BASE, BASE, BASE, 1);\n\n    assert.ok(markdown.includes('Performance'));\n    assert.ok(markdown.includes('Accessibility'));\n    assert.ok(markdown.includes('Best Practices'));\n    assert.ok(markdown.includes('SEO'));\n  });\n\n  it('renders category scores as integers on a 1-100 scale', () => {\n    const prod = makeCiResult({ performance: 0.80 });\n    const prev = makeCiResult({ performance: 0.93 });\n    const { markdown } = compareResults(prod, prod, prev, prev, 1);\n\n    assert.ok(\n      markdown.includes('| Performance | 80 | 80 | 93 | 93 |'),\n      'Performance row should contain all four scores as integers',\n    );\n  });\n\n  it('shows all four column values independently', () => {\n    const prodDesktop = makeCiResult({ seo: 0.88 });\n    const prodMobile = makeCiResult({ seo: 0.75 });\n    const prevDesktop = makeCiResult({ seo: 0.91 });\n    const prevMobile = makeCiResult({ seo: 0.82 });\n    const { markdown } = compareResults(prodDesktop, prodMobile, prevDesktop, prevMobile, 1);\n\n    assert.ok(markdown.includes('| SEO | 88 | 75 | 91 | 82 |'));\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Core Web Vitals section\n// ---------------------------------------------------------------------------\n\ndescribe('Core Web Vitals section', () => {\n  it('contains the Core Web Vitals heading', () => {\n    const { markdown } = compareResults(BASE, BASE, BASE, BASE, 1);\n\n    assert.ok(markdown.includes('### Core Web Vitals'));\n  });\n\n  it('renders all six metrics', () => {\n    const { markdown } = compareResults(BASE, BASE, BASE, BASE, 1);\n\n    assert.ok(markdown.includes('LCP'));\n    assert.ok(markdown.includes('CLS'));\n    assert.ok(markdown.includes('FCP'));\n    assert.ok(markdown.includes('TBT'));\n    assert.ok(markdown.includes('Max Potential FID'));\n    assert.ok(markdown.includes('Time to Interactive'));\n  });\n\n  it('passes displayValue through unchanged', () => {\n    const ci = makeCiResult({\n      metrics: {\n        ...DEFAULT_METRICS,\n        'largest-contentful-paint': { displayValue: '4.8 s' },\n      },\n    });\n    const { markdown } = compareResults(ci, ci, ci, ci, 1);\n\n    assert.ok(markdown.includes('4.8 s'), 'displayValue should appear as-is');\n  });\n\n  it('shows — for a metric missing from a result', () => {\n    const ciMissingMetric = makeCiResult({ metrics: {} });\n    const { markdown } = compareResults(BASE, ciMissingMetric, BASE, BASE, 1);\n\n    assert.ok(markdown.includes('—'), 'Missing metric should show —');\n  });\n\n  it('shows four displayValues per metric row', () => {\n    const prodDesktop = makeCiResult({ metrics: { ...DEFAULT_METRICS, 'total-blocking-time': { displayValue: '80 ms' } } });\n    const prodMobile = makeCiResult({ metrics: { ...DEFAULT_METRICS, 'total-blocking-time': { displayValue: '320 ms' } } });\n    const prevDesktop = makeCiResult({ metrics: { ...DEFAULT_METRICS, 'total-blocking-time': { displayValue: '75 ms' } } });\n    const prevMobile = makeCiResult({ metrics: { ...DEFAULT_METRICS, 'total-blocking-time': { displayValue: '310 ms' } } });\n    const { markdown } = compareResults(prodDesktop, prodMobile, prevDesktop, prevMobile, 1);\n\n    assert.ok(markdown.includes('| TBT | 80 ms | 320 ms | 75 ms | 310 ms |'));\n  });\n});\n"
  },
  {
    "path": ".github/scripts/__tests__/post-bundle-comment.test.mts",
    "content": "import { describe, it, beforeEach } from 'node:test';\nimport assert from 'node:assert/strict';\nimport { writeFileSync, mkdirSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { createRequire } from 'node:module';\n\nconst require = createRequire(import.meta.url);\nconst postBundleComment = require('../post-bundle-comment.js') as (args: {\n  github: ReturnType<typeof makeGithub>['github'];\n  context: ReturnType<typeof makeContext>;\n  reportPath?: string;\n}) => Promise<void>;\n\nconst marker = '<!-- bundle-size-report -->';\n\nlet tmpDir: string;\nlet reportPath: string;\n\nbeforeEach(() => {\n  tmpDir = join(tmpdir(), `post-bundle-test-${Date.now()}`);\n  mkdirSync(tmpDir, { recursive: true });\n  reportPath = join(tmpDir, 'report.md');\n  writeFileSync(reportPath, '## Bundle Size Report\\n\\nSome content here.');\n});\n\ninterface Comment {\n  id: number;\n  body: string;\n}\n\ninterface GithubCalls {\n  create: object[];\n  update: object[];\n  list: object[];\n}\n\n// Helper to create a mock github object and record calls\nfunction makeGithub(existingComments: Comment[] = []) {\n  const calls: GithubCalls = { create: [], update: [], list: [] };\n  const github = {\n    rest: {\n      issues: {\n        listComments: async (args: object) => {\n          calls.list.push(args);\n          return { data: existingComments };\n        },\n        createComment: async (args: object) => {\n          calls.create.push(args);\n        },\n        updateComment: async (args: object) => {\n          calls.update.push(args);\n        },\n      },\n    },\n  };\n  return { github, calls };\n}\n\nfunction makeContext({ owner = 'test-owner', repo = 'test-repo', number = 42 } = {}) {\n  return {\n    repo: { owner, repo },\n    issue: { number },\n  };\n}\n\ndescribe('post-bundle-comment', () => {\n  it('creates a new comment when no existing comment contains the marker', async () => {\n    const { github, calls } = makeGithub([]);\n    await postBundleComment({ github, context: makeContext(), reportPath });\n\n    assert.equal(calls.create.length, 1, 'Should create exactly one comment');\n    assert.equal(calls.update.length, 0, 'Should not update any comment');\n  });\n\n  it('updates existing comment when marker found', async () => {\n    const existing = { id: 99, body: `${marker}\\nOld content` };\n    const { github, calls } = makeGithub([existing]);\n    await postBundleComment({ github, context: makeContext(), reportPath });\n\n    assert.equal(calls.update.length, 1, 'Should update exactly one comment');\n    assert.equal(calls.create.length, 0, 'Should not create a new comment');\n    assert.equal((calls.update[0] as { comment_id: number }).comment_id, 99, 'Should update the correct comment by id');\n  });\n\n  it('body always starts with marker and newline', async () => {\n    const { github, calls } = makeGithub([]);\n    await postBundleComment({ github, context: makeContext(), reportPath });\n\n    const body = (calls.create[0] as { body: string }).body;\n\n    assert.ok(body.startsWith(`${marker}\\n`), `Body should start with marker, got: ${body.slice(0, 50)}`);\n  });\n\n  it('updated comment body also starts with marker and newline', async () => {\n    const existing = { id: 7, body: `${marker}\\nStale content` };\n    const { github, calls } = makeGithub([existing]);\n    await postBundleComment({ github, context: makeContext(), reportPath });\n\n    const body = (calls.update[0] as { body: string }).body;\n\n    assert.ok(body.startsWith(`${marker}\\n`));\n  });\n\n  it('includes report file content in the comment body', async () => {\n    const { github, calls } = makeGithub([]);\n    await postBundleComment({ github, context: makeContext(), reportPath });\n\n    const body = (calls.create[0] as { body: string }).body;\n\n    assert.ok(body.includes('## Bundle Size Report'), 'Should include report heading');\n    assert.ok(body.includes('Some content here.'), 'Should include report body content');\n  });\n\n  it('reads report from a custom reportPath', async () => {\n    const customPath = join(tmpDir, 'custom.md');\n\n    writeFileSync(customPath, 'Custom report content for testing!');\n\n    const { github, calls } = makeGithub([]);\n    await postBundleComment({ github, context: makeContext(), reportPath: customPath });\n\n    assert.ok((calls.create[0] as { body: string }).body.includes('Custom report content for testing!'));\n  });\n\n  it('passes correct owner, repo, issue_number from context to listComments', async () => {\n    const { github, calls } = makeGithub([]);\n    await postBundleComment({\n      github,\n      context: makeContext({ owner: 'my-org', repo: 'my-repo', number: 123 }),\n      reportPath,\n    });\n\n    assert.equal((calls.list[0] as { owner: string }).owner, 'my-org');\n    assert.equal((calls.list[0] as { repo: string }).repo, 'my-repo');\n    assert.equal((calls.list[0] as { issue_number: number }).issue_number, 123);\n  });\n\n  it('passes correct owner, repo, issue_number from context to createComment', async () => {\n    const { github, calls } = makeGithub([]);\n    await postBundleComment({\n      github,\n      context: makeContext({ owner: 'my-org', repo: 'my-repo', number: 123 }),\n      reportPath,\n    });\n\n    assert.equal((calls.create[0] as { owner: string }).owner, 'my-org');\n    assert.equal((calls.create[0] as { repo: string }).repo, 'my-repo');\n    assert.equal((calls.create[0] as { issue_number: number }).issue_number, 123);\n  });\n\n  it('passes correct owner and repo to updateComment', async () => {\n    const existing = { id: 55, body: `${marker}\\nOld` };\n    const { github, calls } = makeGithub([existing]);\n    await postBundleComment({\n      github,\n      context: makeContext({ owner: 'org2', repo: 'repo2', number: 7 }),\n      reportPath,\n    });\n\n    assert.equal((calls.update[0] as { owner: string }).owner, 'org2');\n    assert.equal((calls.update[0] as { repo: string }).repo, 'repo2');\n  });\n\n  it('uses the first comment that contains the marker (not just exact match)', async () => {\n    const comments = [\n      { id: 1, body: 'Just a regular comment' },\n      { id: 2, body: `${marker}\\nFirst bundle report` },\n      { id: 3, body: `${marker}\\nSecond bundle report` },\n    ];\n    const { github, calls } = makeGithub(comments);\n    await postBundleComment({ github, context: makeContext(), reportPath });\n\n    assert.equal(calls.update.length, 1);\n    assert.equal((calls.update[0] as { comment_id: number }).comment_id, 2, 'Should update the first matching comment');\n  });\n\n  it('creates comment when existing comments do not contain the marker', async () => {\n    const comments = [\n      { id: 10, body: 'No marker here' },\n      { id: 11, body: 'Also no marker' },\n    ];\n    const { github, calls } = makeGithub(comments);\n    await postBundleComment({ github, context: makeContext(), reportPath });\n\n    assert.equal(calls.create.length, 1);\n    assert.equal(calls.update.length, 0);\n  });\n});\n"
  },
  {
    "path": ".github/scripts/__tests__/post-unlighthouse-commit-comment.test.mts",
    "content": "import { describe, it, beforeEach } from 'node:test';\nimport assert from 'node:assert/strict';\nimport { writeFileSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { createRequire } from 'node:module';\n\nconst require = createRequire(import.meta.url);\nconst postReport = require('../post-unlighthouse-commit-comment.js') as (args: {\n  github: ReturnType<typeof makeGithub>['github'];\n  context: ReturnType<typeof makeContext>;\n  reportPath?: string;\n}) => Promise<void>;\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\ninterface Calls {\n  createCommitComment: object[];\n}\n\nfunction makeGithub() {\n  const calls: Calls = { createCommitComment: [] };\n  const github = {\n    rest: {\n      repos: {\n        createCommitComment: async (args: object) => {\n          calls.createCommitComment.push(args);\n        },\n      },\n    },\n  };\n  return { github, calls };\n}\n\nfunction makeContext({\n  owner = 'test-owner',\n  repo = 'test-repo',\n  sha = 'abc123',\n  runId = 99,\n}: {\n  owner?: string;\n  repo?: string;\n  sha?: string;\n  runId?: number;\n} = {}) {\n  return {\n    repo: { owner, repo },\n    runId,\n    payload: {\n      deployment: { sha },\n    },\n  };\n}\n\nlet reportPath: string;\n\nbeforeEach(() => {\n  const tmpDir = join(tmpdir(), `post-unlighthouse-report-test-${Date.now()}`);\n  mkdirSync(tmpDir, { recursive: true });\n  reportPath = join(tmpDir, 'report.md');\n  writeFileSync(reportPath, '## Unlighthouse Audit — `canary`\\n\\nSome results.');\n});\n\n// ---------------------------------------------------------------------------\n// Early exits\n// ---------------------------------------------------------------------------\n\ndescribe('early exits', () => {\n  it('does nothing when deployment sha is missing', async () => {\n    const { github, calls } = makeGithub();\n    const context = { ...makeContext(), payload: {} };\n    await postReport({ github, context, reportPath });\n\n    assert.equal(calls.createCommitComment.length, 0);\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Commit comment creation\n// ---------------------------------------------------------------------------\n\ndescribe('commit comment creation', () => {\n  it('creates a commit comment with the deployment sha', async () => {\n    const { github, calls } = makeGithub();\n    await postReport({ github, context: makeContext({ sha: 'deadbeef' }), reportPath });\n\n    assert.equal(calls.createCommitComment.length, 1);\n    assert.equal(\n      (calls.createCommitComment[0] as { commit_sha: string }).commit_sha,\n      'deadbeef',\n    );\n  });\n\n  it('passes the correct owner and repo', async () => {\n    const { github, calls } = makeGithub();\n    await postReport({\n      github,\n      context: makeContext({ owner: 'my-org', repo: 'my-repo' }),\n      reportPath,\n    });\n\n    assert.equal((calls.createCommitComment[0] as { owner: string }).owner, 'my-org');\n    assert.equal((calls.createCommitComment[0] as { repo: string }).repo, 'my-repo');\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Comment body\n// ---------------------------------------------------------------------------\n\ndescribe('comment body', () => {\n  it('starts with the canary marker', async () => {\n    const { github, calls } = makeGithub();\n    await postReport({ github, context: makeContext(), reportPath });\n\n    const body = (calls.createCommitComment[0] as { body: string }).body;\n\n    assert.ok(\n      body.startsWith('<!-- canary-lighthouse-report -->\\n'),\n      `Body should start with marker, got: ${body.slice(0, 60)}`,\n    );\n  });\n\n  it('includes the report file content', async () => {\n    const { github, calls } = makeGithub();\n    await postReport({ github, context: makeContext(), reportPath });\n\n    const body = (calls.createCommitComment[0] as { body: string }).body;\n\n    assert.ok(body.includes('## Unlighthouse Audit'));\n    assert.ok(body.includes('Some results.'));\n  });\n\n  it('includes the workflow run link', async () => {\n    const { github, calls } = makeGithub();\n    await postReport({\n      github,\n      context: makeContext({ owner: 'my-org', repo: 'my-repo', runId: 12345 }),\n      reportPath,\n    });\n\n    const body = (calls.createCommitComment[0] as { body: string }).body;\n\n    assert.ok(\n      body.includes('https://github.com/my-org/my-repo/actions/runs/12345'),\n      'Body should contain the workflow run URL',\n    );\n  });\n\n  it('run link is preceded by a newline', async () => {\n    const { github, calls } = makeGithub();\n    await postReport({ github, context: makeContext(), reportPath });\n\n    const body = (calls.createCommitComment[0] as { body: string }).body;\n    const linkIndex = body.indexOf('[Full Unlighthouse report');\n\n    assert.ok(linkIndex > 0, 'Run link should be present');\n    assert.equal(body[linkIndex - 1], '\\n', 'Run link should be preceded by a newline');\n  });\n});\n"
  },
  {
    "path": ".github/scripts/__tests__/post-unlighthouse-pr-comment.test.mts",
    "content": "import { describe, it, beforeEach } from 'node:test';\nimport assert from 'node:assert/strict';\nimport { writeFileSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { createRequire } from 'node:module';\n\nconst require = createRequire(import.meta.url);\nconst postComment = require('../post-unlighthouse-pr-comment.js') as (args: {\n  github: ReturnType<typeof makeGithub>['github'];\n  context: ReturnType<typeof makeContext>;\n  provider?: string;\n  reportPath?: string;\n  metaPath?: string;\n}) => Promise<void>;\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\ninterface Comment {\n  id: number;\n  body: string;\n}\n\ninterface Calls {\n  listPrs: object[];\n  listComments: object[];\n  create: object[];\n  update: object[];\n}\n\nfunction makeGithub(opts: { prs?: { number: number }[]; comments?: Comment[] } = {}) {\n  const calls: Calls = { listPrs: [], listComments: [], create: [], update: [] };\n  const github = {\n    rest: {\n      repos: {\n        listPullRequestsAssociatedWithCommit: async (args: object) => {\n          calls.listPrs.push(args);\n          return { data: opts.prs ?? [{ number: 42 }] };\n        },\n      },\n      issues: {\n        listComments: async (args: object) => {\n          calls.listComments.push(args);\n          return { data: opts.comments ?? [] };\n        },\n        createComment: async (args: object) => {\n          calls.create.push(args);\n        },\n        updateComment: async (args: object) => {\n          calls.update.push(args);\n        },\n      },\n    },\n  };\n  return { github, calls };\n}\n\nfunction makeContext({\n  owner = 'test-owner',\n  repo = 'test-repo',\n  sha = 'abc123',\n  runId = 99,\n}: {\n  owner?: string;\n  repo?: string;\n  sha?: string;\n  runId?: number;\n} = {}) {\n  return {\n    repo: { owner, repo },\n    runId,\n    payload: {\n      deployment: { sha },\n    },\n  };\n}\n\nlet tmpDir: string;\nlet reportPath: string;\nlet metaPath: string;\n\nbeforeEach(() => {\n  tmpDir = join(tmpdir(), `post-unlighthouse-test-${Date.now()}`);\n  mkdirSync(tmpDir, { recursive: true });\n  reportPath = join(tmpDir, 'report.md');\n  metaPath = join(tmpDir, 'meta.json');\n  writeFileSync(reportPath, '## Unlighthouse Performance Comparison\\n\\nSome results.');\n  writeFileSync(metaPath, JSON.stringify({ hasChanges: true }));\n});\n\n// ---------------------------------------------------------------------------\n// Early exits\n// ---------------------------------------------------------------------------\n\ndescribe('early exits', () => {\n  it('does nothing when hasChanges is false', async () => {\n    writeFileSync(metaPath, JSON.stringify({ hasChanges: false }));\n    const { github, calls } = makeGithub();\n    await postComment({ github, context: makeContext(), reportPath, metaPath });\n\n    assert.equal(calls.create.length, 0);\n    assert.equal(calls.update.length, 0);\n  });\n\n  it('does nothing when deployment sha is missing', async () => {\n    const { github, calls } = makeGithub();\n    const context = { ...makeContext(), payload: {} };\n    await postComment({ github, context, reportPath, metaPath });\n\n    assert.equal(calls.create.length, 0);\n    assert.equal(calls.update.length, 0);\n  });\n\n  it('does nothing when no PR is associated with the sha', async () => {\n    const { github, calls } = makeGithub({ prs: [] });\n    await postComment({ github, context: makeContext(), reportPath, metaPath });\n\n    assert.equal(calls.create.length, 0);\n    assert.equal(calls.update.length, 0);\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Comment creation\n// ---------------------------------------------------------------------------\n\ndescribe('comment creation', () => {\n  it('creates a comment when no existing comment has the marker', async () => {\n    const { github, calls } = makeGithub({ comments: [] });\n    await postComment({ github, context: makeContext(), reportPath, metaPath });\n\n    assert.equal(calls.create.length, 1);\n    assert.equal(calls.update.length, 0);\n  });\n\n  it('uses the PR number found from the commit sha', async () => {\n    const { github, calls } = makeGithub({ prs: [{ number: 77 }] });\n    await postComment({ github, context: makeContext(), reportPath, metaPath });\n\n    assert.equal((calls.create[0] as { issue_number: number }).issue_number, 77);\n  });\n\n  it('passes the correct owner and repo', async () => {\n    const { github, calls } = makeGithub();\n    await postComment({\n      github,\n      context: makeContext({ owner: 'my-org', repo: 'my-repo' }),\n      reportPath,\n      metaPath,\n    });\n\n    assert.equal((calls.create[0] as { owner: string }).owner, 'my-org');\n    assert.equal((calls.create[0] as { repo: string }).repo, 'my-repo');\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Comment update\n// ---------------------------------------------------------------------------\n\ndescribe('comment update', () => {\n  it('updates an existing comment that contains the marker', async () => {\n    const marker = '<!-- unlighthouse-vercel-report -->';\n    const existing = { id: 55, body: `${marker}\\nOld content` };\n    const { github, calls } = makeGithub({ comments: [existing] });\n    await postComment({ github, context: makeContext(), provider: 'vercel', reportPath, metaPath });\n\n    assert.equal(calls.update.length, 1);\n    assert.equal(calls.create.length, 0);\n    assert.equal((calls.update[0] as { comment_id: number }).comment_id, 55);\n  });\n\n  it('creates a new comment when existing comments do not contain the marker', async () => {\n    const comments = [\n      { id: 1, body: 'unrelated comment' },\n      { id: 2, body: '<!-- some-other-marker -->\\nOther report' },\n    ];\n    const { github, calls } = makeGithub({ comments });\n    await postComment({ github, context: makeContext(), provider: 'vercel', reportPath, metaPath });\n\n    assert.equal(calls.create.length, 1);\n    assert.equal(calls.update.length, 0);\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Comment body\n// ---------------------------------------------------------------------------\n\ndescribe('comment body', () => {\n  it('starts with the provider-specific marker', async () => {\n    const { github, calls } = makeGithub();\n    await postComment({ github, context: makeContext(), provider: 'vercel', reportPath, metaPath });\n\n    const body = (calls.create[0] as { body: string }).body;\n\n    assert.ok(\n      body.startsWith('<!-- unlighthouse-vercel-report -->\\n'),\n      `Body should start with vercel marker, got: ${body.slice(0, 60)}`,\n    );\n  });\n\n  it('uses provider name in the marker', async () => {\n    const { github, calls } = makeGithub();\n    await postComment({ github, context: makeContext(), provider: 'cloudflare', reportPath, metaPath });\n\n    const body = (calls.create[0] as { body: string }).body;\n\n    assert.ok(body.includes('<!-- unlighthouse-cloudflare-report -->'));\n  });\n\n  it('includes the report file content', async () => {\n    const { github, calls } = makeGithub();\n    await postComment({ github, context: makeContext(), reportPath, metaPath });\n\n    const body = (calls.create[0] as { body: string }).body;\n\n    assert.ok(body.includes('## Unlighthouse Performance Comparison'));\n    assert.ok(body.includes('Some results.'));\n  });\n\n  it('includes the workflow run link', async () => {\n    const { github, calls } = makeGithub();\n    await postComment({\n      github,\n      context: makeContext({ owner: 'my-org', repo: 'my-repo', runId: 12345 }),\n      reportPath,\n      metaPath,\n    });\n\n    const body = (calls.create[0] as { body: string }).body;\n\n    assert.ok(\n      body.includes('https://github.com/my-org/my-repo/actions/runs/12345'),\n      'Body should contain the workflow run URL',\n    );\n  });\n\n  it('run link is not inside a table (preceded by a blank line)', async () => {\n    const { github, calls } = makeGithub();\n    await postComment({ github, context: makeContext(), reportPath, metaPath });\n\n    const body = (calls.create[0] as { body: string }).body;\n    const linkIndex = body.indexOf('[Full Unlighthouse report');\n\n    assert.ok(linkIndex > 0, 'Run link should be present');\n    // The character before the link text should be a newline (blank line separator)\n    assert.equal(body[linkIndex - 1], '\\n', 'Run link should be preceded by a blank line');\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Sha lookup\n// ---------------------------------------------------------------------------\n\ndescribe('sha lookup', () => {\n  it('passes the deployment sha to listPullRequestsAssociatedWithCommit', async () => {\n    const { github, calls } = makeGithub();\n    await postComment({\n      github,\n      context: makeContext({ sha: 'deadbeef' }),\n      reportPath,\n      metaPath,\n    });\n\n    assert.equal(\n      (calls.listPrs[0] as { commit_sha: string }).commit_sha,\n      'deadbeef',\n    );\n  });\n});\n"
  },
  {
    "path": ".github/scripts/audit-unlighthouse.mts",
    "content": "#!/usr/bin/env node\n/* eslint-disable no-console, no-restricted-syntax, no-plusplus */\n\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\nimport { resolve } from \"node:path\";\n\ninterface CiResult {\n  summary: {\n    score: number;\n    categories: Record<string, { score: number }>;\n    metrics: Record<string, { displayValue: string }>;\n  };\n}\n\nfunction loadCiResult(filePath: string): CiResult {\n  if (!existsSync(filePath)) {\n    console.error(`Error: file not found: ${filePath}`);\n    process.exit(1);\n  }\n\n  return JSON.parse(readFileSync(filePath, \"utf-8\")) as CiResult;\n}\n\nfunction score(value: number): string {\n  return String(Math.round(value * 100));\n}\n\nfunction row(label: string, desktop: string, mobile: string): string {\n  return `| ${label} | ${desktop} | ${mobile} |`;\n}\n\nconst CATEGORY_ORDER = [\"performance\", \"accessibility\", \"best-practices\", \"seo\"];\n\nconst CATEGORY_LABELS: Record<string, string> = {\n  performance: \"Performance\",\n  accessibility: \"Accessibility\",\n  \"best-practices\": \"Best Practices\",\n  seo: \"SEO\",\n};\n\nconst METRIC_ORDER = [\n  \"largest-contentful-paint\",\n  \"cumulative-layout-shift\",\n  \"first-contentful-paint\",\n  \"total-blocking-time\",\n  \"max-potential-fid\",\n  \"interactive\",\n];\n\nconst METRIC_LABELS: Record<string, string> = {\n  \"largest-contentful-paint\": \"LCP\",\n  \"cumulative-layout-shift\": \"CLS\",\n  \"first-contentful-paint\": \"FCP\",\n  \"total-blocking-time\": \"TBT\",\n  \"max-potential-fid\": \"Max Potential FID\",\n  interactive: \"Time to Interactive\",\n};\n\nfunction buildReport(\n  desktop: CiResult,\n  mobile: CiResult,\n  branch?: string,\n): string {\n  const branchLabel = branch ? ` — \\`${branch}\\`` : \"\";\n\n  const lines: string[] = [];\n\n  lines.push(`## Unlighthouse Audit${branchLabel}`);\n  lines.push(\"Unlighthouse scores for the latest commit on this branch.\");\n  lines.push(\"\");\n\n  lines.push(\"### Summary Score\");\n  lines.push(\n    \"_Aggregate score across all categories as reported by Unlighthouse._\",\n  );\n  lines.push(\"\");\n  lines.push(\"| | Desktop | Mobile |\");\n  lines.push(\"|:-|:--------|:-------|\");\n  lines.push(row(\"Score\", score(desktop.summary.score), score(mobile.summary.score)));\n  lines.push(\"\");\n\n  lines.push(\"### Category Scores\");\n  lines.push(\"\");\n  lines.push(\"| Category | Desktop | Mobile |\");\n  lines.push(\"|:---------|:--------|:-------|\");\n\n  for (const id of CATEGORY_ORDER) {\n    lines.push(\n      row(\n        CATEGORY_LABELS[id] ?? id,\n        score(desktop.summary.categories[id]?.score ?? 0),\n        score(mobile.summary.categories[id]?.score ?? 0),\n      ),\n    );\n  }\n\n  lines.push(\"\");\n\n  lines.push(\"### Core Web Vitals\");\n  lines.push(\"\");\n  lines.push(\"| Metric | Desktop | Mobile |\");\n  lines.push(\"|:-------|:--------|:-------|\");\n\n  for (const id of METRIC_ORDER) {\n    lines.push(\n      row(\n        METRIC_LABELS[id] ?? id,\n        desktop.summary.metrics[id]?.displayValue ?? \"—\",\n        mobile.summary.metrics[id]?.displayValue ?? \"—\",\n      ),\n    );\n  }\n\n  lines.push(\"\");\n\n  return lines.join(\"\\n\");\n}\n\nexport { buildReport };\nexport type { CiResult };\n\nconst isMain = process.argv[1] === fileURLToPath(import.meta.url);\n\nif (isMain) {\n  const { values } = parseArgs({\n    options: {\n      desktop: { type: \"string\" },\n      mobile: { type: \"string\" },\n      branch: { type: \"string\" },\n      output: { type: \"string\" },\n    },\n  });\n\n  if (!values.desktop || !values.mobile) {\n    console.error(\n      \"Usage: report-unlighthouse.mts --desktop <path> --mobile <path> [--branch <name>] [--output <path>]\",\n    );\n    process.exit(1);\n  }\n\n  const desktop = loadCiResult(resolve(values.desktop));\n  const mobile = loadCiResult(resolve(values.mobile));\n  const markdown = buildReport(desktop, mobile, values.branch);\n  const outputPath = values.output ? resolve(values.output) : null;\n\n  if (outputPath) {\n    writeFileSync(outputPath, markdown);\n    console.error(`Unlighthouse report written to ${outputPath}`);\n  } else {\n    process.stdout.write(markdown);\n  }\n}\n"
  },
  {
    "path": ".github/scripts/bundle-size.mts",
    "content": "#!/usr/bin/env node\n/* eslint-disable no-console, no-restricted-syntax, no-plusplus, no-continue */\n\nimport { existsSync, readFileSync, readdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\nimport { gzipSync } from \"node:zlib\";\n\n// eslint-disable-next-line no-underscore-dangle\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst CORE_DIR = resolve(__dirname, \"..\", \"..\", \"core\");\n\ninterface ChunkSizes {\n  js: number;\n  css: number;\n}\n\ninterface RouteMetric {\n  js: number;\n  css: number;\n  firstLoadJs: number;\n}\n\ninterface BundleReport {\n  commitSha: string;\n  updatedAt: string;\n  firstLoadJs: number;\n  totalJs: number;\n  totalCss: number;\n  shared?: { js: number; css: number };\n  routes?: Record<string, RouteMetric>;\n}\n\ninterface CompareOptions {\n  threshold?: number;\n}\n\nfunction round1(n: number): number {\n  return Math.round(n * 10) / 10;\n}\n\nconst sizeCache = new Map<string, number>();\n\nfunction clearSizeCache(): void {\n  sizeCache.clear();\n}\n\nfunction getGzipSize(filePath: string): number {\n  if (sizeCache.has(filePath)) return sizeCache.get(filePath)!;\n\n  if (!existsSync(filePath)) {\n    sizeCache.set(filePath, 0);\n\n    return 0;\n  }\n\n  const data = readFileSync(filePath);\n  const gzipped = gzipSync(data, { level: 6 });\n  const sizeKb = gzipped.length / 1024;\n\n  sizeCache.set(filePath, sizeKb);\n\n  return sizeKb;\n}\n\nfunction sumChunkSizes(chunks: Iterable<string>, dir: string): ChunkSizes {\n  let js = 0;\n  let css = 0;\n\n  for (const chunk of chunks) {\n    const size = getGzipSize(join(dir, chunk));\n\n    if (chunk.endsWith(\".css\")) {\n      css += size;\n    } else {\n      js += size;\n    }\n  }\n\n  return { js, css };\n}\n\nfunction parseManifestEntries(entries: Record<string, string[]>): {\n  layouts: Record<string, string[]>;\n  pages: Record<string, string[]>;\n} {\n  const layouts: Record<string, string[]> = {};\n  const pages: Record<string, string[]> = {};\n\n  for (const [route, chunks] of Object.entries(entries)) {\n    if (route.endsWith(\"/layout\")) {\n      layouts[route] = chunks;\n    } else if (route.endsWith(\"/page\")) {\n      pages[route] = chunks;\n    }\n  }\n\n  return { layouts, pages };\n}\n\nfunction computeRootLayout(\n  layoutPaths: string[],\n  layouts: Record<string, string[]>,\n  sharedChunks: Set<string>,\n  nextDir: string,\n): {\n  rootLayoutPath: string | null;\n  rootLayoutChunks: Set<string>;\n  rootLayoutJs: number;\n  rootLayoutCss: number;\n} {\n  const sorted = [...layoutPaths].sort(\n    (a, b) => a.split(\"/\").length - b.split(\"/\").length,\n  );\n  const rootLayoutPath = sorted[0] ?? null;\n  const rootLayoutChunks = new Set<string>();\n  let rootLayoutJs = 0;\n  let rootLayoutCss = 0;\n\n  if (rootLayoutPath) {\n    const uniqueChunks = layouts[rootLayoutPath].filter(\n      (c) => !sharedChunks.has(c),\n    );\n    const sizes = sumChunkSizes(uniqueChunks, nextDir);\n\n    rootLayoutJs = sizes.js;\n    rootLayoutCss = sizes.css;\n    uniqueChunks.forEach((c) => rootLayoutChunks.add(c));\n  }\n\n  return { rootLayoutPath, rootLayoutChunks, rootLayoutJs, rootLayoutCss };\n}\n\nfunction computeRouteMetrics(\n  pages: Record<string, string[]>,\n  layouts: Record<string, string[]>,\n  sharedChunks: Set<string>,\n  rootLayoutPath: string | null,\n  rootLayoutChunks: Set<string>,\n  firstLoadJs: number,\n  nextDir: string,\n): Record<string, RouteMetric> {\n  const routes: Record<string, RouteMetric> = {};\n\n  for (const [route, chunks] of Object.entries(pages)) {\n    const segments = route.split(\"/\");\n\n    segments.pop(); // remove 'page'\n\n    const ancestorLayouts: string[] = [];\n\n    for (let i = segments.length; i >= 1; i--) {\n      const parentPath = `${segments.slice(0, i).join(\"/\")}/layout`;\n\n      if (layouts[parentPath]) {\n        ancestorLayouts.push(parentPath);\n      }\n    }\n\n    const routeChunks = new Set<string>();\n\n    for (const chunk of chunks.filter((c) => !sharedChunks.has(c))) {\n      if (!rootLayoutChunks.has(chunk)) {\n        routeChunks.add(chunk);\n      }\n    }\n\n    for (const layoutPath of ancestorLayouts) {\n      if (layoutPath === rootLayoutPath) continue;\n\n      for (const chunk of layouts[layoutPath].filter(\n        (c) => !sharedChunks.has(c),\n      )) {\n        if (!rootLayoutChunks.has(chunk)) {\n          routeChunks.add(chunk);\n        }\n      }\n    }\n\n    const sizes = sumChunkSizes(routeChunks, nextDir);\n\n    routes[route] = {\n      js: round1(sizes.js),\n      css: round1(sizes.css),\n      firstLoadJs: round1(firstLoadJs + sizes.js + sizes.css),\n    };\n  }\n\n  return routes;\n}\n\nfunction readTurbopackEntries(serverAppDir: string): Record<string, string[]> {\n  const entries: Record<string, string[]> = {};\n\n  function scanDir(dir: string): void {\n    const items = readdirSync(dir, { withFileTypes: true });\n\n    for (const item of items) {\n      const fullPath = join(dir, item.name);\n\n      if (item.isDirectory()) {\n        scanDir(fullPath);\n      } else if (item.name.endsWith(\"_client-reference-manifest.js\")) {\n        try {\n          const content = readFileSync(fullPath, \"utf-8\");\n          const g: Record<string, unknown> = {};\n          // eslint-disable-next-line no-new-func\n          const fn = new Function(\"globalThis\", \"self\", `${content}\\nreturn globalThis;`);\n          const result = fn(g, g) as {\n            __RSC_MANIFEST?: Record<\n              string,\n              { clientModules?: Record<string, { chunks?: string[] }> }\n            >;\n          };\n          const manifest = result.__RSC_MANIFEST;\n\n          if (!manifest) continue;\n\n          for (const [routeKey, entry] of Object.entries(manifest)) {\n            if (!routeKey.endsWith(\"/page\")) continue;\n\n            const chunks = new Set<string>();\n\n            for (const mod of Object.values(entry.clientModules ?? {})) {\n              for (const chunk of mod.chunks ?? []) {\n                // Normalize: \"/_next/static/chunks/xxx.js\" → \"static/chunks/xxx.js\"\n                chunks.add(chunk.replace(/^\\/_next\\//, \"\"));\n              }\n            }\n\n            entries[routeKey] = [...chunks];\n          }\n        } catch {\n          // Skip malformed manifest files\n        }\n      }\n    }\n  }\n\n  scanDir(serverAppDir);\n\n  return entries;\n}\n\nfunction compareReport(\n  baseline: BundleReport,\n  current: BundleReport,\n  { threshold = 5 }: CompareOptions = {},\n): string {\n  function hasChanged(base: number, curr: number): boolean {\n    if (round1(curr - base) === 0) return false;\n    const pct = base > 0 ? ((curr - base) / base) * 100 : null;\n    if (pct !== null && round1(pct) === 0) return false;\n    return true;\n  }\n\n  function formatDelta(base: number, curr: number): string {\n    const delta = curr - base;\n    const rounded = round1(delta);\n    const sign = delta >= 0 ? \"+\" : \"\";\n    const pct = base > 0 ? (delta / base) * 100 : 0;\n    const pctStr = base > 0 ? ` (${sign}${round1(pct)}%)` : \"\";\n    return `${sign}${rounded} kB${pctStr}`;\n  }\n\n  function isWarning(base: number, curr: number): boolean {\n    const delta = curr - base;\n    const pct = base > 0 ? (delta / base) * 100 : 0;\n\n    return delta > 1 && pct > threshold;\n  }\n\n  function displayRoute(route: string): string {\n    return route.replace(/^\\/\\[locale\\]/, \"\");\n  }\n\n  const lines: string[] = [];\n\n  lines.push(\"## Bundle Size Report\");\n  lines.push(\"\");\n  lines.push(\n    `Comparing against baseline from \\`${baseline.commitSha}\\` (${baseline.updatedAt}).`,\n  );\n  lines.push(\"\");\n\n  const changedMetrics = [\n    {\n      name: \"First Load JS\",\n      base: baseline.firstLoadJs,\n      curr: current.firstLoadJs,\n    },\n    { name: \"Total JS\", base: baseline.totalJs, curr: current.totalJs },\n    { name: \"Total CSS\", base: baseline.totalCss, curr: current.totalCss },\n  ].filter((m) => hasChanged(m.base, m.curr));\n\n  const allRoutes = new Set([\n    ...Object.keys(baseline.routes ?? {}),\n    ...Object.keys(current.routes ?? {}),\n  ]);\n\n  const sortedRoutes = [...allRoutes].sort();\n  const routeLines: string[] = [];\n\n  for (const route of sortedRoutes) {\n    const display = displayRoute(route);\n    const base = baseline.routes?.[route];\n    const curr = current.routes?.[route];\n\n    if (!base && curr) {\n      routeLines.push(\n        `| ${display} | -- | ${round1(curr.firstLoadJs)} kB | ✨ NEW | |`,\n      );\n    } else if (base && !curr) {\n      routeLines.push(\n        `| ${display} | ${round1(base.firstLoadJs)} kB | -- | REMOVED | |`,\n      );\n    } else if (base && curr && hasChanged(base.firstLoadJs, curr.firstLoadJs)) {\n      const d = formatDelta(base.firstLoadJs, curr.firstLoadJs);\n      const warn = isWarning(base.firstLoadJs, curr.firstLoadJs) ? \" ⚠️\" : \"\";\n\n      routeLines.push(\n        `| ${display} | ${round1(base.firstLoadJs)} kB | ${round1(curr.firstLoadJs)} kB | ${d} |${warn} |`,\n      );\n    }\n  }\n\n  if (changedMetrics.length === 0 && routeLines.length === 0) {\n    lines.push(\"No bundle size changes detected.\");\n    lines.push(\"\");\n    return lines.join(\"\\n\");\n  }\n\n  if (changedMetrics.length > 0) {\n    lines.push(\"| Metric | Baseline | Current | Delta | |\");\n    lines.push(\"|:-------|:---------|:--------|:------|:-|\");\n\n    for (const m of changedMetrics) {\n      const d = formatDelta(m.base, m.curr);\n      const warn = isWarning(m.base, m.curr) ? \" ⚠️\" : \"\";\n\n      lines.push(\n        `| ${m.name} | ${round1(m.base)} kB | ${round1(m.curr)} kB | ${d} |${warn} |`,\n      );\n    }\n\n    lines.push(\"\");\n  }\n\n  lines.push(\"### Per-Route First Load JS\");\n  lines.push(\"\");\n\n  if (routeLines.length > 0) {\n    lines.push(\"| Route | Baseline | Current | Delta | |\");\n    lines.push(\"|:------|:---------|:--------|:------|:-|\");\n    lines.push(...routeLines);\n    lines.push(\"\");\n    lines.push(\n      `> Threshold: ${threshold}% increase. Routes with ⚠️ exceed the threshold.`,\n    );\n  } else {\n    lines.push(\"_No route changes detected._\");\n  }\n\n  lines.push(\"\");\n\n  return lines.join(\"\\n\");\n}\n\nfunction generate(\n  nextDir: string,\n  values: Record<string, string | undefined>,\n): void {\n  const appManifestPath = join(nextDir, \"app-build-manifest.json\");\n  const buildManifestPath = join(nextDir, \"build-manifest.json\");\n  const serverAppDir = join(nextDir, \"server\", \"app\");\n\n  const isWebpack = existsSync(appManifestPath);\n  const isTurbopack = !isWebpack && existsSync(serverAppDir);\n\n  if (!isWebpack && !isTurbopack) {\n    console.error(\n      \"Error: No build output found (.next/app-build-manifest.json or .next/server/app/). Run `next build` first.\",\n    );\n    process.exit(1);\n  }\n\n  const buildManifest = JSON.parse(\n    readFileSync(buildManifestPath, \"utf-8\"),\n  ) as {\n    rootMainFiles?: string[];\n    polyfillFiles?: string[];\n  };\n\n  const rootMainFiles = new Set(buildManifest.rootMainFiles ?? []);\n  const polyfillFiles = new Set(buildManifest.polyfillFiles ?? []);\n  const sharedChunks = new Set([...rootMainFiles, ...polyfillFiles]);\n\n  let entries: Record<string, string[]>;\n\n  if (isWebpack) {\n    const appManifest = JSON.parse(readFileSync(appManifestPath, \"utf-8\")) as {\n      pages?: Record<string, string[]>;\n    };\n\n    entries = appManifest.pages ?? {};\n  } else {\n    entries = readTurbopackEntries(serverAppDir);\n  }\n  const { layouts, pages } = parseManifestEntries(entries);\n\n  // Shared JS = sum of rootMainFiles gzipped sizes\n  const sharedSizes = sumChunkSizes(rootMainFiles, nextDir);\n  const sharedJs = round1(sharedSizes.js);\n\n  // Root layout\n  const { rootLayoutPath, rootLayoutChunks, rootLayoutJs, rootLayoutCss } =\n    computeRootLayout(Object.keys(layouts), layouts, sharedChunks, nextDir);\n\n  const sharedCss = round1(rootLayoutCss);\n  const firstLoadJs = round1(sharedJs + rootLayoutJs + rootLayoutCss);\n\n  // Total JS and CSS across all unique chunks\n  const allChunksSet = new Set<string>();\n\n  for (const chunks of Object.values(entries)) {\n    for (const chunk of chunks) {\n      allChunksSet.add(chunk);\n    }\n  }\n\n  const totals = sumChunkSizes(allChunksSet, nextDir);\n  const totalJs = round1(totals.js);\n  const totalCss = round1(totals.css);\n\n  // Per-route metrics\n  const routes = computeRouteMetrics(\n    pages,\n    layouts,\n    sharedChunks,\n    rootLayoutPath,\n    rootLayoutChunks,\n    firstLoadJs,\n    nextDir,\n  );\n\n  const result: BundleReport = {\n    commitSha: values.sha ?? \"unknown\",\n    updatedAt: new Date().toISOString().split(\"T\")[0],\n    firstLoadJs,\n    shared: { js: sharedJs, css: sharedCss },\n    routes,\n    totalJs,\n    totalCss,\n  };\n\n  const output = values.output ?? null;\n  const json = `${JSON.stringify(result, null, 2)}\\n`;\n\n  if (output) {\n    writeFileSync(resolve(output), json);\n    console.error(`Bundle size report written to ${output}`);\n  } else {\n    process.stdout.write(json);\n  }\n}\n\nfunction compare(\n  nextDir: string,\n  values: Record<string, string | undefined>,\n): void {\n  const baselinePath = resolve(\n    values.baseline ?? join(CORE_DIR, \"bundle-baseline.json\"),\n  );\n  const currentPath = resolve(values.current ?? \"\");\n  const threshold = Number(values.threshold ?? \"5\");\n\n  if (!currentPath || !existsSync(currentPath)) {\n    console.error(\"Error: --current <path> is required and must exist\");\n    process.exit(1);\n  }\n\n  if (!existsSync(baselinePath)) {\n    console.error(`Error: baseline not found at ${baselinePath}`);\n    process.exit(1);\n  }\n\n  const baseline = JSON.parse(\n    readFileSync(baselinePath, \"utf-8\"),\n  ) as BundleReport;\n  const current = JSON.parse(\n    readFileSync(currentPath, \"utf-8\"),\n  ) as BundleReport;\n\n  process.stdout.write(compareReport(baseline, current, { threshold }));\n}\n\nexport {\n  round1,\n  getGzipSize,\n  sumChunkSizes,\n  parseManifestEntries,\n  computeRootLayout,\n  computeRouteMetrics,\n  compareReport,\n  clearSizeCache,\n  readTurbopackEntries,\n};\n\nexport type { BundleReport, RouteMetric, ChunkSizes, CompareOptions };\n\nconst isMain = process.argv[1] === fileURLToPath(import.meta.url);\n\nif (isMain) {\n  const { values, positionals } = parseArgs({\n    allowPositionals: true,\n    options: {\n      output: { type: \"string\" },\n      baseline: { type: \"string\" },\n      current: { type: \"string\" },\n      threshold: { type: \"string\" },\n      sha: { type: \"string\" },\n      dir: { type: \"string\" },\n    },\n  });\n\n  const NEXT_DIR = values.dir ? resolve(values.dir) : join(CORE_DIR, \".next\");\n  const command = positionals.at(0);\n\n  if (command === \"generate\") {\n    generate(NEXT_DIR, values);\n  } else if (command === \"compare\") {\n    compare(NEXT_DIR, values);\n  } else {\n    console.error(\"Usage: bundle-size.mts <generate|compare> [options]\");\n    console.error(\"\");\n    console.error(\"Commands:\");\n    console.error(\n      \"  generate  Analyze .next/ build output and produce bundle size JSON\",\n    );\n    console.error(\"    --output <path>  Write JSON to file instead of stdout\");\n    console.error(\"\");\n    console.error(\"  compare   Compare current bundle against a baseline\");\n    console.error(\n      \"    --baseline <path>  Path to baseline JSON (default: ./bundle-baseline.json)\",\n    );\n    console.error(\n      \"    --current <path>   Path to current bundle JSON (required)\",\n    );\n    console.error(\n      \"    --threshold <n>    Warning threshold percentage (default: 5)\",\n    );\n    process.exit(1);\n  }\n}\n"
  },
  {
    "path": ".github/scripts/compare-unlighthouse.mts",
    "content": "#!/usr/bin/env node\n/* eslint-disable no-console, no-restricted-syntax, no-plusplus, no-continue */\n\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\nimport { resolve } from \"node:path\";\n\ninterface CiResult {\n  summary: {\n    score: number;\n    categories: Record<string, { score: number }>;\n    metrics: Record<string, { displayValue: string }>;\n  };\n}\n\nfunction loadCiResult(filePath: string): CiResult {\n  if (!existsSync(filePath)) {\n    console.error(`Error: file not found: ${filePath}`);\n    process.exit(1);\n  }\n\n  return JSON.parse(readFileSync(filePath, \"utf-8\")) as CiResult;\n}\n\nfunction score(value: number): string {\n  return String(Math.round(value * 100));\n}\n\nfunction row(\n  label: string,\n  prodDesktop: string,\n  prodMobile: string,\n  prevDesktop: string,\n  prevMobile: string,\n): string {\n  return `| ${label} | ${prodDesktop} | ${prodMobile} | ${prevDesktop} | ${prevMobile} |`;\n}\n\nconst CATEGORY_ORDER = [\"performance\", \"accessibility\", \"best-practices\", \"seo\"];\n\nconst CATEGORY_LABELS: Record<string, string> = {\n  performance: \"Performance\",\n  accessibility: \"Accessibility\",\n  \"best-practices\": \"Best Practices\",\n  seo: \"SEO\",\n};\n\nconst METRIC_ORDER = [\n  \"largest-contentful-paint\",\n  \"cumulative-layout-shift\",\n  \"first-contentful-paint\",\n  \"total-blocking-time\",\n  \"max-potential-fid\",\n  \"interactive\",\n];\n\nconst METRIC_LABELS: Record<string, string> = {\n  \"largest-contentful-paint\": \"LCP\",\n  \"cumulative-layout-shift\": \"CLS\",\n  \"first-contentful-paint\": \"FCP\",\n  \"total-blocking-time\": \"TBT\",\n  \"max-potential-fid\": \"Max Potential FID\",\n  interactive: \"Time to Interactive\",\n};\n\nconst COL_HEADER =\n  \"| | Prod Desktop | Prod Mobile | Preview Desktop | Preview Mobile |\";\nconst COL_SEP =\n  \"|:-|:------------|:------------|:----------------|:---------------|\";\n\nfunction compareResults(\n  productionDesktop: CiResult,\n  productionMobile: CiResult,\n  previewDesktop: CiResult,\n  previewMobile: CiResult,\n  threshold: number,\n  provider?: string,\n): { markdown: string; hasChanges: boolean } {\n  const thresholdDecimal = threshold / 100;\n\n  // hasChanges: any summary or category score pair differs by >= threshold\n  let hasChanges =\n    Math.abs(previewDesktop.summary.score - productionDesktop.summary.score) >=\n      thresholdDecimal ||\n    Math.abs(previewMobile.summary.score - productionMobile.summary.score) >=\n      thresholdDecimal;\n\n  if (!hasChanges) {\n    for (const id of CATEGORY_ORDER) {\n      const deltaDesktop = Math.abs(\n        (previewDesktop.summary.categories[id]?.score ?? 0) -\n          (productionDesktop.summary.categories[id]?.score ?? 0),\n      );\n      const deltaMobile = Math.abs(\n        (previewMobile.summary.categories[id]?.score ?? 0) -\n          (productionMobile.summary.categories[id]?.score ?? 0),\n      );\n\n      if (deltaDesktop >= thresholdDecimal || deltaMobile >= thresholdDecimal) {\n        hasChanges = true;\n        break;\n      }\n    }\n  }\n\n  const lines: string[] = [];\n\n  const providerLabel = provider\n    ? ` — ${provider.charAt(0).toUpperCase()}${provider.slice(1)}`\n    : \"\";\n\n  lines.push(`## Unlighthouse Performance Comparison${providerLabel}`);\n  lines.push(\n    \"Comparing PR preview deployment Unlighthouse scores vs production Unlighthouse scores.\",\n  );\n  lines.push(\"\");\n\n  lines.push(\"### Summary Score\");\n  lines.push(\n    \"_Aggregate score across all categories as reported by Unlighthouse._\",\n  );\n  lines.push(\"\");\n  lines.push(COL_HEADER);\n  lines.push(COL_SEP);\n  lines.push(\n    row(\n      \"Score\",\n      score(productionDesktop.summary.score),\n      score(productionMobile.summary.score),\n      score(previewDesktop.summary.score),\n      score(previewMobile.summary.score),\n    ),\n  );\n  lines.push(\"\");\n\n  lines.push(\"### Category Scores\");\n  lines.push(\"\");\n  lines.push(\n    \"| Category | Prod Desktop | Prod Mobile | Preview Desktop | Preview Mobile |\",\n  );\n  lines.push(\n    \"|:---------|:------------|:------------|:----------------|:---------------|\",\n  );\n\n  for (const id of CATEGORY_ORDER) {\n    lines.push(\n      row(\n        CATEGORY_LABELS[id] ?? id,\n        score(productionDesktop.summary.categories[id]?.score ?? 0),\n        score(productionMobile.summary.categories[id]?.score ?? 0),\n        score(previewDesktop.summary.categories[id]?.score ?? 0),\n        score(previewMobile.summary.categories[id]?.score ?? 0),\n      ),\n    );\n  }\n\n  lines.push(\"\");\n\n  lines.push(\"### Core Web Vitals\");\n  lines.push(\"\");\n  lines.push(\n    \"| Metric | Prod Desktop | Prod Mobile | Preview Desktop | Preview Mobile |\",\n  );\n  lines.push(\n    \"|:-------|:------------|:------------|:----------------|:---------------|\",\n  );\n\n  for (const id of METRIC_ORDER) {\n    lines.push(\n      row(\n        METRIC_LABELS[id] ?? id,\n        productionDesktop.summary.metrics[id]?.displayValue ?? \"—\",\n        productionMobile.summary.metrics[id]?.displayValue ?? \"—\",\n        previewDesktop.summary.metrics[id]?.displayValue ?? \"—\",\n        previewMobile.summary.metrics[id]?.displayValue ?? \"—\",\n      ),\n    );\n  }\n\n  lines.push(\"\");\n\n  return { markdown: lines.join(\"\\n\"), hasChanges };\n}\n\nexport { compareResults };\nexport type { CiResult };\n\nconst isMain = process.argv[1] === fileURLToPath(import.meta.url);\n\nif (isMain) {\n  const { values } = parseArgs({\n    options: {\n      \"preview-desktop\": { type: \"string\" },\n      \"preview-mobile\": { type: \"string\" },\n      \"production-desktop\": { type: \"string\" },\n      \"production-mobile\": { type: \"string\" },\n      output: { type: \"string\" },\n      \"meta-output\": { type: \"string\" },\n      threshold: { type: \"string\" },\n      provider: { type: \"string\" },\n    },\n  });\n\n  const previewDesktopPath = values[\"preview-desktop\"] ?? \"\";\n  const previewMobilePath = values[\"preview-mobile\"] ?? \"\";\n  const productionDesktopPath = values[\"production-desktop\"] ?? \"\";\n  const productionMobilePath = values[\"production-mobile\"] ?? \"\";\n\n  if (\n    !previewDesktopPath ||\n    !previewMobilePath ||\n    !productionDesktopPath ||\n    !productionMobilePath\n  ) {\n    console.error(\n      \"Usage: compare-unlighthouse.mts --preview-desktop <path> --preview-mobile <path> --production-desktop <path> --production-mobile <path> [--output <path>] [--meta-output <path>] [--threshold <n>] [--provider <name>]\",\n    );\n    process.exit(1);\n  }\n\n  const threshold = Number(values.threshold ?? \"1\");\n\n  const previewDesktop = loadCiResult(resolve(previewDesktopPath));\n  const previewMobile = loadCiResult(resolve(previewMobilePath));\n  const productionDesktop = loadCiResult(resolve(productionDesktopPath));\n  const productionMobile = loadCiResult(resolve(productionMobilePath));\n\n  const { markdown, hasChanges } = compareResults(\n    productionDesktop,\n    productionMobile,\n    previewDesktop,\n    previewMobile,\n    threshold,\n    values.provider,\n  );\n\n  const outputPath = values.output ? resolve(values.output) : null;\n  const metaOutputPath = values[\"meta-output\"]\n    ? resolve(values[\"meta-output\"])\n    : null;\n\n  if (outputPath) {\n    writeFileSync(outputPath, markdown);\n    console.error(`Unlighthouse comparison report written to ${outputPath}`);\n  } else {\n    process.stdout.write(markdown);\n  }\n\n  if (metaOutputPath) {\n    writeFileSync(\n      metaOutputPath,\n      `${JSON.stringify({ hasChanges }, null, 2)}\\n`,\n    );\n    console.error(`Meta output written to ${metaOutputPath}`);\n  }\n}\n"
  },
  {
    "path": ".github/scripts/post-bundle-comment.js",
    "content": "const fs = require('fs');\n\nmodule.exports = async ({ github, context, reportPath = '/tmp/bundle-report.md' }) => {\n  const marker = '<!-- bundle-size-report -->';\n  const body = marker + '\\n' + fs.readFileSync(reportPath, 'utf-8');\n\n  const { data: comments } = await github.rest.issues.listComments({\n    owner: context.repo.owner,\n    repo: context.repo.repo,\n    issue_number: context.issue.number,\n  });\n\n  const existing = comments.find(c => c.body.includes(marker));\n\n  if (existing) {\n    await github.rest.issues.updateComment({\n      owner: context.repo.owner,\n      repo: context.repo.repo,\n      comment_id: existing.id,\n      body,\n    });\n  } else {\n    await github.rest.issues.createComment({\n      owner: context.repo.owner,\n      repo: context.repo.repo,\n      issue_number: context.issue.number,\n      body,\n    });\n  }\n};\n"
  },
  {
    "path": ".github/scripts/post-unlighthouse-commit-comment.js",
    "content": "const fs = require('fs');\n\nmodule.exports = async ({ github, context, reportPath = '/tmp/unlighthouse-report.md' }) => {\n  const sha = context.payload.deployment?.sha;\n\n  if (!sha) return;\n\n  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;\n  const marker = `<!-- canary-lighthouse-report -->`;\n  const body = marker + '\\n' + fs.readFileSync(reportPath, 'utf-8') + `\\n[Full Unlighthouse report →](${runUrl})\\n`;\n\n  await github.rest.repos.createCommitComment({\n    owner: context.repo.owner,\n    repo: context.repo.repo,\n    commit_sha: sha,\n    body,\n  });\n};\n"
  },
  {
    "path": ".github/scripts/post-unlighthouse-pr-comment.js",
    "content": "const fs = require('fs');\n\nmodule.exports = async ({ github, context, provider = 'unknown', reportPath = '/tmp/unlighthouse-report.md', metaPath = '/tmp/unlighthouse-meta.json' }) => {\n  // Exit early if no changes\n  const { hasChanges } = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));\n\n  if (!hasChanges) return;\n\n  // Find PR from commit SHA (context.issue.number is 0 in deployment_status events;\n  // deployment.ref is also the SHA in Vercel deployments, not the branch name)\n  const sha = context.payload.deployment?.sha;\n\n  if (!sha) return;\n\n  const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({\n    owner: context.repo.owner,\n    repo: context.repo.repo,\n    commit_sha: sha,\n  });\n\n  const prNumber = prs[0]?.number;\n\n  if (!prNumber) return;\n\n  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;\n  const marker = `<!-- unlighthouse-${provider}-report -->`;\n  const body = marker + '\\n' + fs.readFileSync(reportPath, 'utf-8') + `\\n[Full Unlighthouse report →](${runUrl})\\n`;\n\n  const { data: comments } = await github.rest.issues.listComments({\n    owner: context.repo.owner,\n    repo: context.repo.repo,\n    issue_number: prNumber,\n  });\n\n  const existing = comments.find(c => c.body.includes(marker));\n\n  if (existing) {\n    await github.rest.issues.updateComment({\n      owner: context.repo.owner,\n      repo: context.repo.repo,\n      comment_id: existing.id,\n      body,\n    });\n  } else {\n    await github.rest.issues.createComment({\n      owner: context.repo.owner,\n      repo: context.repo.repo,\n      issue_number: prNumber,\n      body,\n    });\n  }\n};\n"
  },
  {
    "path": ".github/scripts/prevent-invalid-changesets.js",
    "content": "const fs = require(\"fs\");\n\nmodule.exports = async ({ core, exec }) => {\n  try {\n    await exec.exec(\"git\", [\n      \"fetch\",\n      \"https://github.com/bigcommerce/catalyst.git\",\n      \"integrations/makeswift\",\n    ]);\n\n    const { stdout } = await exec.getExecOutput(\"git\", [\n      \"diff\",\n      \"--name-only\",\n      `origin/integrations/makeswift...HEAD`,\n    ]);\n\n    const allFilenames = stdout.split(\"\\n\").filter((line) => line.trim());\n    const changesetFilenames = allFilenames.filter(\n      (file) => file.startsWith(\".changeset/\") && file.endsWith(\".md\"),\n    );\n\n    if (changesetFilenames.length === 0) {\n      core.info(\"No changeset files found to validate\");\n      return;\n    }\n\n    core.info(`Found ${changesetFilenames.length} changeset files to validate`);\n\n    for (const filename of changesetFilenames) {\n      core.info(`Checking ${filename}...`);\n\n      // .changeset/*.md filenames should only contain alphanumeric characters, hyphens, and underscores\n      if (!/^\\.changeset\\/[a-zA-Z0-9_-]+\\.md$/.test(filename)) {\n        core.setFailed(`Invalid filename pattern: ${filename}`);\n        return;\n      }\n\n      // extra defense against path traversal attacks\n      if (\n        filename.includes(\"..\") ||\n        (filename.includes(\"/\") && !filename.startsWith(\".changeset/\"))\n      ) {\n        core.setFailed(`Suspicious file path: ${filename}`);\n        return;\n      }\n\n      if (!fs.existsSync(filename)) {\n        core.warning(\n          `File not found: ${filename}. This is likely a version PR where the changeset was already consumed. Skipping validation for this file.`,\n        );\n        continue;\n      }\n\n      // check file size (limit to 100KB)\n      const stats = fs.statSync(filename);\n      if (stats.size > 102400) {\n        core.error(`File too large`, { file: filename });\n        core.setFailed(`File ${filename} is too large`);\n        return;\n      }\n\n      if (stats.isSymbolicLink()) {\n        core.error(`Symlinks are not allowed`, { file: filename });\n        core.setFailed(`File ${filename} is a symlink`);\n        return;\n      }\n\n      const content = fs.readFileSync(filename, \"utf8\");\n\n      // starts with \"---\", captures everything until the next \"---\"\n      const frontmatterMatch = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n\n      if (!frontmatterMatch) {\n        core.error(`Failed to extract frontmatter or file has no frontmatter`, {\n          file: filename,\n        });\n        core.setFailed(`File ${filename} has invalid or missing frontmatter`);\n        return;\n      }\n\n      const frontmatter = frontmatterMatch[1];\n\n      // extract all packages starting with \"@bigcommerce/\n      const packageMatches = frontmatter.match(/\"@bigcommerce\\/[^\"]+\"/g);\n\n      if (packageMatches) {\n        const invalidPackages = packageMatches.filter(\n          (pkg) => pkg !== '\"@bigcommerce/catalyst-makeswift\"',\n        );\n\n        if (invalidPackages.length > 0) {\n          core.error(\n            `Invalid package found in changeset file. Only @bigcommerce/catalyst-makeswift is allowed.`,\n            { file: filename },\n          );\n          core.setFailed(\n            `File ${filename} contains invalid packages: ${invalidPackages.join(\n              \", \",\n            )}`,\n          );\n          return;\n        }\n      }\n    }\n\n    core.info(\"All changeset files validated successfully\");\n  } catch (error) {\n    core.setFailed(`Validation failed: ${error.message}`);\n  }\n};\n"
  },
  {
    "path": ".github/workflows/basic.yml",
    "content": "name: Basic\n\non:\n  push:\n    branches: [canary, integrations/makeswift, integrations/b2b-makeswift]\n  pull_request:\n    types: [opened, synchronize]\n  merge_group:\n    types: [checks_requested]\n\nenv:\n  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n  TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n  TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}\n  BIGCOMMERCE_STORE_HASH: ${{ vars.BIGCOMMERCE_STORE_HASH }}\n  BIGCOMMERCE_CHANNEL_ID: ${{ vars.BIGCOMMERCE_CHANNEL_ID }}\n  BIGCOMMERCE_CLIENT_ID: ${{ secrets.BIGCOMMERCE_CLIENT_ID }}\n  BIGCOMMERCE_CLIENT_SECRET: ${{ secrets.BIGCOMMERCE_CLIENT_SECRET }}\n  BIGCOMMERCE_STOREFRONT_TOKEN: ${{ secrets.BIGCOMMERCE_STOREFRONT_TOKEN }}\n  BIGCOMMERCE_ACCESS_TOKEN: ${{ secrets.BIGCOMMERCE_ACCESS_TOKEN }}\n  TEST_CUSTOMER_ID: ${{ vars.TEST_CUSTOMER_ID }}\n  TEST_CUSTOMER_EMAIL: ${{ vars.TEST_CUSTOMER_EMAIL }}\n  TEST_CUSTOMER_PASSWORD: ${{ secrets.TEST_CUSTOMER_PASSWORD }}\n  TESTS_FALLBACK_LOCALE: ${{ vars.TESTS_FALLBACK_LOCALE }}\n  TESTS_READ_ONLY: ${{ vars.TESTS_READ_ONLY }}\n  DEFAULT_PRODUCT_ID: ${{ vars.DEFAULT_PRODUCT_ID }}\n  DEFAULT_COMPLEX_PRODUCT_ID: ${{ vars.DEFAULT_COMPLEX_PRODUCT_ID }}\n\njobs:\n  lint-typecheck:\n    name: Lint, Typecheck, and gql.tada\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 2\n\n      - uses: pnpm/action-setup@v3\n\n      - name: Use Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: \".nvmrc\"\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Gql Tada\n        run: pnpm run -r generate\n\n      - name: Lint\n        run: pnpm run lint -- --max-warnings=0\n\n      - name: Typecheck\n        run: pnpm run typecheck\n\n  cli-tests:\n    name: CLI Tests\n\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 2\n\n      - uses: pnpm/action-setup@v3\n\n      - name: Use Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: \".nvmrc\"\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Run Tests\n        run: pnpm run test\n"
  },
  {
    "path": ".github/workflows/bundle-size.yml",
    "content": "name: Bundle Size\n# Reports the bundle size impact of a PR by comparing the current build against\n# a live build of the base branch (canary or integrations/makeswift).\n#\n# build-pr and build-baseline run in parallel, each uploading a JSON artifact.\n# compare downloads both artifacts, runs the comparison, and posts the PR comment.\n\non:\n  pull_request:\n    types: [opened, synchronize]\n\nenv:\n  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n  TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n  TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}\n  BIGCOMMERCE_STORE_HASH: ${{ vars.BIGCOMMERCE_STORE_HASH }}\n  BIGCOMMERCE_CHANNEL_ID: ${{ vars.BIGCOMMERCE_CHANNEL_ID }}\n  BIGCOMMERCE_CLIENT_ID: ${{ secrets.BIGCOMMERCE_CLIENT_ID }}\n  BIGCOMMERCE_CLIENT_SECRET: ${{ secrets.BIGCOMMERCE_CLIENT_SECRET }}\n  BIGCOMMERCE_STOREFRONT_TOKEN: ${{ secrets.BIGCOMMERCE_STOREFRONT_TOKEN }}\n  BIGCOMMERCE_ACCESS_TOKEN: ${{ secrets.BIGCOMMERCE_ACCESS_TOKEN }}\n\njobs:\n  build-pr:\n    name: Build & Measure PR Bundle\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          path: pr\n\n      - uses: pnpm/action-setup@v4\n        with:\n          package_json_file: pr/package.json\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version-file: pr/.nvmrc\n          cache: pnpm\n          cache-dependency-path: pr/pnpm-lock.yaml\n\n      - run: pnpm install --frozen-lockfile\n        working-directory: pr\n\n      - run: pnpm build\n        working-directory: pr\n\n      - run: node .github/scripts/bundle-size.mts generate --output /tmp/bundle-current.json --sha ${{ github.sha }}\n        working-directory: pr\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: bundle-current\n          path: /tmp/bundle-current.json\n\n  build-baseline:\n    name: Build & Measure Baseline Bundle\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          path: pr\n\n      - name: Detect baseline branch\n        id: baseline\n        run: |\n          PKG_NAME=$(node -p \"require('./pr/core/package.json').name\")\n          if [ \"$PKG_NAME\" = \"@bigcommerce/catalyst-makeswift\" ]; then\n            echo \"branch=integrations/makeswift\" >> $GITHUB_OUTPUT\n          else\n            echo \"branch=canary\" >> $GITHUB_OUTPUT\n          fi\n\n      - uses: actions/checkout@v4\n        with:\n          ref: ${{ steps.baseline.outputs.branch }}\n          path: baseline\n\n      - uses: pnpm/action-setup@v4\n        with:\n          package_json_file: pr/package.json\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version-file: pr/.nvmrc\n          cache: pnpm\n          cache-dependency-path: baseline/pnpm-lock.yaml\n\n      - run: pnpm install --frozen-lockfile\n        working-directory: baseline\n\n      - run: pnpm build\n        working-directory: baseline\n\n      - name: Generate baseline bundle size\n        run: |\n          SHA=$(git -C $GITHUB_WORKSPACE/baseline rev-parse --short HEAD)\n          node .github/scripts/bundle-size.mts generate --dir $GITHUB_WORKSPACE/baseline/core/.next --output /tmp/bundle-baseline.json --sha $SHA\n        working-directory: pr\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: bundle-baseline\n          path: /tmp/bundle-baseline.json\n\n  compare:\n    name: Compare Bundles & Post Report\n    needs: [build-pr, build-baseline]\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: write\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          path: pr\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version-file: pr/.nvmrc\n\n      - uses: actions/download-artifact@v4\n        with:\n          pattern: bundle-*\n          path: /tmp\n          merge-multiple: true\n\n      - run: node .github/scripts/bundle-size.mts compare --baseline /tmp/bundle-baseline.json --current /tmp/bundle-current.json > /tmp/bundle-report.md\n        working-directory: pr\n\n      - run: cat /tmp/bundle-report.md >> \"$GITHUB_STEP_SUMMARY\"\n\n      - uses: actions/github-script@v7\n        with:\n          script: |\n            const postComment = require('./pr/.github/scripts/post-bundle-comment.js')\n            await postComment({ github, context })\n"
  },
  {
    "path": ".github/workflows/changesets-release.yml",
    "content": "name: Changesets Release\n\non:\n  push:\n    branches:\n      - canary\n      - integrations/makeswift\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\npermissions:\n  id-token: write\n  contents: write\n  packages: write\n  pull-requests: write\n\njobs:\n  changesets-release:\n    name: Changesets Release\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Repo\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v3\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: \".nvmrc\"\n          cache: \"pnpm\"\n\n      - name: Install Dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Build Packages\n        run: pnpm --filter \"./packages/**\" build\n        env:\n          CLI_SEGMENT_WRITE_KEY: ${{ secrets.CLI_SEGMENT_WRITE_KEY }}\n\n      - name: Create Release Pull Request or Publish to npm\n        id: changesets\n        uses: changesets/action@v1\n        with:\n          publish: pnpm exec changeset publish\n          title: \"Version Packages (`${{ github.ref_name }}`)\"\n          commit: \"Version Packages (`${{ github.ref_name }}`)\"\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Production Tag Deployment\nenv:\n    # secrets is for dependabot compatibility; prefer vars when available\n    VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID != '' && vars.VERCEL_ORG_ID || secrets.VERCEL_ORG_ID }}\n    VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID != '' && vars.VERCEL_PROJECT_ID || secrets.VERCEL_PROJECT_ID }}\non:\n    push:\n        tags:\n            - \"@bigcommerce/catalyst-core@latest\"\n            - \"@bigcommerce/catalyst-makeswift@latest\"\n            - \"@bigcommerce/catalyst-b2b-makeswift@latest\"\njobs:\n    deploy-tag:\n        name: Deploy `${{ github.ref_name }}` tag\n        runs-on: ubuntu-latest\n        concurrency:\n            group: ${{ github.workflow }}-${{ github.ref_name }}\n        steps:\n            - uses: actions/checkout@v4\n\n            - name: Install Vercel CLI\n              run: npm install --global vercel@latest\n\n            - name: Configure catalyst-core deployment\n              if: contains(github.ref_name, 'catalyst-core@')\n              run: |\n                  echo \"DOMAIN=catalyst-demo.site\" >> $GITHUB_ENV\n                  echo \"CHANNEL_ID=${{ vars.CORE_BIGCOMMERCE_CHANNEL_ID }}\" >> $GITHUB_ENV\n                  echo \"STOREFRONT_TOKEN=${{ secrets.CORE_BIGCOMMERCE_STOREFRONT_TOKEN }}\" >> $GITHUB_ENV\n\n            - name: Configure catalyst-makeswift deployment\n              if: contains(github.ref_name, 'catalyst-makeswift@')\n              run: |\n                  echo \"DOMAIN=makeswift.catalyst-demo.site\" >> $GITHUB_ENV\n                  echo \"CHANNEL_ID=${{ vars.MAKESWIFT_BIGCOMMERCE_CHANNEL_ID }}\" >> $GITHUB_ENV\n                  echo \"STOREFRONT_TOKEN=${{ secrets.MAKESWIFT_BIGCOMMERCE_STOREFRONT_TOKEN }}\" >> $GITHUB_ENV\n                  echo \"MAKESWIFT_KEY=${{ secrets.MAKESWIFT_SITE_API_KEY }}\" >> $GITHUB_ENV\n\n            - name: Configure catalyst-b2b-makeswift deployment\n              if: contains(github.ref_name, 'catalyst-b2b-makeswift@')\n              run: |\n                  echo \"DOMAIN=b2b-makeswift.catalyst-demo.site\" >> $GITHUB_ENV\n                  echo \"CHANNEL_ID=${{ vars.B2B_MAKESWIFT_BIGCOMMERCE_CHANNEL_ID }}\" >> $GITHUB_ENV\n                  echo \"STOREFRONT_TOKEN=${{ secrets.B2B_MAKESWIFT_BIGCOMMERCE_STOREFRONT_TOKEN }}\" >> $GITHUB_ENV\n                  echo \"MAKESWIFT_KEY=${{ secrets.B2B_MAKESWIFT_SITE_API_KEY }}\" >> $GITHUB_ENV\n                  echo \"B2B_API_HOST=${{ vars.B2B_API_HOST }}\" >> $GITHUB_ENV\n                  echo \"BIGCOMMERCE_ACCESS_TOKEN=${{ secrets.B2B_BIGCOMMERCE_ACCESS_TOKEN }}\" >> $GITHUB_ENV\n\n            - name: Deploy to Vercel\n              id: deploy\n              timeout-minutes: 15\n              env:\n                  VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}\n              run: |\n                  DEPLOY_ARGS=(\n                    --token=\"$VERCEL_TOKEN\"\n                    --env BIGCOMMERCE_CHANNEL_ID=\"$CHANNEL_ID\"\n                    --env BIGCOMMERCE_STOREFRONT_TOKEN=\"$STOREFRONT_TOKEN\"\n                  )\n\n                  if [[ -n \"$MAKESWIFT_KEY\" ]]; then\n                    DEPLOY_ARGS+=(--env MAKESWIFT_SITE_API_KEY=\"$MAKESWIFT_KEY\")\n                  fi\n\n                  if [[ -n \"$B2B_API_HOST\" ]]; then\n                    DEPLOY_ARGS+=(--env B2B_API_HOST=\"$B2B_API_HOST\")\n                  fi\n\n                  if [[ -n \"$BIGCOMMERCE_ACCESS_TOKEN\" ]]; then\n                    DEPLOY_ARGS+=(--env BIGCOMMERCE_ACCESS_TOKEN=\"$BIGCOMMERCE_ACCESS_TOKEN\")\n                  fi\n\n                  DEPLOYMENT_URL=$(vercel deploy --scope=\"${{ vars.VERCEL_TEAM_SLUG }}\" \"${DEPLOY_ARGS[@]}\")\n                  echo \"deployment_url=$DEPLOYMENT_URL\" >> $GITHUB_OUTPUT\n\n            - name: Set Vercel Domain Alias\n              timeout-minutes: 5\n              env:\n                  VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}\n              run: |\n                  vercel alias ${{ steps.deploy.outputs.deployment_url }} $DOMAIN --scope=\"${{ vars.VERCEL_TEAM_SLUG }}\" --token=\"$VERCEL_TOKEN\"\n"
  },
  {
    "path": ".github/workflows/e2e.yml",
    "content": "name: E2E Tests\n\non:\n  pull_request:\n    types: [opened, synchronize]\n    branches: [canary, integrations/makeswift, integrations/b2b-makeswift]\n\nenv:\n  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n  TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n  TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}\n  BIGCOMMERCE_STORE_HASH: ${{ vars.BIGCOMMERCE_STORE_HASH }}\n  BIGCOMMERCE_CHANNEL_ID: ${{ vars.BIGCOMMERCE_CHANNEL_ID }}\n  BIGCOMMERCE_CLIENT_ID: ${{ secrets.BIGCOMMERCE_CLIENT_ID }}\n  BIGCOMMERCE_CLIENT_SECRET: ${{ secrets.BIGCOMMERCE_CLIENT_SECRET }}\n  BIGCOMMERCE_STOREFRONT_TOKEN: ${{ secrets.BIGCOMMERCE_STOREFRONT_TOKEN }}\n  BIGCOMMERCE_ACCESS_TOKEN: ${{ secrets.BIGCOMMERCE_ACCESS_TOKEN }}\n  TEST_CUSTOMER_ID: ${{ vars.TEST_CUSTOMER_ID }}\n  TEST_CUSTOMER_EMAIL: ${{ vars.TEST_CUSTOMER_EMAIL }}\n  TEST_CUSTOMER_PASSWORD: ${{ secrets.TEST_CUSTOMER_PASSWORD }}\n  TESTS_FALLBACK_LOCALE: ${{ vars.TESTS_FALLBACK_LOCALE }}\n  TESTS_READ_ONLY: ${{ vars.TESTS_READ_ONLY }}\n  DEFAULT_PRODUCT_ID: ${{ vars.DEFAULT_PRODUCT_ID }}\n  DEFAULT_COMPLEX_PRODUCT_ID: ${{ vars.DEFAULT_COMPLEX_PRODUCT_ID }}\n\njobs:\n  e2e-tests:\n    name: E2E Functional Tests (${{ matrix.name }})\n\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        include:\n          - name: default\n            browsers: chromium webkit\n            test-filter: tests/ui/e2e\n            trailing-slash: true\n            locale-var: TESTS_LOCALE\n            artifact-name: playwright-report\n          - name: TRAILING_SLASH=false\n            browsers: chromium\n            test-filter: tests/ui/e2e --grep @no-trailing-slash\n            trailing-slash: false\n            locale-var: TESTS_LOCALE\n            artifact-name: playwright-report-no-trailing\n          - name: alternate locale\n            browsers: chromium\n            test-filter: tests/ui/e2e --grep @alternate-locale\n            trailing-slash: true\n            locale-var: TESTS_ALTERNATE_LOCALE\n            artifact-name: playwright-report-alternate-locale\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 2\n\n      - uses: pnpm/action-setup@v3\n\n      - name: Use Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: \".nvmrc\"\n          cache: \"pnpm\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Install Playwright browsers\n        run: pnpm exec playwright install --with-deps ${{ matrix.browsers }}\n        working-directory: ./core\n\n      - name: Build catalyst\n        run: pnpm build\n\n      - name: Start server\n        run: |\n          mkdir -p ./.tests/reports/\n          pnpm start > ./.tests/reports/nextjs.app.log 2>&1 &\n          npx wait-on http://localhost:3000 --timeout 60000\n        working-directory: ./core\n        env:\n          PORT: 3000\n          AUTH_SECRET: ${{ secrets.TESTS_AUTH_SECRET }}\n          AUTH_TRUST_HOST: ${{ vars.TESTS_AUTH_TRUST_HOST }}\n          BIGCOMMERCE_TRUSTED_PROXY_SECRET: ${{ secrets.BIGCOMMERCE_TRUSTED_PROXY_SECRET }}\n          TESTS_LOCALE: ${{ vars[matrix.locale-var] }}\n          TRAILING_SLASH: ${{ matrix.trailing-slash }}\n          DEFAULT_REVALIDATE_TARGET: ${{ matrix.name == 'default' && '1' || '' }}\n\n      - name: Run E2E tests\n        run: pnpm exec playwright test ${{ matrix.test-filter }}\n        working-directory: ./core\n        env:\n          PLAYWRIGHT_TEST_BASE_URL: http://localhost:3000\n\n      - name: Upload test results\n        if: ${{ !cancelled() }}\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ matrix.artifact-name }}\n          path: ./core/.tests/reports/\n          retention-days: 3\n"
  },
  {
    "path": ".github/workflows/native-hosting.yml",
    "content": "name: Native Hosting\n\non:\n  push:\n    branches: [canary]\n\njobs:\n  build-and-deploy:\n    name: Build and Deploy\n    runs-on: ubuntu-latest\n    concurrency:\n      group: ${{ github.workflow }}\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - uses: pnpm/action-setup@v3\n\n      - name: Use Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: '.nvmrc'\n          cache: 'pnpm'\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Install Catalyst CLI\n        run: pnpm add @bigcommerce/catalyst@alpha @opennextjs/cloudflare@1.17.3\n        working-directory: core\n\n      - name: Convert proxy.ts to middleware.ts\n        working-directory: core\n        run: |\n          mv proxy.ts middleware.ts\n          sed -i 's/export const proxy/export const middleware/' middleware.ts\n          sed -i \"s/export const config = {/export const config = {\\n  runtime: 'experimental-edge',/\" middleware.ts\n\n      - name: Build monorepo packages\n        run: pnpm --filter \"./packages/*\" build\n\n      - name: Generate GraphQL types\n        run: pnpm run generate\n        working-directory: core\n        env:\n          BIGCOMMERCE_STORE_HASH: ${{ vars.NATIVE_HOSTING_BIGCOMMERCE_STORE_HASH }}\n          BIGCOMMERCE_STOREFRONT_TOKEN: ${{ secrets.NATIVE_HOSTING_BIGCOMMERCE_STOREFRONT_TOKEN }}\n          BIGCOMMERCE_CHANNEL_ID: ${{ vars.NATIVE_HOSTING_BIGCOMMERCE_CHANNEL_ID }}\n\n      - name: Build\n        run: pnpm exec catalyst build\n        working-directory: core\n        env:\n          # CLI env vars\n          CATALYST_PROJECT_UUID: ${{ secrets.NATIVE_HOSTING_BIGCOMMERCE_PROJECT_UUID }}\n          # App env vars (needed by Next.js build for GraphQL calls in next.config.ts)\n          BIGCOMMERCE_STORE_HASH: ${{ vars.NATIVE_HOSTING_BIGCOMMERCE_STORE_HASH }}\n          BIGCOMMERCE_STOREFRONT_TOKEN: ${{ secrets.NATIVE_HOSTING_BIGCOMMERCE_STOREFRONT_TOKEN }}\n          BIGCOMMERCE_CHANNEL_ID: ${{ vars.NATIVE_HOSTING_BIGCOMMERCE_CHANNEL_ID }}\n          AUTH_SECRET: ${{ secrets.NATIVE_HOSTING_AUTH_SECRET }}\n\n      - name: Deploy\n        run: |\n          pnpm exec catalyst deploy --prebuilt \\\n            --secret BIGCOMMERCE_STORE_HASH=${{ vars.NATIVE_HOSTING_BIGCOMMERCE_STORE_HASH }} \\\n            --secret BIGCOMMERCE_STOREFRONT_TOKEN=${{ secrets.NATIVE_HOSTING_BIGCOMMERCE_STOREFRONT_TOKEN }} \\\n            --secret BIGCOMMERCE_CHANNEL_ID=${{ vars.NATIVE_HOSTING_BIGCOMMERCE_CHANNEL_ID }} \\\n            --secret AUTH_SECRET=${{ secrets.NATIVE_HOSTING_AUTH_SECRET }}\n        working-directory: core\n        env:\n          CATALYST_STORE_HASH: ${{ vars.NATIVE_HOSTING_BIGCOMMERCE_STORE_HASH }}\n          CATALYST_ACCESS_TOKEN: ${{ secrets.NATIVE_HOSTING_BIGCOMMERCE_ACCESS_TOKEN }}\n          CATALYST_PROJECT_UUID: ${{ secrets.NATIVE_HOSTING_BIGCOMMERCE_PROJECT_UUID }}\n"
  },
  {
    "path": ".github/workflows/prevent-invalid-changesets.yml",
    "content": "name: Prevent invalid packages for Changesets\n\non:\n  pull_request:\n    branches:\n      - integrations/makeswift\n\npermissions:\n  contents: read\n  pull-requests: read\n\njobs:\n  validate-changesets:\n    runs-on: ubuntu-latest\n    name: Validate Changeset Packages\n    steps:\n      - name: Checkout PR code\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Validate changesets only target @bigcommerce/catalyst-makeswift\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const script = require('./.github/scripts/prevent-invalid-changesets.js')\n            await script({ core, exec })\n"
  },
  {
    "path": ".github/workflows/regression-tests.yml",
    "content": "name: Regression Tests\n\non:\n  deployment_status:\n    states: [\"success\"]\n\nenv:\n  VERCEL_PROTECTION_BYPASS: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}\n\njobs:\n  detect-provider:\n    name: Detect Deployment Provider\n    runs-on: ubuntu-latest\n    outputs:\n      provider: ${{ steps.detect.outputs.provider }}\n      production-url: ${{ steps.detect.outputs.production-url }}\n      branch-label: ${{ steps.detect.outputs.branch-label }}\n      is-preview: ${{ steps.detect.outputs.is-preview }}\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Detect provider and production URL\n        id: detect\n        run: |\n          CREATOR=\"${{ github.event.deployment_status.creator.login }}\"\n          ENVIRONMENT=\"${{ github.event.deployment.environment }}\"\n\n          if [[ \"$ENVIRONMENT\" == \"Preview\" ]]; then\n            echo \"is-preview=true\" >> $GITHUB_OUTPUT\n          else\n            echo \"is-preview=false\" >> $GITHUB_OUTPUT\n          fi\n\n          if [[ \"$CREATOR\" == \"vercel[bot]\" ]]; then\n            echo \"provider=vercel\" >> $GITHUB_OUTPUT\n            PKG_NAME=$(node -p \"require('./core/package.json').name\")\n\n            if [[ \"$ENVIRONMENT\" == \"Preview\" ]]; then\n              case \"$PKG_NAME\" in\n                \"@bigcommerce/catalyst-core\")\n                  echo \"production-url=https://canary.catalyst-demo.site/\" >> $GITHUB_OUTPUT ;;\n                \"@bigcommerce/catalyst-makeswift\")\n                  echo \"production-url=https://canary.makeswift.catalyst-demo.site\" >> $GITHUB_OUTPUT ;;\n                *)\n                  echo \"::warning::No production URL configured for package: $PKG_NAME. Skipping comparison.\"\n                  echo \"production-url=\" >> $GITHUB_OUTPUT ;;\n              esac\n              echo \"branch-label=\" >> $GITHUB_OUTPUT\n            else\n              case \"$PKG_NAME\" in\n                \"@bigcommerce/catalyst-core\")\n                  echo \"branch-label=canary\" >> $GITHUB_OUTPUT\n                  echo \"production-url=${{ github.event.deployment_status.target_url }}\" >> $GITHUB_OUTPUT ;;\n                \"@bigcommerce/catalyst-makeswift\")\n                  echo \"branch-label=integrations/makeswift\" >> $GITHUB_OUTPUT\n                  echo \"production-url=${{ github.event.deployment_status.target_url }}\" >> $GITHUB_OUTPUT ;;\n                *)\n                  echo \"branch-label=\" >> $GITHUB_OUTPUT\n                  echo \"production-url=\" >> $GITHUB_OUTPUT ;;\n              esac\n            fi\n\n          elif [[ \"$CREATOR\" == \"cloudflare-pages[bot]\" ]]; then\n            echo \"provider=cloudflare\" >> $GITHUB_OUTPUT\n            echo \"::warning::Cloudflare production URL not yet configured. Skipping comparison.\"\n            echo \"production-url=\" >> $GITHUB_OUTPUT\n            echo \"branch-label=\" >> $GITHUB_OUTPUT\n\n          else\n            echo \"::warning::Unknown deployment provider: $CREATOR. Skipping audits.\"\n            echo \"provider=unknown\" >> $GITHUB_OUTPUT\n            echo \"production-url=\" >> $GITHUB_OUTPUT\n            echo \"branch-label=\" >> $GITHUB_OUTPUT\n          fi\n\n  unlighthouse-audit-preview:\n    name: Unlighthouse Audit Preview (${{ needs.detect-provider.outputs.provider }}) - ${{ matrix.device }}\n    needs: [detect-provider]\n    if: needs.detect-provider.outputs.is-preview == 'true'\n    runs-on: ubuntu-latest\n    concurrency:\n      group: regression-preview-${{ github.event.deployment.ref }}-${{ matrix.device }}\n      cancel-in-progress: true\n    strategy:\n      matrix:\n        device: [desktop, mobile]\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Install Dependencies\n        run: npm install @unlighthouse/cli puppeteer -g\n\n      - name: Unlighthouse audit on ${{ matrix.device }} (preview)\n        env:\n          PROVIDER: ${{ needs.detect-provider.outputs.provider }}\n          PREVIEW_URL: ${{ github.event.deployment_status.target_url }}\n        run: |\n          if [[ \"$PROVIDER\" == \"vercel\" ]]; then\n            unlighthouse-ci --site \"$PREVIEW_URL\" --${{ matrix.device }} --disable-robots-txt \\\n              --extra-headers \"x-vercel-protection-bypass=$VERCEL_PROTECTION_BYPASS,x-vercel-set-bypass-cookie=true\"\n          else\n            unlighthouse-ci --site \"$PREVIEW_URL\" --${{ matrix.device }} --disable-robots-txt\n          fi\n\n      - name: Upload ${{ matrix.device }} preview audit\n        if: failure() || success()\n        uses: actions/upload-artifact@v4\n        with:\n          name: unlighthouse-preview-${{ matrix.device }}-report\n          path: \"./.unlighthouse/\"\n          include-hidden-files: \"true\"\n\n  unlighthouse-audit-production:\n    name: Unlighthouse Audit Production (${{ needs.detect-provider.outputs.provider }}) - ${{ matrix.device }}\n    needs: [detect-provider]\n    if: needs.detect-provider.outputs.production-url != ''\n    runs-on: ubuntu-latest\n    concurrency:\n      group: regression-production-${{ github.event.deployment.environment == 'Preview' && github.event.deployment.ref || github.event.deployment.sha }}-${{ matrix.device }}\n      cancel-in-progress: ${{ github.event.deployment.environment == 'Preview' }}\n    strategy:\n      matrix:\n        device: [desktop, mobile]\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install Dependencies\n        run: npm install @unlighthouse/cli puppeteer -g\n\n      - name: Unlighthouse audit on ${{ matrix.device }} (production)\n        env:\n          PRODUCTION_URL: ${{ needs.detect-provider.outputs.production-url }}\n        run: unlighthouse-ci --site \"$PRODUCTION_URL\" --${{ matrix.device }} --disable-robots-txt\n\n      - name: Upload ${{ matrix.device }} production audit\n        if: failure() || success()\n        uses: actions/upload-artifact@v4\n        with:\n          name: unlighthouse-production-${{ matrix.device }}-report\n          path: \"./.unlighthouse/\"\n          include-hidden-files: \"true\"\n\n  unlighthouse-compare:\n    name: Unlighthouse Compare & Comment (${{ needs.detect-provider.outputs.provider }})\n    needs: [detect-provider, unlighthouse-audit-preview, unlighthouse-audit-production]\n    if: needs.detect-provider.outputs.is-preview == 'true' && needs.detect-provider.outputs.production-url != ''\n    runs-on: ubuntu-latest\n    permissions:\n      pull-requests: write\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version-file: \".nvmrc\"\n\n      - name: Download all Unlighthouse artifacts\n        uses: actions/download-artifact@v4\n        with:\n          pattern: unlighthouse-*-report\n          path: /tmp/unlighthouse-artifacts\n          merge-multiple: false\n\n      - name: Compare audits\n        env:\n          PROVIDER: ${{ needs.detect-provider.outputs.provider }}\n        run: |\n          node .github/scripts/compare-unlighthouse.mts \\\n            --preview-desktop    /tmp/unlighthouse-artifacts/unlighthouse-preview-desktop-report/ci-result.json \\\n            --preview-mobile     /tmp/unlighthouse-artifacts/unlighthouse-preview-mobile-report/ci-result.json \\\n            --production-desktop /tmp/unlighthouse-artifacts/unlighthouse-production-desktop-report/ci-result.json \\\n            --production-mobile  /tmp/unlighthouse-artifacts/unlighthouse-production-mobile-report/ci-result.json \\\n            --output /tmp/unlighthouse-report.md \\\n            --meta-output /tmp/unlighthouse-meta.json \\\n            --provider \"$PROVIDER\"\n          cat /tmp/unlighthouse-report.md >> \"$GITHUB_STEP_SUMMARY\"\n\n      - name: Post PR comment\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const postComment = require('./.github/scripts/post-unlighthouse-pr-comment.js')\n            await postComment({\n              github,\n              context,\n              provider: '${{ needs.detect-provider.outputs.provider }}',\n              reportPath: '/tmp/unlighthouse-report.md',\n              metaPath: '/tmp/unlighthouse-meta.json',\n            })\n\n  unlighthouse-report:\n    name: Unlighthouse Report (${{ needs.detect-provider.outputs.provider }}) — ${{ needs.detect-provider.outputs.branch-label }}\n    needs: [detect-provider, unlighthouse-audit-production]\n    if: needs.detect-provider.outputs.is-preview == 'false' && needs.detect-provider.outputs.production-url != ''\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version-file: \".nvmrc\"\n\n      - name: Download production Unlighthouse artifacts\n        uses: actions/download-artifact@v4\n        with:\n          pattern: unlighthouse-production-*-report\n          path: /tmp/unlighthouse-artifacts\n          merge-multiple: false\n\n      - name: Format report\n        run: |\n          node .github/scripts/audit-unlighthouse.mts \\\n            --desktop /tmp/unlighthouse-artifacts/unlighthouse-production-desktop-report/ci-result.json \\\n            --mobile  /tmp/unlighthouse-artifacts/unlighthouse-production-mobile-report/ci-result.json \\\n            --branch  \"${{ needs.detect-provider.outputs.branch-label }}\" \\\n            --output  /tmp/unlighthouse-report.md\n          cat /tmp/unlighthouse-report.md >> \"$GITHUB_STEP_SUMMARY\"\n\n      - name: Post commit comment\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const postReport = require('./.github/scripts/post-unlighthouse-commit-comment.js')\n            await postReport({ github, context, reportPath: '/tmp/unlighthouse-report.md' })\n"
  },
  {
    "path": ".github/workflows/translations-changeset.yml",
    "content": "name: Create translations patch\n\non:\n  pull_request:\n    types:\n      - opened\n    branches:\n      - canary\n\njobs:\n  create-translations-patch:\n    if: github.actor == 'bc-svc-local'\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repo\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 2\n\n      - name: Use commit SHA for filename\n        id: generate-sha\n        run: |\n          short_sha=$(echo \"${GITHUB_SHA}\" | cut -c1-8)\n          echo \"SHORT_SHA=$short_sha\" >> $GITHUB_OUTPUT\n\n      - name: Create a translations changeset\n        env:\n          SHORT_SHA: ${{ steps.generate-sha.outputs.SHORT_SHA }}\n        run: |\n          mkdir -p .changeset\n          echo \"---\n          \\\"@bigcommerce/catalyst-core\\\": patch\n          ---\n\n          Update translations.\" > .changeset/translations-patch-$SHORT_SHA.md\n\n      - name: Commit changeset\n        env:\n          SHORT_SHA: ${{ steps.generate-sha.outputs.SHORT_SHA }}\n        run: |\n          git config --global user.name 'bc-svc-local'\n          git config --global user.email 'bc-svc-local@users.noreply.github.com'\n          git add .changeset/translations-patch-$SHORT_SHA.md\n          git commit -m \"chore(core): create translations patch\"\n      \n      - name: Push changeset\n        env:\n          TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} HEAD:$GITHUB_HEAD_REF\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\ndist\n.turbo\n.vscode/**/*\n!.vscode/settings.example.json\n!.vscode/launch.example.json\n.idea\n.vercel\n.catalyst\n.env\n.env*.local\n.env*.test\ntest-results/\nplaywright-report/\nplaywright/.cache/\n.tests\nbigcommerce.graphql\nbigcommerce-graphql.d.ts\n.DS_Store\ncoverage/\n.history\n.unlighthouse\n.bigcommerce\n.mcp.json\n"
  },
  {
    "path": ".nvmrc",
    "content": "24\n"
  },
  {
    "path": ".vscode/launch.example.json",
    "content": "{\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Catalyst: debug server-side\",\n            \"type\": \"node-terminal\",\n            \"request\": \"launch\",\n            \"command\": \"pnpm run dev\",\n            \"cwd\": \"${workspaceFolder}\"\n        },\n        {\n            \"name\": \"Catalyst: debug client-side\",\n            \"type\": \"chrome\", // Use \"chrome\" \"firefox\" or \"msedge\" as needed\n            \"request\": \"launch\",\n            \"url\": \"http://localhost:3000\",\n            \"webRoot\": \"${workspaceFolder}/core\"\n        },\n        {\n            \"name\": \"Catalyst: debug full stack\",\n            \"type\": \"node\",\n            \"request\": \"launch\",\n            \"cwd\": \"${workspaceFolder}/core\",\n            \"program\": \"${workspaceFolder}/core/node_modules/next/dist/bin/next\",\n            \"args\": [\"dev\"],\n            \"runtimeArgs\": [\"--inspect\"],\n            \"skipFiles\": [\"<node_internals>/**\"],\n            \"env\": {\n                \"NODE_ENV\": \"development\"\n            },\n            \"serverReadyAction\": {\n                \"action\": \"openExternally\",\n                \"killOnServerStop\": true,\n                \"pattern\": \"- Local:.+(https?://.+)\",\n                \"uriFormat\": \"%s\"\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": ".vscode/settings.example.json",
    "content": "{\n  \"typescript.tsdk\": \"node_modules/typescript/lib\",\n  \"typescript.enablePromptUseWorkspaceTsdk\": true,\n  \"eslint.workingDirectories\": [\n    { \"pattern\": \"core\" },\n    { \"pattern\": \"packages/*/\" }\n  ]\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at engineering@bigcommerce.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Catalyst\n\nThanks for showing interest in contributing!\n\nThe following is a set of guidelines for contributing to Catalyst. These are just guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.\n\n## Repository Structure\n\nCatalyst is a monorepo that contains the code for the Catalyst Next.js application inside of `core/`, and supporting packages such as the GraphQL API client and the `create-catalyst` CLI in `packages/`.\n\nThe default branch for this repository is called `canary`. This is the primary development branch where active development takes place, including the introduction of new features, bug fixes, and other changes before they are released in stable versions.\n\nTo contribute to the `canary` branch, you can create a new branch off of `canary` and submit a PR against that branch.\n\n## API Scope\n\nCatalyst is intended to work with the [BigCommerce Storefront GraphQL API](https://developer.bigcommerce.com/docs/storefront/graphql) and not directly integrate out of the box with the [REST Management API](https://developer.bigcommerce.com/docs/rest-management).\n\nYou're welcome to integrate the REST Management API in your own fork, but we will not accept pull requests that incorporate or depend on the REST Management API. If your contribution requires Management API functionality, it is out of scope for this project.\n\n## Makeswift Integration\n\nIn addition to `canary`, we also maintain the `integrations/makeswift` branch, which contains additional code required to integrate with [Makeswift](https://www.makeswift.com).\n\nTo contribute to the `integrations/makeswift` branch, you can create a new branch off of `integrations/makeswift` and submit a PR against that branch.\n\n### Keeping `integrations/makeswift` in sync with `canary`\n\nExcept for the additional code required to integrate with Makeswift, the `integrations/makeswift` branch is a mirror of the `canary` branch. This means that the `integrations/makeswift` branch should be kept in sync with the `canary` branch as much as possible.\n\n#### Prerequisites\n\nIn order to complete the following steps, you will need to have met the following prerequisites:\n\n- You have a remote named `origin` pointing to the [`bigcommerce/catalyst` repository on GitHub](https://github.com/bigcommerce/catalyst).\n- You have rights to push to the `integrations/makeswift` branch on GitHub.\n\n#### Steps\n\n1. Fetch latest from `origin`\n\n   ```bash\n   git fetch origin\n   ```\n\n2. Create a branch to perform a merge from `canary`\n\n   ```bash\n   git checkout -B sync-integrations-makeswift origin/integrations/makeswift\n   ```\n\n> [!TIP]\n> The `-B` flag means \"create branch or reset existing branch\":\n>\n> - If the local branch doesn't exist, it creates it from `origin/integrations/makeswift`\n> - If the local branch exists, it resets it to match `origin/integrations/makeswift`\n\n3. Merge `canary` and resolve merge conflicts, if necessary:\n\n   ```bash\n   git merge canary\n   ```\n\n> [!WARNING]\n> **Gotchas when merging canary into integrations/makeswift:**\n>\n> - The `name` field in `core/package.json` should remain `@bigcommerce/catalyst-makeswift`\n> - The `version` field in `core/package.json` should remain whatever the latest published `@bigcommerce/catalyst-makeswift` version was\n> - The latest release in `core/CHANGELOG.md` should remain whatever the latest published `@bigcommerce/catalyst-makeswift` version was\n\n4. After resolving any merge conflicts, open a new PR in GitHub to merge your `sync-integrations-makeswift` into `integrations/makeswift`. This PR should be code reviewed and approved before the next steps.\n\n5. Rebase `integrations/makeswift` to establish new merge base\n\n   ```bash\n   git checkout -B integrations/makeswift origin/integrations/makeswift\n   git rebase sync-integrations-makeswift\n   ```\n\n6. Push the changes up to GitHub:\n\n   ```bash\n   git push origin integrations/makeswift\n   ```\n\nThis should close the PR in GitHub automatically.\n\n> [!IMPORTANT]\n> Do not squash or rebase-and-merge PRs into `integrations/makeswift`. Always use a true merge commit or rebase locally (as shown below). This is to preserve the merge commit and establish a new merge base between `canary` and `integrations/makeswift`.\n\n## Cutting New Releases\n\nCatalyst uses [Changesets](https://github.com/changesets/changesets) to manage version bumps, changelogs, and publishing. Releases happen in **two stages**:\n\n1. Cut a release from `canary`\n2. Sync that release into `integrations/makeswift` and cut again\n\nThis ensures `integrations/makeswift` remains a faithful mirror of `canary` while including its additional integration code.\n\n#### Stage 1: Cut a release from `canary`\n\n1. Begin the release process by merging the **Version Packages (`canary`)** PR. When `.changeset/` files exist on `canary`, a GitHub Action opens a **Version Packages (`canary`)** PR. This PR consolidates pending changesets, bumps versions, and updates changelogs. Merging this PR should publish new tags to GitHub, and optionally publish new package versions to NPM.\n\n#### Stage 2: Sync and Release `integrations/makeswift`\n\n2. Follow steps 1-6 under \"[Keeping `integrations/makeswift` in sync with `canary`](#keeping-integrationsmakeswift-in-sync-with-canary)\", with one addition: **include a changeset for `@bigcommerce/catalyst-makeswift` in the sync merge commit** rather than opening a separate PR for it afterwards.\n\n   - Match the bump type from Stage 1 (e.g., if `@bigcommerce/catalyst-core` went from `1.4.2` to `1.5.0`, use `minor`)\n   - Create a changeset file in `.changeset/` (e.g., `.changeset/sync-canary-1-5-0.md`):\n\n     ```\n     ---\n     \"@bigcommerce/catalyst-makeswift\": minor\n     ---\n\n     Pulls in changes from the `@bigcommerce/catalyst-core@1.5.0` release. For more information, see the [changelog entry](https://github.com/bigcommerce/catalyst/blob/<canary-sha>/core/CHANGELOG.md#150).\n     ```\n\n   - Replace `<canary-sha>` with the merge commit SHA of the Version Packages PR on `canary` so the link remains stable\n   - Amend this changeset into the merge commit alongside any other sync changes (changeset cleanup, `core/package.json` and `core/CHANGELOG.md` fixes, etc.)\n\n3. Merge the **Version Packages (`integrations/makeswift`)** PR: After the sync lands, Changesets will open a PR (similar to Stage 1) bumping `@bigcommerce/catalyst-makeswift`. Merge it following the same process. This cuts a new release of the Makeswift variant.\n\n4. **Tags and Releases:** Confirm tags exist for both `@bigcommerce/catalyst-core` and `@bigcommerce/catalyst-makeswift`. Update `latest` tags to point to the new releases:\n\n   ```bash\n   git fetch origin --tags\n   git tag @bigcommerce/catalyst-core@latest @bigcommerce/catalyst-core@<version> -f\n   git tag @bigcommerce/catalyst-makeswift@latest @bigcommerce/catalyst-makeswift@<version> -f\n   git push origin @bigcommerce/catalyst-core@latest -f\n   git push origin @bigcommerce/catalyst-makeswift@latest -f\n   ```\n\n### Additional Notes\n\n- **Release cadence:** Teams typically review on Wednesdays whether to cut a release, but you may cut releases more frequently as needed.\n\n## Other Ways to Contribute\n\n- Consider reporting bugs, contributing to test coverage, or helping spread the word about Catalyst.\n\n## Git Commit Messages\n\n- Use the present tense (\"Add feature\" not \"Added feature\")\n- Use the imperative mood (\"Move cursor to...\" not \"Moves cursor to...\")\n- Limit the first line to 72 characters or less\n- Reference pull requests and external links liberally\n\nThank you again for your interest in contributing to Catalyst!\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 BigCommerce\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<a href=\"https://catalyst.dev\" target=\"_blank\" rel=\"noopener norerrer\">\n  <img src=\"https://storage.googleapis.com/bigcommerce-developers/images/catalyst_readme_banner.png\" alt=\"Catalyst for Composable Commerce Image Banner\" title=\"Catalyst\">\n</a>\n\n<br />\n<br />\n\n<div align=\"center\">\n\n[![MIT License](https://img.shields.io/github/license/bigcommerce/catalyst)](LICENSE.md)\n[![Lighthouse Report](https://github.com/bigcommerce/catalyst/actions/workflows/lighthouse.yml/badge.svg)](https://github.com/bigcommerce/catalyst/actions/workflows/lighthouse.yml) [![Lint, Typecheck, gql.tada](https://github.com/bigcommerce/catalyst/actions/workflows/basic.yml/badge.svg)](https://github.com/bigcommerce/catalyst/actions/workflows/basic.yml)\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bigcommerce/catalyst)\n\n</div>\n\n**Catalyst** is the composable, fully customizable headless commerce framework for\n[BigCommerce](https://www.bigcommerce.com/). Catalyst is built with [Next.js](https://nextjs.org/), uses\nour [React](https://react.dev/) storefront components, and is backed by the\n[GraphQL Storefront API](https://docs.bigcommerce.com/developer/docs/storefront/guides/graphql-storefront-api/overview).\n\nBy choosing Catalyst, you'll have a fully-functional storefront within a few seconds, and spend zero time on wiring\nup APIs or building SEO, Accessibility, and Performance-optimized ecommerce components you've probably written many\ntimes before. You can instead go straight to work building your brand and making this your own.\n\n## Demo\n\n- [Catalyst Demo](https://catalyst-demo.site)\n\n![-----------------------------------------------------](https://storage.googleapis.com/bigcommerce-developers/images/catalyst_readme_hr.png)\n\n<p align=\"center\">\n <a href=\"https://www.catalyst.dev\">🚀 catalyst.dev</a> •\n <a href=\"https://docs.bigcommerce.com/developer/community/connect\">🤗 BigCommerce Developer Community</a> •\n <a href=\"https://github.com/bigcommerce/catalyst/discussions\">💬 GitHub Discussions</a> •\n <a href=\"https://docs.bigcommerce.com/developer/docs/storefront/catalyst/overview\">💡 Documentation</a>\n</p>\n\n![-----------------------------------------------------](https://storage.googleapis.com/bigcommerce-developers/images/catalyst_readme_hr.png)\n\n## Deploy via One-Click Catalyst App\n\nThe easiest way to deploy your Catalyst Storefront is to use the [One-Click Catalyst App](http://login.bigcommerce.com/deep-links/app/53284) available in the BigCommerce App Marketplace.\n\nCheck out the [Commerce One-Click Catalyst Documentation](https://docs.bigcommerce.com/developer/docs/storefront/catalyst/getting-started/workflows/one-click-catalyst) for more details.\n\n## Getting Started\n\n**Requirements:**\n\n- A [BigCommerce account](https://www.bigcommerce.com/start-your-trial)\n- Node.js version 24\n- Corepack-enabled `pnpm`\n\n  ```bash\n  corepack enable pnpm\n  ```\n\n1. Install the latest version of Catalyst:\n\n   ```bash\n   pnpm create @bigcommerce/catalyst@latest\n   ```\n\n2. Run the local development server:\n\n   ```bash\n   pnpm run dev\n   ```\n\nLearn more about Catalyst at [catalyst.dev](https://catalyst.dev).\n\n## Resources\n\n- [Catalyst Documentation](https://docs.bigcommerce.com/developer/docs/storefront/catalyst/overview)\n- [GraphQL Storefront API Playground](https://docs.bigcommerce.com/developer/docs/storefront/guides/graphql-storefront-api/overview#accessing-the-graphql-storefront-playground)\n- [BigCommerce DevDocs](https://docs.bigcommerce.com/developer/docs/overview/quick-start)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Reporting security issues\nBigCommerce is dedicated to the responsible disclosure of security vulnerabilities.\nIf you have found a security vulnerability in an active open-source repository created and owned by BigCommerce, please report it to our [public bug bounty program](https://bugcrowd.com/bigcommerce). If you would prefer to submit via email, please send your report to [security@bigcommerce.com](mailto:security@bigcommerce.com).\n\nWe ask that you **do not** open a public GitHub issue to report security concerns.\n\n_Note: Only submissions to our bounty program on BugCrowd will be eligible for bounties. Bounty eligibility and amounts are determined according to the program guidelines._\n\n_Note: Bugs in 3rd-party modules and/or dependencies should be reported to the owners/maintainers or those modules and/or dependencies, BigCommerce has no control or authority over third party content._\n\nThank you in advance for collaborating with us to help protect us and our customers.\n"
  },
  {
    "path": "core/.eslintignore",
    "content": "# Dependencies\nnode_modules/\n\n# Build outputs\n.next/\n.wrangler/\n.open-next/\nout/\ndist/\nbuild/\n\n# Generated files\n.turbo/\nmessages/*.d.json.ts\nnext-env.d.ts\n*-graphql.d.ts\n\n# Test outputs\nplaywright-report/\ntest-results/\n.tests/\n\n# Cache\n.eslintcache\n"
  },
  {
    "path": "core/.eslintrc.cjs",
    "content": "// @ts-check\n\nrequire('@bigcommerce/eslint-config/patch');\n\n/** @type {import('eslint').Linter.LegacyConfig} */\nconst config = {\n  root: true,\n  extends: [\n    '@bigcommerce/catalyst/base',\n    '@bigcommerce/catalyst/react',\n    '@bigcommerce/catalyst/next',\n    '@bigcommerce/catalyst/prettier',\n  ],\n  rules: {\n    '@typescript-eslint/naming-convention': 'off',\n    '@next/next/no-html-link-for-pages': 'off',\n    'import/dynamic-import-chunkname': 'off',\n    'no-underscore-dangle': ['error', { allow: ['__typename'] }],\n    '@typescript-eslint/prefer-nullish-coalescing': 'off',\n    '@typescript-eslint/no-unsafe-enum-comparison': 'off',\n    '@typescript-eslint/no-restricted-imports': [\n      'error',\n      {\n        paths: [\n          {\n            name: 'next/link',\n            message: \"Please import 'Link' from '~/components/Link' instead.\",\n          },\n          {\n            name: 'next/image',\n            importNames: ['default'],\n            message:\n              \"Please import 'Image' from '~/components/image' instead. This component handles CDN and static image optimization.\",\n          },\n          {\n            name: '~/i18n/routing',\n            importNames: ['Link'],\n            message: \"Please import 'Link' from '~/components/Link' instead.\",\n          },\n          {\n            name: 'next/router',\n            importNames: ['useRouter'],\n            message: 'Please import from `~/i18n/routing` instead.',\n          },\n          {\n            name: 'next/navigation',\n            importNames: ['redirect', 'permanentRedirect', 'useRouter', 'usePathname'],\n            message: 'Please import from `~/i18n/routing` instead.',\n          },\n          {\n            name: '@playwright/test',\n            importNames: ['expect', 'test'],\n            message: 'Please import from `~/tests/fixtures` instead.',\n          },\n        ],\n      },\n    ],\n    'check-file/folder-naming-convention': [\n      'error',\n      {\n        '**': 'NEXT_JS_APP_ROUTER_CASE',\n      },\n    ],\n  },\n  overrides: [\n    {\n      files: ['**/*.spec.ts', '**/*.test.ts'],\n      rules: {\n        '@typescript-eslint/no-restricted-imports': [\n          'error',\n          {\n            paths: [\n              {\n                name: 'next-intl/server',\n                importNames: ['getTranslations', 'getFormatter'],\n                message:\n                  'Please import `getTranslations` from `~/tests/lib/i18n` and `getFormatter` from `~/tests/lib/formatter` instead.',\n              },\n            ],\n          },\n        ],\n      },\n    },\n  ],\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "core/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n/test-results/\n/playwright-report/\n/playwright/.cache/\n/.tests\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n\n# generated\nclient/generated\n\n# next-intl\nmessages/*.d.json.ts\n\n# secrets\n.catalyst\n\n# Build config\nbuild-config.json\n\n# OpenNext\n.open-next\n.wrangler\n"
  },
  {
    "path": "core/AGENTS.md",
    "content": "# AGENTS.md\n\n## BigCommerce Catalyst Codebase Overview\n\nThis document provides guidance for Large Language Models (LLMs) working with the BigCommerce Catalyst codebase, focusing on the **Next.js App Router application** architecture, data fetching patterns, and key design principles.\n\n**Catalyst is built as a Next.js App Router application** with React Server Components, enabling server-side data fetching, automatic code splitting, and optimal performance for e-commerce workloads.\n\n## Repository Structure\n\nThe main Next.js application is located in the `/core` directory, which contains the complete e-commerce storefront implementation. Other packages exist outside of `/core` but are not the primary focus for most development work.\n\n## Proxy Architecture\n\nThe application uses the Next.js 16 proxy pattern (`proxy.ts`) with a composed proxy stack that significantly alters the default Next.js routing behavior. The proxy composition (in the `proxies/` directory) includes authentication, internationalization, analytics, channel handling, and most importantly, custom routing.\n\n### Custom Routing with `with-routes`\n\nThe `with-routes` proxy is the most critical component that overrides Next.js's default path-based routing. Instead of relying on file-based routing, this proxy:\n\n1. **Queries the BigCommerce GraphQL API** to resolve incoming URL paths to specific entity types (products, categories, brands, blog posts, pages).\n\n2. **Rewrites requests** to internal Next.js routes based on the resolved entity type.\n\n3. **Handles redirects** automatically based on BigCommerce's redirect configuration.\n\nThis means that URLs like `/my-product-name` can resolve to `/en/product/123` internally, providing flexible URL structure while maintaining SEO-friendly paths.\n\n## Data Fetching and Partial Prerendering (PPR)\n\n### PPR Configuration\n\nThe application uses Next.js Partial Prerendering with incremental adoption. This allows static parts of pages to be prerendered while dynamic content streams in.\n\n### Streamable Pattern\n\nThe `Streamable<T>` pattern is a core architectural concept that enables efficient data streaming and React Server Component compatibility.\n\n#### What is Streamable?\n\n```typescript\nexport type Streamable<T> = T | Promise<T>;\n```\n\nA `Streamable<T>` represents data that can be either:\n- **Immediate**: Already resolved data of type `T`\n- **Deferred**: A Promise that will resolve to type `T`\n\n#### Core Streamable API\n\nLocated in `core/vibes/soul/lib/streamable.tsx`, the Streamable system provides:\n\n**`Streamable.from()`** - Creates a streamable from a lazy promise factory:\n```typescript\nconst streamableProducts = Streamable.from(async () => {\n  const customerToken = await getSessionCustomerAccessToken();\n  const currencyCode = await getPreferredCurrencyCode();\n  return getProducts(customerToken, currencyCode);\n});\n```\n\n**`Streamable.all()`** - Combines multiple streamables with automatic caching:\n```typescript\nconst combined = Streamable.all([\n  streamableProducts,\n  streamableCategories,\n  streamableUser\n]);\n```\n\n**`useStreamable()`** - Hook for consuming streamables in components:\n```typescript\nfunction MyComponent({ data }: { data: Streamable<Product[]> }) {\n  const products = useStreamable(data);\n  return <div>{products.map(...)}</div>;\n}\n```\n\n**`<Stream>` Component** - Provides Suspense boundary for streamable data:\n```tsx\n<Stream value={streamableProducts} fallback={<ProductSkeleton />}>\n  {(products) => <ProductList products={products} />}\n</Stream>\n```\n\n#### Streamable Benefits\n\n- **Performance**: Enables concurrent data fetching and streaming\n- **Caching**: Automatic promise deduplication and stability\n- **Flexibility**: Works with both sync and async data\n- **Suspense Integration**: Built-in React Suspense support\n- **Composition**: Easy chaining and combination of data sources\n\n### Data Fetching Best Practices\n\n1. **Use React's `cache()` function** for server-side data fetching to memoize function results and prevent repeated fetches or computations **per request** (React will invalidate the cache for all memoized functions for each server request).\n\n2. **Implement proper cache strategies** based on whether user authentication is present.\n\n3. **Leverage Streamable for progressive enhancement** where static content loads immediately and dynamic content streams in.\n\n## GraphQL API Client\n\n### Centralized Client Configuration\n\nAll interactions with the BigCommerce Storefront GraphQL API should use the centralized GraphQL client. This client provides:\n\n- Automatic channel ID resolution based on locale\n- Proper authentication token handling\n- Request/response logging in development\n- Error handling with automatic auth redirects\n- IP address forwarding for personalization\n\n### Usage Pattern\n\nAlways import and use the configured client rather than making direct API calls. The client handles all the necessary headers, authentication, and channel context automatically.\n\n## UI Design System (Vibes)\n\n### Architecture Overview\n\nThe `vibes/` directory contains the **highly customizable and styleable UI layer** that is completely separate from data fetching and business logic. This separation enables:\n\n- **Complete visual customization** without touching data logic\n- **Theme-based styling** through CSS variables\n- **Reusable components** across different page contexts\n- **Clear separation of concerns** between data and presentation\n\n### Vibes vs Pages Architecture\n\n**`vibes/` folder**: Contains presentation components that are meant to be highly customizable and styleable to change the UI:\n- Accept `Streamable<T>` data as props\n- Handle rendering, styling, and user interactions\n- Support theming through CSS variables\n- No direct data fetching or business logic\n\n**`page.tsx` files**: Where data fetching patterns should live:\n- Handle authentication and authorization\n- Create `Streamable` data sources\n- Transform API responses for vibes components\n- Manage routing and server-side logic\n\n### Component Hierarchy\n\n```\nvibes/soul/\n├── lib/\n│   └── streamable.tsx     # Streamable utilities\n├── primitives/           # Basic UI components\n│   ├── button/\n│   ├── product-card/\n│   └── navigation/\n└── sections/             # Complex UI sections\n    ├── product-list/\n    ├── featured-product-carousel/\n    └── footer/\n```\n\n1. **Primitives** (`vibes/soul/primitives/`) - Basic reusable UI components like buttons, cards, forms.\n\n2. **Sections** (`vibes/soul/sections/`) - Page-level components that compose primitives into complete page sections.\n\n3. **Library** (`vibes/soul/lib/`) - Utility functions and patterns like the Streamable implementation.\n\n### Data Flow Pattern\n\n```\npage.tsx → Streamable data → Vibes components → User interaction\n```\n\n**Example Pattern:**\n```typescript\n// app/[locale]/(default)/page.tsx - Data fetching\nexport default async function HomePage({ params }: Props) {\n  const streamableProducts = Streamable.from(async () => {\n    const customerToken = await getSessionCustomerAccessToken();\n    return getProducts(customerToken);\n  });\n\n  return (\n    <FeaturedProductList \n      products={streamableProducts} // Pass streamable to vibes\n      title=\"Featured Products\"\n    />\n  );\n}\n\n// vibes/soul/sections/featured-product-list/index.tsx - Presentation\nexport function FeaturedProductList({ \n  products, \n  title \n}: {\n  products: Streamable<Product[]>; // Accept streamable\n  title: string;\n}) {\n  return (\n    <section>\n      <h2>{title}</h2>\n      <Stream value={products} fallback={<ProductSkeleton />}>\n        {(productList) => (\n          <div className=\"grid\">\n            {productList.map(product => <ProductCard key={product.id} product={product} />)}\n          </div>\n        )}\n      </Stream>\n    </section>\n  );\n}\n```\n\n### Import Patterns\n\nComponents should be imported from the vibes design system using the `@/vibes/soul/` alias, maintaining clear separation between business logic in `/components` and design system components in `/vibes`.\n\n## App Router Data Fetching Patterns\n\n### Server Components by Default\n\nAll pages are React Server Components, enabling:\n- Server-side data fetching with zero client JavaScript\n- Automatic code splitting and optimization\n- SEO-friendly content rendering\n- Direct database/API access\n\n### File-based Routing Structure\n\n```\napp/[locale]/(default)/\n├── page.tsx              # Homepage with data fetching\n├── layout.tsx            # Shared layout components\n├── product/[slug]/\n│   ├── page.tsx          # Product detail page\n│   └── page-data.ts      # Product data fetching logic\n├── (faceted)/category/[slug]/\n│   └── page.tsx          # Category page\n└── cart/\n    └── page.tsx          # Cart page\n```\n\n### Data Fetching Example\n\n```typescript\n// page.tsx - Server Component with async data fetching\nexport default async function ProductPage({ params, searchParams }: Props) {\n  const { slug } = await params;\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  \n  // Create streamables for concurrent data loading\n  const streamableProduct = Streamable.from(async () => {\n    return getProduct(slug, customerAccessToken);\n  });\n\n  const streamableReviews = Streamable.from(async () => {\n    const product = await streamableProduct; // Reuses cached promise\n    return getProductReviews(product.id);\n  });\n\n  return (\n    <ProductDetail \n      product={streamableProduct}\n      reviews={streamableReviews}\n    />\n  );\n}\n```\n\n## Key Architectural Principles\n\n1. **App Router Architecture**: Built on Next.js App Router with React Server Components for optimal performance\n2. **Routing Flexibility**: Unlike typical Next.js applications, URLs are resolved dynamically via GraphQL rather than file structure\n3. **Progressive Enhancement**: Static content loads immediately with dynamic content streaming via PPR and Streamable\n4. **Vibes Separation**: Complete separation between data fetching (`page.tsx`) and presentation (`vibes/`) concerns\n5. **Centralized API Access**: All BigCommerce API interactions go through the configured GraphQL client\n6. **Proxy-First**: Critical functionality like routing, auth, and internationalization handled at the proxy layer\n\n## Notes\n\nThis codebase differs significantly from typical Next.js applications due to the custom routing proxy and e-commerce-specific patterns. The `with-routes` proxy (composed within `proxy.ts`) essentially turns Next.js into a headless CMS router, where content structure is determined by the BigCommerce backend rather than the filesystem. Understanding this fundamental difference is crucial for working effectively with the codebase.\n\nThe Streamable pattern and PPR integration provide excellent user experience through progressive loading, but require understanding of React's newer concurrent features like the `use()` hook and Suspense boundaries.\n"
  },
  {
    "path": "core/CHANGELOG.md",
    "content": "# Changelog\n\n## 1.6.2\n\n### Patch Changes\n\n- [#2947](https://github.com/bigcommerce/catalyst/pull/2947) [`e198d89`](https://github.com/bigcommerce/catalyst/commit/e198d8966d589bd6707cdb1588986c9c092d73be) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add root-level not-found page so /404 renders a branded page instead of the default Vercel error screen\n\n- [#2945](https://github.com/bigcommerce/catalyst/pull/2945) [`4479964`](https://github.com/bigcommerce/catalyst/commit/447996400e6fcc6388937011e101d802308e6b33) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n## 1.6.1\n\n### Patch Changes\n\n- [#2934](https://github.com/bigcommerce/catalyst/pull/2934) [`6a5b019`](https://github.com/bigcommerce/catalyst/commit/6a5b019083aa3e000e5989f6f13256b57c22c479) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Fix extra thick border on dropdown menu by changing `ring` (3px) to `ring-1` (1px) to match the Select component styling.\n\n## 1.6.0\n\n### Minor Changes\n\n- [#2896](https://github.com/bigcommerce/catalyst/pull/2896) [`fc84210`](https://github.com/bigcommerce/catalyst/commit/fc84210ab8562ce24320d3d0e3284ed318a4cbce) Thanks [@jamesqquick](https://github.com/jamesqquick)! - Add reCAPTCHA v2 support to storefront forms. The reCAPTCHA widget is rendered on the registration, contact, and product review forms when enabled in the BigCommerce admin. All validation and error handling is performed server-side in the corresponding form actions. The token is read from the native `g-recaptcha-response` field that the widget injects into the form, eliminating the need for manual token extraction on the client.\n\n  ## Migration steps\n\n  ### Step 1: Install dependencies\n\n  Add `react-google-recaptcha` and its type definitions:\n\n  ```bash\n  pnpm add react-google-recaptcha\n  pnpm add -D @types/react-google-recaptcha\n  ```\n\n  ### Step 2: Add the reCAPTCHA server library\n\n  Create `core/lib/recaptcha/constants.ts`:\n\n  ```ts\n  export interface ReCaptchaSettings {\n    isEnabledOnStorefront: boolean;\n    siteKey: string;\n  }\n\n  export const RECAPTCHA_TOKEN_FORM_KEY = 'g-recaptcha-response';\n  ```\n\n  Create `core/lib/recaptcha.ts` with the server-side helpers for fetching reCAPTCHA settings, extracting the token from form data, and asserting the token is present. See the file in this release for the full implementation.\n\n  ### Step 3: Add reCAPTCHA translation strings\n\n  Update `core/messages/en.json` to add the `recaptchaRequired` message in each form namespace:\n\n  ```diff\n    \"Auth\": {\n      \"Register\": {\n  +     \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n  ```\n\n  ```diff\n    \"Product\": {\n      \"Reviews\": {\n        \"Form\": {\n  +       \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n  ```\n\n  ```diff\n    \"WebPages\": {\n      \"ContactUs\": {\n        \"Form\": {\n  +       \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n  ```\n\n  ```diff\n    \"Form\": {\n  +   \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n  ```\n\n  ### Step 4: Update GraphQL mutations to accept reCAPTCHA token\n\n  Update `core/app/[locale]/(default)/(auth)/register/_actions/register-customer.ts`:\n\n  ```diff\n  + import { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha';\n    ...\n    const RegisterCustomerMutation = graphql(`\n  -   mutation RegisterCustomerMutation($input: RegisterCustomerInput!) {\n  +   mutation RegisterCustomerMutation(\n  +     $input: RegisterCustomerInput!\n  +     $reCaptchaV2: ReCaptchaV2Input\n  +   ) {\n        customer {\n  -       registerCustomer(input: $input) {\n  +       registerCustomer(input: $input, reCaptchaV2: $reCaptchaV2) {\n  ```\n\n  Update `core/app/[locale]/(default)/product/[slug]/_actions/submit-review.ts`:\n\n  ```diff\n  + import { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha';\n    ...\n    const AddProductReviewMutation = graphql(`\n  -   mutation AddProductReviewMutation($input: AddProductReviewInput!) {\n  +   mutation AddProductReviewMutation(\n  +     $input: AddProductReviewInput!\n  +     $reCaptchaV2: ReCaptchaV2Input\n  +   ) {\n        catalog {\n  -       addProductReview(input: $input) {\n  +       addProductReview(input: $input, reCaptchaV2: $reCaptchaV2) {\n  ```\n\n  Update `core/app/[locale]/(default)/webpages/[id]/contact/_actions/submit-contact-form.ts`:\n\n  ```diff\n  + import { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha';\n    ...\n    const SubmitContactUsMutation = graphql(`\n  -   mutation SubmitContactUsMutation($input: SubmitContactUsInput!) {\n  -     submitContactUs(input: $input) {\n  +   mutation SubmitContactUsMutation($input: SubmitContactUsInput!, $reCaptchaV2: ReCaptchaV2Input) {\n  +     submitContactUs(input: $input, reCaptchaV2: $reCaptchaV2) {\n  ```\n\n  ### Step 5: Add server-side reCAPTCHA validation to form actions\n\n  In each of the three server actions above, add the validation block after the `parseWithZod` check and pass the token to the GraphQL mutation. For example in `register-customer.ts`:\n\n  ```diff\n  +   const { siteKey, token } = await getRecaptchaFromForm(formData);\n  +   const recaptchaValidation = assertRecaptchaTokenPresent(siteKey, token, t('recaptchaRequired'));\n  +\n  +   if (!recaptchaValidation.success) {\n  +     return {\n  +       lastResult: submission.reply({ formErrors: recaptchaValidation.formErrors }),\n  +     };\n  +   }\n      ...\n      const response = await client.fetch({\n        document: RegisterCustomerMutation,\n        variables: {\n          input,\n  +       reCaptchaV2:\n  +         recaptchaValidation.token != null ? { token: recaptchaValidation.token } : undefined,\n        },\n  ```\n\n  Apply the same pattern to `submit-review.ts` and `submit-contact-form.ts`.\n\n  ### Step 6: Pass `recaptchaSiteKey` to form components\n\n  Fetch the site key in each page and pass it down through the component tree.\n\n  Update `core/app/[locale]/(default)/(auth)/register/page.tsx`:\n\n  ```diff\n  + import { getRecaptchaSiteKey } from '~/lib/recaptcha';\n    ...\n  + const recaptchaSiteKey = await getRecaptchaSiteKey();\n    ...\n    <DynamicFormSection\n  +   recaptchaSiteKey={recaptchaSiteKey}\n  ```\n\n  Update `core/app/[locale]/(default)/product/[slug]/page.tsx`:\n\n  ```diff\n  + import { getRecaptchaSiteKey } from '~/lib/recaptcha';\n    ...\n  - const { product: baseProduct, settings } = await getProduct(productId, customerAccessToken);\n  + const [{ product: baseProduct, settings }, recaptchaSiteKey] = await Promise.all([\n  +   getProduct(productId, customerAccessToken),\n  +   getRecaptchaSiteKey(),\n  + ]);\n    ...\n    <ProductDetail\n  +   recaptchaSiteKey={recaptchaSiteKey}\n    ...\n    <Reviews\n  +   recaptchaSiteKey={recaptchaSiteKey}\n  ```\n\n  Update `core/app/[locale]/(default)/webpages/[id]/contact/page.tsx`:\n\n  ```diff\n  + import { getRecaptchaSiteKey } from '~/lib/recaptcha';\n    ...\n  + const recaptchaSiteKey = await getRecaptchaSiteKey();\n    ...\n    <DynamicForm\n  +   recaptchaSiteKey={recaptchaSiteKey}\n  ```\n\n  ### Step 7: Render the reCAPTCHA widget in form components\n\n  Update `core/vibes/soul/form/dynamic-form/index.tsx`:\n\n  ```diff\n  + import RecaptchaWidget from 'react-google-recaptcha';\n    ...\n    export interface DynamicFormProps<F extends Field> {\n  +   recaptchaSiteKey?: string;\n    }\n    ...\n  +         {recaptchaSiteKey ? <RecaptchaWidget sitekey={recaptchaSiteKey} /> : null}\n  ```\n\n  Update `core/vibes/soul/sections/reviews/review-form.tsx`:\n\n  ```diff\n  + import RecaptchaWidget from 'react-google-recaptcha';\n    ...\n    interface Props {\n  +   recaptchaSiteKey?: string;\n    }\n    ...\n  +           {recaptchaSiteKey ? (\n  +             <div>\n  +               <RecaptchaWidget sitekey={recaptchaSiteKey} />\n  +             </div>\n  +           ) : null}\n  ```\n\n  ### Step 8: Thread `recaptchaSiteKey` through intermediate components\n\n  Add the `recaptchaSiteKey?: string` prop and pass it through in:\n  - `core/vibes/soul/sections/dynamic-form-section/index.tsx`\n  - `core/vibes/soul/sections/product-detail/index.tsx`\n  - `core/vibes/soul/sections/reviews/index.tsx`\n  - `core/app/[locale]/(default)/product/[slug]/_components/reviews.tsx`\n\n  Each of these accepts the prop and forwards it to the form component that renders the widget.\n\n### Patch Changes\n\n- [#2925](https://github.com/bigcommerce/catalyst/pull/2925) [`4e2f8f8`](https://github.com/bigcommerce/catalyst/commit/4e2f8f855ebcde561ae93b98f648fa05474aedf8) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n## 1.5.0\n\n### Minor Changes\n\n- [#2905](https://github.com/bigcommerce/catalyst/pull/2905) [`6f788e9`](https://github.com/bigcommerce/catalyst/commit/6f788e95dbf6e21bd025a32173f5e2c8e81aee46) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Use dynamic imports for next/headers, next/navigation, and next-intl/server in the client module to avoid AsyncLocalStorage poisoning during next.config.ts resolution\n\n- [#2801](https://github.com/bigcommerce/catalyst/pull/2801) [`18cfdc8`](https://github.com/bigcommerce/catalyst/commit/18cfdc8ca018c33ea49b462ecb6f055a153cd4ab) Thanks [@Tharaae](https://github.com/Tharaae)! - Fetch product inventory data with a separate GQL query with no caching\n\n  ## Migration\n\n  The files to be rebased for this change to be applied are:\n  - core/app/[locale]/(default)/product/[slug]/page-data.ts\n  - core/app/[locale]/(default)/product/[slug]/page.tsx\n\n- [#2863](https://github.com/bigcommerce/catalyst/pull/2863) [`6a23c90`](https://github.com/bigcommerce/catalyst/commit/6a23c90714b2218db45f17cebe395b21753157e7) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add pagination support for the product gallery. When a product has more images than the initial page load, new images will load as batches once the user reaches the end of the existing thumbnails. Thumbnail images now will display in horizontal direction in all viewport sizes.\n\n  ## Migration\n  1. Create the new server action file `core/app/[locale]/(default)/product/[slug]/_actions/get-more-images.ts` with a GraphQL query to fetch additional product images with pagination.\n  2. Update the product page data fetching in `core/app/[locale]/(default)/product/[slug]/page-data.ts` to include `pageInfo` (with `hasNextPage` and `endCursor`) from the images query.\n  3. Update `core/app/[locale]/(default)/product/[slug]/page.tsx` to pass the new pagination props (`pageInfo`, `productId`, `loadMoreAction`) to the `ProductDetail` component.\n  4. The `ProductGallery` component now accepts optional props for pagination:\n     - `pageInfo?: { hasNextPage: boolean; endCursor: string | null }`\n     - `productId?: number`\n     - `loadMoreAction?: ProductGalleryLoadMoreAction`\n\n  Due to the number of changes, it is recommended to use the PR as a reference for migration.\n\n- [#2758](https://github.com/bigcommerce/catalyst/pull/2758) [`d78bc85`](https://github.com/bigcommerce/catalyst/commit/d78bc85fa4a6ae39d2b99a347a3f9fc56725826a) Thanks [@Tharaae](https://github.com/Tharaae)! - Add the following messages to each line item on cart page based on store inventory settings:\n  - Fully/partially out-of-stock message if enabled on the store and the line item is currently out of stock\n  - Ready-to-ship quantity if enabled on the store\n  - Backordered quantity if enabled on the store\n\n  ## Migration\n\n  For existing Catalyst stores, to get the newly added feature, simply rebase the existing code with the new release code. The files to be rebased for this change to be applied are:\n  - core/app/[locale]/(default)/cart/page-data.ts\n  - core/app/[locale]/(default)/cart/page.tsx\n  - core/messages/en.json\n  - core/vibes/soul/sections/cart/client.tsx\n\n- [#2907](https://github.com/bigcommerce/catalyst/pull/2907) [`35adccb`](https://github.com/bigcommerce/catalyst/commit/35adccb0429f462efaa5bfb4cabf4d51b4a62522) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Upgrade Next.js to v16 and align peer dependencies.\n\n  To migrate your Catalyst storefront to Next.js 16:\n  - Update `next` to `^16.0.0` in your `package.json` and install dependencies.\n  - Replace any usage of `unstable_expireTag` with `revalidateTag` and `unstable_expirePath` with `revalidatePath` from `next/cache`.\n  - Update `tsconfig.json` to use `\"moduleResolution\": \"bundler\"` and `\"module\": \"nodenext\"` as required by Next.js 16.\n  - Address Next.js 16 deprecation lint errors (e.g. legacy `<img>` elements, missing `rel=\"noopener noreferrer\"` on external links).\n  - Rename `middleware.ts` to `proxy.ts` and change `export const middleware` to `export const proxy` (Next.js 16 proxy pattern).\n  - Ensure you are running Node.js 24+ (proxy runs on the Node.js runtime, not Edge).\n\n### Patch Changes\n\n- [#2916](https://github.com/bigcommerce/catalyst/pull/2916) [`e3185b6`](https://github.com/bigcommerce/catalyst/commit/e3185b6f0612424af01b1515b440b6f43988da66) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Fix analytics visit count inflation by implementing a sliding window for the visit cookie TTL, guarding against prefetch/RSC requests creating spurious visits, and reordering middleware so analytics cookies survive locale redirects.\n\n- [#2852](https://github.com/bigcommerce/catalyst/pull/2852) [`a7395f1`](https://github.com/bigcommerce/catalyst/commit/a7395f1a6778fe93080e8fcb05dce423cbc3acc0) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Uses regular `dompurify` (DP) instead of `isomorphic-dompurify` (IDP), because IDP requires JSDOM. JSDOM doesn't work in edge-runtime environments even with nodejs compatibility. We only need it on the client anyways for the JSON-LD schema, so it doesn't need the isomorphic aspect of it. This also changes `core/app/[locale]/(default)/product/[slug]/_components/product-review-schema/product-review-schema.tsx` to be a client-component to enable `dompurify to work correctly.\n\n  ## Migration\n  1. Remove the old dependency and add the new:\n\n  ```bash\n  pnpm rm isomorphic-dompurify\n  pnpm add dompurify -S\n  ```\n\n  2. Change the import in `core/app/[locale]/(default)/product/[slug]/_components/product-review-schema/product-review-schema.tsx`:\n\n  ```diff\n  - import DOMPurify from 'isomorphic-dompurify';\n  +// eslint-disable-next-line import/no-named-as-default\n  +import DOMPurify from 'dompurify';\n  ```\n\n  3. Add the `'use client';` directive to the top of `core/app/[locale]/(default)/product/[slug]/_components/product-review-schema/product-review-schema.tsx`.\n\n- [#2844](https://github.com/bigcommerce/catalyst/pull/2844) [`74dee6e`](https://github.com/bigcommerce/catalyst/commit/74dee6e6cafc57ea0e6eea94aafc4b38063352b1) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Update forms to translate the form field validation errors\n\n  ## Migration\n\n  Due to the amount of changes, it is recommended to just use the PR as a reference for migration.\n\n  Detailed migration steps can be found on the PR here:\n  https://github.com/bigcommerce/catalyst/pull/2844\n\n- [#2901](https://github.com/bigcommerce/catalyst/pull/2901) [`8b5fee6`](https://github.com/bigcommerce/catalyst/commit/8b5fee6a1f396f000748d3e9bb65e44383148eb4) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Fix GiftCertificateCard not updating when selecting a new amount on the gift certificate purchase form\n\n- [#2858](https://github.com/bigcommerce/catalyst/pull/2858) [`0633612`](https://github.com/bigcommerce/catalyst/commit/06336122585db5021de9028ed88e2bc48c6faede) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Use state abbreviation instead of entityId for cart shipping form state values. The shipping API expects state abbreviations, and using entityId caused form submissions to fail. Additionally, certain US military states that share the same abbreviation (AE) are now filtered out to prevent duplicate key issues and ambiguous submissions.\n\n  ## Migration steps\n\n  ### Step 1: Add blacklist for states with duplicate abbreviations\n\n  Certain US states share the same abbreviation (AE), which causes issues with the shipping API and React select dropdowns. Add a blacklist to filter these out.\n\n  Update `core/app/[locale]/(default)/cart/page.tsx`:\n\n  ```diff\n    const countries = shippingCountries.map((country) => ({\n      value: country.code,\n      label: country.name,\n    }));\n\n  + // These US states share the same abbreviation (AE), which causes issues:\n  + // 1. The shipping API uses abbreviations, so it can't distinguish between them\n  + // 2. React select dropdowns require unique keys, causing duplicate key warnings\n  + const blacklistedUSStates = new Set([\n  +   'Armed Forces Africa',\n  +   'Armed Forces Canada',\n  +   'Armed Forces Middle East',\n  + ]);\n\n    const statesOrProvinces = shippingCountries.map((country) => ({\n  ```\n\n  ### Step 2: Use state abbreviation instead of entityId\n\n  Update the state mapping to use `abbreviation` instead of `entityId`, and apply the blacklist filter for US states.\n\n  Update `core/app/[locale]/(default)/cart/page.tsx`:\n\n  ```diff\n    const statesOrProvinces = shippingCountries.map((country) => ({\n      country: country.code,\n  -   states: country.statesOrProvinces.map((state) => ({\n  -     value: state.entityId.toString(),\n  -     label: state.name,\n  -   })),\n  +   states: country.statesOrProvinces\n  +     .filter((state) => country.code !== 'US' || !blacklistedUSStates.has(state.name))\n  +     .map((state) => ({\n  +       value: state.abbreviation,\n  +       label: state.name,\n  +     })),\n    }));\n  ```\n\n- [#2856](https://github.com/bigcommerce/catalyst/pull/2856) [`f5330c7`](https://github.com/bigcommerce/catalyst/commit/f5330c7248b2e3a32b2bfbb8e3bc6c11742a5d27) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add canonical URLs and hreflang alternates for SEO. Pages now set `alternates.canonical` and `alternates.languages` in `generateMetadata` via the new `getMetadataAlternates` helper in `core/lib/seo/canonical.ts`. The helper fetches the vanity URL via GraphQL (`site.settings.url.vanityUrl`) and is cached per request. The default locale uses no path prefix; other locales use `/{locale}/path`. The root locale layout sets `metadataBase` to the configured vanity URL so canonical URLs resolve correctly. On Vercel preview deployments (`VERCEL_ENV=preview`), `metadataBase` and canonical/hreflang URLs use `VERCEL_URL` instead of the production vanity URL to prevent preview environments from generating SEO metadata pointing to production.\n\n  ## Migration steps\n\n  ### Step 1: Root layout metadata base\n\n  The root locale layout now sets `metadataBase` from the vanity URL fetched via GraphQL. On Vercel preview deployments, `VERCEL_URL` is used instead so preview environments don't point to production. `URL.canParse` guards against malformed URLs.\n\n  Update `core/app/[locale]/layout.tsx`:\n\n  ```diff\n  + const vanityUrl = data.site.settings?.url.vanityUrl;\n  +\n  + // Use preview deployment URL so metadataBase (canonical, og:url) points at the preview, not production.\n  + let baseUrl: URL | undefined;\n  + const previewUrl =\n  +   process.env.VERCEL_ENV === 'preview' ? `https://${process.env.VERCEL_URL}` : undefined;\n  +\n  + if (previewUrl && URL.canParse(previewUrl)) {\n  +   baseUrl = new URL(previewUrl);\n  + } else if (vanityUrl && URL.canParse(vanityUrl)) {\n  +   baseUrl = new URL(vanityUrl);\n  + }\n  +\n    return {\n  +   metadataBase: baseUrl,\n      title: {\n  ```\n\n  ### Step 2: Canonical/hreflang base URL for preview environments\n\n  The `getMetadataAlternates` function in `core/lib/seo/canonical.ts` now checks for a Vercel preview URL before falling back to the GraphQL vanity URL. `URL.canParse` guards against malformed URLs.\n\n  Update `core/lib/seo/canonical.ts`:\n\n  ```diff\n   export async function getMetadataAlternates(options: CanonicalUrlOptions) {\n     const { path, locale, includeAlternates = true } = options;\n\n  -  const baseUrl = await getVanityUrl();\n  +  // Use preview deployment URL so canonical/hreflang URLs point at the preview, not production.\n  +  const previewUrl =\n  +    process.env.VERCEL_ENV === 'preview' ? `https://${process.env.VERCEL_URL}` : undefined;\n  +  const baseUrl = previewUrl && URL.canParse(previewUrl) ? previewUrl : await getVanityUrl();\n  ```\n\n  ### Step 3: GraphQL fragment updates\n\n  Add the `path` field to brand, blog post, and product queries so metadata can build canonical URLs.\n\n  Update `core/app/[locale]/(default)/(faceted)/brand/[slug]/page-data.ts`:\n\n  ```diff\n    site {\n      brand(entityId: $entityId) {\n        name\n  +     path\n        seo {\n  ```\n\n  Update `core/app/[locale]/(default)/blog/[blogId]/page-data.ts`:\n\n  ```diff\n    author\n    htmlBody\n    name\n  + path\n    publishedDate {\n  ```\n\n  Update `core/app/[locale]/(default)/product/[slug]/page-data.ts` (in the metadata query):\n\n  ```diff\n    site {\n      product(entityId: $entityId) {\n        name\n  +     path\n        defaultImage {\n  ```\n\n  ### Step 4: Page metadata alternates\n\n  Add the `getMetadataAlternates` import and set `alternates` in `generateMetadata` for each page. The function is async and must be awaited. Ensure `core/lib/seo/canonical.ts` exists (it is included in this release).\n\n  Update `core/app/[locale]/(default)/page.tsx` (home):\n\n  ```diff\n  + import { Metadata } from 'next';\n    import { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\n    ...\n  + import { getMetadataAlternates } from '~/lib/seo/canonical';\n    ...\n  + export async function generateMetadata({ params }: Props): Promise<Metadata> {\n  +   const { locale } = await params;\n  +   return {\n  +     alternates: await getMetadataAlternates({ path: '/', locale }),\n  +   };\n  + }\n  +\n    export default async function Home({ params }: Props) {\n  ```\n\n  For entity pages (product, category, brand, blog, blog post, webpage), add the import and include `alternates` in the existing `generateMetadata` return value using the entity `path` (or breadcrumb-derived path for category and webpage). Example for a brand page:\n\n  ```diff\n  + import { getMetadataAlternates } from '~/lib/seo/canonical';\n    ...\n    export async function generateMetadata(props: Props): Promise<Metadata> {\n  -   const { slug } = await props.params;\n  +   const { slug, locale } = await props.params;\n      ...\n      return {\n        title: pageTitle || brand.name,\n        description: metaDescription,\n        keywords: metaKeywords ? metaKeywords.split(',') : null,\n  +     alternates: await getMetadataAlternates({ path: brand.path, locale }),\n      };\n    }\n  ```\n\n  ### Step 5: Gift certificates pages\n\n  Update `core/app/[locale]/(default)/gift-certificates/page.tsx`:\n\n  ```diff\n  + import { getMetadataAlternates } from '~/lib/seo/canonical';\n    ...\n    export async function generateMetadata({ params }: Props): Promise<Metadata> {\n      const { locale } = await params;\n      const t = await getTranslations({ locale, namespace: 'GiftCertificates' });\n\n      return {\n        title: t('title') || 'Gift certificates',\n  +     alternates: await getMetadataAlternates({ path: '/gift-certificates', locale }),\n      };\n    }\n  ```\n\n  Update `core/app/[locale]/(default)/gift-certificates/balance/page.tsx`:\n\n  ```diff\n  + import { getMetadataAlternates } from '~/lib/seo/canonical';\n    ...\n      return {\n        title: t('title') || 'Gift certificates - Check balance',\n  +     alternates: await getMetadataAlternates({ path: '/gift-certificates/balance', locale }),\n      };\n  ```\n\n  Add `generateMetadata` to `core/app/[locale]/(default)/gift-certificates/purchase/page.tsx`:\n\n  ```diff\n  + import { Metadata } from 'next';\n    import { getFormatter, getTranslations } from 'next-intl/server';\n    ...\n  + import { getMetadataAlternates } from '~/lib/seo/canonical';\n    ...\n  + export async function generateMetadata({ params }: Props): Promise<Metadata> {\n  +   const { locale } = await params;\n  +   const t = await getTranslations({ locale, namespace: 'GiftCertificates' });\n  +\n  +   return {\n  +     title: t('Purchase.title'),\n  +     alternates: await getMetadataAlternates({ path: '/gift-certificates/purchase', locale }),\n  +   };\n  + }\n  ```\n\n  ### Step 6: Contact page\n\n  Update `core/app/[locale]/(default)/webpages/[id]/contact/page.tsx`:\n\n  ```diff\n  + import { getMetadataAlternates } from '~/lib/seo/canonical';\n    ...\n    export async function generateMetadata({ params }: Props): Promise<Metadata> {\n  -   const { id } = await params;\n  +   const { id, locale } = await params;\n      const webpage = await getWebPage(id);\n      const { pageTitle, metaDescription, metaKeywords } = webpage.seo;\n\n      return {\n        title: pageTitle || webpage.title,\n        description: metaDescription,\n        keywords: metaKeywords ? metaKeywords.split(',') : null,\n  +     alternates: await getMetadataAlternates({ path: webpage.path, locale }),\n      };\n    }\n  ```\n\n  ### Step 7: Public wishlist page\n\n  Update `core/app/[locale]/(default)/wishlist/[token]/page.tsx`:\n\n  ```diff\n  + import { getMetadataAlternates } from '~/lib/seo/canonical';\n    ...\n    export async function generateMetadata({ params, searchParams }: Props): Promise<Metadata> {\n      const { locale, token } = await params;\n      ...\n      return {\n        title: wishlist?.name ?? t('title'),\n  +     alternates: await getMetadataAlternates({ path: `/wishlist/${token}`, locale }),\n      };\n    }\n  ```\n\n  ### Step 8: Compare page\n\n  Update `core/app/[locale]/(default)/compare/page.tsx`:\n\n  ```diff\n  + import { getMetadataAlternates } from '~/lib/seo/canonical';\n    ...\n    export async function generateMetadata({ params }: Props): Promise<Metadata> {\n      const { locale } = await params;\n      const t = await getTranslations({ locale, namespace: 'Compare' });\n\n      return {\n        title: t('title'),\n  +     alternates: await getMetadataAlternates({ path: '/compare', locale }),\n      };\n    }\n  ```\n\n- [#2898](https://github.com/bigcommerce/catalyst/pull/2898) [`46ee3de`](https://github.com/bigcommerce/catalyst/commit/46ee3de640f030be56111c668102bbeeb961b4a4) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Conditionally include optional SEO metadata fields in `generateMetadata` across page files. Fields `description`, `keywords`, `alternates`, and `openGraph` are now only included in the returned metadata object when they have a value, using spread syntax (`...(value && { key: value })`). Previously, these fields were always set — potentially assigning `null` or an empty string — which could cause Next.js to render empty `<meta>` tags.\n\n  ## Migration steps\n\n  Update `generateMetadata` in the following pages to use conditional spread syntax for optional metadata fields:\n\n  ### brand, category, webpages (contact + normal)\n\n  ```diff\n    return {\n      title: pageTitle || entity.name,\n  -   description: metaDescription,\n  -   keywords: metaKeywords ? metaKeywords.split(',') : null,\n  +   ...(metaDescription && { description: metaDescription }),\n  +   ...(metaKeywords && { keywords: metaKeywords.split(',') }),\n    };\n  ```\n\n  For `brand/[slug]/page.tsx`, also guard the `alternates` field:\n\n  ```diff\n  -   alternates: await getMetadataAlternates({ path: brand.path, locale }),\n  +   ...(brand.path && { alternates: await getMetadataAlternates({ path: brand.path, locale }) }),\n  ```\n\n  ### blog/[blogId]/page.tsx\n\n  ```diff\n    return {\n      title: pageTitle || blogPost.name,\n  -   description: metaDescription,\n  -   keywords: metaKeywords ? metaKeywords.split(',') : null,\n  +   ...(metaDescription && { description: metaDescription }),\n  +   ...(metaKeywords && { keywords: metaKeywords.split(',') }),\n      ...(blogPost.path && {\n        alternates: await getMetadataAlternates({ path: blogPost.path, locale }),\n      }),\n    };\n  ```\n\n  ### product/[slug]/page.tsx\n\n  ```diff\n  - keywords: metaKeywords ? metaKeywords.split(',') : null,\n  + ...(metaKeywords && { keywords: metaKeywords.split(',') }),\n  - openGraph: url\n  -   ? {\n  -       images: [{ url, alt }],\n  -     }\n  -   : null,\n  + ...(url && { openGraph: { images: [{ url, alt }] } }),\n  ```\n\n  ### blog/page.tsx\n\n  Extract the description to a variable and spread conditionally:\n\n  ```diff\n  + const description =\n  +   blog?.description && blog.description.length > 150\n  +     ? `${blog.description.substring(0, 150)}...`\n  +     : blog?.description;\n  +\n    return {\n      title: blog?.name ?? t('title'),\n  -   description:\n  -     blog?.description && blog.description.length > 150\n  -       ? `${blog.description.substring(0, 150)}...`\n  -       : blog?.description,\n  +   ...(description && { description }),\n      ...(blog?.path && { alternates: await getMetadataAlternates({ path: blog.path, locale }) }),\n    };\n  ```\n\n- [#2897](https://github.com/bigcommerce/catalyst/pull/2897) [`8d128fc`](https://github.com/bigcommerce/catalyst/commit/8d128fc75006ef8ab330e3597bfcf15cdc70da71) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n## 1.4.2\n\n### Patch Changes\n\n- [#2842](https://github.com/bigcommerce/catalyst/pull/2842) [`aadc1e3`](https://github.com/bigcommerce/catalyst/commit/aadc1e35533733905e7ce9ada457c2679995a727) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Addresses https://vercel.com/changelog/summary-of-cve-2026-23864 by bumping React and Next.js\n\n## 1.4.1\n\n### Patch Changes\n\n- [#2827](https://github.com/bigcommerce/catalyst/pull/2827) [`49b1097`](https://github.com/bigcommerce/catalyst/commit/49b1097c5d4f56b8c3a8d3d97181b09cb79a4070) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Filter out child cart items (items with `parentEntityId`) from cart and cart analytics to prevent duplicate line items when products have parent-child relationships, such as product bundles.\n\n  ## Migration steps\n\n  ### Step 1: GraphQL Fragment Updates\n\n  The `parentEntityId` field has been added to both physical and digital cart item fragments to identify child items.\n\n  Update `core/app/[locale]/(default)/cart/page-data.ts`:\n\n  ```diff\n    export const PhysicalItemFragment = graphql(`\n      fragment PhysicalItemFragment on CartPhysicalItem {\n        entityId\n        quantity\n        productEntityId\n        variantEntityId\n  +     parentEntityId\n        listPrice {\n          currencyCode\n          value\n        }\n      }\n    `);\n\n    export const DigitalItemFragment = graphql(`\n      fragment DigitalItemFragment on CartDigitalItem {\n        entityId\n        quantity\n        productEntityId\n        variantEntityId\n  +     parentEntityId\n        listPrice {\n          currencyCode\n          value\n        }\n      }\n    `);\n  ```\n\n  ### Step 2: Cart Display Filtering\n\n  Cart line items are now filtered to exclude child items when displaying the cart.\n\n  Update `core/app/[locale]/(default)/cart/page.tsx`:\n\n  ```diff\n    const lineItems = [\n      ...cart.lineItems.giftCertificates,\n      ...cart.lineItems.physicalItems,\n      ...cart.lineItems.digitalItems,\n  - ];\n  + ].filter((item) => !('parentEntityId' in item) || !item.parentEntityId);\n  ```\n\n  ### Step 3: Analytics Data Filtering\n\n  Analytics data collection now only includes top-level items to prevent duplicate tracking.\n\n  Update `core/app/[locale]/(default)/cart/page.tsx` in the `getAnalyticsData` function:\n\n  ```diff\n  - const lineItems = [...cart.lineItems.physicalItems, ...cart.lineItems.digitalItems];\n  + const lineItems = [...cart.lineItems.physicalItems, ...cart.lineItems.digitalItems].filter(\n  +   (item) => !item.parentEntityId, // Only include top-level items\n  + );\n  ```\n\n  ### Step 4: Styling Update\n\n  Cart subtitle text color has been updated for improved contrast.\n\n  Update `core/vibes/soul/sections/cart/client.tsx`:\n\n  ```diff\n  -                  <span className=\"text-[var(--cart-subtext-text,hsl(var(--contrast-300)))] contrast-more:text-[var(--cart-subtitle-text,hsl(var(--contrast-500)))]\">\n  +                  <span className=\"text-[var(--cart-subtext-text,hsl(var(--contrast-400)))] contrast-more:text-[var(--cart-subtitle-text,hsl(var(--contrast-500)))]\">\n                       {lineItem.subtitle}\n                     </span>\n  ```\n\n- [#2811](https://github.com/bigcommerce/catalyst/pull/2811) [`b57bffa`](https://github.com/bigcommerce/catalyst/commit/b57bffaf28b8c9714f99f7af581871f6e9f6f944) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Fix pagination cursor persistence when changing sort order. The `before` and `after` query parameters are now cleared when the sort option changes, preventing stale pagination cursors from causing incorrect results or empty pages.\n\n- [#2833](https://github.com/bigcommerce/catalyst/pull/2833) [`a520dbc`](https://github.com/bigcommerce/catalyst/commit/a520dbcf14e78b54e10a38c05e11f427b30431c1) Thanks [@jamesqquick](https://github.com/jamesqquick)! - Add placeholders for gift certificate inputs and remove redundant placeholders in the gift certificate purchase form.\n\n- [#2818](https://github.com/bigcommerce/catalyst/pull/2818) [`74e4dd1`](https://github.com/bigcommerce/catalyst/commit/74e4dd11ebf00c312695013c86aab29930fd7a53) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Disable product filters that are no longer available based on the selection.\n\n  ## Migration steps\n\n  ### Step 1\n\n  Update the `facetsTransformer` function in `core/data-transformers/facets-transformer.ts` to handle disabled filters:\n\n  ```diff\n    return allFacets.map((facet) => {\n      const refinedFacet = refinedFacets.find((f) => f.displayName === facet.displayName);\n\n  +    if (refinedFacet == null) {\n  +      return null;\n  +    }\n  +\n      if (facet.__typename === 'CategorySearchFilter') {\n        const refinedCategorySearchFilter =\n  -        refinedFacet?.__typename === 'CategorySearchFilter' ? refinedFacet : null;\n  +        refinedFacet.__typename === 'CategorySearchFilter' ? refinedFacet : null;\n\n        return {\n          type: 'toggle-group' as const,\n          paramName: 'categoryIn',\n          label: facet.displayName,\n          defaultCollapsed: facet.isCollapsedByDefault,\n          options: facet.categories.map((category) => {\n            const refinedCategory = refinedCategorySearchFilter?.categories.find(\n              (c) => c.entityId === category.entityId,\n            );\n            const isSelected = filters.categoryEntityIds?.includes(category.entityId) === true;\n  +          const disabled = refinedCategory == null && !isSelected;\n  +          const productCountLabel = disabled ? '' : ` (${category.productCount})`;\n  +          const label = facet.displayProductCount\n  +            ? `${category.name}${productCountLabel}`\n  +            : category.name;\n\n            return {\n  -            label: facet.displayProductCount\n  -              ? `${category.name} (${category.productCount})`\n  -              : category.name,\n  +            label,\n              value: category.entityId.toString(),\n  -            disabled: refinedCategory == null && !isSelected,\n  +            disabled,\n            };\n          }),\n        };\n      }\n\n      if (facet.__typename === 'BrandSearchFilter') {\n        const refinedBrandSearchFilter =\n  -        refinedFacet?.__typename === 'BrandSearchFilter' ? refinedFacet : null;\n  +        refinedFacet.__typename === 'BrandSearchFilter' ? refinedFacet : null;\n\n        return {\n          type: 'toggle-group' as const,\n          paramName: 'brand',\n          label: facet.displayName,\n          defaultCollapsed: facet.isCollapsedByDefault,\n          options: facet.brands.map((brand) => {\n            const refinedBrand = refinedBrandSearchFilter?.brands.find(\n              (b) => b.entityId === brand.entityId,\n            );\n            const isSelected = filters.brandEntityIds?.includes(brand.entityId) === true;\n  +          const disabled = refinedBrand == null && !isSelected;\n  +          const productCountLabel = disabled ? '' : ` (${brand.productCount})`;\n  +          const label = facet.displayProductCount\n  +            ? `${brand.name}${productCountLabel}`\n  +            : brand.name;\n\n            return {\n  -            label: facet.displayProductCount ? `${brand.name} (${brand.productCount})` : brand.name,\n  +            label,\n              value: brand.entityId.toString(),\n  -            disabled: refinedBrand == null && !isSelected,\n  +            disabled,\n            };\n          }),\n        };\n      }\n\n      if (facet.__typename === 'ProductAttributeSearchFilter') {\n        const refinedProductAttributeSearchFilter =\n  -        refinedFacet?.__typename === 'ProductAttributeSearchFilter' ? refinedFacet : null;\n  +        refinedFacet.__typename === 'ProductAttributeSearchFilter' ? refinedFacet : null;\n\n        return {\n          type: 'toggle-group' as const,\n          paramName: `attr_${facet.filterKey}`,\n          label: facet.displayName,\n          defaultCollapsed: facet.isCollapsedByDefault,\n          options: facet.attributes.map((attribute) => {\n            const refinedAttribute = refinedProductAttributeSearchFilter?.attributes.find(\n              (a) => a.value === attribute.value,\n            );\n\n            const isSelected =\n              filters.productAttributes?.some((attr) => attr.values.includes(attribute.value)) ===\n              true;\n\n  +          const disabled = refinedAttribute == null && !isSelected;\n  +          const productCountLabel = disabled ? '' : ` (${attribute.productCount})`;\n  +          const label = facet.displayProductCount\n  +            ? `${attribute.value}${productCountLabel}`\n  +            : attribute.value;\n  +\n            return {\n  -            label: facet.displayProductCount\n  -              ? `${attribute.value} (${attribute.productCount})`\n  -              : attribute.value,\n  +            label,\n              value: attribute.value,\n  -            disabled: refinedAttribute == null && !isSelected,\n  +            disabled,\n            };\n          }),\n        };\n      }\n\n      if (facet.__typename === 'RatingSearchFilter') {\n        const refinedRatingSearchFilter =\n  -        refinedFacet?.__typename === 'RatingSearchFilter' ? refinedFacet : null;\n  +        refinedFacet.__typename === 'RatingSearchFilter' ? refinedFacet : null;\n        const isSelected = filters.rating?.minRating != null;\n\n        return {\n          type: 'rating' as const,\n          paramName: 'minRating',\n          label: facet.displayName,\n          disabled: refinedRatingSearchFilter == null && !isSelected,\n          defaultCollapsed: facet.isCollapsedByDefault,\n        };\n      }\n\n      if (facet.__typename === 'PriceSearchFilter') {\n        const refinedPriceSearchFilter =\n  -        refinedFacet?.__typename === 'PriceSearchFilter' ? refinedFacet : null;\n  +        refinedFacet.__typename === 'PriceSearchFilter' ? refinedFacet : null;\n        const isSelected = filters.price?.minPrice != null || filters.price?.maxPrice != null;\n\n        return {\n          type: 'range' as const,\n          minParamName: 'minPrice',\n          maxParamName: 'maxPrice',\n          label: facet.displayName,\n          min: facet.selected?.minPrice ?? undefined,\n          max: facet.selected?.maxPrice ?? undefined,\n          disabled: refinedPriceSearchFilter == null && !isSelected,\n          defaultCollapsed: facet.isCollapsedByDefault,\n        };\n      }\n\n      if (facet.freeShipping) {\n        const refinedFreeShippingSearchFilter =\n  -        refinedFacet?.__typename === 'OtherSearchFilter' && refinedFacet.freeShipping\n  +        refinedFacet.__typename === 'OtherSearchFilter' && refinedFacet.freeShipping\n            ? refinedFacet\n            : null;\n        const isSelected = filters.isFreeShipping === true;\n\n        return {\n          type: 'toggle-group' as const,\n          paramName: `shipping`,\n          label: t('freeShippingLabel'),\n          defaultCollapsed: facet.isCollapsedByDefault,\n          options: [\n            {\n              label: t('freeShippingLabel'),\n              value: 'free_shipping',\n              disabled: refinedFreeShippingSearchFilter == null && !isSelected,\n            },\n          ],\n        };\n      }\n\n      if (facet.isFeatured) {\n        const refinedIsFeaturedSearchFilter =\n  -        refinedFacet?.__typename === 'OtherSearchFilter' && refinedFacet.isFeatured\n  +        refinedFacet.__typename === 'OtherSearchFilter' && refinedFacet.isFeatured\n            ? refinedFacet\n            : null;\n        const isSelected = filters.isFeatured === true;\n\n        return {\n          type: 'toggle-group' as const,\n          paramName: `isFeatured`,\n          label: t('isFeaturedLabel'),\n          defaultCollapsed: facet.isCollapsedByDefault,\n          options: [\n            {\n              label: t('isFeaturedLabel'),\n              value: 'on',\n              disabled: refinedIsFeaturedSearchFilter == null && !isSelected,\n            },\n          ],\n        };\n      }\n\n      if (facet.isInStock) {\n        const refinedIsInStockSearchFilter =\n  -        refinedFacet?.__typename === 'OtherSearchFilter' && refinedFacet.isInStock\n  +        refinedFacet.__typename === 'OtherSearchFilter' && refinedFacet.isInStock\n            ? refinedFacet\n            : null;\n        const isSelected = filters.hideOutOfStock === true;\n\n        return {\n          type: 'toggle-group' as const,\n          paramName: `stock`,\n          label: t('inStockLabel'),\n          defaultCollapsed: facet.isCollapsedByDefault,\n          options: [\n            {\n              label: t('inStockLabel'),\n              value: 'in_stock',\n              disabled: refinedIsInStockSearchFilter == null && !isSelected,\n            },\n          ],\n        };\n      }\n\n      return null;\n    });\n  ```\n\n  ### Step 2\n\n  Fix the disabled state CSS classes in `core/vibes/soul/form/toggle-group/index.tsx`:\n\n  ```diff\n            <ToggleGroupPrimitive.Item\n              aria-label={option.label}\n              className={clsx(\n  -              'data-disabled:pointer-events-none data-disabled:opacity-50 h-12 whitespace-nowrap rounded-full border px-4 font-body text-sm font-normal leading-normal transition-colors focus-visible:outline-0 focus-visible:ring-2',\n  +              'h-12 whitespace-nowrap rounded-full border px-4 font-body text-sm font-normal leading-normal transition-colors focus-visible:outline-0 focus-visible:ring-2 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n                {\n                  light:\n                    'border-[var(--toggle-group-light-border,hsl(var(--contrast-100)))] ring-[var(--toggle-group-light-focus,hsl(var(--primary)))] data-[state=on]:border-[var(--toggle-group-light-on-border,hsl(var(--foreground)))] data-[state=off]:bg-[var(--toggle-group-light-off-background,hsl(var(--background)))] data-[state=on]:bg-[var(--toggle-group-light-on-background,hsl(var(--foreground)))] data-[state=off]:text-[var(--toggle-group-light-off-text,hsl(var(--foreground)))] data-[state=on]:text-[var(--toggle-group-light-on-text,hsl(var(--background)))] data-[state=off]:hover:border-[var(--toggle-group-light-off-border-hover,hsl(var(--contrast-200)))] data-[state=off]:hover:bg-[var(--toggle-group-light-off-background-hover,hsl(var(--contrast-100)))]',\n  ```\n\n  ### Step 3\n\n  Update the FiltersPanel component in `core/vibes/soul/sections/products-list-section/filters-panel.tsx`\n\n  ```diff\n  import { clsx } from 'clsx';\n  import { parseAsString, useQueryStates } from 'nuqs';\n  -import { Suspense, useOptimistic, useState, useTransition } from 'react';\n  +import { useOptimistic, useState, useTransition } from 'react';\n\n  import { Checkbox } from '@/vibes/soul/form/checkbox';\n  import { RangeInput } from '@/vibes/soul/form/range-input';\n  import { ToggleGroup } from '@/vibes/soul/form/toggle-group';\n  -import { Streamable, useStreamable } from '@/vibes/soul/lib/streamable';\n  +import { Stream, Streamable, useStreamable } from '@/vibes/soul/lib/streamable';\n  import { Accordion, AccordionItem } from '@/vibes/soul/primitives/accordion';\n  import { Button } from '@/vibes/soul/primitives/button';\n  import { CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination';\n  import { Rating } from '@/vibes/soul/primitives/rating';\n  import { Link } from '~/components/link';\n\n  import { getFilterParsers } from './filter-parsers';\n  ```\n\n  ```diff\n    rangeFilterApplyLabel?: Streamable<string>;\n  }\n\n  +type InnerProps = Props & { filters: Filter[] };\n  +\n  function getParamCountLabel(params: Record<string, string | null | string[]>, key: string) {\n    const value = params[key];\n\n    if (Array.isArray(value) && value.length > 0) return `(${value.length})`;\n\n    return '';\n  }\n\n  export function FiltersPanel({\n    className,\n  -  filters,\n  +  filters: streamableFilters,\n    resetFiltersLabel,\n    rangeFilterApplyLabel,\n  }: Props) {\n    return (\n  -    <Suspense fallback={<FiltersSkeleton />}>\n  -      <FiltersPanelInner\n  -        className={className}\n  -        filters={filters}\n  -        rangeFilterApplyLabel={rangeFilterApplyLabel}\n  -        resetFiltersLabel={resetFiltersLabel}\n  -      />\n  -    </Suspense>\n  +    <Stream fallback={<FiltersSkeleton />} value={streamableFilters}>\n  +      {(filters) => (\n  +        <FiltersPanelInner\n  +          className={className}\n  +          filters={filters}\n  +          rangeFilterApplyLabel={rangeFilterApplyLabel}\n  +          resetFiltersLabel={resetFiltersLabel}\n  +        />\n  +      )}\n  +    </Stream>\n    );\n  }\n\n  export function FiltersPanelInner({\n    className,\n  -  filters: streamableFilters,\n  +  filters,\n    resetFiltersLabel: streamableResetFiltersLabel,\n    rangeFilterApplyLabel: streamableRangeFilterApplyLabel,\n    paginationInfo: streamablePaginationInfo,\n  -}: Props) {\n  -  const filters = useStreamable(streamableFilters);\n  +}: InnerProps) {\n    const resetFiltersLabel = useStreamable(streamableResetFiltersLabel) ?? 'Reset filters';\n    const rangeFilterApplyLabel = useStreamable(streamableRangeFilterApplyLabel);\n    const paginationInfo = useStreamable(streamablePaginationInfo);\n    const startCursorParamName = paginationInfo?.startCursorParamName ?? 'before';\n    const endCursorParamName = paginationInfo?.endCursorParamName ?? 'after';\n    const [params, setParams] = useQueryStates(\n      {\n        ...getFilterParsers(filters),\n        [startCursorParamName]: parseAsString,\n        [endCursorParamName]: parseAsString,\n      },\n      {\n        shallow: false,\n        history: 'push',\n      },\n    );\n    const [isPending, startTransition] = useTransition();\n    const [optimisticParams, setOptimisticParams] = useOptimistic(params);\n  -  const [accordionItems, setAccordionItems] = useState(() =>\n  +  const [expandedItems, setExpandedItems] = useState(() => {\n  +    const initial = new Set<string>();\n  +\n      filters\n        .filter((filter) => filter.type !== 'link-group')\n  -      .map((filter, index) => ({\n  -        key: index.toString(),\n  -        value: index.toString(),\n  +      .slice(0, 3)\n  +      .forEach((filter) => {\n  +        initial.add(filter.label.toLowerCase());\n  +      });\n  +\n  +    return initial;\n  +  });\n  +\n  +  const accordionItems = filters\n  +    .filter((filter) => filter.type !== 'link-group')\n  +    .map((filter) => {\n  +      return {\n  +        key: filter.label.toLowerCase(),\n  +        value: filter.label.toLowerCase(),\n          filter,\n  -        expanded: index < 3,\n  -      })),\n  -  );\n  +        expanded: expandedItems.has(filter.label.toLowerCase()),\n  +      };\n  +    });\n\n    if (filters.length === 0) return null;\n\n    const linkGroupFilters = filters.filter(\n      (filter): filter is LinkGroupFilter => filter.type === 'link-group',\n    );\n  ```\n\n  ```diff\n        ))}\n        <Accordion\n  -        onValueChange={(items) =>\n  -          setAccordionItems((prevItems) =>\n  -            prevItems.map((prevItem) => ({\n  -              ...prevItem,\n  -              expanded: items.includes(prevItem.value),\n  -            })),\n  -          )\n  -        }\n  +        onValueChange={(items) => {\n  +          setExpandedItems(new Set(items));\n  +        }}\n          type=\"multiple\"\n          value={accordionItems.filter((item) => item.expanded).map((item) => item.value)}\n        >\n  ```\n\n- [#2822](https://github.com/bigcommerce/catalyst/pull/2822) [`5c3e4d2`](https://github.com/bigcommerce/catalyst/commit/5c3e4d25c2d929af2b86cee3e4133838e5f3987b) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Run E2E tests on PRs, add 'required' prop to cart shipping calculator\n\n  ## Migration\n\n  Add `required` prop to the Country selector and Country input fields in `core/vibes/soul/sections/cart/shipping-form/index.tsx` on lines 280 and 289.\n\n- [#2813](https://github.com/bigcommerce/catalyst/pull/2813) [`ea9d633`](https://github.com/bigcommerce/catalyst/commit/ea9d6337d2bb8e5c166cb1de3385631e12fea4a3) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Delete duplicate Select component.\n\n- [#2823](https://github.com/bigcommerce/catalyst/pull/2823) [`dcad856`](https://github.com/bigcommerce/catalyst/commit/dcad8565d5eb835a5a68785f59811a67149d0137) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Refactor DynamicForm actions to decouple fields and passwordComplexity from state, passing them as separate arguments instead. This reduces state payload size by removing fields from state objects and stripping options from fields before passing them to actions (options are only needed for rendering, not processing). All form actions now accept a `DynamicFormActionArgs` object as the first parameter containing fields and optional passwordComplexity, followed by the previous state and formData.\n\n  ## Migration steps\n\n  ### Step 1: Changes to DynamicForm component\n\n  The `DynamicForm` component and related utilities have been updated to support the new action signature pattern:\n\n  **`core/vibes/soul/form/dynamic-form/index.tsx`**:\n  - Added `DynamicFormActionArgs<F>` interface that contains `fields` and optional `passwordComplexity`\n  - Updated `DynamicFormAction<F>` type to accept `DynamicFormActionArgs<F>` as the first parameter\n  - Removed `fields` and `passwordComplexity` from the `State` interface\n  - Added automatic removal of `options` from fields before passing to actions (options are only needed for rendering)\n  - Updated action binding to use `action.bind(null, { fields: fieldsWithoutOptions, passwordComplexity })`\n\n  **`core/vibes/soul/form/dynamic-form/utils.ts`** (new file):\n  - Added `removeOptionsFromFields()` utility function that strips the `options` property from field definitions before passing them to actions, reducing the state payload size\n\n  ```diff\n  + export interface DynamicFormActionArgs<F extends Field> {\n  +   fields: Array<F | FieldGroup<F>>;\n  +   passwordComplexity?: PasswordComplexitySettings | null;\n  + }\n  +\n  + type Action<F extends Field, S, P> = (\n  +   args: DynamicFormActionArgs<F>,\n  +   state: Awaited<S>,\n  +   payload: P,\n  + ) => S | Promise<S>;\n  +\n    interface State {\n      lastResult: SubmissionResult | null;\n  -   fields: Array<F | FieldGroup<F>>;\n  -   passwordComplexity?: PasswordComplexitySettings | null;\n    }\n  ```\n\n  ### Step 2: Update DynamicForm action signatures\n\n  All form actions that use `DynamicForm` must be updated to accept `DynamicFormActionArgs<F>` as the first parameter instead of including fields in the state.\n\n  Update your form action function signature:\n\n  ```diff\n  + import { DynamicFormActionArgs } from '@/vibes/soul/form/dynamic-form';\n    import { Field, FieldGroup, schema } from '@/vibes/soul/form/dynamic-form/schema';\n\n  - export async function myFormAction<F extends Field>(\n  -   prevState: {\n  -     lastResult: SubmissionResult | null;\n  -     fields: Array<F | FieldGroup<F>>;\n  -     passwordComplexity?: PasswordComplexitySettings | null;\n  -   },\n  -   formData: FormData,\n  - ) {\n  + export async function myFormAction<F extends Field>(\n  +   { fields, passwordComplexity }: DynamicFormActionArgs<F>,\n  +   _prevState: {\n  +     lastResult: SubmissionResult | null;\n  +   },\n  +   formData: FormData,\n  + ) {\n  ```\n\n  ### Step 2: Remove fields and passwordComplexity from state interfaces\n\n  Update state interfaces to remove fields and passwordComplexity properties:\n\n  ```diff\n    interface State {\n      lastResult: SubmissionResult | null;\n  -   fields: Array<Field | FieldGroup<Field>>;\n  -   passwordComplexity?: PasswordComplexitySettings | null;\n    }\n  ```\n\n  ### Step 3: Update action implementations\n\n  Remove references to `prevState.fields` and `prevState.passwordComplexity` in action implementations:\n\n  ```diff\n    const submission = parseWithZod(formData, {\n  -   schema: schema(prevState.fields, prevState.passwordComplexity),\n  +   schema: schema(fields, passwordComplexity),\n    });\n\n    if (submission.status !== 'success') {\n      return {\n        lastResult: submission.reply(),\n  -     fields: prevState.fields,\n  -     passwordComplexity: prevState.passwordComplexity,\n      };\n    }\n  ```\n\n  ### Step 4: Update action calls in components\n\n  For actions used with `AddressListSection`, update the action signature to accept fields as the first parameter:\n\n  ```diff\n  - export async function addressAction(\n  -   prevState: Awaited<State>,\n  -   formData: FormData,\n  - ): Promise<State> {\n  + export async function addressAction(\n  +   fields: Array<Field | FieldGroup<Field>>,\n  +   prevState: Awaited<State>,\n  +   formData: FormData,\n  + ): Promise<State> {\n  ```\n\n  ### Step 5: Update DynamicForm usage\n\n  No changes needed to `DynamicForm` component usage. The component automatically handles binding fields and passwordComplexity to actions. The `DynamicForm` component now:\n  - Automatically removes options from fields before passing them to actions (reducing payload size)\n  - Binds fields and passwordComplexity to the action using `action.bind()`\n  - Maintains the same props interface, so existing usage continues to work\n\n  ### Affected files\n\n  The following files were updated in this refactor:\n  - `core/vibes/soul/form/dynamic-form/index.tsx` - Added `DynamicFormActionArgs` type and updated action binding\n  - `core/vibes/soul/form/dynamic-form/utils.ts` - Added `removeOptionsFromFields` utility function\n  - `core/app/[locale]/(default)/(auth)/register/_actions/register-customer.ts`\n  - `core/app/[locale]/(default)/account/addresses/_actions/address-action.ts`\n  - `core/app/[locale]/(default)/account/addresses/_actions/create-address.ts`\n  - `core/app/[locale]/(default)/account/addresses/_actions/update-address.ts`\n  - `core/app/[locale]/(default)/account/addresses/_actions/delete-address.ts`\n  - `core/app/[locale]/(default)/gift-certificates/purchase/_actions/add-to-cart.tsx`\n  - `core/app/[locale]/(default)/webpages/[id]/contact/_actions/submit-contact-form.ts`\n  - `core/vibes/soul/sections/address-list-section/index.tsx`\n\n- [#2816](https://github.com/bigcommerce/catalyst/pull/2816) [`b4b87a3`](https://github.com/bigcommerce/catalyst/commit/b4b87a361790bf2edd7614bab1d97490d91ed22f) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Add support for additional HTML attributes on script tags. The scripts transformer now extracts and passes through attributes like `async`, `defer`, `crossorigin`, and `data-*` attributes from BigCommerce script tags to the C15T consent manager, ensuring scripts load with their intended behavior.\n\n- [#2817](https://github.com/bigcommerce/catalyst/pull/2817) [`d469078`](https://github.com/bigcommerce/catalyst/commit/d4690786b17a7d05ef32f8941e4090101980f8bc) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Persist the checkbox product modifier since it can modify pricing and other product data. By persisting this and tracking in the url, this will trigger a product refetch when added or removed. Incidentally, now we manually control what fields are persisted, since `option.isVariantOption` doesn't apply to `checkbox`, additionally multi options modifiers that are not variant options can also modify price and other product data.\n\n  ## Migration\n\n  ### Step 1\n\n  Update `product-options-transformer.ts` to manually track persisted fields:\n\n  ```ts\n  case 'DropdownList': {\n      return {\n          // before\n          // persist: option.isVariantOption,\n          // after (manually persist)\n          persist: true,\n          type: 'select',\n          label: option.displayName,\n          required: option.isRequired,\n          name: option.entityId.toString(),\n          defaultValue: values.find((value) => value.isDefault)?.entityId.toString(),\n          options: values.map((value) => ({\n          label: value.label,\n          value: value.entityId.toString(),\n          })),\n      };\n  }\n  ```\n\n  Fields that persist and can affect product pricing when selected:\n  - Swatch\n  - RectangleBoxes\n  - RadioButtons\n  - ProductPickList\n  - ProductPickListWithImages\n  - CheckboxOption\n\n  ### Step 2\n\n  Remove `isVariantOption` from GQL query since we no longer use it:\n\n  ```ts\n  export const ProductOptionsFragment = graphql(\n    `\n      fragment ProductOptionsFragment on Product {\n        entityId\n        productOptions(first: 50) {\n          edges {\n            node {\n              __typename\n              entityId\n              displayName\n              isRequired\n              isVariantOption // remove this\n              ...MultipleChoiceFieldFragment\n              ...CheckboxFieldFragment\n              ...NumberFieldFragment\n              ...TextFieldFragment\n              ...MultiLineTextFieldFragment\n              ...DateFieldFragment\n            }\n          }\n        }\n      }\n    `,\n    [\n      MultipleChoiceFieldFragment,\n      CheckboxFieldFragment,\n      NumberFieldFragment,\n      TextFieldFragment,\n      MultiLineTextFieldFragment,\n      DateFieldFragment,\n    ],\n  );\n  ```\n\n  ### Step 3\n\n  Update `product-detail-form.tsx` to include separate handing of the checkbox field:\n\n  ```ts\n  const defaultValue = fields.reduce<{\n    [Key in keyof SchemaRawShape]?: z.infer<SchemaRawShape[Key]>;\n  }>(\n    (acc, field) => {\n      // Checkbox field has to be handled separately because we want to convert checked or unchecked value to true or undefined respectively.\n      // This is because the form expects a boolean value, but we want to store the checked or unchecked value in the query params.\n      if (field.type === 'checkbox') {\n        if (params[field.name] === field.checkedValue) {\n          return {\n            ...acc,\n            [field.name]: 'true',\n          };\n        }\n\n        if (params[field.name] === field.uncheckedValue) {\n          return {\n            ...acc,\n            [field.name]: undefined,\n          };\n        }\n\n        return {\n          ...acc,\n          [field.name]: field.defaultValue, // Default value is either 'true' or undefined\n        };\n      }\n\n      return {\n        ...acc,\n        [field.name]: params[field.name] ?? field.defaultValue,\n      };\n    },\n    { quantity: minQuantity ?? 1 },\n  );\n\n  ...\n\n  const handleChange = useCallback(\n    (value: string) => {\n      // Checkbox field has to be handled separately because we want to convert 'true' or '' to the checked or unchecked value respectively.\n      if (field.type === 'checkbox') {\n        void setParams({ [field.name]: value ? field.checkedValue : field.uncheckedValue });\n      } else {\n        void setParams({ [field.name]: value || null }); // Passing `null` to remove the value from the query params if fieldValue is falsey\n      }\n\n      controls.change(value || ''); // If fieldValue is falsey, we set it to an empty string\n    },\n    [setParams, field, controls],\n  );\n  ```\n\n  ### Step 4\n\n  Update schema in `core/vibes/soul/sections/product-detail/schema.ts`:\n\n  ```ts\n  type CheckboxField = {\n    type: 'checkbox';\n    defaultValue?: string;\n    checkedValue: string; // add\n    uncheckedValue: string; // add\n  } & FormField;\n  ```\n\n- [#2820](https://github.com/bigcommerce/catalyst/pull/2820) [`a50fa6f`](https://github.com/bigcommerce/catalyst/commit/a50fa6fda64cbb1370862f43d272b0069f6d6307) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Fix WishlistDetails page from exceeding GraphQL complexity limit, and fix wishlist e2e tests.\n\n  Additionally, add the `required` prop to `core/components/wishlist/modals/new.tsx` and `core/components/wishlist/modals/rename.tsx`\n\n  ## Migration\n\n  ### Step 1: Update wishlist GraphQL fragments\n\n  In `core/components/wishlist/fragment.ts`, replace the `WishlistItemProductFragment` to use explicit fields instead of `ProductCardFragment`:\n\n  ```typescript\n  export const WishlistItemProductFragment = graphql(\n    `\n      fragment WishlistItemProductFragment on Product {\n        entityId\n        name\n        defaultImage {\n          altText\n          url: urlTemplate(lossy: true)\n        }\n        path\n        brand {\n          name\n          path\n        }\n        reviewSummary {\n          numberOfReviews\n          averageRating\n        }\n        sku\n        showCartAction\n        inventory {\n          isInStock\n        }\n        availabilityV2 {\n          status\n        }\n        ...PricingFragment\n      }\n    `,\n    [PricingFragment],\n  );\n  ```\n\n  Remove `ProductCardFragment` from all fragment dependencies in the same file.\n\n  ### Step 2: Update product card transformer\n\n  In `core/data-transformers/product-card-transformer.ts`:\n  1. Import the `WishlistItemProductFragment`:\n     ```typescript\n     import { WishlistItemProductFragment } from '~/components/wishlist/fragment';\n     ```\n  2. Update the `singleProductCardTransformer` function signature to accept both fragment types:\n     ```typescript\n     product: ResultOf<typeof ProductCardFragment | typeof WishlistItemProductFragment>;\n     ```\n  3. Add a conditional check for the `inventoryMessage` field:\n     ```typescript\n     inventoryMessage:\n       'variants' in product\n         ? getInventoryMessage(product, outOfStockMessage, showBackorderMessage)\n         : undefined,\n     ```\n  4. Update the `productCardTransformer` function signature similarly:\n     ```typescript\n     products: Array<ResultOf<typeof ProductCardFragment | typeof WishlistItemProductFragment>>;\n     ```\n\n  ### Step 3: Fix wishlist e2e tests\n\n  In `core/tests/ui/e2e/account/wishlists.spec.ts`, update label selectors to use `{ exact: true }` for specificity:\n\n  Update all locators for the wishlist name input selectors:\n\n  ```diff\n  - page.getByLabel(t('Form.nameLabel'))\n  + page.getByLabel(t('Form.nameLabel'), { exact: true })\n  ```\n\n  ### Step 4: Fix mobile wishlist e2e tests\n\n  In `core/tests/ui/e2e/account/wishlists.mobile.spec.ts`, update translation calls to use namespace prefixes:\n  1. Update the translation initialization:\n\n  ```diff\n  - const t = await getTranslations('Account.Wishlist');\n  + const t = await getTranslations();\n  ```\n\n  2. Update all translation keys to include the namespace:\n\n  ```diff\n  - await locator.getByRole('button', { name: t('actionsTitle') }).click();\n  - await page.getByRole('menuitem', { name: t('share') }).click();\n  + await locator.getByRole('button', { name: t('Wishlist.actionsTitle') }).click();\n  + await page.getByRole('menuitem', { name: t('Wishlist.share') }).click();\n  ```\n\n  ```diff\n  - await expect(page.getByText(t('shareSuccess'))).toBeVisible();\n  + await expect(page.getByText(t('Wishlist.shareSuccess'))).toBeVisible();\n  ```\n\n  ### Step 5: Add `required` prop to wishlist modals\n\n  Update the modal forms to include the `required` prop on the name input field:\n\n  In `core/components/wishlist/modals/new.tsx`:\n\n  ```diff\n        <Input\n          {...getInputProps(fields.wishlistName, { type: 'text' })}\n          defaultValue={defaultValue.current}\n          errors={fields.wishlistName.errors}\n          key={fields.wishlistName.id}\n          label={nameLabel}\n          onChange={(e) => {\n            defaultValue.current = e.target.value;\n          }}\n  +       required\n        />\n  ```\n\n  In `core/components/wishlist/modals/rename.tsx`:\n\n  ```diff\n        <Input\n          {...getInputProps(fields.wishlistName, { type: 'text' })}\n          defaultValue={defaultValue.current}\n          errors={fields.wishlistName.errors}\n          key={fields.wishlistName.id}\n          label={nameLabel}\n          onChange={(e) => {\n            defaultValue.current = e.target.value;\n          }}\n  +       required\n        />\n  ```\n\n- [#2814](https://github.com/bigcommerce/catalyst/pull/2814) [`fcb946e`](https://github.com/bigcommerce/catalyst/commit/fcb946e47aafc8be2e63bf3fe776ec6caab1fa72) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Shoppers will now see the store's actual password complexity requirements in the tooltip on the new customer registration form, preventing confusion and failed registration attempts. The schema() function in core/vibes/soul/form/dynamic-form/schema.ts now accepts an optional second parameter passwordComplexity to enable dynamic password validation. The DynamicForm, DynamicFormSection components and their associated server actions also accept an optional passwordComplexity prop that flows through to the schema. Action Required: If you have custom registration or password forms and want to use store-specific password complexity settings, fetch passwordComplexitySettings from the GraphQL API (under site.settings.customers.passwordComplexitySettings) and pass it to your DynamicFormSection component and maintain it in your server action's state. If you don't pass it, password validation defaults to: minimum 8 characters, at least one number, and at least one special character. Conflict Resolution: If merging into custom forms, ensure the passwordComplexity prop is threaded through: Page → DynamicFormSection → DynamicForm → useActionState → schema(). In server actions, add passwordComplexity?: Parameters<typeof schema>[1] to your state type and include it in all return statements to maintain state consistency.\n\n- [#2821](https://github.com/bigcommerce/catalyst/pull/2821) [`e5a03f6`](https://github.com/bigcommerce/catalyst/commit/e5a03f6a73fe16da55b96b75de70d3bbd846dfba) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Fix data-disabled class selectors in UI components\n\n  ## Migration\n\n  Updated Tailwind CSS class selectors from `data-disabled:` to `data-[disabled]:` in the following components:\n  - `vibes/soul/form/button-radio-group/index.tsx`\n  - `vibes/soul/form/card-radio-group/index.tsx`\n  - `vibes/soul/form/radio-group/index.tsx`\n  - `vibes/soul/form/rating-radio-group/index.tsx`\n  - `vibes/soul/form/swatch-radio-group/index.tsx`\n  - `vibes/soul/form/switch/index.tsx`\n  - `vibes/soul/primitives/dropdown-menu/index.tsx`\n\n  If you have customized any of these components, update your class names:\n\n  ```diff\n  - data-disabled:pointer-events-none data-disabled:opacity-50\n  + data-[disabled]:pointer-events-none data-[disabled]:opacity-50\n  ```\n\n  This change ensures proper styling of disabled states using the correct Tailwind CSS data attribute syntax.\n\n- [#2819](https://github.com/bigcommerce/catalyst/pull/2819) [`a1f1ed8`](https://github.com/bigcommerce/catalyst/commit/a1f1ed857eab954e085ba67956a954fb2b63882a) Thanks [@jamesqquick](https://github.com/jamesqquick)! - The login form input data will no longer reset on a failed login attempt.\n\n- [#2836](https://github.com/bigcommerce/catalyst/pull/2836) [`06fd9aa`](https://github.com/bigcommerce/catalyst/commit/06fd9aa921ad2c4b8b60706e114a9f425f049d40) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n- [#2826](https://github.com/bigcommerce/catalyst/pull/2826) [`b5f460c`](https://github.com/bigcommerce/catalyst/commit/b5f460c0a5fa5161c853884e18816796b9ea73d9) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n- [#2815](https://github.com/bigcommerce/catalyst/pull/2815) [`52ee85e`](https://github.com/bigcommerce/catalyst/commit/52ee85ef1b08fc6114aa953f7a7e67c11876e7e2) Thanks [@jamesqquick](https://github.com/jamesqquick)! - Add default optional text to form input labels for inputs that are not required.\n\n  ## Migration\n\n  The new required props are optional, so they are backwards compatible. However, this does mean that the `(optional)` text will now show up on fields that aren't explicitly marked as required by passing the required prop to the Label component.\n\n- [#2829](https://github.com/bigcommerce/catalyst/pull/2829) [`8096cc5`](https://github.com/bigcommerce/catalyst/commit/8096cc5ebaf983431f06bd2bd94a9a81899c2b29) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Improve accessibility for price displays by adding screen reader announcements for original prices, sale prices, and price ranges. Visual price elements are hidden from assistive technologies using `aria-hidden=\"true\"` to prevent duplicate announcements, while visually hidden text provides context about pricing information.\n\n  ## Migration steps\n\n  ### Step 1: Update Cart Price Display\n\n  Update `core/vibes/soul/sections/cart/client.tsx` to add accessibility labels for sale prices:\n\n  ```diff\n          {lineItem.salePrice && lineItem.salePrice !== lineItem.price ? (\n            <span className=\"font-medium @xl:ml-auto\">\n  -           <span className=\"line-through\">{lineItem.price}</span> {lineItem.salePrice}\n  +           <span className=\"sr-only\">{t('originalPrice', { price: lineItem.price })}</span>\n  +           <span aria-hidden=\"true\" className=\"line-through\">\n  +             {lineItem.price}\n  +           </span>{' '}\n  +           <span className=\"sr-only\">{t('currentPrice', { price: lineItem.salePrice })}</span>\n  +           <span aria-hidden=\"true\">{lineItem.salePrice}</span>\n            </span>\n          ) : (\n            <span className=\"font-medium @xl:ml-auto\">{lineItem.price}</span>\n          )}\n  ```\n\n  ### Step 2: Update PriceLabel Component\n\n  Update `core/vibes/soul/primitives/price-label/index.tsx` to add accessibility improvements for sale prices and price ranges:\n\n  ```diff\n    import { clsx } from 'clsx';\n  + import { useTranslations } from 'next-intl';\n\n    export function PriceLabel({ className, colorScheme = 'light', price }: Props) {\n  +   const t = useTranslations('Components.Price');\n\n      if (typeof price === 'string') {\n        return (\n          ...\n        );\n      }\n\n      switch (price.type) {\n        case 'range':\n          return (\n            <span ...>\n  -           {price.minValue}\n  -           &nbsp;&ndash;&nbsp;\n  -           {price.maxValue}\n  +           <span className=\"sr-only\">\n  +             {t('range', { minValue: price.minValue, maxValue: price.maxValue })}\n  +           </span>\n  +           <span aria-hidden=\"true\">\n  +             {price.minValue} - {price.maxValue}\n  +           </span>\n            </span>\n          );\n\n        case 'sale':\n          return (\n            <span className={clsx('block font-semibold', className)}>\n  +           <span className=\"sr-only\">{t('originalPrice', { price: price.previousValue })}</span>\n              <span\n  +             aria-hidden=\"true\"\n                className={clsx(\n                  'font-normal line-through opacity-50',\n                  ...\n                )}\n              >\n                {price.previousValue}\n              </span>{' '}\n  +           <span className=\"sr-only\">{t('currentPrice', { price: price.currentValue })}</span>\n              <span\n  +             aria-hidden=\"true\"\n                className={clsx(\n                  ...\n                )}\n              >\n                {price.currentValue}\n              </span>\n            </span>\n          );\n      }\n    }\n  ```\n\n  ### Step 3: Add Translation Keys\n\n  Update `core/messages/en.json` to include new translation keys for price accessibility:\n\n  ```diff\n    \"Cart\": {\n      \"title\": \"Cart\",\n      \"heading\": \"Your cart\",\n      \"proceedToCheckout\": \"Proceed to checkout\",\n      \"increment\": \"Increase quantity\",\n      \"decrement\": \"Decrease quantity\",\n      \"removeItem\": \"Remove item\",\n      \"cartCombined\": \"We noticed you had items saved in a previous cart, so we've added them to your current cart for you.\",\n      \"cartRestored\": \"You started a cart on another device, and we've restored it here so you can pick up where you left off.\",\n      \"cartUpdateInProgress\": \"You have a cart update in progress. Are you sure you want to leave this page? Your changes may be lost.\",\n  +   \"originalPrice\": \"Original price was {price}.\",\n  +   \"currentPrice\": \"Current price is {price}.\",\n  ```\n\n  ```diff\n      },\n  +   \"Price\": {\n  +     \"originalPrice\": \"Original price was {price}.\",\n  +     \"currentPrice\": \"Current price is {price}.\",\n  +     \"range\": \"Price from {minValue} to {maxValue}.\"\n  +   }\n    },\n    \"GiftCertificates\": {\n  ```\n\n- [#2809](https://github.com/bigcommerce/catalyst/pull/2809) [`dd559b2`](https://github.com/bigcommerce/catalyst/commit/dd559b2d9f354387e19d7c81a809eed97bfc9be3) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Minor UX improvements for the Reviews section:\n  - Show `totalCount` for reviews.\n  - Show `averageRating` up to the first decimal.\n  - Hide `averageRating` next to rating stars when there are no reviews.\n\n## 1.4.0\n\n### Minor Changes\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Upgrade c15t to 1.8.2, migrate from custom mode to offline mode, refactor consent cookie handling to use c15t's compact format, add script location support for HEAD/BODY rendering, and add privacy policy link support to CookieBanner.\n\n  ## What Changed\n  - Upgraded `@c15t/nextjs` to version `1.8.2`\n  - Changed consent manager mode from `custom` (with endpoint handlers) to `offline` mode\n    - Removed custom `handlers.ts` implementation\n  - Added `enabled` prop to `C15TConsentManagerProvider` to control consent manager functionality\n  - Removed custom consent cookie encoder/decoder implementations (`decoder.ts`, `encoder.ts`)\n  - Added `parse-compact-format.ts` to handle c15t's compact cookie format\n    - Compact format: `i.t:timestamp,c.necessary:1,c.functionality:1,etc...`\n  - Updated cookie parsing logic in both client and server to use the new compact format parser\n  - Scripts now support `location` field from BigCommerce API and can be rendered in `<head>` or `<body>` based on the `target` property\n  - `CookieBanner` now supports the `privacyPolicyUrl` field from BigCommerce API and will be rendered in the banner description if available.\n\n  ## Migration Path\n\n  ### Consent Manager Provider Changes\n\n  The `ConsentManagerProvider` now uses `offline` mode instead of `custom` mode with endpoint handlers. The provider configuration has been simplified:\n\n  **Before:**\n\n  ```typescript\n  <C15TConsentManagerProvider\n    options={{\n      mode: 'custom',\n      consentCategories: ['necessary', 'functionality', 'marketing', 'measurement'],\n      endpointHandlers: {\n        showConsentBanner: () => showConsentBanner(isCookieConsentEnabled),\n        setConsent,\n        verifyConsent,\n      },\n    }}\n  >\n    <ClientSideOptionsProvider scripts={scripts}>\n      {children}\n    </ClientSideOptionsProvider>\n  </C15TConsentManagerProvider>\n  ```\n\n  **After:**\n\n  ```typescript\n  <C15TConsentManagerProvider\n    options={{\n      mode: 'offline',\n      storageConfig: {\n        storageKey: CONSENT_COOKIE_NAME,\n        crossSubdomain: true,\n      },\n      consentCategories: ['necessary', 'functionality', 'marketing', 'measurement'],\n      enabled: isCookieConsentEnabled,\n    }}\n  >\n    <ClientSideOptionsProvider scripts={scripts}>\n      {children}\n    </ClientSideOptionsProvider>\n  </C15TConsentManagerProvider>\n  ```\n\n  **Key changes:**\n  - `mode` changed from `'custom'` to `'offline'`\n  - Removed `endpointHandlers` - no longer needed in offline mode\n  - Added `enabled` prop to control consent manager functionality\n  - Added `storageConfig` for cookie storage configuration\n\n  ### Cookie Handling\n\n  If you have custom code that directly reads or writes consent cookies, you'll need to update it:\n\n  **Before:**\n  The previous implementation used custom encoding/decoding. If you were directly accessing consent cookie values, you would have needed to use the custom decoder.\n\n  **After:**\n  The consent cookie now uses c15t's compact format. The public API for reading cookies remains the same:\n\n  ```typescript\n  import { getConsentCookie } from '~/lib/consent-manager/cookies/client'; // client-side\n  // or\n  import { getConsentCookie } from '~/lib/consent-manager/cookies/server'; // server-side\n\n  const consent = getConsentCookie();\n  ```\n\n  The `getConsentCookie()` function now internally uses `parseCompactFormat()` to parse the compact format cookie string. If you were directly parsing cookie values, you should now use the `getConsentCookie()` helper instead.\n\n  `getConsentCookie` now returns a compact version of the consent values:\n\n  ```typescript\n  {\n    i.t: 123456789,\n    c.necessary: true,\n    c.functionality: true,\n    c.marketing: false,\n    c.measurment: false\n  }\n  ```\n\n  Updated instances where `getConsentCookie` is used to reflect this new schema.\n\n  Removed `setConsentCookie` from server and client since this is now handled by the c15t library.\n\n  ### Script Location Support\n\n  Scripts now support rendering in either `<head>` or `<body>` based on the `location` field from the BigCommerce API:\n\n  ```typescript\n  // Scripts transformer now includes target based on location\n  target: script.location === 'HEAD' ? 'head' : 'body';\n  ```\n\n  The `ScriptsFragment` GraphQL query now includes the `location` field, allowing scripts to be placed in the appropriate DOM location. `FOOTER` location is still not supported.\n\n  ### Privacy Policy\n\n  The `RootLayoutMetadataQuery` GraphQL query now includes the `privacyPolicyUrl` field, which renders a provicy policy link in the `CookieBanner` description.\n\n  ```typescript\n  <CookieBanner\n    privacyPolicyUrl=\"https://example.com/privacy-policy\"\n    // ... other props\n  />\n  ```\n\n  The privacy policy link:\n  - Opens in a new tab (`target=\"_blank\"`)\n  - Only renders if `privacyPolicyUrl` is provided as a non-empty string\n\n  Add translatable `privacyPolicy` field to `Components.ConsentManager.CookieBanner` translation namespace for the privacy policy link text.\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Conditionally display product ratings in the storefront based on `site.settings.display.showProductRating`. The storefront logic when this setting is enabled/disabled matches exactly the logic of Stencil + Cornerstone.\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Adds product review submission functionality to the product detail page via a modal form with validation for rating, title, review text, name, and email fields. Integrates with BigCommerce's GraphQL API using Conform and Zod for form validation and real-time feedback.\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Introduce displayName and displayKey fields to facets for improved labeling and filtering\n\n  Facet filters now use the `displayName` field for more descriptive labels in the UI, replacing the deprecated `name` field. Product attribute facets now support the `filterKey` field for consistent parameter naming. The facet transformer has been updated to use `displayName` with a fallback to `filterName` when `displayName` is not available.\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Updated product and brand pages to include the number of reviews in the product data. Fixed visual spacing within product cards. Enhanced the Rating component to display the number of reviews alongside the rating. Introduced a new RatingLink component for smooth scrolling to reviews section on PDP.\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Make newsletter signup component on homepage render conditionally based on BigCommerce settings.\n\n  ## What Changed\n  - Newsletter signup component (`Subscribe`) on homepage now conditionally renders based on `showNewsletterSignup` setting from BigCommerce.\n  - Added `showNewsletterSignup` field to `HomePageQuery` GraphQL query to fetch newsletter settings.\n  - Newsletter signup now uses `Stream` component with `Streamable` pattern for progressive loading.\n\n  ## Migration\n\n  To make newsletter signup component render conditionally based on BigCommerce settings, update your homepage code:\n\n  ### 1. Update GraphQL Query (`page-data.ts`)\n\n  Add the `newsletter` field to your `HomePageQuery`:\n\n  ```typescript\n  const HomePageQuery = graphql(\n    `\n      query HomePageQuery($currencyCode: currencyCode) {\n        site {\n          // ... existing fields\n          settings {\n            inventory {\n              defaultOutOfStockMessage\n              showOutOfStockMessage\n              showBackorderMessage\n            }\n            newsletter {\n              showNewsletterSignup\n            }\n          }\n        }\n      }\n    `,\n    [FeaturedProductsCarouselFragment, FeaturedProductsListFragment],\n  );\n  ```\n\n  ### 2. Update Homepage Component (`page.tsx`)\n\n  Import `Stream` and create a streamable for newsletter settings:\n\n  ```typescript\n  import { Stream, Streamable } from '@/vibes/soul/lib/streamable';\n\n  // Inside your component, create the streamable:\n  const streamableShowNewsletterSignup = Streamable.from(async () => {\n    const data = await streamablePageData;\n    const { showNewsletterSignup } = data.site.settings?.newsletter ?? {};\n    return showNewsletterSignup;\n  });\n\n  // Replace direct rendering with conditional Stream:\n  <Stream fallback={null} value={streamableShowNewsletterSignup}>\n    {(showNewsletterSignup) => showNewsletterSignup && <Subscribe />}\n  </Stream>\n  ```\n\n  **Before:**\n\n  ```typescript\n  <Subscribe />\n  ```\n\n  **After:**\n\n  ```typescript\n  <Stream fallback={null} value={streamableShowNewsletterSignup}>\n    {(showNewsletterSignup) => showNewsletterSignup && <Subscribe />}\n  </Stream>\n  ```\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Refactor the `ReviewForm` to accept `trigger` prop instead of `formButtonLabel` for flexible rendering.\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Adds OpenTelemetry instrumentation for Catalyst, enabling the collection of spans for Catalyst storefronts.\n\n  ### Migration\n\n  Change is new code only, so just copy over `/core/instrumentation.ts` and `core/lib/otel/tracers.ts`.\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Implement functional newsletter subscription feature with BigCommerce GraphQL API integration.\n\n  ## What Changed\n  - Replaced the mock implementation in `subscribe.ts` with a real BigCommerce GraphQL API call using the `SubscribeToNewsletterMutation`.\n  - Added comprehensive error handling for invalid emails, already-subscribed users, and unexpected errors.\n  - Improved form error handling in `InlineEmailForm` to use `form.errors` instead of field-level errors for better error display.\n  - Added comprehensive E2E tests and test fixtures for subscription functionality.\n\n  ## Migration Guide\n\n  Replace the `subscribe` action in `core/components/subscribe/_actions/subscribe.ts` with the latest changes to include:\n  - BigCommerce GraphQL mutation for newsletter subscription\n  - Error handling for invalid emails, already-subscribed users, and unexpected errors\n  - Proper error messages returned via Conform's `submission.reply()`\n\n  Update `inline-email-form` to fix issue of not showing server-side error messages from form actions.\n\n  **`core/vibes/soul/primitives/inline-email-form/index.tsx`**\n  1. Add import for `FieldError` component:\n\n  ```tsx\n  import { FieldError } from '@/vibes/soul/form/field-error';\n  ```\n\n  2. Remove the field errors extraction:\n\n  ```tsx\n  // Remove: const { errors = [] } = fields.email;\n  ```\n\n  3. Update border styling to check both form and field errors:\n\n  ```tsx\n  // Changed from:\n  errors.length ? 'border-error' : 'border-black',\n\n  // Changed to:\n  form.errors?.length || fields.email.errors?.length\n    ? 'border-error focus-within:border-error'\n    : 'border-black focus-within:border-primary',\n  ```\n\n  4. Update error rendering to display both field-level and form-level errors:\n\n  ```tsx\n  // Changed from:\n  {\n    errors.map((error, index) => (\n      <FormStatus key={index} type=\"error\">\n        {error}\n      </FormStatus>\n    ));\n  }\n\n  // Changed to:\n  {\n    fields.email.errors?.map((error) => <FieldError key={error}>{error}</FieldError>);\n  }\n  {\n    form.errors?.map((error, index) => (\n      <FormStatus key={index} type=\"error\">\n        {error}\n      </FormStatus>\n    ));\n  }\n  ```\n\n  This change ensures that server-side error messages returned from form actions (like `formErrors` from Conform's `submission.reply()`) are now properly displayed to users.\n\n  Add the following translation keys to your locale files (e.g., `messages/en.json`):\n\n  ```json\n  {\n    \"Components\": {\n      \"Subscribe\": {\n        \"title\": \"Sign up for our newsletter\",\n        \"placeholder\": \"Enter your email\",\n        \"description\": \"Stay up to date with the latest news and offers from our store.\",\n        \"subscribedToNewsletter\": \"You have been subscribed to our newsletter!\",\n        \"Errors\": {\n          \"invalidEmail\": \"Please enter a valid email address.\",\n          \"somethingWentWrong\": \"Something went wrong. Please try again later.\"\n        }\n      }\n    }\n  }\n  ```\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Separate first and last name fields on user session object.\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Conditionally enable storefront reviews functionality based on `site.settings.reviews.enabled`. The storefront logic when this setting is enabled/disabled matches exactly the logic of Stencil + Cornerstone.\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Add out-of-stock / backorder message to product cards on PLPs based on store settings:\n  - Add out of stock message if the product is out of stock and stock is set to display it.\n  - Add the backorder message if the product has no on-hand stock and is available for backorder and the store/product is set to display the backorder message\n\n  ## Migration\n\n  ### Option 1: Automatic Migration (Recommended)\n\n  For existing Catalyst stores, the simplest way to get the newly added feature is to rebase the existing code with the new release code. The files that will be updated are listed below.\n\n  ### Option 2: Manual Migration\n\n  If you prefer not to rebase or have made customizations that prevent rebasing, follow these manual steps:\n\n  #### Step 1: Update GraphQL Fragment\n\n  Add the inventory fields to your product card fragment in `core/components/product-card/fragment.ts` under `Product`:\n\n  ```graphql\n  inventory {\n    hasVariantInventory\n    isInStock\n    aggregated {\n      availableForBackorder\n      unlimitedBackorder\n      availableOnHand\n    }\n  }\n  variants(first: 1) {\n    edges {\n      node {\n        entityId\n        sku\n        inventory {\n          byLocation {\n            edges {\n              node {\n                locationEntityId\n                backorderMessage\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n  ```\n\n  #### Step 2: Update Product interface in Product Card component\n\n  Update the `Product` interface in `core/vibes/soul/primitives/product-card/index.tsx` adding the following field to it:\n\n  `inventoryMessage?: string;`\n\n  #### Step 3: Update Data Transformer\n\n  Modify `core/data-transformers/product-card-transformer.ts` to include inventory message in the transformed data. You can simply copy the whole file from this release as it does not have UI breaking changes.\n\n  #### Step 4: Update Product Card Layout\n\n  Update `core/vibes/soul/primitives/product-card/index.tsx` layout to display the new `inventoryMessage` product field.\n\n  #### Step 5: Update Page Data GraphQL queries\n\n  Add inventory settings queries to the pages data. Add the following query to the main GQL query under `site.settings`:\n\n  ```\n  inventory {\n    defaultOutOfStockMessage\n    showOutOfStockMessage\n    showBackorderMessage\n  }\n  ```\n\n  to the following page data files:\n  - `core/app/[locale]/(default)/(faceted)/brand/[slug]/page-data.ts`\n  - `core/app/[locale]/(default)/(faceted)/category/[slug]/page-data.ts`\n  - `core/app/[locale]/(default)/(faceted)/search/page-data.ts`\n  - `core/app/[locale]/(default)/page-data.ts`\n\n  #### Step 6: Update Page Components\n\n  Update the corresponding page components to use the `productCardTransformer` method (if not already using it) to get the product card, and pass inventory data to those product cards based on the store inventory settings. Use the following code while retrieving the product lists:\n\n  ```\n      const { defaultOutOfStockMessage, showOutOfStockMessage, showBackorderMessage } =\n        data.site.settings?.inventory ?? {};\n\n      return productCardTransformer(\n        featuredProducts,\n        format,\n        showOutOfStockMessage ? defaultOutOfStockMessage : undefined,\n        showBackorderMessage,\n      );\n  ```\n\n  in the following files:\n  - `core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx`\n  - `core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx`\n  - `core/app/[locale]/(default)/(faceted)/search/page.tsx`\n  - `core/app/[locale]/(default)/page.tsx`\n\n  ### Files Modified in This Change\n  - `core/app/[locale]/(default)/(faceted)/brand/[slug]/page-data.ts`\n  - `core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx`\n  - `core/app/[locale]/(default)/(faceted)/category/[slug]/page-data.ts`\n  - `core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx`\n  - `core/app/[locale]/(default)/(faceted)/search/page-data.ts`\n  - `core/app/[locale]/(default)/(faceted)/search/page.tsx`\n  - `core/app/[locale]/(default)/page-data.ts`\n  - `core/app/[locale]/(default)/page.tsx`\n  - `core/components/product-card/fragment.ts`\n  - `core/data-transformers/product-card-transformer.ts`\n  - `core/vibes/soul/primitives/product-card/index.tsx`\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Add newsletter subscription toggle to account settings page, allowing customers to manage their marketing preferences directly from their account.\n\n  ## What Changed\n  - Added `NewsletterSubscriptionForm` component with a toggle switch for subscribing/unsubscribing to newsletters\n  - Created `updateNewsletterSubscription` server action that handles both subscribe and unsubscribe operations via BigCommerce GraphQL API\n  - Updated `AccountSettingsSection` to conditionally display the newsletter subscription form when enabled\n  - Enhanced `CustomerSettingsQuery` to fetch `isSubscribedToNewsletter` status and `showNewsletterSignup` store setting\n  - Updated account settings page to pass newsletter subscription props and bind customer info to the action\n  - Added translation keys for newsletter subscription UI in `Account.Settings.NewsletterSubscription` namespace\n  - Added E2E tests for subscribing and unsubscribing functionality\n\n  ## Migration Guide\n\n  To add the newsletter subscription toggle to your account settings page:\n\n  ### Step 1: Copy the server action\n\n  Copy the new server action file to your account settings directory:\n\n  ```bash\n  cp core/app/[locale]/(default)/account/settings/_actions/update-newsletter-subscription.ts \\\n     your-app/app/[locale]/(default)/account/settings/_actions/update-newsletter-subscription.ts\n  ```\n\n  ### Step 2: Update the GraphQL query\n\n  Update `core/app/[locale]/(default)/account/settings/page-data.tsx` to include newsletter subscription fields:\n\n  ```tsx\n  // Renamed CustomerSettingsQuery to AccountSettingsQuery\n  const AccountSettingsQuery = graphql(`\n    query AccountSettingsQuery(...) {\n      customer {\n        ...\n        isSubscribedToNewsletter  # Add this field\n      }\n      site {\n        settings {\n          ...\n          newsletter {            # Add this section\n            showNewsletterSignup\n          }\n        }\n      }\n    }\n  `);\n  ```\n\n  Also update the return statement to include `newsletterSettings`:\n\n  ```tsx\n  const newsletterSettings = response.data.site.settings?.newsletter;\n\n  return {\n    ...newsletterSettings, // Add this\n  };\n  ```\n\n  ### Step 3: Copy the NewsletterSubscriptionForm component\n\n  Copy the new form component:\n\n  ```bash\n  cp core/vibes/soul/sections/account-settings/newsletter-subscription-form.tsx \\\n     your-app/vibes/soul/sections/account-settings/newsletter-subscription-form.tsx\n  ```\n\n  ### Step 4: Update AccountSettingsSection\n\n  Update `core/vibes/soul/sections/account-settings/index.tsx`:\n  1. Import the new component:\n\n  ```tsx\n  import {\n    NewsletterSubscriptionForm,\n    UpdateNewsletterSubscriptionAction,\n  } from './newsletter-subscription-form';\n  ```\n\n  2. Add props to the interface:\n\n  ```tsx\n  export interface AccountSettingsSectionProps {\n    ...\n    newsletterSubscriptionEnabled?: boolean;\n    isAccountSubscribed?: boolean;\n    newsletterSubscriptionTitle?: string;\n    newsletterSubscriptionLabel?: string;\n    newsletterSubscriptionCtaLabel?: string;\n    updateNewsletterSubscriptionAction?: UpdateNewsletterSubscriptionAction;\n  }\n  ```\n\n  3. Add the form section in the component (after the change password form):\n\n  ```tsx\n  {\n    newsletterSubscriptionEnabled && updateNewsletterSubscriptionAction && (\n      <div className=\"border-t border-[var(--account-settings-section-border,hsl(var(--contrast-100)))] pt-12\">\n        <h1 className=\"@xl:text-2xl mb-10 font-[family-name:var(--account-settings-section-font-family,var(--font-family-heading))] text-2xl font-medium leading-none text-[var(--account-settings-section-text,var(--foreground))]\">\n          {newsletterSubscriptionTitle}\n        </h1>\n        <NewsletterSubscriptionForm\n          action={updateNewsletterSubscriptionAction}\n          ctaLabel={newsletterSubscriptionCtaLabel}\n          isAccountSubscribed={isAccountSubscribed}\n          label={newsletterSubscriptionLabel}\n        />\n      </div>\n    );\n  }\n  ```\n\n  ### Step 5: Update the account settings page\n\n  Update `core/app/[locale]/(default)/account/settings/page.tsx`:\n  1. Import the action:\n\n  ```tsx\n  import { updateNewsletterSubscription } from './_actions/update-newsletter-subscription';\n  ```\n\n  2. Extract newsletter settings from the query:\n\n  ```tsx\n  const newsletterSubscriptionEnabled = accountSettings.storeSettings?.showNewsletterSignup;\n  const isAccountSubscribed = accountSettings.customerInfo.isSubscribedToNewsletter;\n  ```\n\n  3. Bind customer info to the action:\n\n  ```tsx\n  const updateNewsletterSubscriptionActionWithCustomerInfo = updateNewsletterSubscription.bind(\n    null,\n    {\n      customerInfo: accountSettings.customerInfo,\n    },\n  );\n  ```\n\n  4. Pass props to `AccountSettingsSection`:\n\n  ```tsx\n  <AccountSettingsSection\n    ...\n    isAccountSubscribed={isAccountSubscribed}\n    newsletterSubscriptionCtaLabel={t('cta')}\n    newsletterSubscriptionEnabled={newsletterSubscriptionEnabled}\n    newsletterSubscriptionLabel={t('NewsletterSubscription.label')}\n    newsletterSubscriptionTitle={t('NewsletterSubscription.title')}\n    updateNewsletterSubscriptionAction={updateNewsletterSubscriptionActionWithCustomerInfo}\n  />\n  ```\n\n  ### Step 6: Add translation keys\n\n  Add the following keys to your locale files (e.g., `messages/en.json`):\n\n  ```json\n  {\n    \"Account\": {\n      \"Settings\": {\n        ...\n        \"NewsletterSubscription\": {\n          \"title\": \"Marketing preferences\",\n          \"label\": \"Opt-in to receive emails about new products and promotions.\",\n          \"marketingPreferencesUpdated\": \"Marketing preferences have been updated successfully!\",\n          \"somethingWentWrong\": \"Something went wrong. Please try again later.\"\n        }\n      }\n    }\n  }\n  ```\n\n  ### Step 7: Verify the feature\n  1. Ensure your BigCommerce store has newsletter signup enabled in store settings\n  2. Navigate to `/account/settings` as a logged-in customer\n  3. Verify the newsletter subscription toggle appears below the change password form\n  4. Test subscribing and unsubscribing functionality\n\n  The newsletter subscription form will only display if `newsletterSubscriptionEnabled` is `true` (controlled by the `showNewsletterSignup` store setting).\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Add the following backorder messages to PDP based on the store inventory settings and the product backorders data:\n  - Backorder availability prompt\n  - Quantity on backorder\n  - Backorder message\n\n  ## Migration\n\n  For existing Catalyst stores, to get the newly added feature, simply rebase the existing code with the new release code. The files to be rebased for this change to be applied are:\n  - core/messages/en.json\n  - core/app/[locale]/(default)/product/[slug]/page-data.ts\n  - core/app/[locale]/(default)/product/[slug]/page.tsx\n  - core/app/[locale]/(default)/product/[slug]/\\_components/product-viewed/fragment.ts\n  - core/vibes/soul/sections/product-detail/index.tsx\n  - core/vibes/soul/sections/product-detail/product-detail-form.tsx\n\n### Patch Changes\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Update /login/token route error handling and messaging\n\n  ## Migration steps\n\n  ### 1. Add `invalidToken` translation key to the `Auth.Login` namespace:\n\n  ```json\n  \"invalidToken\": \"Your login link is invalid or has expired. Please try logging in again.\",\n  ```\n\n  ### 2. In `core/app/[locale]/(default)/(auth)/login/token/[token]/route.ts`, add a `console.error` in the `catch` block to log the error details:\n\n  ```typescript\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    // ...\n  }\n  ```\n\n  ### 3. In `core/app/[locale]/(default)/(auth)/login/page.tsx`, add `error` prop to searchParams and pass it down into the `SignInSection` component:\n\n  ```typescript\n  export default async function Login({ params, searchParams }: Props) {\n    const { locale } = await params;\n    const { redirectTo = '/account/orders', error } = await searchParams;\n\n    setRequestLocale(locale);\n\n    const t = await getTranslations('Auth.Login');\n    const vanityUrl = buildConfig.get('urls').vanityUrl;\n    const redirectUrl = new URL(redirectTo, vanityUrl);\n    const redirectTarget = redirectUrl.pathname + redirectUrl.search;\n    const tokenErrorMessage = error === 'InvalidToken' ? t('invalidToken') : undefined;\n\n    return (\n      <>\n        <ForceRefresh />\n        <SignInSection\n          action={login.bind(null, { redirectTo: redirectTarget })}\n          emailLabel={t('email')}\n          error={tokenErrorMessage}\n          ...\n  ```\n\n  ### 4. Update `core/vibes/soul/sections/sign-in-section/index.tsx` and add the `error` prop, and pass it down to `SignInForm`:\n\n  ```typescript\n  interface Props {\n    // ... existing props\n    error?: string;\n  }\n\n  // ...\n\n  export function SignInSection({\n    // ... existing variables\n    error,\n  }: Props) {\n    // ...\n    <SignInForm\n      action={action}\n      emailLabel={emailLabel}\n      error={error}\n  ```\n\n  ### 5. Update `core/vibes/soul/sections/sign-in-section/sign-in-form.tsx` to take the error prop and display it in the form errors:\n\n  ```typescript\n  interface Props {\n    // ... existing props\n    error?: string;\n  }\n\n  export function SignInForm({\n    // ... existing variables\n    error,\n  }: Props) {\n    // ...\n    useEffect(() => {\n      // If the form errors change when an \"error\" search param is in the URL,\n      // the search param should be removed to prevent showing stale errors.\n      if (form.errors) {\n        const url = new URL(window.location.href);\n\n        if (url.searchParams.has('error')) {\n          url.searchParams.delete('error');\n          window.history.replaceState({}, '', url.toString());\n        }\n      }\n    }, [form.errors]);\n\n    const formErrors = () => {\n      // Form errors should take precedence over the error prop that is passed in.\n      // This ensures that the most recent errors are displayed to avoid confusion.\n      if (form.errors) {\n        return form.errors;\n      }\n\n      if (error) {\n        return [error];\n      }\n\n      return [];\n    };\n\n    return (\n      <form {...getFormProps(form)} action={formAction} className=\"flex grow flex-col gap-5\">\n        // ...\n        <SubmitButton>{submitLabel}</SubmitButton>\n        {formErrors().map((err, index) => (\n          <FormStatus key={index} type=\"error\">\n            {err}\n          </FormStatus>\n        ))}\n      </form>\n    );\n  }\n  ```\n\n  ### 6. Copy all changes in the `core/tests` directory\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Passes `formButtonLabel` from `Reviews` to `ReviewsEmptyState` (was missing) and sets a default value for `formButtonLabel`\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Remove \"Exclusive Offers\" field temporarily. Currently, the field is not fully implemented in GraphQL, so it may be misleading to display it on the storefront if it's not actually doing anything when registering a customer.\n\n  Once the Register Customer operation takes this field into account, we can display it again.\n\n  ## Migration\n\n  Update `core/app/[locale]/(default)/(auth)/register/page.tsx` and add the function:\n\n  ```ts\n  // There is currently a GraphQL gap where the \"Exclusive Offers\" field isn't accounted for\n  // during customer registration, so the field should not be shown on the Catalyst storefront until it is hooked up.\n  function removeExlusiveOffersField(field: Field | Field[]): boolean {\n    if (Array.isArray(field)) {\n      // Exclusive offers field will always have ID '25', since it is made upon store creation and is also read-only.\n      return !field.some((f) => f.id === '25');\n    }\n\n    return field.id !== '25';\n  }\n  ```\n\n  Then, add the following code at the end of the `const fields` declaration:\n\n  ```ts\n      })\n      .filter(exists)\n      .filter(removeExlusiveOffersField); // <---\n  ```\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Add missing check for optional text field in `core/vibes/soul/form/dynamic-form/schema.ts`.\n\n  ## Migration\n\n  Add `if (field.required !== true) fieldSchema = fieldSchema.optional();` to `text` case in `core/vibes/soul/form/dynamic-form/schema.ts`:\n\n  ```typescript\n  case 'text':\n      fieldSchema = z.string();\n\n      if (field.pattern != null) {\n      fieldSchema = fieldSchema.regex(new RegExp(field.pattern), {\n          message: 'Invalid format.',\n      });\n      }\n\n      if (field.required !== true) fieldSchema = fieldSchema.optional();\n\n      break;\n  ```\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Improved login error handling to display a custom error message when BigCommerce indicates a password reset is required, instead of showing a generic error message.\n\n  ## What's Fixed\n\n  When attempting to log in with an account that requires a password reset, users now see an informative error message: \"Password reset required. Please check your email for instructions to reset your password.\"\n\n  **Before**: Generic \"something went wrong\" error message\n  **After**: Clear error message explaining the password reset requirement\n\n  ## Migration\n\n  ### Step 1: Update Translation Files\n\n  Add this translation key to your locale files (e.g., `core/messages/en.json`):\n\n  ```json\n  {\n    \"Auth\": {\n      \"Login\": {\n        \"passwordResetRequired\": \"Password reset required. Please check your email for instructions to reset your password.\"\n      }\n    }\n  }\n  ```\n\n  Repeat for all supported locales if you maintain custom translations.\n\n  ### Step 2: Update Login Server Action\n\n  In your login server action (e.g., `core/app/[locale]/(default)/(auth)/login/_actions/login.ts`):\n\n  Add the password reset error handling block:\n\n  ```typescript\n  if (\n    error instanceof AuthError &&\n    error.type === 'CallbackRouteError' &&\n    error.cause &&\n    error.cause.err instanceof BigCommerceGQLError &&\n    error.cause.err.message.includes('Reset password\"')\n  ) {\n    return submission.reply({ formErrors: [t('passwordResetRequired')] });\n  }\n  ```\n\n  This should be placed in your error handling, before the generic \"Invalid credentials\" check.\n\n- [#2803](https://github.com/bigcommerce/catalyst/pull/2803) [`dbd80fe`](https://github.com/bigcommerce/catalyst/commit/dbd80fe8d74d4e97252fa94c6568115748b6bbea) Thanks [@jorgemoya](https://github.com/jorgemoya)! - - Added optional `salePrice?: string` property to the `CartLineItem` interface\n  - Cart UI now displays sale prices with a strikethrough on the original price when `salePrice` is provided and differs from `price`\n\n  ## Migration\n\n  If you're using the `Cart` component with custom line items, you can now optionally include a `salePrice` property:\n\n  ```tsx\n  const lineItems = [\n    {\n      // ... other properties\n      price: '$100.00',\n      salePrice: '$80.00', // Optional: when provided, displays as strikethrough price + sale price\n    },\n  ];\n  ```\n\n  ### Backward Compatibility\n\n  This change is **fully backward compatible**. The `salePrice` property is optional, so existing implementations will continue to work without modification. If `salePrice` is not provided or equals `price`, only the regular price will be displayed.\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Update translations.\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Update translations.\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Update translations.\n\n- [#2806](https://github.com/bigcommerce/catalyst/pull/2806) [`becb67d`](https://github.com/bigcommerce/catalyst/commit/becb67df001e4a85a3e59ece24c3e44bd3a24cf6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Update translations.\n\n## 1.3.7\n\n### Patch Changes\n\n- [#2772](https://github.com/bigcommerce/catalyst/pull/2772) [`2670f4d`](https://github.com/bigcommerce/catalyst/commit/2670f4d0837d843e425a179bff588119f689567f) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Catalyst has been upgraded to Next.js 15.5.9. This is a patch version upgrade that requires migration steps for existing stores to fix a security vulnerability.\n\n  ## 🔒 Security Update\n\n  **This upgrade addresses a security vulnerability ([CVE-2025-55184 + CVE-2025-55183](https://nextjs.org/blog/security-update-2025-12-11))** that affects React Server Components. These vulnerabilities allow a Denial of Service attack and Source Code Exposure attach. This upgrade includes:\n  - Next.js 15.5.9 with the security patch\n  - React 19.1.4 and React DOM 19.1.4 with the security patch\n\n  **All users are strongly encouraged to upgrade immediately.**\n\n  ## Key Changes\n  - ⚡ **Next.js 15.5.9**: Upgraded from Next.js 15.5.7 to 15.5.9\n  - ⚛️ **React 19**: Upgraded to React 19.1.4 and React DOM 19.1.4\n\n  ## Migration Guide\n\n  ### Update Dependencies\n\n  If you're maintaining a custom Catalyst store, update your `package.json`:\n\n  ```json\n  {\n    \"dependencies\": {\n      \"next\": \"15.5.9\",\n      \"react\": \"19.1.4\",\n      \"react-dom\": \"19.1.4\"\n    },\n    \"devDependencies\": {\n      \"@next/bundle-analyzer\": \"15.5.9\",\n      \"eslint-config-next\": \"15.5.9\"\n    }\n  }\n  ```\n\n  Then run:\n\n  ```bash\n  pnpm install\n  ```\n\n## 1.3.6\n\n### Patch Changes\n\n- [#2762](https://github.com/bigcommerce/catalyst/pull/2762) [`7f3a184`](https://github.com/bigcommerce/catalyst/commit/7f3a184508acb50a09ecbdb811ec5ce34865e363) Thanks [@chanceaclark](https://github.com/chanceaclark)! - # Next.js 15.5.8 Upgrade\n\n  Catalyst has been upgraded to Next.js 15.5.8. This is a patch version upgrade that requires migration steps for existing stores to fix a security vulnerability.\n\n  ## 🔒 Critical Security Update\n\n  **This upgrade addresses a critical security vulnerability ([CVE-2025-55184 + CVE-2025-55183](https://nextjs.org/blog/security-update-2025-12-11))** that affects React Server Components. These vulnerabilities allow a Denial of Service attack and Source Code Exposure attach. This upgrade includes:\n  - Next.js 15.5.8 with the security patch\n  - React 19.1.3 and React DOM 19.1.3 with the security patch\n\n  **All users are strongly encouraged to upgrade immediately.**\n\n  ## Key Changes\n  - ⚡ **Next.js 15.5.8**: Upgraded from Next.js 15.5.7 to 15.5.8\n  - ⚛️ **React 19**: Upgraded to React 19.1.3 and React DOM 19.1.3\n\n  ## Migration Guide\n\n  ### Update Dependencies\n\n  If you're maintaining a custom Catalyst store, update your `package.json`:\n\n  ```json\n  {\n    \"dependencies\": {\n      \"next\": \"15.5.8\",\n      \"react\": \"19.1.3\",\n      \"react-dom\": \"19.1.3\"\n    },\n    \"devDependencies\": {\n      \"@next/bundle-analyzer\": \"15.5.8\",\n      \"eslint-config-next\": \"15.5.8\"\n    }\n  }\n  ```\n\n  Then run:\n\n  ```bash\n  pnpm install\n  ```\n\n## 1.3.5\n\n### Patch Changes\n\n- [#2744](https://github.com/bigcommerce/catalyst/pull/2744) [`720fe17`](https://github.com/bigcommerce/catalyst/commit/720fe1722295841a995277ec514bc8280644b879) Thanks [@chanceaclark](https://github.com/chanceaclark)! - # Next.js 15.5.7 Upgrade\n\n  Catalyst has been upgraded to Next.js 15.5.7. This is a patch version upgrade that requires migration steps for existing stores to fix a security vulnerability.\n\n  ## 🔒 Critical Security Update\n\n  **This upgrade addresses a critical security vulnerability ([CVE-2025-55182](https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components))** that affects React Server Components. The vulnerability allowed unauthenticated remote code execution on servers running React Server Components. This upgrade includes:\n  - Next.js 15.5.7 with the security patch\n  - React 19.1.2 and React DOM 19.1.2 with the security patch\n\n  **All users are strongly encouraged to upgrade immediately.**\n\n  ## Key Changes\n  - ⚡ **Next.js 15.5.7**: Upgraded from Next.js 15.5.1-canary.4 to 15.5.7 (no more canary)\n  - ⚛️ **React 19**: Upgraded to React 19.1.2 and React DOM 19.1.2\n  - 🔄 **Partial Prerendering (PPR) Removed**: Removed partial prerendering as it's unsupported in non-canary versions of Next.js 15.\n\n  ### ⚠️ Partial Prerendering (PPR) Removed\n\n  **Important**: PPR (Partial Prerendering) has been **removed** in this release as it's unsupported in non-canary versions of Next.js 15.\n  - The `ppr` experimental flag has been removed from `next.config.ts`\n  - Full support for Next.js 16's and it's new cache component patterns will be added in a future release\n  - This may result in different performance characteristics compared to the Next.js 15 + PPR setup\n\n  ## Migration Guide\n\n  ### Step 1: Update Dependencies\n\n  If you're maintaining a custom Catalyst store, update your `package.json`:\n\n  ```json\n  {\n    \"dependencies\": {\n      \"next\": \"15.5.7\",\n      \"react\": \"^19.1.2\",\n      \"react-dom\": \"^19.1.2\"\n    },\n    \"devDependencies\": {\n      \"@next/bundle-analyzer\": \"15.5.7\",\n      \"eslint-config-next\": \"15.5.7\"\n    }\n  }\n  ```\n\n  Then run:\n\n  ```bash\n  pnpm install\n  ```\n\n  ### Step 2: Update next.config.ts\n\n  Remove or comment out PPR configuration:\n\n  ```typescript\n  // Remove or disable:\n  // experimental: {\n  //   ppr: 'incremental',\n  // }\n  ```\n\n  Remove or comment out eslint config\n\n  ```typescript\n  // eslint: {\n  //     ignoreDuringBuilds: !!process.env.CI,\n  //     dirs: [\n  //     'app',\n  //     'auth',\n  //     'build-config',\n  //     'client',\n  //     'components',\n  //     'data-transformers',\n  //     'i18n',\n  //     'lib',\n  //     'middlewares',\n  //     'scripts',\n  //     'tests',\n  //     'vibes',\n  //     ],\n  // },\n  ```\n\n  ### Step 3: Remove `export const experimental_ppr`\n\n  Remove any references to `export const experimental_ppr` in your codebase as it is not being used anymore.\n\n## 1.3.4\n\n### Patch Changes\n\n- [#2720](https://github.com/bigcommerce/catalyst/pull/2720) [`ebd5993`](https://github.com/bigcommerce/catalyst/commit/ebd5993cfbb507da1d2341fb6f1a7276eee50795) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Noop release to account for typecheck issue.\n\n## 1.3.3\n\n### Patch Changes\n\n- [#2695](https://github.com/bigcommerce/catalyst/pull/2695) [`6d565c2`](https://github.com/bigcommerce/catalyst/commit/6d565c2cbf98e3fa0c2b0142734fc68a5d48bd2c) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Add Gift Certificates link to the header/footer.\n\n- [#2694](https://github.com/bigcommerce/catalyst/pull/2694) [`fdedcaa`](https://github.com/bigcommerce/catalyst/commit/fdedcaa99c83d5c32c54dec0974962b7d17447cf) Thanks [@BC-AdamWard](https://github.com/BC-AdamWard)! - Fix anonymous session cookie maxAge calculation to correctly set 7 days instead of 7 hours.\n\n## 1.3.2\n\n### Patch Changes\n\n- [`ce1731f`](https://github.com/bigcommerce/catalyst/commit/ce1731f0cb0f0e411c3ffa4734b0256dbdacafbb) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Updates the scripts transformer to account for inline scripts having a `src` attribute with empty content.\n\n## 1.3.1\n\n### Patch Changes\n\n- [#2679](https://github.com/bigcommerce/catalyst/pull/2679) [`323483a`](https://github.com/bigcommerce/catalyst/commit/323483adfdd7dc1bf925034ae360d7a803d76ab9) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Update the `lint` command to utilize `eslint` CLI directly. `next lint` is deprecated in Next.js version 16 and this provides a lower migration impact when the time comes.\n\n- [#2681](https://github.com/bigcommerce/catalyst/pull/2681) [`cacfb55`](https://github.com/bigcommerce/catalyst/commit/cacfb55b89d7de64c80eb66b671d279898a43a1e) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Minor refactor to improve the performance when navigating from the cart to the checkout.\n  - [#2680](https://github.com/bigcommerce/catalyst/pull/2680)\n  - [#2681](https://github.com/bigcommerce/catalyst/pull/2681)\n\n  ### Migration\n\n  Use the above PR diffs as a reference.\n  1. Remove `core/app/[locale]/(default)/cart/_actions/redirect-to-checkout.ts`\n  2. Update `checkoutAction` in `core/app/[locale]/(default)/cart/page.tsx` to `\"/checkout\"`\n  3. Copy changes to `core/app/[locale]/(default)/checkout/route.ts`\n  4. Update `core/lib/server-toast.ts` and set the cookie `maxAge` to `1` - this ensures any toast errors live through the redirect back to the `/cart` page\n  5. Copy changes in `core/vibes/soul/sections/cart/client.tsx`\n  6. Update `en.json` with the updated translation values (optional)\n\n## 1.3.0\n\n### Minor Changes\n\n- [#2659](https://github.com/bigcommerce/catalyst/pull/2659) [`abaa461`](https://github.com/bigcommerce/catalyst/commit/abaa46102d1024b99d6a3fca116ac910c104b719) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Adds consent-aware script loading to Catalyst's consent manager, achieving parity with Stencil's behavior where scripts are conditionally rendered based on user consent preferences. BigCommerce scripts from the Store Scripts API are now transformed and loaded via C15T's ClientSideOptionsProvider, with ESSENTIAL/UNKNOWN scripts rendering by default, all scripts rendering when consent is fully granted, and specific scripts loading based on granular consent selections.\n\n- [#2643](https://github.com/bigcommerce/catalyst/pull/2643) [`391e20d`](https://github.com/bigcommerce/catalyst/commit/391e20d2c03e8f607f6f6c80d294565e79546693) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Adds a new consent manager provider using the `@c15t/nextjs` package to handle cookie consent management with support for multiple consent categories and cookie-based persistence.\n\n- [#2666](https://github.com/bigcommerce/catalyst/pull/2666) [`ed1f615`](https://github.com/bigcommerce/catalyst/commit/ed1f615a9b84a70e24fd7015fbd17bd5abe47695) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Passes the shoppers consent to the checkout redirect mutation\n\n- [#2619](https://github.com/bigcommerce/catalyst/pull/2619) [`19077cd`](https://github.com/bigcommerce/catalyst/commit/19077cd294c5c710dfdeae54f29f13a76401bfa4) Thanks [@Tharaae](https://github.com/Tharaae)! - Add current stock message to product details page based on the store/product inventory settings.\n\n  ## Migration\n\n  For existing Catalyst stores, to get the newly added feature, simply rebase the existing code with the new release code. The files to be rebased for this change to be applied are:\n  - core/messages/en.json\n  - core/app/[locale]/(default)/product/[slug]/page-data.ts\n  - core/app/[locale]/(default)/product/[slug]/page.tsx\n\n- [#2664](https://github.com/bigcommerce/catalyst/pull/2664) [`71cfd62`](https://github.com/bigcommerce/catalyst/commit/71cfd62e99c223391e77d20198e9cf673bc61dd9) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Provides a way to track analytics consent updates within the analytics provider. This also enables a the Google Analytics provider to be able to get the initial consent values so it can initialize the default consent values.\n\n- [#2661](https://github.com/bigcommerce/catalyst/pull/2661) [`be00b44`](https://github.com/bigcommerce/catalyst/commit/be00b44cd3afd82dff84dfa1104eccbcb6df946f) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Integrates Catalyst's consent manager with the BigCommerce Control Panel's Cookie Consent setting, allowing merchants to centrally control whether the consent banner displays on the storefront. When disabled in the Control Panel, the consent banner is suppressed and all script categories are consented implicitly, matching Stencil behavior.\n\n- [#2650](https://github.com/bigcommerce/catalyst/pull/2650) [`416796f`](https://github.com/bigcommerce/catalyst/commit/416796fa4143d28ce41ba89a3f176f3b7fba552c) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Added consent manager UI components with Catalyst styling and next-intl integration. The `CookieBanner` and `ConsentManagerDialog` provide a customizable banner and preference dialog for cookie consent.\n\n### Patch Changes\n\n- [#2577](https://github.com/bigcommerce/catalyst/pull/2577) [`baf07ca`](https://github.com/bigcommerce/catalyst/commit/baf07ca89fdbb65bebf5926738b72690e4e6db60) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove unused exports from core\n\n- [#2551](https://github.com/bigcommerce/catalyst/pull/2551) [`be23108`](https://github.com/bigcommerce/catalyst/commit/be2310809342d83cfc1a3619a537d2abf66dfe79) Thanks [@jkanive](https://github.com/jkanive)! - fix: resolve maintenance page width issues\n  - Add w-full classes to ensure proper width expansion\n  - Remove flex-1 in favor of w-full for column layout\n\n- [#2574](https://github.com/bigcommerce/catalyst/pull/2574) [`be80d14`](https://github.com/bigcommerce/catalyst/commit/be80d14c1c1051189e629d182dfd46fc60c363b1) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove unused dependencies.\n\n- [#2609](https://github.com/bigcommerce/catalyst/pull/2609) [`4e6f58d`](https://github.com/bigcommerce/catalyst/commit/4e6f58dfda649571c56ec57b642e7addfb92a4aa) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Adds the product count to the facet label if the facet provides the count. This also fixes an issue where the facets weren't respecting the collapse by default setting.\n\n- [#2572](https://github.com/bigcommerce/catalyst/pull/2572) [`337b7ce`](https://github.com/bigcommerce/catalyst/commit/337b7ce05ee71ec3d937c3aa373aec8516896254) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove unused UI files.\n\n- [#2580](https://github.com/bigcommerce/catalyst/pull/2580) [`f790cd6`](https://github.com/bigcommerce/catalyst/commit/f790cd67a4b632ecb4f3dfd5e7d416859cbcb042) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove unused export types from core.\n\n- [#2670](https://github.com/bigcommerce/catalyst/pull/2670) [`d5fbb73`](https://github.com/bigcommerce/catalyst/commit/d5fbb7394696dc77da5cf9b615a2b73eba28e8e5) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Fixed issue with 301 redirect loops when `TRAILING_SLASH` is set to `false`, or when 301 redirects exist targeting the same path but with different capitalization.\n\n- [#2585](https://github.com/bigcommerce/catalyst/pull/2585) [`a40b96f`](https://github.com/bigcommerce/catalyst/commit/a40b96f743ce5462cba4615026cbc6951aa87104) Thanks [@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)! - Add graceful error handling for invalid anonymous JWT cookies\n\n- [#2578](https://github.com/bigcommerce/catalyst/pull/2578) [`bb7940c`](https://github.com/bigcommerce/catalyst/commit/bb7940cedd169f053d55b787cc2b7183f737edba) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove recpatcha code until we're ready to add it at a later point (if needed).\n\n  ## Migration\n  - A lot of the code removed was just old commented out blocks.\n  - Remove any recaptcha mention from graphql mutation and queries\n\n- [#2656](https://github.com/bigcommerce/catalyst/pull/2656) [`ff9aa17`](https://github.com/bigcommerce/catalyst/commit/ff9aa17a78c4731af02ebb4d220f86960c0e9169) Thanks [@dependabot](https://github.com/apps/dependabot)! - Updates next-auth to the latest beta version\n\n  ## Migration\n\n  Delete the `@ts-expect-error` comments within the `with-auth.ts` middleware.\n\n- [#2662](https://github.com/bigcommerce/catalyst/pull/2662) [`8c6626e`](https://github.com/bigcommerce/catalyst/commit/8c6626e104d81dce09ecc623cdec949a23224ee8) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Replace `StreamableAnalyticsProvider`with simplified `AnalyticsProvider` component.\n  - Removed `StreamableAnalyticsProvider` that used `Streamable.from()` for async data loading.\n  - Added new `AnalyticsProvider` component that accepts `channelId` and `settings` as direct props.\n  - Simplifies analytics initialization by removing unnecessary streaming complexity.\n  - Maintains same functionality with cleaner, more straightforward implementation.\n  - Fixes issue of events not triggering by properly wrapping `children` inside the provider.\n\n  ## Migration\n  - Use new `AnalyticsProvider` component in `core/app/[locale]/layout.tsx`, instead of `StreamableAnalyticsProvider`.\n\n- [#2667](https://github.com/bigcommerce/catalyst/pull/2667) [`c8dbba6`](https://github.com/bigcommerce/catalyst/commit/c8dbba6de4aa70dbe7c4ced3ce29375aea214d1f) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n- [#2589](https://github.com/bigcommerce/catalyst/pull/2589) [`d3391ee`](https://github.com/bigcommerce/catalyst/commit/d3391eea6c87d05629e15f4f47bb5ad54c47f081) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n- [#2675](https://github.com/bigcommerce/catalyst/pull/2675) [`ab9f11e`](https://github.com/bigcommerce/catalyst/commit/ab9f11ed0d75a0f6cd6f3abc039c1652415057a2) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n- [#2608](https://github.com/bigcommerce/catalyst/pull/2608) [`3d47825`](https://github.com/bigcommerce/catalyst/commit/3d4782536022e1f9a33237963c4f013558108cf3) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n- [#2648](https://github.com/bigcommerce/catalyst/pull/2648) [`7914650`](https://github.com/bigcommerce/catalyst/commit/791465079b8abaeae7662745c28e99f67876393c) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n## 1.2.0\n\n### Minor Changes\n\n- [#2475](https://github.com/bigcommerce/catalyst/pull/2475) [`33b574d`](https://github.com/bigcommerce/catalyst/commit/33b574d2fefe8514c4e512acf3f706058a6c8a2f) Thanks [@bookernath](https://github.com/bookernath)! - Implement Vercel Runtime Cache API as replacement for Vercel KV adapter\n\n### Patch Changes\n\n- [#2526](https://github.com/bigcommerce/catalyst/pull/2526) [`2089a58`](https://github.com/bigcommerce/catalyst/commit/2089a58f6bdaeab68a014ad66422932f392e6c46) Thanks [@chanceaclark](https://github.com/chanceaclark)! - The anonymous session cookie had `secure` always set to true regardless if we were prefixing it or not. This change updates the cookie to set `secure` to the same \"value\" if we prefix the cookie with `__Secure-`.\n\n- [#2564](https://github.com/bigcommerce/catalyst/pull/2564) [`69797a4`](https://github.com/bigcommerce/catalyst/commit/69797a4c9f0bfc8b27b7f144ded5545fdbb5e5cf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add visual queues when the cart state is being updated in the Cart page. Will also warn about pending state when trying to navigate away from page.\n\n  ## Migration\n  1. Update `/core/vibes/soul/sections/cart/client.tsx` to include latest changes:\n  - Use `isLineItemActionPending` to track when we need to disable checkout button and add a loading state.\n  - Add skeletons to checkout summary fields that will update when the pending state is complete.\n  - Add side effects to handle when a user `beforeunload` and when user tries to navigate using a link.\n  - Add prop to `lineItemActionPendingLabel` to be able to pass in a translatable label to the window alert.\n  2. Add label to dictionary of choice.\n\n- [#2521](https://github.com/bigcommerce/catalyst/pull/2521) [`6f6a8af`](https://github.com/bigcommerce/catalyst/commit/6f6a8af4fd7a5754b9d08aef75c4e40ab3057318) Thanks [@bookernath](https://github.com/bookernath)! - Preconnect to checkout domain on cart page to improve checkout load time\n\n- Updated dependencies [[`707ec24`](https://github.com/bigcommerce/catalyst/commit/707ec24745b6a0040551328d64657ff40df4e252), [`a27054f`](https://github.com/bigcommerce/catalyst/commit/a27054f4f22013707d40a100b15122c22354c956)]:\n  - @bigcommerce/catalyst-client@1.0.1\n\n## 1.1.0\n\n### Minor Changes\n\n- [#2477](https://github.com/bigcommerce/catalyst/pull/2477) [`02af32c`](https://github.com/bigcommerce/catalyst/commit/02af32c459719f97e8973a19b6889e5fa73d0c38) Thanks [@bookernath](https://github.com/bookernath)! - Add support for Scripts API/Script Manager scripts rendering via next/script\n\n### Patch Changes\n\n- [#2465](https://github.com/bigcommerce/catalyst/pull/2465) [`a438bb6`](https://github.com/bigcommerce/catalyst/commit/a438bb660bc3bd11adacd125769ba99ba2e1c38d) Thanks [@bookernath](https://github.com/bookernath)! - Bump next to 15.4.0-canary.114 to fix issue with PDPs 500ing on Docker builds\n\n- [#2474](https://github.com/bigcommerce/catalyst/pull/2474) [`989bf97`](https://github.com/bigcommerce/catalyst/commit/989bf974c534a7201782ace9a4bf3fe745e8af01) Thanks [@bookernath](https://github.com/bookernath)! - Respect min/max purchase quantity from API in quantity selector\n\n- [#2464](https://github.com/bigcommerce/catalyst/pull/2464) [`474f960`](https://github.com/bigcommerce/catalyst/commit/474f960c4c428e28874022b36ae2b03e0b301e20) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove edge runtime declarations to be able to run Catalyst with OpenNext.\n\n- [#2468](https://github.com/bigcommerce/catalyst/pull/2468) [`8b64931`](https://github.com/bigcommerce/catalyst/commit/8b6493156a70490c0c35c35d45ebd9ad8f23615c) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n## 1.0.1\n\n### Patch Changes\n\n- [#2448](https://github.com/bigcommerce/catalyst/pull/2448) [`e4444a2`](https://github.com/bigcommerce/catalyst/commit/e4444a2ca83b5b73776c842feff56e47f57344dc) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Fixes an issue where the anonymous session wasn't getting cleared after an actual session was established.\n\n## 1.0.0\n\n### Major Changes\n\n- [`6b17bdb`](https://github.com/bigcommerce/catalyst/commit/6b17bdb) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Introduce Soul VIBE UI library to the repository.\n\n- Added a collection of reusable primitives with modern styles\n- Prebuilt sections and page templates that are easy to use\n- Fast performance and modern patterns leveraging the latest features of Next.js\n- Easy customization to best represent your brand\n- Utilize @conform-to/react for progressively enhanced HTML forms\n\nJoin the discussion [here](https://github.com/bigcommerce/catalyst/discussions/1861) for more details of this major milestone for Catalyst!\n\n### Minor Changes\n\n- [`589c91a`](https://github.com/bigcommerce/catalyst/commit/589c91a) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Enable cart restoration on non-persistent cart logouts.\n\n**Migration**\n\nUpdate the logout mutation to include the `cartEntityId` variable + the `cartUnassignResult` node and make sure the `client.fetch` method contains the new variable.\n\n```diff\n-mutation LogoutMutation {\n+mutation LogoutMutation($cartEntityId: String) {\n-  logout {\n+  logout(cartEntityId: $cartEntityId) {\n    result\n+    cartUnassignResult {\n+      cart {\n+        entityId\n+      }\n+    }\n  }\n}\n```\n\n- [`32a28b9`](https://github.com/bigcommerce/catalyst/commit/32a28b9) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Use isomorphic-dompurify to santize any sort of shopper supplied input.\n\n- [`f039b2c`](https://github.com/bigcommerce/catalyst/commit/f039b2c) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Properly handle `BigCommerceGQLError` in actions, by returning the error messages from the request.\n\n- [`dd66f96`](https://github.com/bigcommerce/catalyst/commit/dd66f96) Thanks [@matthewvolk](https://github.com/matthewvolk)! - In order to maintain parity with Stencil's 404 page, we wanted to allow the user to search from the 404 page. Since the search included with the header component is fully featured, we included a CTA to open the same search that you get when clicking the search icon in the header.\n\n**Migration**\n\nMost changes are additive, so they should hopefully be easy to resolve if flagged for merge conflicts. Change #3 below replaces the Search state with the new search context, be sure to pay attention to the new\n\n1. This change adds a new directory under `core/` called `context/` containing a `search-context.tsx` file. Since this is a new file, there shouldn't be any merge conflicts\n2. `SearchProvider` is imported into `core/app/providers` and replaces the React fragment (`<>`) that currently wraps `<Toaster>` and `{children}`\n3. In `core/vibes/soul/primitives/navigation`, replace `useState` with `useSearch` imported from the new context file, and update the dependency arrays for the `useEffect`'s in the `Navigation component`\n4. Add search `Button` that calls `setIsSearchOpen(true)` to the `NotFound` component in `core/vibes/sections/not-found/index.tsx`\n\n- [`62b891c`](https://github.com/bigcommerce/catalyst/commit/62b891c) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Adds support for nested web page children / trees. Restructure web page routing to support a layout file.\n\n- [`44342ee`](https://github.com/bigcommerce/catalyst/commit/44342ee) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Sets a default session when any user first visits the page.\n\n- [`ff57b8a`](https://github.com/bigcommerce/catalyst/commit/ff57b8a) Thanks [@eugene(yevhenii)kuzmenko](<https://github.com/eugene(yevhenii)kuzmenko>)! - Pass analytics cookies to checkout mutation to preserve the analytics session whenever shopper redirects to the external checkout\n\n- [`067d5a4`](https://github.com/bigcommerce/catalyst/commit/067d5a4) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Move the anonymous session into it's own cookie, separate from Auth.js in order to have better non-persistent cart support.\n\n**Migration**\n\nIf you were using `await signIn('anonymous', { redirect: false });`, you'll need to migrate over to using the `await anonymousSignIn()` function. Otherwise, we am only changing the underlying logic in existing API's so pulling in the changes should immediately pick this up.\n\n- [`9b3541d`](https://github.com/bigcommerce/catalyst/commit/9b3541d) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Adds a new analytics provider meant to replace the other provider. This provider is built being framework agnostic but exposes a react provider to use within context. The initial implementation comes with a Google Analytics provider with some basic events to get started. We need to add some other events around starting checkout, banners, consent loading, and search. This change is additive only so no migration is needed until consumption.\n\n- [`bd3bc8b`](https://github.com/bigcommerce/catalyst/commit/bd3bc8b) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Implement the new analytics provider, utilizing the GoogleAnalytics provider as the first analytics solution.\n\nMost changes are additive so merge conflicts should be easy to resolve. In order to use the new provider from the previous provider, if it's already not setup in the BigCommerce control panel for checkout analytics, you'll need to add the GA4 property ID. This will automatically be used by the new GoogleAnalytics provider.\n\n- [`70afa5a`](https://github.com/bigcommerce/catalyst/commit/70afa5a) Thanks [@eugene(yevhenii)kuzmenko](<https://github.com/eugene(yevhenii)kuzmenko>)! - Dispatch Visit started and Product Viewed analytics events\n\n- [`da2a462`](https://github.com/bigcommerce/catalyst/commit/da2a462) Thanks [@bookernath](https://github.com/bookernath)! - Add currency selector to header\n\n- [`f3b4d90`](https://github.com/bigcommerce/catalyst/commit/f3b4d90) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Add Wishlist account pages and public wishlist page\n\n- [`59ff1ce`](https://github.com/bigcommerce/catalyst/commit/59ff1ce) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Fetches the stores URLs on build which can remove the need of setting NEXT_PUBLIC_BIGCOMMERCE_CDN_HOSTNAME. The environment variable is still provided in case customization is needed.\n\n- [`a0e6425`](https://github.com/bigcommerce/catalyst/commit/a0e6425) Thanks [@eugene(yevhenii)kuzmenko](<https://github.com/eugene(yevhenii)kuzmenko>)! - Adds analytics cookies needed for native analytics.\n\nThis is a add-only change, so migration should be as simple as pulling in the new code.\n\n- [`a601f7e`](https://github.com/bigcommerce/catalyst/commit/a601f7e) Thanks [@jorgemoya](https://github.com/jorgemoya)! - This refactor optimizes compare for caching and the eventual use of dynamicIO.\n\n**Key modifications include:**\n\n- Our query functions now take in all params required for fetching, instead of accessing dynamic variables internally. This is important to serialize arguments if we want to eventually `use cache`.\n- Use `Streamable.from` to generate our streaming props that are passed to our UI components.\n\n**Migration instructions:**\n\n- Updated `/app/[locale]/(default)/compare/page.tsx` to use `Streamable.from` pattern.\n- Renamed `getCompareData` query to `getComparedProducts`.\n  - Updated query\n  - Returns empty `[]` if no product ids are passed\n\n- [`c6e38a6`](https://github.com/bigcommerce/catalyst/commit/c6e38a6) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Reorganize and cleanup files:\n- Moved `core/context/search-context` to `core/lib/search`.\n- Moved `core/client/mutations/add-cart-line-item.ts` and `core/client/mutations/create-cart.ts` into `core/lib/cart/*`.\n- Removed `core/client/queries/get-cart.ts` in favor of a smaller, more focused query within `core/lib/cart/validate-cart.ts`.\n\n**Migration**\n\n- Replace imports from `~/context/search-context` to `~/lib/search`.\n- Replace imports from `~/client/mutations/` to `~/lib/cart/`.\n- Remove any direct imports from `~/client/queries/get-cart.ts` and use the new `validate-cart.ts` query instead. If you need the previous `getCart` function, you can copy it from the old file and adapt it to your needs.\n\n- [`7b3b81c`](https://github.com/bigcommerce/catalyst/commit/7b3b81c) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Replaces the REST-powered `client.fetchShippingZones` method with a GraphQL-powered query containing the `site.settings.shipping.supportedShippingDestinations` field.\n\n**Migration:**\n\n1. The return type of `getShippingCountries` has the same shape as the `Country` BigCommerce GraphQL type, so you should be able to copy the graphql query from `core/app/[locale]/(default)/cart/page-data.ts` into your project and replace the existing `getShippingCountries` method in there.\n2. Remove the argument `data.geography` from the `getShippingCountries` invocation in `core/app/[locale]/(default)/cart/page.tsx`\n3. Finally, you should be able to delete the file `core/client/management/get-shipping-zones.ts` assuming it is no longer referenced anywhere in `core/`\n\n- [`53e0b5e`](https://github.com/bigcommerce/catalyst/commit/53e0b5e) Thanks [@jorgemoya](https://github.com/jorgemoya)! - This refactor optimizes category PLP for caching and the eventual use of dynamicIO. With these changes we leverage data caching to hit mostly cache data for guest shoppers in different locales and with different currencies.\n\n**Key modifications include:**\n\n- We don't stream in Category page data, instead it's a blocking call that will redirect to `notFound` when category is not found. Same for metadata.\n- Our query functions now take in all params required for fetching, instead of accessing dynamic variables internally. This is important to serialize arguments if we want to eventually `use cache`.\n- Use `Streamable.from` to generate our streaming props that are passed to our UI components.\n- Remove use of nuqs' `createSearchParamsCache` in favor of nuqs' `createLoader`.\n\n**Migration instructions:**\n\n- Update `/(facted)/category/[slug]/page.tsx`\n  - For this page we are now doing a blocking request for category page data. Instead of having functions that each would read from props, we share streamable functions that can be passed to our UI components. We still stream in filter and product data.\n- Update `/(facted)/category/[slug]/page-data.tsx`\n  - Request now accept `customerAccessToken` as a prop instead of calling internally.\n- Update`/(facted)/category/[slug]/fetch-compare-products.ts`\n  - Request now accept `customerAccessToken` as a prop instead of calling internally.\n- Update `/(faceted)/fetch-faceted-search.ts`\n  - Request now accept `customerAccessToken` and `currencyCode` as a prop instead of calling internally.\n\n- [`537db2c`](https://github.com/bigcommerce/catalyst/commit/537db2c) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Add the ability to redirect from the login page. Developers can now append a relative path to the `\u001f?redirectTo=` query param on the `/login` page. When a shopper successfully logs in, it'll redirect them to the given relative path. Defaults to `/account/orders` to prevent a breaking change.\n\n- [`b20dfb0`](https://github.com/bigcommerce/catalyst/commit/b20dfb0) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Adds an eslint rule to import expect and test from ~/tests/fixtures instead of the @playwright/test module. This is to create a more consistent testing experience across the codebase.\n\n**Migration**\n\nAny import statements that import `expect` and `test` from `@playwright/test` should be updated to import from `~/tests/fixtures` instead. All other imports from `@playwright/test` should remain unchanged.\n\n```diff\n-import { expect, type Page, test } from '@playwright/test';\n+import { type Page } from '@playwright/test';\n+\n+import { expect, test } from '~/tests/fixtures';\n```\n\n- [`f0464a8`](https://github.com/bigcommerce/catalyst/commit/f0464a8) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Drops CSS support for Safari < 15 due to those versions only having 0.09% global usage.\n\n- [`1d6cf64`](https://github.com/bigcommerce/catalyst/commit/1d6cf64) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Render address fields for customer registration form.\n\n- [`42ded4a`](https://github.com/bigcommerce/catalyst/commit/42ded4a) Thanks [@jorgemoya](https://github.com/jorgemoya)! - This refactor optimizes home page, header, and footer for caching and the eventual use of dynamicIO. With these changes we leverage data caching to hit mostly cache data for guest shoppers in different locales and with different currencies.\n\n**Key modifications include:**\n\n- Header and Footer now have a blocking request for the shared data that is the same for all users.\n- Data that can change for logged in users is now a separate request.\n- Our query functions now take in all params required for fetching, instead of accessing dynamic variables internally. This is important to serialize arguments if we want to eventually `use cache`.\n- Dynamic fetches (using customerAccessToken or preferred currency) are now all streaming queries.\n- Use `Streamable.from` to generate our streaming props that are passed to our UI components.\n- Update Header UI component to allow streaming in of currencies data.\n\n**Migration instructions:**\n\n- Renamed `/app/[locale]/(default)/query.ts` to `/app/[locale]/(default)/page-data.ts`, include page query on this page.\n- Updated `/app/[locale]/(default)/page.ts` to use `Streamable.from` pattern.\n- Split data that can vary by user from `core/components/footer/fragment.ts` and `core/components/header/fragment.ts`\n- Updated `core/components/header/index.tsx` and `core/components/footer/index.tsx` to fetch shared data in a blocking request and pass data that varies by customer as streamable data. Updated to use the new `Streamable.from` pattern.\n- [`061063f`](https://github.com/bigcommerce/catalyst/commit/061063f) Thanks [@jorgemoya](https://github.com/jorgemoya)! - This refactor optimizes search PLP for caching and the eventual use of dynamicIO. With these changes we leverage data caching to hit mostly cache data for guest shoppers in different locales and with different currencies.\n\n**Key modifications include:**\n\n- We don't stream in Search page data, instead it's a blocking call to get page data.\n- Our query functions now take in all params required for fetching, instead of accessing dynamic variables internally. This is important to serialize arguments if we want to eventually `use cache`.\n- Use `Streamable.from` to generate our streaming props that are passed to our UI components.\n- Remove use of nuqs' `createSearchParamsCache` in favor of nuqs' `createLoader`.\n\n**Migration instructions:**\n\n- Update `/(facted)/search/page.tsx`\n  - For this page we are now doing a blocking request for brand page data. Instead of having functions that each would read from props, we share streamable functions that can be passed to our UI components. We still stream in filter and product data.\n\n- [`da2a462`](https://github.com/bigcommerce/catalyst/commit/da2a462) Thanks [@bookernath](https://github.com/bookernath)! - Adds the ability to redirect after logout.\n\n- [`863d744`](https://github.com/bigcommerce/catalyst/commit/863d744) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Removes the old analytics provider in favor of the provider that fetches the configuration from the GraphQL API.\n\n- [`061063f`](https://github.com/bigcommerce/catalyst/commit/061063f) Thanks [@jorgemoya](https://github.com/jorgemoya)! - This refactor optimizes brand PLP for caching and the eventual use of dynamicIO. With these changes we leverage data caching to hit mostly cache data for guest shoppers in different locales and with different currencies.\n\n**Key modifications include:**\n\n- We don't stream in Brand page data, instead it's a blocking call that will redirect to `notFound` when brand is not found. Same for metadata.\n- Our query functions now take in all params required for fetching, instead of accessing dynamic variables internally. This is important to serialize arguments if we want to eventually `use cache`.\n- Use `Streamable.from` to generate our streaming props that are passed to our UI components.\n- Remove use of nuqs' `createSearchParamsCache` in favor of nuqs' `createLoader`.\n\n**Migration instructions:**\n\n- Update `/(facted)/brand/[slug]/page.tsx`\n  - For this page we are now doing a blocking request for brand page data. Instead of having functions that each would read from props, we share streamable functions that can be passed to our UI components. We still stream in filter and product data.\n- Update `/(facted)/brand/[slug]/page-data.tsx`\n  - Request now accept `customerAccessToken` as a prop instead of calling internally.\n\n### Patch Changes\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`c73b57e`](https://github.com/bigcommerce/catalyst/commit/c73b57e) Thanks [@migueloller](https://github.com/migueloller)! - Use `setRequestLocale` in all pages and layouts and pass `locale` parameter to `getTranslations` in all `generateMetadata` to maximize static rendering. This is part of the ongoing work in preparation of enabling PPR and `dynamicIO` for all routes.\n\n- [`d70596e`](https://github.com/bigcommerce/catalyst/commit/d70596e) Thanks [@alanpledger](https://github.com/alanpledger)! - Fixes types for signIn credentials and improves error handling for registering a customer.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Applied streamable pattern to Cart.\n\n- [`54ee390`](https://github.com/bigcommerce/catalyst/commit/54ee390) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove unecessary `fetchOptions` in object that has nothing to do with a client request.\n\n- [`ab1f0a0`](https://github.com/bigcommerce/catalyst/commit/ab1f0a0) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Add wishlist support to product display pages\n\n**Migration**\n\n- Ensure WishlistButton component is passed to additionalActions prop on ProductDetail\n- Ensure WishlistButtonForm is used on product page\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add persistent cart support\n\n- [`27b2823`](https://github.com/bigcommerce/catalyst/commit/27b2823) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Fix issue where delete button is not displayed if you have only 1 address\n\n**Migration steps:**\n\nUpdate `/core/app/[locale]/(default)/account/addresses/page.tsx` and pass the `minimumAddressCount={0}` prop to the AddressListSection component.\n\nExample:\n\n```tsx\nreturn (\n  <AddressListSection\n    addressAction={addressAction}\n    addresses={addresses}\n    cancelLabel={t('cancel')}\n    createLabel={t('create')}\n    deleteLabel={t('delete')}\n    editLabel={t('edit')}\n    fields={[...fields, { name: 'id', type: 'hidden', label: 'ID' }]}\n    minimumAddressCount={0}\n    setDefaultLabel={t('setDefault')}\n    showAddFormLabel={t('cta')}\n    title={t('title')}\n    updateLabel={t('update')}\n  />\n);\n```\n\n- [`0779856`](https://github.com/bigcommerce/catalyst/commit/0779856) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Adds Tailwind classes used to style the checkbox input and label based on the disabled state of the checkbox.\n\n**Migration:**\n\nSince this is a one-file change, you should be able to simply grab the diff from [this PR](https://github.com/bigcommerce/catalyst/pull/2399). The main changes to note are that we are [adding a `peer` class](https://v3.tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-sibling-state) to the CheckboxPrimitive.Root, explicitly styling the `enabled` pseudoclass, and only applying hover styles when the checkbox is enabled.\n\n- [`604450d`](https://github.com/bigcommerce/catalyst/commit/604450d) Thanks [@bookernath](https://github.com/bookernath)! - Re-apply auth grouping approach with middleware exemption to preserve functionality of /login/token endpoint for Customer Login API\n\n- [`82290cd`](https://github.com/bigcommerce/catalyst/commit/82290cd) Thanks [@migueloller](https://github.com/migueloller)! - Upgrade `next-intl` to v4 and add strong types for translated messages via TypeScript type augmentation.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Clean up 'en' dictionary.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove unused dependencies.\n\n- [`6b0c85a`](https://github.com/bigcommerce/catalyst/commit/6b0c85a) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Remove unused search props, add missing search translations\n\n**Migration**\n\n`core/components/header/index.tsx`\n\nEnsure the following props are passed to the `HeaderSection` navigation prop:\n\n```tsx\n        searchInputPlaceholder: t('Search.inputPlaceholder'),\n        searchSubmitLabel: t('Search.submitLabel'),\n```\n\n`core/messages/en.json`\n\nAdd the following keys to the `Components.Header.Search` translations:\n\n```json\n        \"somethingWentWrong\": \"Something went wrong. Please try again.\",\n        \"inputPlaceholder\": \"Search products, categories, brands...\",\n        \"submitLabel\": \"Search\"\n```\n\n`core/vibes/soul/primitives/navigation/index.tsx`\n\nCopy all changes from this file:\n\n1. Create `searchSubmitLabel?: string;` property, ensure it is passed into `SearchForm`\n2. On the `SearchForm`, remove the `searchCtaLabel = 'View more',` property, as it is unused, and rename `submitLabel` to `searchSubmitLabel`\n3. Ensure that `SearchForm` passes `searchSubmitLabel` to the `SearchButton`: `<SubmitButton loading={isPending} submitLabel={searchSubmitLabel} />`\n4. Remove the `searchCtaLabel` property from the `SearchResults` component\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Format totalCount value for i18n.\n\n- [`dd42b25`](https://github.com/bigcommerce/catalyst/commit/dd42b25) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Fix the faceted search pages to account for facets with spaces or other special characters in the name.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add date field to product details form.\n\n- [`d9685ee`](https://github.com/bigcommerce/catalyst/commit/d9685ee) Thanks [@bookernath](https://github.com/bookernath)! - Remove featured products panel from 404 page, allowing the page to be static in preparation for adding a search box\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`8baf8b3`](https://github.com/bigcommerce/catalyst/commit/8baf8b3) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Memoize `GetCartCountQuery` using React.js `cache()` so that it only hits the GraphQL API once per render, instead of twice.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add shipping selection to checkout.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`6401bb2`](https://github.com/bigcommerce/catalyst/commit/6401bb2) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update ProductListSection's and ReviewsSection's `totalCount` prop to string.\n\n- [`43351ab`](https://github.com/bigcommerce/catalyst/commit/43351ab) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Dedupe requests in various pages by properly caching/memoizing the function per page render.\n\n- [`b19ee74`](https://github.com/bigcommerce/catalyst/commit/b19ee74) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Updates `SelectField` to have a hidden input to pass the value of the select to the form. This is a workaround for a [Radix Select issue](https://github.com/radix-ui/primitives/issues/3198) that auto selects the first option in the select when submitting a form (even when no selection has been made).\n\nAdditionally, fixes an issue of incorrectly adding an empty query param for product options when an option is empty.\n\n**Migration**\n\nMigration is straighforward and requires adding the hidden input to the component and renaming the `name` prop for the `Select` component to something temporary.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`d663741`](https://github.com/bigcommerce/catalyst/commit/d663741) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Revert UI changes for product form since streaming in fields causes an issue with the form.\n\n- [`7bc57c8`](https://github.com/bigcommerce/catalyst/commit/7bc57c8) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Set a min-height for the Navigation fallback skeleton to prevent layout shift.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`c70bff2`](https://github.com/bigcommerce/catalyst/commit/c70bff2) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Dedupe requests in \"webpages\" by properly caching/memoizing the fetch function per page render.\n\n- [`5a853c2`](https://github.com/bigcommerce/catalyst/commit/5a853c2) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Check for `error.type` instead of `error.name` auth error in Login, since `error.name` gets minified in production and the check never returns `true`. Additionally, add a check for the `cause.err` to be of type `BigcommerceGQLError`.\n\n**Migration:**\n\n- Change `error.name === 'CallbackRouteError'` to `error.type === 'CallbackRouteError'` check in the error handling of the login action and include `error.cause.err instanceof BigCommerceGQLError`.\n\n- [`fada842`](https://github.com/bigcommerce/catalyst/commit/fada842) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Adds the `__Secure-` prefix to the add additional broswer security policies around this cookie.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`976c74d`](https://github.com/bigcommerce/catalyst/commit/976c74d) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Fix blog post card date formatting on alternate locales\n\n**Migration**\n\n`core/vibes/soul/primitives/blog-post-card/index.tsx`\n\nUpdate the component to use `<time dateTime={date}>{date}</time>` for the date, instead of calling `new Date(date).toLocaleDateString(...)`.\n\n- [`9176f56`](https://github.com/bigcommerce/catalyst/commit/9176f56) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Fix possibility of duplicate `key` error in Breadcrumbs component for truncated breadcrumbs.\n\n**Migration**\n\nUpdate `core/vibes/soul/sections/breadcrumbs/index.tsx` to use `index` as the `key` property instead of `href`\n\n- [`9827e4c`](https://github.com/bigcommerce/catalyst/commit/9827e4c) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Translate home breadcrumb in Contact Us page.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`48d5c99`](https://github.com/bigcommerce/catalyst/commit/48d5c99) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Fix public wishlist analytics/server error\n\n- Add translation key for a Publish Wishlist empty state\n\n**Migration**\n\n1. Add the following imports to `core/app/[locale]/(default)/wishlist/[token]/page.tsx`:\n\n```tsx\nimport { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { WishlistAnalyticsProvider } from '~/app/[locale]/(default)/account/wishlists/[id]/_components/wishlist-analytics-provider';\n```\n\n2. Add the following function into the file:\n\n```tsx\nconst getAnalyticsData = async (token: string, searchParamsPromise: Promise<SearchParams>) => {\n  const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise);\n  const wishlist = await getPublicWishlist(token, searchParamsParsed);\n\n  if (!wishlist) {\n    return [];\n  }\n\n  return removeEdgesAndNodes(wishlist.items)\n    .map(({ product }) => product)\n    .filter((product) => product !== null)\n    .map((product) => {\n      return {\n        id: product.entityId,\n        name: product.name,\n        sku: product.sku,\n        brand: product.brand?.name ?? '',\n        price: product.prices?.price.value ?? 0,\n        currency: product.prices?.price.currencyCode ?? '',\n      };\n    });\n};\n```\n\n3. Wrap the component in the `WishlistAnalyticsProvider`:\n\n```tsx\nexport default async function PublicWishlist({ params, searchParams }: Props) {\n  // ...\n  return (\n    <WishlistAnalyticsProvider data={Streamable.from(() => getAnalyticsData(token, searchParams))}>\n      // ...\n    </WishlistAnalyticsProvider>\n  );\n}\n```\n\n4. Update `/core/messages/en.json` \"PublishWishlist\" to have translations:\n\n```json\n  \"PublicWishlist\": {\n    \"title\": \"Public Wish List\",\n    \"defaultName\": \"Public wish list\",\n    \"emptyWishlist\": \"This wish list doesn't have any products yet.\"\n  },\n```\n\n5. Update `WishlistDetails` component to accept the `emptyStateText` and `placeholderCount` props:\n\n```tsx\n// ...\nexport const WishlistDetails = ({\n  className = '',\n  wishlist: streamableWishlist,\n  emptyStateText,\n  paginationInfo,\n  headerActions,\n  prevHref,\n  placeholderCount,\n  action,\n  removeAction,\n}: Props) => {\n```\n\n6. Update `WishlistDetails` component to pass the `emptyStateText` and `placeholderCount` props to both the `WishlistDetailSkeleton` and `WishlistItems` components:\n\n```tsx\n<WishlistDetailSkeleton\n  className={className}\n  headerActions={typeof headerActions === 'function' ? headerActions() : headerActions}\n  placeholderCount={placeholderCount}\n  prevHref={prevHref}\n/>\n```\n\n```tsx\n<WishlistItems\n  action={action}\n  emptyStateText={emptyStateText}\n  items={items}\n  placeholderCount={placeholderCount}\n  removeAction={removeAction}\n  wishlistId={wishlist.id}\n/>\n```\n\n- [`1147a9e`](https://github.com/bigcommerce/catalyst/commit/1147a9e) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Deduplicate default image in the image gallery in PDP.\n\n- [`47b3ad0`](https://github.com/bigcommerce/catalyst/commit/47b3ad0) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Fix an issue with orders with deleted products throwing an error and stopping page render by settings the errorPolicy for requests to ignore errors and update Soul components to render the products without using links for these cases.\n\n- [`589c91a`](https://github.com/bigcommerce/catalyst/commit/589c91a) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Remove cache from a customer-specific wishlist query.\n\n- [`aecc145`](https://github.com/bigcommerce/catalyst/commit/aecc145) Thanks [@alekseygurtovoy](https://github.com/alekseygurtovoy)! - fix: localized home page routes are rewritten to the \"catch all\" page\n\n- [`3015503`](https://github.com/bigcommerce/catalyst/commit/3015503) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Fix style override issues with the latest version of the Tailwind bump. Changes should be easily rebasable.\n\n- [`a7b369c`](https://github.com/bigcommerce/catalyst/commit/a7b369c) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Fixes the error warning by having a `ProductPickList` with no images, by making the `image` prop optional for when it is not needed.\n\n**Migration**\n\n- Update `schema.ts` to allow optional `image` prop for `CardRadioField`\n- Update `productOptionsTransformer` switch to have two cases for `ProductPickList`\n  - `ProductPickList` with no image object\n  - `ProductPickListWithImages` with image object\n- Update ui component to make the `image` prop optional and conditionally render the image.\n\n- [`f16a6be`](https://github.com/bigcommerce/catalyst/commit/f16a6be) Thanks [@migueloller](https://github.com/migueloller)! - Adds `Streamable.from` and uses it wherever we were unintentionally executing an async function in a React Server Component.\n\n- [`43351ab`](https://github.com/bigcommerce/catalyst/commit/43351ab) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Pass in currency code to quick search results.\n\n- [`17d72ca`](https://github.com/bigcommerce/catalyst/commit/17d72ca) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Add the `store_hash` `<meta />` element to better support merchants. This enabled BigCommerce to identify the store more easily.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`7071dfe`](https://github.com/bigcommerce/catalyst/commit/7071dfe) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Add locale prefix to auth middleware protected route URLPattern\n\n**Migration**\n\nIn `core/middlewares/with-auth.ts`, update the `protectedPathPattern` variable to include an optional path segment for the locale:\n\n```tsx\nconst protectedPathPattern = new URLPattern({ pathname: `{/:locale}?/(account)/*` });\n```\n\n- [`67715bf`](https://github.com/bigcommerce/catalyst/commit/67715bf) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Update GQL client and auth middleware to handle invalid tokens and invalidate session.\n\n**Summary**\n\nThis will ensure that if a user is logged out elsewhere, they will be redirected to the /login page when they try to access a protected route.\n\nPreviously, the pages would 404 which is misleading.\n\n**Migration**\n\n1. Copy all changes from the `/core/client` directory and the `/packages/client` directory\n2. Copy translation values\n3. Copy all changes from the `/core/app/[locale]/(default)/account/` directory server actions\n4. Copy all changes from the `/core/app/[locale]/(default)/checkout/route.ts` file\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`6c77e57`](https://github.com/bigcommerce/catalyst/commit/6c77e57) Thanks [@jorgemoya](https://github.com/jorgemoya)! - This refactor optimizes PDP for caching and the eventual use of dynamicIO. With these changes we leverage data caching to hit mostly cache data for guest shoppers in different locales and with different currencies.\n\n**Key modifications include:**\n\n- Split queries into four:\n  - Page Metadata (metadata fields that only depend on locale)\n  - Product (for fields that only depend on locale)\n  - Streamable Product (for fields that depend on locale and variant selection)\n  - Product Pricing and Related Products (for fields that require locale, variant selection, and currency -- in this case, pricing and related products)\n- We don't stream in Product data, instead it's a blocking call that will redirect to `notFound` when product is not found.\n- Our query functions now take in all params required for fetching, instead of accessing dynamic variables internally. This is important to serialize arguments if we want to eventually `use cache`.\n- Use `Streamable.from` to generate our streaming props that are passed to our UI components.\n- Update UI components to allow streaming product options before streaming in buy button.\n\n**Migration instructions:**\n\n- Update `/product/[slug]/page.tsx`\n  - For this page we are now doing a blocking request that is simplified for metadata and as a base product. Instead of having functions that each would read from props, we share streamable functions that can be passed to our UI components.\n- Update `/product/[slug]/page-data.tsx`\n  - Expect our requests to be simplified/merged, essentially replacing what we had before for new requests and functions.\n- Update`/product/[slug]/_components`.\n  - Similar to `page.tsx` and `page.data`, expect changes in the fragments defined and how we pass streamable functions to UI components.\n- Update `/vibes/soul/product-detail/index.tsx` & `/vibes/soul/product-detail/product-detail-form.tsx`\n  - Minor changes to allow streaming in data.\n\n- [`8a25424`](https://github.com/bigcommerce/catalyst/commit/8a25424) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Refactors the sign in functionality to use two separate providers instead of one. This is some work needed to be done in order to provide a better API for session syncing so it shouldn't effect any existing functionality.\n\n- [`e968366`](https://github.com/bigcommerce/catalyst/commit/e968366) Thanks [@alekseygurtovoy](https://github.com/alekseygurtovoy)! - fix: `useCompareDrawer` does not throw on missing context\n\n- [`a19b3ba`](https://github.com/bigcommerce/catalyst/commit/a19b3ba) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Fix persistent cart behavior during login.\n\n**Migration**\n\nIn `core/auth/index.ts`, create the `cartIdSchema` variable:\n\n```ts\nconst cartIdSchema = z\n  .string()\n  .uuid()\n  .or(z.literal('undefined')) // auth.js seems to pass the cart id as a string literal 'undefined' when not set.\n  .optional()\n  .transform((val) => (val === 'undefined' ? undefined : val));\n```\n\nThen, update all `Credentials` schemas to use this new `cartIdSchema`:\n\n```ts\nconst PasswordCredentials = z.object({\n  email: z.string().email(),\n  password: z.string().min(1),\n  cartId: cartIdSchema,\n});\n\nconst AnonymousCredentials = z.object({\n  cartId: cartIdSchema,\n});\n\nconst JwtCredentials = z.object({\n  jwt: z.string(),\n  cartId: cartIdSchema,\n});\n\nconst SessionUpdate = z.object({\n  user: z.object({\n    cartId: cartIdSchema,\n  }),\n});\n```\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add discounts summary item to Cart.\n\n- [`2de3c51`](https://github.com/bigcommerce/catalyst/commit/2de3c51) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Fixes an issue with the checkbox not properly triggering the required validation.\n- Fixes an issue with the checkbox not setting the default value from the API.\n- Fixes an issue with the field value being incorrectly set as `undefined`\n\n**Migration**\n\nUpdate the props to set a `checked` value and pasa an empty string when checked box is unselected.\n\n```\ncase 'checkbox':\n    return (\n    <Checkbox\n        checked={controls.value === 'true'}\n        errors={formField.errors}\n        key={formField.id}\n        label={field.label}\n        name={formField.name}\n        onBlur={controls.blur}\n        onCheckedChange={(value) => handleChange(value ? 'true' : '')}\n        onFocus={controls.focus}\n        required={formField.required}\n        value={controls.value ?? ''}\n    />\n    );\n```\n\n- [`c5ce9dc`](https://github.com/bigcommerce/catalyst/commit/c5ce9dc) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Properly handle the auth error when login is invalid.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`2a7b05f`](https://github.com/bigcommerce/catalyst/commit/2a7b05f) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Add translations for 'Search' button on 404 page\n\n**Migration**\n\n1. Add `\"search\"` translation key in the `\"NotFound\"` translations\n2. In `core/vibes/soul/sections/not-found/index.tsx`, add a `ctaLabel` property and ensure it is used in place of the \"Search\" text\n3. In `core/app/[locale]/not-found.tsx`, pass the `ctaLabel` prop as the new translation key `ctaLabel={t('search')}`\n\n- [`c095663`](https://github.com/bigcommerce/catalyst/commit/c095663) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Moves some auth related route handlers under the (auth) route group. This is to cleanup some of the routing.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add result type to all `generateMetadata`.\n\n- [`a15d84c`](https://github.com/bigcommerce/catalyst/commit/a15d84c) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Renames `core/app/[locale]/(default)/product/[slug]/_components/product-analytics-provider/index.tsx` to `core/app/[locale]/(default)/product/[slug]/_components/product-analytics-provider.tsx` for consistency with the other analytics components.\n\n**Migration**\n\nTo migrate, rename the file with git:\n\n```bash\ngit mv core/app/[locale]/(default)/product/[slug]/_components/product-analytics-provider/index.tsx core/app/[locale]/(default)/product/[slug]/_components/product-analytics-provider.tsx\n```\n\n- [`5e5314b`](https://github.com/bigcommerce/catalyst/commit/5e5314b) Thanks [@jorgemoya](https://github.com/jorgemoya)! - We want state to be persitent on the `ProductDetailForm`, even after submit. This change will allow the API error messages to properly show when the form is submitted. Additionally, other form fields will retain state (like item quantity).\n\n**Migration**\n\n- Update `ProductDetailForm` to prevent reset on submit, by removing `requestFormReset` in the `onSubmit`.\n- Remove `router.refresh()` call and instead call new `revalidateCart` action.\n  - `revalidateCart` is an action that `revalidateTag(TAGS.cart)`\n  - This prevents the form from fully refreshing on success.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`8c4f374`](https://github.com/bigcommerce/catalyst/commit/8c4f374) Thanks [@jordanarldt](https://github.com/jordanarldt)! - - Redirect to `/account/wishlists/` when a wishlist ID is not found\n- Pass `actionsTitle` to WishlistActionsMenu on WishlistDetails page\n\n**Migration**\n\n1. Copy changes from `/core/app/[locale]/(default)/account/wishlists/[id]/_components/wishlist-actions.tsx` - Ensure that `actionsTitle` is an allowed property and that it is passed into the `WishlistActionsMenu` component\n2. Copy changes from `/core/app/[locale]/(default)/account/wishlists/[id]/page.tsx` - Redirect to `/account/wishlists/` on 404\n3. Ensure that the `removeButtonTitle` prop is passed down all the way to the `RemoveWishlistItemButton` component in the `WishlistItemCard` component\n\n- [`45bbd92`](https://github.com/bigcommerce/catalyst/commit/45bbd92) Thanks [@jordanarldt](https://github.com/jordanarldt)! - - Update the account pages to match the style of VIBES and remain consistent with the rest of Catalyst.\n- Updated OrderDetails line items styling to display cost of each item and the selected `productOptions`\n- Created OrderDetails skeletons\n- Updated /account/orders/[id] to use `Streamable`\n\n**Migration**\n\n1. Copy all changes in the `/core/vibes/soul` directory\n2. Copy all changes in the `/core/app/[locale]/(default)/account` directory\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add coupon code form to Cart page.\n\n- [`e8c693a`](https://github.com/bigcommerce/catalyst/commit/e8c693a) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Add toast message when changing password\n\n**Migration**\n\n`core/vibes/soul/sections/account-settings/change-password-form.tsx`\n\n1. Import `toast`:\n\n```ts\nimport { toast } from '@/vibes/soul/primitives/toaster';\n```\n\n2. Update the `ChangePasswordAction` types:\n\n```ts\ntype Action<S, P> = (state: Awaited<S>, payload: P) => S | Promise<S>;\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  successMessage?: string;\n}\n\nexport type ChangePasswordAction = Action<State, FormData>;\n```\n\n3. Update the `useActionState` hook:\n\n```ts\nconst [state, formAction] = useActionState(action, { lastResult: null });\n```\n\n4. Update the `useEffect` hook to display a toast message on success:\n\n```ts\nuseEffect(() => {\n  if (state.lastResult?.status === 'success' && state.successMessage != null) {\n    toast.success(state.successMessage);\n  }\n\n  if (state.lastResult?.error) {\n    // eslint-disable-next-line no-console\n    console.log(state.lastResult.error);\n  }\n}, [state]);\n```\n\n`core/app/[locale]/(default)/account/settings/_actions/change-password.ts`\n\nUpdate all of the `return` values to match the new `ChangePasswordAction` interface, and return the `passwordUpdated` message on success.\n\n```ts\nexport const changePassword: ChangePasswordAction = async (prevState, formData) => {\n  const t = await getTranslations('Account.Settings');\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const submission = parseWithZod(formData, { schema: changePasswordSchema });\n\n  if (submission.status !== 'success') {\n    return { lastResult: submission.reply() };\n  }\n\n  const input = {\n    currentPassword: submission.value.currentPassword,\n    newPassword: submission.value.password,\n  };\n\n  try {\n    const response = await client.fetch({\n      document: CustomerChangePasswordMutation,\n      variables: {\n        input,\n      },\n      customerAccessToken,\n    });\n\n    const result = response.data.customer.changePassword;\n\n    if (result.errors.length > 0) {\n      return {\n        lastResult: submission.reply({ formErrors: result.errors.map((error) => error.message) }),\n      };\n    }\n\n    return {\n      lastResult: submission.reply(),\n      successMessage: t('passwordUpdated'),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => message),\n        }),\n      };\n    }\n\n    if (error instanceof Error) {\n      return {\n        lastResult: submission.reply({ formErrors: [error.message] }),\n      };\n    }\n\n    return { lastResult: submission.reply({ formErrors: [t('somethingWentWrong')] }) };\n  }\n};\n```\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Disable prefetch for the `/logout` link.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add textarea field to product details form.\n\n- [`525afdb`](https://github.com/bigcommerce/catalyst/commit/525afdb) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update empty state for account pages, adjusting headers and empty designs.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Set currency on cart at creation time\n\n- [`e145673`](https://github.com/bigcommerce/catalyst/commit/e145673) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Allow a list of CDN hostnames for cases when there can be more than one CDN available for image loader.\n\n**Migration:**\n\n- Update `build-config` schema to make `cdnUrls` an array of strings.\n- Update `next.config.ts` to set `cdnUrls` as an array, and set multiple preconnected Link headers (one per CDN).\n- `shouldUseLoaderProp` function now reads from array.\n\n- [`6b99400`](https://github.com/bigcommerce/catalyst/commit/6b99400) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Split coupon discounts and regular discounts from summary items, use total `cart.discountedAmount` for discounts.\n\n- [`0900330`](https://github.com/bigcommerce/catalyst/commit/0900330) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Refactors redirecting to checkout as a route. This will enable session syncing to happen through a redirect using the sites and routes API.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`7668774`](https://github.com/bigcommerce/catalyst/commit/7668774) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Disable PPR in Compare page due to an issue of Next.js and PPR, which causes the products to be removed once one is added to cart. More info: https://github.com/vercel/next.js/issues/59407.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`e8a9ebf`](https://github.com/bigcommerce/catalyst/commit/e8a9ebf) Thanks [@bookernath](https://github.com/bookernath)! - Revert auth route reorganization to fix regression with /login/token endpoint\n\n- [`84d416a`](https://github.com/bigcommerce/catalyst/commit/84d416a) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Soft fail analytics events if the provider is not rendered\n\n- [`6aef70b`](https://github.com/bigcommerce/catalyst/commit/6aef70b) Thanks [@chancellorclark](https://github.com/chancellorclark)! - Refactors the add to cart logic to handle some shared functionality like revalidating the tags and setting the cart state.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Use `setRequestLocale` only where needed\n\n- [`96f7c8e`](https://github.com/bigcommerce/catalyst/commit/96f7c8e) Thanks [@jordanarldt](https://github.com/jordanarldt)! - - Fix incorrect/missing translation messages\n- Separate defaultLocale in to a separate file\n- Remove caching in `/account` pages\n- Update `WishlistListItem` for better accessibility\n\n**Migration**\n\nUse this PR as a reference: https://github.com/bigcommerce/catalyst/pull/2341\n\n1. Update your `messages/en.json` file with the translation keys added in this PR\n2. Ensure that all components are being passed the correct translation keys\n3. Update all references to `defaultLocale` to point to the `~/i18n/locales` file created in this PR\n4. Update all pages in `/core/app/[locale]/(default)/account/` and ensure that `cache: 'no-store'` is set on the `client.fetch` calls\n5. Update the `WishlistListItem` component to use the new accessibility features/tags as shown in the PR\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`5b83a97`](https://github.com/bigcommerce/catalyst/commit/5b83a97) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Pass search params to router.redirect when swapping locales.\n\n**Migration**\n\nModify `useSwitchLocale` hook to include `Object.fromEntries(searchParams.entries())`.\n\n- [`edda0e3`](https://github.com/bigcommerce/catalyst/commit/edda0e3) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add missing border style for `Input`, `NumberInput` and `DatePicker`.\n\n**Migration**\n\nFollowing convention, add these conditional classes to the fields using `clsx`:\n\n```\n{\nlight:\n    errors && errors.length > 0\n    ? 'border-[var(--input-light-border-error,hsl(var(--error)))]'\n    : 'border-[var(--input-light-border,hsl(var(--contrast-100)))]',\ndark:\n    errors && errors.length > 0\n    ? 'border-[var(--input-dark-border-error,hsl(var(--error)))]'\n    : 'border-[var(--input-dark-border,hsl(var(--contrast-500)))]',\n}[colorScheme],\n```\n\n- [`aade48a`](https://github.com/bigcommerce/catalyst/commit/aade48a) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove explicit locale override in Link component that was appending default locale to links even with the 'as-needed' mode.\n\n- [`11ecddf`](https://github.com/bigcommerce/catalyst/commit/11ecddf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update translations.\n\n- [`157ea54`](https://github.com/bigcommerce/catalyst/commit/157ea54) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Rename some GQL query/mutations/fragments to standardized naming.\n\n- [`c4e56c6`](https://github.com/bigcommerce/catalyst/commit/c4e56c6) Thanks [@alekseygurtovoy](https://github.com/alekseygurtovoy)! - fix: switching locales redirects user to the home page\n\n- [`d9edb44`](https://github.com/bigcommerce/catalyst/commit/d9edb44) Thanks [@bookernath](https://github.com/bookernath)! - Remove unused variants collection from query for PDP\n\n- [`816290a`](https://github.com/bigcommerce/catalyst/commit/816290a) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Add aria-label to currency selector and PDP wishlist buttons\n\n**Migration**\n\n1. Copy all changes from the `/messages/en.json` file to get updated translation keys\n2. Add the `label` prop to the `Heart` component in `/core/vibes/soul/primitives/favorite/heart.tsx`\n3. Add the `label` prop to the `Favorite` component in `/core/vibes/soul/primitives/favorite/index.tsx` and pass it to the `Heart` component\n4. Copy all changes in the `/core/vibes/soul/navigation/index.tsx` file to add the `switchCurrencyLabel` property\n5. Update `/core/components/header/index.tsx` file to pass the `switchCurrencyLabel` to the `HeaderSection` component\n6. Update `/core/app/[locale]/(default)/product/[slug]/_components/wishlist-button/index.tsx` to pass the `label` prop to the `Favorite` component\n\n## 0.24.1\n\n### Patch Changes\n\n- [`632a645`](https://github.com/bigcommerce/catalyst/commit/632a645850c500be9ea478490e1df4b98d9b3543) Thanks [@bookernath](https://github.com/bookernath)! - Add stub for generating Customer Login API tokens for SSO integrations\n\n- [`632a645`](https://github.com/bigcommerce/catalyst/commit/632a645850c500be9ea478490e1df4b98d9b3543) Thanks [@bookernath](https://github.com/bookernath)! - Add /login/token endpoint to power Customer Login API\n\n- [#1816](https://github.com/bigcommerce/catalyst/pull/1816) [`6eb30ac`](https://github.com/bigcommerce/catalyst/commit/6eb30ac1745e2dcc37aef892fb001f218d9b8ddb) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n## 0.24.0\n\n### Minor Changes\n\n- [#1749](https://github.com/bigcommerce/catalyst/pull/1749) [`cacdd22`](https://github.com/bigcommerce/catalyst/commit/cacdd22de140897f57fb8aaf52b2a9e7f48c23c4) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Change the rest of the auth pages to use toasts.\n\n- [#1746](https://github.com/bigcommerce/catalyst/pull/1746) [`0e34915`](https://github.com/bigcommerce/catalyst/commit/0e34915171da18ed141ecfacc6fa4c2a8f5e4c23) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Converts the change password messages over to using a toast. This should provide a better DX and UX.\n\n- [#1747](https://github.com/bigcommerce/catalyst/pull/1747) [`608b886`](https://github.com/bigcommerce/catalyst/commit/608b886978518f3d27230f50a2ad462363527d63) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Update the register customer page to use toasts for messaging.\n\n- [#1749](https://github.com/bigcommerce/catalyst/pull/1749) [`cacdd22`](https://github.com/bigcommerce/catalyst/commit/cacdd22de140897f57fb8aaf52b2a9e7f48c23c4) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Converts the reset password messages over to using a toast.\n\n- [#1749](https://github.com/bigcommerce/catalyst/pull/1749) [`cacdd22`](https://github.com/bigcommerce/catalyst/commit/cacdd22de140897f57fb8aaf52b2a9e7f48c23c4) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Remove the account state provider components\n\n- [#1749](https://github.com/bigcommerce/catalyst/pull/1749) [`cacdd22`](https://github.com/bigcommerce/catalyst/commit/cacdd22de140897f57fb8aaf52b2a9e7f48c23c4) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Converts the login messages over to using a toast.\n\n- [#1743](https://github.com/bigcommerce/catalyst/pull/1743) [`7c03428`](https://github.com/bigcommerce/catalyst/commit/7c03428bf815bf2cc7b8aa35ff331379f7615094) Thanks [@chanceaclark](https://github.com/chanceaclark)! - After login, redirect to orders page instead of an account overview page. This also removes the account overview page.\n\n- [#1741](https://github.com/bigcommerce/catalyst/pull/1741) [`5136fac`](https://github.com/bigcommerce/catalyst/commit/5136fac6e05c6eb1ebce9707abcf1f180712358e) Thanks [@chanceaclark](https://github.com/chanceaclark)! - If a customer is already logged in, we want to redirect them back to their account pages if they are trying to hit one of the non-logged-in customer auth routes. The prevents any side effects that may occur trying to re-auth the client. This is done by providing a root layout.tsx page under the (auth) route group.\n\n- [#1749](https://github.com/bigcommerce/catalyst/pull/1749) [`cacdd22`](https://github.com/bigcommerce/catalyst/commit/cacdd22de140897f57fb8aaf52b2a9e7f48c23c4) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Converts the change/forgot password messages over to using a toast.\n\n### Patch Changes\n\n- [#1765](https://github.com/bigcommerce/catalyst/pull/1765) [`1c9b880`](https://github.com/bigcommerce/catalyst/commit/1c9b8804cec99f5fd9700b422a3fb9739a850045) Thanks [@bookernath](https://github.com/bookernath)! - Assign cart to customer as part of initial login mutation\n\n- [#1760](https://github.com/bigcommerce/catalyst/pull/1760) [`f6161c5`](https://github.com/bigcommerce/catalyst/commit/f6161c5dcf2fbd65f4192eec36ebd3e62e60bd33) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n## 0.23.0\n\n### Minor Changes\n\n- [#1639](https://github.com/bigcommerce/catalyst/pull/1639) [`ae2c6cd`](https://github.com/bigcommerce/catalyst/commit/ae2c6cd76b2ccc5c994bd298983cb1665c571d02) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - Add orders for customer account. Now customer can open orders history or move to specific order details.\n\n- [#1729](https://github.com/bigcommerce/catalyst/pull/1729) [`d52affe`](https://github.com/bigcommerce/catalyst/commit/d52affe56dee23a81263392030fe635c824fb182) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Removed ReCaptcha validation when you are logged in and making account changes. We have already validated a customer is human at the loggin screen.\n\n- [#1728](https://github.com/bigcommerce/catalyst/pull/1728) [`d7dbd7a`](https://github.com/bigcommerce/catalyst/commit/d7dbd7a04fc8cb87cf223fb5a17af8d59c6431ea) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Convert the messages that were displayed when deleting an address over to using the toast functionality.\n\n### Patch Changes\n\n- [#1727](https://github.com/bigcommerce/catalyst/pull/1727) [`d3c6dbc`](https://github.com/bigcommerce/catalyst/commit/d3c6dbc25c16901f694e053ccdee8193647f5760) Thanks [@migueloller](https://github.com/migueloller)! - Ignore empty strings when parsing array URL search parameters in faceted search.\n\n- [#1730](https://github.com/bigcommerce/catalyst/pull/1730) [`ad8c86d`](https://github.com/bigcommerce/catalyst/commit/ad8c86d574474eb5ed18d99265fe4001d267fb5f) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Fixes the inventory handling to handle some options being out of stock.\n\n## 0.22.1\n\n### Patch Changes\n\n- [#1649](https://github.com/bigcommerce/catalyst/pull/1649) [`d38f164`](https://github.com/bigcommerce/catalyst/commit/d38f164d3e87ca87d3e792f8058a74c1f13e4220) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - improve account forms submit errors message\n\n- [#1651](https://github.com/bigcommerce/catalyst/pull/1651) [`1a222cb`](https://github.com/bigcommerce/catalyst/commit/1a222cb09dfc65b440090f868b01291e644bec4a) Thanks [@bc-yevhenii-buliuk](https://github.com/bc-yevhenii-buliuk)! - refresh the entire list of addresses after deleting an address\n\n- [#1722](https://github.com/bigcommerce/catalyst/pull/1722) [`1f0c2ef`](https://github.com/bigcommerce/catalyst/commit/1f0c2ef9212be079630f64a15a2f121ed7a358f9) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Remove `--turbo` from `pnpm dev` as it has some issues with the latest dependency bump, along with others.\n\n## 0.22.0\n\n### Minor Changes\n\n- [#1717](https://github.com/bigcommerce/catalyst/pull/1717) [`12fea79`](https://github.com/bigcommerce/catalyst/commit/12fea7962c25c395b550717343300561fb8d6a4c) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Add a check for variant stock levels on add to cart button\n\n- [#1674](https://github.com/bigcommerce/catalyst/pull/1674) [`512c338`](https://github.com/bigcommerce/catalyst/commit/512c338e4abcb3cdb7f457e4012e0c90c6a8391a) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Uses the API responses to show better errors when adding a product to the cart.\n\n- [#1710](https://github.com/bigcommerce/catalyst/pull/1710) [`15edf31`](https://github.com/bigcommerce/catalyst/commit/15edf311f5508a85f09acd8135fbf2b4aae09ff0) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Rename `BcImage` to `Image`\n\n- [#1703](https://github.com/bigcommerce/catalyst/pull/1703) [`7b598ff`](https://github.com/bigcommerce/catalyst/commit/7b598ff012ce40fe4b34be780c01cdbbe61e9b7e) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Adds localized data fetching withing the beforeRequest client helper. If information is translated (currently possible to update via the Admin GraphQL API) then we will return the translated product data. See https://developer.bigcommerce.com/docs/store-operations/catalog/graphql-admin/product-basic-info for more information on how to use overrides.\n\n- [#1710](https://github.com/bigcommerce/catalyst/pull/1710) [`15edf31`](https://github.com/bigcommerce/catalyst/commit/15edf311f5508a85f09acd8135fbf2b4aae09ff0) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Force usage of the `<Image/>` component. This component should fallback to using the default image loader if the url doesn't come from the BigCommerce CDN.\n\n- [#1672](https://github.com/bigcommerce/catalyst/pull/1672) [`ffefc61`](https://github.com/bigcommerce/catalyst/commit/ffefc6151b0fb09bf83e7556736452a3138ef9c4) Thanks [@chanceaclark](https://github.com/chanceaclark)! - If a string is not provided in the selected locale, the translation system will fallback to \"en\" for that specific entry.\n\n### Patch Changes\n\n- [#1661](https://github.com/bigcommerce/catalyst/pull/1661) [`93d9984`](https://github.com/bigcommerce/catalyst/commit/93d99844ed4957a5a4611970589a2246b1dffb16) Thanks [@bookernath](https://github.com/bookernath)! - Remove webpack chunk plugin\n\n- [#1688](https://github.com/bigcommerce/catalyst/pull/1688) [`3267840`](https://github.com/bigcommerce/catalyst/commit/3267840981ebb6ed62e0b87f60623d0c4352309d) Thanks [@thebigrick](https://github.com/thebigrick)! - Added aria label for compare button\n\n- [#1617](https://github.com/bigcommerce/catalyst/pull/1617) [`c852961`](https://github.com/bigcommerce/catalyst/commit/c852961063fb090907b23074301fcbc41e75b8ec) Thanks [@bc-yevhenii-buliuk](https://github.com/bc-yevhenii-buliuk)! - UX improvements for account pages\n\n- [#1690](https://github.com/bigcommerce/catalyst/pull/1690) [`ee6bbb9`](https://github.com/bigcommerce/catalyst/commit/ee6bbb96e9c357af249fb881f5de503f9e164fb1) Thanks [@thebigrick](https://github.com/thebigrick)! - Added localization to hardcoded strings\n\n- [#1647](https://github.com/bigcommerce/catalyst/pull/1647) [`ad5ed3f`](https://github.com/bigcommerce/catalyst/commit/ad5ed3f50f6d3025bf299cc04f51bf0864afd3a2) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - update submit create account errors message\n\n- [#1715](https://github.com/bigcommerce/catalyst/pull/1715) [`2960a70`](https://github.com/bigcommerce/catalyst/commit/2960a708084030b484de945e725b5bd0c32462ee) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n- [#1694](https://github.com/bigcommerce/catalyst/pull/1694) [`07f8463`](https://github.com/bigcommerce/catalyst/commit/07f84634000c4d1dac6f89037d9501bc056537c9) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n## 0.21.0\n\n### Minor Changes\n\n- [#1631](https://github.com/bigcommerce/catalyst/pull/1631) [`58d9e7c`](https://github.com/bigcommerce/catalyst/commit/58d9e7ccb7915593cd012cce6d9f4bdf66cb381f) Thanks [@deini](https://github.com/deini)! - fetch available locales at build time\n\n### Patch Changes\n\n- [#1636](https://github.com/bigcommerce/catalyst/pull/1636) [`23abacf`](https://github.com/bigcommerce/catalyst/commit/23abacfb8ff4ff9d269e51821a6a992a9cb2d4f5) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Remove console.error when falling back to defaultChannelId\n\n- [#1636](https://github.com/bigcommerce/catalyst/pull/1636) [`23abacf`](https://github.com/bigcommerce/catalyst/commit/23abacfb8ff4ff9d269e51821a6a992a9cb2d4f5) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Clean up login error handling.\n\n- Updated dependencies [[`23abacf`](https://github.com/bigcommerce/catalyst/commit/23abacfb8ff4ff9d269e51821a6a992a9cb2d4f5)]:\n  - @bigcommerce/catalyst-client@0.14.0\n\n## 0.20.0\n\n### Minor Changes\n\n- [#1623](https://github.com/bigcommerce/catalyst/pull/1623) [`16e3a76`](https://github.com/bigcommerce/catalyst/commit/16e3a763571324dccd9031a79e400409eff9ee0c) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Next 15 upgrade\n\n### Patch Changes\n\n- [#1629](https://github.com/bigcommerce/catalyst/pull/1629) [`72a30a8`](https://github.com/bigcommerce/catalyst/commit/72a30a84193f7ed8a09b770d16dd2c9a8a7d1347) Thanks [@deini](https://github.com/deini)! - Use Typescript on Next Config\n\n- [#1618](https://github.com/bigcommerce/catalyst/pull/1618) [`d60e916`](https://github.com/bigcommerce/catalyst/commit/d60e916661385fab211f7e8b1342dbda2fd504b9) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n- Updated dependencies [[`16e3a76`](https://github.com/bigcommerce/catalyst/commit/16e3a763571324dccd9031a79e400409eff9ee0c)]:\n  - @bigcommerce/catalyst-client@0.13.0\n\n## 0.19.0\n\n### Minor Changes\n\n- [#1262](https://github.com/bigcommerce/catalyst/pull/1262) [`0c2023b`](https://github.com/bigcommerce/catalyst/commit/0c2023bae650039cd79ba51b1161b5c8c16f0b8d) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Removes all usages of the customer impersonation token. Also updates the docs to correspond with the Storefront API Token.\n\n- [#1262](https://github.com/bigcommerce/catalyst/pull/1262) [`0c2023b`](https://github.com/bigcommerce/catalyst/commit/0c2023bae650039cd79ba51b1161b5c8c16f0b8d) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Allows the ability to consume a [storefront token](https://developer.bigcommerce.com/docs/rest-authentication/tokens#storefront-tokens). This new token will allow Catalyst to create `customerAccessToken`'s whenever a user logs into their account. This change doesn't include consuming the either token, only adding the ability to pass it in.\n\n### Patch Changes\n\n- Updated dependencies [[`0c2023b`](https://github.com/bigcommerce/catalyst/commit/0c2023bae650039cd79ba51b1161b5c8c16f0b8d), [`0c2023b`](https://github.com/bigcommerce/catalyst/commit/0c2023bae650039cd79ba51b1161b5c8c16f0b8d)]:\n  - @bigcommerce/catalyst-client@0.12.0\n\n## 0.18.1\n\n### Patch Changes\n\n- [#1525](https://github.com/bigcommerce/catalyst/pull/1525) [`e751319`](https://github.com/bigcommerce/catalyst/commit/e751319728359a2e72d48072a4b68055ed4dbb1e) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - fix warning for using the same keys on items\n\n- [#1521](https://github.com/bigcommerce/catalyst/pull/1521) [`fd83a78`](https://github.com/bigcommerce/catalyst/commit/fd83a78f94b170dcf6e8aed14c61e3791b64c5de) Thanks [@bc-yevhenii-buliuk](https://github.com/bc-yevhenii-buliuk)! - fix styles for active account tab\n\n- [#1520](https://github.com/bigcommerce/catalyst/pull/1520) [`c898792`](https://github.com/bigcommerce/catalyst/commit/c898792a0ed3ee9849cdfeda7018245e491e8016) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - improve error message on reset password page\n\n- [#1524](https://github.com/bigcommerce/catalyst/pull/1524) [`f08883c`](https://github.com/bigcommerce/catalyst/commit/f08883c8fa559f0b6015321e2396606d77fa0ad6) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - improve behaviour for change password page for logged in user\n\n- [#1529](https://github.com/bigcommerce/catalyst/pull/1529) [`22426b2`](https://github.com/bigcommerce/catalyst/commit/22426b256e29b6c3dd145fd6df9ed57c5a99bd75) Thanks [@bc-yevhenii-buliuk](https://github.com/bc-yevhenii-buliuk)! - fix validation message for email on account settings page\n\n- [#1516](https://github.com/bigcommerce/catalyst/pull/1516) [`41270c2`](https://github.com/bigcommerce/catalyst/commit/41270c29a6e21217622c29b18e91f9a24d58ea8b) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n- [#1534](https://github.com/bigcommerce/catalyst/pull/1534) [`de48618`](https://github.com/bigcommerce/catalyst/commit/de486186acfec2604d749b9f6d2b4656a9e9280a) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n## 0.18.0\n\n### Minor Changes\n\n- [#1491](https://github.com/bigcommerce/catalyst/pull/1491) [`313a591`](https://github.com/bigcommerce/catalyst/commit/313a5913181a144b53cb12208132f4a9924e2256) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Bump `next-intl` which includes [some minor changes and updated APIs](<(https://next-intl-docs.vercel.app/blog/next-intl-3-22)>):\n  - Use new `createNavigation` api.\n  - Pass `locale` to redirects.\n  - `setRequestLocale` is no longer unstable.\n\n### Patch Changes\n\n- [#1505](https://github.com/bigcommerce/catalyst/pull/1505) [`691ec2b`](https://github.com/bigcommerce/catalyst/commit/691ec2bcbb8839446463e292856080cc9b16c584) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - update login page & error message styles\n\n- [#1506](https://github.com/bigcommerce/catalyst/pull/1506) [`ac83d3e`](https://github.com/bigcommerce/catalyst/commit/ac83d3eb98e19307a3a82fa94c222cff3c0806f0) Thanks [@bc-yevhenii-buliuk](https://github.com/bc-yevhenii-buliuk)! - remove unnecessary fields from account settings form and update confirmation message\n\n- [#1499](https://github.com/bigcommerce/catalyst/pull/1499) [`b5aea9b`](https://github.com/bigcommerce/catalyst/commit/b5aea9b36159d11a77d090fee62cb1736bc794be) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Bumps next-intl to fix issue with hashes and query params in urls.\n\n- [#1511](https://github.com/bigcommerce/catalyst/pull/1511) [`370d0b1`](https://github.com/bigcommerce/catalyst/commit/370d0b18f0f47100d7e520fcf9f209f6e41f34e9) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - update styles for reset password validation\n\n- [#1454](https://github.com/bigcommerce/catalyst/pull/1454) [`53599e6`](https://github.com/bigcommerce/catalyst/commit/53599e6e02988ab63d158c5c9f587669a5581402) Thanks [@bc-yevhenii-buliuk](https://github.com/bc-yevhenii-buliuk)! - remove unnecessary fields from create account form\n\n- [#1487](https://github.com/bigcommerce/catalyst/pull/1487) [`a22233f`](https://github.com/bigcommerce/catalyst/commit/a22233f8fc94c5ad602fa734cadbb892af34fe6b) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n## 0.17.1\n\n### Patch Changes\n\n- Updated dependencies [[`d4120d3`](https://github.com/bigcommerce/catalyst/commit/d4120d39c10398e842a7ebe14ada685ec8aae3a8)]:\n  - @bigcommerce/catalyst-client@0.11.0\n\n## 0.17.0\n\n### Minor Changes\n\n- [#1401](https://github.com/bigcommerce/catalyst/pull/1401) [`3095002`](https://github.com/bigcommerce/catalyst/commit/3095002d7a10b9c4058016076deb7a45fc8ae7bb) Thanks [@bookernath](https://github.com/bookernath)! - Add dynamic robots.txt from control panel settings\n\n### Patch Changes\n\n- [#1477](https://github.com/bigcommerce/catalyst/pull/1477) [`79e705f`](https://github.com/bigcommerce/catalyst/commit/79e705f151a733a811effed40757030aba6b6300) Thanks [@deini](https://github.com/deini)! - Breadcrumbs for top level category pages are no longer rendered\n\n- [#1467](https://github.com/bigcommerce/catalyst/pull/1467) [`e763a83`](https://github.com/bigcommerce/catalyst/commit/e763a83bcd4b8b5311586247291338eb65fbc476) Thanks [@deini](https://github.com/deini)! - Fixes an issue when a numeric product option set to a minimum <= 0 breaks the counter component.\n\n- [#1459](https://github.com/bigcommerce/catalyst/pull/1459) [`b4485c7`](https://github.com/bigcommerce/catalyst/commit/b4485c76de8c83546c68a7b50fcb7991603dbf6e) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Updates the with-routes middleware to fallback on locale based rewrite logic if the redirect is a dynamic entity redirect.\n\n- [#1469](https://github.com/bigcommerce/catalyst/pull/1469) [`8e9e7f3`](https://github.com/bigcommerce/catalyst/commit/8e9e7f3d40545004b080146b4dbb42f4ac7cf17c) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Fixes the product quantity reseting back to the previous value when adjusting the quantity fails.\n\n- [#1476](https://github.com/bigcommerce/catalyst/pull/1476) [`d47e3ac`](https://github.com/bigcommerce/catalyst/commit/d47e3aceb244713bc996287319357e6af3d865ed) Thanks [@deini](https://github.com/deini)! - adds an empty state to category pages\n\n- [#1458](https://github.com/bigcommerce/catalyst/pull/1458) [`3d67f8d`](https://github.com/bigcommerce/catalyst/commit/3d67f8d0d1776d747e9aa485b0b29a738eeacf3c) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Add no-store to mutations that are rate limited.\n\n- [#1453](https://github.com/bigcommerce/catalyst/pull/1453) [`1c8b042`](https://github.com/bigcommerce/catalyst/commit/1c8b04278074eb55358a5515f330a011de9561b5) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n- Updated dependencies [[`2d1526a`](https://github.com/bigcommerce/catalyst/commit/2d1526a50402b2eb677abd55f19fb904234d1a84)]:\n  - @bigcommerce/catalyst-client@0.10.0\n\n## 0.16.0\n\n### Minor Changes\n\n- [#1410](https://github.com/bigcommerce/catalyst/pull/1410) [`53cca82`](https://github.com/bigcommerce/catalyst/commit/53cca82611272fc3be24505b7c6d5866f10c87fd) Thanks [@bookernath](https://github.com/bookernath)! - Move /reset page to /login/forgot-password in order to reduce top-level routes.\n\n- [#1384](https://github.com/bigcommerce/catalyst/pull/1384) [`17692ca`](https://github.com/bigcommerce/catalyst/commit/17692caa3ff9b25180359d8a020470ece3e589f6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Pass customer ip address into requests that don't rely on cached values.\n\n- [#1388](https://github.com/bigcommerce/catalyst/pull/1388) [`a309a4d`](https://github.com/bigcommerce/catalyst/commit/a309a4dd47083a58c998a4f6d169185177cca571) Thanks [@deini](https://github.com/deini)! - wraps header and footer in suspense boundaries\n\n### Patch Changes\n\n- [#1374](https://github.com/bigcommerce/catalyst/pull/1374) [`1f76f61`](https://github.com/bigcommerce/catalyst/commit/1f76f615b38bb41db770653bd8e7947cd6361b18) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Prepend locale for redirected urls in tests.\n  More info: https://github.com/amannn/next-intl/issues/1335\n\n- [#1373](https://github.com/bigcommerce/catalyst/pull/1373) [`971033f`](https://github.com/bigcommerce/catalyst/commit/971033fc63181bad15aa46abb65b0d44501922c9) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add missing metadata in account settings page.\n\n- [#1370](https://github.com/bigcommerce/catalyst/pull/1370) [`655d518`](https://github.com/bigcommerce/catalyst/commit/655d518b2fd662614539467fff940b2b5ff78567) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n- [#1446](https://github.com/bigcommerce/catalyst/pull/1446) [`ba4820b`](https://github.com/bigcommerce/catalyst/commit/ba4820bf6dd36d0155028ad3db094bd9745d5d94) Thanks [@deini](https://github.com/deini)! - Fixes a bug where product variant was not reliably being selected on PDP when using pre-selected options.\n\n- [#1391](https://github.com/bigcommerce/catalyst/pull/1391) [`4d64c31`](https://github.com/bigcommerce/catalyst/commit/4d64c31d4765dd72c81c1836b66aa1d7cb34b5f5) Thanks [@bookernath](https://github.com/bookernath)! - Get lossy image from API instead of setting param in code\n\n- [#1389](https://github.com/bigcommerce/catalyst/pull/1389) [`a4eaff6`](https://github.com/bigcommerce/catalyst/commit/a4eaff6bb2520f748630e24a6a28ca31cd2eb2c3) Thanks [@bookernath](https://github.com/bookernath)! - Add additional IP address header\n\n- [#1402](https://github.com/bigcommerce/catalyst/pull/1402) [`6e75ef5`](https://github.com/bigcommerce/catalyst/commit/6e75ef5097e0f3227c04ac0d9d7bbc484513bcce) Thanks [@bc-yevhenii-buliuk](https://github.com/bc-yevhenii-buliuk)! - fixing the problem with submitting the password change form\n\n- [#1407](https://github.com/bigcommerce/catalyst/pull/1407) [`ac9832f`](https://github.com/bigcommerce/catalyst/commit/ac9832fcc61f01413a5b8f101f5f27c53ca1fce5) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n- [#1392](https://github.com/bigcommerce/catalyst/pull/1392) [`76227ac`](https://github.com/bigcommerce/catalyst/commit/76227ac06bb349f604f1d2d4a9b68e7d0869eba4) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n- [#1424](https://github.com/bigcommerce/catalyst/pull/1424) [`4874add`](https://github.com/bigcommerce/catalyst/commit/4874addfbdde90ac45aa57c10767587ba4c50735) Thanks [@bc-svc-local](https://github.com/bc-svc-local)! - Update translations.\n\n- [#1445](https://github.com/bigcommerce/catalyst/pull/1445) [`ba3f513`](https://github.com/bigcommerce/catalyst/commit/ba3f513ac4242ce6883ad6ab635d38156a271ca9) Thanks [@deini](https://github.com/deini)! - Adds optimistic updates to all \"Add to cart\" buttons. This change makes the UI feel snappier and give quick feedback on user interaction.\n\n- Updated dependencies [[`17692ca`](https://github.com/bigcommerce/catalyst/commit/17692caa3ff9b25180359d8a020470ece3e589f6)]:\n  - @bigcommerce/catalyst-client@0.9.0\n\n## 0.15.0\n\n### Minor Changes\n\n- [#1362](https://github.com/bigcommerce/catalyst/pull/1362) [`0814afe`](https://github.com/bigcommerce/catalyst/commit/0814afefca00b2497dddb0622df45f4d50865882) Thanks [@deini](https://github.com/deini)! - If app is not running on Vercel's infra, `<Analytics />` and `<SpeedInsights />` are not rendered.\n\n  Opt-out of vercel analytics and speed insights by setting the following env vars to `true`\n  - `DISABLE_VERCEL_ANALYTICS`\n  - `DISABLE_VERCEL_SPEED_INSIGHTS`\n\n- [#1354](https://github.com/bigcommerce/catalyst/pull/1354) [`3d298c7`](https://github.com/bigcommerce/catalyst/commit/3d298c7190e01309ee706c0b9696f8851071e73c) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Move address forms in account to their own /add and /edit pages.\n\n- [#1280](https://github.com/bigcommerce/catalyst/pull/1280) [`27cbfd2`](https://github.com/bigcommerce/catalyst/commit/27cbfd20307d630f44c2c236e2e0c61a9e57be33) Thanks [@bookernath](https://github.com/bookernath)! - Add dynamic favicon from API on a static route\n\n- [#1357](https://github.com/bigcommerce/catalyst/pull/1357) [`3176491`](https://github.com/bigcommerce/catalyst/commit/317649109861e75fa46794e0cbf67dca500947a6) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add /account/settings/change-password route for change password form.\n\n### Patch Changes\n\n- [#1361](https://github.com/bigcommerce/catalyst/pull/1361) [`dd10d06`](https://github.com/bigcommerce/catalyst/commit/dd10d064156e8fc0376f0cce6f698dc8b834f95e) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Enforce use of next-intl's wrapper navigation APIs.\n\n- [#1360](https://github.com/bigcommerce/catalyst/pull/1360) [`00f72dd`](https://github.com/bigcommerce/catalyst/commit/00f72ddc7e3c2cff780430e074341ee72bc0c893) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Change LocalePrefix mode to `as-needed`, since there's an issue that is causing caching problems when using `never`.\n\n  More info about LocalePrefixes: https://next-intl-docs.vercel.app/docs/routing#shared-configuration\n  Open issue: https://github.com/amannn/next-intl/issues/786\n\n- [#1338](https://github.com/bigcommerce/catalyst/pull/1338) [`d50613a`](https://github.com/bigcommerce/catalyst/commit/d50613a669696f34a695bc35b9d40099eeea0660) Thanks [@bc-yevhenii-buliuk](https://github.com/bc-yevhenii-buliuk)! - improve redirect behavior after change password on account page\n\n- [#1358](https://github.com/bigcommerce/catalyst/pull/1358) [`48db1b8`](https://github.com/bigcommerce/catalyst/commit/48db1b80a8aeb8e63fb920bf4374413c0d6c67c5) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update da and fr translations to use correct string templates.\n\n- [#1368](https://github.com/bigcommerce/catalyst/pull/1368) [`d032e65`](https://github.com/bigcommerce/catalyst/commit/d032e659ba0ea1b45dc47e3afcb9094ca4f38afc) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Localize metadata titles.\n\n- [#1369](https://github.com/bigcommerce/catalyst/pull/1369) [`c9a5ab5`](https://github.com/bigcommerce/catalyst/commit/c9a5ab58be4dad966dc8d406ade8433f0f2b5d25) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Pass in default channel to favicon query, since `getLocale` can't be used in routes.\n\n## 0.14.2\n\n### Patch Changes\n\n- Updated dependencies [[`88663d1`](https://github.com/bigcommerce/catalyst/commit/88663d165691380b35f83726f0589896bdc73bf2)]:\n  - @bigcommerce/catalyst-client@0.8.0\n\n## 0.14.1\n\n### Patch Changes\n\n- [#1257](https://github.com/bigcommerce/catalyst/pull/1257) [`d656e79`](https://github.com/bigcommerce/catalyst/commit/d656e7981c7516be560b1944e4351916572b7a05) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - add numbers-only field & utils for account form fields\n\n- [#1277](https://github.com/bigcommerce/catalyst/pull/1277) [`8e6253d`](https://github.com/bigcommerce/catalyst/commit/8e6253dbd3048b8318ce502192bc9f07314b3641) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update Slideshow prop to use altText for image. Rename Hero wrapper component to Slideshow.\n\n- [#1302](https://github.com/bigcommerce/catalyst/pull/1302) [`a620a19`](https://github.com/bigcommerce/catalyst/commit/a620a191d3d30d50d0fa79fc36ad32ee28db8728) Thanks [@deini](https://github.com/deini)! - fix: decode webpage id to fix 404 on some Webpages\n\n- [#1257](https://github.com/bigcommerce/catalyst/pull/1257) [`d656e79`](https://github.com/bigcommerce/catalyst/commit/d656e7981c7516be560b1944e4351916572b7a05) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - add checkboxes field for account & addresses forms\n\n- [#1346](https://github.com/bigcommerce/catalyst/pull/1346) [`33e133d`](https://github.com/bigcommerce/catalyst/commit/33e133df74b263aeabd23f72f6b8ccfdc22c1a36) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - fix placeholder positioning for picklist custom form field\n\n- [#1316](https://github.com/bigcommerce/catalyst/pull/1316) [`4aea109`](https://github.com/bigcommerce/catalyst/commit/4aea109593e7ac060552dca18198e39c0b070e55) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Normalizes translations across all pages, updates the next-intl configuration, and simplifies translation handling in the project.\n\n- [#1257](https://github.com/bigcommerce/catalyst/pull/1257) [`d656e79`](https://github.com/bigcommerce/catalyst/commit/d656e7981c7516be560b1944e4351916572b7a05) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - add dates field for account & address forms\n\n- [#1141](https://github.com/bigcommerce/catalyst/pull/1141) [`9f3c949`](https://github.com/bigcommerce/catalyst/commit/9f3c9492b2d4edcd404cffc92dfcfec6a0afc395) Thanks [@bc-yevhenii-buliuk](https://github.com/bc-yevhenii-buliuk)! - improve redirect behavior after creating new address\n\n- [#1305](https://github.com/bigcommerce/catalyst/pull/1305) [`b11ba3d`](https://github.com/bigcommerce/catalyst/commit/b11ba3d63547d2772a649078274b5b71702c402a) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Refactors tabs in `/account` to each be their own page. This also removes unused links in account home page (and tests) until we have that functionality available.\n\n  Previous structure:\n\n  ```\n  /account\n    [tab]\n      page.tsx\n  ```\n\n  New structure:\n\n  ```\n  /account\n    (tabs)\n      addresses\n        page.tsx\n      settings\n        page.tsx\n      ...etc\n  ```\n\n- [#1257](https://github.com/bigcommerce/catalyst/pull/1257) [`d656e79`](https://github.com/bigcommerce/catalyst/commit/d656e7981c7516be560b1944e4351916572b7a05) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - add multipleChoices field(radio-buttons, picklist) for account & address forms\n\n- [#1334](https://github.com/bigcommerce/catalyst/pull/1334) [`00f43f0`](https://github.com/bigcommerce/catalyst/commit/00f43f045b4ac2f71aef36a41a1ef643bfc66247) Thanks [@deini](https://github.com/deini)! - Fixes a server crash when user switches language settings\n\n- [#1333](https://github.com/bigcommerce/catalyst/pull/1333) [`e2c0153`](https://github.com/bigcommerce/catalyst/commit/e2c01535e0efbd474b1236d0a7e63ad2263475db) Thanks [@deini](https://github.com/deini)! - Splits i18n into request.ts and routing.ts This helps reduce our middleware bundle as we no longer do a dynamic import on our middleware entrypoint.\n\n- [#1342](https://github.com/bigcommerce/catalyst/pull/1342) [`f7bb1e2`](https://github.com/bigcommerce/catalyst/commit/f7bb1e2654912c2b25851f3a86f77fa6f1014817) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update localeSwitcher to use a link instead of a form.\n\n- [#1326](https://github.com/bigcommerce/catalyst/pull/1326) [`255c648`](https://github.com/bigcommerce/catalyst/commit/255c6482a48d735a28c632746b4a652d8ba1dfed) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Ensure recaptcha is bypassed for functional tests.\n\n- [#1278](https://github.com/bigcommerce/catalyst/pull/1278) [`f8553c6`](https://github.com/bigcommerce/catalyst/commit/f8553c6c9fb35ab7a143fabd60719c8156269448) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Fix wrapping author text in BlogPostCard.\n\n- [#1322](https://github.com/bigcommerce/catalyst/pull/1322) [`77ecb4b`](https://github.com/bigcommerce/catalyst/commit/77ecb4bb4f527e079788b0f9dff2468e92d0bc1a) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Split auth forms to four different pages:\n  - /login\n  - /register\n  - /reset\n  - /change-password\n\n  Additionally, moved shared form field components to `/components/form-fields/` and updated translations.\n\n- [#1317](https://github.com/bigcommerce/catalyst/pull/1317) [`7802361`](https://github.com/bigcommerce/catalyst/commit/780236150bab6e2c43e73a230ed69113e3e1bae3) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Rename NEXT_PUBLIC_DEFAULT_REVALIDATE_TARGET to DEFAULT_REVALIDATE_TARGET since we don't need this exposed to the client.\n\n- [#1296](https://github.com/bigcommerce/catalyst/pull/1296) [`fcd44bb`](https://github.com/bigcommerce/catalyst/commit/fcd44bb90bf2d82b098600f4809ae3f37d5c01dc) Thanks [@bookernath](https://github.com/bookernath)! - Add link header to preconnect to CDN\n\n- [#1088](https://github.com/bigcommerce/catalyst/pull/1088) [`644361e`](https://github.com/bigcommerce/catalyst/commit/644361e8a75185e05964a782569c4b17dc5a9f98) Thanks [@bc-yevhenii-buliuk](https://github.com/bc-yevhenii-buliuk)! - improve redirect behavior after creating account\n\n- [#1329](https://github.com/bigcommerce/catalyst/pull/1329) [`ad601e1`](https://github.com/bigcommerce/catalyst/commit/ad601e1be0f2e2b0e458363af13d3b7881f8cf24) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - update multiline form-field to respect required settings\n\n- [#1257](https://github.com/bigcommerce/catalyst/pull/1257) [`d656e79`](https://github.com/bigcommerce/catalyst/commit/d656e7981c7516be560b1944e4351916572b7a05) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - add multilinetext field for account & address forms\n\n- [#1300](https://github.com/bigcommerce/catalyst/pull/1300) [`b32198b`](https://github.com/bigcommerce/catalyst/commit/b32198b78dcd18b05ba0c0f57269cbd62023a654) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Refactor queries, fragments, and mutations in an effort to set a pattern on where these functions need to be defined.\n\n  Shared queries and mutations will remain in /client for now.\n\n- [#1349](https://github.com/bigcommerce/catalyst/pull/1349) [`dd9cf6f`](https://github.com/bigcommerce/catalyst/commit/dd9cf6f61efb6b17322e1485225003d9799cbf9a) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove updateCustomer and getCustomerAddresses queries since they are defined now where they are used.\n\n- [#1313](https://github.com/bigcommerce/catalyst/pull/1313) [`6531bb2`](https://github.com/bigcommerce/catalyst/commit/6531bb2ee9b6a6125cd4f9f0e624e023897387be) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove \"Quick add\" button in PLP for products that have options. Will now just show a button that links to the product.\n\n## 0.14.0\n\n### Minor Changes\n\n- [#1261](https://github.com/bigcommerce/catalyst/pull/1261) [`f715067`](https://github.com/bigcommerce/catalyst/commit/f715067aa36616b3818c9424c57fa08e28936cde) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Remove the need of fetching shipping countries by using the GraphQL data.\n\n- [#1261](https://github.com/bigcommerce/catalyst/pull/1261) [`f715067`](https://github.com/bigcommerce/catalyst/commit/f715067aa36616b3818c9424c57fa08e28936cde) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Fetch shipping zones if access token exists, otherwise regress back to using the geography node on graphql for shipping information. This is part of an effort to remove the need of the `BIGCOMMERCE_ACCESS_TOKEN`.\n\n### Patch Changes\n\n- [#1256](https://github.com/bigcommerce/catalyst/pull/1256) [`686abe9`](https://github.com/bigcommerce/catalyst/commit/686abe9eae18cd2241e7ac17e17f7139d6b87bd6) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Consistency improvements to prop APIs for UI components.\n\n- Updated dependencies [[`f715067`](https://github.com/bigcommerce/catalyst/commit/f715067aa36616b3818c9424c57fa08e28936cde)]:\n  - @bigcommerce/catalyst-client@0.7.0\n\n## 0.13.0\n\n### Minor Changes\n\n- [#1166](https://github.com/bigcommerce/catalyst/pull/1166) [`0661e53`](https://github.com/bigcommerce/catalyst/commit/0661e53e66a12713a5ad23292a0a0eb25cddd9dc) Thanks [@bookernath](https://github.com/bookernath)! - Use default SEO settings from store for pages without SEO information specified, normalize SEO implementation across pages\n\n- [#1194](https://github.com/bigcommerce/catalyst/pull/1194) [`b455b05`](https://github.com/bigcommerce/catalyst/commit/b455b05a6121b005bd5147a25c964b9554b1b350) Thanks [@BC-krasnoshapka](https://github.com/BC-krasnoshapka)! - Add basic support for Google Analytics via [Big Open Data Layer](https://developer.bigcommerce.com/docs/integrations/hosted-analytics). BODL and GA4 integration is encapsulated in `bodl` library which hides current complexity and limitations that will be improved in future. It can be extended with more events and integrations with other analytics providers later. Data transformation from Catalyst data models to BODL and firing events is done in client components, as only frontend events are supported by BODL for now.\n\n  List of currently supported events:\n  - View product category\n  - View product page\n  - Add product to cart\n  - View cart\n  - Remove product from cart\n\n  In order to configure you need to specify `NEXT_PUBLIC_GOOGLE_ANALYTICS_ID` environment variable which is essentially your GA4 ID.\n\n### Patch Changes\n\n- [#1225](https://github.com/bigcommerce/catalyst/pull/1225) [`127f3b6`](https://github.com/bigcommerce/catalyst/commit/127f3b6000f0345a1e277d038025edadeaa09d71) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Change prop `items` to `links` in Header.\n\n- [#1232](https://github.com/bigcommerce/catalyst/pull/1232) [`b7d4986`](https://github.com/bigcommerce/catalyst/commit/b7d4986b390932be770de9adcf12112df4bb58e1) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove `Popover` component, utilize radix primitives instead.\n\n- [#1196](https://github.com/bigcommerce/catalyst/pull/1196) [`b793661`](https://github.com/bigcommerce/catalyst/commit/b793661ab145a2acec5b2fa5aa0c5f1d6865cad9) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add locale picker in header.\n\n- [#1231](https://github.com/bigcommerce/catalyst/pull/1231) [`befb122`](https://github.com/bigcommerce/catalyst/commit/befb122d033ba56b87cb04f31e0f34fe4386d285) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add `Dropdown` component.\n\n- [#1209](https://github.com/bigcommerce/catalyst/pull/1209) [`ef2f3cb`](https://github.com/bigcommerce/catalyst/commit/ef2f3cbddb872a5a2ad1c188f40cd5671eaf77b7) Thanks [@bookernath](https://github.com/bookernath)! - Limit number of chunks in webpack, customizable via env\n\n- [#1239](https://github.com/bigcommerce/catalyst/pull/1239) [`9a37c6a`](https://github.com/bigcommerce/catalyst/commit/9a37c6a25ccaed7b7373cdb3637706c6826a380a) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add `Search` component.\n\n- [#1199](https://github.com/bigcommerce/catalyst/pull/1199) [`e8bf185`](https://github.com/bigcommerce/catalyst/commit/e8bf185f34061be96cfe6a118431c3a4c62df7a2) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add more context when no result is found in search page.\n\n- [#1236](https://github.com/bigcommerce/catalyst/pull/1236) [`7d9e865`](https://github.com/bigcommerce/catalyst/commit/7d9e86568c5422cb74ef512ba851ee709e9d59f0) Thanks [@bookernath](https://github.com/bookernath)! - Exclude node_modules from tailwind config to improve build time\n\n- [#1214](https://github.com/bigcommerce/catalyst/pull/1214) [`4e890ff`](https://github.com/bigcommerce/catalyst/commit/4e890ffe203605c4a77be1acdf33622ff871405d) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Change prop `value` to `title` in Accordions.\n\n- [#1197](https://github.com/bigcommerce/catalyst/pull/1197) [`c831677`](https://github.com/bigcommerce/catalyst/commit/c831677cb873e67a898ffd1efeda0c518c6ab97d) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Set key before spreading prop in some form components.\n\n- [#1188](https://github.com/bigcommerce/catalyst/pull/1188) [`5c77f41`](https://github.com/bigcommerce/catalyst/commit/5c77f41eb6ced4677d85fef1adf898fe697a0452) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Rename brand prop to subtitle in Product Card.\n\n- [#1234](https://github.com/bigcommerce/catalyst/pull/1234) [`052e94a`](https://github.com/bigcommerce/catalyst/commit/052e94abd76b52700badde189ec36aee6cc383b1) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add `Breadcrumbs` component.\n\n- [#1224](https://github.com/bigcommerce/catalyst/pull/1224) [`5f934f9`](https://github.com/bigcommerce/catalyst/commit/5f934f91b790b9dd9001f133bdd75ce06951465c) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Change prop `thumbnail` to `image` in BlogPostCard.\n\n- [#1206](https://github.com/bigcommerce/catalyst/pull/1206) [`d1cf327`](https://github.com/bigcommerce/catalyst/commit/d1cf327d4c2c28f01940391a74cc4750d79b03b7) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add `Slide` component to be used in `Slideshow`.\n\n- [#1198](https://github.com/bigcommerce/catalyst/pull/1198) [`22dc862`](https://github.com/bigcommerce/catalyst/commit/22dc86260daaaeec20276a84b89c152a3ae246a3) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add missing accessibility components to Sheet/Mobile Nav.\n\n- [#1226](https://github.com/bigcommerce/catalyst/pull/1226) [`d6d1224`](https://github.com/bigcommerce/catalyst/commit/d6d1224521d4304bbdb515763aaee402b1a97c94) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Rename `value` to `rating` for Rating component, remove unused props.\n\n- [#1190](https://github.com/bigcommerce/catalyst/pull/1190) [`d01b4e0`](https://github.com/bigcommerce/catalyst/commit/d01b4e0560b1b8b2b3df9ed348231a2fc375f785) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove title prop from Tabs, remove Tabs from /account since it's not needed.\n\n- [#1204](https://github.com/bigcommerce/catalyst/pull/1204) [`bde94ba`](https://github.com/bigcommerce/catalyst/commit/bde94bab5299b933047c58cd3c64a73022c039bc) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add missing accisibility components to Quick Search.\n\n- [#1200](https://github.com/bigcommerce/catalyst/pull/1200) [`51704d9`](https://github.com/bigcommerce/catalyst/commit/51704d9b9a7158c625c84f79e2ba95f98c6dc673) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Use the `geography` node to retrieve a list of countries. This removes one less dependency on the access token.\n\n- [#1235](https://github.com/bigcommerce/catalyst/pull/1235) [`53ccd31`](https://github.com/bigcommerce/catalyst/commit/53ccd31f51e5b6d8f311a340d0bf70b7edb632aa) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add `Pagination` component.\n\n- [#1211](https://github.com/bigcommerce/catalyst/pull/1211) [`ec81a3a`](https://github.com/bigcommerce/catalyst/commit/ec81a3a69182d015395d6dc7bfff1e9af2adb6f9) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Update price prop in ProductCard to accept an object instead of a ReactNode.\n\n- [#1208](https://github.com/bigcommerce/catalyst/pull/1208) [`315ed15`](https://github.com/bigcommerce/catalyst/commit/315ed154e1ccfe316dc4d1037e674b79c3bad308) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Move CompareDrawer to ui components.\n\n- Updated dependencies [[`51704d9`](https://github.com/bigcommerce/catalyst/commit/51704d9b9a7158c625c84f79e2ba95f98c6dc673)]:\n  - @bigcommerce/catalyst-client@0.6.0\n\n## 0.12.0\n\n### Minor Changes\n\n- [#1178](https://github.com/bigcommerce/catalyst/pull/1178) [`f592d9f`](https://github.com/bigcommerce/catalyst/commit/f592d9fe0b71ddd7ceb5e1326ea0280f7b90c3c9) Thanks [@jorgemoya](https://github.com/jorgemoya)! - This refactor changes the structure of our UI components by replacing composability with a prop-based configuration. This change simplifies the use of our components, eliminating the need to build them individually from a composable approach. Additionally, it provides a single location for all class customizations, improving the experience when fully customizing the component. We believe this approach will make it easier to use components correctly and safeguard against incorrect usage. Ultimately, by adopting a prop-based configuration, we aim to achieve full replaceability and simplify theming for our components.\n\n  Before refactor:\n\n  ```\n  <Accordions>\n      <AccordionsItem>\n          <AccordionsTrigger>\n              Title 1\n          </AccordionsTrigger>\n          <AccordionsContent>\n              Item Content 1\n          </AccordionsContent>\n      </AccordionsItem>\n      <AccordionsItem>\n          <AccordionsTrigger>\n              Title 2\n          </AccordionsTrigger>\n          <AccordionsContent>\n              Item Content 2\n          </AccordionsContent>\n      </AccordionsItem>\n  </Accordions>\n  ```\n\n  After refactor:\n\n  ```\n  <Accordions accordions={[\n      {value: 'Title 1', content: 'Item Content 1'},\n      {value: 'Title 2', content: 'Item Content 2'}\n  ]}>\n  ```\n\n  Before refactor:\n\n  ```\n  <Select\n      onValueChange={onSort}\n      value={value}\n  >\n      <SelectContent>\n          <SelectItem value=\"featured\">Featured</SelectItem>\n          <SelectItem value=\"newest\">Newest</SelectItem>\n          <SelectItem value=\"best_selling\">Best selling</SelectItem>\n          <SelectItem value=\"a_to_z\">A to Z</SelectItem>\n          <SelectItem value=\"z_to_a\">Z to A</SelectItem>\n          <SelectItem value=\"best_reviewed\">By reviews</SelectItem>\n          <SelectItem value=\"lowest_price\">Price ascending</SelectItem>\n          <SelectItem value=\"highest_price\">Price descending</SelectItem>\n          <SelectItem value=\"relevance\">Relevance</SelectItem>\n      </SelectContent>\n  </Select>\n  ```\n\n  After refactor:\n\n  ```\n  <Select\n      onValueChange={onSort}\n      options={[\n          { value: 'featured', label: 'Featured' },\n          { value: 'newest', label: 'Newest' },\n          { value: 'best_selling', label: 'Best selling' },\n          { value: 'a_to_z', label: 'A to Z' },\n          { value: 'z_to_a', label: 'Z to A' },\n          { value: 'best_reviewed', label: 'By reviews'},\n          { value: 'lowest_price', label: 'Price ascending' },\n          { value: 'highest_price', label: 'Price descending' },\n          { value: 'relevance', label: 'Relevance' },\n      ]}\n      value={value}\n  />\n  ```\n\n## 0.11.0\n\n### Minor Changes\n\n- [#1156](https://github.com/bigcommerce/catalyst/pull/1156) [`7d91478`](https://github.com/bigcommerce/catalyst/commit/7d9147894deb17ca17048ac95b86e5a8a0def515) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Adds customer information onto the session for consumption in both server and client components\n\n### Patch Changes\n\n- [#1183](https://github.com/bigcommerce/catalyst/pull/1183) [`4e7ed57`](https://github.com/bigcommerce/catalyst/commit/4e7ed57979a82b04cc1fcb47025356c4b746db82) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Uses `next/navigation` for logging in as a customer instead of the built-in `redirectTo` option. That option was not following the `trailingSlash` config set in `next.config.js` which caused test failures.\n\n- [#1179](https://github.com/bigcommerce/catalyst/pull/1179) [`ae8d985`](https://github.com/bigcommerce/catalyst/commit/ae8d985a89c229f945a596d7a905828dfcbe490e) Thanks [@deini](https://github.com/deini)! - bump next to 14.2.5\n\n- Updated dependencies []:\n  - @bigcommerce/catalyst-client@0.5.0\n\n## 0.10.0\n\n### Minor Changes\n\n- [#1057](https://github.com/bigcommerce/catalyst/pull/1057) [`22dd481`](https://github.com/bigcommerce/catalyst/commit/22dd4818edea8ea9f7efc721a598cd978684ede5) Thanks [@bookernath](https://github.com/bookernath)! - Added /sitemap.xml as a proxy to hosted BigCommerce sitemap\n\n### Patch Changes\n\n- [#1098](https://github.com/bigcommerce/catalyst/pull/1098) [`405e791`](https://github.com/bigcommerce/catalyst/commit/405e791af8e7ecc1422f2ce18cb216a8c04cc73b) Thanks [@bookernath](https://github.com/bookernath)! - Move Sitemap Index fetching into the client & normalize user agents\n\n- [#1086](https://github.com/bigcommerce/catalyst/pull/1086) [`e0926ee`](https://github.com/bigcommerce/catalyst/commit/e0926ee21664503f208dafcc8e5939c363801ee1) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - add minor changes to address form layout\n\n- [#1055](https://github.com/bigcommerce/catalyst/pull/1055) [`52214a3`](https://github.com/bigcommerce/catalyst/commit/52214a376bba1fdaa584de31c36f7d6cdc078624) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Make client.fetch channel aware per locale.\n\n- [#1071](https://github.com/bigcommerce/catalyst/pull/1071) [`5d0975b`](https://github.com/bigcommerce/catalyst/commit/5d0975be8accd733e2ed909dba85f04d6d1042f5) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Use customerId in product API to get correct product information.\n\n- [#1077](https://github.com/bigcommerce/catalyst/pull/1077) [`e86f46f`](https://github.com/bigcommerce/catalyst/commit/e86f46fea3bd5630311d3afccb4b2d70aa68f6fe) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Uses the deployment URL for the robots.txt sitemap field rather than another environment variable.\n\n- [#1075](https://github.com/bigcommerce/catalyst/pull/1075) [`4bf7d16`](https://github.com/bigcommerce/catalyst/commit/4bf7d1680df4a7dcc2adcdf24e4faf9e4e470726) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Append channel to kv keys.\n\n- [#1034](https://github.com/bigcommerce/catalyst/pull/1034) [`e648a62`](https://github.com/bigcommerce/catalyst/commit/e648a62bed956a0c2ea43b9bc3ca68e009b57cfc) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add store selector page.\n\n- [#1032](https://github.com/bigcommerce/catalyst/pull/1032) [`982b19c`](https://github.com/bigcommerce/catalyst/commit/982b19c5e80d4b427ec207cc0d72ef5014e4bee8) Thanks [@deini](https://github.com/deini)! - prefetch product option data on hover\n\n- [#1095](https://github.com/bigcommerce/catalyst/pull/1095) [`5df38cf`](https://github.com/bigcommerce/catalyst/commit/5df38cf3b521e5b2077026e045f85e8ddbaee8a7) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Fixes a missing GraphQL field for the updateCustomer mutation.\n\n- [#1056](https://github.com/bigcommerce/catalyst/pull/1056) [`ad7bda7`](https://github.com/bigcommerce/catalyst/commit/ad7bda7387f25b04dc53b4df06ca8929791bc5d6) Thanks [@bc-yevhenii-buliuk](https://github.com/bc-yevhenii-buliuk)! - make selected account tab visible on mobile devices\n\n- [#1087](https://github.com/bigcommerce/catalyst/pull/1087) [`b21a139`](https://github.com/bigcommerce/catalyst/commit/b21a139c447eeb132a2cabef3951f0cb7f779341) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - change pagination layout and minor changes to address book\n\n- Updated dependencies [[`405e791`](https://github.com/bigcommerce/catalyst/commit/405e791af8e7ecc1422f2ce18cb216a8c04cc73b), [`8766305`](https://github.com/bigcommerce/catalyst/commit/8766305b65ca10422e7921b2fd15796e0a09d27a), [`52214a3`](https://github.com/bigcommerce/catalyst/commit/52214a376bba1fdaa584de31c36f7d6cdc078624)]:\n  - @bigcommerce/catalyst-client@0.5.0\n\n## 0.9.1\n\n### Patch Changes\n\n- [#937](https://github.com/bigcommerce/catalyst/pull/937) [`3606639`](https://github.com/bigcommerce/catalyst/commit/3606639e294465cd10aab217c8c74be7cd7a8754) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Show correct status and messaging for the Add to Cart button.\n\n- [#979](https://github.com/bigcommerce/catalyst/pull/979) [`6a6c193`](https://github.com/bigcommerce/catalyst/commit/6a6c1938a05a639212afc41241b4e1cb4cf6cd88) Thanks [@bc-yevhenii-buliuk](https://github.com/bc-yevhenii-buliuk)! - fix redirection to the Login page after password change\n\n- [#972](https://github.com/bigcommerce/catalyst/pull/972) [`3c34e27`](https://github.com/bigcommerce/catalyst/commit/3c34e276d7b735394aa3c9d6205f18b5407ca7a4) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Show correct color of remove button when in loading state.\n\n- [#982](https://github.com/bigcommerce/catalyst/pull/982) [`b8ea900`](https://github.com/bigcommerce/catalyst/commit/b8ea9006a621a9d5f549e4fa1c6bbccb72c3b1ec) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Introduces more consistent naming convention for files related to GraphQL, changes opinions around when it is appropriate to track GraphQL files in version control, fixes an issue where the `generate.cjs` script was swallowing helpful error messaging\n\n- [#977](https://github.com/bigcommerce/catalyst/pull/977) [`bf4739d`](https://github.com/bigcommerce/catalyst/commit/bf4739d0977deb69f3bc1cf0e70f4c96b60c6d89) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add upstash kv adapter.\n\n- [#974](https://github.com/bigcommerce/catalyst/pull/974) [`970651c`](https://github.com/bigcommerce/catalyst/commit/970651c159553983f665a8951419cdd3d977fc02) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add loading state to remove coupon code button.\n\n## 0.9.0\n\n### Minor Changes\n\n- [#794](https://github.com/bigcommerce/catalyst/pull/794) [`956d738`](https://github.com/bigcommerce/catalyst/commit/956d7389bce81e8af8e8cdbe0bae78e3b3f20423) Thanks [@yurytut1993](https://github.com/yurytut1993)! - add update customer form\n\n### Patch Changes\n\n- [#942](https://github.com/bigcommerce/catalyst/pull/942) [`c7c65e0`](https://github.com/bigcommerce/catalyst/commit/c7c65e002d6f473292713c4c5ffa4ab2690cc6f8) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Make select scrollable with popover functionality.\n\n- [#957](https://github.com/bigcommerce/catalyst/pull/957) [`0a3b519`](https://github.com/bigcommerce/catalyst/commit/0a3b5191d1eba6ea70eeb91ef39638d5a6fbf1ca) Thanks [@deini](https://github.com/deini)! - fix custom 404 page not being used\n\n- [#941](https://github.com/bigcommerce/catalyst/pull/941) [`19a3d14`](https://github.com/bigcommerce/catalyst/commit/19a3d147b6b12b38d974649c147a709c0d47557a) Thanks [@bc-yevhenii-buliuk](https://github.com/bc-yevhenii-buliuk)! - update icons on the account page\n\n- [#811](https://github.com/bigcommerce/catalyst/pull/811) [`6661e3e`](https://github.com/bigcommerce/catalyst/commit/6661e3e56e1cc703506f5ee509a7377fb19174f0) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - Add new address for customer\n\n## 0.8.0\n\n### Minor Changes\n\n- [#704](https://github.com/bigcommerce/catalyst/pull/704) [`6e93873`](https://github.com/bigcommerce/catalyst/commit/6e9387326cebf139bb7fb2459f5b9f29c81c876f) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - add change password for logged-in customer\n\n- [#730](https://github.com/bigcommerce/catalyst/pull/730) [`15e4b82`](https://github.com/bigcommerce/catalyst/commit/15e4b82845979e0ea92aae531055552636d433fb) Thanks [@yurytut1993](https://github.com/yurytut1993)! - create register customer page\n\n### Patch Changes\n\n- [#922](https://github.com/bigcommerce/catalyst/pull/922) [`321f67f`](https://github.com/bigcommerce/catalyst/commit/321f67f0f6576f2f6169e3d804705c7a82a9fb1a) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Fix incorrect sale price showing when no sale was active in Cart\n\n- [#896](https://github.com/bigcommerce/catalyst/pull/896) [`b13fecf`](https://github.com/bigcommerce/catalyst/commit/b13fecfa145ceb489553511221f76533d65d6bf9) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Modify Cart page layout to fix mobile rendering issues.\n\n- [#787](https://github.com/bigcommerce/catalyst/pull/787) [`6198648`](https://github.com/bigcommerce/catalyst/commit/6198648c563be61ac6a5a413a005ed63a7d43a58) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - Add delete address functionality for account\n\n- [#909](https://github.com/bigcommerce/catalyst/pull/909) [`bf0e326`](https://github.com/bigcommerce/catalyst/commit/bf0e326e446d3014ae9a3c352173ee1e547f3de8) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Show original price of Cart item when on sale\n\n- [#912](https://github.com/bigcommerce/catalyst/pull/912) [`5ec3d76`](https://github.com/bigcommerce/catalyst/commit/5ec3d76c3af5847604dedfa9c6d1c870246808ef) Thanks [@deini](https://github.com/deini)! - fetch checkout redirect url when user clicks proceed to checkout button\n\n- [#916](https://github.com/bigcommerce/catalyst/pull/916) [`ff231c9`](https://github.com/bigcommerce/catalyst/commit/ff231c9c5d8ae5470fea61de7d42494b68b9f469) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add Button with loading state.\n\n- [#918](https://github.com/bigcommerce/catalyst/pull/918) [`f16936a`](https://github.com/bigcommerce/catalyst/commit/f16936a057de212baafad9e62f556d0d4bb2bfae) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Fix issue with account dropdown in header.\n\n- [#919](https://github.com/bigcommerce/catalyst/pull/919) [`cde181e`](https://github.com/bigcommerce/catalyst/commit/cde181e4a3a768401bda6471562a8128dff3dcb2) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Fix broken Slot functionality in Button\n\n- [#910](https://github.com/bigcommerce/catalyst/pull/910) [`d0352c0`](https://github.com/bigcommerce/catalyst/commit/d0352c08b43e76b4cd838cb7916f9993228e3fa0) Thanks [@deini](https://github.com/deini)! - removes fetch cart redirect from client and fetch it with gql\n\n- [#880](https://github.com/bigcommerce/catalyst/pull/880) [`af61999`](https://github.com/bigcommerce/catalyst/commit/af619997002f33b2a9a5276467ac632218cfc2f8) Thanks [@deini](https://github.com/deini)! - Category pages now use the `categoryEntityId` filter\n\n- Updated dependencies [[`d0352c0`](https://github.com/bigcommerce/catalyst/commit/d0352c08b43e76b4cd838cb7916f9993228e3fa0)]:\n  - @bigcommerce/catalyst-client@0.4.0\n\n## 0.7.0\n\n### Minor Changes\n\n- [#748](https://github.com/bigcommerce/catalyst/pull/748) [`dc03f50`](https://github.com/bigcommerce/catalyst/commit/dc03f50bb1734b26bd15ecf9c1f7fb6e34d3e86c) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - Add customer addresses tab content\n\n- [#760](https://github.com/bigcommerce/catalyst/pull/760) [`d3cb5bd`](https://github.com/bigcommerce/catalyst/commit/d3cb5bd51966aa1bf38453aba2a125f517869931) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - Add dialog component\n\n### Patch Changes\n\n- [#786](https://github.com/bigcommerce/catalyst/pull/786) [`8e6328f`](https://github.com/bigcommerce/catalyst/commit/8e6328fb577e91eede49a92eafa113c5778520de) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Apply the edge runtime to missing routes.\n\n- [#816](https://github.com/bigcommerce/catalyst/pull/816) [`7115843`](https://github.com/bigcommerce/catalyst/commit/711584393f829873ad8d3d48495f1aafa777e46d) Thanks [@avattipalli](https://github.com/avattipalli)! - Move functional tests to apps/core\n\n- [#776](https://github.com/bigcommerce/catalyst/pull/776) [`656693e`](https://github.com/bigcommerce/catalyst/commit/656693ed1ac30a162025b58763fa7beb4dfaad18) Thanks [@yurytut1993](https://github.com/yurytut1993)! - add update customer mutation\n\n- [#845](https://github.com/bigcommerce/catalyst/pull/845) [`dfd5b25`](https://github.com/bigcommerce/catalyst/commit/dfd5b25659cb90e909e73764f246f19322f60a4c) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove additional references to @bigcommerce/components.\n\n- [#808](https://github.com/bigcommerce/catalyst/pull/808) [`c0bca5d`](https://github.com/bigcommerce/catalyst/commit/c0bca5d12257218908dcca54b31d32bf84d087fb) Thanks [@jorgemoya](https://github.com/jorgemoya)! - use next-intl formatter to properly localize dates & prices\n\n- [#854](https://github.com/bigcommerce/catalyst/pull/854) [`0758464`](https://github.com/bigcommerce/catalyst/commit/0758464e4c43ab33e470bb91223249b01e36e780) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Respect isVisibleInNavigation for blog pages\n\n- [#779](https://github.com/bigcommerce/catalyst/pull/779) [`fe34b3e`](https://github.com/bigcommerce/catalyst/commit/fe34b3ed79992f73084214b369b7750141a17c39) Thanks [@deini](https://github.com/deini)! - use LRU cache for DevKvAdapter\n\n- [#789](https://github.com/bigcommerce/catalyst/pull/789) [`86403a6`](https://github.com/bigcommerce/catalyst/commit/86403a6fc66f52f93ace611631614c2844af5a87) Thanks [@deini](https://github.com/deini)! - best-effort in memory cache for vercel kv adapter\n\n- [#815](https://github.com/bigcommerce/catalyst/pull/815) [`984c30c`](https://github.com/bigcommerce/catalyst/commit/984c30ca51601fb8f1c0f6c83bce40c3650f9b23) Thanks [@deini](https://github.com/deini)! - pin nextjs version\n\n- [#814](https://github.com/bigcommerce/catalyst/pull/814) [`c0b5df4`](https://github.com/bigcommerce/catalyst/commit/c0b5df458f049d73b9cfb17426f132f827e4574f) Thanks [@jorgemoya](https://github.com/jorgemoya)! - standardize mutations by returning drilled response\n\n- [#759](https://github.com/bigcommerce/catalyst/pull/759) [`3602d91`](https://github.com/bigcommerce/catalyst/commit/3602d91144513ad0c14b646f2cfc68791d3c3198) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - Add delete customer address mutation\n\n- [#767](https://github.com/bigcommerce/catalyst/pull/767) [`c740cdd`](https://github.com/bigcommerce/catalyst/commit/c740cdd1b561b7abaab7390a8dfcab4d65c89d73) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Move /packages/components into core, update related configurations.\n\n- [#798](https://github.com/bigcommerce/catalyst/pull/798) [`56f3c48`](https://github.com/bigcommerce/catalyst/commit/56f3c4824dd0b31212c15b124cb29be79548fbf2) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Update `tailwindFunctions` to use the correct className utility function `cn`.\n\n- [#769](https://github.com/bigcommerce/catalyst/pull/769) [`1fa1c38`](https://github.com/bigcommerce/catalyst/commit/1fa1c38382871b78c3f51cbcf049532e1b05bbbc) Thanks [@avattipalli](https://github.com/avattipalli)! - add accessible attr for select component\n\n- [#810](https://github.com/bigcommerce/catalyst/pull/810) [`168cdda`](https://github.com/bigcommerce/catalyst/commit/168cddae51638a24a0fb53a3a2f5a5e03a7a4b38) Thanks [@deini](https://github.com/deini)! - split contact us and normal websites into individual pages\n\n- [#777](https://github.com/bigcommerce/catalyst/pull/777) [`fe5c221`](https://github.com/bigcommerce/catalyst/commit/fe5c221aa6e4a4049e89f69e177d722ee94b6f62) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - Add customer address mutation\n\n- [#831](https://github.com/bigcommerce/catalyst/pull/831) [`8349bbf`](https://github.com/bigcommerce/catalyst/commit/8349bbf928dee722fadb5c2119b41756bffaa317) Thanks [@jorgemoya](https://github.com/jorgemoya)! - chore: standardize actions\n\n- [#783](https://github.com/bigcommerce/catalyst/pull/783) [`301b775`](https://github.com/bigcommerce/catalyst/commit/301b775ef967b72ab9d3930eb7ec7488876b48b4) Thanks [@jorgemoya](https://github.com/jorgemoya)! - add loading state on item quantity update and remove when quantity equals 0\n\n- [#852](https://github.com/bigcommerce/catalyst/pull/852) [`3b7ec09`](https://github.com/bigcommerce/catalyst/commit/3b7ec09c26af506f48259806f8d06e4ba8493bc2) Thanks [@electricenjindevops](https://github.com/electricenjindevops)! - Conditionally show featuredProducts on 404 page.\n\n- [#836](https://github.com/bigcommerce/catalyst/pull/836) [`6cbfd02`](https://github.com/bigcommerce/catalyst/commit/6cbfd02e3621e3be72dfc4db6292f66d1575eb95) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Bump react to v18.3.1\n\n- [#793](https://github.com/bigcommerce/catalyst/pull/793) [`76fad25`](https://github.com/bigcommerce/catalyst/commit/76fad25074afaf5b15f9989fa2a6038af96bfdeb) Thanks [@deini](https://github.com/deini)! - use --turbo for next dev\n\n- [#873](https://github.com/bigcommerce/catalyst/pull/873) [`1c7f52f`](https://github.com/bigcommerce/catalyst/commit/1c7f52f13d9dc6faf8bd039c2208fac76ed88d03) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Set a min width to body.\n\n- [#838](https://github.com/bigcommerce/catalyst/pull/838) [`7a0e393`](https://github.com/bigcommerce/catalyst/commit/7a0e39369b5971be3036e0678455ec82bcb5e321) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Respects when `CLIENT_LOGGER=\"false\"` or `KV_LOGGER=\"false\"` is set in .env.local regardless of environment.\n\n- [#773](https://github.com/bigcommerce/catalyst/pull/773) [`7f70719`](https://github.com/bigcommerce/catalyst/commit/7f7071962a091671c64e376598950c2d6fa3ec1d) Thanks [@deini](https://github.com/deini)! - check for auth on /account pages\n\n- [#771](https://github.com/bigcommerce/catalyst/pull/771) [`8af0878`](https://github.com/bigcommerce/catalyst/commit/8af08780469f1ee0ecdf63449aa7a31c2b965c9e) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Add missing `Cart.spinnerText` translation.\n\n- [#778](https://github.com/bigcommerce/catalyst/pull/778) [`32c3373`](https://github.com/bigcommerce/catalyst/commit/32c33730364241d78ea2fb9817d1543bdd1c1e23) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - Add update address mutation\n\n- [#877](https://github.com/bigcommerce/catalyst/pull/877) [`017fa61`](https://github.com/bigcommerce/catalyst/commit/017fa6178dcbd99ee41d84f71dbe263cfcd76181) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Set mobile padding to 16px instead of 24px\n\n- [#875](https://github.com/bigcommerce/catalyst/pull/875) [`78a5f08`](https://github.com/bigcommerce/catalyst/commit/78a5f088e6dc4da5b804e2acee74f9d79ecb6ef7) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Fix header overflow in mobile, hide search when screen width is extra small.\"\n\n- [#743](https://github.com/bigcommerce/catalyst/pull/743) [`30c7624`](https://github.com/bigcommerce/catalyst/commit/30c7624b4430d76ef3efea1314c18c3b400b966d) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - Add customer addresses query\n\n- [#768](https://github.com/bigcommerce/catalyst/pull/768) [`39feb4a`](https://github.com/bigcommerce/catalyst/commit/39feb4a7773719670a394edc19e5e391905158ba) Thanks [@yurytut1993](https://github.com/yurytut1993)! - add get customer query\n\n- [#846](https://github.com/bigcommerce/catalyst/pull/846) [`e2f4311`](https://github.com/bigcommerce/catalyst/commit/e2f43116e9038f676ea0520bb96de7d16bec6424) Thanks [@avattipalli](https://github.com/avattipalli)! - Migrate visual regression tests\n\n## 0.6.0\n\n### Minor Changes\n\n- [#753](https://github.com/bigcommerce/catalyst/pull/753) [`48c040e`](https://github.com/bigcommerce/catalyst/commit/48c040e94745134f4c60b15cadcdb0a0bbcb2a36) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Deprecate `node@18` in favor of latest LTS version `node@20`.\n\n### Patch Changes\n\n- [#755](https://github.com/bigcommerce/catalyst/pull/755) [`6a6af43`](https://github.com/bigcommerce/catalyst/commit/6a6af432d95a221b1685328bd5211fb6fea8ad55) Thanks [@deini](https://github.com/deini)! - pin next version\n\n- [#757](https://github.com/bigcommerce/catalyst/pull/757) [`dac0199`](https://github.com/bigcommerce/catalyst/commit/dac019989c9c1a81526689dc9e75c9d3a0d0dce3) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Update cart select components to use the item-aligned select content in order to scroll with large Select content.\n\n- Updated dependencies [[`48c040e`](https://github.com/bigcommerce/catalyst/commit/48c040e94745134f4c60b15cadcdb0a0bbcb2a36), [`dac0199`](https://github.com/bigcommerce/catalyst/commit/dac019989c9c1a81526689dc9e75c9d3a0d0dce3)]:\n  - @bigcommerce/catalyst-client@0.3.0\n  - @bigcommerce/components@0.3.0\n\n## 0.5.0\n\n### Minor Changes\n\n- [#719](https://github.com/bigcommerce/catalyst/pull/719) [`ab67b34`](https://github.com/bigcommerce/catalyst/commit/ab67b34ea1c6c7b4b5192a0fe2455ab79f001a97) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - Add tabs for customer account\n\n### Patch Changes\n\n- [#740](https://github.com/bigcommerce/catalyst/pull/740) [`d586c21`](https://github.com/bigcommerce/catalyst/commit/d586c2122bf6513b2f7d923957636c7ea8aaf2ce) Thanks [@dependabot](https://github.com/apps/dependabot)! - chore: bump next-auth and use string for user id\n\n- [#749](https://github.com/bigcommerce/catalyst/pull/749) [`5041719`](https://github.com/bigcommerce/catalyst/commit/5041719a753ef36472f9cfac79bbca32b540b6e5) Thanks [@deini](https://github.com/deini)! - fix social icons type errors with latest @types/react\n\n- [#750](https://github.com/bigcommerce/catalyst/pull/750) [`c8973e2`](https://github.com/bigcommerce/catalyst/commit/c8973e2051042e832859f8c559d0fff456e2a621) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add loading state to remove item button in Cart\n\n- [#724](https://github.com/bigcommerce/catalyst/pull/724) [`045cd14`](https://github.com/bigcommerce/catalyst/commit/045cd14f9846ec939a6237c42f57e849425fa4dd) Thanks [@christensenep](https://github.com/christensenep)! - Support serving static pages when the cart is not empty\n\n- Updated dependencies [[`d586c21`](https://github.com/bigcommerce/catalyst/commit/d586c2122bf6513b2f7d923957636c7ea8aaf2ce)]:\n  - @bigcommerce/catalyst-client@0.2.2\n\n## 0.4.0\n\n### Minor Changes\n\n- [#733](https://github.com/bigcommerce/catalyst/pull/733) [`565e871`](https://github.com/bigcommerce/catalyst/commit/565e87173056fe944c94a004a84947ae93e84c00) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Allow applying and removing coupons in cart\n\n- [#716](https://github.com/bigcommerce/catalyst/pull/716) [`b1a2939`](https://github.com/bigcommerce/catalyst/commit/b1a29398fcde23e67c19bb579e714bcde39839cb) Thanks [@bookernath](https://github.com/bookernath)! - Prefetch high-intent cart link immediately after add to cart action\n\n- [#638](https://github.com/bigcommerce/catalyst/pull/638) [`a1f7970`](https://github.com/bigcommerce/catalyst/commit/a1f797098eee668b4f8bf6763100d71d3882cb45) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - Add reset password functionality\n  Update props for message field\n\n- [#665](https://github.com/bigcommerce/catalyst/pull/665) [`980e481`](https://github.com/bigcommerce/catalyst/commit/980e481767b2305e4e8374c2880018b1637525f0) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - add components for change password\n\n### Patch Changes\n\n- [#713](https://github.com/bigcommerce/catalyst/pull/713) [`643033a`](https://github.com/bigcommerce/catalyst/commit/643033abb973942cbe2ff30bd7e2a539fa7984ed) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Fetch and show digital items in Cart summary.\n\n- [#711](https://github.com/bigcommerce/catalyst/pull/711) [`0ec2269`](https://github.com/bigcommerce/catalyst/commit/0ec22699a3313e4ad7473d12fe13e9a8549f9415) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Use checkout field from GQL to populate checkout summary.\n\n- [#732](https://github.com/bigcommerce/catalyst/pull/732) [`ea5a690`](https://github.com/bigcommerce/catalyst/commit/ea5a6900fa59f73f44537cd3a3095ce4a91e26cf) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Hide discounts if null\n\n- [#722](https://github.com/bigcommerce/catalyst/pull/722) [`b3cddde`](https://github.com/bigcommerce/catalyst/commit/b3cdddecabdbc57e8e6454fa02978bc0216527f7) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Preselect first state when country is selected for Shipping Info\n\n- [#734](https://github.com/bigcommerce/catalyst/pull/734) [`86e57a1`](https://github.com/bigcommerce/catalyst/commit/86e57a18db651cbc8df0e1b8ce7c46d0c0c4087a) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Pass customer id to shipping mutation that were missing.\n\n- [#728](https://github.com/bigcommerce/catalyst/pull/728) [`fa83629`](https://github.com/bigcommerce/catalyst/commit/fa8362917efcf572976628619d9da4859c9dcd47) Thanks [@christensenep](https://github.com/christensenep)! - Fix breadcrumbs on PDP to have correct links\n\n- [#731](https://github.com/bigcommerce/catalyst/pull/731) [`41ebe00`](https://github.com/bigcommerce/catalyst/commit/41ebe001a14e766cab5b75a87f639a0b081bcac0) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add tax total in checkout summary\n\n- [#735](https://github.com/bigcommerce/catalyst/pull/735) [`3db9c5f`](https://github.com/bigcommerce/catalyst/commit/3db9c5fa603299a5c5a9a12bd5408f9024677b20) Thanks [@deini](https://github.com/deini)! - Bump dependencies\n\n- [#683](https://github.com/bigcommerce/catalyst/pull/683) [`cfab55b`](https://github.com/bigcommerce/catalyst/commit/cfab55b374f61f89a9de6a09a8cb3daa93ca48d6) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - add change password mutation for logged in customer\n\n- Updated dependencies [[`a1f7970`](https://github.com/bigcommerce/catalyst/commit/a1f797098eee668b4f8bf6763100d71d3882cb45), [`5af4856`](https://github.com/bigcommerce/catalyst/commit/5af4856510406080d75a1e1db16fe55f86082264), [`3db9c5f`](https://github.com/bigcommerce/catalyst/commit/3db9c5fa603299a5c5a9a12bd5408f9024677b20), [`e4dab93`](https://github.com/bigcommerce/catalyst/commit/e4dab93222b2a19d469315266b2d4627a7967294)]:\n  - @bigcommerce/components@0.2.0\n  - @bigcommerce/catalyst-client@0.2.1\n\n## 0.3.0\n\n### Minor Changes\n\n- [#696](https://github.com/bigcommerce/catalyst/pull/696) [`6deba4a`](https://github.com/bigcommerce/catalyst/commit/6deba4a0713b0d14a76439f0cd01baf35f5185e2) Thanks [@deini](https://github.com/deini)! - removes graphql codegen setup, all graphql calls are done using gql.tada\n\n### Patch Changes\n\n- [#694](https://github.com/bigcommerce/catalyst/pull/694) [`b0c912b`](https://github.com/bigcommerce/catalyst/commit/b0c912bfcefe8c6a9dc46d667f9f96124d1ad132) Thanks [@onurstats](https://github.com/onurstats)! - fix login form translation key mismatch\n\n- [#697](https://github.com/bigcommerce/catalyst/pull/697) [`fbc49e1`](https://github.com/bigcommerce/catalyst/commit/fbc49e144f0eadd7824cae81a46ddff523eb30a3) Thanks [@yurytut1993](https://github.com/yurytut1993)! - add customer & address form fields queries\n\n## 0.2.1\n\n### Patch Changes\n\n- [#641](https://github.com/bigcommerce/catalyst/pull/641) [`43b1afd`](https://github.com/bigcommerce/catalyst/commit/43b1afdf8d9977daf329d0e828e73ea8c8b49acb) Thanks [@yurytut1993](https://github.com/yurytut1993)! - add register customer mutation\n\n- Updated dependencies [[`ac733cc`](https://github.com/bigcommerce/catalyst/commit/ac733cc0308b3ebe1189fe6a7d20214dbc382b3f), [`5af0e66`](https://github.com/bigcommerce/catalyst/commit/5af0e66e7b065ea1d158a0d062a6c3216752d5be)]:\n  - @bigcommerce/catalyst-client@0.2.0\n  - @bigcommerce/components@0.1.2\n\n## 0.2.0\n\n### Minor Changes\n\n- [#662](https://github.com/bigcommerce/catalyst/pull/662) [`be5fc87`](https://github.com/bigcommerce/catalyst/commit/be5fc8787c4e9078c0e032c508f5ccd167421416) Thanks [@deini](https://github.com/deini)! - export a graphql() powered by gql.tada\n\n- [#666](https://github.com/bigcommerce/catalyst/pull/666) [`51a2b64`](https://github.com/bigcommerce/catalyst/commit/51a2b6456ae9ef02569f8eb1380c6deb69b6c55d) Thanks [@deini](https://github.com/deini)! - use gql.tada on simple queries\n\n- [#658](https://github.com/bigcommerce/catalyst/pull/658) [`8ff2eb6`](https://github.com/bigcommerce/catalyst/commit/8ff2eb65acaf973cf7d30833c14238338c57ec44) Thanks [@matthewvolk](https://github.com/matthewvolk)! - create graphql schema using gql.tada\n\n- [#663](https://github.com/bigcommerce/catalyst/pull/663) [`faa2330`](https://github.com/bigcommerce/catalyst/commit/faa23305f6be273320de7caa1e451cef0a748215) Thanks [@deini](https://github.com/deini)! - use gql.tada on all mutations\n\n- [#671](https://github.com/bigcommerce/catalyst/pull/671) [`9c5bb8c`](https://github.com/bigcommerce/catalyst/commit/9c5bb8cd9d9b7bf5a1632d9b9cc998950fd993e7) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Hide handling cost in shipping estimate if there is no cost associated.\n\n### Patch Changes\n\n- [#659](https://github.com/bigcommerce/catalyst/pull/659) [`35e5c96`](https://github.com/bigcommerce/catalyst/commit/35e5c9658d28e167d27a3eb77e455f40f023ed03) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Use amount and discount values for cart summary in Cart page\n\n- [#669](https://github.com/bigcommerce/catalyst/pull/669) [`b657f6c`](https://github.com/bigcommerce/catalyst/commit/b657f6c9f9d56ba45cc09a9fa78f0eb684425204) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Use correct font weight and size for Grand Total in Cart Summary\n\n- [#660](https://github.com/bigcommerce/catalyst/pull/660) [`46b0656`](https://github.com/bigcommerce/catalyst/commit/46b06562e07f3e2ef44803758bfe3d2c7ae49455) Thanks [@deini](https://github.com/deini)! - fix auth imports, was causing issues with --turbo\n\n- [#668](https://github.com/bigcommerce/catalyst/pull/668) [`58ca3eb`](https://github.com/bigcommerce/catalyst/commit/58ca3eb943332aaede6e5a41550cfb0ab048c87a) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Fix on hover style for buttons in Shipping Estimator\n\n## 0.1.1\n\n### Patch Changes\n\n- [#645](https://github.com/bigcommerce/catalyst/pull/645) [`ac57f18`](https://github.com/bigcommerce/catalyst/commit/ac57f189845f6b87e12cd2ac0352301226cf8f50) Thanks [@christensenep](https://github.com/christensenep)! - Add intl provider to No Search Results page\n\n- [#644](https://github.com/bigcommerce/catalyst/pull/644) [`a2ce3b5`](https://github.com/bigcommerce/catalyst/commit/a2ce3b5caf37dcd75cf449648ce3e5b795dc80f7) Thanks [@christensenep](https://github.com/christensenep)! - Use focus-visible instead of focus for focus-related styling\n\n- [#628](https://github.com/bigcommerce/catalyst/pull/628) [`e35d947`](https://github.com/bigcommerce/catalyst/commit/e35d9472d8654847dc5f67ba175e00125c83fabd) Thanks [@bc-alexsaiannyi](https://github.com/bc-alexsaiannyi)! - Add mutations for reset password\n\n- Updated dependencies [[`a2ce3b5`](https://github.com/bigcommerce/catalyst/commit/a2ce3b5caf37dcd75cf449648ce3e5b795dc80f7)]:\n  - @bigcommerce/components@0.1.1\n    All notable changes to this project will be documented in this file.\n"
  },
  {
    "path": "core/README.md",
    "content": "<a href=\"https://catalyst.dev\" target=\"_blank\" rel=\"noopener norerrer\">\n  <img src=\"https://storage.googleapis.com/bigcommerce-developers/images/catalyst_readme_banner.png\" alt=\"Catalyst for Composable Commerce Image Banner\" title=\"Catalyst\">\n</a>\n\n<br />\n<br />\n\n<div align=\"center\">\n\n[![MIT License](https://img.shields.io/github/license/bigcommerce/catalyst)](LICENSE.md)\n[![Lighthouse Report](https://github.com/bigcommerce/catalyst/actions/workflows/lighthouse.yml/badge.svg)](https://github.com/bigcommerce/catalyst/actions/workflows/lighthouse.yml) [![Lint, Typecheck, gql.tada](https://github.com/bigcommerce/catalyst/actions/workflows/basic.yml/badge.svg)](https://github.com/bigcommerce/catalyst/actions/workflows/basic.yml)\n\n</div>\n\n**Catalyst** is the composable, fully customizable headless commerce framework for\n[BigCommerce](https://www.bigcommerce.com/). Catalyst is built with [Next.js](https://nextjs.org/), uses\nour [React](https://react.dev/) storefront components, and is backed by the\n[GraphQL Storefront API](https://developer.bigcommerce.com/docs/storefront/graphql).\n\nBy choosing Catalyst, you'll have a fully-functional storefront within a few seconds, and spend zero time on wiring\nup APIs or building SEO, Accessibility, and Performance-optimized ecommerce components you've probably written many\ntimes before. You can instead go straight to work building your brand and making this your own.\n\n## Demo\n\n- [Catalyst Demo](https://catalyst-demo.site)\n\n![-----------------------------------------------------](https://storage.googleapis.com/bigcommerce-developers/images/catalyst_readme_hr.png)\n\n<p align=\"center\">\n <a href=\"https://www.catalyst.dev\">🚀 catalyst.dev</a> •\n <a href=\"https://developer.bigcommerce.com/community\">🤗 BigCommerce Developer Community</a> •\n <a href=\"https://github.com/bigcommerce/catalyst/discussions\">💬 GitHub Discussions</a> •\n <a href=\"/docs\">💡 Docs in this repo</a>\n</p>\n\n![-----------------------------------------------------](https://storage.googleapis.com/bigcommerce-developers/images/catalyst_readme_hr.png)\n\n## Deploy via One-Click Catalyst App\n\nThe easiest way to deploy your Catalyst Storefront is to use the [One-Click Catalyst App](http://login.bigcommerce.com/deep-links/app/53284) available in the BigCommerce App Marketplace.\n\nCheck out the [Catalyst.dev One-Click Catalyst Documentation](https://www.catalyst.dev/docs/getting-started) for more details.\n\n## Getting Started\n\n**Requirements:**\n\n- A [BigCommerce account](https://www.bigcommerce.com/start-your-trial)\n- Node.js version 24\n- Corepack-enabled `pnpm`\n\n  ```bash\n  corepack enable pnpm\n  ```\n\n1. Install the latest version of Catalyst:\n\n   ```bash\n   pnpm create @bigcommerce/catalyst@latest\n   ```\n\n2. Run the local development server:\n\n   ```bash\n   pnpm run dev\n   ```\n\nLearn more about Catalyst at [catalyst.dev](https://catalyst.dev).\n\n## Resources\n\n- [Catalyst Documentation](https://catalyst.dev/docs/)\n- [GraphQL Storefront API Playground](https://developer.bigcommerce.com/graphql-storefront/playground)\n- [GraphQL Storefront API Explorer](https://developer.bigcommerce.com/graphql-storefront/explorer)\n- [BigCommerce DevDocs](https://developer.bigcommerce.com/docs/build)\n"
  },
  {
    "path": "core/app/[locale]/(default)/(auth)/change-password/_actions/change-password.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\n\nconst ChangePasswordMutation = graphql(`\n  mutation ChangePasswordMutation($input: ResetPasswordInput!) {\n    customer {\n      resetPassword(input: $input) {\n        __typename\n        errors {\n          __typename\n          ... on Error {\n            message\n          }\n        }\n      }\n    }\n  }\n`);\n\nconst schema = z.object({\n  password: z.string(),\n});\n\nexport async function changePassword(\n  { token, customerEntityId }: { token: string; customerEntityId: string },\n  _prevState: { lastResult: SubmissionResult | null; successMessage?: string },\n  formData: FormData,\n) {\n  const t = await getTranslations('Auth.ChangePassword');\n  const submission = parseWithZod(formData, { schema });\n\n  if (submission.status !== 'success') {\n    return { lastResult: submission.reply() };\n  }\n\n  try {\n    const response = await client.fetch({\n      document: ChangePasswordMutation,\n      variables: {\n        input: {\n          token,\n          customerEntityId: Number(customerEntityId),\n          newPassword: submission.value.password,\n        },\n      },\n      fetchOptions: {\n        cache: 'no-store',\n      },\n    });\n\n    const result = response.data.customer.resetPassword;\n\n    if (result.errors.length > 0) {\n      return {\n        lastResult: submission.reply({ formErrors: result.errors.map((error) => error.message) }),\n      };\n    }\n\n    return {\n      lastResult: submission.reply(),\n      successMessage: t('passwordUpdated'),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => message),\n        }),\n      };\n    }\n\n    if (error instanceof Error) {\n      return {\n        lastResult: submission.reply({ formErrors: [error.message] }),\n      };\n    }\n\n    return {\n      lastResult: submission.reply({ formErrors: [t('somethingWentWrong')] }),\n    };\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/(auth)/change-password/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\n\nconst ChangePasswordQuery = graphql(`\n  query ChangePasswordQuery {\n    site {\n      settings {\n        customers {\n          passwordComplexitySettings {\n            minimumNumbers\n            minimumPasswordLength\n            minimumSpecialCharacters\n            requireLowerCase\n            requireNumbers\n            requireSpecialCharacters\n            requireUpperCase\n          }\n        }\n      }\n    }\n  }\n`);\n\nexport const getChangePasswordQuery = cache(async () => {\n  const response = await client.fetch({\n    document: ChangePasswordQuery,\n    fetchOptions: { next: { revalidate } },\n  });\n\n  const passwordComplexitySettings =\n    response.data.site.settings?.customers?.passwordComplexitySettings;\n\n  return {\n    passwordComplexitySettings,\n  };\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/(auth)/change-password/page.tsx",
    "content": "/* eslint-disable react/jsx-no-bind */\nimport { Metadata } from 'next';\nimport { getTranslations, setRequestLocale } from 'next-intl/server';\n\nimport { ResetPasswordSection } from '@/vibes/soul/sections/reset-password-section';\nimport { getChangePasswordQuery } from '~/app/[locale]/(default)/(auth)/change-password/page-data';\nimport { redirect } from '~/i18n/routing';\n\nimport { changePassword } from './_actions/change-password';\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n  searchParams: Promise<{\n    c?: string;\n    t?: string;\n  }>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'Auth.ChangePassword' });\n\n  return {\n    title: t('title'),\n  };\n}\n\nexport default async function ChangePassword({ params, searchParams }: Props) {\n  const { locale } = await params;\n\n  setRequestLocale(locale);\n\n  const { c: customerEntityId, t: token } = await searchParams;\n  const t = await getTranslations('Auth.ChangePassword');\n\n  if (!customerEntityId || !token) {\n    return redirect({ href: '/login', locale });\n  }\n\n  const { passwordComplexitySettings } = await getChangePasswordQuery();\n\n  return (\n    <ResetPasswordSection\n      action={changePassword.bind(null, { customerEntityId, token })}\n      confirmPasswordLabel={t('confirmPassword')}\n      newPasswordLabel={t('newPassword')}\n      passwordComplexitySettings={passwordComplexitySettings}\n      title={t('title')}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/(auth)/layout.tsx",
    "content": "import { PropsWithChildren } from 'react';\n\nimport { isLoggedIn } from '~/auth';\nimport { redirect } from '~/i18n/routing';\n\ninterface Props extends PropsWithChildren {\n  params: Promise<{ locale: string }>;\n}\n\nexport default async function Layout({ children, params }: Props) {\n  const loggedIn = await isLoggedIn();\n  const { locale } = await params;\n\n  if (loggedIn) {\n    redirect({ href: '/account/orders', locale });\n  }\n\n  return children;\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/(auth)/login/_actions/login.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { AuthError } from 'next-auth';\nimport { getLocale, getTranslations } from 'next-intl/server';\n\nimport { schema } from '@/vibes/soul/sections/sign-in-section/schema';\nimport { signIn } from '~/auth';\nimport { redirect } from '~/i18n/routing';\nimport { getCartId } from '~/lib/cart';\n\nexport const login = async (\n  { redirectTo }: { redirectTo: string },\n  _lastResult: SubmissionResult | null,\n  formData: FormData,\n) => {\n  const locale = await getLocale();\n  const t = await getTranslations('Auth.Login');\n  const cartId = await getCartId();\n\n  const submission = parseWithZod(formData, { schema });\n\n  if (submission.status !== 'success') {\n    return submission.reply();\n  }\n\n  try {\n    await signIn('password', {\n      email: submission.value.email,\n      password: submission.value.password,\n      cartId,\n      redirect: false,\n    });\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return submission.reply({\n        formErrors: error.errors.map(({ message }) => message),\n      });\n    }\n\n    if (\n      error instanceof AuthError &&\n      error.type === 'CallbackRouteError' &&\n      error.cause &&\n      error.cause.err instanceof BigCommerceGQLError &&\n      error.cause.err.message.includes('Reset password\"')\n    ) {\n      return submission.reply({ formErrors: [t('passwordResetRequired')] });\n    }\n\n    if (\n      error instanceof AuthError &&\n      error.type === 'CallbackRouteError' &&\n      error.cause &&\n      error.cause.err instanceof BigCommerceGQLError &&\n      error.cause.err.message.includes('Invalid credentials')\n    ) {\n      return submission.reply({ formErrors: [t('invalidCredentials')] });\n    }\n\n    return submission.reply({ formErrors: [t('somethingWentWrong')] });\n  }\n\n  return redirect({ href: redirectTo, locale });\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/(auth)/login/forgot-password/_actions/reset-password.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getTranslations } from 'next-intl/server';\n\nimport { schema } from '@/vibes/soul/sections/forgot-password-section/schema';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\n\nconst ResetPasswordMutation = graphql(`\n  mutation ResetPasswordMutation($input: RequestResetPasswordInput!) {\n    customer {\n      requestResetPassword(input: $input) {\n        __typename\n        errors {\n          __typename\n          ... on Error {\n            message\n          }\n        }\n      }\n    }\n  }\n`);\n\nexport const resetPassword = async (\n  _lastResult: { lastResult: SubmissionResult | null; successMessage?: string },\n  formData: FormData,\n): Promise<{ lastResult: SubmissionResult | null; successMessage?: string }> => {\n  const t = await getTranslations('Auth.Login.ForgotPassword');\n\n  const submission = parseWithZod(formData, { schema });\n\n  if (submission.status !== 'success') {\n    return { lastResult: submission.reply() };\n  }\n\n  try {\n    const response = await client.fetch({\n      document: ResetPasswordMutation,\n      variables: {\n        input: {\n          email: submission.value.email,\n          path: '/change-password',\n        },\n      },\n      fetchOptions: {\n        cache: 'no-store',\n      },\n    });\n\n    const result = response.data.customer.requestResetPassword;\n\n    if (result.errors.length > 0) {\n      return {\n        lastResult: submission.reply({ formErrors: result.errors.map((error) => error.message) }),\n      };\n    }\n\n    return {\n      lastResult: submission.reply(),\n      successMessage: t('confirmResetPassword', { email: submission.value.email }),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => message),\n        }),\n      };\n    }\n\n    if (error instanceof Error) {\n      return { lastResult: submission.reply({ formErrors: [error.message] }) };\n    }\n\n    return { lastResult: submission.reply({ formErrors: [t('somethingWentWrong')] }) };\n  }\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/(auth)/login/forgot-password/page.tsx",
    "content": "import { Metadata } from 'next';\nimport { getTranslations, setRequestLocale } from 'next-intl/server';\n\nimport { ForgotPasswordSection } from '@/vibes/soul/sections/forgot-password-section';\n\nimport { resetPassword } from './_actions/reset-password';\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'Auth.Login.ForgotPassword' });\n\n  return {\n    title: t('title'),\n  };\n}\n\nexport default async function Reset(props: Props) {\n  const { locale } = await props.params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Auth.Login.ForgotPassword');\n\n  return (\n    <ForgotPasswordSection action={resetPassword} subtitle={t('subtitle')} title={t('title')} />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/(auth)/login/page.tsx",
    "content": "/* eslint-disable react/jsx-no-bind */\nimport { Metadata } from 'next';\nimport { getTranslations, setRequestLocale } from 'next-intl/server';\n\nimport { ButtonLink } from '@/vibes/soul/primitives/button-link';\nimport { SignInSection } from '@/vibes/soul/sections/sign-in-section';\nimport { buildConfig } from '~/build-config/reader';\nimport { ForceRefresh } from '~/components/force-refresh';\n\nimport { login } from './_actions/login';\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n  searchParams: Promise<{\n    redirectTo?: string;\n    error?: string;\n  }>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'Auth.Login' });\n\n  return {\n    title: t('title'),\n  };\n}\n\nexport default async function Login({ params, searchParams }: Props) {\n  const { locale } = await params;\n  const { redirectTo = '/account/orders', error } = await searchParams;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Auth.Login');\n\n  const vanityUrl = buildConfig.get('urls').vanityUrl;\n  const redirectUrl = new URL(redirectTo, vanityUrl);\n  const redirectTarget = redirectUrl.pathname + redirectUrl.search;\n  const tokenErrorMessage = error === 'InvalidToken' ? t('invalidToken') : undefined;\n\n  return (\n    <>\n      <ForceRefresh />\n      <SignInSection\n        action={login.bind(null, { redirectTo: redirectTarget })}\n        emailLabel={t('email')}\n        error={tokenErrorMessage}\n        forgotPasswordHref=\"/login/forgot-password\"\n        forgotPasswordLabel={t('forgotPassword')}\n        passwordLabel={t('password')}\n        submitLabel={t('cta')}\n        title={t('heading')}\n      >\n        <div className=\"font-[family-name:var(--sign-in-font-family,var(--font-family-body))]\">\n          <h2 className=\"mb-10 font-[family-name:var(--sign-in-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none text-[var(--reset-password-title,hsl(var(--foreground)))] @xl:text-5xl\">\n            {t('CreateAccount.title')}\n          </h2>\n          <div className=\"text-[var(--sign-in-description,hsl(var(--contrast-500)))]\">\n            <p>{t('CreateAccount.accountBenefits')}</p>\n            <ul className=\"mb-10 ml-4 mt-4 list-disc\">\n              <li>{t('CreateAccount.fastCheckout')}</li>\n              <li>{t('CreateAccount.multipleAddresses')}</li>\n              <li>{t('CreateAccount.ordersHistory')}</li>\n              <li>{t('CreateAccount.ordersTracking')}</li>\n              <li>{t('CreateAccount.wishlists')}</li>\n            </ul>\n            <ButtonLink className=\"mt-auto w-full\" href=\"/register\" variant=\"secondary\">\n              {t('CreateAccount.cta')}\n            </ButtonLink>\n          </div>\n        </div>\n      </SignInSection>\n    </>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/(auth)/login/token/[token]/route.ts",
    "content": "/*\n * This route is used to accept customer login token JWTs from the\n * [Customer Login API](https://developer.bigcommerce.com/docs/start/authentication/customer-login)\n * and log the customers in using alternative authentication methods\n */\n\nimport { decodeJwt } from 'jose';\n// eslint-disable-next-line @typescript-eslint/no-restricted-imports\nimport { redirect, unstable_rethrow as rethrow } from 'next/navigation';\n\nimport { signIn } from '~/auth';\nimport { getCartId } from '~/lib/cart';\n\nexport async function GET(_: Request, { params }: { params: Promise<{ token: string }> }) {\n  const { token } = await params;\n  const cartId = await getCartId();\n\n  try {\n    // decode token without checking signature to get redirect path\n    // token is not checked for validity here, so it could be expired or invalid at this point\n    // token validity and signature are checked in the signIn function\n    const claims = decodeJwt(token);\n    const redirectTo =\n      typeof claims.redirect_to === 'string' ? claims.redirect_to : '/account/orders';\n\n    // sign in with token which will check validity against BigCommerce API\n    // and redirect to redirectTo\n    await signIn('jwt', { jwt: token, cartId, redirectTo });\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    rethrow(error);\n\n    redirect(`/login?error=InvalidToken`);\n  }\n}\n\nexport const dynamic = 'force-dynamic';\n"
  },
  {
    "path": "core/app/[locale]/(default)/(auth)/logout/route.ts",
    "content": "import { NextRequest } from 'next/server';\n\nimport { signOut } from '~/auth';\nimport { redirect } from '~/i18n/routing';\nimport { setForceRefreshCookie } from '~/lib/force-refresh';\n\nexport const GET = async (\n  request: NextRequest,\n  { params }: { params: Promise<{ locale: string }> },\n) => {\n  const { locale } = await params;\n  const redirectTo = request.nextUrl.searchParams.get('redirectTo') ?? '/login';\n  const redirectToPathname = new URL(redirectTo, request.nextUrl.origin).pathname;\n\n  await signOut({ redirect: false });\n  await setForceRefreshCookie();\n\n  redirect({ href: redirectToPathname, locale });\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/(auth)/register/_actions/prefixes.ts",
    "content": "export const ADDRESS_FIELDS_NAME_PREFIX = 'customAddress_';\nexport const CUSTOMER_FIELDS_NAME_PREFIX = 'customCustomer_';\n"
  },
  {
    "path": "core/app/[locale]/(default)/(auth)/register/_actions/register-customer.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getLocale, getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { DynamicFormActionArgs } from '@/vibes/soul/form/dynamic-form';\nimport { Field, FieldGroup, schema } from '@/vibes/soul/form/dynamic-form/schema';\nimport { signIn } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { FieldNameToFieldId } from '~/data-transformers/form-field-transformer/utils';\nimport { redirect } from '~/i18n/routing';\nimport { getCartId } from '~/lib/cart';\nimport { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha';\n\nimport { ADDRESS_FIELDS_NAME_PREFIX, CUSTOMER_FIELDS_NAME_PREFIX } from './prefixes';\n\nconst RegisterCustomerMutation = graphql(`\n  mutation RegisterCustomerMutation(\n    $input: RegisterCustomerInput!\n    $reCaptchaV2: ReCaptchaV2Input\n  ) {\n    customer {\n      registerCustomer(input: $input, reCaptchaV2: $reCaptchaV2) {\n        customer {\n          firstName\n          lastName\n        }\n        errors {\n          ... on EmailAlreadyInUseError {\n            message\n          }\n          ... on AccountCreationDisabledError {\n            message\n          }\n          ... on CustomerRegistrationError {\n            message\n          }\n          ... on ValidationError {\n            message\n          }\n        }\n      }\n    }\n  }\n`);\n\nconst stringToNumber = z.string().pipe(z.coerce.number());\n\nconst inputSchema = z.object({\n  firstName: z.string(),\n  lastName: z.string(),\n  email: z.string(),\n  password: z.string(),\n  phone: z.string().optional(),\n  company: z.string().optional(),\n  address: z\n    .object({\n      firstName: z.string(),\n      lastName: z.string(),\n      address1: z.string(),\n      address2: z.string().optional(),\n      city: z.string(),\n      company: z.string().optional(),\n      countryCode: z.string(),\n      stateOrProvince: z.string().optional(),\n      phone: z.string().optional(),\n      postalCode: z.string().optional(),\n      formFields: z.object({\n        checkboxes: z.array(\n          z.object({\n            fieldEntityId: stringToNumber,\n            fieldValueEntityIds: z.array(stringToNumber),\n          }),\n        ),\n        multipleChoices: z.array(\n          z.object({\n            fieldEntityId: stringToNumber,\n            fieldValueEntityId: stringToNumber,\n          }),\n        ),\n        numbers: z.array(\n          z.object({\n            fieldEntityId: stringToNumber,\n            number: stringToNumber,\n          }),\n        ),\n        dates: z.array(\n          z.object({\n            fieldEntityId: stringToNumber,\n            date: z.string(),\n          }),\n        ),\n        passwords: z.array(\n          z.object({\n            fieldEntityId: stringToNumber,\n            password: z.string(),\n          }),\n        ),\n        multilineTexts: z.array(\n          z.object({\n            fieldEntityId: stringToNumber,\n            multilineText: z.string(),\n          }),\n        ),\n        texts: z.array(\n          z.object({\n            fieldEntityId: stringToNumber,\n            text: z.string(),\n          }),\n        ),\n      }),\n    })\n    .optional(),\n  formFields: z.object({\n    checkboxes: z.array(\n      z.object({\n        fieldEntityId: stringToNumber,\n        fieldValueEntityIds: z.array(stringToNumber),\n      }),\n    ),\n    multipleChoices: z.array(\n      z.object({\n        fieldEntityId: stringToNumber,\n        fieldValueEntityId: stringToNumber,\n      }),\n    ),\n    numbers: z.array(\n      z.object({\n        fieldEntityId: stringToNumber,\n        number: stringToNumber,\n      }),\n    ),\n    dates: z.array(\n      z.object({\n        fieldEntityId: stringToNumber,\n        date: z.string(),\n      }),\n    ),\n    passwords: z.array(\n      z.object({\n        fieldEntityId: stringToNumber,\n        password: z.string(),\n      }),\n    ),\n    multilineTexts: z.array(\n      z.object({\n        fieldEntityId: stringToNumber,\n        multilineText: z.string(),\n      }),\n    ),\n    texts: z.array(\n      z.object({\n        fieldEntityId: stringToNumber,\n        text: z.string(),\n      }),\n    ),\n  }),\n});\n\nfunction parseRegisterCustomerInput(\n  value: Record<string, string | number | string[] | undefined>,\n  fields: Array<Field | FieldGroup<Field>>,\n): VariablesOf<typeof RegisterCustomerMutation>['input'] {\n  const customFields = fields\n    .flatMap((f) => (Array.isArray(f) ? f : [f]))\n    .filter(\n      (field) =>\n        ![\n          String(FieldNameToFieldId.email),\n          String(FieldNameToFieldId.password),\n          String(FieldNameToFieldId.confirmPassword),\n          String(FieldNameToFieldId.firstName),\n          String(FieldNameToFieldId.lastName),\n          String(FieldNameToFieldId.address1),\n          String(FieldNameToFieldId.address2),\n          String(FieldNameToFieldId.city),\n          String(FieldNameToFieldId.company),\n          String(FieldNameToFieldId.countryCode),\n          String(FieldNameToFieldId.stateOrProvince),\n          String(FieldNameToFieldId.phone),\n          String(FieldNameToFieldId.postalCode),\n        ].includes(field.name),\n    );\n\n  const customAddressFields = customFields.filter((field) =>\n    field.name.startsWith(ADDRESS_FIELDS_NAME_PREFIX),\n  );\n  const customCustomerFields = customFields.filter((field) =>\n    field.name.startsWith(CUSTOMER_FIELDS_NAME_PREFIX),\n  );\n\n  const mappedInput = {\n    firstName: value.firstName,\n    lastName: value.lastName,\n    email: value.email,\n    password: value.password,\n    phone: value.phone,\n    company: value.company,\n    address: {\n      firstName: value.firstName,\n      lastName: value.lastName,\n      address1: value.address1,\n      address2: value.address2,\n      city: value.city,\n      company: value.company,\n      countryCode: value.countryCode,\n      stateOrProvince: value.stateOrProvince,\n      phone: value.phone,\n      postalCode: value.postalCode,\n      formFields: {\n        checkboxes: customAddressFields\n          .filter((field) => ['checkbox-group'].includes(field.type))\n          .filter((field) => Boolean(value[field.name]))\n          .map((field) => {\n            return {\n              fieldEntityId: field.id,\n              fieldValueEntityIds: Array.isArray(value[field.name])\n                ? value[field.name]\n                : [value[field.name]],\n            };\n          }),\n        multipleChoices: customAddressFields\n          .filter((field) => ['radio-group', 'button-radio-group'].includes(field.type))\n          .filter((field) => Boolean(value[field.name]))\n          .map((field) => {\n            return {\n              fieldEntityId: field.id,\n              fieldValueEntityId: value[field.name],\n            };\n          }),\n        numbers: customAddressFields\n          .filter((field) => ['number'].includes(field.type))\n          .filter((field) => Boolean(value[field.name]))\n          .map((field) => {\n            return {\n              fieldEntityId: field.id,\n              number: value[field.name],\n            };\n          }),\n        dates: customAddressFields\n          .filter((field) => ['date'].includes(field.type))\n          .filter((field) => Boolean(value[field.name]))\n          .map((field) => {\n            return {\n              fieldEntityId: field.id,\n              date: new Date(String(value[field.name])).toISOString(),\n            };\n          }),\n        passwords: customAddressFields\n          .filter((field) => ['password'].includes(field.type))\n          .filter((field) => Boolean(value[field.name]))\n          .map((field) => ({\n            fieldEntityId: field.id,\n            password: value[field.name],\n          })),\n        multilineTexts: customAddressFields\n          .filter((field) => ['textarea'].includes(field.type))\n          .filter((field) => Boolean(value[field.name]))\n          .map((field) => ({\n            fieldEntityId: field.id,\n            multilineText: value[field.name],\n          })),\n        texts: customAddressFields\n          .filter((field) => ['text'].includes(field.type))\n          .filter((field) => Boolean(value[field.name]))\n          .map((field) => ({\n            fieldEntityId: field.id,\n            text: value[field.name],\n          })),\n      },\n    },\n    formFields: {\n      checkboxes: customCustomerFields\n        .filter((field) => ['checkbox-group'].includes(field.type))\n        .filter((field) => Boolean(value[field.name]))\n        .map((field) => {\n          return {\n            fieldEntityId: field.id,\n            fieldValueEntityIds: Array.isArray(value[field.name])\n              ? value[field.name]\n              : [value[field.name]],\n          };\n        }),\n      multipleChoices: customCustomerFields\n        .filter((field) => ['radio-group', 'button-radio-group'].includes(field.type))\n        .filter((field) => Boolean(value[field.name]))\n        .map((field) => {\n          return {\n            fieldEntityId: field.id,\n            fieldValueEntityId: value[field.name],\n          };\n        }),\n      numbers: customCustomerFields\n        .filter((field) => ['number'].includes(field.type))\n        .filter((field) => Boolean(value[field.name]))\n        .map((field) => {\n          return {\n            fieldEntityId: field.id,\n            number: value[field.name],\n          };\n        }),\n      dates: customCustomerFields\n        .filter((field) => ['date'].includes(field.type))\n        .filter((field) => Boolean(value[field.name]))\n        .map((field) => {\n          return {\n            fieldEntityId: field.id,\n            date: new Date(String(value[field.name])).toISOString(),\n          };\n        }),\n      passwords: customCustomerFields\n        .filter((field) => ['password'].includes(field.type))\n        .filter((field) => Boolean(value[field.name]))\n        .map((field) => ({\n          fieldEntityId: field.id,\n          password: value[field.name],\n        })),\n      multilineTexts: customCustomerFields\n        .filter((field) => ['textarea'].includes(field.type))\n        .filter((field) => Boolean(value[field.name]))\n        .map((field) => ({\n          fieldEntityId: field.id,\n          multilineText: value[field.name],\n        })),\n      texts: customCustomerFields\n        .filter((field) => ['text'].includes(field.type))\n        .filter((field) => Boolean(value[field.name]))\n        .map((field) => ({\n          fieldEntityId: field.id,\n          text: value[field.name],\n        })),\n    },\n  };\n\n  return inputSchema.parse(mappedInput);\n}\n\nexport async function registerCustomer<F extends Field>(\n  { fields, passwordComplexity }: DynamicFormActionArgs<F>,\n  _prevState: {\n    lastResult: SubmissionResult | null;\n  },\n  formData: FormData,\n) {\n  const t = await getTranslations('Auth.Register');\n  const locale = await getLocale();\n  const cartId = await getCartId();\n\n  const submission = parseWithZod(formData, {\n    schema: schema(fields, passwordComplexity),\n  });\n\n  if (submission.status !== 'success') {\n    return {\n      lastResult: submission.reply(),\n    };\n  }\n\n  const { siteKey, token } = await getRecaptchaFromForm(formData);\n  const recaptchaValidation = assertRecaptchaTokenPresent(siteKey, token, t('recaptchaRequired'));\n\n  if (!recaptchaValidation.success) {\n    return {\n      lastResult: submission.reply({ formErrors: recaptchaValidation.formErrors }),\n    };\n  }\n\n  try {\n    const input = parseRegisterCustomerInput(submission.value, fields);\n    const response = await client.fetch({\n      document: RegisterCustomerMutation,\n      variables: {\n        input,\n        reCaptchaV2:\n          recaptchaValidation.token != null ? { token: recaptchaValidation.token } : undefined,\n      },\n      fetchOptions: { cache: 'no-store' },\n    });\n\n    const result = response.data.customer.registerCustomer;\n\n    if (result.errors.length > 0) {\n      return {\n        lastResult: submission.reply({\n          formErrors: response.data.customer.registerCustomer.errors.map((error) => error.message),\n        }),\n      };\n    }\n\n    await signIn('password', {\n      email: input.email,\n      password: input.password,\n      cartId,\n      // We want to use next/navigation for the redirect as it\n      // follows basePath and trailing slash configurations.\n      redirect: false,\n    });\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => message),\n        }),\n      };\n    }\n\n    if (error instanceof Error) {\n      return {\n        lastResult: submission.reply({ formErrors: [error.message] }),\n      };\n    }\n\n    return {\n      lastResult: submission.reply({ formErrors: [t('somethingWentWrong')] }),\n    };\n  }\n\n  return redirect({ href: '/account/orders', locale });\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/(auth)/register/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { FormFieldsFragment } from '~/data-transformers/form-field-transformer/fragment';\n\nconst RegisterCustomerQuery = graphql(\n  `\n    query RegisterCustomerQuery(\n      $customerFilters: FormFieldFiltersInput\n      $customerSortBy: FormFieldSortInput\n      $addressFilters: FormFieldFiltersInput\n      $addressSortBy: FormFieldSortInput\n    ) {\n      site {\n        settings {\n          customers {\n            passwordComplexitySettings {\n              minimumNumbers\n              minimumPasswordLength\n              minimumSpecialCharacters\n              requireLowerCase\n              requireNumbers\n              requireSpecialCharacters\n              requireUpperCase\n            }\n          }\n          formFields {\n            customer(filters: $customerFilters, sortBy: $customerSortBy) {\n              ...FormFieldsFragment\n            }\n            shippingAddress(filters: $addressFilters, sortBy: $addressSortBy) {\n              ...FormFieldsFragment\n            }\n          }\n        }\n      }\n      geography {\n        countries {\n          code\n          name\n        }\n      }\n    }\n  `,\n  [FormFieldsFragment],\n);\n\ntype Variables = VariablesOf<typeof RegisterCustomerQuery>;\n\ninterface Props {\n  address?: {\n    filters?: Variables['addressFilters'];\n    sortBy?: Variables['addressSortBy'];\n  };\n\n  customer?: {\n    filters?: Variables['customerFilters'];\n    sortBy?: Variables['customerSortBy'];\n  };\n}\n\nexport const getRegisterCustomerQuery = cache(async ({ address, customer }: Props) => {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const response = await client.fetch({\n    document: RegisterCustomerQuery,\n    variables: {\n      addressFilters: address?.filters,\n      addressSortBy: address?.sortBy,\n      customerFilters: customer?.filters,\n      customerSortBy: customer?.sortBy,\n    },\n    fetchOptions: { cache: 'no-store' },\n    customerAccessToken,\n  });\n\n  const addressFields = response.data.site.settings?.formFields.shippingAddress;\n  const customerFields = response.data.site.settings?.formFields.customer;\n  const countries = response.data.geography.countries;\n  const passwordComplexitySettings =\n    response.data.site.settings?.customers?.passwordComplexitySettings;\n\n  if (!addressFields || !customerFields) {\n    return null;\n  }\n\n  return {\n    addressFields,\n    customerFields,\n    countries,\n    passwordComplexitySettings,\n  };\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/(auth)/register/page.tsx",
    "content": "import { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { getTranslations, setRequestLocale } from 'next-intl/server';\n\nimport { Field } from '@/vibes/soul/form/dynamic-form/schema';\nimport { DynamicFormSection } from '@/vibes/soul/sections/dynamic-form-section';\nimport {\n  formFieldTransformer,\n  injectCountryCodeOptions,\n} from '~/data-transformers/form-field-transformer';\nimport {\n  CUSTOMER_FIELDS_TO_EXCLUDE,\n  REGISTER_CUSTOMER_FORM_LAYOUT,\n  transformFieldsToLayout,\n} from '~/data-transformers/form-field-transformer/utils';\nimport { getRecaptchaSiteKey } from '~/lib/recaptcha';\nimport { exists } from '~/lib/utils';\n\nimport { ADDRESS_FIELDS_NAME_PREFIX, CUSTOMER_FIELDS_NAME_PREFIX } from './_actions/prefixes';\nimport { registerCustomer } from './_actions/register-customer';\nimport { getRegisterCustomerQuery } from './page-data';\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'Auth.Register' });\n\n  return {\n    title: t('title'),\n  };\n}\n\n// There is currently a GraphQL gap where the \"Exclusive Offers\" field isn't accounted for\n// during customer registration, so the field should not be shown on the Catalyst storefront until it is hooked up.\nfunction removeExlusiveOffersField(field: Field | Field[]): boolean {\n  if (Array.isArray(field)) {\n    // Exclusive offers field will always have ID '25', since it is made upon store creation and is also read-only.\n    return !field.some((f) => f.id === '25');\n  }\n\n  return field.id !== '25';\n}\n\nexport default async function Register({ params }: Props) {\n  const { locale } = await params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Auth.Register');\n\n  const registerCustomerData = await getRegisterCustomerQuery({\n    address: { sortBy: 'SORT_ORDER' },\n    customer: { sortBy: 'SORT_ORDER' },\n  });\n\n  if (!registerCustomerData) {\n    notFound();\n  }\n\n  const { addressFields, customerFields, countries, passwordComplexitySettings } =\n    registerCustomerData;\n\n  const recaptchaSiteKey = await getRecaptchaSiteKey();\n\n  const fields = transformFieldsToLayout(\n    [\n      ...addressFields.map((field) => {\n        if (!field.isBuiltIn) {\n          return {\n            ...field,\n            name: `${ADDRESS_FIELDS_NAME_PREFIX}${field.label}`,\n          };\n        }\n\n        return field;\n      }),\n      ...customerFields.map((field) => {\n        if (!field.isBuiltIn) {\n          return {\n            ...field,\n            name: `${CUSTOMER_FIELDS_NAME_PREFIX}${field.label}`,\n          };\n        }\n\n        return field;\n      }),\n    ].filter((field) => !CUSTOMER_FIELDS_TO_EXCLUDE.includes(field.entityId)),\n    REGISTER_CUSTOMER_FORM_LAYOUT,\n  )\n    .map((field) => {\n      if (Array.isArray(field)) {\n        return field.map(formFieldTransformer).filter(exists);\n      }\n\n      return formFieldTransformer(field);\n    })\n    .filter(exists)\n    .map((field) => {\n      if (Array.isArray(field)) {\n        return field.map((f) => injectCountryCodeOptions(f, countries ?? []));\n      }\n\n      return injectCountryCodeOptions(field, countries ?? []);\n    })\n    .filter(exists)\n    .filter(removeExlusiveOffersField);\n\n  return (\n    <DynamicFormSection\n      action={registerCustomer}\n      errorTranslations={{\n        firstName: {\n          invalid_type: t('FieldErrors.firstNameRequired'),\n        },\n        lastName: {\n          invalid_type: t('FieldErrors.lastNameRequired'),\n        },\n        email: {\n          invalid_type: t('FieldErrors.emailRequired'),\n          invalid_string: t('FieldErrors.emailInvalid'),\n        },\n        password: {\n          invalid_type: t('FieldErrors.passwordRequired'),\n          too_small: t('FieldErrors.passwordTooSmall', {\n            minLength: passwordComplexitySettings?.minimumPasswordLength ?? 0,\n          }),\n          lowercase_required: t('FieldErrors.passwordLowercaseRequired'),\n          uppercase_required: t('FieldErrors.passwordUppercaseRequired'),\n          number_required: t('FieldErrors.passwordNumberRequired', {\n            minNumbers: passwordComplexitySettings?.minimumNumbers ?? 1,\n          }),\n          special_character_required: t('FieldErrors.passwordSpecialCharacterRequired'),\n          passwords_must_match: t('FieldErrors.passwordsMustMatch'),\n        },\n        confirmPassword: {\n          invalid_type: t('FieldErrors.passwordRequired'),\n        },\n        address1: {\n          invalid_type: t('FieldErrors.addressLine1Required'),\n        },\n        city: {\n          invalid_type: t('FieldErrors.cityRequired'),\n        },\n        countryCode: {\n          invalid_type: t('FieldErrors.countryRequired'),\n        },\n        stateOrProvince: {\n          invalid_type: t('FieldErrors.stateRequired'),\n        },\n        postalCode: {\n          invalid_type: t('FieldErrors.postalCodeRequired'),\n        },\n      }}\n      fields={fields}\n      passwordComplexity={passwordComplexitySettings}\n      recaptchaSiteKey={recaptchaSiteKey}\n      submitLabel={t('cta')}\n      title={t('heading')}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/(faceted)/brand/[slug]/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\n\nconst BrandPageQuery = graphql(`\n  query BrandPageQuery($entityId: Int!) {\n    site {\n      brand(entityId: $entityId) {\n        name\n        path\n        seo {\n          pageTitle\n          metaDescription\n          metaKeywords\n        }\n      }\n      settings {\n        inventory {\n          defaultOutOfStockMessage\n          showOutOfStockMessage\n          showBackorderMessage\n        }\n        storefront {\n          catalog {\n            productComparisonsEnabled\n          }\n        }\n        display {\n          showProductRating\n        }\n        reviews {\n          enabled\n        }\n      }\n    }\n  }\n`);\n\nexport const getBrandPageData = cache(async (entityId: number, customerAccessToken?: string) => {\n  const response = await client.fetch({\n    document: BrandPageQuery,\n    variables: { entityId },\n    customerAccessToken,\n    fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },\n  });\n\n  return response.data.site;\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx",
    "content": "import type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\nimport { createLoader, SearchParams } from 'nuqs/server';\nimport { cache } from 'react';\n\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { createCompareLoader } from '@/vibes/soul/primitives/compare-drawer/loader';\nimport { ProductsListSection } from '@/vibes/soul/sections/products-list-section';\nimport { getFilterParsers } from '@/vibes/soul/sections/products-list-section/filter-parsers';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { facetsTransformer } from '~/data-transformers/facets-transformer';\nimport { pageInfoTransformer } from '~/data-transformers/page-info-transformer';\nimport { productCardTransformer } from '~/data-transformers/product-card-transformer';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\nimport { getMetadataAlternates } from '~/lib/seo/canonical';\n\nimport { MAX_COMPARE_LIMIT } from '../../../compare/page-data';\nimport { getCompareProducts as getCompareProductsData } from '../../fetch-compare-products';\nimport { fetchFacetedSearch } from '../../fetch-faceted-search';\n\nimport { getBrandPageData } from './page-data';\n\nconst getCachedBrand = cache((brandId: string) => {\n  return {\n    brand: [brandId],\n  };\n});\n\nconst compareLoader = createCompareLoader();\n\nconst createBrandSearchParamsLoader = cache(\n  async (brandId: string, customerAccessToken?: string) => {\n    const cachedBrand = getCachedBrand(brandId);\n    const brandSearch = await fetchFacetedSearch(cachedBrand, undefined, customerAccessToken);\n    const brandFacets = brandSearch.facets.items.filter(\n      (facet) => facet.__typename !== 'BrandSearchFilter',\n    );\n    const transformedBrandFacets = await facetsTransformer({\n      refinedFacets: brandFacets,\n      allFacets: brandFacets,\n      searchParams: {},\n    });\n    const brandFilters = transformedBrandFacets.filter((facet) => facet != null);\n    const filterParsers = getFilterParsers(brandFilters);\n\n    // If there are no filters, return `null`, since calling `createLoader` with an empty\n    // object will throw the following cryptic error:\n    //\n    // ```\n    // Error: [nuqs] Empty search params cache. Search params can't be accessed in Layouts.\n    //   See https://err.47ng.com/NUQS-500\n    // ```\n    if (Object.keys(filterParsers).length === 0) {\n      return null;\n    }\n\n    return createLoader(filterParsers);\n  },\n);\n\ninterface Props {\n  params: Promise<{\n    slug: string;\n    locale: string;\n  }>;\n  searchParams: Promise<SearchParams>;\n}\n\nexport async function generateMetadata(props: Props): Promise<Metadata> {\n  const { slug, locale } = await props.params;\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const brandId = Number(slug);\n\n  const { brand } = await getBrandPageData(brandId, customerAccessToken);\n\n  if (!brand) {\n    return notFound();\n  }\n\n  const { pageTitle, metaDescription, metaKeywords } = brand.seo;\n\n  return {\n    title: pageTitle || brand.name,\n    ...(metaDescription && { description: metaDescription }),\n    ...(metaKeywords && { keywords: metaKeywords.split(',') }),\n    ...(brand.path && { alternates: await getMetadataAlternates({ path: brand.path, locale }) }),\n  };\n}\n\nexport default async function Brand(props: Props) {\n  const { locale, slug } = await props.params;\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Faceted');\n\n  const brandId = Number(slug);\n\n  const { brand, settings } = await getBrandPageData(brandId, customerAccessToken);\n\n  if (!brand) {\n    return notFound();\n  }\n\n  const showRating = Boolean(settings?.reviews.enabled && settings.display.showProductRating);\n\n  const productComparisonsEnabled =\n    settings?.storefront.catalog?.productComparisonsEnabled ?? false;\n\n  const streamableFacetedSearch = Streamable.from(async () => {\n    const searchParams = await props.searchParams;\n    const currencyCode = await getPreferredCurrencyCode();\n\n    const loadSearchParams = await createBrandSearchParamsLoader(slug, customerAccessToken);\n    const parsedSearchParams = loadSearchParams?.(searchParams) ?? {};\n\n    const search = await fetchFacetedSearch(\n      {\n        ...searchParams,\n        ...parsedSearchParams,\n        brand: [slug],\n      },\n      currencyCode,\n      customerAccessToken,\n    );\n\n    return search;\n  });\n\n  const streamableProducts = Streamable.from(async () => {\n    const format = await getFormatter();\n\n    const search = await streamableFacetedSearch;\n    const products = search.products.items;\n\n    const { defaultOutOfStockMessage, showOutOfStockMessage, showBackorderMessage } =\n      settings?.inventory ?? {};\n\n    return productCardTransformer(\n      products,\n      format,\n      showOutOfStockMessage ? defaultOutOfStockMessage : undefined,\n      showBackorderMessage,\n    );\n  });\n\n  const streamableTotalCount = Streamable.from(async () => {\n    const format = await getFormatter();\n    const search = await streamableFacetedSearch;\n\n    return format.number(search.products.collectionInfo?.totalItems ?? 0);\n  });\n\n  const streamablePagination = Streamable.from(async () => {\n    const search = await streamableFacetedSearch;\n\n    return pageInfoTransformer(search.products.pageInfo);\n  });\n\n  const streamableFilters = Streamable.from(async () => {\n    const searchParams = await props.searchParams;\n    const loadSearchParams = await createBrandSearchParamsLoader(slug, customerAccessToken);\n    const parsedSearchParams = loadSearchParams?.(searchParams) ?? {};\n    const cachedBrand = getCachedBrand(slug);\n    const categorySearch = await fetchFacetedSearch(cachedBrand, undefined, customerAccessToken);\n    const refinedSearch = await streamableFacetedSearch;\n\n    const allFacets = categorySearch.facets.items.filter(\n      (facet) => facet.__typename !== 'BrandSearchFilter',\n    );\n    const refinedFacets = refinedSearch.facets.items.filter(\n      (facet) => facet.__typename !== 'BrandSearchFilter',\n    );\n\n    const transformedFacets = await facetsTransformer({\n      refinedFacets,\n      allFacets,\n      searchParams: { ...searchParams, ...parsedSearchParams },\n    });\n\n    return transformedFacets.filter((facet) => facet != null);\n  });\n\n  const streamableCompareProducts = Streamable.from(async () => {\n    const searchParams = await props.searchParams;\n\n    if (!productComparisonsEnabled) {\n      return [];\n    }\n\n    const { compare } = compareLoader(searchParams);\n\n    const compareIds = { entityIds: compare ? compare.map((id: string) => Number(id)) : [] };\n\n    const products = await getCompareProductsData(compareIds, customerAccessToken);\n\n    return products.map((product) => ({\n      id: product.entityId.toString(),\n      title: product.name,\n      image: product.defaultImage\n        ? { src: product.defaultImage.url, alt: product.defaultImage.altText }\n        : undefined,\n      href: product.path,\n    }));\n  });\n\n  return (\n    <ProductsListSection\n      compareLabel={t('Compare.compare')}\n      compareProducts={streamableCompareProducts}\n      emptyStateSubtitle={t('Brand.Empty.subtitle')}\n      emptyStateTitle={t('Brand.Empty.title')}\n      filterLabel={t('FacetedSearch.filters')}\n      filters={streamableFilters}\n      filtersPanelTitle={t('FacetedSearch.filters')}\n      maxCompareLimitMessage={t('Compare.maxCompareLimit')}\n      maxItems={MAX_COMPARE_LIMIT}\n      paginationInfo={streamablePagination}\n      products={streamableProducts}\n      rangeFilterApplyLabel={t('FacetedSearch.Range.apply')}\n      removeLabel={t('Compare.remove')}\n      resetFiltersLabel={t('FacetedSearch.resetFilters')}\n      showCompare={productComparisonsEnabled}\n      showRating={showRating}\n      sortDefaultValue=\"featured\"\n      sortLabel={t('Search.title')}\n      sortOptions={[\n        { value: 'featured', label: t('SortBy.featuredItems') },\n        { value: 'newest', label: t('SortBy.newestItems') },\n        { value: 'best_selling', label: t('SortBy.bestSellingItems') },\n        { value: 'a_to_z', label: t('SortBy.aToZ') },\n        { value: 'z_to_a', label: t('SortBy.zToA') },\n        { value: 'best_reviewed', label: t('SortBy.byReview') },\n        { value: 'lowest_price', label: t('SortBy.priceAscending') },\n        { value: 'highest_price', label: t('SortBy.priceDescending') },\n        { value: 'relevance', label: t('SortBy.relevance') },\n      ]}\n      sortParamName=\"sort\"\n      title={brand.name}\n      totalCount={streamableTotalCount}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/(faceted)/category/[slug]/_components/category-viewed.tsx",
    "content": "'use client';\n\nimport { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { useEffect, useRef } from 'react';\n\nimport { FragmentOf } from '~/client/graphql';\nimport { ProductCardFragment } from '~/components/product-card/fragment';\nimport { useAnalytics } from '~/lib/analytics/react';\n\nimport { getCategoryPageData } from '../page-data';\n\ntype Category = Awaited<ReturnType<typeof getCategoryPageData>>['category'];\ntype productSearchItem = FragmentOf<typeof ProductCardFragment>;\n\ninterface Props {\n  category: NonNullable<Category>;\n  products: productSearchItem[];\n}\n\nexport const CategoryViewed = ({ category, products }: Props) => {\n  const isMounted = useRef(false);\n  const analytics = useAnalytics();\n\n  useEffect(() => {\n    if (isMounted.current) {\n      return;\n    }\n\n    isMounted.current = true;\n\n    analytics?.navigation.categoryViewed({\n      id: category.entityId,\n      name: category.name,\n      currency: products[0]?.prices?.price.currencyCode || 'USD',\n      items: products.map((p) => {\n        return {\n          id: p.entityId.toString(),\n          name: p.name,\n          brand: p.brand?.name,\n          price: p.prices?.price.value,\n          categories: removeEdgesAndNodes(category.breadcrumbs).map(({ name }) => name),\n        };\n      }),\n    });\n  }, [analytics, category, products]);\n\n  return null;\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/(faceted)/category/[slug]/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { BreadcrumbsCategoryFragment } from '~/components/breadcrumbs/fragment';\n\nconst CategoryPageQuery = graphql(\n  `\n    query CategoryPageQuery($entityId: Int!) {\n      site {\n        category(entityId: $entityId) {\n          entityId\n          name\n          ...BreadcrumbsFragment\n          seo {\n            pageTitle\n            metaDescription\n            metaKeywords\n          }\n        }\n        categoryTree(rootEntityId: $entityId) {\n          entityId\n          name\n          path\n          children {\n            entityId\n            name\n            path\n            children {\n              entityId\n              name\n              path\n            }\n          }\n        }\n        settings {\n          inventory {\n            defaultOutOfStockMessage\n            showOutOfStockMessage\n            showBackorderMessage\n          }\n          storefront {\n            catalog {\n              productComparisonsEnabled\n            }\n          }\n          display {\n            showProductRating\n          }\n          reviews {\n            enabled\n          }\n        }\n      }\n    }\n  `,\n  [BreadcrumbsCategoryFragment],\n);\n\nexport const getCategoryPageData = cache(async (entityId: number, customerAccessToken?: string) => {\n  const response = await client.fetch({\n    document: CategoryPageQuery,\n    variables: { entityId },\n    customerAccessToken,\n    fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },\n  });\n\n  return response.data.site;\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\nimport { createLoader, SearchParams } from 'nuqs/server';\nimport { cache } from 'react';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { createCompareLoader } from '@/vibes/soul/primitives/compare-drawer/loader';\nimport { ProductsListSection } from '@/vibes/soul/sections/products-list-section';\nimport { getFilterParsers } from '@/vibes/soul/sections/products-list-section/filter-parsers';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { facetsTransformer } from '~/data-transformers/facets-transformer';\nimport { pageInfoTransformer } from '~/data-transformers/page-info-transformer';\nimport { productCardTransformer } from '~/data-transformers/product-card-transformer';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\nimport { getMetadataAlternates } from '~/lib/seo/canonical';\n\nimport { MAX_COMPARE_LIMIT } from '../../../compare/page-data';\nimport { getCompareProducts } from '../../fetch-compare-products';\nimport { fetchFacetedSearch } from '../../fetch-faceted-search';\n\nimport { CategoryViewed } from './_components/category-viewed';\nimport { getCategoryPageData } from './page-data';\n\nconst getCachedCategory = cache((categoryId: number) => {\n  return {\n    category: categoryId,\n  };\n});\n\nconst compareLoader = createCompareLoader();\n\nconst createCategorySearchParamsLoader = cache(\n  async (categoryId: number, customerAccessToken?: string) => {\n    const cachedCategory = getCachedCategory(categoryId);\n    const categorySearch = await fetchFacetedSearch(cachedCategory, undefined, customerAccessToken);\n    const categoryFacets = categorySearch.facets.items.filter(\n      (facet) => facet.__typename !== 'CategorySearchFilter',\n    );\n    const transformedCategoryFacets = await facetsTransformer({\n      refinedFacets: categoryFacets,\n      allFacets: categoryFacets,\n      searchParams: {},\n    });\n    const categoryFilters = transformedCategoryFacets.filter((facet) => facet != null);\n    const filterParsers = getFilterParsers(categoryFilters);\n\n    // If there are no filters, return `null`, since calling `createLoader` with an empty\n    // object will throw the following cryptic error:\n    //\n    // ```\n    // Error: [nuqs] Empty search params cache. Search params can't be accessed in Layouts.\n    //   See https://err.47ng.com/NUQS-500\n    // ```\n    if (Object.keys(filterParsers).length === 0) {\n      return null;\n    }\n\n    return createLoader(filterParsers);\n  },\n);\n\ninterface Props {\n  params: Promise<{\n    slug: string;\n    locale: string;\n  }>;\n  searchParams: Promise<SearchParams>;\n}\n\nexport async function generateMetadata(props: Props): Promise<Metadata> {\n  const { slug, locale } = await props.params;\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const categoryId = Number(slug);\n\n  const { category } = await getCategoryPageData(categoryId, customerAccessToken);\n\n  if (!category) {\n    return notFound();\n  }\n\n  const { pageTitle, metaDescription, metaKeywords } = category.seo;\n\n  const breadcrumbs = removeEdgesAndNodes(category.breadcrumbs);\n  const categoryPath = breadcrumbs[breadcrumbs.length - 1]?.path;\n\n  return {\n    title: pageTitle || category.name,\n    ...(metaDescription && { description: metaDescription }),\n    ...(metaKeywords && { keywords: metaKeywords.split(',') }),\n    ...(categoryPath && {\n      alternates: await getMetadataAlternates({ path: categoryPath, locale }),\n    }),\n  };\n}\n\nexport default async function Category(props: Props) {\n  const { slug, locale } = await props.params;\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Faceted');\n\n  const categoryId = Number(slug);\n\n  const { category, settings, categoryTree } = await getCategoryPageData(\n    categoryId,\n    customerAccessToken,\n  );\n\n  if (!category) {\n    return notFound();\n  }\n\n  const breadcrumbs = removeEdgesAndNodes(category.breadcrumbs).map(({ name, path }) => ({\n    label: name,\n    href: path ?? '#',\n  }));\n\n  const showRating = Boolean(settings?.reviews.enabled && settings.display.showProductRating);\n\n  const productComparisonsEnabled =\n    settings?.storefront.catalog?.productComparisonsEnabled ?? false;\n\n  const streamableFacetedSearch = Streamable.from(async () => {\n    const searchParams = await props.searchParams;\n    const currencyCode = await getPreferredCurrencyCode();\n\n    const loadSearchParams = await createCategorySearchParamsLoader(\n      categoryId,\n      customerAccessToken,\n    );\n    const parsedSearchParams = loadSearchParams?.(searchParams) ?? {};\n\n    const search = await fetchFacetedSearch(\n      {\n        ...searchParams,\n        ...parsedSearchParams,\n        category: categoryId,\n      },\n      currencyCode,\n      customerAccessToken,\n    );\n\n    return search;\n  });\n\n  const streamableProducts = Streamable.from(async () => {\n    const format = await getFormatter();\n\n    const search = await streamableFacetedSearch;\n    const products = search.products.items;\n\n    const { defaultOutOfStockMessage, showOutOfStockMessage, showBackorderMessage } =\n      settings?.inventory ?? {};\n\n    return productCardTransformer(\n      products,\n      format,\n      showOutOfStockMessage ? defaultOutOfStockMessage : undefined,\n      showBackorderMessage,\n    );\n  });\n\n  const streamableTotalCount = Streamable.from(async () => {\n    const format = await getFormatter();\n    const search = await streamableFacetedSearch;\n\n    return format.number(search.products.collectionInfo?.totalItems ?? 0);\n  });\n\n  const streamablePagination = Streamable.from(async () => {\n    const search = await streamableFacetedSearch;\n\n    return pageInfoTransformer(search.products.pageInfo);\n  });\n\n  const streamableFilters = Streamable.from(async () => {\n    const searchParams = await props.searchParams;\n\n    const loadSearchParams = await createCategorySearchParamsLoader(\n      categoryId,\n      customerAccessToken,\n    );\n    const parsedSearchParams = loadSearchParams?.(searchParams) ?? {};\n    const cachedCategory = getCachedCategory(categoryId);\n    const categorySearch = await fetchFacetedSearch(cachedCategory, undefined, customerAccessToken);\n    const refinedSearch = await streamableFacetedSearch;\n\n    const allFacets = categorySearch.facets.items.filter(\n      (facet) => facet.__typename !== 'CategorySearchFilter',\n    );\n    const refinedFacets = refinedSearch.facets.items.filter(\n      (facet) => facet.__typename !== 'CategorySearchFilter',\n    );\n\n    const transformedFacets = await facetsTransformer({\n      refinedFacets,\n      allFacets,\n      searchParams: { ...searchParams, ...parsedSearchParams },\n    });\n\n    const filters = transformedFacets.filter((facet) => facet != null);\n\n    const tree = categoryTree[0];\n    const subCategoriesFilters =\n      tree == null || tree.children.length === 0\n        ? []\n        : [\n            {\n              type: 'link-group' as const,\n              label: t('Category.subCategories'),\n              links: tree.children.map((child) => ({\n                label: child.name,\n                href: child.path,\n              })),\n            },\n          ];\n\n    return [...subCategoriesFilters, ...filters];\n  });\n\n  const streamableCompareProducts = Streamable.from(async () => {\n    const searchParams = await props.searchParams;\n\n    if (!productComparisonsEnabled) {\n      return [];\n    }\n\n    const { compare } = compareLoader(searchParams);\n\n    const compareIds = { entityIds: compare ? compare.map((id: string) => Number(id)) : [] };\n\n    const products = await getCompareProducts(compareIds, customerAccessToken);\n\n    return products.map((product) => ({\n      id: product.entityId.toString(),\n      title: product.name,\n      image: product.defaultImage\n        ? { src: product.defaultImage.url, alt: product.defaultImage.altText }\n        : undefined,\n      href: product.path,\n    }));\n  });\n\n  return (\n    <>\n      <ProductsListSection\n        breadcrumbs={breadcrumbs}\n        compareLabel={t('Compare.compare')}\n        compareProducts={streamableCompareProducts}\n        emptyStateSubtitle={t('Category.Empty.subtitle')}\n        emptyStateTitle={t('Category.Empty.title')}\n        filterLabel={t('FacetedSearch.filters')}\n        filters={streamableFilters}\n        filtersPanelTitle={t('FacetedSearch.filters')}\n        maxCompareLimitMessage={t('Compare.maxCompareLimit')}\n        maxItems={MAX_COMPARE_LIMIT}\n        paginationInfo={streamablePagination}\n        products={streamableProducts}\n        rangeFilterApplyLabel={t('FacetedSearch.Range.apply')}\n        removeLabel={t('Compare.remove')}\n        resetFiltersLabel={t('FacetedSearch.resetFilters')}\n        showCompare={productComparisonsEnabled}\n        showRating={showRating}\n        sortDefaultValue=\"featured\"\n        sortLabel={t('SortBy.sortBy')}\n        sortOptions={[\n          { value: 'featured', label: t('SortBy.featuredItems') },\n          { value: 'newest', label: t('SortBy.newestItems') },\n          { value: 'best_selling', label: t('SortBy.bestSellingItems') },\n          { value: 'a_to_z', label: t('SortBy.aToZ') },\n          { value: 'z_to_a', label: t('SortBy.zToA') },\n          { value: 'best_reviewed', label: t('SortBy.byReview') },\n          { value: 'lowest_price', label: t('SortBy.priceAscending') },\n          { value: 'highest_price', label: t('SortBy.priceDescending') },\n          { value: 'relevance', label: t('SortBy.relevance') },\n        ]}\n        sortParamName=\"sort\"\n        title={category.name}\n        totalCount={streamableTotalCount}\n      />\n      <Stream value={streamableFacetedSearch}>\n        {(search) => <CategoryViewed category={category} products={search.products.items} />}\n      </Stream>\n    </>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/(faceted)/fetch-compare-products.ts",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { VariablesOf } from 'gql.tada';\nimport { cache } from 'react';\nimport { z } from 'zod';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\n\nimport { MAX_COMPARE_LIMIT } from '../compare/page-data';\n\nconst CompareProductsSchema = z.object({\n  entityIds: z\n    .array(\n      z.preprocess(\n        (val) => (!Number.isNaN(val) ? val : undefined), // Remove NaN before validation\n        z.number().optional(),\n      ),\n    )\n    .transform((arr) => arr.filter((num) => num !== undefined)), // Remove `undefined` values\n});\n\nconst CompareProductsQuery = graphql(`\n  query CompareProductsQuery($entityIds: [Int!], $first: Int) {\n    site {\n      products(entityIds: $entityIds, first: $first) {\n        edges {\n          node {\n            entityId\n            name\n            defaultImage {\n              url: urlTemplate(lossy: true)\n              altText\n            }\n            path\n          }\n        }\n      }\n    }\n  }\n`);\n\ntype Variables = VariablesOf<typeof CompareProductsQuery>;\n\nexport const getCompareProducts = cache(\n  async (variables: Variables, customerAccessToken?: string) => {\n    const parsedVariables = CompareProductsSchema.parse(variables);\n\n    if (parsedVariables.entityIds.length === 0) {\n      return [];\n    }\n\n    const response = await client.fetch({\n      document: CompareProductsQuery,\n      variables: { ...parsedVariables, first: MAX_COMPARE_LIMIT },\n      customerAccessToken,\n      fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },\n    });\n\n    return removeEdgesAndNodes(response.data.site.products);\n  },\n);\n"
  },
  {
    "path": "core/app/[locale]/(default)/(faceted)/fetch-faceted-search.ts",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { cache } from 'react';\nimport { z } from 'zod';\n\nimport { client } from '~/client';\nimport { PaginationFragment } from '~/client/fragments/pagination';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { CurrencyCode } from '~/components/header/fragment';\nimport { ProductCardFragment } from '~/components/product-card/fragment';\n\nconst GetProductSearchResultsQuery = graphql(\n  `\n    query GetProductSearchResultsQuery(\n      $first: Int\n      $last: Int\n      $after: String\n      $before: String\n      $filters: SearchProductsFiltersInput!\n      $sort: SearchProductsSortInput\n      $currencyCode: currencyCode\n    ) {\n      site {\n        search {\n          searchProducts(filters: $filters, sort: $sort) {\n            products(first: $first, after: $after, last: $last, before: $before) {\n              pageInfo {\n                ...PaginationFragment\n              }\n              collectionInfo {\n                totalItems\n              }\n              edges {\n                node {\n                  ...ProductCardFragment\n                }\n              }\n            }\n            filters {\n              edges {\n                node {\n                  __typename\n                  displayName\n                  isCollapsedByDefault\n                  ... on BrandSearchFilter {\n                    displayProductCount\n                    displayName\n                    brands {\n                      pageInfo {\n                        ...PaginationFragment\n                      }\n                      edges {\n                        cursor\n                        node {\n                          entityId\n                          name\n                          isSelected\n                          productCount\n                        }\n                      }\n                    }\n                  }\n                  ... on CategorySearchFilter {\n                    displayProductCount\n                    displayName\n                    categories {\n                      pageInfo {\n                        ...PaginationFragment\n                      }\n                      edges {\n                        cursor\n                        node {\n                          entityId\n                          name\n                          isSelected\n                          productCount\n                          subCategories {\n                            pageInfo {\n                              ...PaginationFragment\n                            }\n                            edges {\n                              cursor\n                              node {\n                                entityId\n                                name\n                                isSelected\n                                productCount\n                              }\n                            }\n                          }\n                        }\n                      }\n                    }\n                  }\n                  ... on ProductAttributeSearchFilter {\n                    displayProductCount\n                    filterName\n                    filterKey\n                    displayName\n                    attributes {\n                      pageInfo {\n                        ...PaginationFragment\n                      }\n                      edges {\n                        cursor\n                        node {\n                          value\n                          isSelected\n                          productCount\n                        }\n                      }\n                    }\n                  }\n                  ... on RatingSearchFilter {\n                    displayName\n                    ratings {\n                      pageInfo {\n                        ...PaginationFragment\n                      }\n                      edges {\n                        cursor\n                        node {\n                          value\n                          isSelected\n                          productCount\n                        }\n                      }\n                    }\n                  }\n                  ... on PriceSearchFilter {\n                    displayName\n                    selected {\n                      minPrice\n                      maxPrice\n                    }\n                  }\n                  ... on OtherSearchFilter {\n                    displayProductCount\n                    freeShipping {\n                      isSelected\n                      productCount\n                    }\n                    isFeatured {\n                      isSelected\n                      productCount\n                    }\n                    isInStock {\n                      isSelected\n                      productCount\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n        settings {\n          storefront {\n            catalog {\n              productComparisonsEnabled\n            }\n          }\n        }\n      }\n    }\n  `,\n  [PaginationFragment, ProductCardFragment],\n);\n\ntype Variables = VariablesOf<typeof GetProductSearchResultsQuery>;\ntype SearchProductsSortInput = Variables['sort'];\ntype SearchProductsFiltersInput = Variables['filters'];\n\ninterface ProductSearch {\n  limit?: number | null;\n  before?: string | null;\n  after?: string | null;\n  sort?: SearchProductsSortInput | null;\n  filters: SearchProductsFiltersInput;\n}\n\nconst getProductSearchResults = cache(\n  async (\n    { limit = 9, after, before, sort, filters }: ProductSearch,\n    currencyCode?: CurrencyCode,\n    customerAccessToken?: string,\n  ) => {\n    const filterArgs = { filters, sort };\n    const paginationArgs = before ? { last: limit, before } : { first: limit, after };\n\n    const response = await client.fetch({\n      document: GetProductSearchResultsQuery,\n      variables: { ...filterArgs, ...paginationArgs, currencyCode },\n      customerAccessToken,\n      fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate: 300 } },\n    });\n\n    const { site } = response.data;\n\n    const searchResults = site.search.searchProducts;\n\n    const items = removeEdgesAndNodes(searchResults.products).map((product) => ({\n      ...product,\n    }));\n\n    return {\n      facets: {\n        items: removeEdgesAndNodes(searchResults.filters).map((node) => {\n          switch (node.__typename) {\n            case 'BrandSearchFilter':\n              return {\n                ...node,\n                brands: removeEdgesAndNodes(node.brands),\n              };\n\n            case 'CategorySearchFilter':\n              return {\n                ...node,\n                categories: removeEdgesAndNodes(node.categories),\n              };\n\n            case 'ProductAttributeSearchFilter':\n              return {\n                ...node,\n                attributes: removeEdgesAndNodes(node.attributes),\n              };\n\n            case 'RatingSearchFilter':\n              return {\n                ...node,\n                ratings: removeEdgesAndNodes(node.ratings),\n              };\n\n            default:\n              return node;\n          }\n        }),\n      },\n      products: {\n        collectionInfo: searchResults.products.collectionInfo,\n        pageInfo: searchResults.products.pageInfo,\n        items,\n      },\n    };\n  },\n);\n\nconst SearchParamSchema = z.union([z.string(), z.array(z.string()), z.undefined()]);\n\nconst SearchParamToArray = SearchParamSchema.transform((value) => {\n  if (Array.isArray(value)) {\n    return value;\n  }\n\n  if (typeof value === 'string' && value !== '') {\n    return [value];\n  }\n\n  return undefined;\n});\n\nconst PrivateSortParam = z.union([\n  z.literal('A_TO_Z'),\n  z.literal('BEST_REVIEWED'),\n  z.literal('BEST_SELLING'),\n  z.literal('FEATURED'),\n  z.literal('HIGHEST_PRICE'),\n  z.literal('LOWEST_PRICE'),\n  z.literal('NEWEST'),\n  z.literal('RELEVANCE'),\n  z.literal('Z_TO_A'),\n]) satisfies z.ZodType<SearchProductsSortInput>;\n\nconst PublicSortParam = z.string().toUpperCase().pipe(PrivateSortParam);\n\nconst SearchProductsFiltersInputSchema = z.object({\n  brandEntityIds: z.array(z.number()).nullish(),\n  categoryEntityId: z.number().nullish(),\n  categoryEntityIds: z.array(z.number()).nullish(),\n  hideOutOfStock: z.boolean().nullish(),\n  isFeatured: z.boolean().nullish(),\n  isFreeShipping: z.boolean().nullish(),\n  price: z\n    .object({\n      maxPrice: z.number().nullish(),\n      minPrice: z.number().nullish(),\n    })\n    .nullish(),\n  productAttributes: z\n    .array(\n      z.object({\n        attribute: z.string(),\n        values: z.array(z.string()),\n      }),\n    )\n    .nullish(),\n  rating: z\n    .object({\n      maxRating: z.number().nullish(),\n      minRating: z.number().nullish(),\n    })\n    .nullish(),\n  searchSubCategories: z.boolean().nullish(),\n  searchTerm: z.string().nullish(),\n}) satisfies z.ZodType<SearchProductsFiltersInput>;\n\nconst PrivateSearchParamsSchema = z.object({\n  after: z.string().nullish(),\n  before: z.string().nullish(),\n  limit: z.number().nullish(),\n  sort: PrivateSortParam.nullish(),\n  filters: SearchProductsFiltersInputSchema,\n});\n\nexport const PublicSearchParamsSchema = z.object({\n  after: z.string().nullish(),\n  before: z.string().nullish(),\n  brand: SearchParamToArray.nullish().transform((value) => value?.map(Number)),\n  category: z.coerce.number().optional(),\n  categoryIn: SearchParamToArray.nullish().transform((value) => value?.map(Number)),\n  isFeatured: z.coerce.boolean().nullish(),\n  limit: z.coerce.number().nullish(),\n  minPrice: z.coerce.number().nullish(),\n  maxPrice: z.coerce.number().nullish(),\n  minRating: z.coerce.number().nullish(),\n  maxRating: z.coerce.number().nullish(),\n  sort: PublicSortParam.nullish(),\n  // In the future we should support more stock filters, e.g. out of stock, low stock, etc.\n  stock: SearchParamToArray.nullish().transform((value) =>\n    value?.filter((stock) => z.enum(['in_stock']).safeParse(stock).success),\n  ),\n  // In the future we should support more shipping filters, e.g. 2 day shipping, same day, etc.\n  shipping: SearchParamToArray.nullish().transform((value) =>\n    value?.filter((stock) => z.enum(['free_shipping']).safeParse(stock).success),\n  ),\n  term: z.string().nullish(),\n});\n\nconst AttributeKey = z.custom<`attr_${string}`>((val) => {\n  return typeof val === 'string' ? /^attr_.+$/.test(val) : false;\n});\n\nexport const PublicToPrivateParams = PublicSearchParamsSchema.catchall(SearchParamToArray.nullish())\n  .transform((publicParams) => {\n    const { after, before, limit, sort, ...filters } = publicParams;\n\n    const {\n      brand,\n      category,\n      categoryIn,\n      isFeatured,\n      minPrice,\n      maxPrice,\n      minRating,\n      maxRating,\n      term,\n      shipping,\n      stock,\n      // There is a bug in Next.js that is adding the path params to the searchParams. We need to filter out the slug params for now.\n      // https://github.com/vercel/next.js/issues/51802\n      slug,\n      ...additionalParams\n    } = filters;\n\n    // Assuming the rest of the params are product attributes for now. We need to see if we can get the GQL endpoint to ingore unknown params.\n    const productAttributes = Object.entries(additionalParams)\n      .filter(([attribute]) => AttributeKey.safeParse(attribute).success)\n      .filter(([, values]) => values != null)\n      .map(([attribute, values]) => ({\n        attribute: attribute.replace('attr_', ''),\n        values,\n      }));\n\n    return {\n      after,\n      before,\n      limit,\n      sort,\n      filters: {\n        brandEntityIds: brand,\n        categoryEntityId: category,\n        categoryEntityIds: categoryIn,\n        hideOutOfStock: stock?.includes('in_stock'),\n        isFreeShipping: shipping?.includes('free_shipping'),\n        isFeatured,\n        price:\n          minPrice || maxPrice\n            ? {\n                maxPrice,\n                minPrice,\n              }\n            : undefined,\n        productAttributes,\n        rating:\n          minRating || maxRating\n            ? {\n                maxRating,\n                minRating,\n              }\n            : undefined,\n        searchTerm: term,\n      },\n    };\n  })\n  .pipe(PrivateSearchParamsSchema);\n\nexport const fetchFacetedSearch = cache(\n  // We need to make sure the reference passed into this function is the same if we want it to be memoized.\n  async (\n    params: z.input<typeof PublicSearchParamsSchema>,\n    currencyCode?: CurrencyCode,\n    customerAccessToken?: string,\n  ) => {\n    const { after, before, limit = 9, sort, filters } = PublicToPrivateParams.parse(params);\n\n    return getProductSearchResults(\n      {\n        after,\n        before,\n        limit,\n        sort,\n        filters,\n      },\n      currencyCode,\n      customerAccessToken,\n    );\n  },\n);\n"
  },
  {
    "path": "core/app/[locale]/(default)/(faceted)/search/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\n\nconst SearchPageQuery = graphql(`\n  query SearchPageQuery {\n    site {\n      settings {\n        inventory {\n          defaultOutOfStockMessage\n          showOutOfStockMessage\n          showBackorderMessage\n        }\n        storefront {\n          catalog {\n            productComparisonsEnabled\n          }\n        }\n        display {\n          showProductRating\n        }\n        reviews {\n          enabled\n        }\n      }\n    }\n  }\n`);\n\nexport const getSearchPageData = cache(async () => {\n  const response = await client.fetch({\n    document: SearchPageQuery,\n    fetchOptions: { next: { revalidate } },\n  });\n\n  return response.data.site;\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/(faceted)/search/page.tsx",
    "content": "import { Metadata } from 'next';\nimport { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\nimport { createLoader, SearchParams } from 'nuqs/server';\nimport { cache } from 'react';\n\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { createCompareLoader } from '@/vibes/soul/primitives/compare-drawer/loader';\nimport { ProductsListSection } from '@/vibes/soul/sections/products-list-section';\nimport { getFilterParsers } from '@/vibes/soul/sections/products-list-section/filter-parsers';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { facetsTransformer } from '~/data-transformers/facets-transformer';\nimport { pageInfoTransformer } from '~/data-transformers/page-info-transformer';\nimport { productCardTransformer } from '~/data-transformers/product-card-transformer';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\n\nimport { MAX_COMPARE_LIMIT } from '../../compare/page-data';\nimport { getCompareProducts as getCompareProductsData } from '../fetch-compare-products';\nimport { fetchFacetedSearch } from '../fetch-faceted-search';\n\nimport { getSearchPageData } from './page-data';\n\nconst compareLoader = createCompareLoader();\n\nconst createSearchSearchParamsLoader = cache(\n  async (searchParams: SearchParams, customerAccessToken?: string) => {\n    const searchTerm = typeof searchParams.term === 'string' ? searchParams.term : '';\n\n    if (!searchTerm) {\n      return null;\n    }\n\n    const search = await fetchFacetedSearch(searchParams, undefined, customerAccessToken);\n    const searchFacets = search.facets.items;\n    const transformedSearchFacets = await facetsTransformer({\n      refinedFacets: searchFacets,\n      allFacets: searchFacets,\n      searchParams: {},\n    });\n    const searchFilters = transformedSearchFacets.filter((facet) => facet != null);\n    const filterParsers = getFilterParsers(searchFilters);\n\n    // If there are no filters, return `null`, since calling `createLoader` with an empty\n    // object will throw the following cryptic error:\n    //\n    // ```\n    // Error: [nuqs] Empty search params cache. Search params can't be accessed in Layouts.\n    //   See https://err.47ng.com/NUQS-500\n    // ```\n    if (Object.keys(filterParsers).length === 0) {\n      return null;\n    }\n\n    return createLoader(filterParsers);\n  },\n);\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n  searchParams: Promise<SearchParams>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'Faceted.Search' });\n\n  return {\n    title: t('title'),\n  };\n}\n\nexport default async function Search(props: Props) {\n  const { locale } = await props.params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Faceted');\n\n  const { settings } = await getSearchPageData();\n\n  const showRating = Boolean(settings?.reviews.enabled && settings.display.showProductRating);\n\n  const productComparisonsEnabled =\n    settings?.storefront.catalog?.productComparisonsEnabled ?? false;\n\n  const streamableFacetedSearch = Streamable.from(async () => {\n    const searchParams = await props.searchParams;\n    const customerAccessToken = await getSessionCustomerAccessToken();\n    const currencyCode = await getPreferredCurrencyCode();\n\n    const loadSearchParams = await createSearchSearchParamsLoader(\n      searchParams,\n      customerAccessToken,\n    );\n    const parsedSearchParams = loadSearchParams?.(searchParams) ?? {};\n\n    const search = await fetchFacetedSearch(\n      {\n        ...searchParams,\n        ...parsedSearchParams,\n      },\n      currencyCode,\n      customerAccessToken,\n    );\n\n    return search;\n  });\n\n  const streamableProducts = Streamable.from(async () => {\n    const format = await getFormatter();\n\n    const searchParams = await props.searchParams;\n    const searchTerm = typeof searchParams.term === 'string' ? searchParams.term : '';\n\n    if (!searchTerm) {\n      return [];\n    }\n\n    const search = await streamableFacetedSearch;\n    const products = search.products.items;\n\n    const { defaultOutOfStockMessage, showOutOfStockMessage, showBackorderMessage } =\n      settings?.inventory ?? {};\n\n    return productCardTransformer(\n      products,\n      format,\n      showOutOfStockMessage ? defaultOutOfStockMessage : undefined,\n      showBackorderMessage,\n    );\n  });\n\n  const streamableTitle = Streamable.from(async () => {\n    const searchParams = await props.searchParams;\n    const searchTerm = typeof searchParams.term === 'string' ? searchParams.term : '';\n\n    return `${t('Search.searchResults')} \"${searchTerm}\"`;\n  });\n\n  const streamableTotalCount = Streamable.from(async () => {\n    const format = await getFormatter();\n    const searchParams = await props.searchParams;\n    const searchTerm = typeof searchParams.term === 'string' ? searchParams.term : '';\n\n    if (!searchTerm) {\n      return format.number(0);\n    }\n\n    const search = await streamableFacetedSearch;\n\n    return format.number(search.products.collectionInfo?.totalItems ?? 0);\n  });\n\n  const streamableEmptyStateTitle = Streamable.from(async () => {\n    const searchParams = await props.searchParams;\n    const searchTerm = typeof searchParams.term === 'string' ? searchParams.term : '';\n\n    return t('Search.Empty.title', { term: searchTerm });\n  });\n\n  const streamablePagination = Streamable.from(async () => {\n    const searchParams = await props.searchParams;\n    const searchTerm = typeof searchParams.term === 'string' ? searchParams.term : '';\n\n    if (!searchTerm) {\n      return {\n        startCursorParamName: 'before',\n        endCursorParamName: 'after',\n        endCursor: null,\n        startCursor: null,\n      };\n    }\n\n    const search = await streamableFacetedSearch;\n\n    return pageInfoTransformer(search.products.pageInfo);\n  });\n\n  const streamableFilters = Streamable.from(async () => {\n    const searchParams = await props.searchParams;\n    const searchTerm = typeof searchParams.term === 'string' ? searchParams.term : '';\n    const customerAccessToken = await getSessionCustomerAccessToken();\n\n    if (!searchTerm) {\n      return [];\n    }\n\n    const loadSearchParams = await createSearchSearchParamsLoader(\n      searchParams,\n      customerAccessToken,\n    );\n    const parsedSearchParams = loadSearchParams?.(searchParams) ?? {};\n    const categorySearch = await fetchFacetedSearch({}, undefined, customerAccessToken);\n    const refinedSearch = await streamableFacetedSearch;\n\n    const allFacets = categorySearch.facets.items.filter(\n      (facet) => facet.__typename !== 'CategorySearchFilter',\n    );\n    const refinedFacets = refinedSearch.facets.items.filter(\n      (facet) => facet.__typename !== 'CategorySearchFilter',\n    );\n\n    const transformedFacets = await facetsTransformer({\n      refinedFacets,\n      allFacets,\n      searchParams: { ...searchParams, ...parsedSearchParams },\n    });\n\n    return transformedFacets.filter((facet) => facet != null);\n  });\n\n  const streamableCompareProducts = Streamable.from(async () => {\n    const searchParams = await props.searchParams;\n    const customerAccessToken = await getSessionCustomerAccessToken();\n\n    if (!productComparisonsEnabled) {\n      return [];\n    }\n\n    const { compare } = compareLoader(searchParams);\n\n    const compareIds = { entityIds: compare ? compare.map((id: string) => Number(id)) : [] };\n\n    const products = await getCompareProductsData(compareIds, customerAccessToken);\n\n    return products.map((product) => ({\n      id: product.entityId.toString(),\n      title: product.name,\n      image: product.defaultImage\n        ? { src: product.defaultImage.url, alt: product.defaultImage.altText }\n        : undefined,\n      href: product.path,\n    }));\n  });\n\n  return (\n    <ProductsListSection\n      breadcrumbs={[\n        { label: t('Search.Breadcrumbs.home'), href: '/' },\n        { label: t('Search.Breadcrumbs.search'), href: `#` },\n      ]}\n      compareLabel={t('Compare.compare')}\n      compareProducts={streamableCompareProducts}\n      emptyStateSubtitle={t('Search.Empty.subtitle')}\n      emptyStateTitle={streamableEmptyStateTitle}\n      filterLabel={t('FacetedSearch.filters')}\n      filters={streamableFilters}\n      filtersPanelTitle={t('FacetedSearch.filters')}\n      maxCompareLimitMessage={t('Compare.maxCompareLimit')}\n      maxItems={MAX_COMPARE_LIMIT}\n      paginationInfo={streamablePagination}\n      products={streamableProducts}\n      rangeFilterApplyLabel={t('FacetedSearch.Range.apply')}\n      removeLabel={t('Compare.remove')}\n      resetFiltersLabel={t('FacetedSearch.resetFilters')}\n      showCompare={productComparisonsEnabled}\n      showRating={showRating}\n      sortDefaultValue=\"featured\"\n      sortLabel={t('SortBy.sortBy')}\n      sortOptions={[\n        { value: 'featured', label: t('SortBy.featuredItems') },\n        { value: 'newest', label: t('SortBy.newestItems') },\n        { value: 'best_selling', label: t('SortBy.bestSellingItems') },\n        { value: 'a_to_z', label: t('SortBy.aToZ') },\n        { value: 'z_to_a', label: t('SortBy.zToA') },\n        { value: 'best_reviewed', label: t('SortBy.byReview') },\n        { value: 'lowest_price', label: t('SortBy.priceAscending') },\n        { value: 'highest_price', label: t('SortBy.priceDescending') },\n        { value: 'relevance', label: t('SortBy.relevance') },\n      ]}\n      sortParamName=\"sort\"\n      title={streamableTitle}\n      totalCount={streamableTotalCount}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/[...rest]/page.tsx",
    "content": "import { notFound } from 'next/navigation';\n\nexport default function CatchAllPage() {\n  notFound();\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/_components/slideshow/index.tsx",
    "content": "import { useTranslations } from 'next-intl';\n\nimport { Slideshow as SlideshowSection } from '~/vibes/soul/sections/slideshow';\n\nimport SlideBg01 from './slide-bg-01.jpg';\nimport SlideBg02 from './slide-bg-02.jpg';\nimport SlideBg03 from './slide-bg-03.jpg';\n\nexport function Slideshow() {\n  const t = useTranslations('Home.Slideshow');\n\n  const slides = [\n    {\n      title: t('Slide01.title'),\n      image: {\n        src: SlideBg01.src,\n        alt: t('Slide01.alt'),\n        blurDataUrl: SlideBg01.blurDataURL,\n      },\n      description: t('Slide01.description'),\n      cta: {\n        href: '/shop-all',\n        label: t('Slide01.cta'),\n      },\n    },\n    {\n      title: t('Slide02.title'),\n      image: {\n        src: SlideBg02.src,\n        alt: t('Slide02.alt'),\n        blurDataUrl: SlideBg02.blurDataURL,\n      },\n      description: t('Slide02.description'),\n      cta: {\n        href: '/shop-all',\n        label: t('Slide02.cta'),\n      },\n    },\n    {\n      title: t('Slide03.title'),\n      image: {\n        src: SlideBg03.src,\n        alt: t('Slide03.alt'),\n        blurDataUrl: SlideBg03.blurDataURL,\n      },\n      description: t('Slide03.description'),\n      cta: {\n        href: '/shop-all',\n        label: t('Slide03.cta'),\n      },\n    },\n  ];\n\n  return <SlideshowSection slides={slides} />;\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/addresses/_actions/address-action.ts",
    "content": "import { SubmissionResult } from '@conform-to/react';\n\nimport { Field, FieldGroup } from '@/vibes/soul/form/dynamic-form/schema';\nimport { Address, DefaultAddressConfiguration } from '@/vibes/soul/sections/address-list-section';\n\nimport { createAddress } from './create-address';\nimport { deleteAddress } from './delete-address';\nimport { updateAddress } from './update-address';\n\nexport interface State {\n  addresses: Address[];\n  lastResult: SubmissionResult | null;\n  defaultAddress?: DefaultAddressConfiguration;\n}\n\nexport async function addressAction(\n  fields: Array<Field | FieldGroup<Field>>,\n  prevState: Awaited<State>,\n  formData: FormData,\n): Promise<State> {\n  'use server';\n\n  const intent = formData.get('intent');\n\n  switch (intent) {\n    case 'create': {\n      return await createAddress(fields, prevState, formData);\n    }\n\n    case 'update': {\n      return await updateAddress(fields, prevState, formData);\n    }\n\n    case 'delete': {\n      return await deleteAddress(prevState, formData);\n    }\n\n    default: {\n      return prevState;\n    }\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/addresses/_actions/create-address.ts",
    "content": "import { BigCommerceAPIError, BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { parseWithZod } from '@conform-to/zod';\nimport { revalidateTag } from 'next/cache';\nimport { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { Field, FieldGroup } from '@/vibes/soul/form/dynamic-form/schema';\nimport { schema } from '@/vibes/soul/sections/address-list-section/schema';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\nimport { FieldNameToFieldId } from '~/data-transformers/form-field-transformer/utils';\n\nimport { type State } from './address-action';\n\nconst AddCustomerAddressMutation = graphql(`\n  mutation AddCustomerAddressMutation($input: AddCustomerAddressInput!) {\n    customer {\n      addCustomerAddress(input: $input) {\n        errors {\n          ... on CustomerAddressCreationError {\n            message\n          }\n          ... on CustomerNotLoggedInError {\n            message\n          }\n          ... on ValidationError {\n            message\n            path\n          }\n        }\n        address {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\nconst stringToNumber = z.coerce.string().pipe(z.coerce.number());\n\nconst inputSchema = z.object({\n  firstName: z.string(),\n  lastName: z.string(),\n  address1: z.string(),\n  address2: z.string().optional(),\n  city: z.string(),\n  company: z.string().optional(),\n  countryCode: z.string(),\n  stateOrProvince: z.string().optional(),\n  phone: z.string().optional(),\n  postalCode: z.string().optional(),\n  formFields: z.object({\n    checkboxes: z.array(\n      z.object({\n        fieldEntityId: stringToNumber,\n        fieldValueEntityIds: z.array(stringToNumber),\n      }),\n    ),\n    multipleChoices: z.array(\n      z.object({\n        fieldEntityId: stringToNumber,\n        fieldValueEntityId: stringToNumber,\n      }),\n    ),\n    numbers: z.array(\n      z.object({\n        fieldEntityId: stringToNumber,\n        number: stringToNumber,\n      }),\n    ),\n    dates: z.array(\n      z.object({\n        fieldEntityId: stringToNumber,\n        date: z.string(),\n      }),\n    ),\n    passwords: z.array(\n      z.object({\n        fieldEntityId: stringToNumber,\n        password: z.string(),\n      }),\n    ),\n    multilineTexts: z.array(\n      z.object({\n        fieldEntityId: stringToNumber,\n        multilineText: z.string(),\n      }),\n    ),\n    texts: z.array(\n      z.object({\n        fieldEntityId: stringToNumber,\n        text: z.string(),\n      }),\n    ),\n  }),\n});\n\nfunction parseAddAddressInput(\n  value: Record<string, unknown>,\n  fields: Array<Field | FieldGroup<Field>>,\n): VariablesOf<typeof AddCustomerAddressMutation>['input'] {\n  const customFields = fields\n    .flatMap((f) => (Array.isArray(f) ? f : [f]))\n    .filter(\n      (field) =>\n        field.id &&\n        ![\n          String(FieldNameToFieldId.firstName),\n          String(FieldNameToFieldId.lastName),\n          String(FieldNameToFieldId.address1),\n          String(FieldNameToFieldId.address2),\n          String(FieldNameToFieldId.city),\n          String(FieldNameToFieldId.company),\n          String(FieldNameToFieldId.countryCode),\n          String(FieldNameToFieldId.stateOrProvince),\n          String(FieldNameToFieldId.phone),\n          String(FieldNameToFieldId.postalCode),\n        ].includes(field.id),\n    );\n  const mappedInput = {\n    firstName: value.firstName,\n    lastName: value.lastName,\n    address1: value.address1,\n    address2: value.address2,\n    city: value.city,\n    company: value.company,\n    countryCode: value.countryCode,\n    stateOrProvince: value.stateOrProvince,\n    phone: value.phone,\n    postalCode: value.postalCode,\n    formFields: {\n      checkboxes: customFields\n        .filter((field) => ['checkbox-group'].includes(field.type))\n        .filter((field) => Boolean(value[field.name]))\n        .map((field) => {\n          return {\n            fieldEntityId: field.id,\n            fieldValueEntityIds: Array.isArray(value[field.name])\n              ? value[field.name]\n              : [value[field.name]],\n          };\n        }),\n      multipleChoices: customFields\n        .filter((field) => ['radio-group', 'button-radio-group'].includes(field.type))\n        .filter((field) => Boolean(value[field.name]))\n        .map((field) => {\n          return {\n            fieldEntityId: field.id,\n            fieldValueEntityId: value[field.name],\n          };\n        }),\n      numbers: customFields\n        .filter((field) => ['number'].includes(field.type))\n        .filter((field) => Boolean(value[field.name]))\n        .map((field) => {\n          return {\n            fieldEntityId: field.id,\n            number: value[field.name],\n          };\n        }),\n      dates: customFields\n        .filter((field) => ['date'].includes(field.type))\n        .filter((field) => Boolean(value[field.name]))\n        .map((field) => {\n          return {\n            fieldEntityId: field.id,\n            date: new Date(String(value[field.name])).toISOString(),\n          };\n        }),\n      passwords: customFields\n        .filter((field) => ['password'].includes(field.type))\n        .filter((field) => Boolean(value[field.name]))\n        .map((field) => ({\n          fieldEntityId: field.id,\n          password: value[field.name],\n        })),\n      multilineTexts: customFields\n        .filter((field) => ['textarea'].includes(field.type))\n        .filter((field) => Boolean(value[field.name]))\n        .map((field) => ({\n          fieldEntityId: field.id,\n          multilineText: value[field.name],\n        })),\n      texts: customFields\n        .filter((field) => ['text'].includes(field.type))\n        .filter((field) => Boolean(value[field.name]))\n        .map((field) => ({\n          fieldEntityId: field.id,\n          text: value[field.name],\n        })),\n    },\n  };\n\n  return inputSchema.parse(mappedInput);\n}\n\nexport async function createAddress(\n  fields: Array<Field | FieldGroup<Field>>,\n  prevState: Awaited<State>,\n  formData: FormData,\n): Promise<State> {\n  const t = await getTranslations('Account.Addresses');\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const submission = parseWithZod(formData, { schema });\n\n  if (submission.status !== 'success') {\n    return {\n      ...prevState,\n      lastResult: submission.reply(),\n    };\n  }\n\n  try {\n    const input = parseAddAddressInput(submission.value, fields);\n\n    const response = await client.fetch({\n      document: AddCustomerAddressMutation,\n      customerAccessToken,\n      fetchOptions: { cache: 'no-store' },\n      variables: {\n        input,\n      },\n    });\n\n    const result = response.data.customer.addCustomerAddress;\n\n    if (result.errors.length > 0) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: result.errors.map((error) => error.message) }),\n      };\n    }\n\n    revalidateTag(TAGS.customer, { expire: 0 });\n\n    return {\n      addresses: [\n        ...prevState.addresses,\n        {\n          ...submission.value,\n          id: String(result.address?.entityId),\n        },\n      ],\n      lastResult: submission.reply({ resetForm: true }),\n      defaultAddress: prevState.defaultAddress,\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => message),\n        }),\n      };\n    }\n\n    if (error instanceof BigCommerceAPIError) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: [error.message] }),\n      };\n    }\n\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: [t('somethingWentWrong')] }),\n    };\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/addresses/_actions/delete-address.ts",
    "content": "import { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { parseWithZod } from '@conform-to/zod';\nimport { revalidateTag } from 'next/cache';\nimport { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { schema } from '@/vibes/soul/sections/address-list-section/schema';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\n\nimport { type State } from './address-action';\n\nconst DeleteCustomerAddressMutation = graphql(`\n  mutation DeleteCustomerAddressMutation($input: DeleteCustomerAddressInput!) {\n    customer {\n      deleteCustomerAddress(input: $input) {\n        errors {\n          __typename\n          ... on CustomerAddressDeletionError {\n            message\n          }\n          ... on CustomerNotLoggedInError {\n            message\n          }\n        }\n      }\n    }\n  }\n`);\n\nconst stringToNumber = z.coerce.string().pipe(z.coerce.number());\n\nconst inputSchema = z.object({\n  addressEntityId: stringToNumber,\n});\n\nfunction parseDeleteAddressInput(\n  value: Record<string, unknown>,\n): VariablesOf<typeof DeleteCustomerAddressMutation>['input'] {\n  return inputSchema.parse({\n    addressEntityId: value.id,\n  });\n}\n\nexport async function deleteAddress(prevState: Awaited<State>, formData: FormData): Promise<State> {\n  const t = await getTranslations('Account.Addresses');\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const submission = parseWithZod(formData, { schema });\n\n  if (submission.status !== 'success') {\n    return {\n      ...prevState,\n      lastResult: submission.reply(),\n    };\n  }\n\n  try {\n    const input = parseDeleteAddressInput(submission.value);\n\n    const response = await client.fetch({\n      document: DeleteCustomerAddressMutation,\n      customerAccessToken,\n      fetchOptions: { cache: 'no-store' },\n      variables: {\n        input,\n      },\n    });\n\n    const result = response.data.customer.deleteCustomerAddress;\n\n    if (result.errors.length > 0) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: result.errors.map((error) => error.message) }),\n      };\n    }\n\n    revalidateTag(TAGS.customer, { expire: 0 });\n\n    return {\n      addresses: prevState.addresses.filter(\n        (address) => address.id !== String(submission.value.id),\n      ),\n      lastResult: submission.reply({ resetForm: true }),\n      defaultAddress: prevState.defaultAddress,\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => message),\n        }),\n      };\n    }\n\n    if (error instanceof Error) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: [error.message] }),\n      };\n    }\n\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: [t('somethingWentWrong')] }),\n    };\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/addresses/_actions/update-address.ts",
    "content": "import { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { parseWithZod } from '@conform-to/zod';\nimport { revalidateTag } from 'next/cache';\nimport { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { Field, FieldGroup } from '@/vibes/soul/form/dynamic-form/schema';\nimport { schema } from '@/vibes/soul/sections/address-list-section/schema';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\nimport { FieldNameToFieldId } from '~/data-transformers/form-field-transformer/utils';\n\nimport { type State } from './address-action';\n\nconst UpdateCustomerAddressMutation = graphql(`\n  mutation UpdateCustomerAddressMutation($input: UpdateCustomerAddressInput!) {\n    customer {\n      updateCustomerAddress(input: $input) {\n        errors {\n          __typename\n          ... on AddressDoesNotExistError {\n            message\n          }\n          ... on CustomerAddressUpdateError {\n            message\n          }\n          ... on CustomerNotLoggedInError {\n            message\n          }\n          ... on ValidationError {\n            message\n            path\n          }\n        }\n        address {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\nconst stringToNumber = z.coerce.string().pipe(z.coerce.number());\n\nconst inputSchema = z.object({\n  addressEntityId: stringToNumber,\n  data: z.object({\n    firstName: z.string().optional(),\n    lastName: z.string().optional(),\n    address1: z.string().optional(),\n    address2: z.string().optional(),\n    city: z.string().optional(),\n    company: z.string().optional(),\n    countryCode: z.string().optional(),\n    stateOrProvince: z.string().optional(),\n    phone: z.string().optional(),\n    postalCode: z.string().optional(),\n    formFields: z\n      .object({\n        checkboxes: z.array(\n          z.object({\n            fieldEntityId: stringToNumber,\n            fieldValueEntityIds: z.array(stringToNumber),\n          }),\n        ),\n        multipleChoices: z.array(\n          z.object({\n            fieldEntityId: stringToNumber,\n            fieldValueEntityId: stringToNumber,\n          }),\n        ),\n        numbers: z.array(\n          z.object({\n            fieldEntityId: stringToNumber,\n            number: stringToNumber,\n          }),\n        ),\n        dates: z.array(\n          z.object({\n            fieldEntityId: stringToNumber,\n            date: z.string(),\n          }),\n        ),\n        passwords: z.array(\n          z.object({\n            fieldEntityId: stringToNumber,\n            password: z.string(),\n          }),\n        ),\n        multilineTexts: z.array(\n          z.object({\n            fieldEntityId: stringToNumber,\n            multilineText: z.string(),\n          }),\n        ),\n        texts: z.array(\n          z.object({\n            fieldEntityId: stringToNumber,\n            text: z.string(),\n          }),\n        ),\n      })\n      .optional(),\n  }),\n});\n\nfunction parseUpdateAddressInput(\n  value: Record<string, unknown>,\n  fields: Array<Field | FieldGroup<Field>>,\n): VariablesOf<typeof UpdateCustomerAddressMutation>['input'] {\n  const customFields = fields\n    .flatMap((f) => (Array.isArray(f) ? f : [f]))\n    .filter((field) => Boolean(value[field.name]))\n    .filter(\n      (field) =>\n        field.id &&\n        ![\n          String(FieldNameToFieldId.firstName),\n          String(FieldNameToFieldId.lastName),\n          String(FieldNameToFieldId.address1),\n          String(FieldNameToFieldId.address2),\n          String(FieldNameToFieldId.city),\n          String(FieldNameToFieldId.company),\n          String(FieldNameToFieldId.countryCode),\n          String(FieldNameToFieldId.stateOrProvince),\n          String(FieldNameToFieldId.phone),\n          String(FieldNameToFieldId.postalCode),\n        ].includes(field.id),\n    );\n  const mappedInput = {\n    addressEntityId: value.id,\n    data: {\n      firstName: value.firstName,\n      lastName: value.lastName,\n      address1: value.address1,\n      address2: value.address2,\n      city: value.city,\n      company: value.company,\n      countryCode: value.countryCode,\n      stateOrProvince: value.stateOrProvince,\n      phone: value.phone,\n      postalCode: value.postalCode,\n      formFields: {\n        checkboxes: customFields\n          .filter((field) => ['checkbox-group'].includes(field.type))\n          .filter((field) => Boolean(value[field.name]))\n          .map((field) => {\n            return {\n              fieldEntityId: field.id,\n              fieldValueEntityIds: Array.isArray(value[field.name])\n                ? value[field.name]\n                : [value[field.name]],\n            };\n          }),\n        multipleChoices: customFields\n          .filter((field) => ['radio-group', 'button-radio-group'].includes(field.type))\n          .filter((field) => Boolean(value[field.name]))\n          .map((field) => {\n            return {\n              fieldEntityId: field.id,\n              fieldValueEntityId: value[field.name],\n            };\n          }),\n        numbers: customFields\n          .filter((field) => ['number'].includes(field.type))\n          .filter((field) => Boolean(value[field.name]))\n          .map((field) => {\n            return {\n              fieldEntityId: field.id,\n              number: value[field.name],\n            };\n          }),\n        dates: customFields\n          .filter((field) => ['date'].includes(field.type))\n          .filter((field) => Boolean(value[field.name]))\n          .map((field) => {\n            return {\n              fieldEntityId: field.id,\n              date: new Date(String(value[field.name])).toISOString(),\n            };\n          }),\n        passwords: customFields\n          .filter((field) => ['password'].includes(field.type))\n          .filter((field) => Boolean(value[field.name]))\n          .map((field) => ({\n            fieldEntityId: field.id,\n            password: value[field.name],\n          })),\n        multilineTexts: customFields\n          .filter((field) => ['textarea'].includes(field.type))\n          .filter((field) => Boolean(value[field.name]))\n          .map((field) => ({\n            fieldEntityId: field.id,\n            multilineText: value[field.name],\n          })),\n        texts: customFields\n          .filter((field) => ['text'].includes(field.type))\n          .filter((field) => Boolean(value[field.name]))\n          .map((field) => ({\n            fieldEntityId: field.id,\n            text: value[field.name],\n          })),\n      },\n    },\n  };\n\n  return inputSchema.parse(mappedInput);\n}\n\nexport async function updateAddress(\n  fields: Array<Field | FieldGroup<Field>>,\n  prevState: Awaited<State>,\n  formData: FormData,\n): Promise<State> {\n  const t = await getTranslations('Account.Addresses');\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const submission = parseWithZod(formData, { schema });\n\n  if (submission.status !== 'success') {\n    return {\n      ...prevState,\n      lastResult: submission.reply(),\n    };\n  }\n\n  try {\n    const input = parseUpdateAddressInput(submission.value, fields);\n\n    const response = await client.fetch({\n      document: UpdateCustomerAddressMutation,\n      customerAccessToken,\n      fetchOptions: { cache: 'no-store' },\n      variables: {\n        input,\n      },\n    });\n\n    const result = response.data.customer.updateCustomerAddress;\n\n    if (result.errors.length > 0) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: result.errors.map((error) => error.message) }),\n      };\n    }\n\n    revalidateTag(TAGS.customer, { expire: 0 });\n\n    return {\n      addresses: prevState.addresses.map((address) =>\n        address.id === submission.value.id ? submission.value : address,\n      ),\n      lastResult: submission.reply({ resetForm: true }),\n      defaultAddress: prevState.defaultAddress,\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => message),\n        }),\n      };\n    }\n\n    if (error instanceof Error) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: [error.message] }),\n      };\n    }\n\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: [t('somethingWentWrong')] }),\n    };\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/addresses/page-data.ts",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { cache } from 'react';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { PaginationFragment } from '~/client/fragments/pagination';\nimport { graphql } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\nimport {\n  FormFieldsFragment,\n  FormFieldValuesFragment,\n} from '~/data-transformers/form-field-transformer/fragment';\n\nconst GetCustomerAddressesQuery = graphql(\n  `\n    query GetCustomerAddressesQuery($after: String, $before: String, $first: Int, $last: Int) {\n      customer {\n        entityId\n        addresses(before: $before, after: $after, first: $first, last: $last) {\n          pageInfo {\n            ...PaginationFragment\n          }\n          collectionInfo {\n            totalItems\n          }\n          edges {\n            node {\n              entityId\n              firstName\n              lastName\n              address1\n              address2\n              city\n              stateOrProvince\n              countryCode\n              phone\n              postalCode\n              company\n              formFields {\n                ...FormFieldValuesFragment\n              }\n            }\n          }\n        }\n      }\n      site {\n        settings {\n          formFields {\n            shippingAddress {\n              ...FormFieldsFragment\n            }\n          }\n        }\n      }\n      geography {\n        countries {\n          code\n          name\n        }\n      }\n    }\n  `,\n  [PaginationFragment, FormFieldValuesFragment, FormFieldsFragment],\n);\n\ninterface Pagination {\n  after?: string;\n  before?: string;\n  limit?: number;\n}\n\nexport const getCustomerAddresses = cache(\n  async ({ before = '', after = '', limit = 10 }: Pagination) => {\n    const customerAccessToken = await getSessionCustomerAccessToken();\n    const paginationArgs = before ? { last: limit, before } : { first: limit, after };\n\n    const response = await client.fetch({\n      document: GetCustomerAddressesQuery,\n      variables: { ...paginationArgs },\n      customerAccessToken,\n      fetchOptions: { cache: 'no-store', next: { tags: [TAGS.customer] } },\n    });\n\n    const addresses = response.data.customer?.addresses;\n\n    if (!addresses) {\n      return undefined;\n    }\n\n    return {\n      pageInfo: addresses.pageInfo,\n      totalAddresses: addresses.collectionInfo?.totalItems ?? 0,\n      addresses: removeEdgesAndNodes({ edges: addresses.edges }),\n      shippingAddressFields: response.data.site.settings?.formFields.shippingAddress,\n      countries: response.data.geography.countries,\n    };\n  },\n);\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/addresses/page.tsx",
    "content": "import { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { getTranslations, setRequestLocale } from 'next-intl/server';\n\nimport { Address, AddressListSection } from '@/vibes/soul/sections/address-list-section';\nimport {\n  formFieldTransformer,\n  injectCountryCodeOptions,\n} from '~/data-transformers/form-field-transformer';\nimport {\n  ADDRESS_FORM_LAYOUT,\n  mapFormFieldValueToName,\n  transformFieldsToLayout,\n} from '~/data-transformers/form-field-transformer/utils';\nimport { exists } from '~/lib/utils';\n\nimport { addressAction } from './_actions/address-action';\nimport { getCustomerAddresses } from './page-data';\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n  searchParams: Promise<{\n    [key: string]: string | string[] | undefined;\n    before?: string;\n    after?: string;\n  }>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'Account.Addresses' });\n\n  return {\n    title: t('title'),\n  };\n}\n\nexport default async function Addresses({ params, searchParams }: Props) {\n  const { locale } = await params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Account.Addresses');\n  const { before, after } = await searchParams;\n\n  const data = await getCustomerAddresses({\n    ...(after && { after }),\n    ...(before && { before }),\n  });\n\n  if (!data) {\n    notFound();\n  }\n\n  const { shippingAddressFields = [], countries } = data;\n\n  const addresses = data.addresses.map<Address>((address) => ({\n    id: address.entityId.toString(),\n    firstName: address.firstName,\n    lastName: address.lastName,\n    address1: address.address1,\n    address2: address.address2 ?? undefined,\n    city: address.city,\n    stateOrProvince: address.stateOrProvince ?? undefined,\n    countryCode: address.countryCode,\n    postalCode: address.postalCode ?? undefined,\n    phone: address.phone ?? undefined,\n    company: address.company ?? undefined,\n    ...address.formFields.reduce((acc, field) => {\n      return {\n        ...acc,\n        ...mapFormFieldValueToName(field),\n      };\n    }, {}),\n  }));\n\n  const fields = transformFieldsToLayout(shippingAddressFields, ADDRESS_FORM_LAYOUT)\n    .map((field) => {\n      if (Array.isArray(field)) {\n        return field.map(formFieldTransformer).filter(exists);\n      }\n\n      return formFieldTransformer(field);\n    })\n    .filter(exists)\n    .map((field) => {\n      if (Array.isArray(field)) {\n        return field.map((f) => injectCountryCodeOptions(f, countries ?? []));\n      }\n\n      return injectCountryCodeOptions(field, countries ?? []);\n    })\n    .filter(exists);\n\n  return (\n    <AddressListSection\n      addressAction={addressAction}\n      addresses={addresses}\n      cancelLabel={t('cancel')}\n      createLabel={t('create')}\n      deleteLabel={t('delete')}\n      editLabel={t('edit')}\n      emptyStateTitle={t('EmptyState.title')}\n      fields={[...fields, { name: 'id', type: 'hidden', label: 'ID' }]}\n      minimumAddressCount={0}\n      setDefaultLabel={t('setDefault')}\n      showAddFormLabel={t('cta')}\n      title={t('title')}\n      updateLabel={t('update')}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/layout.tsx",
    "content": "import { getTranslations, setRequestLocale } from 'next-intl/server';\nimport { PropsWithChildren } from 'react';\n\nimport { SidebarMenu } from '@/vibes/soul/sections/sidebar-menu';\nimport { StickySidebarLayout } from '@/vibes/soul/sections/sticky-sidebar-layout';\n\ninterface Props extends PropsWithChildren {\n  params: Promise<{ locale: string }>;\n}\n\nexport default async function Layout({ children, params }: Props) {\n  const { locale } = await params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Account.Layout');\n\n  return (\n    <StickySidebarLayout\n      sidebar={\n        <SidebarMenu\n          links={[\n            { href: '/account/orders/', label: t('orders') },\n            { href: '/account/addresses/', label: t('addresses') },\n            { href: '/account/settings/', label: t('settings') },\n            { href: '/account/wishlists/', label: t('wishlists') },\n            { href: '/logout', label: t('logout'), prefetch: 'none' },\n          ]}\n        />\n      }\n      sidebarSize=\"small\"\n    >\n      {children}\n    </StickySidebarLayout>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/orders/[id]/page-data.tsx",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { cache } from 'react';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\n\nimport { OrderGiftCertificateItemFragment, OrderItemFragment } from '../fragment';\n\nconst CustomerOrderDetails = graphql(\n  `\n    query CustomerOrderDetails($filter: OrderFilterInput) {\n      site {\n        order(filter: $filter) {\n          entityId\n          orderedAt {\n            utc\n          }\n          status {\n            label\n            value\n          }\n          totalIncTax {\n            value\n            currencyCode\n          }\n          subTotal {\n            value\n            currencyCode\n          }\n          discounts {\n            nonCouponDiscountTotal {\n              value\n              currencyCode\n            }\n            couponDiscounts {\n              couponCode\n              discountedAmount {\n                value\n                currencyCode\n              }\n            }\n          }\n          shippingCostTotal {\n            value\n            currencyCode\n          }\n          taxTotal {\n            value\n            currencyCode\n          }\n          billingAddress {\n            firstName\n            lastName\n            address1\n            city\n            stateOrProvince\n            postalCode\n            country\n          }\n          payments {\n            edges {\n              node {\n                paymentMethodId\n                paymentMethodName\n                detail {\n                  __typename\n                  ... on CreditCardPaymentInstrument {\n                    brand\n                    last4\n                  }\n                  ... on GiftCertificatePaymentInstrument {\n                    code\n                  }\n                }\n                amount {\n                  value\n                  currencyCode\n                }\n              }\n            }\n          }\n          consignments {\n            shipping {\n              edges {\n                node {\n                  entityId\n                  shippingAddress {\n                    firstName\n                    lastName\n                    address1\n                    address2\n                    city\n                    stateOrProvince\n                    postalCode\n                    country\n                  }\n                  shipments {\n                    edges {\n                      node {\n                        entityId\n                        shippedAt {\n                          utc\n                        }\n                        shippingMethodName\n                        shippingProviderName\n                        tracking {\n                          __typename\n                          ... on OrderShipmentNumberAndUrlTracking {\n                            number\n                            url\n                          }\n                          ... on OrderShipmentUrlOnlyTracking {\n                            url\n                          }\n                          ... on OrderShipmentNumberOnlyTracking {\n                            number\n                          }\n                        }\n                      }\n                    }\n                  }\n                  lineItems {\n                    edges {\n                      node {\n                        ...OrderItemFragment\n                      }\n                    }\n                  }\n                }\n              }\n            }\n            email {\n              giftCertificates {\n                edges {\n                  node {\n                    recipientEmail\n                    lineItems {\n                      edges {\n                        node {\n                          ...OrderGiftCertificateItemFragment\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  `,\n  [OrderItemFragment, OrderGiftCertificateItemFragment],\n);\n\nexport const getCustomerOrderDetails = cache(async (id: number) => {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const response = await client.fetch({\n    document: CustomerOrderDetails,\n    variables: {\n      filter: {\n        entityId: id,\n      },\n    },\n    fetchOptions: { cache: 'no-store', next: { tags: [TAGS.customer] } },\n    customerAccessToken,\n    errorPolicy: 'auth',\n  });\n\n  const order = response.data.site.order;\n\n  if (!order) {\n    return undefined;\n  }\n\n  return {\n    ...order,\n    consignments: {\n      shipping:\n        order.consignments?.shipping &&\n        removeEdgesAndNodes(order.consignments.shipping).map((consignment) => {\n          return {\n            ...consignment,\n            lineItems: removeEdgesAndNodes(consignment.lineItems),\n            shipments: removeEdgesAndNodes(consignment.shipments),\n          };\n        }),\n      email:\n        order.consignments?.email &&\n        removeEdgesAndNodes(order.consignments.email.giftCertificates).map(\n          ({ recipientEmail, lineItems }) => {\n            return {\n              email: recipientEmail,\n              lineItems: removeEdgesAndNodes(lineItems).map(({ entityId, name, salePrice }) => ({\n                entityId,\n                name,\n                salePrice,\n              })),\n            };\n          },\n        ),\n    },\n  };\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/orders/[id]/page.tsx",
    "content": "import { notFound } from 'next/navigation';\nimport { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\n\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { OrderDetailsSection } from '@/vibes/soul/sections/order-details-section';\nimport { orderDetailsTransformer } from '~/data-transformers/order-details-transformer';\n\nimport { getCustomerOrderDetails } from './page-data';\n\ninterface Props {\n  params: Promise<{\n    id: string;\n    locale: string;\n  }>;\n}\n\nexport default async function OrderDetails(props: Props) {\n  const { id, locale } = await props.params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Account.Orders.Details');\n  const format = await getFormatter();\n\n  const streamableOrder = Streamable.from(async () => {\n    const order = await getCustomerOrderDetails(Number(id));\n\n    if (!order) {\n      notFound();\n    }\n\n    return orderDetailsTransformer(order, t, format);\n  });\n\n  return (\n    <OrderDetailsSection\n      order={streamableOrder}\n      orderSummaryLabel={t('orderSummary')}\n      prevHref=\"/account/orders\"\n      shipmentAddressLabel={t('shippingAddress')}\n      shipmentMethodLabel={t('shippingMethod')}\n      summaryTotalLabel={t('summaryTotal')}\n      title={t('title', { orderNumber: id })}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/orders/fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nexport const OrderItemFragment = graphql(`\n  fragment OrderItemFragment on OrderPhysicalLineItem {\n    entityId\n    productEntityId\n    brand\n    name\n    quantity\n    baseCatalogProduct {\n      path\n    }\n    image {\n      url: urlTemplate(lossy: true)\n      altText\n    }\n    subTotalListPrice {\n      value\n      currencyCode\n    }\n    catalogProductWithOptionSelections {\n      prices {\n        price {\n          value\n          currencyCode\n        }\n      }\n    }\n    productOptions {\n      __typename\n      name\n      value\n    }\n  }\n`);\n\nexport const OrderGiftCertificateItemFragment = graphql(`\n  fragment OrderGiftCertificateItemFragment on OrderGiftCertificateLineItem {\n    entityId\n    name\n    salePrice {\n      value\n      formattedV2\n      currencyCode\n    }\n  }\n`);\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/orders/page-data.ts",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { cache } from 'react';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { PaginationFragment } from '~/client/fragments/pagination';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\n\nimport { OrderGiftCertificateItemFragment, OrderItemFragment } from './fragment';\n\nconst CustomerAllOrders = graphql(\n  `\n    query CustomerAllOrders(\n      $after: String\n      $before: String\n      $first: Int\n      $last: Int\n      $filters: OrdersFiltersInput\n    ) {\n      customer {\n        orders(after: $after, before: $before, first: $first, last: $last, filters: $filters) {\n          pageInfo {\n            ...PaginationFragment\n          }\n          edges {\n            node {\n              entityId\n              orderedAt {\n                utc\n              }\n              status {\n                label\n                value\n              }\n              totalIncTax {\n                value\n                currencyCode\n              }\n              consignments {\n                shipping {\n                  edges {\n                    node {\n                      lineItems {\n                        edges {\n                          node {\n                            ...OrderItemFragment\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n                email {\n                  giftCertificates {\n                    edges {\n                      node {\n                        lineItems {\n                          edges {\n                            node {\n                              ...OrderGiftCertificateItemFragment\n                            }\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  `,\n  [OrderItemFragment, OrderGiftCertificateItemFragment, PaginationFragment],\n);\n\ntype OrdersFiltersInput = VariablesOf<typeof CustomerAllOrders>['filters'];\ntype OrderStatus = NonNullable<OrdersFiltersInput>['status'];\ntype OrderDateRange = NonNullable<OrdersFiltersInput>['dateRange'];\n\ninterface CustomerOrdersArgs {\n  after?: string;\n  before?: string;\n  limit?: number;\n  filterByStatus?: OrderStatus;\n  filterByDateRange?: OrderDateRange;\n}\n\nexport const getCustomerOrders = cache(\n  async ({\n    before = '',\n    after = '',\n    filterByStatus,\n    filterByDateRange,\n    limit = 5,\n  }: CustomerOrdersArgs) => {\n    const customerAccessToken = await getSessionCustomerAccessToken();\n    const paginationArgs = before ? { last: limit, before } : { first: limit, after };\n    const filtersArgs = {\n      filters: {\n        ...(filterByDateRange && { dateRange: filterByDateRange }),\n        ...(filterByStatus && { status: filterByStatus }),\n      },\n    };\n    const response = await client.fetch({\n      document: CustomerAllOrders,\n      variables: { ...paginationArgs, ...filtersArgs },\n      customerAccessToken,\n      fetchOptions: { cache: 'no-store', next: { tags: [TAGS.customer] } },\n      errorPolicy: 'auth',\n    });\n\n    const orders = response.data.customer?.orders;\n\n    if (!orders) {\n      return undefined;\n    }\n\n    const data = {\n      orders: removeEdgesAndNodes(orders).map((order) => {\n        return {\n          ...order,\n          consignments: {\n            shipping:\n              order.consignments?.shipping &&\n              removeEdgesAndNodes(order.consignments.shipping).map((consignment) => {\n                return {\n                  ...consignment,\n                  lineItems: removeEdgesAndNodes(consignment.lineItems),\n                };\n              }),\n            email:\n              order.consignments?.email &&\n              removeEdgesAndNodes(order.consignments.email.giftCertificates).map(\n                ({ lineItems }) => {\n                  return {\n                    lineItems: removeEdgesAndNodes(lineItems).map(\n                      ({ entityId, name, salePrice }) => ({\n                        entityId,\n                        name,\n                        salePrice,\n                      }),\n                    ),\n                  };\n                },\n              ),\n          },\n        };\n      }),\n      pageInfo: orders.pageInfo,\n    };\n\n    return data;\n  },\n);\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/orders/page.tsx",
    "content": "import { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\n\nimport { Order, OrderList } from '@/vibes/soul/sections/order-list';\nimport { ordersTransformer } from '~/data-transformers/orders-transformer';\nimport { defaultPageInfo, pageInfoTransformer } from '~/data-transformers/page-info-transformer';\n\nimport { getCustomerOrders } from './page-data';\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n  searchParams: Promise<{\n    [key: string]: string | string[] | undefined;\n    before?: string;\n    after?: string;\n  }>;\n}\n\nasync function getOrders(after?: string, before?: string): Promise<Order[]> {\n  const format = await getFormatter();\n  const customerOrdersDetails = await getCustomerOrders({\n    ...(after && { after }),\n    ...(before && { before }),\n  });\n\n  if (!customerOrdersDetails) {\n    return [];\n  }\n\n  const { orders } = customerOrdersDetails;\n\n  return ordersTransformer(orders, format);\n}\n\nasync function getPaginationInfo(after?: string, before?: string) {\n  const customerOrdersDetails = await getCustomerOrders({\n    ...(after && { after }),\n    ...(before && { before }),\n  });\n\n  return pageInfoTransformer(customerOrdersDetails?.pageInfo ?? defaultPageInfo);\n}\n\nexport default async function Orders({ params, searchParams }: Props) {\n  const { locale } = await params;\n\n  setRequestLocale(locale);\n\n  const { before, after } = await searchParams;\n  const t = await getTranslations('Account.Orders');\n\n  return (\n    <OrderList\n      emptyStateActionLabel={t('EmptyState.cta')}\n      emptyStateTitle={t('EmptyState.title')}\n      orderNumberLabel={t('orderNumber')}\n      orders={getOrders(after, before)}\n      paginationInfo={getPaginationInfo(after, before)}\n      title={t('title')}\n      totalLabel={t('totalPrice')}\n      viewDetailsLabel={t('viewDetails')}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/settings/_actions/change-password.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { ChangePasswordAction } from '@/vibes/soul/sections/account-settings/change-password-form';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\n\nconst CustomerChangePasswordMutation = graphql(`\n  mutation CustomerChangePasswordMutation($input: ChangePasswordInput!) {\n    customer {\n      changePassword(input: $input) {\n        errors {\n          ... on ValidationError {\n            message\n            path\n          }\n          ... on CustomerDoesNotExistError {\n            message\n          }\n          ... on CustomerPasswordError {\n            message\n          }\n          ... on CustomerNotLoggedInError {\n            message\n          }\n        }\n      }\n    }\n  }\n`);\n\nconst schema = z.object({\n  currentPassword: z.string().trim(),\n  password: z.string(),\n});\n\nexport const changePassword: ChangePasswordAction = async (prevState, formData) => {\n  const t = await getTranslations('Account.Settings');\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  const submission = parseWithZod(formData, { schema });\n\n  if (submission.status !== 'success') {\n    return { lastResult: submission.reply() };\n  }\n\n  const input = {\n    currentPassword: submission.value.currentPassword,\n    newPassword: submission.value.password,\n  };\n\n  try {\n    const response = await client.fetch({\n      document: CustomerChangePasswordMutation,\n      variables: {\n        input,\n      },\n      customerAccessToken,\n    });\n\n    const result = response.data.customer.changePassword;\n\n    if (result.errors.length > 0) {\n      return {\n        lastResult: submission.reply({ formErrors: result.errors.map((error) => error.message) }),\n      };\n    }\n\n    return {\n      lastResult: submission.reply(),\n      successMessage: t('passwordUpdated'),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => message),\n        }),\n      };\n    }\n\n    if (error instanceof Error) {\n      return {\n        lastResult: submission.reply({ formErrors: [error.message] }),\n      };\n    }\n\n    return { lastResult: submission.reply({ formErrors: [t('somethingWentWrong')] }) };\n  }\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/settings/_actions/update-customer.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { parseWithZod } from '@conform-to/zod';\nimport { revalidateTag } from 'next/cache';\nimport { getTranslations } from 'next-intl/server';\n\nimport { updateAccountSchema } from '@/vibes/soul/sections/account-settings/schema';\nimport { UpdateAccountAction } from '@/vibes/soul/sections/account-settings/update-account-form';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\n\nconst UpdateCustomerMutation = graphql(`\n  mutation UpdateCustomerMutation($input: UpdateCustomerInput!) {\n    customer {\n      updateCustomer(input: $input) {\n        customer {\n          firstName\n          lastName\n        }\n        errors {\n          __typename\n          ... on UnexpectedUpdateCustomerError {\n            message\n          }\n          ... on EmailAlreadyInUseError {\n            message\n          }\n          ... on ValidationError {\n            message\n          }\n          ... on CustomerDoesNotExistError {\n            message\n          }\n          ... on CustomerNotLoggedInError {\n            message\n          }\n        }\n      }\n    }\n  }\n`);\n\nexport const updateCustomer: UpdateAccountAction = async (prevState, formData) => {\n  const t = await getTranslations('Account.Settings');\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const submission = parseWithZod(formData, { schema: updateAccountSchema });\n\n  if (submission.status !== 'success') {\n    return {\n      account: prevState.account,\n      lastResult: submission.reply(),\n    };\n  }\n\n  try {\n    const response = await client.fetch({\n      document: UpdateCustomerMutation,\n      customerAccessToken,\n      variables: {\n        input: submission.value,\n      },\n      fetchOptions: { cache: 'no-store' },\n    });\n\n    const result = response.data.customer.updateCustomer;\n\n    if (result.errors.length > 0) {\n      return {\n        account: prevState.account,\n        lastResult: submission.reply({ formErrors: result.errors.map((error) => error.message) }),\n      };\n    }\n\n    revalidateTag(TAGS.customer, { expire: 0 });\n\n    return {\n      account: submission.value,\n      successMessage: t('settingsUpdated'),\n      lastResult: submission.reply(),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        account: prevState.account,\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => message),\n        }),\n      };\n    }\n\n    if (error instanceof Error) {\n      return {\n        account: prevState.account,\n        lastResult: submission.reply({ formErrors: [error.message] }),\n      };\n    }\n\n    return {\n      account: prevState.account,\n      lastResult: submission.reply({ formErrors: [t('somethingWentWrong')] }),\n    };\n  }\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/settings/_actions/update-newsletter-subscription.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { revalidateTag } from 'next/cache';\nimport { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\n\nconst updateNewsletterSubscriptionSchema = z.object({\n  intent: z.enum(['subscribe', 'unsubscribe']),\n});\n\nconst SubscribeToNewsletterMutation = graphql(`\n  mutation SubscribeToNewsletterMutation($input: CreateSubscriberInput!) {\n    newsletter {\n      subscribe(input: $input) {\n        errors {\n          __typename\n          ... on CreateSubscriberAlreadyExistsError {\n            message\n          }\n          ... on CreateSubscriberEmailInvalidError {\n            message\n          }\n          ... on CreateSubscriberUnexpectedError {\n            message\n          }\n          ... on CreateSubscriberLastNameInvalidError {\n            message\n          }\n          ... on CreateSubscriberFirstNameInvalidError {\n            message\n          }\n        }\n      }\n    }\n  }\n`);\n\nconst UnsubscribeFromNewsletterMutation = graphql(`\n  mutation UnsubscribeFromNewsletterMutation($input: RemoveSubscriberInput!) {\n    newsletter {\n      unsubscribe(input: $input) {\n        errors {\n          __typename\n          ... on RemoveSubscriberEmailInvalidError {\n            message\n          }\n          ... on RemoveSubscriberUnexpectedError {\n            message\n          }\n        }\n      }\n    }\n  }\n`);\n\nexport const updateNewsletterSubscription = async (\n  {\n    customerInfo,\n  }: {\n    customerInfo: {\n      email: string;\n      firstName: string;\n      lastName: string;\n    };\n  },\n  _prevState: { lastResult: SubmissionResult | null },\n  formData: FormData,\n) => {\n  const t = await getTranslations('Account.Settings.NewsletterSubscription');\n\n  const submission = parseWithZod(formData, { schema: updateNewsletterSubscriptionSchema });\n\n  if (submission.status !== 'success') {\n    return { lastResult: submission.reply() };\n  }\n\n  try {\n    let errors;\n\n    if (submission.value.intent === 'subscribe') {\n      const response = await client.fetch({\n        document: SubscribeToNewsletterMutation,\n        variables: {\n          input: {\n            email: customerInfo.email,\n            firstName: customerInfo.firstName,\n            lastName: customerInfo.lastName,\n          },\n        },\n      });\n\n      errors = response.data.newsletter.subscribe.errors;\n    } else {\n      const response = await client.fetch({\n        document: UnsubscribeFromNewsletterMutation,\n        variables: {\n          input: {\n            email: customerInfo.email,\n          },\n        },\n      });\n\n      errors = response.data.newsletter.unsubscribe.errors;\n    }\n\n    if (errors.length > 0) {\n      // Not handling returned errors from API since we will display a generic error message to the user\n      // Still returning the errors to the client for debugging purposes\n      return {\n        lastResult: submission.reply({\n          formErrors: errors.map(({ message }) => message),\n        }),\n      };\n    }\n\n    revalidateTag(TAGS.customer, { expire: 0 });\n\n    return {\n      lastResult: submission.reply(),\n      successMessage: t('marketingPreferencesUpdated'),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => message),\n        }),\n      };\n    }\n\n    if (error instanceof Error) {\n      return {\n        lastResult: submission.reply({ formErrors: [error.message] }),\n      };\n    }\n\n    return {\n      lastResult: submission.reply({ formErrors: [String(error)] }),\n    };\n  }\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/settings/page-data.tsx",
    "content": "import { cache } from 'react';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\nimport { FormFieldsFragment } from '~/data-transformers/form-field-transformer/fragment';\n\nconst AccountSettingsQuery = graphql(\n  `\n    query AccountSettingsQuery(\n      $customerFilters: FormFieldFiltersInput\n      $customerSortBy: FormFieldSortInput\n      $addressFilters: FormFieldFiltersInput\n      $addressSortBy: FormFieldSortInput\n    ) {\n      customer {\n        entityId\n        email\n        firstName\n        lastName\n        company\n        isSubscribedToNewsletter\n      }\n      site {\n        settings {\n          formFields {\n            customer(filters: $customerFilters, sortBy: $customerSortBy) {\n              ...FormFieldsFragment\n            }\n            shippingAddress(filters: $addressFilters, sortBy: $addressSortBy) {\n              ...FormFieldsFragment\n            }\n          }\n          newsletter {\n            showNewsletterSignup\n          }\n          customers {\n            passwordComplexitySettings {\n              minimumNumbers\n              minimumPasswordLength\n              minimumSpecialCharacters\n              requireLowerCase\n              requireNumbers\n              requireSpecialCharacters\n              requireUpperCase\n            }\n          }\n        }\n      }\n    }\n  `,\n  [FormFieldsFragment],\n);\n\ntype Variables = VariablesOf<typeof AccountSettingsQuery>;\n\ninterface Props {\n  address?: {\n    filters?: Variables['addressFilters'];\n    sortBy?: Variables['addressSortBy'];\n  };\n\n  customer?: {\n    filters?: Variables['customerFilters'];\n    sortBy?: Variables['customerSortBy'];\n  };\n}\n\nexport const getAccountSettingsQuery = cache(async ({ address, customer }: Props = {}) => {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const response = await client.fetch({\n    document: AccountSettingsQuery,\n    variables: {\n      addressFilters: address?.filters,\n      addressSortBy: address?.sortBy,\n      customerFilters: customer?.filters,\n      customerSortBy: customer?.sortBy,\n    },\n    fetchOptions: { cache: 'no-store', next: { tags: [TAGS.customer] } },\n    customerAccessToken,\n  });\n\n  const addressFields = response.data.site.settings?.formFields.shippingAddress;\n  const customerFields = response.data.site.settings?.formFields.customer;\n  const customerInfo = response.data.customer;\n  const newsletterSettings = response.data.site.settings?.newsletter;\n  const passwordComplexitySettings =\n    response.data.site.settings?.customers?.passwordComplexitySettings;\n\n  if (!addressFields || !customerFields || !customerInfo) {\n    return null;\n  }\n\n  return {\n    addressFields,\n    customerFields,\n    customerInfo,\n    newsletterSettings,\n    passwordComplexitySettings,\n  };\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/settings/page.tsx",
    "content": "/* eslint-disable react/jsx-no-bind */\nimport { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { getTranslations, setRequestLocale } from 'next-intl/server';\n\nimport { AccountSettingsSection } from '@/vibes/soul/sections/account-settings';\n\nimport { changePassword } from './_actions/change-password';\nimport { updateCustomer } from './_actions/update-customer';\nimport { updateNewsletterSubscription } from './_actions/update-newsletter-subscription';\nimport { getAccountSettingsQuery } from './page-data';\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'Account.Settings' });\n\n  return {\n    title: t('title'),\n  };\n}\n\nexport default async function Settings({ params }: Props) {\n  const { locale } = await params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Account.Settings');\n\n  const accountSettings = await getAccountSettingsQuery();\n\n  if (!accountSettings) {\n    notFound();\n  }\n\n  const newsletterSubscriptionEnabled = accountSettings.newsletterSettings?.showNewsletterSignup;\n  const isAccountSubscribed = accountSettings.customerInfo.isSubscribedToNewsletter;\n\n  const updateNewsletterSubscriptionActionWithCustomerInfo = updateNewsletterSubscription.bind(\n    null,\n    {\n      customerInfo: accountSettings.customerInfo,\n    },\n  );\n\n  return (\n    <AccountSettingsSection\n      account={accountSettings.customerInfo}\n      changePasswordAction={changePassword}\n      changePasswordSubmitLabel={t('cta')}\n      changePasswordTitle={t('changePassword')}\n      confirmPasswordLabel={t('confirmPassword')}\n      currentPasswordLabel={t('currentPassword')}\n      isAccountSubscribed={isAccountSubscribed}\n      newPasswordLabel={t('newPassword')}\n      newsletterSubscriptionCtaLabel={t('cta')}\n      newsletterSubscriptionEnabled={newsletterSubscriptionEnabled}\n      newsletterSubscriptionLabel={t('NewsletterSubscription.label')}\n      newsletterSubscriptionTitle={t('NewsletterSubscription.title')}\n      passwordComplexitySettings={accountSettings.passwordComplexitySettings}\n      title={t('title')}\n      updateAccountAction={updateCustomer}\n      updateAccountSubmitLabel={t('cta')}\n      updateNewsletterSubscriptionAction={updateNewsletterSubscriptionActionWithCustomerInfo}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/[id]/_actions/add-to-cart.tsx",
    "content": "'use server';\n\nimport { BigCommerceAPIError, BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { Link } from '~/components/link';\nimport { addToOrCreateCart } from '~/lib/cart';\nimport { MissingCartError } from '~/lib/cart/error';\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  successMessage?: React.ReactNode;\n  errorMessage?: string;\n}\n\nconst schema = z.object({\n  productId: z.number(),\n  variantId: z.number().optional(),\n});\n\nexport async function addWishlistItemToCart(prevState: State, formData: FormData): Promise<State> {\n  const t = await getTranslations('Product.ProductDetails');\n  const submission = parseWithZod(formData, { schema });\n\n  if (submission.status !== 'success') {\n    return { lastResult: submission.reply() };\n  }\n\n  try {\n    const { productId, variantId } = schema.parse(submission.value);\n    const quantity = 1;\n\n    await addToOrCreateCart({\n      lineItems: [\n        {\n          productEntityId: productId,\n          variantEntityId: variantId,\n          quantity,\n        },\n      ],\n    });\n\n    return {\n      lastResult: submission.reply(),\n      successMessage: t.rich('successMessage', {\n        cartItems: quantity,\n        cartLink: (chunks) => (\n          <Link className=\"underline\" href=\"/cart\" prefetch=\"viewport\" prefetchKind=\"full\">\n            {chunks}\n          </Link>\n        ),\n      }),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof MissingCartError) {\n      return {\n        ...prevState,\n        lastResult: { status: 'error' },\n        errorMessage: t('missingCart'),\n      };\n    }\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        ...prevState,\n        lastResult: { status: 'error' },\n        errorMessage: error.message.includes('variant ID is required')\n          ? t('variantRequiredError')\n          : t('unknownError'),\n      };\n    }\n\n    if (error instanceof BigCommerceAPIError) {\n      return {\n        ...prevState,\n        lastResult: { status: 'error' },\n        errorMessage: t('unknownError'),\n      };\n    }\n\n    return {\n      ...prevState,\n      lastResult: { status: 'error' },\n      errorMessage: t('unknownError'),\n    };\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/[id]/_components/visibility-switch.tsx",
    "content": "'use client';\n\nimport { useActionState, useEffect, useTransition } from 'react';\n\nimport { Switch } from '@/vibes/soul/form/switch';\nimport { toast } from '@/vibes/soul/primitives/toaster';\nimport { Wishlist } from '@/vibes/soul/sections/wishlist-details';\n\nimport { toggleWishlistVisibility } from '../../_actions/change-wishlist-visibility';\n\nexport const WishlistVisibilitySwitch = ({\n  id,\n  visibility: { isPublic, publicLabel, privateLabel },\n}: Wishlist) => {\n  const [state, formAction] = useActionState(toggleWishlistVisibility, { lastResult: null });\n  const [isPending, startTransition] = useTransition();\n  const onCheckedChange = (value: boolean) => {\n    startTransition(() => {\n      const formData = new FormData();\n\n      formData.append('wishlistId', id);\n      formData.append('wishlistIsPublic', value ? 'true' : 'false');\n\n      formAction(formData);\n    });\n  };\n\n  useEffect(() => {\n    if (state.lastResult?.status === 'error' && Boolean(state.errorMessage)) {\n      toast.error(state.errorMessage);\n    }\n  }, [state]);\n\n  return (\n    <Switch\n      checked={isPublic}\n      label={{ on: publicLabel, off: privateLabel }}\n      labelPosition=\"right\"\n      loading={isPending}\n      onCheckedChange={onCheckedChange}\n      size=\"small\"\n    />\n  );\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/[id]/_components/wishlist-actions.tsx",
    "content": "import { SwitchSkeleton } from '@/vibes/soul/form/switch';\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\nimport { Wishlist } from '@/vibes/soul/sections/wishlist-details';\nimport {\n  WishlistShareButton,\n  WishlistShareButtonSkeleton,\n} from '~/components/wishlist/share-button';\n\nimport { WishlistAction, WishlistActionsMenu } from '../../_components/wishlist-actions-menu';\n\nimport { WishlistVisibilitySwitch } from './visibility-switch';\n\ninterface Props {\n  wishlist: Wishlist;\n  isMobileUser: Streamable<boolean>;\n  shareLabel: string;\n  shareCloseLabel: string;\n  shareCopyLabel: string;\n  shareModalTitle: string;\n  shareSuccessMessage: string;\n  shareCopiedMessage: string;\n  shareDisabledTooltip: string;\n  menuActions: WishlistAction[];\n  actionsTitle?: string;\n}\n\nexport const WishlistActions = ({\n  wishlist,\n  isMobileUser,\n  shareLabel,\n  shareCloseLabel,\n  shareCopyLabel,\n  shareModalTitle,\n  shareSuccessMessage,\n  shareCopiedMessage,\n  shareDisabledTooltip,\n  menuActions,\n  actionsTitle,\n}: Props) => {\n  const { publicUrl } = wishlist;\n\n  return (\n    <div className=\"flex items-center\">\n      <div className=\"flex flex-1 items-center justify-between gap-4\">\n        <div className=\"flex-1\">\n          <WishlistVisibilitySwitch {...wishlist} />\n        </div>\n        <div className=\"flex items-center gap-2 pl-4 @lg:border-l @lg:border-l-contrast-100\">\n          {publicUrl != null && publicUrl !== '' && (\n            <WishlistShareButton\n              closeLabel={shareCloseLabel}\n              copiedMessage={shareCopiedMessage}\n              copyLabel={shareCopyLabel}\n              disabledTooltip={shareDisabledTooltip}\n              isMobileUser={isMobileUser}\n              isPublic={wishlist.visibility.isPublic}\n              label={shareLabel}\n              modalTitle={shareModalTitle}\n              publicUrl={publicUrl}\n              successMessage={shareSuccessMessage}\n              wishlistName={wishlist.name}\n            />\n          )}\n          <WishlistActionsMenu actionsTitle={actionsTitle} items={menuActions} />\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport function WishlistActionsSkeleton() {\n  return (\n    <div className=\"flex items-center\">\n      <div className=\"flex flex-1 items-center justify-between gap-4\">\n        <div className=\"flex-1\">\n          <SwitchSkeleton characterCount={5} />\n        </div>\n        <div className=\"flex items-center gap-2 pl-4 @lg:border-l @lg:border-l-contrast-100\">\n          <WishlistShareButtonSkeleton />\n          <Skeleton.Box className=\"h-10 w-10 rounded-full\" />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/[id]/_components/wishlist-analytics-provider.tsx",
    "content": "'use client';\n\nimport { PropsWithChildren, Suspense } from 'react';\nimport { z } from 'zod';\n\nimport { Streamable, useStreamable } from '@/vibes/soul/lib/streamable';\nimport { EventsProvider } from '~/components/analytics/events';\nimport { useAnalytics } from '~/lib/analytics/react';\n\ninterface AddToCartContext {\n  id: number;\n  name: string;\n  brand: string;\n  sku?: string;\n  currency: string;\n  price: number;\n}\n\nconst AddToCartSchema = z.object({\n  productId: z.number({ coerce: true }),\n  variantId: z.number({ coerce: true }),\n});\n\nexport function WishlistAnalyticsProvider(\n  props: PropsWithChildren<{ data: Streamable<AddToCartContext[]> }>,\n) {\n  return (\n    <Suspense fallback={props.children}>\n      <WishlistAnalyticsProviderResolved {...props} />\n    </Suspense>\n  );\n}\n\nfunction WishlistAnalyticsProviderResolved({\n  children,\n  data,\n}: PropsWithChildren<{ data: Streamable<AddToCartContext[]> }>) {\n  const analytics = useAnalytics();\n  const products = useStreamable(data);\n\n  const onAddToCart = (payload?: FormData) => {\n    const parsedPayload = AddToCartSchema.safeParse(Object.fromEntries(payload?.entries() ?? []));\n\n    if (parsedPayload.success) {\n      const { productId, variantId: variant_id } = parsedPayload.data;\n      const product = products.find(({ id }) => id === productId);\n\n      if (product) {\n        const { id, name, brand, sku, price, currency } = product;\n\n        analytics?.cart.productAdded({\n          currency,\n          value: 1 * price,\n          items: [\n            {\n              id: id.toString(),\n              name,\n              brand,\n              sku,\n              price,\n              quantity: 1,\n              variant_id,\n            },\n          ],\n        });\n      }\n    }\n  };\n\n  return <EventsProvider onAddToCart={onAddToCart}>{children}</EventsProvider>;\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/[id]/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\nimport { WishlistPaginatedItemsFragment } from '~/components/wishlist/fragment';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\n\nconst WishlistDetailsQuery = graphql(\n  `\n    query WishlistDetailsQuery(\n      $first: Int\n      $after: String\n      $last: Int\n      $before: String\n      $entityId: Int!\n      $currencyCode: currencyCode\n    ) {\n      customer {\n        wishlists(filters: { entityIds: [$entityId] }) {\n          edges {\n            node {\n              ...WishlistPaginatedItemsFragment\n            }\n          }\n        }\n      }\n    }\n  `,\n  [WishlistPaginatedItemsFragment],\n);\n\ninterface Pagination {\n  limit: number;\n  before: string | null;\n  after: string | null;\n}\n\nexport const getCustomerWishlist = cache(async (entityId: number, pagination: Pagination) => {\n  const { before, after, limit = 9 } = pagination;\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  const currencyCode = await getPreferredCurrencyCode();\n  const paginationArgs = before ? { last: limit, before } : { first: limit, after };\n  const response = await client.fetch({\n    document: WishlistDetailsQuery,\n    variables: { ...paginationArgs, currencyCode, entityId },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store', next: { tags: [TAGS.customer] } },\n  });\n\n  const wishlist = response.data.customer?.wishlists.edges?.[0]?.node;\n\n  if (!wishlist) {\n    return null;\n  }\n\n  return wishlist;\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/[id]/page.tsx",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\nimport { SearchParams } from 'nuqs';\nimport { createSearchParamsCache, parseAsInteger, parseAsString } from 'nuqs/server';\n\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination';\nimport { Wishlist, WishlistDetails } from '@/vibes/soul/sections/wishlist-details';\nimport { ExistingResultType } from '~/client/util';\nimport { defaultPageInfo, pageInfoTransformer } from '~/data-transformers/page-info-transformer';\nimport { wishlistDetailsTransformer } from '~/data-transformers/wishlists-transformer';\nimport { redirect } from '~/i18n/routing';\nimport { isMobileUser } from '~/lib/user-agent';\n\nimport { removeWishlistItem } from '../_actions/remove-wishlist-item';\nimport { getDeleteWishlistModal, getRenameWishlistModal } from '../modals';\n\nimport { addWishlistItemToCart } from './_actions/add-to-cart';\nimport { WishlistActions, WishlistActionsSkeleton } from './_components/wishlist-actions';\nimport { WishlistAnalyticsProvider } from './_components/wishlist-analytics-provider';\nimport { getCustomerWishlist } from './page-data';\n\ninterface Props {\n  params: Promise<{ locale: string; id: string }>;\n  searchParams: Promise<SearchParams>;\n}\n\nconst defaultWishlistItemsLimit = 10;\nconst searchParamsCache = createSearchParamsCache({\n  tag: parseAsString,\n  before: parseAsString,\n  after: parseAsString,\n  limit: parseAsInteger.withDefault(defaultWishlistItemsLimit),\n});\n\nasync function getWishlist(\n  id: string,\n  t: ExistingResultType<typeof getTranslations<'Wishlist'>>,\n  pt: ExistingResultType<typeof getTranslations<'Product.ProductDetails'>>,\n  searchParamsPromise: Promise<SearchParams>,\n  locale: string,\n): Promise<Wishlist> {\n  const entityId = Number(id);\n  const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise);\n  const formatter = await getFormatter();\n  const wishlist = await getCustomerWishlist(entityId, searchParamsParsed);\n\n  if (!wishlist) {\n    return redirect({ href: '/account/wishlists/', locale });\n  }\n\n  return wishlistDetailsTransformer(wishlist, t, pt, formatter);\n}\n\nconst getAnalyticsData = async (id: string, searchParamsPromise: Promise<SearchParams>) => {\n  const entityId = Number(id);\n  const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise);\n  const wishlist = await getCustomerWishlist(entityId, searchParamsParsed);\n\n  if (!wishlist) {\n    return [];\n  }\n\n  return removeEdgesAndNodes(wishlist.items)\n    .map(({ product }) => product)\n    .filter((product) => product !== null)\n    .map((product) => {\n      return {\n        id: product.entityId,\n        name: product.name,\n        sku: product.sku,\n        brand: product.brand?.name ?? '',\n        price: product.prices?.price.value ?? 0,\n        currency: product.prices?.price.currencyCode ?? '',\n      };\n    });\n};\n\nasync function getPaginationInfo(\n  id: string,\n  searchParamsPromise: Promise<SearchParams>,\n): Promise<CursorPaginationInfo> {\n  const entityId = Number(id);\n  const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise);\n  const wishlist = await getCustomerWishlist(entityId, searchParamsParsed);\n\n  return pageInfoTransformer(wishlist?.items.pageInfo ?? defaultPageInfo);\n}\n\nexport default async function WishlistPage({ params, searchParams }: Props) {\n  const { locale, id } = await params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Wishlist');\n  const pt = await getTranslations('Product.ProductDetails');\n  const wishlistActions = (wishlist?: Wishlist) => {\n    if (!wishlist) {\n      return <WishlistActionsSkeleton />;\n    }\n\n    return (\n      <WishlistActions\n        actionsTitle={t('actionsTitle')}\n        isMobileUser={isMobileUser()}\n        menuActions={[\n          {\n            label: t('rename'),\n            modal: getRenameWishlistModal(wishlist, t),\n          },\n          {\n            label: t('delete'),\n            variant: 'danger',\n            modal: getDeleteWishlistModal(wishlist, t),\n          },\n        ]}\n        shareCloseLabel={t('Modal.close')}\n        shareCopiedMessage={t('shareCopied')}\n        shareCopyLabel={t('Modal.copy')}\n        shareDisabledTooltip={t('shareDisabled')}\n        shareLabel={t('share')}\n        shareModalTitle={t('Modal.shareTitle', { name: wishlist.name })}\n        shareSuccessMessage={t('shareSuccess')}\n        wishlist={wishlist}\n      />\n    );\n  };\n\n  return (\n    <WishlistAnalyticsProvider data={Streamable.from(() => getAnalyticsData(id, searchParams))}>\n      <WishlistDetails\n        action={addWishlistItemToCart}\n        emptyStateText={t('emptyWishlist')}\n        headerActions={wishlistActions}\n        paginationInfo={Streamable.from(() => getPaginationInfo(id, searchParams))}\n        prevHref=\"/account/wishlists\"\n        removeAction={removeWishlistItem}\n        removeButtonTitle={t('removeButtonTitle')}\n        wishlist={Streamable.from(() => getWishlist(id, t, pt, searchParams, locale))}\n      />\n    </WishlistAnalyticsProvider>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/_actions/change-wishlist-visibility.ts",
    "content": "'use server';\n\nimport { BigCommerceAuthError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { revalidateTag } from 'next/cache';\nimport { getTranslations } from 'next-intl/server';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { TAGS } from '~/client/tags';\n\nimport { UpdateWishlistMutation } from './mutation';\nimport { toggleWishlistVisibilitySchema } from './schema';\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  successMessage?: string;\n  errorMessage?: string;\n}\n\nexport async function toggleWishlistVisibility(\n  prevState: Awaited<State>,\n  formData: FormData,\n): Promise<State> {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  const t = await getTranslations('Wishlist');\n  const submission = parseWithZod(formData, { schema: toggleWishlistVisibilitySchema });\n\n  if (submission.status !== 'success') {\n    return {\n      ...prevState,\n      lastResult: submission.reply(),\n    };\n  }\n\n  if (!customerAccessToken) {\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: [t('Errors.unauthorized')] }),\n      errorMessage: t('Errors.unauthorized'),\n    };\n  }\n\n  try {\n    const { wishlistId, wishlistIsPublic } = toggleWishlistVisibilitySchema.parse(submission.value);\n    const isPublic = wishlistIsPublic === 'true';\n\n    const response = await client.fetch({\n      document: UpdateWishlistMutation,\n      customerAccessToken,\n      fetchOptions: { cache: 'no-store' },\n      variables: { wishlistId, input: { isPublic } },\n    });\n\n    const result = response.data.wishlist.updateWishlist?.result;\n\n    if (result?.isPublic !== isPublic) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: [t('Errors.updateFailed')] }),\n        errorMessage: t('Errors.updateFailed'),\n      };\n    }\n\n    revalidateTag(TAGS.customer, { expire: 0 });\n\n    return {\n      lastResult: submission.reply(),\n      successMessage: t('Result.updateSuccess'),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceAuthError) {\n      const authErrorMessage = t('Errors.unauthorized');\n\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: [authErrorMessage] }),\n        errorMessage: authErrorMessage,\n      };\n    }\n\n    const errorMessage = t('Errors.unexpected');\n\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: [errorMessage] }),\n      errorMessage,\n    };\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/_actions/delete-wishlist.ts",
    "content": "'use server';\n\nimport { BigCommerceAuthError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { revalidateTag } from 'next/cache';\nimport { getTranslations } from 'next-intl/server';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { TAGS } from '~/client/tags';\nimport { serverToast } from '~/lib/server-toast';\n\nimport { DeleteWishlistMutation } from './mutation';\nimport { deleteWishlistSchema } from './schema';\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  successMessage?: string;\n}\n\nexport async function deleteWishlist(\n  prevState: Awaited<State>,\n  formData: FormData,\n): Promise<State> {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  const t = await getTranslations('Wishlist');\n  const submission = parseWithZod(formData, { schema: deleteWishlistSchema });\n\n  if (submission.status !== 'success') {\n    return {\n      ...prevState,\n      lastResult: submission.reply(),\n    };\n  }\n\n  if (!customerAccessToken) {\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: [t('Errors.unauthorized')] }),\n    };\n  }\n\n  try {\n    const { wishlistId } = deleteWishlistSchema.parse(submission.value);\n\n    const response = await client.fetch({\n      document: DeleteWishlistMutation,\n      customerAccessToken,\n      fetchOptions: { cache: 'no-store' },\n      variables: { wishlistId },\n    });\n\n    const result = response.data.wishlist.deleteWishlists?.result;\n\n    if (result !== 'success') {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: [t('Errors.deleteFailed')] }),\n      };\n    }\n\n    revalidateTag(TAGS.customer, { expire: 0 });\n\n    // Server toast has to be used here since the item is being deleted. When revalidateTag is called,\n    // the wishlist items will update, and the element node containing the useEffect will be removed.\n    await serverToast.success(t('Result.deleteSuccess'));\n\n    return {\n      lastResult: submission.reply(),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceAuthError) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: [t('Errors.unauthorized')] }),\n      };\n    }\n\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: [t('Errors.unexpected')] }),\n    };\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/_actions/mutation.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nexport const CreateWishlistMutation = graphql(`\n  mutation CreateWishlistMutation($input: CreateWishlistInput!) {\n    wishlist {\n      createWishlist(input: $input) {\n        result {\n          entityId\n          name\n          isPublic\n        }\n      }\n    }\n  }\n`);\n\nexport const UpdateWishlistMutation = graphql(`\n  mutation UpdateWishlistMutation($wishlistId: Int!, $input: WishlistUpdateDataInput!) {\n    wishlist {\n      updateWishlist(input: { entityId: $wishlistId, data: $input }) {\n        result {\n          entityId\n          name\n          isPublic\n        }\n      }\n    }\n  }\n`);\n\nexport const DeleteWishlistItemsMutation = graphql(`\n  mutation DeleteWishlistItemsMutation($wishlistId: Int!, $itemIds: [Int!]!) {\n    wishlist {\n      deleteWishlistItems(input: { entityId: $wishlistId, itemEntityIds: $itemIds }) {\n        result {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\nexport const DeleteWishlistMutation = graphql(`\n  mutation DeleteWishlistMutation($wishlistId: Int!) {\n    wishlist {\n      deleteWishlists(input: { entityIds: [$wishlistId] }) {\n        result\n      }\n    }\n  }\n`);\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/_actions/new-wishlist.ts",
    "content": "'use server';\n\nimport { BigCommerceAuthError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { revalidateTag } from 'next/cache';\nimport { getTranslations } from 'next-intl/server';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { TAGS } from '~/client/tags';\n\nimport { CreateWishlistMutation } from './mutation';\nimport { newWishlistSchema } from './schema';\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  successMessage?: string;\n}\n\nexport async function newWishlist(prevState: Awaited<State>, formData: FormData): Promise<State> {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  const t = await getTranslations('Wishlist');\n  const schema = newWishlistSchema({ required_error: t('Errors.nameRequired') });\n  const submission = parseWithZod(formData, { schema });\n\n  if (submission.status !== 'success') {\n    return {\n      ...prevState,\n      lastResult: submission.reply(),\n    };\n  }\n\n  if (!customerAccessToken) {\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: [t('Errors.unauthorized')] }),\n    };\n  }\n\n  try {\n    const { wishlistName, wishlistIsPublic, wishlistItems } = schema.parse(submission.value);\n    const isPublic = wishlistIsPublic === 'true';\n\n    const response = await client.fetch({\n      document: CreateWishlistMutation,\n      customerAccessToken,\n      fetchOptions: { cache: 'no-store' },\n      variables: { input: { name: wishlistName, isPublic, items: wishlistItems } },\n    });\n\n    const result = response.data.wishlist.createWishlist?.result;\n\n    if (result?.name !== wishlistName) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: [t('Errors.updateFailed')] }),\n      };\n    }\n\n    revalidateTag(TAGS.customer, { expire: 0 });\n\n    return {\n      lastResult: submission.reply(),\n      successMessage: t('Result.createSuccess'),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceAuthError) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: [t('Errors.unauthorized')] }),\n      };\n    }\n\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: [t('Errors.unexpected')] }),\n    };\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/_actions/remove-wishlist-item.ts",
    "content": "'use server';\n\nimport { BigCommerceAuthError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { revalidateTag } from 'next/cache';\nimport { getTranslations } from 'next-intl/server';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { TAGS } from '~/client/tags';\nimport { serverToast } from '~/lib/server-toast';\n\nimport { DeleteWishlistItemsMutation } from './mutation';\nimport { removeWishlistItemSchema } from './schema';\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  errorMessage?: string;\n}\n\nexport async function removeWishlistItem(\n  prevState: Awaited<State>,\n  formData: FormData,\n): Promise<State> {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  const t = await getTranslations('Wishlist');\n  const submission = parseWithZod(formData, { schema: removeWishlistItemSchema });\n\n  if (submission.status !== 'success') {\n    return {\n      ...prevState,\n      lastResult: { status: 'error' },\n      errorMessage: t('Errors.unexpected'),\n    };\n  }\n\n  if (!customerAccessToken) {\n    return {\n      ...prevState,\n      lastResult: { status: 'error' },\n      errorMessage: t('Errors.unauthorized'),\n    };\n  }\n\n  try {\n    const { wishlistId, wishlistItemId } = removeWishlistItemSchema.parse(submission.value);\n\n    const response = await client.fetch({\n      document: DeleteWishlistItemsMutation,\n      customerAccessToken,\n      fetchOptions: { cache: 'no-store' },\n      variables: { wishlistId, itemIds: [wishlistItemId] },\n    });\n\n    const result = response.data.wishlist.deleteWishlistItems?.result;\n\n    if (!result) {\n      return {\n        ...prevState,\n        lastResult: { status: 'error' },\n        errorMessage: t('Errors.removeProductFailed'),\n      };\n    }\n\n    revalidateTag(TAGS.customer, { expire: 0 });\n\n    // Server toast has to be used here since the item is being deleted. When revalidateTag is called,\n    // the wishlist items will update, and the element node containing the useEffect will be removed.\n    await serverToast.success(t('Result.removeItemSuccess'));\n\n    return {\n      lastResult: submission.reply(),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceAuthError) {\n      return {\n        ...prevState,\n        lastResult: { status: 'error' },\n        errorMessage: t('Errors.unauthorized'),\n      };\n    }\n\n    return {\n      ...prevState,\n      lastResult: { status: 'error' },\n      errorMessage: t('Errors.unexpected'),\n    };\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/_actions/rename-wishlist.ts",
    "content": "'use server';\n\nimport { BigCommerceAuthError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { revalidateTag } from 'next/cache';\nimport { getTranslations } from 'next-intl/server';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { TAGS } from '~/client/tags';\n\nimport { UpdateWishlistMutation } from './mutation';\nimport { renameWishlistSchema } from './schema';\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  successMessage?: string;\n}\n\nexport async function renameWishlist(\n  prevState: Awaited<State>,\n  formData: FormData,\n): Promise<State> {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  const t = await getTranslations('Wishlist');\n  const schema = renameWishlistSchema({ required_error: t('Errors.nameRequired') });\n  const submission = parseWithZod(formData, { schema });\n\n  if (submission.status !== 'success') {\n    return {\n      ...prevState,\n      lastResult: submission.reply(),\n    };\n  }\n\n  if (!customerAccessToken) {\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: [t('Errors.unauthorized')] }),\n    };\n  }\n\n  try {\n    const { wishlistId, wishlistName } = schema.parse(submission.value);\n\n    const response = await client.fetch({\n      document: UpdateWishlistMutation,\n      customerAccessToken,\n      fetchOptions: { cache: 'no-store' },\n      variables: { wishlistId, input: { name: wishlistName } },\n    });\n\n    const result = response.data.wishlist.updateWishlist?.result;\n\n    if (result?.name !== wishlistName) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: [t('Errors.updateFailed')] }),\n      };\n    }\n\n    revalidateTag(TAGS.customer, { expire: 0 });\n\n    return {\n      lastResult: submission.reply(),\n      successMessage: t('Result.updateSuccess'),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceAuthError) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: [t('Errors.unauthorized')] }),\n      };\n    }\n\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: [t('Errors.unexpected')] }),\n    };\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/_actions/schema.ts",
    "content": "import { z } from 'zod';\n\nconst wishlistId = z.number().nonnegative().min(1);\n\nconst wishlistItemSchema = z.object({\n  productEntityId: z.number().nonnegative().min(1),\n  variantEntityId: z.number().nonnegative().min(1).optional(),\n});\n\nexport const newWishlistSchema = ({\n  required_error = 'Wish list name cannot be empty.',\n}: {\n  required_error?: string;\n}) =>\n  z.object({\n    wishlistName: z.string({ required_error }).trim().nonempty(),\n    wishlistIsPublic: z.enum(['true', 'false']).optional(),\n    wishlistItems: z.array(wishlistItemSchema).optional(),\n  });\n\nexport const renameWishlistSchema = ({\n  required_error = 'Wish list name cannot be empty.',\n}: {\n  required_error?: string;\n}) =>\n  z.object({\n    wishlistId,\n    wishlistName: z.string({ required_error }).trim().nonempty(),\n  });\n\nexport const removeWishlistItemSchema = z.object({\n  wishlistId,\n  wishlistItemId: z.number().nonnegative().min(1),\n});\n\nexport const toggleWishlistVisibilitySchema = z.object({\n  wishlistId,\n  wishlistIsPublic: z.enum(['true', 'false']),\n});\n\nexport const deleteWishlistSchema = z.object({\n  wishlistId,\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/_components/new-wishlist-button.tsx",
    "content": "'use client';\n\nimport { useState } from 'react';\n\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { toast } from '@/vibes/soul/primitives/toaster';\nimport { Modal, ModalFormState } from '~/components/modal';\n\nimport { WishlistModalProps } from './wishlist-actions-menu';\n\ninterface Props {\n  label: string;\n  variant?: 'primary' | 'tertiary';\n  modal: WishlistModalProps;\n}\n\nexport const NewWishlistButton = ({ modal, variant = 'primary', label }: Props) => {\n  const [isOpen, setOpen] = useState(false);\n  const { formAction: action, ...props } = modal;\n  const onSuccess = ({ successMessage }: ModalFormState) => {\n    if (successMessage !== '' && successMessage !== undefined) {\n      toast.success(successMessage);\n      setOpen(false);\n    }\n  };\n\n  const onError = ({ errorMessage }: ModalFormState) => {\n    if (errorMessage !== '' && errorMessage !== undefined) {\n      toast.error(errorMessage);\n    }\n  };\n\n  if (!action) {\n    return null;\n  }\n\n  return (\n    <Modal\n      className=\"min-w-64 @lg:min-w-96\"\n      form={{ action, onSuccess, onError }}\n      isOpen={isOpen}\n      setOpen={setOpen}\n      trigger={\n        <Button size=\"small\" variant={variant}>\n          {label}\n        </Button>\n      }\n      {...props}\n    />\n  );\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/_components/wishlist-actions-menu.tsx",
    "content": "'use client';\n\nimport { EllipsisIcon } from 'lucide-react';\nimport { useReducer } from 'react';\n\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { DropdownMenu } from '@/vibes/soul/primitives/dropdown-menu';\nimport { toast } from '@/vibes/soul/primitives/toaster';\nimport { Modal, ModalButton, ModalFormAction, ModalFormState } from '~/components/modal';\n\nimport { getShareWishlistModal } from '../modals';\n\ninterface WishlistActionBase {\n  className?: string;\n  label: string | React.ReactNode;\n  disabled?: boolean;\n  variant?: 'default' | 'danger';\n}\n\nexport interface WishlistModalProps {\n  title: string;\n  children: React.ReactNode;\n  hideHeader?: boolean;\n  buttons?: ModalButton[];\n  formAction?: ModalFormAction;\n}\n\ninterface WishlistModalAction extends WishlistActionBase {\n  key?: string;\n  modal: WishlistModalProps;\n}\n\ninterface WishlistMenuAction extends WishlistActionBase {\n  action?: string | ((e: React.MouseEvent<HTMLDivElement>) => void);\n}\n\nexport type WishlistAction = WishlistModalAction | WishlistMenuAction;\n\ninterface Props {\n  actionsTitle?: string;\n  share?: {\n    wishlistName: string;\n    label: string;\n    publicUrl: string;\n    modalTitle: string;\n    copiedMessage: string;\n    isMobileUser: boolean;\n    isPublic: boolean;\n    successMessage: string;\n    disabledTooltip: string;\n    closeLabel: string;\n    copyLabel: string;\n  };\n  items: WishlistAction[];\n}\n\nfunction reducer(state: Record<string, boolean>, action: { modal: string; open: boolean }) {\n  return {\n    ...state,\n    [action.modal]: action.open,\n  };\n}\n\nfunction getShareMenuItemProps(\n  share: Props['share'],\n  key: string,\n  nativeShare: (name: string, publicUrl: string) => Promise<void>,\n  copyToClipboard: (publicUrl: string) => Promise<void>,\n): WishlistAction | undefined {\n  if (!share) {\n    return undefined;\n  }\n\n  if (share.isMobileUser) {\n    return {\n      label: share.label,\n      disabled: !share.isPublic,\n      action: () => {\n        void nativeShare(share.wishlistName, share.publicUrl);\n      },\n    };\n  }\n\n  return {\n    label: share.label,\n    disabled: !share.isPublic,\n    key,\n    modal: getShareWishlistModal(\n      share.modalTitle,\n      share.copyLabel,\n      share.closeLabel,\n      share.publicUrl,\n      () => {\n        void copyToClipboard(share.publicUrl);\n      },\n    ),\n  };\n}\n\nexport const WishlistActionsMenu = ({ actionsTitle, items, share }: Props) => {\n  const [state, dispatch] = useReducer(reducer, {});\n  const shareModalKey = 'share-dropdown-modal';\n  const getShareUrl = (publicUrl: string) => String(new URL(publicUrl, window.location.origin));\n  const nativeShare = async (title: string, publicUrl: string) => {\n    try {\n      await navigator.share({ url: getShareUrl(publicUrl), title });\n      toast.success(share?.successMessage);\n    } catch {\n      // noop\n    }\n  };\n\n  const copyToClipboard = async (publicUrl: string) => {\n    try {\n      await navigator.clipboard.writeText(getShareUrl(publicUrl));\n      toast.success(share?.copiedMessage);\n      dispatch({ modal: shareModalKey, open: false });\n    } catch {\n      // noop\n    }\n  };\n\n  const shareProps = getShareMenuItemProps(share, shareModalKey, nativeShare, copyToClipboard);\n  const shareMenuItem = shareProps ? [shareProps] : [];\n  const menuItems = [...shareMenuItem, ...items].map((item, index) => {\n    if ('modal' in item) {\n      const key = item.key ?? `dropdown-modal-${index}`;\n\n      return {\n        ...item,\n        key,\n        action: () => dispatch({ modal: key, open: true }),\n      };\n    }\n\n    return item;\n  });\n\n  const modals = menuItems.filter((item) => 'modal' in item);\n\n  const handleModalFormSuccess = (modal: string) => {\n    return ({ successMessage }: ModalFormState) => {\n      if (successMessage !== undefined && successMessage !== '') {\n        toast.success(successMessage);\n        dispatch({ modal, open: false });\n      }\n    };\n  };\n\n  return (\n    <>\n      <DropdownMenu className=\"min-w-40\" items={menuItems}>\n        <Button\n          className=\"data-[state=open]:after:translate-x-0\"\n          shape=\"circle\"\n          size=\"small\"\n          variant=\"tertiary\"\n        >\n          <EllipsisIcon size={20}>\n            <title>{actionsTitle}</title>\n          </EllipsisIcon>\n        </Button>\n      </DropdownMenu>\n      {modals.map(({ key, modal: { formAction: action, ...modalProps } }) => (\n        <Modal\n          className=\"min-w-64 max-w-lg @lg:min-w-96\"\n          form={action ? { action, onSuccess: handleModalFormSuccess(key) } : undefined}\n          isOpen={state[key] ?? false}\n          key={key}\n          setOpen={(open) => dispatch({ modal: key, open })}\n          {...modalProps}\n        />\n      ))}\n    </>\n  );\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/modals.tsx",
    "content": "import { getTranslations } from 'next-intl/server';\n\nimport { Wishlist } from '@/vibes/soul/sections/wishlist-details';\nimport { ExistingResultType } from '~/client/util';\nimport { ChangeWishlistVisibilityModal } from '~/components/wishlist/modals/change-visibility';\nimport { DeleteWishlistModal } from '~/components/wishlist/modals/delete';\nimport { NewWishlistModal } from '~/components/wishlist/modals/new';\nimport { RenameWishlistModal } from '~/components/wishlist/modals/rename';\nimport { ShareWishlistModal } from '~/components/wishlist/modals/share';\n\nimport { toggleWishlistVisibility } from './_actions/change-wishlist-visibility';\nimport { deleteWishlist } from './_actions/delete-wishlist';\nimport { newWishlist } from './_actions/new-wishlist';\nimport { renameWishlist } from './_actions/rename-wishlist';\nimport { WishlistModalProps } from './_components/wishlist-actions-menu';\n\nconst bold = (chunks: React.ReactNode) => <span className=\"font-bold\">{chunks}</span>;\n\nexport const getNewWishlistModal = (\n  t: ExistingResultType<typeof getTranslations<'Wishlist'>>,\n): WishlistModalProps => ({\n  children: (\n    <NewWishlistModal nameLabel={t('Form.nameLabel')} requiredError={t('Errors.nameRequired')} />\n  ),\n  title: t('Modal.newTitle'),\n  formAction: newWishlist,\n  buttons: [\n    {\n      label: t('Modal.cancel'),\n      type: 'cancel',\n    },\n    {\n      label: t('Modal.create'),\n      type: 'submit',\n    },\n  ],\n});\n\nexport const getRenameWishlistModal = (\n  wishlist: Wishlist,\n  t: ExistingResultType<typeof getTranslations<'Wishlist'>>,\n): WishlistModalProps => ({\n  children: (\n    <RenameWishlistModal\n      {...wishlist}\n      nameLabel={t('Form.nameLabel')}\n      requiredError={t('Errors.nameRequired')}\n    />\n  ),\n  title: t('Modal.renameTitle', { name: wishlist.name }),\n  formAction: renameWishlist,\n  buttons: [\n    {\n      label: t('Modal.cancel'),\n      type: 'cancel',\n    },\n    {\n      label: t('Modal.save'),\n      type: 'submit',\n    },\n  ],\n});\n\nexport const getChangeWishlistVisibilityModal = (\n  wishlist: Wishlist,\n  t: ExistingResultType<typeof getTranslations<'Wishlist'>>,\n): WishlistModalProps => {\n  const name = wishlist.name;\n  const title = wishlist.visibility.isPublic\n    ? t('Modal.changeVisibilityPrivateTitle', { name })\n    : t('Modal.changeVisibilityPublicTitle', { name });\n\n  const message = wishlist.visibility.isPublic\n    ? t.rich('Modal.makePrivateContent', { name, bold })\n    : t.rich('Modal.makePublicContent', { name, bold });\n\n  return {\n    children: <ChangeWishlistVisibilityModal {...wishlist} message={message} />,\n    title,\n    formAction: toggleWishlistVisibility,\n    hideHeader: true,\n    buttons: [\n      {\n        label: t('Modal.cancel'),\n        type: 'cancel',\n      },\n      {\n        label: wishlist.visibility.isPublic ? t('makePrivate') : t('makePublic'),\n        type: 'submit',\n      },\n    ],\n  };\n};\n\nexport const getShareWishlistModal = (\n  title: string,\n  copyLabel: string,\n  closeLabel: string,\n  publicUrl: string,\n  action: () => void | Promise<void>,\n): WishlistModalProps => ({\n  children: <ShareWishlistModal publicUrl={publicUrl} />,\n  title,\n  buttons: [\n    { type: 'cancel', label: closeLabel },\n    { label: copyLabel, variant: 'primary', action },\n  ],\n});\n\nexport const getDeleteWishlistModal = (\n  wishlist: Wishlist,\n  t: ExistingResultType<typeof getTranslations<'Wishlist'>>,\n): WishlistModalProps => ({\n  children: (\n    <DeleteWishlistModal\n      {...wishlist}\n      message={t.rich('Modal.deleteContent', { name: wishlist.name, bold })}\n    />\n  ),\n  title: t('Modal.deleteTitle', { name: wishlist.name }),\n  formAction: deleteWishlist,\n  hideHeader: true,\n  buttons: [\n    {\n      label: t('Modal.cancel'),\n      type: 'cancel',\n    },\n    {\n      label: t('Modal.delete'),\n      variant: 'danger',\n      type: 'submit',\n    },\n  ],\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\nimport { WishlistsFragment } from '~/components/wishlist/fragment';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\n\nconst WishlistsPageQuery = graphql(\n  `\n    query WishlistsPageQuery(\n      $first: Int\n      $after: String\n      $last: Int\n      $before: String\n      $filters: WishlistFiltersInput\n      $currencyCode: currencyCode\n    ) {\n      customer {\n        wishlists(first: $first, after: $after, last: $last, before: $before, filters: $filters) {\n          ...WishlistsFragment\n        }\n      }\n    }\n  `,\n  [WishlistsFragment],\n);\n\ninterface Pagination {\n  limit: number;\n  before: string | null;\n  after: string | null;\n}\n\nexport const getCustomerWishlists = cache(async ({ limit = 9, before, after }: Pagination) => {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  const currencyCode = await getPreferredCurrencyCode();\n  const paginationArgs = before ? { last: limit, before } : { first: limit, after };\n  const response = await client.fetch({\n    document: WishlistsPageQuery,\n    variables: { ...paginationArgs, currencyCode },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store', next: { tags: [TAGS.customer] } },\n  });\n\n  const wishlists = response.data.customer?.wishlists;\n\n  if (!wishlists) {\n    return null;\n  }\n\n  return wishlists;\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/account/wishlists/page.tsx",
    "content": "import { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\nimport { SearchParams } from 'nuqs';\nimport { createSearchParamsCache, parseAsInteger, parseAsString } from 'nuqs/server';\n\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\nimport { Wishlist } from '@/vibes/soul/sections/wishlist-details';\nimport { WishlistsSection } from '@/vibes/soul/sections/wishlists-section';\nimport { ExistingResultType } from '~/client/util';\nimport { defaultPageInfo, pageInfoTransformer } from '~/data-transformers/page-info-transformer';\nimport { wishlistsTransformer } from '~/data-transformers/wishlists-transformer';\nimport { isMobileUser } from '~/lib/user-agent';\n\nimport { NewWishlistButton } from './_components/new-wishlist-button';\nimport { WishlistActionsMenu } from './_components/wishlist-actions-menu';\nimport {\n  getChangeWishlistVisibilityModal,\n  getDeleteWishlistModal,\n  getNewWishlistModal,\n  getRenameWishlistModal,\n} from './modals';\nimport { getCustomerWishlists } from './page-data';\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n  searchParams: Promise<SearchParams>;\n}\n\nconst defaultWishlistsLimit = 10;\nconst searchParamsCache = createSearchParamsCache({\n  tag: parseAsString,\n  before: parseAsString,\n  after: parseAsString,\n  limit: parseAsInteger.withDefault(defaultWishlistsLimit),\n});\n\nasync function listWishlists(\n  searchParamsPromise: Promise<SearchParams>,\n  t: ExistingResultType<typeof getTranslations<'Wishlist'>>,\n): Promise<Wishlist[]> {\n  const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise);\n  const formatter = await getFormatter();\n  const wishlists = await getCustomerWishlists(searchParamsParsed);\n\n  if (!wishlists) {\n    return [];\n  }\n\n  return wishlistsTransformer(wishlists, t, formatter);\n}\n\nasync function getPaginationInfo(\n  searchParamsPromise: Promise<SearchParams>,\n): Promise<CursorPaginationInfo> {\n  const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise);\n  const wishlists = await getCustomerWishlists(searchParamsParsed);\n\n  return pageInfoTransformer(wishlists?.pageInfo ?? defaultPageInfo);\n}\n\nexport default async function Wishlists({ params, searchParams }: Props) {\n  const { locale } = await params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Wishlist');\n  const isMobile = await isMobileUser();\n  const newWishlistModal = getNewWishlistModal(t);\n\n  return (\n    <WishlistsSection\n      actions={<NewWishlistButton label={t('new')} modal={newWishlistModal} variant=\"tertiary\" />}\n      emptyStateCallToAction={\n        <NewWishlistButton label={t('noWishlistsCallToAction')} modal={newWishlistModal} />\n      }\n      emptyStateTitle={t('noWishlists')}\n      emptyWishlistStateText={t('emptyWishlist')}\n      itemActions={{\n        component: (wishlist) => {\n          if (!wishlist) {\n            return <Skeleton.Box className=\"h-10 w-10 rounded-full\" />;\n          }\n\n          return (\n            <WishlistActionsMenu\n              actionsTitle={t('actionsTitle')}\n              items={[\n                {\n                  label: t('rename'),\n                  modal: getRenameWishlistModal(wishlist, t),\n                },\n                {\n                  label: wishlist.visibility.isPublic ? t('makePrivate') : t('makePublic'),\n                  modal: getChangeWishlistVisibilityModal(wishlist, t),\n                },\n                {\n                  label: t('delete'),\n                  variant: 'danger',\n                  modal: getDeleteWishlistModal(wishlist, t),\n                },\n              ]}\n              share={\n                wishlist.publicUrl\n                  ? {\n                      wishlistName: wishlist.name,\n                      modalTitle: t('Modal.shareTitle', { name: wishlist.name }),\n                      publicUrl: wishlist.publicUrl,\n                      closeLabel: t('Modal.close'),\n                      copyLabel: t('Modal.copy'),\n                      copiedMessage: t('shareCopied'),\n                      disabledTooltip: t('shareDisabled'),\n                      label: t('share'),\n                      successMessage: t('shareSuccess'),\n                      isPublic: wishlist.visibility.isPublic,\n                      isMobileUser: isMobile,\n                    }\n                  : undefined\n              }\n            />\n          );\n        },\n      }}\n      paginationInfo={Streamable.from(() => getPaginationInfo(searchParams))}\n      title={t('title')}\n      viewWishlistLabel={t('viewWishlist')}\n      wishlists={Streamable.from(() => listWishlists(searchParams, t))}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/blog/[blogId]/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\n\nconst BlogPageQuery = graphql(`\n  query BlogPageQuery($entityId: Int!) {\n    site {\n      content {\n        blog {\n          name\n          path\n          post(entityId: $entityId) {\n            author\n            htmlBody\n            name\n            path\n            publishedDate {\n              utc\n            }\n            tags\n            thumbnailImage {\n              altText\n              url: urlTemplate(lossy: true)\n            }\n            seo {\n              pageTitle\n              metaDescription\n              metaKeywords\n            }\n          }\n        }\n      }\n    }\n  }\n`);\n\ntype Variables = VariablesOf<typeof BlogPageQuery>;\n\nexport const getBlogPageData = cache(async (variables: Variables) => {\n  const response = await client.fetch({\n    document: BlogPageQuery,\n    variables,\n    fetchOptions: { next: { revalidate } },\n  });\n\n  const { blog } = response.data.site.content;\n\n  if (!blog?.post) {\n    return null;\n  }\n\n  return blog;\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/blog/[blogId]/page.tsx",
    "content": "import type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\nimport { cache } from 'react';\n\nimport { BlogPostContent, BlogPostContentBlogPost } from '@/vibes/soul/sections/blog-post-content';\nimport { Breadcrumb } from '@/vibes/soul/sections/breadcrumbs';\nimport { getMetadataAlternates } from '~/lib/seo/canonical';\n\nimport { getBlogPageData } from './page-data';\n\nconst cachedBlogPageDataVariables = cache((blogId: string) => ({ entityId: Number(blogId) }));\n\ninterface Props {\n  params: Promise<{\n    locale: string;\n    blogId: string;\n  }>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { blogId, locale } = await params;\n\n  const variables = cachedBlogPageDataVariables(blogId);\n\n  const blog = await getBlogPageData(variables);\n  const blogPost = blog?.post;\n\n  if (!blogPost) {\n    return {};\n  }\n\n  const { pageTitle, metaDescription, metaKeywords } = blogPost.seo;\n\n  return {\n    title: pageTitle || blogPost.name,\n    ...(metaDescription && { description: metaDescription }),\n    ...(metaKeywords && { keywords: metaKeywords.split(',') }),\n    ...(blogPost.path && {\n      alternates: await getMetadataAlternates({ path: blogPost.path, locale }),\n    }),\n  };\n}\n\nasync function getBlogPost(props: Props): Promise<BlogPostContentBlogPost> {\n  const format = await getFormatter();\n\n  const { blogId } = await props.params;\n\n  const variables = cachedBlogPageDataVariables(blogId);\n\n  const blog = await getBlogPageData(variables);\n  const blogPost = blog?.post;\n\n  if (!blog || !blogPost) {\n    return notFound();\n  }\n\n  return {\n    author: blogPost.author ?? undefined,\n    title: blogPost.name,\n    content: blogPost.htmlBody,\n    date: format.dateTime(new Date(blogPost.publishedDate.utc)),\n    image: blogPost.thumbnailImage\n      ? { alt: blogPost.thumbnailImage.altText, src: blogPost.thumbnailImage.url }\n      : undefined,\n    tags: blogPost.tags.map((tag) => ({\n      label: tag,\n      link: {\n        href: `${blog.path}?tag=${tag}`,\n      },\n    })),\n  };\n}\n\nasync function getBlogPostBreadcrumbs(props: Props): Promise<Breadcrumb[]> {\n  const t = await getTranslations('Blog');\n\n  const { blogId } = await props.params;\n\n  const variables = cachedBlogPageDataVariables(blogId);\n\n  const blog = await getBlogPageData(variables);\n  const blogPost = blog?.post;\n\n  if (!blog || !blogPost) {\n    return notFound();\n  }\n\n  return [\n    {\n      label: t('home'),\n      href: '/',\n    },\n    {\n      label: blog.name,\n      href: blog.path,\n    },\n    {\n      label: blogPost.name,\n      href: '#',\n    },\n  ];\n}\n\nexport default async function Blog(props: Props) {\n  const { locale } = await props.params;\n\n  setRequestLocale(locale);\n\n  return (\n    <BlogPostContent blogPost={getBlogPost(props)} breadcrumbs={getBlogPostBreadcrumbs(props)} />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/blog/page-data.ts",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { getFormatter } from 'next-intl/server';\nimport { cache } from 'react';\n\nimport { client } from '~/client';\nimport { PaginationFragment } from '~/client/fragments/pagination';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\n\nconst BlogQuery = graphql(`\n  query BlogQuery {\n    site {\n      content {\n        blog {\n          name\n          description\n          path\n        }\n      }\n    }\n  }\n`);\n\nconst BlogPostsPageQuery = graphql(\n  `\n    query BlogPostsPageQuery(\n      $first: Int\n      $after: String\n      $last: Int\n      $before: String\n      $filters: BlogPostsFiltersInput\n    ) {\n      site {\n        content {\n          blog {\n            posts(first: $first, after: $after, last: $last, before: $before, filters: $filters) {\n              edges {\n                node {\n                  author\n                  entityId\n                  name\n                  path\n                  plainTextSummary\n                  publishedDate {\n                    utc\n                  }\n                  thumbnailImage {\n                    url: urlTemplate(lossy: true)\n                    altText\n                  }\n                }\n              }\n              pageInfo {\n                ...PaginationFragment\n              }\n            }\n          }\n        }\n      }\n    }\n  `,\n  [PaginationFragment],\n);\n\ninterface BlogPostsFiltersInput {\n  tag: string | null;\n}\n\ninterface Pagination {\n  limit: number;\n  before: string | null;\n  after: string | null;\n}\n\nexport const getBlog = cache(async () => {\n  const response = await client.fetch({\n    document: BlogQuery,\n    fetchOptions: { next: { revalidate } },\n  });\n\n  return response.data.site.content.blog;\n});\n\nexport const getBlogPosts = cache(\n  async ({ tag, limit = 9, before, after }: BlogPostsFiltersInput & Pagination) => {\n    const filterArgs = tag ? { filters: { tags: [tag] } } : {};\n    const paginationArgs = before ? { last: limit, before } : { first: limit, after };\n\n    const response = await client.fetch({\n      document: BlogPostsPageQuery,\n      variables: { ...filterArgs, ...paginationArgs },\n      fetchOptions: { next: { revalidate } },\n    });\n\n    const { blog } = response.data.site.content;\n\n    if (!blog) {\n      return null;\n    }\n\n    const format = await getFormatter();\n\n    return {\n      pageInfo: blog.posts.pageInfo,\n      posts: removeEdgesAndNodes(blog.posts).map((post) => ({\n        id: String(post.entityId),\n        author: post.author,\n        content: post.plainTextSummary,\n        date: format.dateTime(new Date(post.publishedDate.utc)),\n        image: post.thumbnailImage\n          ? {\n              src: post.thumbnailImage.url,\n              alt: post.thumbnailImage.altText,\n            }\n          : undefined,\n        href: post.path,\n        title: post.name,\n      })),\n    };\n  },\n);\n"
  },
  {
    "path": "core/app/[locale]/(default)/blog/page.tsx",
    "content": "import type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { getTranslations, setRequestLocale } from 'next-intl/server';\nimport { SearchParams } from 'nuqs';\nimport { createSearchParamsCache, parseAsInteger, parseAsString } from 'nuqs/server';\n\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { FeaturedBlogPostList } from '@/vibes/soul/sections/featured-blog-post-list';\nimport { defaultPageInfo, pageInfoTransformer } from '~/data-transformers/page-info-transformer';\nimport { getMetadataAlternates } from '~/lib/seo/canonical';\n\nimport { getBlog, getBlogPosts } from './page-data';\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n  searchParams: Promise<SearchParams>;\n}\n\nconst defaultPostLimit = 9;\n\nconst searchParamsCache = createSearchParamsCache({\n  tag: parseAsString,\n  before: parseAsString,\n  after: parseAsString,\n  limit: parseAsInteger.withDefault(defaultPostLimit),\n});\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'Blog' });\n  const blog = await getBlog();\n\n  const description =\n    blog?.description && blog.description.length > 150\n      ? `${blog.description.substring(0, 150)}...`\n      : blog?.description;\n\n  return {\n    title: blog?.name ?? t('title'),\n    ...(description && { description }),\n    ...(blog?.path && { alternates: await getMetadataAlternates({ path: blog.path, locale }) }),\n  };\n}\n\nasync function listBlogPosts(searchParamsPromise: Promise<SearchParams>) {\n  const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise);\n  const blogPosts = await getBlogPosts(searchParamsParsed);\n  const posts = blogPosts?.posts ?? [];\n\n  return posts;\n}\n\nasync function getEmptyStateTitle(): Promise<string | null> {\n  const t = await getTranslations('Blog.Empty');\n\n  return t('title');\n}\n\nasync function getEmptyStateSubtitle(): Promise<string | null> {\n  const t = await getTranslations('Blog.Empty');\n\n  return t('subtitle');\n}\n\nasync function getPaginationInfo(searchParamsPromise: Promise<SearchParams>) {\n  const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise);\n  const blogPosts = await getBlogPosts(searchParamsParsed);\n\n  return pageInfoTransformer(blogPosts?.pageInfo ?? defaultPageInfo);\n}\n\nexport default async function Blog(props: Props) {\n  const { locale } = await props.params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Blog');\n\n  const searchParamsParsed = searchParamsCache.parse(await props.searchParams);\n  const { tag } = searchParamsParsed;\n  const blog = await getBlog();\n\n  if (!blog) {\n    return notFound();\n  }\n\n  const tagCrumb = tag ? [{ label: tag, href: '#' }] : [];\n\n  return (\n    <FeaturedBlogPostList\n      breadcrumbs={[\n        {\n          label: t('home'),\n          href: '/',\n        },\n        {\n          label: blog.name,\n          href: tag ? blog.path : '#',\n        },\n        ...tagCrumb,\n      ]}\n      description={blog.description}\n      emptyStateSubtitle={Streamable.from(getEmptyStateSubtitle)}\n      emptyStateTitle={Streamable.from(getEmptyStateTitle)}\n      paginationInfo={Streamable.from(() => getPaginationInfo(props.searchParams))}\n      placeholderCount={6}\n      posts={Streamable.from(() => listBlogPosts(props.searchParams))}\n      title={blog.name}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_actions/add-shipping-cost.ts",
    "content": "'use server';\n\nimport { revalidateTag } from 'next/cache';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\n\nconst SelectCheckoutShippingOptionMutation = graphql(`\n  mutation SelectCheckoutShippingOption($input: SelectCheckoutShippingOptionInput!) {\n    checkout {\n      selectCheckoutShippingOption(input: $input) {\n        checkout {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\ninterface Props {\n  checkoutEntityId: string;\n  consignmentEntityId: string;\n  shippingOptionEntityId: string;\n}\n\nexport const addShippingCost = async ({\n  checkoutEntityId,\n  consignmentEntityId,\n  shippingOptionEntityId,\n}: Props) => {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const response = await client.fetch({\n    document: SelectCheckoutShippingOptionMutation,\n    variables: {\n      input: {\n        checkoutEntityId,\n        consignmentEntityId,\n        data: {\n          shippingOptionEntityId,\n        },\n      },\n    },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n\n  const result = response.data.checkout.selectCheckoutShippingOption?.checkout;\n\n  revalidateTag(TAGS.checkout, { expire: 0 });\n\n  return result;\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_actions/add-shipping-info.ts",
    "content": "'use server';\n\nimport { revalidateTag } from 'next/cache';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\n\nconst AddCheckoutShippingConsignmentsMutation = graphql(`\n  mutation AddCheckoutShippingConsignments($input: AddCheckoutShippingConsignmentsInput!) {\n    checkout {\n      addCheckoutShippingConsignments(input: $input) {\n        checkout {\n          entityId\n          shippingConsignments {\n            availableShippingOptions {\n              cost {\n                value\n              }\n              description\n              entityId\n            }\n          }\n        }\n      }\n    }\n  }\n`);\n\ninterface AddProps {\n  checkoutEntityId: string;\n  address: {\n    countryCode: string;\n    city?: string;\n    stateOrProvince?: string;\n    postalCode?: string;\n  };\n  lineItems: Array<{ quantity: number; lineItemEntityId: string }>;\n}\n\nexport const addCheckoutShippingConsignments = async ({\n  checkoutEntityId,\n  address,\n  lineItems,\n}: AddProps) => {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const response = await client.fetch({\n    document: AddCheckoutShippingConsignmentsMutation,\n    variables: {\n      input: {\n        checkoutEntityId,\n        data: {\n          consignments: [\n            {\n              address: {\n                ...address,\n                shouldSaveAddress: false,\n              },\n              lineItems,\n            },\n          ],\n        },\n      },\n    },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n\n  revalidateTag(TAGS.checkout, { expire: 0 });\n\n  return response.data.checkout.addCheckoutShippingConsignments?.checkout;\n};\n\nconst UpdateCheckoutShippingConsignmentMutation = graphql(`\n  mutation UpdateCheckoutShippingConsignment($input: UpdateCheckoutShippingConsignmentInput!) {\n    checkout {\n      updateCheckoutShippingConsignment(input: $input) {\n        checkout {\n          entityId\n          shippingConsignments {\n            availableShippingOptions {\n              cost {\n                value\n              }\n              description\n              entityId\n            }\n          }\n        }\n      }\n    }\n  }\n`);\n\ninterface UpdateProps {\n  checkoutEntityId: string;\n  shippingId: string;\n  address: {\n    countryCode: string;\n    city?: string;\n    stateOrProvince?: string;\n    postalCode?: string;\n  };\n  lineItems: Array<{ quantity: number; lineItemEntityId: string }>;\n}\n\nexport const updateCheckoutShippingConsignment = async ({\n  checkoutEntityId,\n  address,\n  shippingId,\n  lineItems,\n}: UpdateProps) => {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const response = await client.fetch({\n    document: UpdateCheckoutShippingConsignmentMutation,\n    variables: {\n      input: {\n        checkoutEntityId,\n        consignmentEntityId: shippingId,\n        data: {\n          consignment: {\n            address: {\n              ...address,\n              shouldSaveAddress: false,\n            },\n            lineItems,\n          },\n        },\n      },\n    },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n\n  revalidateTag(TAGS.checkout, { expire: 0 });\n\n  return response.data.checkout.updateCheckoutShippingConsignment?.checkout;\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_actions/apply-coupon-code.ts",
    "content": "'use server';\n\nimport { revalidateTag } from 'next/cache';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\n\nconst ApplyCheckoutCouponMutation = graphql(`\n  mutation ApplyCheckoutCouponMutation($applyCheckoutCouponInput: ApplyCheckoutCouponInput!) {\n    checkout {\n      applyCheckoutCoupon(input: $applyCheckoutCouponInput) {\n        checkout {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\ntype Variables = VariablesOf<typeof ApplyCheckoutCouponMutation>;\n\ninterface Props {\n  checkoutEntityId: Variables['applyCheckoutCouponInput']['checkoutEntityId'];\n  couponCode: Variables['applyCheckoutCouponInput']['data']['couponCode'];\n}\n\nexport const applyCouponCode = async ({ checkoutEntityId, couponCode }: Props) => {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const response = await client.fetch({\n    document: ApplyCheckoutCouponMutation,\n    variables: {\n      applyCheckoutCouponInput: {\n        checkoutEntityId,\n        data: {\n          couponCode,\n        },\n      },\n    },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n\n  const checkout = response.data.checkout.applyCheckoutCoupon?.checkout;\n\n  revalidateTag(TAGS.checkout, { expire: 0 });\n\n  return checkout;\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_actions/apply-gift-certificate.ts",
    "content": "'use server';\n\nimport { revalidateTag } from 'next/cache';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\n\nconst ApplyCheckoutGiftCertificateMutation = graphql(`\n  mutation ApplyCheckoutGiftCertificateMutation(\n    $applyCheckoutGiftCertificateInput: ApplyCheckoutGiftCertificateInput!\n  ) {\n    checkout {\n      applyCheckoutGiftCertificate(input: $applyCheckoutGiftCertificateInput) {\n        checkout {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\ntype Variables = VariablesOf<typeof ApplyCheckoutGiftCertificateMutation>;\n\ninterface Props {\n  checkoutEntityId: Variables['applyCheckoutGiftCertificateInput']['checkoutEntityId'];\n  giftCertificateCode: Variables['applyCheckoutGiftCertificateInput']['data']['giftCertificateCode'];\n}\n\nexport const applyGiftCertificate = async ({ checkoutEntityId, giftCertificateCode }: Props) => {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const response = await client.fetch({\n    document: ApplyCheckoutGiftCertificateMutation,\n    variables: {\n      applyCheckoutGiftCertificateInput: {\n        checkoutEntityId,\n        data: {\n          giftCertificateCode,\n        },\n      },\n    },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n\n  const checkout = response.data.checkout.applyCheckoutGiftCertificate?.checkout;\n\n  revalidateTag(TAGS.checkout, { expire: 0 });\n\n  return checkout;\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_actions/remove-coupon-code.ts",
    "content": "'use server';\n\nimport { revalidateTag } from 'next/cache';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\n\nconst UnapplyCheckoutCouponMutation = graphql(`\n  mutation UnapplyCheckoutCouponMutation($unapplyCheckoutCouponInput: UnapplyCheckoutCouponInput!) {\n    checkout {\n      unapplyCheckoutCoupon(input: $unapplyCheckoutCouponInput) {\n        checkout {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\ntype Variables = VariablesOf<typeof UnapplyCheckoutCouponMutation>;\n\ninterface Props {\n  checkoutEntityId: Variables['unapplyCheckoutCouponInput']['checkoutEntityId'];\n  couponCode: Variables['unapplyCheckoutCouponInput']['data']['couponCode'];\n}\n\nexport const removeCouponCode = async ({ checkoutEntityId, couponCode }: Props) => {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const response = await client.fetch({\n    document: UnapplyCheckoutCouponMutation,\n    variables: {\n      unapplyCheckoutCouponInput: {\n        checkoutEntityId,\n        data: {\n          couponCode,\n        },\n      },\n    },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n\n  const checkout = response.data.checkout.unapplyCheckoutCoupon?.checkout;\n\n  revalidateTag(TAGS.checkout, { expire: 0 });\n\n  return checkout;\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_actions/remove-gift-certificate.ts",
    "content": "'use server';\n\nimport { revalidateTag } from 'next/cache';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\n\nconst UnapplyCheckoutGiftCertificateMutation = graphql(`\n  mutation UnapplyCheckoutGiftCertificateMutation(\n    $unapplyCheckoutGiftCertificateInput: UnapplyCheckoutGiftCertificateInput!\n  ) {\n    checkout {\n      unapplyCheckoutGiftCertificate(input: $unapplyCheckoutGiftCertificateInput) {\n        checkout {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\ntype Variables = VariablesOf<typeof UnapplyCheckoutGiftCertificateMutation>;\n\ninterface Props {\n  checkoutEntityId: Variables['unapplyCheckoutGiftCertificateInput']['checkoutEntityId'];\n  giftCertificateCode: Variables['unapplyCheckoutGiftCertificateInput']['data']['giftCertificateCode'];\n}\n\nexport const removeGiftCertificate = async ({ checkoutEntityId, giftCertificateCode }: Props) => {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const response = await client.fetch({\n    document: UnapplyCheckoutGiftCertificateMutation,\n    variables: {\n      unapplyCheckoutGiftCertificateInput: {\n        checkoutEntityId,\n        data: {\n          giftCertificateCode,\n        },\n      },\n    },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n\n  const checkout = response.data.checkout.unapplyCheckoutGiftCertificate?.checkout;\n\n  revalidateTag(TAGS.checkout, { expire: 0 });\n\n  return checkout;\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_actions/remove-item.ts",
    "content": "'use server';\n\nimport { revalidateTag } from 'next/cache';\nimport { getTranslations } from 'next-intl/server';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\nimport { clearCartId, getCartId } from '~/lib/cart';\n\nconst DeleteCartLineItemMutation = graphql(`\n  mutation DeleteCartLineItemMutation($input: DeleteCartLineItemInput!) {\n    cart {\n      deleteCartLineItem(input: $input) {\n        cart {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\ntype Variables = VariablesOf<typeof DeleteCartLineItemMutation>;\ntype DeleteCartLineItemInput = Variables['input'];\n\nexport async function removeItem({\n  lineItemEntityId,\n}: Omit<DeleteCartLineItemInput, 'cartEntityId'>) {\n  const t = await getTranslations('Cart.Errors');\n\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const cartId = await getCartId();\n\n  if (!cartId) {\n    throw new Error(t('cartNotFound'));\n  }\n\n  if (!lineItemEntityId) {\n    throw new Error(t('lineItemNotFound'));\n  }\n\n  const response = await client.fetch({\n    document: DeleteCartLineItemMutation,\n    variables: {\n      input: {\n        cartEntityId: cartId,\n        lineItemEntityId,\n      },\n    },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n\n  const cart = response.data.cart.deleteCartLineItem?.cart;\n\n  // If we remove the last item in a cart the cart is deleted\n  // so we need to remove the cartId cookie\n  // TODO: We need to figure out if it actually failed.\n  if (!cart) {\n    await clearCartId();\n  }\n\n  revalidateTag(TAGS.cart, { expire: 0 });\n\n  return cart;\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_actions/update-coupon-code.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getTranslations } from 'next-intl/server';\n\nimport { couponCodeActionFormDataSchema } from '@/vibes/soul/sections/cart/schema';\nimport { getCartId } from '~/lib/cart';\n\nimport { getCart } from '../page-data';\n\nimport { applyCouponCode } from './apply-coupon-code';\nimport { removeCouponCode } from './remove-coupon-code';\n\nexport const updateCouponCode = async (\n  prevState: Awaited<{\n    couponCodes: string[];\n    lastResult: SubmissionResult | null;\n  }>,\n  formData: FormData,\n): Promise<{\n  couponCodes: string[];\n  lastResult: SubmissionResult | null;\n}> => {\n  const t = await getTranslations('Cart.CheckoutSummary.CouponCode');\n  const submission = parseWithZod(formData, {\n    schema: couponCodeActionFormDataSchema({ required_error: t('invalidCouponCode') }),\n  });\n\n  const cartId = await getCartId();\n\n  if (!cartId) {\n    return { ...prevState, lastResult: submission.reply({ formErrors: [t('cartNotFound')] }) };\n  }\n\n  const cart = await getCart({ cartId });\n  const checkout = cart.site.checkout;\n\n  if (!checkout) {\n    return { ...prevState, lastResult: submission.reply({ formErrors: [t('cartNotFound')] }) };\n  }\n\n  const checkoutEntityId = checkout.entityId;\n\n  if (!checkoutEntityId) {\n    return { ...prevState, lastResult: submission.reply({ formErrors: [t('cartNotFound')] }) };\n  }\n\n  if (submission.status !== 'success') {\n    return {\n      ...prevState,\n      lastResult: submission.reply(),\n    };\n  }\n\n  switch (submission.value.intent) {\n    case 'apply': {\n      try {\n        await applyCouponCode({\n          checkoutEntityId,\n          couponCode: submission.value.couponCode,\n        });\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.error(error);\n\n        if (error instanceof BigCommerceGQLError) {\n          return {\n            ...prevState,\n            lastResult: submission.reply({\n              formErrors: error.errors.map(({ message }) => {\n                if (message.includes('Incorrect or mismatch:')) {\n                  return t('invalidCouponCode');\n                }\n\n                return message;\n              }),\n            }),\n          };\n        }\n\n        if (error instanceof Error) {\n          return { ...prevState, lastResult: submission.reply({ formErrors: [error.message] }) };\n        }\n\n        return { ...prevState, lastResult: submission.reply({ formErrors: [String(error)] }) };\n      }\n\n      const couponCode = submission.value.couponCode;\n\n      return {\n        couponCodes: [...prevState.couponCodes, couponCode],\n        lastResult: submission.reply({ resetForm: true }),\n      };\n    }\n\n    case 'delete': {\n      try {\n        await removeCouponCode({\n          checkoutEntityId,\n          couponCode: submission.value.couponCode,\n        });\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.error(error);\n\n        if (error instanceof BigCommerceGQLError) {\n          return {\n            ...prevState,\n            lastResult: submission.reply({\n              formErrors: error.errors.map(({ message }) => message),\n            }),\n          };\n        }\n\n        if (error instanceof Error) {\n          return { ...prevState, lastResult: submission.reply({ formErrors: [error.message] }) };\n        }\n\n        return { ...prevState, lastResult: submission.reply({ formErrors: [String(error)] }) };\n      }\n\n      const couponCode = submission.value.couponCode;\n\n      return {\n        couponCodes: prevState.couponCodes.filter((item) => item !== couponCode),\n        lastResult: submission.reply({ resetForm: true }),\n      };\n    }\n\n    default: {\n      return prevState;\n    }\n  }\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_actions/update-gift-certificate.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getTranslations } from 'next-intl/server';\n\nimport { giftCertificateCodeActionFormDataSchema } from '@/vibes/soul/sections/cart/schema';\nimport { getCartId } from '~/lib/cart';\n\nimport { getCart } from '../page-data';\n\nimport { applyGiftCertificate } from './apply-gift-certificate';\nimport { removeGiftCertificate } from './remove-gift-certificate';\n\nexport const updateGiftCertificate = async (\n  prevState: Awaited<{\n    giftCertificateCodes: string[];\n    lastResult: SubmissionResult | null;\n  }>,\n  formData: FormData,\n): Promise<{\n  giftCertificateCodes: string[];\n  lastResult: SubmissionResult | null;\n}> => {\n  const t = await getTranslations('Cart.GiftCertificate');\n  const submission = parseWithZod(formData, {\n    schema: giftCertificateCodeActionFormDataSchema({\n      required_error: t('invalidGiftCertificate'),\n    }),\n  });\n\n  const cartId = await getCartId();\n\n  if (!cartId) {\n    return { ...prevState, lastResult: submission.reply({ formErrors: [t('cartNotFound')] }) };\n  }\n\n  const cart = await getCart({ cartId });\n  const checkout = cart.site.checkout;\n\n  if (!checkout) {\n    return { ...prevState, lastResult: submission.reply({ formErrors: [t('cartNotFound')] }) };\n  }\n\n  const checkoutEntityId = checkout.entityId;\n\n  if (!checkoutEntityId) {\n    return { ...prevState, lastResult: submission.reply({ formErrors: [t('cartNotFound')] }) };\n  }\n\n  if (submission.status !== 'success') {\n    return {\n      ...prevState,\n      lastResult: submission.reply(),\n    };\n  }\n\n  switch (submission.value.intent) {\n    case 'apply': {\n      try {\n        await applyGiftCertificate({\n          checkoutEntityId,\n          giftCertificateCode: submission.value.giftCertificateCode,\n        });\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.error(error);\n\n        if (error instanceof BigCommerceGQLError) {\n          return {\n            ...prevState,\n            lastResult: submission.reply({\n              formErrors: error.errors.map(({ message }) => {\n                if (message.includes('Incorrect or mismatch:')) {\n                  return t('invalidGiftCertificate');\n                }\n\n                return message;\n              }),\n            }),\n          };\n        }\n\n        if (error instanceof Error) {\n          return { ...prevState, lastResult: submission.reply({ formErrors: [error.message] }) };\n        }\n\n        return { ...prevState, lastResult: submission.reply({ formErrors: [String(error)] }) };\n      }\n\n      const giftCertificateCode = submission.value.giftCertificateCode;\n\n      return {\n        giftCertificateCodes: [...prevState.giftCertificateCodes, giftCertificateCode],\n        lastResult: submission.reply({ resetForm: true }),\n      };\n    }\n\n    case 'delete': {\n      try {\n        await removeGiftCertificate({\n          checkoutEntityId,\n          giftCertificateCode: submission.value.giftCertificateCode,\n        });\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.error(error);\n\n        if (error instanceof BigCommerceGQLError) {\n          return {\n            ...prevState,\n            lastResult: submission.reply({\n              formErrors: error.errors.map(({ message }) => message),\n            }),\n          };\n        }\n\n        if (error instanceof Error) {\n          return { ...prevState, lastResult: submission.reply({ formErrors: [error.message] }) };\n        }\n\n        return { ...prevState, lastResult: submission.reply({ formErrors: [String(error)] }) };\n      }\n\n      const giftCertificateCode = submission.value.giftCertificateCode;\n\n      return {\n        giftCertificateCodes: prevState.giftCertificateCodes.filter(\n          (item) => item !== giftCertificateCode,\n        ),\n        lastResult: submission.reply({ resetForm: true }),\n      };\n    }\n\n    default: {\n      return prevState;\n    }\n  }\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_actions/update-line-item.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { FragmentOf } from 'gql.tada';\nimport { getTranslations } from 'next-intl/server';\n\nimport { CartLineItem } from '@/vibes/soul/sections/cart';\nimport { cartLineItemActionFormDataSchema } from '@/vibes/soul/sections/cart/schema';\n\nimport { DigitalItemFragment, PhysicalItemFragment } from '../page-data';\n\nimport { removeItem } from './remove-item';\nimport { CartSelectedOptionsInput, updateQuantity } from './update-quantity';\n\ntype LineItem = {\n  selectedOptions:\n    | FragmentOf<typeof PhysicalItemFragment>['selectedOptions']\n    | FragmentOf<typeof DigitalItemFragment>['selectedOptions'];\n  productEntityId: number;\n  variantEntityId: number | null;\n} & CartLineItem;\n\nexport const updateLineItem = async (\n  prevState: Awaited<{\n    lineItems: LineItem[];\n    lastResult: SubmissionResult | null;\n  }>,\n  formData: FormData,\n): Promise<{\n  lineItems: LineItem[];\n  lastResult: SubmissionResult | null;\n}> => {\n  const t = await getTranslations('Cart.Errors');\n\n  const submission = parseWithZod(formData, { schema: cartLineItemActionFormDataSchema });\n\n  if (submission.status !== 'success') {\n    return {\n      ...prevState,\n      lastResult: submission.reply(),\n    };\n  }\n\n  const cartLineItem = prevState.lineItems.find((item) => item.id === submission.value.id);\n\n  if (!cartLineItem) {\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: [t('lineItemNotFound')] }),\n    };\n  }\n\n  switch (submission.value.intent) {\n    case 'increment': {\n      const parsedSelectedOptions = cartLineItem.selectedOptions.reduce<CartSelectedOptionsInput>(\n        (accum, option) => {\n          let multipleChoicesOptionInput;\n          let checkboxOptionInput;\n          let numberFieldOptionInput;\n          let textFieldOptionInput;\n          let multiLineTextFieldOptionInput;\n          let dateFieldOptionInput;\n\n          switch (option.__typename) {\n            case 'CartSelectedMultipleChoiceOption':\n              multipleChoicesOptionInput = {\n                optionEntityId: option.entityId,\n                optionValueEntityId: option.valueEntityId,\n              };\n\n              if (accum.multipleChoices) {\n                return {\n                  ...accum,\n                  multipleChoices: [...accum.multipleChoices, multipleChoicesOptionInput],\n                };\n              }\n\n              return {\n                ...accum,\n                multipleChoices: [multipleChoicesOptionInput],\n              };\n\n            case 'CartSelectedCheckboxOption':\n              checkboxOptionInput = {\n                optionEntityId: option.entityId,\n                optionValueEntityId: option.valueEntityId,\n              };\n\n              if (accum.checkboxes) {\n                return {\n                  ...accum,\n                  checkboxes: [...accum.checkboxes, checkboxOptionInput],\n                };\n              }\n\n              return { ...accum, checkboxes: [checkboxOptionInput] };\n\n            case 'CartSelectedNumberFieldOption':\n              numberFieldOptionInput = {\n                optionEntityId: option.entityId,\n                number: option.number,\n              };\n\n              if (accum.numberFields) {\n                return {\n                  ...accum,\n                  numberFields: [...accum.numberFields, numberFieldOptionInput],\n                };\n              }\n\n              return { ...accum, numberFields: [numberFieldOptionInput] };\n\n            case 'CartSelectedTextFieldOption':\n              textFieldOptionInput = {\n                optionEntityId: option.entityId,\n                text: option.text,\n              };\n\n              if (accum.textFields) {\n                return {\n                  ...accum,\n                  textFields: [...accum.textFields, textFieldOptionInput],\n                };\n              }\n\n              return { ...accum, textFields: [textFieldOptionInput] };\n\n            case 'CartSelectedMultiLineTextFieldOption':\n              multiLineTextFieldOptionInput = {\n                optionEntityId: option.entityId,\n                text: option.text,\n              };\n\n              if (accum.multiLineTextFields) {\n                return {\n                  ...accum,\n                  multiLineTextFields: [\n                    ...accum.multiLineTextFields,\n                    multiLineTextFieldOptionInput,\n                  ],\n                };\n              }\n\n              return {\n                ...accum,\n                multiLineTextFields: [multiLineTextFieldOptionInput],\n              };\n\n            case 'CartSelectedDateFieldOption':\n              dateFieldOptionInput = {\n                optionEntityId: option.entityId,\n                date: new Date(String(option.date.utc)).toISOString(),\n              };\n\n              if (accum.dateFields) {\n                return {\n                  ...accum,\n                  dateFields: [...accum.dateFields, dateFieldOptionInput],\n                };\n              }\n\n              return { ...accum, dateFields: [dateFieldOptionInput] };\n          }\n\n          return accum;\n        },\n        {},\n      );\n\n      try {\n        await updateQuantity({\n          lineItemEntityId: cartLineItem.id,\n          productEntityId: cartLineItem.productEntityId,\n          variantEntityId: cartLineItem.variantEntityId,\n          selectedOptions: parsedSelectedOptions,\n          quantity: cartLineItem.quantity + 1,\n        });\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.error(error);\n\n        if (error instanceof BigCommerceGQLError) {\n          return {\n            ...prevState,\n            lastResult: submission.reply({\n              formErrors: error.errors.map(({ message }) => message),\n            }),\n          };\n        }\n\n        if (error instanceof Error) {\n          return { ...prevState, lastResult: submission.reply({ formErrors: [error.message] }) };\n        }\n\n        return { ...prevState, lastResult: submission.reply({ formErrors: [String(error)] }) };\n      }\n\n      const item = submission.value;\n\n      return {\n        lineItems: prevState.lineItems.map((lineItem) =>\n          lineItem.id === item.id ? { ...lineItem, quantity: lineItem.quantity + 1 } : lineItem,\n        ),\n        lastResult: submission.reply({ resetForm: true }),\n      };\n    }\n\n    case 'decrement': {\n      const parsedSelectedOptions = cartLineItem.selectedOptions.reduce<CartSelectedOptionsInput>(\n        (accum, option) => {\n          let multipleChoicesOptionInput;\n          let checkboxOptionInput;\n          let numberFieldOptionInput;\n          let textFieldOptionInput;\n          let multiLineTextFieldOptionInput;\n          let dateFieldOptionInput;\n\n          switch (option.__typename) {\n            case 'CartSelectedMultipleChoiceOption':\n              multipleChoicesOptionInput = {\n                optionEntityId: option.entityId,\n                optionValueEntityId: option.valueEntityId,\n              };\n\n              if (accum.multipleChoices) {\n                return {\n                  ...accum,\n                  multipleChoices: [...accum.multipleChoices, multipleChoicesOptionInput],\n                };\n              }\n\n              return {\n                ...accum,\n                multipleChoices: [multipleChoicesOptionInput],\n              };\n\n            case 'CartSelectedCheckboxOption':\n              checkboxOptionInput = {\n                optionEntityId: option.entityId,\n                optionValueEntityId: option.valueEntityId,\n              };\n\n              if (accum.checkboxes) {\n                return {\n                  ...accum,\n                  checkboxes: [...accum.checkboxes, checkboxOptionInput],\n                };\n              }\n\n              return { ...accum, checkboxes: [checkboxOptionInput] };\n\n            case 'CartSelectedNumberFieldOption':\n              numberFieldOptionInput = {\n                optionEntityId: option.entityId,\n                number: option.number,\n              };\n\n              if (accum.numberFields) {\n                return {\n                  ...accum,\n                  numberFields: [...accum.numberFields, numberFieldOptionInput],\n                };\n              }\n\n              return { ...accum, numberFields: [numberFieldOptionInput] };\n\n            case 'CartSelectedTextFieldOption':\n              textFieldOptionInput = {\n                optionEntityId: option.entityId,\n                text: option.text,\n              };\n\n              if (accum.textFields) {\n                return {\n                  ...accum,\n                  textFields: [...accum.textFields, textFieldOptionInput],\n                };\n              }\n\n              return { ...accum, textFields: [textFieldOptionInput] };\n\n            case 'CartSelectedMultiLineTextFieldOption':\n              multiLineTextFieldOptionInput = {\n                optionEntityId: option.entityId,\n                text: option.text,\n              };\n\n              if (accum.multiLineTextFields) {\n                return {\n                  ...accum,\n                  multiLineTextFields: [\n                    ...accum.multiLineTextFields,\n                    multiLineTextFieldOptionInput,\n                  ],\n                };\n              }\n\n              return {\n                ...accum,\n                multiLineTextFields: [multiLineTextFieldOptionInput],\n              };\n\n            case 'CartSelectedDateFieldOption':\n              dateFieldOptionInput = {\n                optionEntityId: option.entityId,\n                date: new Date(String(option.date.utc)).toISOString(),\n              };\n\n              if (accum.dateFields) {\n                return {\n                  ...accum,\n                  dateFields: [...accum.dateFields, dateFieldOptionInput],\n                };\n              }\n\n              return { ...accum, dateFields: [dateFieldOptionInput] };\n          }\n\n          return accum;\n        },\n        {},\n      );\n\n      try {\n        await updateQuantity({\n          lineItemEntityId: cartLineItem.id,\n          productEntityId: cartLineItem.productEntityId,\n          variantEntityId: cartLineItem.variantEntityId,\n          selectedOptions: parsedSelectedOptions,\n          quantity: cartLineItem.quantity - 1,\n        });\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.error(error);\n\n        if (error instanceof BigCommerceGQLError) {\n          return {\n            ...prevState,\n            lastResult: submission.reply({\n              formErrors: error.errors.map(({ message }) => message),\n            }),\n          };\n        }\n\n        if (error instanceof Error) {\n          return { ...prevState, lastResult: submission.reply({ formErrors: [error.message] }) };\n        }\n\n        return { ...prevState, lastResult: submission.reply({ formErrors: [String(error)] }) };\n      }\n\n      const item = submission.value;\n\n      return {\n        lineItems: prevState.lineItems.map((lineItem) =>\n          lineItem.id === item.id ? { ...lineItem, quantity: lineItem.quantity - 1 } : lineItem,\n        ),\n        lastResult: submission.reply({ resetForm: true }),\n      };\n    }\n\n    case 'delete': {\n      try {\n        await removeItem({ lineItemEntityId: submission.value.id });\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.error(error);\n\n        if (error instanceof BigCommerceGQLError) {\n          return {\n            ...prevState,\n            lastResult: submission.reply({\n              formErrors: error.errors.map(({ message }) => message),\n            }),\n          };\n        }\n\n        if (error instanceof Error) {\n          return { ...prevState, lastResult: submission.reply({ formErrors: [error.message] }) };\n        }\n\n        return { ...prevState, lastResult: submission.reply({ formErrors: [String(error)] }) };\n      }\n\n      const deletedItem = submission.value;\n\n      return {\n        lineItems: prevState.lineItems.filter((item) => item.id !== deletedItem.id),\n        lastResult: submission.reply({ resetForm: true }),\n      };\n    }\n\n    default: {\n      return prevState;\n    }\n  }\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_actions/update-quantity.ts",
    "content": "'use server';\n\nimport { revalidatePath } from 'next/cache';\nimport { getTranslations } from 'next-intl/server';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { getCartId } from '~/lib/cart';\n\nimport { removeItem } from './remove-item';\n\nconst UpdateCartLineItemMutation = graphql(`\n  mutation UpdateCartLineItem($input: UpdateCartLineItemInput!) {\n    cart {\n      updateCartLineItem(input: $input) {\n        cart {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\ntype CartLineItemInput = ReturnType<typeof graphql.scalar<'CartLineItemInput'>>;\nexport type CartSelectedOptionsInput = ReturnType<\n  typeof graphql.scalar<'CartSelectedOptionsInput'>\n>;\ntype Variables = VariablesOf<typeof UpdateCartLineItemMutation>;\ntype UpdateCartLineItemInput = Variables['input'];\n\ninterface UpdateProductQuantityParams extends CartLineItemInput {\n  lineItemEntityId: UpdateCartLineItemInput['lineItemEntityId'];\n}\n\nexport const updateQuantity = async ({\n  lineItemEntityId,\n  productEntityId,\n  quantity,\n  variantEntityId,\n  selectedOptions,\n}: UpdateProductQuantityParams) => {\n  const t = await getTranslations('Cart.Errors');\n\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const cartId = await getCartId();\n\n  if (!cartId) {\n    throw new Error(t('cartNotFound'));\n  }\n\n  if (!lineItemEntityId) {\n    throw new Error(t('lineItemNotFound'));\n  }\n\n  if (quantity === 0) {\n    const result = await removeItem({ lineItemEntityId });\n\n    return result;\n  }\n\n  const cartLineItemData = Object.assign(\n    { quantity, productEntityId },\n    variantEntityId && { variantEntityId },\n    selectedOptions && { selectedOptions },\n  );\n\n  const response = await client.fetch({\n    document: UpdateCartLineItemMutation,\n    variables: {\n      input: {\n        cartEntityId: cartId,\n        lineItemEntityId,\n        data: {\n          lineItem: cartLineItemData,\n        },\n      },\n    },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n\n  const cart = response.data.cart.updateCartLineItem?.cart;\n\n  if (!cart) {\n    throw new Error(t('failedToUpdateQuantity'));\n  }\n\n  revalidatePath('/cart');\n\n  return cart;\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_actions/update-shipping-info.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getTranslations } from 'next-intl/server';\n\nimport { shippingActionFormDataSchema } from '@/vibes/soul/sections/cart/schema';\nimport { ShippingFormState } from '@/vibes/soul/sections/cart/shipping-form';\nimport { getCartId } from '~/lib/cart';\n\nimport { getCart } from '../page-data';\n\nimport { addShippingCost } from './add-shipping-cost';\nimport {\n  addCheckoutShippingConsignments,\n  updateCheckoutShippingConsignment,\n} from './add-shipping-info';\n\nexport const updateShippingInfo = async (\n  prevState: Awaited<ShippingFormState>,\n  formData: FormData,\n): Promise<ShippingFormState> => {\n  const t = await getTranslations('Cart.CheckoutSummary.Shipping');\n\n  const submission = parseWithZod(formData, {\n    schema: shippingActionFormDataSchema({ required_error: t('countryRequired') }),\n  });\n\n  const cartId = await getCartId();\n\n  if (!cartId) {\n    return { ...prevState, lastResult: submission.reply({ formErrors: [t('cartNotFound')] }) };\n  }\n\n  const cart = await getCart({ cartId });\n  const checkout = cart.site.checkout;\n\n  if (!checkout || !cart.site.cart) {\n    return { ...prevState, lastResult: submission.reply({ formErrors: [t('cartNotFound')] }) };\n  }\n\n  const checkoutEntityId = checkout.entityId;\n\n  if (!checkoutEntityId) {\n    return { ...prevState, lastResult: submission.reply({ formErrors: [t('cartNotFound')] }) };\n  }\n\n  if (submission.status !== 'success') {\n    return {\n      ...prevState,\n      lastResult: submission.reply(),\n    };\n  }\n\n  const lineItems = [\n    ...cart.site.cart.lineItems.physicalItems,\n    ...cart.site.cart.lineItems.digitalItems,\n  ].map((item) => ({\n    lineItemEntityId: item.entityId,\n    quantity: item.quantity,\n  }));\n\n  const shippingConsignment =\n    checkout.shippingConsignments?.find((consignment) => consignment.selectedShippingOption) ||\n    checkout.shippingConsignments?.[0];\n\n  const shippingId = shippingConsignment?.entityId;\n\n  switch (submission.value.intent) {\n    case 'add-address': {\n      try {\n        const result = shippingId\n          ? await updateCheckoutShippingConsignment({\n              checkoutEntityId,\n              address: {\n                countryCode: submission.value.country,\n                city: submission.value.city,\n                stateOrProvince: submission.value.state,\n                postalCode: submission.value.postalCode,\n              },\n              lineItems,\n              shippingId,\n            })\n          : await addCheckoutShippingConsignments({\n              checkoutEntityId,\n              address: {\n                countryCode: submission.value.country,\n                city: submission.value.city,\n                stateOrProvince: submission.value.state,\n                postalCode: submission.value.postalCode,\n              },\n              lineItems,\n            });\n\n        const updatedShippingConsignment = result ? result.shippingConsignments?.[0] : undefined;\n\n        if (!updatedShippingConsignment?.availableShippingOptions) {\n          return {\n            ...prevState,\n            lastResult: submission.reply({ formErrors: [t('cartNotFound')] }),\n          };\n        }\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.error(error);\n\n        if (error instanceof BigCommerceGQLError) {\n          return {\n            ...prevState,\n            lastResult: submission.reply({\n              formErrors: error.errors.map(({ message }) => message),\n            }),\n          };\n        }\n\n        if (error instanceof Error) {\n          return { ...prevState, lastResult: submission.reply({ formErrors: [error.message] }) };\n        }\n\n        return { ...prevState, lastResult: submission.reply({ formErrors: [String(error)] }) };\n      }\n\n      return {\n        ...prevState,\n        lastResult: submission.reply({ resetForm: true }),\n      };\n    }\n\n    case 'add-shipping': {\n      try {\n        if (!shippingConsignment?.entityId) {\n          return {\n            ...prevState,\n            lastResult: submission.reply({ formErrors: [t('cartNotFound')] }),\n          };\n        }\n\n        await addShippingCost({\n          checkoutEntityId,\n          consignmentEntityId: shippingConsignment.entityId,\n          shippingOptionEntityId: submission.value.shippingOption,\n        });\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.error(error);\n\n        if (error instanceof BigCommerceGQLError) {\n          return {\n            ...prevState,\n            lastResult: submission.reply({\n              formErrors: error.errors.map(({ message }) => message),\n            }),\n          };\n        }\n\n        if (error instanceof Error) {\n          return { ...prevState, lastResult: submission.reply({ formErrors: [error.message] }) };\n        }\n\n        return { ...prevState, lastResult: submission.reply({ formErrors: [String(error)] }) };\n      }\n\n      return {\n        ...prevState,\n        lastResult: submission.reply({ resetForm: true }),\n      };\n    }\n\n    default: {\n      return prevState;\n    }\n  }\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_components/cart-analytics-provider.tsx",
    "content": "'use client';\n\nimport { PropsWithChildren, Suspense } from 'react';\nimport { z } from 'zod';\n\nimport { Streamable, useStreamable } from '@/vibes/soul/lib/streamable';\nimport { EventsProvider } from '~/components/analytics/events';\nimport { useAnalytics } from '~/lib/analytics/react';\n\ninterface AddToCartContext {\n  entityId: string;\n  id: number;\n  name: string;\n  brand: string;\n  sku?: string;\n  currency: string;\n  price: number;\n}\n\nconst AddToCartSchema = z.object({\n  id: z.string(),\n  quantity: z.number({ coerce: true }).default(1),\n});\n\nexport function CartAnalyticsProvider(\n  props: PropsWithChildren<{ data: Streamable<AddToCartContext[]> }>,\n) {\n  return (\n    <Suspense fallback={props.children}>\n      <CartAnalyticsProviderResolved {...props} />\n    </Suspense>\n  );\n}\n\nfunction CartAnalyticsProviderResolved({\n  children,\n  data,\n}: PropsWithChildren<{ data: Streamable<AddToCartContext[]> }>) {\n  const analytics = useAnalytics();\n  const products = useStreamable(data);\n\n  const onAddToCart = (payload?: FormData) => {\n    const parsedPayload = AddToCartSchema.safeParse(Object.fromEntries(payload?.entries() ?? []));\n\n    if (parsedPayload.success) {\n      const { id, quantity } = parsedPayload.data;\n\n      const product = products.find(({ entityId }) => entityId === id);\n\n      if (product) {\n        const { id: productId, name, brand, sku, price, currency } = product;\n\n        analytics?.cart.productAdded({\n          currency,\n          value: quantity * price,\n          items: [\n            {\n              id: productId.toString(),\n              name,\n              brand,\n              sku,\n              price,\n              quantity,\n            },\n          ],\n        });\n      }\n    }\n  };\n\n  const onRemoveFromCart = (payload?: FormData) => {\n    const parsedPayload = AddToCartSchema.safeParse(Object.fromEntries(payload?.entries() ?? []));\n\n    if (parsedPayload.success) {\n      const { id, quantity } = parsedPayload.data;\n\n      const product = products.find(({ entityId }) => entityId === id);\n\n      if (product) {\n        const { id: productId, name, brand, sku, price, currency } = product;\n\n        analytics?.cart.productRemoved({\n          currency,\n          value: quantity * price,\n          items: [\n            {\n              id: productId.toString(),\n              name,\n              brand,\n              sku,\n              price,\n              quantity,\n            },\n          ],\n        });\n      }\n    }\n  };\n\n  return (\n    <EventsProvider onAddToCart={onAddToCart} onRemoveFromCart={onRemoveFromCart}>\n      {children}\n    </EventsProvider>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_components/cart-viewed.tsx",
    "content": "'use client';\n\nimport { useEffect, useRef } from 'react';\n\nimport { FragmentOf } from '~/client/graphql';\nimport { useAnalytics } from '~/lib/analytics/react';\n\nimport {\n  CartGiftCertificateFragment,\n  DigitalItemFragment,\n  PhysicalItemFragment,\n} from '../page-data';\n\ntype PhysicalItem = FragmentOf<typeof PhysicalItemFragment>;\ntype DigitalItem = FragmentOf<typeof DigitalItemFragment>;\ntype GiftCertificateItem = FragmentOf<typeof CartGiftCertificateFragment>;\ntype LineItem = PhysicalItem | DigitalItem | GiftCertificateItem;\n\ninterface Props {\n  subtotal?: number;\n  currencyCode: string;\n  lineItems: LineItem[];\n}\n\nexport const CartViewed = ({ subtotal, currencyCode, lineItems }: Props) => {\n  const isMounted = useRef(false);\n  const analytics = useAnalytics();\n\n  useEffect(() => {\n    if (isMounted.current) {\n      return;\n    }\n\n    isMounted.current = true;\n\n    analytics?.cart.cartViewed({\n      currency: currencyCode,\n      value: subtotal ?? 0,\n      items: lineItems.map((lineItem) => {\n        if (lineItem.__typename === 'CartGiftCertificate') {\n          return {\n            id: lineItem.entityId.toString(),\n            name: lineItem.name,\n            price: lineItem.amount.value,\n            quantity: 1,\n          };\n        }\n\n        return {\n          id: lineItem.productEntityId.toString(),\n          name: lineItem.name,\n          brand: lineItem.brand ?? undefined,\n          sku: lineItem.sku ?? undefined,\n          price: lineItem.listPrice.value,\n          variant_id: lineItem.variantEntityId ?? undefined,\n          quantity: lineItem.quantity,\n        };\n      }),\n    });\n  }, [analytics, currencyCode, lineItems, subtotal]);\n\n  return null;\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/_components/checkout-preconnect.tsx",
    "content": "'use client';\n\nimport { preconnect } from 'react-dom';\n\nexport function CheckoutPreconnect({ url }: { url: string }) {\n  preconnect(url);\n\n  return null;\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/loading.tsx",
    "content": "import { useTranslations } from 'next-intl';\n\nimport { CartSkeleton } from '@/vibes/soul/sections/cart';\n\nexport default function Loading() {\n  const t = useTranslations('Cart');\n\n  return <CartSkeleton title={t('title')} />;\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { TAGS } from '~/client/tags';\n\nexport const PhysicalItemFragment = graphql(`\n  fragment PhysicalItemFragment on CartPhysicalItem {\n    __typename\n    name\n    brand\n    sku\n    image {\n      url: urlTemplate(lossy: true)\n    }\n    entityId\n    quantity\n    productEntityId\n    variantEntityId\n    parentEntityId\n    listPrice {\n      currencyCode\n      value\n    }\n    salePrice {\n      currencyCode\n      value\n    }\n    discountedAmount {\n      currencyCode\n      value\n    }\n    selectedOptions {\n      __typename\n      entityId\n      name\n      ... on CartSelectedMultipleChoiceOption {\n        value\n        valueEntityId\n      }\n      ... on CartSelectedCheckboxOption {\n        value\n        valueEntityId\n      }\n      ... on CartSelectedNumberFieldOption {\n        number\n      }\n      ... on CartSelectedMultiLineTextFieldOption {\n        text\n      }\n      ... on CartSelectedTextFieldOption {\n        text\n      }\n      ... on CartSelectedDateFieldOption {\n        date {\n          utc\n        }\n      }\n    }\n    url\n    stockPosition {\n      backorderMessage\n      quantityOnHand\n      quantityBackordered\n      quantityOutOfStock\n    }\n  }\n`);\n\nexport const DigitalItemFragment = graphql(`\n  fragment DigitalItemFragment on CartDigitalItem {\n    __typename\n    name\n    brand\n    sku\n    image {\n      url: urlTemplate(lossy: true)\n    }\n    entityId\n    quantity\n    productEntityId\n    variantEntityId\n    parentEntityId\n    listPrice {\n      currencyCode\n      value\n    }\n    salePrice {\n      currencyCode\n      value\n    }\n    discountedAmount {\n      currencyCode\n      value\n    }\n    selectedOptions {\n      __typename\n      entityId\n      name\n      ... on CartSelectedMultipleChoiceOption {\n        value\n        valueEntityId\n      }\n      ... on CartSelectedCheckboxOption {\n        value\n        valueEntityId\n      }\n      ... on CartSelectedNumberFieldOption {\n        number\n      }\n      ... on CartSelectedMultiLineTextFieldOption {\n        text\n      }\n      ... on CartSelectedTextFieldOption {\n        text\n      }\n      ... on CartSelectedDateFieldOption {\n        date {\n          utc\n        }\n      }\n    }\n    url\n  }\n`);\n\nexport const CartGiftCertificateFragment = graphql(`\n  fragment CartGiftCertificateFragment on CartGiftCertificate {\n    __typename\n    entityId\n    name\n    message\n    isTaxable\n    sender {\n      name\n      email\n    }\n    recipient {\n      name\n      email\n    }\n    amount {\n      currencyCode\n      value\n    }\n    amountInDisplayCurrency {\n      currencyCode\n      value\n    }\n    theme\n  }\n`);\n\nconst MoneyFieldsFragment = graphql(`\n  fragment MoneyFieldsFragment on Money {\n    currencyCode\n    value\n  }\n`);\n\nconst ShippingInfoFragment = graphql(`\n  fragment ShippingInfoFragment on Checkout {\n    entityId\n    shippingConsignments {\n      entityId\n      availableShippingOptions {\n        cost {\n          value\n        }\n        description\n        entityId\n        isRecommended\n      }\n      selectedShippingOption {\n        entityId\n        description\n        cost {\n          value\n        }\n      }\n      address {\n        city\n        countryCode\n        stateOrProvince\n        postalCode\n      }\n    }\n    handlingCostTotal {\n      value\n    }\n    shippingCostTotal {\n      currencyCode\n      value\n    }\n  }\n`);\n\nconst GeographyFragment = graphql(\n  `\n    fragment GeographyFragment on Geography {\n      countries {\n        entityId\n        name\n        code\n        statesOrProvinces {\n          entityId\n          name\n          abbreviation\n        }\n      }\n    }\n  `,\n  [],\n);\n\nconst CartPageQuery = graphql(\n  `\n    query CartPageQuery($cartId: String, $currencyCode: currencyCode) {\n      site {\n        settings {\n          inventory {\n            defaultOutOfStockMessage\n            showOutOfStockMessage\n            showBackorderMessage\n            showQuantityOnBackorder\n            showQuantityOnHand\n          }\n          url {\n            checkoutUrl\n          }\n          giftCertificates(currencyCode: $currencyCode) {\n            isEnabled\n          }\n        }\n        cart(entityId: $cartId) {\n          entityId\n          version\n          currencyCode\n          discountedAmount {\n            ...MoneyFieldsFragment\n          }\n          lineItems {\n            physicalItems {\n              ...PhysicalItemFragment\n            }\n            digitalItems {\n              ...DigitalItemFragment\n            }\n            giftCertificates {\n              ...CartGiftCertificateFragment\n            }\n            totalQuantity\n          }\n        }\n        checkout(entityId: $cartId) {\n          entityId\n          subtotal {\n            ...MoneyFieldsFragment\n          }\n          grandTotal {\n            ...MoneyFieldsFragment\n          }\n          taxTotal {\n            ...MoneyFieldsFragment\n          }\n          cart {\n            currencyCode\n          }\n          coupons {\n            code\n            discountedAmount {\n              ...MoneyFieldsFragment\n            }\n          }\n          giftCertificates {\n            code\n            balance {\n              ...MoneyFieldsFragment\n            }\n            used {\n              ...MoneyFieldsFragment\n            }\n          }\n          ...ShippingInfoFragment\n        }\n      }\n      geography {\n        ...GeographyFragment\n      }\n    }\n  `,\n  [\n    PhysicalItemFragment,\n    DigitalItemFragment,\n    MoneyFieldsFragment,\n    ShippingInfoFragment,\n    GeographyFragment,\n    CartGiftCertificateFragment,\n  ],\n);\n\ntype Variables = VariablesOf<typeof CartPageQuery>;\n\nexport const getCart = async (variables: Variables) => {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const { data } = await client.fetch({\n    document: CartPageQuery,\n    variables,\n    customerAccessToken,\n    fetchOptions: {\n      cache: 'no-store',\n      next: {\n        tags: [TAGS.cart, TAGS.checkout],\n      },\n    },\n  });\n\n  return data;\n};\n\nconst SupportedShippingDestinationsQuery = graphql(`\n  query SupportedShippingDestinations {\n    site {\n      settings {\n        shipping {\n          supportedShippingDestinations {\n            countries {\n              entityId\n              code\n              name\n              statesOrProvinces {\n                entityId\n                name\n                abbreviation\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n`);\n\nexport const getShippingCountries = cache(async () => {\n  const { data } = await client.fetch({\n    document: SupportedShippingDestinationsQuery,\n    fetchOptions: { next: { revalidate } },\n  });\n\n  return data.site.settings?.shipping?.supportedShippingDestinations.countries ?? [];\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/cart/page.tsx",
    "content": "import { Metadata } from 'next';\nimport { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\n\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { Cart as CartComponent, CartEmptyState } from '@/vibes/soul/sections/cart';\nimport { CartAnalyticsProvider } from '~/app/[locale]/(default)/cart/_components/cart-analytics-provider';\nimport { getCartId } from '~/lib/cart';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\nimport { exists } from '~/lib/utils';\n\nimport { updateCouponCode } from './_actions/update-coupon-code';\nimport { updateGiftCertificate } from './_actions/update-gift-certificate';\nimport { updateLineItem } from './_actions/update-line-item';\nimport { updateShippingInfo } from './_actions/update-shipping-info';\nimport { CartViewed } from './_components/cart-viewed';\nimport { CheckoutPreconnect } from './_components/checkout-preconnect';\nimport { getCart, getShippingCountries } from './page-data';\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n}\n\nconst CHECKOUT_URL = process.env.TRAILING_SLASH !== 'false' ? '/checkout/' : '/checkout';\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'Cart' });\n\n  return {\n    title: t('title'),\n  };\n}\n\nconst getAnalyticsData = async (cartId: string) => {\n  const data = await getCart({ cartId });\n\n  const cart = data.site.cart;\n\n  if (!cart) {\n    return [];\n  }\n\n  const lineItems = [...cart.lineItems.physicalItems, ...cart.lineItems.digitalItems].filter(\n    (item) => !item.parentEntityId, // Only include top-level items\n  );\n\n  return lineItems.map((item) => {\n    return {\n      entityId: item.entityId,\n      id: item.productEntityId,\n      name: item.name,\n      brand: item.brand ?? '',\n      sku: item.sku ?? '',\n      price: item.listPrice.value,\n      quantity: item.quantity,\n      currency: item.listPrice.currencyCode,\n    };\n  });\n};\n\n// eslint-disable-next-line complexity\nexport default async function Cart({ params }: Props) {\n  const { locale } = await params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Cart');\n  const tGiftCertificates = await getTranslations('GiftCertificates');\n  const format = await getFormatter();\n  const cartId = await getCartId();\n\n  if (!cartId) {\n    return (\n      <CartEmptyState\n        cta={{ label: t('Empty.cta'), href: '/shop-all' }}\n        subtitle={t('Empty.subtitle')}\n        title={t('Empty.title')}\n      />\n    );\n  }\n\n  const currencyCode = await getPreferredCurrencyCode();\n  const data = await getCart({ cartId, currencyCode });\n\n  const cart = data.site.cart;\n  const checkout = data.site.checkout;\n  const giftCertificatesEnabled = data.site.settings?.giftCertificates?.isEnabled ?? false;\n\n  if (!cart) {\n    return (\n      <CartEmptyState\n        cta={{ label: t('Empty.cta'), href: '/shop-all' }}\n        subtitle={t('Empty.subtitle')}\n        title={t('Empty.title')}\n      />\n    );\n  }\n\n  const lineItems = [\n    ...cart.lineItems.giftCertificates,\n    ...cart.lineItems.physicalItems,\n    ...cart.lineItems.digitalItems,\n  ].filter((item) => !('parentEntityId' in item) || !item.parentEntityId);\n\n  const formattedLineItems = lineItems.map((item) => {\n    if (item.__typename === 'CartGiftCertificate') {\n      return {\n        typename: item.__typename,\n        id: item.entityId,\n        title: item.name,\n        subtitle: `${t('GiftCertificate.to')}: ${item.recipient.name} (${item.recipient.email})${item.message ? `, ${t('GiftCertificate.message')}: ${item.message}` : ''}`,\n        quantity: 1,\n        price: format.number(item.amount.value, {\n          style: 'currency',\n          currency: item.amount.currencyCode,\n        }),\n        sender: item.sender,\n        recipient: item.recipient,\n        message: item.message,\n        href: undefined,\n        selectedOptions: [],\n        productEntityId: 0,\n        variantEntityId: 0,\n      };\n    }\n\n    let inventoryMessages;\n\n    if (item.__typename === 'CartPhysicalItem') {\n      if (item.stockPosition?.quantityOutOfStock === item.quantity) {\n        inventoryMessages = {\n          outOfStockMessage: data.site.settings?.inventory?.showOutOfStockMessage\n            ? data.site.settings.inventory.defaultOutOfStockMessage\n            : undefined,\n        };\n      } else {\n        inventoryMessages = {\n          quantityReadyToShipMessage:\n            data.site.settings?.inventory?.showQuantityOnHand &&\n            !!item.stockPosition?.quantityOnHand\n              ? t('quantityReadyToShip', {\n                  quantity: Number(item.stockPosition.quantityOnHand),\n                })\n              : undefined,\n          quantityBackorderedMessage:\n            data.site.settings?.inventory?.showQuantityOnBackorder &&\n            !!item.stockPosition?.quantityBackordered\n              ? t('quantityOnBackorder', {\n                  quantity: Number(item.stockPosition.quantityBackordered),\n                })\n              : undefined,\n          quantityOutOfStockMessage:\n            data.site.settings?.inventory?.showOutOfStockMessage &&\n            !!item.stockPosition?.quantityOutOfStock\n              ? t('partiallyAvailable', {\n                  quantity: item.quantity - Number(item.stockPosition.quantityOutOfStock),\n                })\n              : undefined,\n          backorderMessage:\n            data.site.settings?.inventory?.showBackorderMessage &&\n            !!item.stockPosition?.quantityBackordered\n              ? (item.stockPosition.backorderMessage ?? undefined)\n              : undefined,\n        };\n      }\n    }\n\n    return {\n      typename: item.__typename,\n      id: item.entityId,\n      quantity: item.quantity,\n      price: format.number(item.listPrice.value, {\n        style: 'currency',\n        currency: item.listPrice.currencyCode,\n      }),\n      salePrice: format.number(item.salePrice.value, {\n        style: 'currency',\n        currency: item.salePrice.currencyCode,\n      }),\n      subtitle: item.selectedOptions\n        .map((option) => {\n          switch (option.__typename) {\n            case 'CartSelectedMultipleChoiceOption':\n            case 'CartSelectedCheckboxOption':\n              return `${option.name}: ${option.value}`;\n\n            case 'CartSelectedNumberFieldOption':\n              return `${option.name}: ${option.number}`;\n\n            case 'CartSelectedMultiLineTextFieldOption':\n            case 'CartSelectedTextFieldOption':\n              return `${option.name}: ${option.text}`;\n\n            case 'CartSelectedDateFieldOption':\n              return `${option.name}: ${format.dateTime(new Date(option.date.utc))}`;\n\n            default:\n              return '';\n          }\n        })\n        .join(', '),\n      title: item.name,\n      image: item.image?.url ? { src: item.image.url, alt: item.name } : undefined,\n      href: new URL(item.url).pathname,\n      selectedOptions: item.selectedOptions,\n      productEntityId: item.productEntityId,\n      variantEntityId: item.variantEntityId,\n      inventoryMessages,\n    };\n  });\n\n  const totalCouponDiscount =\n    checkout?.coupons.reduce((sum, coupon) => sum + coupon.discountedAmount.value, 0) ?? 0;\n\n  const totalLineItemDiscount = [\n    ...cart.lineItems.physicalItems,\n    ...cart.lineItems.digitalItems,\n  ].reduce((sum, item) => sum + item.discountedAmount.value, 0);\n\n  const totalDiscount = cart.discountedAmount.value + totalLineItemDiscount;\n\n  const giftCertificatesSummary =\n    checkout?.giftCertificates.reduce<Array<{ code: string; used: number }>>((acc, c) => {\n      acc.push({\n        code: c.code,\n        used: c.used.value,\n      });\n\n      return acc;\n    }, []) ?? [];\n\n  const shippingConsignment =\n    checkout?.shippingConsignments?.find((consignment) => consignment.selectedShippingOption) ||\n    checkout?.shippingConsignments?.[0];\n\n  const shippingCountries = await getShippingCountries();\n\n  const countries = shippingCountries.map((country) => ({\n    value: country.code,\n    label: country.name,\n  }));\n\n  // These US states share the same abbreviation (AE), which causes issues:\n  // 1. The shipping API uses abbreviations, so it can't distinguish between them\n  // 2. React select dropdowns require unique keys, causing duplicate key warnings\n  const blacklistedUSStates = new Set([\n    'Armed Forces Africa',\n    'Armed Forces Canada',\n    'Armed Forces Middle East',\n  ]);\n\n  const statesOrProvinces = shippingCountries.map((country) => ({\n    country: country.code,\n    states: country.statesOrProvinces\n      .filter((state) => country.code !== 'US' || !blacklistedUSStates.has(state.name))\n      .map((state) => ({\n        value: state.abbreviation,\n        label: state.name,\n      })),\n  }));\n\n  const showShippingForm =\n    shippingConsignment?.address && !shippingConsignment.selectedShippingOption;\n\n  const checkoutUrl = data.site.settings?.url.checkoutUrl;\n\n  return (\n    <>\n      <CartAnalyticsProvider data={Streamable.from(() => getAnalyticsData(cartId))}>\n        {checkoutUrl ? <CheckoutPreconnect url={checkoutUrl} /> : null}\n        <CartComponent\n          cart={{\n            lineItems: formattedLineItems,\n            total: format.number(checkout?.grandTotal?.value || 0, {\n              style: 'currency',\n              currency: cart.currencyCode,\n            }),\n            totalLabel: t('CheckoutSummary.total'),\n            summaryItems: [\n              {\n                label: t('CheckoutSummary.subTotal'),\n                value: format.number(checkout?.subtotal?.value ?? 0, {\n                  style: 'currency',\n                  currency: cart.currencyCode,\n                }),\n              },\n              totalDiscount > 0\n                ? {\n                    label: t('CheckoutSummary.discounts'),\n                    value: `-${format.number(totalDiscount, {\n                      style: 'currency',\n                      currency: cart.currencyCode,\n                    })}`,\n                  }\n                : null,\n              totalCouponDiscount > 0\n                ? {\n                    label: t('CheckoutSummary.CouponCode.couponCode'),\n                    value: `-${format.number(totalCouponDiscount, {\n                      style: 'currency',\n                      currency: cart.currencyCode,\n                    })}`,\n                  }\n                : null,\n              ...giftCertificatesSummary.map((gc) => ({\n                label: `${t('GiftCertificate.giftCertificate')} (${gc.code})`,\n                value: `-${format.number(gc.used, {\n                  style: 'currency',\n                  currency: cart.currencyCode,\n                })}`,\n              })),\n              checkout?.taxTotal && {\n                label: t('CheckoutSummary.tax'),\n                value: format.number(checkout.taxTotal.value, {\n                  style: 'currency',\n                  currency: cart.currencyCode,\n                }),\n              },\n            ].filter(exists),\n          }}\n          checkoutAction={CHECKOUT_URL}\n          checkoutLabel={t('proceedToCheckout')}\n          couponCode={{\n            action: updateCouponCode,\n            couponCodes: checkout?.coupons.map((coupon) => coupon.code) ?? [],\n            ctaLabel: t('CheckoutSummary.CouponCode.apply'),\n            label: t('CheckoutSummary.CouponCode.couponCode'),\n            removeLabel: t('CheckoutSummary.CouponCode.removeCouponCode'),\n          }}\n          decrementLineItemLabel={t('decrement')}\n          deleteLineItemLabel={t('removeItem')}\n          emptyState={{\n            title: t('Empty.title'),\n            subtitle: t('Empty.subtitle'),\n            cta: { label: t('Empty.cta'), href: '/shop-all' },\n          }}\n          giftCertificate={\n            giftCertificatesEnabled\n              ? {\n                  action: updateGiftCertificate,\n                  giftCertificateCodes: checkout?.giftCertificates.map((gc) => gc.code) ?? [],\n                  ctaLabel: t('GiftCertificate.apply'),\n                  label: t('GiftCertificate.giftCertificateCode'),\n                  placeholder: tGiftCertificates('CheckBalance.inputPlaceholder'),\n                  removeLabel: t('GiftCertificate.removeGiftCertificate'),\n                }\n              : undefined\n          }\n          incrementLineItemLabel={t('increment')}\n          key={`${cart.entityId}-${cart.version}`}\n          lineItemAction={updateLineItem}\n          lineItemActionPendingLabel={t('cartUpdateInProgress')}\n          shipping={{\n            action: updateShippingInfo,\n            countries,\n            states: statesOrProvinces,\n            address: shippingConsignment?.address\n              ? {\n                  country: shippingConsignment.address.countryCode,\n                  city:\n                    shippingConsignment.address.city !== ''\n                      ? (shippingConsignment.address.city ?? undefined)\n                      : undefined,\n                  state:\n                    shippingConsignment.address.stateOrProvince !== ''\n                      ? (shippingConsignment.address.stateOrProvince ?? undefined)\n                      : undefined,\n                  postalCode:\n                    shippingConsignment.address.postalCode !== ''\n                      ? (shippingConsignment.address.postalCode ?? undefined)\n                      : undefined,\n                }\n              : undefined,\n            shippingOptions: shippingConsignment?.availableShippingOptions\n              ? shippingConsignment.availableShippingOptions.map((option) => ({\n                  label: option.description,\n                  value: option.entityId,\n                  price: format.number(option.cost.value, {\n                    style: 'currency',\n                    currency: checkout?.cart?.currencyCode,\n                  }),\n                }))\n              : undefined,\n            shippingOption: shippingConsignment?.selectedShippingOption\n              ? {\n                  value: shippingConsignment.selectedShippingOption.entityId,\n                  label: shippingConsignment.selectedShippingOption.description,\n                  price: format.number(shippingConsignment.selectedShippingOption.cost.value, {\n                    style: 'currency',\n                    currency: checkout?.cart?.currencyCode,\n                  }),\n                }\n              : undefined,\n            showShippingForm,\n            shippingLabel: t('CheckoutSummary.Shipping.shipping'),\n            addLabel: t('CheckoutSummary.Shipping.add'),\n            changeLabel: t('CheckoutSummary.Shipping.change'),\n            countryLabel: t('CheckoutSummary.Shipping.country'),\n            cityLabel: t('CheckoutSummary.Shipping.city'),\n            stateLabel: t('CheckoutSummary.Shipping.state'),\n            postalCodeLabel: t('CheckoutSummary.Shipping.postalCode'),\n            updateShippingOptionsLabel: t('CheckoutSummary.Shipping.updatedShippingOptions'),\n            viewShippingOptionsLabel: t('CheckoutSummary.Shipping.viewShippingOptions'),\n            cancelLabel: t('CheckoutSummary.Shipping.cancel'),\n            editAddressLabel: t('CheckoutSummary.Shipping.editAddress'),\n            shippingOptionsLabel: t('CheckoutSummary.Shipping.shippingOptions'),\n            updateShippingLabel: t('CheckoutSummary.Shipping.updateShipping'),\n            addShippingLabel: t('CheckoutSummary.Shipping.addShipping'),\n            noShippingOptionsLabel: t('CheckoutSummary.Shipping.noShippingOptions'),\n          }}\n          summaryTitle={t('CheckoutSummary.title')}\n          title={t('title')}\n        />\n      </CartAnalyticsProvider>\n      <CartViewed\n        currencyCode={cart.currencyCode}\n        lineItems={lineItems}\n        subtotal={checkout?.subtotal?.value}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/checkout/route.ts",
    "content": "import { BigCommerceAuthError } from '@bigcommerce/catalyst-client';\nimport { unstable_rethrow as rethrow } from 'next/navigation';\nimport { NextRequest, NextResponse } from 'next/server';\nimport { getTranslations } from 'next-intl/server';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { getChannelIdFromLocale } from '~/channels.config';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { redirect } from '~/i18n/routing';\nimport { getVisitIdCookie, getVisitorIdCookie } from '~/lib/analytics/bigcommerce';\nimport { getCartId } from '~/lib/cart';\nimport { getConsentCookie } from '~/lib/consent-manager/cookies/server';\nimport { serverToast } from '~/lib/server-toast';\n\nconst CheckoutRedirectMutation = graphql(`\n  mutation CheckoutRedirectMutation(\n    $cartId: String!\n    $visitId: String!\n    $visitorId: String!\n    $referer: URL!\n    $userAgent: String!\n    $analyticsConsent: Boolean!\n    $functionalConsent: Boolean!\n    $targetingConsent: Boolean!\n  ) {\n    cart {\n      createCartRedirectUrls(\n        input: {\n          cartEntityId: $cartId\n          analytics: {\n            initiator: { visitId: $visitId, visitorId: $visitorId }\n            request: { url: $referer, userAgent: $userAgent }\n            consent: {\n              analytics: $analyticsConsent\n              functional: $functionalConsent\n              targeting: $targetingConsent\n            }\n          }\n        }\n      ) {\n        errors {\n          ... on NotFoundError {\n            __typename\n          }\n        }\n        redirectUrls {\n          redirectedCheckoutUrl\n        }\n      }\n    }\n  }\n`);\n\nexport async function GET(req: NextRequest, { params }: { params: Promise<{ locale: string }> }) {\n  const { locale } = await params;\n  const cartId = req.nextUrl.searchParams.get('cartId') ?? (await getCartId());\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  const channelId = getChannelIdFromLocale(locale);\n  const t = await getTranslations('Cart.Errors');\n\n  if (!cartId) {\n    await serverToast.error(t('cartNotFound'));\n\n    return redirect({ href: '/cart', locale });\n  }\n\n  const visitId = await getVisitIdCookie();\n  const visitorId = await getVisitorIdCookie();\n  const consent = await getConsentCookie();\n\n  try {\n    const { data } = await client.fetch({\n      document: CheckoutRedirectMutation,\n      variables: {\n        cartId,\n        visitId: visitId ?? '',\n        visitorId: visitorId ?? '',\n        analyticsConsent: consent?.['c.measurement'] ?? false,\n        functionalConsent: consent?.['c.functionality'] ?? false,\n        targetingConsent: consent?.['c.marketing'] ?? false,\n        referer: req.headers.get('referer') ?? '',\n        userAgent: req.headers.get('user-agent') ?? '',\n      },\n      fetchOptions: { cache: 'no-store' },\n      customerAccessToken,\n      channelId,\n    });\n\n    if (\n      data.cart.createCartRedirectUrls.errors.length > 0 ||\n      !data.cart.createCartRedirectUrls.redirectUrls\n    ) {\n      await serverToast.error(t('somethingWentWrong'));\n\n      return redirect({ href: '/cart', locale });\n    }\n\n    return redirect({\n      href: data.cart.createCartRedirectUrls.redirectUrls.redirectedCheckoutUrl,\n      locale,\n    });\n  } catch (error) {\n    rethrow(error);\n\n    if (error instanceof BigCommerceAuthError) {\n      return redirect({ href: '/logout?redirectTo=/checkout/', locale });\n    }\n\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    return NextResponse.json(\n      { message: 'Server error' },\n      { status: 500, statusText: 'Server error' },\n    );\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/compare/_actions/add-to-cart.tsx",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getTranslations } from 'next-intl/server';\nimport { ReactNode } from 'react';\n\nimport { compareAddToCartFormDataSchema } from '@/vibes/soul/primitives/compare-card/schema';\nimport { Link } from '~/components/link';\nimport { addToOrCreateCart } from '~/lib/cart';\nimport { MissingCartError } from '~/lib/cart/error';\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  successMessage?: ReactNode;\n  errorMessage?: string;\n}\n\nexport const addToCart = async (\n  prevState: State,\n  payload: FormData,\n): Promise<{\n  lastResult: SubmissionResult | null;\n  successMessage?: ReactNode;\n}> => {\n  const t = await getTranslations('Compare');\n\n  const submission = parseWithZod(payload, { schema: compareAddToCartFormDataSchema });\n\n  if (submission.status !== 'success') {\n    return { lastResult: submission.reply() };\n  }\n\n  const productEntityId = Number(submission.value.id);\n  const quantity = 1;\n\n  try {\n    await addToOrCreateCart({\n      lineItems: [\n        {\n          productEntityId,\n          quantity,\n        },\n      ],\n    });\n\n    return {\n      lastResult: submission.reply(),\n      successMessage: t.rich('successMessage', {\n        cartItems: quantity,\n        cartLink: (chunks) => (\n          <Link className=\"underline\" href=\"/cart\" prefetch=\"viewport\" prefetchKind=\"full\">\n            {chunks}\n          </Link>\n        ),\n      }),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => {\n            if (message.includes('Not enough stock:')) {\n              // This removes the item id from the message. It's very brittle, but it's the only\n              // solution to do it until our API returns a better error message.\n              return message.replace('Not enough stock: ', '').replace(/\\(\\w.+\\)\\s{1}/, '');\n            }\n\n            return message;\n          }),\n        }),\n      };\n    }\n\n    if (error instanceof MissingCartError) {\n      return {\n        lastResult: submission.reply({ formErrors: [t('missingCart')] }),\n      };\n    }\n\n    if (error instanceof Error) {\n      return {\n        lastResult: submission.reply({ formErrors: [error.message] }),\n      };\n    }\n\n    return {\n      lastResult: submission.reply({ formErrors: [t('unknownError')] }),\n    };\n  }\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/compare/_components/compare-analytics-provider.tsx",
    "content": "'use client';\n\nimport { PropsWithChildren, Suspense } from 'react';\nimport { z } from 'zod';\n\nimport { Streamable, useStreamable } from '@/vibes/soul/lib/streamable';\nimport { EventsProvider } from '~/components/analytics/events';\nimport { useAnalytics } from '~/lib/analytics/react';\n\ninterface AddToCartContext {\n  id: number;\n  name: string;\n  brand: string;\n  sku?: string;\n  currency: string;\n  price: number;\n}\n\nconst AddToCartSchema = z.object({\n  id: z.number({ coerce: true }),\n  quantity: z.number({ coerce: true }).default(1),\n});\n\nexport function CompareAnalyticsProvider(\n  props: PropsWithChildren<{ data: Streamable<AddToCartContext[]> }>,\n) {\n  return (\n    <Suspense fallback={props.children}>\n      <CompareAnalyticsProviderResolved {...props} />\n    </Suspense>\n  );\n}\n\nfunction CompareAnalyticsProviderResolved({\n  children,\n  data,\n}: PropsWithChildren<{ data: Streamable<AddToCartContext[]> }>) {\n  const analytics = useAnalytics();\n  const products = useStreamable(data);\n\n  const onAddToCart = (payload?: FormData) => {\n    const parsedPayload = AddToCartSchema.safeParse(Object.fromEntries(payload?.entries() ?? []));\n\n    if (parsedPayload.success) {\n      const { id: productId, quantity } = parsedPayload.data;\n      const product = products.find(({ id }) => id === productId);\n\n      if (product) {\n        const { id, name, brand, sku, price, currency } = product;\n\n        analytics?.cart.productAdded({\n          currency,\n          value: 1 * price,\n          items: [\n            {\n              id: id.toString(),\n              name,\n              brand,\n              sku,\n              price,\n              quantity,\n            },\n          ],\n        });\n      }\n    }\n  };\n\n  return <EventsProvider onAddToCart={onAddToCart}>{children}</EventsProvider>;\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/compare/page-data.ts",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { cache } from 'react';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { CurrencyCode } from '~/components/header/fragment';\nimport { ProductCardFragment } from '~/components/product-card/fragment';\n\nexport const MAX_COMPARE_LIMIT = 10;\n\nconst ComparedProductsQuery = graphql(\n  `\n    query ComparedProductsQuery($entityIds: [Int!], $first: Int, $currencyCode: currencyCode) {\n      site {\n        products(entityIds: $entityIds, first: $first) {\n          edges {\n            node {\n              ...ProductCardFragment\n              description\n              sku\n              weight {\n                value\n                unit\n              }\n              condition\n              customFields {\n                edges {\n                  node {\n                    entityId\n                    name\n                    value\n                  }\n                }\n              }\n              productOptions(first: 1) {\n                edges {\n                  node {\n                    entityId\n                  }\n                }\n              }\n              inventory {\n                isInStock\n              }\n              availabilityV2 {\n                status\n              }\n            }\n          }\n        }\n      }\n    }\n  `,\n  [ProductCardFragment],\n);\n\nexport const getComparedProducts = cache(\n  async (productIds: number[] = [], currencyCode?: CurrencyCode, customerAccessToken?: string) => {\n    if (productIds.length === 0) {\n      return [];\n    }\n\n    const { data } = await client.fetch({\n      document: ComparedProductsQuery,\n      variables: {\n        entityIds: productIds,\n        first: productIds.length ? MAX_COMPARE_LIMIT : 0,\n        currencyCode,\n      },\n      customerAccessToken,\n      fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },\n    });\n\n    return removeEdgesAndNodes(data.site.products);\n  },\n);\n"
  },
  {
    "path": "core/app/[locale]/(default)/compare/page.tsx",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { Metadata } from 'next';\nimport { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\nimport * as z from 'zod';\n\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { CompareSection } from '@/vibes/soul/sections/compare-section';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { pricesTransformer } from '~/data-transformers/prices-transformer';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\nimport { getMetadataAlternates } from '~/lib/seo/canonical';\n\nimport { addToCart } from './_actions/add-to-cart';\nimport { CompareAnalyticsProvider } from './_components/compare-analytics-provider';\nimport { getComparedProducts } from './page-data';\n\nconst CompareParamsSchema = z.object({\n  ids: z\n    .union([z.string(), z.array(z.string()), z.undefined()])\n    .transform((value) => {\n      if (Array.isArray(value)) {\n        return value;\n      }\n\n      if (typeof value === 'string') {\n        return [...value.split(',')];\n      }\n\n      return undefined;\n    })\n    .transform((value) => value?.map((id) => parseInt(id, 10))),\n});\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n  searchParams: Promise<{\n    ids?: string | string[];\n  }>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'Compare' });\n\n  return {\n    title: t('title'),\n    alternates: await getMetadataAlternates({ path: '/compare', locale }),\n  };\n}\n\nexport default async function Compare(props: Props) {\n  const { locale } = await props.params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Compare');\n\n  const streamableProducts = Streamable.from(async () => {\n    const customerAccessToken = await getSessionCustomerAccessToken();\n    const currencyCode = await getPreferredCurrencyCode();\n\n    const searchParams = await props.searchParams;\n    const parsed = CompareParamsSchema.parse(searchParams);\n    const productIds = parsed.ids?.filter((id) => !Number.isNaN(id));\n\n    const products = await getComparedProducts(productIds, currencyCode, customerAccessToken);\n    const format = await getFormatter();\n\n    return products.map((product) => ({\n      id: product.entityId.toString(),\n      title: product.name,\n      href: product.path,\n      image: product.defaultImage\n        ? { src: product.defaultImage.url, alt: product.defaultImage.altText }\n        : undefined,\n      price: pricesTransformer(product.prices, format),\n      subtitle: product.brand?.name ?? undefined,\n      rating: product.reviewSummary.averageRating,\n      description: <div dangerouslySetInnerHTML={{ __html: product.description }} />,\n      customFields: [\n        { name: t('sku'), value: product.sku },\n        { name: t('weight'), value: `${product.weight?.value} ${product.weight?.unit}` },\n        ...removeEdgesAndNodes(product.customFields).map(({ name, value }) => ({ name, value })),\n      ],\n      hasVariants: removeEdgesAndNodes(product.productOptions).length > 0,\n      isPreorder: product.availabilityV2.status === 'Preorder',\n      disabled: product.availabilityV2.status === 'Unavailable' || !product.inventory.isInStock,\n    }));\n  });\n\n  const streamableAnalyticsData = Streamable.from(async () => {\n    const customerAccessToken = await getSessionCustomerAccessToken();\n    const currencyCode = await getPreferredCurrencyCode();\n\n    const searchParams = await props.searchParams;\n    const parsed = CompareParamsSchema.parse(searchParams);\n    const productIds = parsed.ids?.filter((id) => !Number.isNaN(id));\n\n    const products = await getComparedProducts(productIds, currencyCode, customerAccessToken);\n\n    return products.map((product) => {\n      return {\n        id: product.entityId,\n        name: product.name,\n        sku: product.sku,\n        brand: product.brand?.name ?? '',\n        price: product.prices?.price.value ?? 0,\n        currency: product.prices?.price.currencyCode ?? '',\n      };\n    });\n  });\n\n  return (\n    <CompareAnalyticsProvider data={streamableAnalyticsData}>\n      <CompareSection\n        addToCartAction={addToCart}\n        addToCartLabel={t('addToCart')}\n        descriptionLabel={t('description')}\n        emptyStateTitle={t('noProductsToCompare')}\n        nextLabel={t('next')}\n        noDescriptionLabel={t('noDescription')}\n        noOtherDetailsLabel={t('noOtherDetails')}\n        noRatingsLabel={t('noRatings')}\n        otherDetailsLabel={t('otherDetails')}\n        previousLabel={t('previous')}\n        products={streamableProducts}\n        ratingLabel={t('rating')}\n        title={t('title')}\n        viewOptionsLabel={t('viewOptions')}\n      />\n    </CompareAnalyticsProvider>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/error.tsx",
    "content": "'use client';\n\nimport { useTranslations } from 'next-intl';\n\nimport { Error as ErrorSection } from '@/vibes/soul/sections/error';\n\ninterface Props {\n  error: Error & { digest?: string };\n  reset: () => void;\n}\n\nexport default function Error({ reset }: Props) {\n  const t = useTranslations('Error');\n\n  return (\n    <ErrorSection\n      ctaAction={reset}\n      ctaLabel={t('cta')}\n      subtitle={t('subtitle')}\n      title={t('title')}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/gift-certificates/balance/_actions/get-gift-certificate-by-code.ts",
    "content": "'use server';\n\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getFormatter, getTranslations } from 'next-intl/server';\n\nimport { GiftCertificateData } from '@/vibes/soul/sections/gift-certificate-balance-section';\nimport { giftCertificateCodeSchema } from '@/vibes/soul/sections/gift-certificate-balance-section/schema';\nimport { client } from '~/client';\nimport { graphql, ResultOf } from '~/client/graphql';\nimport { ExistingResultType } from '~/client/util';\n\nimport { GiftCertificateFragment } from '../fragment';\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  data: GiftCertificateData | null;\n  errorMessage?: string;\n}\n\nconst GetGiftCertificateByCodeQuery = graphql(\n  `\n    query GetGiftCertificateByCode($code: String!) {\n      site {\n        giftCertificate(code: $code) {\n          ...GiftCertificateFragment\n        }\n      }\n    }\n  `,\n  [GiftCertificateFragment],\n);\n\nfunction transformGiftCertificate(\n  giftCertificate: ResultOf<typeof GiftCertificateFragment>,\n  format: ExistingResultType<typeof getFormatter>,\n): GiftCertificateData | null {\n  if (!giftCertificate.amount.formattedV2 || !giftCertificate.balance.formattedV2) {\n    return null;\n  }\n\n  return {\n    code: giftCertificate.code,\n    currencyCode: giftCertificate.currencyCode,\n    status: giftCertificate.status,\n    amount: giftCertificate.amount.formattedV2,\n    balance: giftCertificate.balance.formattedV2,\n    senderName: giftCertificate.sender.name,\n    recipientName: giftCertificate.recipient.name,\n    purchasedAt: format.dateTime(new Date(giftCertificate.purchasedAt.utc), { dateStyle: 'long' }),\n    expiresAt: giftCertificate.expiresAt?.utc\n      ? format.dateTime(new Date(giftCertificate.expiresAt.utc), { dateStyle: 'long' })\n      : null,\n  };\n}\n\nexport async function getGiftCertificateByCode(\n  prevState: Awaited<State>,\n  formData: FormData,\n): Promise<State> {\n  const t = await getTranslations('GiftCertificates.CheckBalance');\n  const format = await getFormatter();\n  const schema = giftCertificateCodeSchema({ required_error: t('Errors.codeRequired') });\n  const submission = parseWithZod(formData, { schema });\n\n  if (submission.status !== 'success') {\n    return {\n      ...prevState,\n      lastResult: submission.reply(),\n    };\n  }\n\n  try {\n    const { code } = schema.parse(submission.value);\n    const response = await client.fetch({\n      document: GetGiftCertificateByCodeQuery,\n      fetchOptions: { cache: 'no-store' },\n      variables: { code },\n    });\n\n    if (!response.data.site.giftCertificate) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: [t('Errors.invalidCode')] }),\n        errorMessage: t('Errors.invalidCode'),\n      };\n    }\n\n    const giftCertificate = transformGiftCertificate(response.data.site.giftCertificate, format);\n\n    if (!giftCertificate) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: [t('Errors.somethingWentWrong')] }),\n        errorMessage: t('Errors.somethingWentWrong'),\n      };\n    }\n\n    return {\n      lastResult: submission.reply(),\n      data: giftCertificate,\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: [t('Errors.somethingWentWrong')] }),\n      errorMessage: t('Errors.somethingWentWrong'),\n    };\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/gift-certificates/balance/fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nexport const GiftCertificateFragment = graphql(`\n  fragment GiftCertificateFragment on GiftCertificate {\n    code\n    currencyCode\n    status\n    theme\n    sender {\n      name\n    }\n    recipient {\n      name\n    }\n    amount {\n      value\n      formattedV2\n    }\n    balance {\n      value\n      formattedV2\n    }\n    purchasedAt {\n      utc\n    }\n    expiresAt {\n      utc\n    }\n  }\n`);\n"
  },
  {
    "path": "core/app/[locale]/(default)/gift-certificates/balance/page.tsx",
    "content": "import type { Metadata } from 'next';\nimport { getTranslations, setRequestLocale } from 'next-intl/server';\n\nimport { GiftCertificateCheckBalanceSection } from '@/vibes/soul/sections/gift-certificate-balance-section';\nimport { redirect } from '~/i18n/routing';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\nimport { getMetadataAlternates } from '~/lib/seo/canonical';\n\nimport { getGiftCertificatesData } from '../page-data';\n\nimport { getGiftCertificateByCode } from './_actions/get-gift-certificate-by-code';\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'GiftCertificates' });\n\n  return {\n    title: t('title') || 'Gift certificates - Check balance',\n    alternates: await getMetadataAlternates({ path: '/gift-certificates/balance', locale }),\n  };\n}\n\nexport default async function GiftCertificates(props: Props) {\n  const { locale } = await props.params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('GiftCertificates');\n  const currencyCode = await getPreferredCurrencyCode();\n  const data = await getGiftCertificatesData(currencyCode);\n\n  if (!data.giftCertificatesEnabled) {\n    return redirect({ href: '/', locale });\n  }\n\n  return (\n    <GiftCertificateCheckBalanceSection\n      action={getGiftCertificateByCode}\n      breadcrumbs={[\n        {\n          label: t('title'),\n          href: '/gift-certificates',\n        },\n        {\n          label: t('CheckBalance.title'),\n          href: '#',\n        },\n      ]}\n      checkBalanceLabel={t('checkBalanceLabel')}\n      description={t('CheckBalance.description')}\n      expiresAtLabel={t('expiresAtLabel')}\n      inputLabel={t('CheckBalance.inputLabel')}\n      inputPlaceholder={t('CheckBalance.inputPlaceholder')}\n      logo={data.logo}\n      purchasedDateLabel={t('CheckBalance.purchasedDateLabel')}\n      senderLabel={t('CheckBalance.senderLabel')}\n      title={t('CheckBalance.title')}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/gift-certificates/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { CurrencyCode } from '~/components/header/fragment';\nimport { StoreLogoFragment } from '~/components/store-logo/fragment';\nimport { logoTransformer } from '~/data-transformers/logo-transformer';\n\nconst GiftCertificatesRootQuery = graphql(\n  `\n    query GiftCertificatesRootQuery($currencyCode: currencyCode) {\n      site {\n        settings {\n          giftCertificates(currencyCode: $currencyCode) {\n            isEnabled\n          }\n          currency {\n            defaultCurrency\n          }\n          ...StoreLogoFragment\n        }\n      }\n    }\n  `,\n  [StoreLogoFragment],\n);\n\nexport const getGiftCertificatesData = cache(async (currencyCode?: CurrencyCode) => {\n  const response = await client.fetch({\n    document: GiftCertificatesRootQuery,\n    variables: { currencyCode },\n    fetchOptions: { next: { revalidate } },\n  });\n\n  return {\n    giftCertificatesEnabled: response.data.site.settings?.giftCertificates?.isEnabled ?? false,\n    defaultCurrency: response.data.site.settings?.currency.defaultCurrency ?? undefined,\n    logo: response.data.site.settings ? logoTransformer(response.data.site.settings) : '',\n  };\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/gift-certificates/page.tsx",
    "content": "import type { Metadata } from 'next';\nimport { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\n\nimport { GiftCertificatesSection } from '@/vibes/soul/sections/gift-certificates-section';\nimport { redirect } from '~/i18n/routing';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\nimport { getMetadataAlternates } from '~/lib/seo/canonical';\n\nimport { getGiftCertificatesData } from './page-data';\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'GiftCertificates' });\n\n  return {\n    title: t('title') || 'Gift certificates',\n    alternates: await getMetadataAlternates({ path: '/gift-certificates', locale }),\n  };\n}\n\nexport default async function GiftCertificates(props: Props) {\n  const { locale } = await props.params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('GiftCertificates');\n  const format = await getFormatter();\n  const currencyCode = await getPreferredCurrencyCode();\n  const data = await getGiftCertificatesData(currencyCode);\n\n  if (!data.giftCertificatesEnabled) {\n    return redirect({ href: '/', locale });\n  }\n\n  const exampleBalance = format.number(25.0, {\n    style: 'currency',\n    currency: currencyCode ?? data.defaultCurrency,\n  });\n\n  return (\n    <GiftCertificatesSection\n      checkBalanceHref=\"/gift-certificates/balance\"\n      checkBalanceLabel={t('checkBalanceLabel')}\n      description={t('description')}\n      exampleBalance={exampleBalance}\n      logo={data.logo}\n      purchaseHref=\"/gift-certificates/purchase\"\n      purchaseLabel={t('purchaseLabel')}\n      title={t('title')}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/gift-certificates/purchase/_actions/add-to-cart.tsx",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getFormatter, getTranslations } from 'next-intl/server';\nimport { ReactNode } from 'react';\nimport { z } from 'zod';\n\nimport { DynamicFormActionArgs } from '@/vibes/soul/form/dynamic-form';\nimport { Field } from '@/vibes/soul/form/dynamic-form/schema';\nimport { client } from '~/client';\nimport { graphql, ResultOf } from '~/client/graphql';\nimport { ExistingResultType } from '~/client/util';\nimport { Link } from '~/components/link';\nimport { addToOrCreateCart } from '~/lib/cart';\nimport { MissingCartError } from '~/lib/cart/error';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\n\nimport { GiftCertificateSettingsFragment } from '../fragment';\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  successMessage?: ReactNode;\n}\n\nconst GiftCertificateSettingsQuery = graphql(\n  `\n    query GiftCertificateSettings($currencyCode: currencyCode) {\n      site {\n        settings {\n          giftCertificates(currencyCode: $currencyCode) {\n            ...GiftCertificateSettingsFragment\n          }\n        }\n      }\n    }\n  `,\n  [GiftCertificateSettingsFragment],\n);\n\nconst schema = (\n  giftCertificateSettings: ResultOf<typeof GiftCertificateSettingsFragment> | undefined,\n  t: ExistingResultType<typeof getTranslations<'GiftCertificates.Purchase'>>,\n) => {\n  return z\n    .object({\n      senderName: z.string(),\n      senderEmail: z.string().email(),\n      recipientName: z.string(),\n      recipientEmail: z.string().email(),\n      message: z.string().optional(),\n      amount: z.number({\n        required_error: t('Form.Errors.amountRequired'),\n        invalid_type_error: t('Form.Errors.amountInvalid'),\n      }),\n    })\n    .superRefine((data, ctx) => {\n      if (!giftCertificateSettings) {\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          message: t('Form.Errors.unexpectedSettingsError'),\n        });\n\n        return;\n      }\n\n      if (\n        'minimumAmount' in giftCertificateSettings &&\n        'maximumAmount' in giftCertificateSettings\n      ) {\n        const min = giftCertificateSettings.minimumAmount.value;\n        const max = giftCertificateSettings.maximumAmount.value;\n\n        if (data.amount < min || data.amount > max) {\n          ctx.addIssue({\n            code: z.ZodIssueCode.custom,\n            path: ['amount'],\n            message: t('Form.Errors.amountOutOfRange', {\n              minAmount: String(min),\n              maxAmount: String(max),\n            }),\n          });\n\n          return;\n        }\n      }\n\n      if ('amounts' in giftCertificateSettings) {\n        const validAmounts = giftCertificateSettings.amounts.map((amt) => amt.value);\n\n        if (!validAmounts.includes(data.amount)) {\n          ctx.addIssue({\n            code: z.ZodIssueCode.custom,\n            path: ['amount'],\n            message: t('Form.Errors.amountInvalid'),\n          });\n        }\n      }\n    });\n};\n\nexport async function addGiftCertificateToCart<F extends Field>(\n  _args: DynamicFormActionArgs<F>,\n  _prevState: State,\n  formData: FormData,\n): Promise<State> {\n  const t = await getTranslations('GiftCertificates.Purchase');\n  const format = await getFormatter();\n  const currencyCode = await getPreferredCurrencyCode();\n  const settingsResp = await client.fetch({\n    document: GiftCertificateSettingsQuery,\n    variables: { currencyCode },\n  });\n\n  const submission = parseWithZod(formData, {\n    schema: schema(settingsResp.data.site.settings?.giftCertificates ?? undefined, t),\n  });\n\n  if (submission.status !== 'success') {\n    return { lastResult: submission.reply() };\n  }\n\n  const amountFormatted = format.number(submission.value.amount, {\n    style: 'currency',\n    currency: currencyCode,\n  });\n\n  try {\n    await addToOrCreateCart({\n      giftCertificates: [\n        {\n          name: `${amountFormatted} Gift Certificate`,\n          sender: {\n            name: submission.value.senderName,\n            email: submission.value.senderEmail,\n          },\n          recipient: {\n            name: submission.value.recipientName,\n            email: submission.value.recipientEmail,\n          },\n          message: submission.value.message ?? undefined,\n          theme: 'GENERAL',\n          amount: submission.value.amount,\n          quantity: 1,\n        },\n      ],\n    });\n\n    return {\n      lastResult: submission.reply(),\n      successMessage: t.rich('successMessage', {\n        cartLink: (chunks) => (\n          <Link className=\"underline\" href=\"/cart\" prefetch=\"viewport\" prefetchKind=\"full\">\n            {chunks}\n          </Link>\n        ),\n      }),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => {\n            return message;\n          }),\n        }),\n      };\n    }\n\n    if (error instanceof MissingCartError) {\n      return {\n        lastResult: submission.reply({ formErrors: [t('missingCart')] }),\n      };\n    }\n\n    if (error instanceof Error) {\n      return {\n        lastResult: submission.reply({ formErrors: [error.message] }),\n      };\n    }\n\n    return {\n      lastResult: submission.reply({ formErrors: [t('unknownError')] }),\n    };\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/gift-certificates/purchase/fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nexport const GiftCertificateSettingsFragment = graphql(`\n  fragment GiftCertificateSettingsFragment on GiftCertificateSettings {\n    __typename\n    isEnabled\n    currencyCode\n    expiry {\n      unit\n      value\n    }\n    ... on FixedAmountGiftCertificateSettings {\n      amounts {\n        value\n        formattedV2\n      }\n    }\n    ... on CustomAmountGiftCertificateSettings {\n      minimumAmount {\n        value\n        formattedV2\n      }\n      maximumAmount {\n        value\n        formattedV2\n      }\n    }\n  }\n`);\n"
  },
  {
    "path": "core/app/[locale]/(default)/gift-certificates/purchase/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { CurrencyCode } from '~/components/header/fragment';\nimport { StoreLogoFragment } from '~/components/store-logo/fragment';\nimport { logoTransformer } from '~/data-transformers/logo-transformer';\n\nimport { GiftCertificateSettingsFragment } from './fragment';\n\nconst GiftCertificatePurchaseSettingsQuery = graphql(\n  `\n    query GiftCertificatePurchaseSettingsQuery($currencyCode: currencyCode) {\n      site {\n        settings {\n          giftCertificates(currencyCode: $currencyCode) {\n            ...GiftCertificateSettingsFragment\n          }\n          currency {\n            defaultCurrency\n          }\n          storeName\n          ...StoreLogoFragment\n        }\n      }\n    }\n  `,\n  [GiftCertificateSettingsFragment, StoreLogoFragment],\n);\n\nexport const getGiftCertificatePurchaseData = cache(async (currencyCode?: CurrencyCode) => {\n  const response = await client.fetch({\n    document: GiftCertificatePurchaseSettingsQuery,\n    variables: { currencyCode },\n    fetchOptions: { next: { revalidate } },\n  });\n\n  return {\n    giftCertificateSettings: response.data.site.settings?.giftCertificates ?? null,\n    logo: response.data.site.settings ? logoTransformer(response.data.site.settings) : '',\n    storeName: response.data.site.settings?.storeName ?? undefined,\n    defaultCurrency: response.data.site.settings?.currency.defaultCurrency ?? undefined,\n  };\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/gift-certificates/purchase/page.tsx",
    "content": "import { ResultOf } from 'gql.tada';\nimport { Metadata } from 'next';\nimport { getFormatter, getTranslations } from 'next-intl/server';\n\nimport { Field, FieldGroup } from '@/vibes/soul/form/dynamic-form/schema';\nimport { GiftCertificatePurchaseSection } from '@/vibes/soul/sections/gift-certificate-purchase-section';\nimport { GiftCertificateSettingsFragment } from '~/app/[locale]/(default)/gift-certificates/purchase/fragment';\nimport { ExistingResultType } from '~/client/util';\nimport { redirect } from '~/i18n/routing';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\nimport { getMetadataAlternates } from '~/lib/seo/canonical';\n\nimport { addGiftCertificateToCart } from './_actions/add-to-cart';\nimport { getGiftCertificatePurchaseData } from './page-data';\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'GiftCertificates' });\n\n  return {\n    title: t('Purchase.title'),\n    alternates: await getMetadataAlternates({ path: '/gift-certificates/purchase', locale }),\n  };\n}\n\nfunction getFields(\n  giftCertificateSettings: ResultOf<typeof GiftCertificateSettingsFragment>,\n  expiresAt: string | undefined,\n  t: ExistingResultType<typeof getTranslations<'GiftCertificates'>>,\n): Array<Field | FieldGroup<Field>> {\n  const baseFields: Array<Field | FieldGroup<Field>> = [\n    [\n      {\n        type: 'text',\n        name: 'senderName',\n        label: t('Purchase.Form.senderNameLabel'),\n        required: true,\n      },\n      {\n        type: 'email',\n        name: 'senderEmail',\n        label: t('Purchase.Form.senderEmailLabel'),\n        required: true,\n      },\n    ],\n    [\n      {\n        type: 'text',\n        name: 'recipientName',\n        label: t('Purchase.Form.recipientNameLabel'),\n        required: true,\n      },\n      {\n        type: 'email',\n        name: 'recipientEmail',\n        label: t('Purchase.Form.recipientEmailLabel'),\n        required: true,\n      },\n    ],\n    {\n      type: 'textarea',\n      name: 'message',\n      label: t('Purchase.Form.messageLabel'),\n      required: false,\n    },\n    {\n      type: 'checkbox',\n      name: 'nonRefundable',\n      label: t('Purchase.Form.nonRefundableCheckboxLabel'),\n      required: true,\n    },\n  ];\n\n  if (expiresAt) {\n    baseFields.push({\n      type: 'checkbox',\n      name: 'expirationConsent',\n      label: t('Purchase.Form.expiryCheckboxLabel', { expiryDate: expiresAt }),\n      required: true,\n    });\n  }\n\n  const amountFields: Array<Field | FieldGroup<Field>> =\n    giftCertificateSettings.__typename === 'CustomAmountGiftCertificateSettings'\n      ? [\n          {\n            type: 'text',\n            name: 'amount',\n            label: t('Purchase.Form.customAmountLabel', {\n              minAmount: String(giftCertificateSettings.minimumAmount.value),\n              maxAmount: String(giftCertificateSettings.maximumAmount.value),\n            }),\n            pattern: '^[0-9]*\\\\.?[0-9]+$',\n            required: true,\n          },\n        ]\n      : [\n          {\n            type: 'select',\n            name: 'amount',\n            label: t('Purchase.Form.amountLabel'),\n            defaultValue: '0',\n            options: [\n              {\n                label: t('Purchase.Form.selectAmountPlaceholder'),\n                value: '0',\n              },\n              ...giftCertificateSettings.amounts.map((amount) => ({\n                label: amount.formattedV2 ?? '',\n                value: String(amount.value),\n              })),\n            ],\n            required: true,\n          },\n        ];\n\n  return [...amountFields, ...baseFields];\n}\n\nfunction getExpiryDate(\n  expiry: ResultOf<typeof GiftCertificateSettingsFragment>['expiry'],\n): number | undefined {\n  if (!expiry?.unit || !expiry.value) {\n    return undefined;\n  }\n\n  switch (expiry.unit) {\n    case 'DAYS':\n      return Date.now() + expiry.value * 24 * 60 * 60 * 1000;\n\n    case 'WEEKS':\n      return Date.now() + expiry.value * 7 * 24 * 60 * 60 * 1000;\n\n    case 'MONTHS':\n      return Date.now() + expiry.value * 30 * 24 * 60 * 60 * 1000;\n\n    case 'YEARS':\n      return Date.now() + expiry.value * 365 * 24 * 60 * 60 * 1000;\n\n    default:\n      return undefined;\n  }\n}\n\nexport default async function GiftCertificatePurchasePage({ params }: Props) {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'GiftCertificates' });\n  const format = await getFormatter();\n  const currencyCode = await getPreferredCurrencyCode();\n  const data = await getGiftCertificatePurchaseData(currencyCode);\n\n  if (!data.giftCertificateSettings?.isEnabled) {\n    return redirect({ href: '/', locale });\n  }\n\n  const expiryDate = getExpiryDate(data.giftCertificateSettings.expiry);\n  const expiresAt = expiryDate ? format.dateTime(expiryDate, { dateStyle: 'long' }) : undefined;\n  const fields = getFields(data.giftCertificateSettings, expiresAt, t);\n\n  return (\n    <GiftCertificatePurchaseSection\n      action={addGiftCertificateToCart}\n      breadcrumbs={[\n        { label: t('title'), href: '/gift-certificates' },\n        { label: t('Purchase.breadcrumbTitle'), href: '#' },\n      ]}\n      ctaLabel={t('Purchase.Form.ctaLabel')}\n      currencyCode={currencyCode ?? data.defaultCurrency}\n      description={t('Purchase.description')}\n      expiresAt={expiresAt}\n      expiresAtLabel={t('expiresAtLabel')}\n      formFields={fields}\n      logo={data.logo}\n      settings={{\n        minCustomAmount:\n          'minimumAmount' in data.giftCertificateSettings\n            ? data.giftCertificateSettings.minimumAmount.value\n            : undefined,\n        maxCustomAmount:\n          'maximumAmount' in data.giftCertificateSettings\n            ? data.giftCertificateSettings.maximumAmount.value\n            : undefined,\n      }}\n      subtitle={data.storeName}\n      title={t('Purchase.title')}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/layout.tsx",
    "content": "import { setRequestLocale } from 'next-intl/server';\nimport { PropsWithChildren } from 'react';\n\nimport { Footer } from '~/components/footer';\nimport { Header } from '~/components/header';\n\ninterface Props extends PropsWithChildren {\n  params: Promise<{ locale: string }>;\n}\n\nexport default async function DefaultLayout({ params, children }: Props) {\n  const { locale } = await params;\n\n  setRequestLocale(locale);\n\n  return (\n    <>\n      <Header />\n\n      <main>{children}</main>\n\n      <Footer />\n    </>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { FeaturedProductsCarouselFragment } from '~/components/featured-products-carousel/fragment';\nimport { FeaturedProductsListFragment } from '~/components/featured-products-list/fragment';\nimport { FooterFragment, FooterSectionsFragment } from '~/components/footer/fragment';\nimport { CurrencyCode, HeaderFragment, HeaderLinksFragment } from '~/components/header/fragment';\n\nexport const LayoutQuery = graphql(\n  `\n    query LayoutQuery {\n      site {\n        ...HeaderFragment\n        ...FooterFragment\n      }\n    }\n  `,\n  [HeaderFragment, FooterFragment],\n);\n\nconst GiftCertificatesEnabledFragment = graphql(`\n  fragment GiftCertificatesEnabledFragment on Settings {\n    giftCertificates(currencyCode: $currencyCode) {\n      isEnabled\n    }\n  }\n`);\n\nexport const GetLinksAndSectionsQuery = graphql(\n  `\n    query GetLinksAndSectionsQuery($currencyCode: currencyCode) {\n      site {\n        settings {\n          ...GiftCertificatesEnabledFragment\n        }\n        ...HeaderLinksFragment\n        ...FooterSectionsFragment\n      }\n    }\n  `,\n  [HeaderLinksFragment, FooterSectionsFragment, GiftCertificatesEnabledFragment],\n);\n\nconst HomePageQuery = graphql(\n  `\n    query HomePageQuery($currencyCode: currencyCode) {\n      site {\n        featuredProducts(first: 12) {\n          edges {\n            node {\n              ...FeaturedProductsListFragment\n            }\n          }\n        }\n        newestProducts(first: 12) {\n          edges {\n            node {\n              ...FeaturedProductsCarouselFragment\n            }\n          }\n        }\n        settings {\n          inventory {\n            defaultOutOfStockMessage\n            showOutOfStockMessage\n            showBackorderMessage\n          }\n          newsletter {\n            showNewsletterSignup\n          }\n        }\n      }\n    }\n  `,\n  [FeaturedProductsCarouselFragment, FeaturedProductsListFragment],\n);\n\nexport const getPageData = cache(\n  async (currencyCode?: CurrencyCode, customerAccessToken?: string) => {\n    const { data } = await client.fetch({\n      document: HomePageQuery,\n      customerAccessToken,\n      variables: { currencyCode },\n      fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },\n    });\n\n    return data;\n  },\n);\n"
  },
  {
    "path": "core/app/[locale]/(default)/page.tsx",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { Metadata } from 'next';\nimport { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { FeaturedProductCarousel } from '@/vibes/soul/sections/featured-product-carousel';\nimport { FeaturedProductList } from '@/vibes/soul/sections/featured-product-list';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { Subscribe } from '~/components/subscribe';\nimport { productCardTransformer } from '~/data-transformers/product-card-transformer';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\nimport { getMetadataAlternates } from '~/lib/seo/canonical';\n\nimport { Slideshow } from './_components/slideshow';\nimport { getPageData } from './page-data';\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  return {\n    alternates: await getMetadataAlternates({ path: '/', locale }),\n  };\n}\n\nexport default async function Home({ params }: Props) {\n  const { locale } = await params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Home');\n  const format = await getFormatter();\n\n  const streamablePageData = Streamable.from(async () => {\n    const customerAccessToken = await getSessionCustomerAccessToken();\n    const currencyCode = await getPreferredCurrencyCode();\n\n    return getPageData(currencyCode, customerAccessToken);\n  });\n\n  const streamableFeaturedProducts = Streamable.from(async () => {\n    const data = await streamablePageData;\n\n    const featuredProducts = removeEdgesAndNodes(data.site.featuredProducts);\n\n    const { defaultOutOfStockMessage, showOutOfStockMessage, showBackorderMessage } =\n      data.site.settings?.inventory ?? {};\n\n    return productCardTransformer(\n      featuredProducts,\n      format,\n      showOutOfStockMessage ? defaultOutOfStockMessage : undefined,\n      showBackorderMessage,\n    );\n  });\n\n  const streamableNewestProducts = Streamable.from(async () => {\n    const data = await streamablePageData;\n\n    const newestProducts = removeEdgesAndNodes(data.site.newestProducts);\n\n    const { defaultOutOfStockMessage, showOutOfStockMessage, showBackorderMessage } =\n      data.site.settings?.inventory ?? {};\n\n    return productCardTransformer(\n      newestProducts,\n      format,\n      showOutOfStockMessage ? defaultOutOfStockMessage : undefined,\n      showBackorderMessage,\n    );\n  });\n\n  const streamableShowNewsletterSignup = Streamable.from(async () => {\n    const data = await streamablePageData;\n\n    const { showNewsletterSignup } = data.site.settings?.newsletter ?? {};\n\n    return showNewsletterSignup;\n  });\n\n  return (\n    <>\n      <Slideshow />\n\n      <FeaturedProductList\n        cta={{ label: t('FeaturedProducts.cta'), href: '/shop-all' }}\n        description={t('FeaturedProducts.description')}\n        emptyStateSubtitle={t('FeaturedProducts.emptyStateSubtitle')}\n        emptyStateTitle={t('FeaturedProducts.emptyStateTitle')}\n        products={streamableFeaturedProducts}\n        title={t('FeaturedProducts.title')}\n      />\n\n      <FeaturedProductCarousel\n        cta={{ label: t('NewestProducts.cta'), href: '/shop-all/?sort=newest' }}\n        description={t('NewestProducts.description')}\n        emptyStateSubtitle={t('NewestProducts.emptyStateSubtitle')}\n        emptyStateTitle={t('NewestProducts.emptyStateTitle')}\n        nextLabel={t('NewestProducts.nextProducts')}\n        previousLabel={t('NewestProducts.previousProducts')}\n        products={streamableNewestProducts}\n        title={t('NewestProducts.title')}\n      />\n\n      <Stream fallback={null} value={streamableShowNewsletterSignup}>\n        {(showNewsletterSignup) => showNewsletterSignup && <Subscribe />}\n      </Stream>\n    </>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_actions/add-to-cart.tsx",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getTranslations } from 'next-intl/server';\nimport { ReactNode } from 'react';\n\nimport { Field, schema } from '@/vibes/soul/sections/product-detail/schema';\nimport { graphql } from '~/client/graphql';\nimport { Link } from '~/components/link';\nimport { addToOrCreateCart } from '~/lib/cart';\nimport { MissingCartError } from '~/lib/cart/error';\n\ntype CartSelectedOptionsInput = ReturnType<typeof graphql.scalar<'CartSelectedOptionsInput'>>;\n\ninterface State {\n  fields: Field[];\n  lastResult: SubmissionResult | null;\n  successMessage?: ReactNode;\n}\n\nexport const addToCart = async (\n  prevState: State,\n  payload: FormData,\n): Promise<{\n  fields: Field[];\n  lastResult: SubmissionResult | null;\n  successMessage?: ReactNode;\n}> => {\n  const t = await getTranslations('Product.ProductDetails');\n\n  const submission = parseWithZod(payload, { schema: schema(prevState.fields) });\n\n  if (submission.status !== 'success') {\n    return { lastResult: submission.reply(), fields: prevState.fields };\n  }\n\n  const productEntityId = Number(submission.value.id);\n  const quantity = Number(submission.value.quantity);\n\n  const selectedOptions = prevState.fields.reduce<CartSelectedOptionsInput>((accum, field) => {\n    const optionValueEntityId = submission.value[field.name];\n\n    let multipleChoicesOptionInput;\n    let checkboxOptionInput;\n    let numberFieldOptionInput;\n    let textFieldOptionInput;\n    let multiLineTextFieldOptionInput;\n    let dateFieldOptionInput;\n\n    // Skip empty strings since option is empty\n    if (!optionValueEntityId) return accum;\n\n    switch (field.type) {\n      case 'select':\n      case 'radio-group':\n      case 'swatch-radio-group':\n      case 'card-radio-group':\n      case 'button-radio-group':\n        multipleChoicesOptionInput = {\n          optionEntityId: Number(field.name),\n          optionValueEntityId: Number(optionValueEntityId),\n        };\n\n        if (accum.multipleChoices) {\n          return {\n            ...accum,\n            multipleChoices: [...accum.multipleChoices, multipleChoicesOptionInput],\n          };\n        }\n\n        return { ...accum, multipleChoices: [multipleChoicesOptionInput] };\n\n      case 'checkbox':\n        checkboxOptionInput = {\n          optionEntityId: Number(field.name),\n          optionValueEntityId:\n            optionValueEntityId === 'true'\n              ? Number(field.checkedValue)\n              : Number(field.uncheckedValue),\n        };\n\n        if (accum.checkboxes) {\n          return { ...accum, checkboxes: [...accum.checkboxes, checkboxOptionInput] };\n        }\n\n        return { ...accum, checkboxes: [checkboxOptionInput] };\n\n      case 'number':\n        numberFieldOptionInput = {\n          optionEntityId: Number(field.name),\n          number: Number(optionValueEntityId),\n        };\n\n        if (accum.numberFields) {\n          return { ...accum, numberFields: [...accum.numberFields, numberFieldOptionInput] };\n        }\n\n        return { ...accum, numberFields: [numberFieldOptionInput] };\n\n      case 'text':\n        textFieldOptionInput = {\n          optionEntityId: Number(field.name),\n          text: String(optionValueEntityId),\n        };\n\n        if (accum.textFields) {\n          return {\n            ...accum,\n            textFields: [...accum.textFields, textFieldOptionInput],\n          };\n        }\n\n        return { ...accum, textFields: [textFieldOptionInput] };\n\n      case 'textarea':\n        multiLineTextFieldOptionInput = {\n          optionEntityId: Number(field.name),\n          text: String(optionValueEntityId),\n        };\n\n        if (accum.multiLineTextFields) {\n          return {\n            ...accum,\n            multiLineTextFields: [...accum.multiLineTextFields, multiLineTextFieldOptionInput],\n          };\n        }\n\n        return { ...accum, multiLineTextFields: [multiLineTextFieldOptionInput] };\n\n      case 'date':\n        dateFieldOptionInput = {\n          optionEntityId: Number(field.name),\n          date: new Date(String(optionValueEntityId)).toISOString(),\n        };\n\n        if (accum.dateFields) {\n          return {\n            ...accum,\n            dateFields: [...accum.dateFields, dateFieldOptionInput],\n          };\n        }\n\n        return { ...accum, dateFields: [dateFieldOptionInput] };\n\n      default:\n        return { ...accum };\n    }\n  }, {});\n\n  try {\n    await addToOrCreateCart({\n      lineItems: [\n        {\n          productEntityId,\n          selectedOptions,\n          quantity,\n        },\n      ],\n    });\n\n    return {\n      lastResult: submission.reply(),\n      fields: prevState.fields,\n      successMessage: t.rich('successMessage', {\n        cartItems: quantity,\n        cartLink: (chunks) => (\n          <Link className=\"underline\" href=\"/cart\" prefetch=\"viewport\" prefetchKind=\"full\">\n            {chunks}\n          </Link>\n        ),\n      }),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => {\n            if (message.includes('Not enough stock:')) {\n              // This removes the item id from the message. It's very brittle, but it's the only\n              // solution to do it until our API returns a better error message.\n              return message.replace('Not enough stock: ', '').replace(/\\(\\w.+\\)\\s{1}/, '');\n            }\n\n            return message;\n          }),\n        }),\n        fields: prevState.fields,\n      };\n    }\n\n    if (error instanceof MissingCartError) {\n      return {\n        lastResult: submission.reply({ formErrors: [t('missingCart')] }),\n        fields: prevState.fields,\n      };\n    }\n\n    if (error instanceof Error) {\n      return {\n        lastResult: submission.reply({ formErrors: [error.message] }),\n        fields: prevState.fields,\n      };\n    }\n\n    return {\n      lastResult: submission.reply({ formErrors: [t('unknownError')] }),\n      fields: prevState.fields,\n    };\n  }\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_actions/get-more-images.ts",
    "content": "'use server';\n\nimport { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\n\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\n\nconst MoreProductImagesQuery = graphql(`\n  query MoreProductImagesQuery($entityId: Int!, $first: Int!, $after: String!) {\n    site {\n      product(entityId: $entityId) {\n        images(first: $first, after: $after) {\n          pageInfo {\n            hasNextPage\n            endCursor\n          }\n          edges {\n            node {\n              altText\n              url: urlTemplate(lossy: true)\n            }\n          }\n        }\n      }\n    }\n  }\n`);\n\nexport async function getMoreProductImages(\n  productId: number,\n  cursor: string,\n  limit = 12,\n): Promise<{\n  images: Array<{ src: string; alt: string }>;\n  pageInfo: { hasNextPage: boolean; endCursor: string | null };\n}> {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const { data } = await client.fetch({\n    document: MoreProductImagesQuery,\n    variables: { entityId: productId, first: limit, after: cursor },\n    customerAccessToken,\n    fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },\n  });\n\n  const images = removeEdgesAndNodes(data.site.product?.images ?? { edges: [] });\n\n  return {\n    images: images.map((img) => ({ src: img.url, alt: img.altText })),\n    pageInfo: data.site.product?.images.pageInfo ?? { hasNextPage: false, endCursor: null },\n  };\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_actions/submit-review.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getTranslations } from 'next-intl/server';\n\nimport { schema } from '@/vibes/soul/sections/reviews/schema';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha';\n\nconst AddProductReviewMutation = graphql(`\n  mutation AddProductReviewMutation(\n    $input: AddProductReviewInput!\n    $reCaptchaV2: ReCaptchaV2Input\n  ) {\n    catalog {\n      addProductReview(input: $input, reCaptchaV2: $reCaptchaV2) {\n        __typename\n        errors {\n          __typename\n          ... on Error {\n            message\n          }\n        }\n      }\n    }\n  }\n`);\n\nexport async function submitReview(\n  prevState: { lastResult: SubmissionResult | null; successMessage?: string },\n  payload: FormData,\n) {\n  const t = await getTranslations('Product.Reviews.Form');\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  const submission = parseWithZod(payload, { schema });\n\n  if (submission.status !== 'success') {\n    return { ...prevState, lastResult: submission.reply() };\n  }\n\n  const { siteKey, token } = await getRecaptchaFromForm(payload);\n  const recaptchaValidation = assertRecaptchaTokenPresent(siteKey, token, t('recaptchaRequired'));\n\n  if (!recaptchaValidation.success) {\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: recaptchaValidation.formErrors }),\n    };\n  }\n\n  const { productEntityId, ...input } = submission.value;\n\n  try {\n    const response = await client.fetch({\n      document: AddProductReviewMutation,\n      customerAccessToken,\n      fetchOptions: { cache: 'no-store' },\n      variables: {\n        input: {\n          review: {\n            ...input,\n          },\n          productEntityId,\n        },\n        reCaptchaV2:\n          recaptchaValidation.token != null ? { token: recaptchaValidation.token } : undefined,\n      },\n    });\n\n    const result = response.data.catalog.addProductReview;\n\n    if (result.errors.length > 0) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: result.errors.map(({ message }) => message) }),\n      };\n    }\n\n    return {\n      ...prevState,\n      lastResult: submission.reply(),\n      successMessage: t('successMessage'),\n    };\n  } catch (error) {\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: error.errors.map(({ message }) => message) }),\n      };\n    }\n\n    if (error instanceof Error) {\n      return {\n        ...prevState,\n        lastResult: submission.reply({ formErrors: [error.message] }),\n      };\n    }\n\n    return {\n      ...prevState,\n      lastResult: submission.reply({ formErrors: [t('somethingWentWrong')] }),\n    };\n  }\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_actions/wishlist-action.ts",
    "content": "'use server';\n\nimport {\n  BigCommerceAPIError,\n  BigCommerceGQLError,\n  removeEdgesAndNodes,\n} from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { revalidateTag } from 'next/cache';\nimport { getLocale, getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { CreateWishlistMutation } from '~/app/[locale]/(default)/account/wishlists/_actions/mutation';\nimport { newWishlist } from '~/app/[locale]/(default)/account/wishlists/_actions/new-wishlist';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { buildConfig } from '~/build-config/reader';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\nimport { WishlistMutationError } from '~/components/wishlist/error';\nimport { redirect } from '~/i18n/routing';\nimport { serverToast } from '~/lib/server-toast';\n\nconst VariantIdFromSkuQuery = graphql(`\n  query VariantIdFromSkuQuery($productId: Int!, $sku: String!) {\n    site {\n      product(entityId: $productId) {\n        variants(skus: [$sku]) {\n          edges {\n            node {\n              entityId\n            }\n          }\n        }\n      }\n    }\n  }\n`);\n\nconst AddToWishlistMutation = graphql(`\n  mutation AddToWishlistMutation($wishlistId: Int!, $productId: Int!, $variantId: Int) {\n    wishlist {\n      addWishlistItems(\n        input: {\n          entityId: $wishlistId\n          items: [{ productEntityId: $productId, variantEntityId: $variantId }]\n        }\n      ) {\n        result {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\nconst DeleteWishlistItemsMutation = graphql(`\n  mutation DeleteWishlistItemsMutation($wishlistId: Int!, $wishlistItemId: Int!) {\n    wishlist {\n      deleteWishlistItems(input: { entityId: $wishlistId, itemEntityIds: [$wishlistItemId] }) {\n        result {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\nconst menuItemSchema = z.object({\n  action: z.enum(['add', 'remove']),\n  wishlistId: z.number().nonnegative(),\n  wishlistItemId: z.number().optional(),\n  redirectTo: z.string(),\n});\n\nconst menuItemParser = z.string().transform((str, ctx) => {\n  try {\n    return menuItemSchema.parse(JSON.parse(str));\n  } catch {\n    ctx.addIssue({ code: 'custom', message: 'Invalid menuItem payload.' });\n\n    return z.NEVER;\n  }\n});\n\nconst schema = z.object({\n  productId: z.number().nonnegative().min(1),\n  selectedSku: z.string(),\n  menuItem: menuItemParser,\n});\n\ninterface WishlistAddMutationVariables {\n  wishlistId: number;\n  productId: number;\n  variantId?: number;\n}\n\ninterface WishlistRemoveMutationVariables {\n  wishlistId: number;\n  wishlistItemId: number;\n}\n\nasync function getVariantIdFromSku(productId: number, sku: string, customerAccessToken?: string) {\n  const { data } = await client.fetch({\n    document: VariantIdFromSkuQuery,\n    variables: { productId, sku },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n\n  if (!data.site.product?.variants) {\n    return undefined;\n  }\n\n  return removeEdgesAndNodes(data.site.product.variants)[0]?.entityId ?? undefined;\n}\n\nasync function addToDefaultWishlist(\n  customerAccessToken: string,\n  wishlistName: string,\n  productId: number,\n  variantId?: number,\n) {\n  const { data } = await client.fetch({\n    document: CreateWishlistMutation,\n    variables: {\n      input: {\n        name: wishlistName,\n        isPublic: false,\n        items: [{ productEntityId: productId, variantEntityId: variantId }],\n      },\n    },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n\n  if (!data.wishlist.createWishlist?.result) {\n    throw new WishlistMutationError('Failed to add item to default wishlist. Response was empty.');\n  }\n}\n\nasync function addToWishlist(customerAccessToken: string, variables: WishlistAddMutationVariables) {\n  const { data } = await client.fetch({\n    document: AddToWishlistMutation,\n    variables,\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n\n  if (!data.wishlist.addWishlistItems?.result) {\n    throw new WishlistMutationError('Failed to add item to wishlist. Response was empty.');\n  }\n}\n\nasync function removeFromWishlist(\n  customerAccessToken: string,\n  variables: WishlistRemoveMutationVariables,\n) {\n  const { data } = await client.fetch({\n    document: DeleteWishlistItemsMutation,\n    variables,\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n\n  if (!data.wishlist.deleteWishlistItems?.result) {\n    throw new WishlistMutationError('Failed to add item to wishlist. Response was empty.');\n  }\n}\n\nfunction getLoginRedirect(redirectTo: string) {\n  const vanityUrl = buildConfig.get('urls').vanityUrl;\n  const redirectToUrl = new URL(redirectTo, vanityUrl);\n  const redirectToParam = redirectToUrl.pathname + redirectToUrl.search;\n  const loginParams = new URLSearchParams({ redirectTo: redirectToParam });\n\n  return `/login?${loginParams.toString()}`;\n}\n\nexport async function wishlistAction(payload: FormData): Promise<void> {\n  const locale = await getLocale();\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  const t = await getTranslations('Wishlist');\n  const submission = parseWithZod(payload, { schema });\n\n  if (submission.status !== 'success') {\n    await serverToast.error(t('Errors.unexpected'));\n\n    return;\n  }\n\n  if (!customerAccessToken) {\n    redirect({ href: getLoginRedirect(submission.value.menuItem.redirectTo), locale });\n\n    return;\n  }\n\n  try {\n    const {\n      productId,\n      selectedSku,\n      menuItem: { action, wishlistId, wishlistItemId },\n    } = submission.value;\n\n    switch (action) {\n      case 'add': {\n        const variantId = await getVariantIdFromSku(productId, selectedSku, customerAccessToken);\n\n        if (wishlistId === 0) {\n          await addToDefaultWishlist(\n            customerAccessToken,\n            t('Button.defaultWishlistName'),\n            productId,\n            variantId,\n          );\n        } else {\n          await addToWishlist(customerAccessToken, { wishlistId, productId, variantId });\n        }\n\n        await serverToast.success(t('Button.addSuccessMessage'));\n\n        break;\n      }\n\n      case 'remove': {\n        if (!wishlistItemId) {\n          throw new WishlistMutationError('wishlistItemId is required for remove action');\n        }\n\n        await removeFromWishlist(customerAccessToken, { wishlistId, wishlistItemId });\n        await serverToast.success(t('Button.removeSuccessMessage'));\n\n        break;\n      }\n    }\n\n    revalidateTag(TAGS.customer, { expire: 0 });\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      if (error.message.includes('Please sign in')) {\n        redirect({ href: getLoginRedirect(submission.value.menuItem.redirectTo), locale });\n\n        return;\n      }\n\n      await serverToast.error(t('Errors.unexpected'));\n    }\n\n    if (error instanceof BigCommerceAPIError) {\n      await serverToast.error(t('Errors.unexpected'));\n    }\n\n    await serverToast.error(t('Errors.unexpected'));\n  }\n}\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  successMessage?: string;\n}\n\nconst addToNewWishlistSchema = z.object({\n  productId: z.number().nonnegative().min(1),\n  selectedSku: z.string(),\n  wishlistName: z.string().trim().nonempty(),\n  redirectTo: z.string(),\n});\n\nexport async function addToNewWishlist(\n  prevState: Awaited<State>,\n  formData: FormData,\n): Promise<State> {\n  const locale = await getLocale();\n  const t = await getTranslations('Wishlist');\n  const submission = parseWithZod(formData, { schema: addToNewWishlistSchema });\n\n  if (submission.status !== 'success') {\n    return { lastResult: submission.reply({ formErrors: [t('Errors.unexpected')] }) };\n  }\n\n  const { productId, selectedSku, redirectTo } = submission.value;\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  const variantId = await getVariantIdFromSku(productId, selectedSku, customerAccessToken);\n\n  formData.append('wishlistItems[0].productEntityId', productId.toString());\n\n  if (variantId) {\n    formData.append('wishlistItems[0].variantEntityId', variantId.toString());\n  }\n\n  if (!customerAccessToken) {\n    redirect({ href: getLoginRedirect(redirectTo), locale });\n\n    return { lastResult: null };\n  }\n\n  const result = await newWishlist(prevState, formData);\n\n  if (result.lastResult?.status === 'success') {\n    await serverToast.success(t('Button.addSuccessMessage'));\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_components/product-analytics-provider.tsx",
    "content": "'use client';\n\nimport { PropsWithChildren, Suspense } from 'react';\nimport { z } from 'zod';\n\nimport { Streamable, useStreamable } from '@/vibes/soul/lib/streamable';\nimport { EventsProvider } from '~/components/analytics/events';\nimport { useAnalytics } from '~/lib/analytics/react';\n\ninterface AddToCartContext {\n  id: number;\n  name: string;\n  brand: string;\n  sku?: string;\n  currency: string;\n  price: number;\n}\n\nconst AddToCartSchema = z.object({\n  quantity: z.number({ coerce: true }).default(1),\n});\n\nexport function ProductAnalyticsProvider(\n  props: PropsWithChildren<{ data: Streamable<AddToCartContext> }>,\n) {\n  return (\n    <Suspense fallback={props.children}>\n      <ProductAnalyticsProviderResolved {...props} />\n    </Suspense>\n  );\n}\n\nfunction ProductAnalyticsProviderResolved({\n  children,\n  data,\n}: PropsWithChildren<{ data: Streamable<AddToCartContext> }>) {\n  const analytics = useAnalytics();\n  const { id, name, brand, sku, currency, price } = useStreamable(data);\n\n  const onAddToCart = (payload?: FormData) => {\n    const parsedPayload = AddToCartSchema.safeParse(Object.fromEntries(payload?.entries() ?? []));\n\n    if (parsedPayload.success) {\n      const { quantity } = parsedPayload.data;\n\n      analytics?.cart.productAdded({\n        currency,\n        value: quantity * price,\n        items: [\n          {\n            id: id.toString(),\n            name,\n            brand,\n            sku,\n            price,\n            quantity,\n          },\n        ],\n      });\n    }\n  };\n\n  return <EventsProvider onAddToCart={onAddToCart}>{children}</EventsProvider>;\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_components/product-review-schema/fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nexport const ProductReviewSchemaFragment = graphql(`\n  fragment ProductReviewSchemaFragment on Review {\n    author {\n      name\n    }\n    title\n    text\n    rating\n    createdAt {\n      utc\n    }\n  }\n`);\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_components/product-review-schema/product-review-schema.tsx",
    "content": "'use client';\n\n// eslint-disable-next-line import/no-named-as-default\nimport DOMPurify from 'dompurify';\nimport { useFormatter } from 'next-intl';\nimport { Product as ProductSchemaType, WithContext } from 'schema-dts';\n\nimport { FragmentOf } from '~/client/graphql';\n\nimport { ProductReviewSchemaFragment } from './fragment';\n\ninterface Props {\n  productId: number;\n  reviews: Array<FragmentOf<typeof ProductReviewSchemaFragment>>;\n}\n\nexport const ProductReviewSchema = ({ reviews, productId }: Props) => {\n  const format = useFormatter();\n\n  const productReviewSchema: WithContext<ProductSchemaType> = {\n    '@context': 'https://schema.org',\n    '@type': 'Product',\n    '@id': `product-${productId}`,\n    review: reviews.map((review) => {\n      return {\n        '@type': 'Review' as const,\n        datePublished: format.dateTime(new Date(review.createdAt.utc)),\n        name: review.title,\n        reviewBody: review.text,\n        author: {\n          '@type': 'Person' as const,\n          name: review.author.name,\n        },\n        reviewRating: {\n          '@type': 'Rating' as const,\n          bestRating: 5,\n          ratingValue: review.rating,\n          worstRating: 1,\n        },\n      };\n    }),\n  };\n\n  return (\n    <script\n      dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(JSON.stringify(productReviewSchema)) }}\n      type=\"application/ld+json\"\n    />\n  );\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_components/product-schema/fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nexport const ProductSchemaFragment = graphql(`\n  fragment ProductSchemaFragment on Product {\n    name\n    path\n    plainTextDescription(characterLimit: 1200)\n    sku\n    gtin\n    mpn\n    brand {\n      name\n      path\n    }\n    reviewSummary {\n      averageRating\n      numberOfReviews\n    }\n    defaultImage {\n      url: urlTemplate(lossy: true)\n    }\n    condition\n    availabilityV2 {\n      status\n    }\n  }\n`);\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_components/product-schema/index.tsx",
    "content": "import { Product as ProductSchemaType, WithContext } from 'schema-dts';\n\nimport { PricingFragment } from '~/client/fragments/pricing';\nimport { FragmentOf } from '~/client/graphql';\n\nimport { ProductSchemaFragment } from './fragment';\n\ninterface Props {\n  product: FragmentOf<typeof ProductSchemaFragment> & FragmentOf<typeof PricingFragment>;\n}\n\nexport const ProductSchema = ({ product }: Props) => {\n  /* TODO: use common default image when product has no images */\n  const image = product.defaultImage ? { image: product.defaultImage.url } : null;\n\n  const sku = product.sku ? { sku: product.sku } : null;\n  const gtin = product.gtin ? { gtin: product.gtin } : null;\n  const mpn = product.mpn ? { mpn: product.mpn } : null;\n\n  const brand = product.brand\n    ? {\n        '@type': 'Brand' as const,\n        url: product.brand.path,\n        name: product.brand.name,\n      }\n    : null;\n\n  const aggregateRating =\n    product.reviewSummary.numberOfReviews > 0\n      ? {\n          '@type': 'AggregateRating' as const,\n          ratingValue: product.reviewSummary.averageRating,\n          reviewCount: product.reviewSummary.numberOfReviews,\n        }\n      : null;\n\n  const priceSpecification = product.prices\n    ? {\n        '@type': 'PriceSpecification' as const,\n        price: product.prices.price.value,\n        priceCurrency: product.prices.price.currencyCode,\n        ...(product.prices.priceRange.min.value !== product.prices.priceRange.max.value\n          ? {\n              minPrice: product.prices.priceRange.min.value,\n              maxPrice: product.prices.priceRange.max.value,\n            }\n          : null),\n      }\n    : null;\n\n  enum ItemCondition {\n    NEW = 'https://schema.org/NewCondition',\n    USED = 'https://schema.org/UsedCondition',\n    REFURBISHED = 'https://schema.org/RefurbishedCondition',\n  }\n\n  const itemCondition = ItemCondition[product.condition ?? 'NEW'];\n\n  enum Availability {\n    Preorder = 'PreOrder',\n    Unavailable = 'OutOfStock',\n    Available = 'InStock',\n  }\n\n  const availability = Availability[product.availabilityV2.status];\n\n  const productSchema: WithContext<ProductSchemaType> = {\n    '@context': 'https://schema.org',\n    '@type': 'Product',\n    name: product.name,\n    url: product.path,\n    description: product.plainTextDescription,\n    ...(brand && { brand }),\n    ...(aggregateRating && { aggregateRating }),\n    ...image,\n    ...sku,\n    ...gtin,\n    ...mpn,\n    offers: {\n      '@type': 'Offer',\n      ...(priceSpecification && { priceSpecification }),\n      itemCondition,\n      availability,\n      url: product.path,\n    },\n  };\n\n  return (\n    <script\n      dangerouslySetInnerHTML={{ __html: JSON.stringify(productSchema) }}\n      type=\"application/ld+json\"\n    />\n  );\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_components/product-viewed/fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nexport const ProductViewedFragment = graphql(`\n  fragment ProductViewedFragment on Product {\n    entityId\n    name\n    brand {\n      name\n    }\n    sku\n    description\n    plainTextDescription(characterLimit: 1200)\n    path\n    variants {\n      edges {\n        node {\n          entityId\n        }\n      }\n    }\n  }\n`);\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_components/product-viewed/index.tsx",
    "content": "'use client';\n\nimport { useEffect, useRef } from 'react';\n\nimport { PricingFragment } from '~/client/fragments/pricing';\nimport { FragmentOf } from '~/client/graphql';\nimport { useAnalytics } from '~/lib/analytics/react';\n\nimport { ProductViewedFragment } from './fragment';\n\ninterface Props {\n  product: FragmentOf<typeof ProductViewedFragment> & FragmentOf<typeof PricingFragment>;\n}\n\nexport const ProductViewed = ({ product }: Props) => {\n  const isMounted = useRef(false);\n  const analytics = useAnalytics();\n\n  useEffect(() => {\n    if (isMounted.current) {\n      return;\n    }\n\n    isMounted.current = true;\n\n    analytics?.navigation.productViewed({\n      value: product.prices?.price.value ?? 0,\n      currency: product.prices?.price.currencyCode ?? 'USD',\n      items: [\n        {\n          id: product.entityId.toString(),\n          name: product.name,\n          brand: product.brand?.name,\n          sku: product.sku,\n          price: product.prices?.salePrice?.value,\n        },\n      ],\n    });\n  }, [analytics, product]);\n\n  return null;\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_components/reviews.tsx",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { getFormatter, getTranslations } from 'next-intl/server';\nimport { createLoader, parseAsString, SearchParams } from 'nuqs/server';\nimport { cache } from 'react';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Reviews as ReviewsSection } from '@/vibes/soul/sections/reviews';\nimport { auth } from '~/auth';\nimport { client } from '~/client';\nimport { PaginationFragment } from '~/client/fragments/pagination';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { defaultPageInfo, pageInfoTransformer } from '~/data-transformers/page-info-transformer';\n\nimport { submitReview } from '../_actions/submit-review';\nimport { getStreamableProduct } from '../page-data';\n\nimport { ProductReviewSchemaFragment } from './product-review-schema/fragment';\nimport { ProductReviewSchema } from './product-review-schema/product-review-schema';\n\nconst PaginationSearchParamNames = {\n  BEFORE: 'reviews_before',\n  AFTER: 'reviews_after',\n} as const;\n\nconst loadReviewsPaginationSearchParams = createLoader({\n  [PaginationSearchParamNames.BEFORE]: parseAsString,\n  [PaginationSearchParamNames.AFTER]: parseAsString,\n});\n\nconst ReviewsQuery = graphql(\n  `\n    query ReviewsQuery($entityId: Int!, $first: Int, $after: String, $before: String, $last: Int) {\n      site {\n        product(entityId: $entityId) {\n          reviewSummary {\n            averageRating\n            numberOfReviews\n          }\n          reviews(first: $first, after: $after, before: $before, last: $last) {\n            pageInfo {\n              ...PaginationFragment\n            }\n            edges {\n              node {\n                ...ProductReviewSchemaFragment\n                author {\n                  name\n                }\n                entityId\n                title\n                text\n                rating\n                createdAt {\n                  utc\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  `,\n  [ProductReviewSchemaFragment, PaginationFragment],\n);\n\nconst getReviews = cache(async (productId: number, paginationArgs: object) => {\n  const { data } = await client.fetch({\n    document: ReviewsQuery,\n    variables: { ...paginationArgs, entityId: productId },\n    fetchOptions: { next: { revalidate } },\n  });\n\n  return data.site.product;\n});\n\ninterface Props {\n  productId: number;\n  searchParams: Promise<SearchParams>;\n  streamableImages: Streamable<{\n    images: Array<{ src: string; alt: string }>;\n    pageInfo?: { hasNextPage: boolean; endCursor: string | null };\n  }>;\n  streamableProduct: Streamable<Awaited<ReturnType<typeof getStreamableProduct>>>;\n  recaptchaSiteKey?: string;\n}\n\nexport const Reviews = async ({\n  productId,\n  searchParams,\n  streamableProduct,\n  streamableImages,\n  recaptchaSiteKey,\n}: Props) => {\n  const t = await getTranslations('Product.Reviews');\n\n  const streamableReviewsData = Streamable.from(async () => {\n    const paginationSearchParams = await loadReviewsPaginationSearchParams(searchParams);\n\n    const {\n      [PaginationSearchParamNames.AFTER]: after,\n      [PaginationSearchParamNames.BEFORE]: before,\n    } = paginationSearchParams;\n    const paginationArgs = before == null ? { first: 5, after } : { last: 5, before };\n\n    return getReviews(productId, paginationArgs);\n  });\n\n  const streamableReviews = Streamable.from(async () => {\n    const product = await streamableReviewsData;\n    const format = await getFormatter();\n\n    if (!product) {\n      return [];\n    }\n\n    return removeEdgesAndNodes(product.reviews).map((review) => ({\n      id: review.entityId.toString(),\n      rating: review.rating,\n      review: review.text,\n      name: review.author.name,\n      date: format.dateTime(new Date(review.createdAt.utc), {\n        year: 'numeric',\n        month: 'long',\n        day: 'numeric',\n      }),\n    }));\n  });\n\n  const streamableAvergeRating = Streamable.from(async () => {\n    const product = await streamableReviewsData;\n\n    if (!product) {\n      return 0;\n    }\n\n    return product.reviewSummary.averageRating;\n  });\n\n  const streamablePaginationInfo = Streamable.from(async () => {\n    const product = await streamableReviewsData;\n\n    return pageInfoTransformer(product?.reviews.pageInfo ?? defaultPageInfo, {\n      startCursorParamName: PaginationSearchParamNames.BEFORE,\n      endCursorParamName: PaginationSearchParamNames.AFTER,\n    });\n  });\n\n  const streamableProductName = Streamable.from(async () => {\n    const product = await streamableProduct;\n\n    return { name: product?.name ?? '' };\n  });\n\n  const streamableUser = Streamable.from(async () => {\n    const session = await auth();\n    const firstName = session?.user?.firstName ?? '';\n    const lastName = session?.user?.lastName ?? '';\n\n    if (!firstName || !lastName) {\n      return { email: session?.user?.email ?? '', name: '' };\n    }\n\n    const lastInitial = lastName.charAt(0).toUpperCase();\n    const obfuscatedName = `${firstName} ${lastInitial}.`;\n\n    return { email: session?.user?.email ?? '', name: obfuscatedName };\n  });\n\n  const streamableTotalCount = Streamable.from(async () => {\n    const product = await streamableReviewsData;\n\n    return product?.reviewSummary.numberOfReviews ?? 0;\n  });\n\n  return (\n    <>\n      <ReviewsSection\n        action={submitReview}\n        averageRating={streamableAvergeRating}\n        emptyStateMessage={t('empty')}\n        formButtonLabel={t('Form.button')}\n        formEmailLabel={t('Form.emailLabel')}\n        formModalTitle={t('Form.title')}\n        formNameLabel={t('Form.nameLabel')}\n        formRatingLabel={t('Form.ratingLabel')}\n        formReviewLabel={t('Form.reviewLabel')}\n        formSubmitLabel={t('Form.submit')}\n        formTitleLabel={t('Form.titleLabel')}\n        nextLabel={t('next')}\n        paginationInfo={streamablePaginationInfo}\n        previousLabel={t('previous')}\n        productId={productId}\n        recaptchaSiteKey={recaptchaSiteKey}\n        reviews={streamableReviews}\n        reviewsLabel={t('title')}\n        streamableImages={streamableImages}\n        streamableProduct={streamableProductName}\n        streamableUser={streamableUser}\n        totalCount={streamableTotalCount}\n      />\n      <Stream fallback={null} value={streamableReviewsData}>\n        {(product) =>\n          product &&\n          removeEdgesAndNodes(product.reviews).length > 0 && (\n            <ProductReviewSchema\n              productId={productId}\n              reviews={removeEdgesAndNodes(product.reviews)}\n            />\n          )\n        }\n      </Stream>\n    </>\n  );\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_components/search-params-router-refresh.tsx",
    "content": "'use client';\n\nimport { useSearchParams } from 'next/navigation';\nimport { SearchParams } from 'nuqs';\nimport { useEffect } from 'react';\n\nimport { useRouter } from '~/i18n/routing';\n\n// Not-so-great workaround for https://github.com/vercel/next.js/issues/59407\nexport const SearchParamsRouterRefresh = ({\n  searchParamsServer,\n}: {\n  searchParamsServer: SearchParams;\n}) => {\n  const router = useRouter();\n  const searchParamsClient = useSearchParams();\n  const paramsAreEqual = Array.from(searchParamsClient.entries()).every(\n    ([key, value]) => searchParamsServer[key] === value,\n  );\n\n  useEffect(() => {\n    if (!paramsAreEqual) {\n      router.refresh();\n    }\n  }, [router, paramsAreEqual]);\n\n  return null;\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_components/wishlist-button/add-to-new-wishlist-modal.tsx",
    "content": "'use client';\n\nimport { useSearchParams } from 'next/navigation';\n\nimport { Modal } from '~/components/modal';\nimport { NewWishlistModal } from '~/components/wishlist/modals/new';\nimport { usePathname, useRouter } from '~/i18n/routing';\n\nimport { addToNewWishlist } from '../../_actions/wishlist-action';\n\ninterface Props {\n  title: string;\n  cancelLabel: string;\n  createLabel: string;\n  nameLabel: string;\n  requiredError: string;\n  modalVisible: boolean;\n  productId: number;\n  selectedSku: string;\n}\n\nexport const AddToNewWishlistModal = ({\n  title,\n  cancelLabel,\n  createLabel,\n  nameLabel,\n  requiredError,\n  modalVisible,\n  productId,\n  selectedSku,\n}: Props) => {\n  const pathname = usePathname();\n  const searchParams = useSearchParams();\n  const router = useRouter();\n  const closeModal = () => {\n    const params = new URLSearchParams(searchParams.toString());\n\n    params.delete('action');\n\n    router.push(params.size === 0 ? pathname : `${pathname}?${params.toString()}`);\n  };\n\n  return (\n    <Modal\n      buttons={[\n        {\n          label: cancelLabel,\n          type: 'cancel',\n        },\n        {\n          label: createLabel,\n          type: 'submit',\n        },\n      ]}\n      className=\"min-w-64 @lg:min-w-96\"\n      form={{ action: addToNewWishlist, onSuccess: closeModal }}\n      isOpen={modalVisible}\n      setOpen={(open) => {\n        if (!open) {\n          closeModal();\n        }\n      }}\n      title={title}\n    >\n      <input name=\"productId\" type=\"hidden\" value={productId} />\n      <input name=\"selectedSku\" type=\"hidden\" value={selectedSku} />\n      <input\n        name=\"redirectTo\"\n        type=\"hidden\"\n        value={searchParams.size === 0 ? pathname : `${pathname}?${searchParams.toString()}`}\n      />\n      <NewWishlistModal nameLabel={nameLabel} requiredError={requiredError} />\n    </Modal>\n  );\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_components/wishlist-button/dropdown.tsx",
    "content": "'use client';\n\nimport { clsx } from 'clsx';\nimport { CheckIcon, PlusIcon, XIcon } from 'lucide-react';\nimport { useSearchParams } from 'next/navigation';\n\nimport { DropdownMenu, DropdownMenuItem } from '@/vibes/soul/primitives/dropdown-menu';\nimport { usePathname, useRouter } from '~/i18n/routing';\n\nimport { WishlistButtonWishlistInfo } from '.';\n\ninterface Props extends React.PropsWithChildren {\n  formId: string;\n  newWishlistLabel: string;\n  wishlists: WishlistButtonWishlistInfo[];\n  isLoggedIn: boolean;\n}\n\nexport const WishlistButtonDropdown = ({\n  formId,\n  newWishlistLabel,\n  wishlists,\n  isLoggedIn,\n  children,\n}: Props) => {\n  const pathname = usePathname();\n  const searchParams = useSearchParams();\n  const router = useRouter();\n  const addToNewWishlistAction = () => {\n    const params = new URLSearchParams(searchParams.toString());\n\n    params.set('action', 'addToNewWishlist');\n\n    const path = params.size === 0 ? pathname : `${pathname}?${params.toString()}`;\n\n    if (isLoggedIn) {\n      router.push(path);\n    } else {\n      const loginParams = new URLSearchParams({ redirectTo: path });\n\n      router.push(`/login?${loginParams.toString()}`);\n    }\n  };\n\n  const items: DropdownMenuItem[] = wishlists.map(\n    ({ entityId: wishlistId, name, wishlistItemId }) => ({\n      label: (\n        <button\n          className=\"group block w-full text-left\"\n          form={formId}\n          name=\"menuItem\"\n          type=\"submit\"\n          value={JSON.stringify({\n            wishlistId,\n            wishlistItemId,\n            action: wishlistItemId ? 'remove' : 'add',\n            redirectTo:\n              searchParams.size === 0 ? pathname : `${pathname}?${searchParams.toString()}`,\n          })}\n        >\n          <div className=\"flex items-center gap-3\">\n            <div\n              className={clsx(\n                'flex-1 overflow-hidden text-ellipsis',\n                wishlistItemId ? 'font-bold' : '',\n              )}\n            >\n              {name}\n            </div>\n            {wishlistItemId !== undefined && (\n              <div>\n                <CheckIcon className=\"group-hover:hidden\" size={16} />\n                <XIcon className=\"hidden group-hover:block\" size={16} />\n              </div>\n            )}\n          </div>\n        </button>\n      ),\n    }),\n  );\n\n  return (\n    <DropdownMenu\n      align=\"start\"\n      className=\"text-nowrap\"\n      items={[\n        {\n          label: (\n            <div className=\"flex items-center gap-2\">\n              <PlusIcon size={20} />\n              <span>{newWishlistLabel}</span>\n            </div>\n          ),\n          action: addToNewWishlistAction,\n        },\n        'separator',\n        ...items,\n      ]}\n    >\n      <div>{children}</div>\n    </DropdownMenu>\n  );\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_components/wishlist-button/form.tsx",
    "content": "import { getTranslations } from 'next-intl/server';\nimport { SearchParams } from 'nuqs';\n\nimport { Streamable } from '@/vibes/soul/lib/streamable';\n\nimport { wishlistAction } from '../../_actions/wishlist-action';\nimport { SearchParamsRouterRefresh } from '../search-params-router-refresh';\n\nimport { AddToNewWishlistModal } from './add-to-new-wishlist-modal';\n\ninterface Props {\n  formId: string;\n  productId: number;\n  productSku: Streamable<string>;\n  searchParams: Promise<SearchParams>;\n}\n\n// Since the wishlist button lives inside of the ProductDetailsForm, we need a separate/detached form\n// to handle the actions through. The form ID is provided to the 'form' attribute of the wishlist menu items.\n// The Modal also contains a form, which needs to be detached.\nexport const WishlistButtonForm = async ({\n  formId,\n  productId,\n  productSku,\n  searchParams,\n}: Props) => {\n  const t = await getTranslations('Wishlist');\n  const modalVisible = (await searchParams).action === 'addToNewWishlist';\n  const sku = await productSku;\n\n  return (\n    <>\n      <SearchParamsRouterRefresh searchParamsServer={await searchParams} />\n      <form action={wishlistAction} id={formId}>\n        <input name=\"productId\" type=\"hidden\" value={productId} />\n        <input name=\"selectedSku\" type=\"hidden\" value={sku} />\n      </form>\n      <AddToNewWishlistModal\n        cancelLabel={t('Modal.cancel')}\n        createLabel={t('Modal.create')}\n        modalVisible={modalVisible}\n        nameLabel={t('Form.nameLabel')}\n        productId={productId}\n        requiredError={t('Errors.nameRequired')}\n        selectedSku={sku}\n        title={t('Modal.newTitle')}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/_components/wishlist-button/index.tsx",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { getTranslations } from 'next-intl/server';\nimport { cache } from 'react';\n\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { Favorite } from '@/vibes/soul/primitives/favorite';\nimport { getSessionCustomerAccessToken, isLoggedIn } from '~/auth';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\n\nimport { WishlistButtonDropdown } from './dropdown';\n\nconst wishlistButtonLimit = 50;\nconst WishlistButtonQuery = graphql(`\n  query WishlistButtonQuery($first: Int, $productId: Int!) {\n    customer {\n      wishlistsContainingProduct: wishlists(\n        first: $first\n        filters: { productEntityIds: [$productId] }\n      ) {\n        edges {\n          node {\n            entityId\n            name\n            items(first: 50, filters: { productEntityIds: [$productId] }) {\n              edges {\n                node {\n                  entityId\n                  productEntityId\n                  variantEntityId\n                  product {\n                    sku\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n      wishlists(first: $first) {\n        edges {\n          node {\n            entityId\n            name\n          }\n        }\n      }\n    }\n  }\n`);\n\nconst getWishlistButtonData = cache(async (productId: number, customerAccessToken?: string) => {\n  if (!customerAccessToken) {\n    return null;\n  }\n\n  const { data } = await client.fetch({\n    document: WishlistButtonQuery,\n    variables: { productId, first: wishlistButtonLimit },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store', next: { tags: [TAGS.customer] } },\n  });\n\n  return data.customer;\n});\n\nexport interface WishlistButtonWishlistInfo {\n  entityId: number;\n  name: string;\n  wishlistItemId?: number;\n}\n\ninterface WishlistButton {\n  isProductInWishlist: boolean;\n  wishlists: WishlistButtonWishlistInfo[];\n}\n\nasync function getWishlistButton(\n  productId: number,\n  productSku?: Streamable<string>,\n): Promise<WishlistButton> {\n  const t = await getTranslations('Wishlist.Button');\n  const data = await getWishlistButtonData(productId, await getSessionCustomerAccessToken());\n\n  if (!data?.wishlists.edges?.length) {\n    const defaultWishlist: WishlistButtonWishlistInfo = {\n      entityId: 0,\n      name: t('defaultWishlistName'),\n    };\n\n    return {\n      isProductInWishlist: false,\n      wishlists: [defaultWishlist],\n    };\n  }\n\n  const sku = await productSku;\n  const wishlistsWithSku = removeEdgesAndNodes(data.wishlistsContainingProduct)\n    .map((wishlist) => ({\n      ...wishlist,\n      items: removeEdgesAndNodes(wishlist.items),\n    }))\n    .filter((wishlist) => wishlist.items.some(({ product }) => product?.sku === sku));\n\n  const allWishlists = removeEdgesAndNodes(data.wishlists);\n  const wishlists: WishlistButtonWishlistInfo[] = allWishlists\n    .map(({ entityId, name }) => ({\n      entityId,\n      name,\n      wishlistItemId: wishlistsWithSku\n        .find((wishlist) => wishlist.entityId === entityId)\n        ?.items.find(({ product }) => product?.sku === sku)?.entityId,\n    }))\n    .sort((a, b) => {\n      const aHasProduct = a.wishlistItemId !== undefined;\n      const bHasProduct = b.wishlistItemId !== undefined;\n\n      if (aHasProduct === bHasProduct) {\n        return b.entityId - a.entityId;\n      }\n\n      return aHasProduct ? -1 : 1;\n    });\n\n  return {\n    isProductInWishlist: wishlistsWithSku.length > 0,\n    wishlists,\n  };\n}\n\ninterface Props {\n  productId: number;\n  formId: string;\n  productSku?: Streamable<string>;\n}\n\nexport const WishlistButton = async ({ productId, productSku, formId }: Props) => {\n  const t = await getTranslations('Wishlist.Button');\n  const { isProductInWishlist, wishlists } = await getWishlistButton(productId, productSku);\n\n  return (\n    <WishlistButtonDropdown\n      formId={formId}\n      isLoggedIn={await isLoggedIn()}\n      newWishlistLabel={t('addToNewWishlist')}\n      wishlists={wishlists}\n    >\n      <Favorite checked={isProductInWishlist} label={t('label')} />\n    </WishlistButtonDropdown>\n  );\n};\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { client } from '~/client';\nimport { PricingFragment } from '~/client/fragments/pricing';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { FeaturedProductsCarouselFragment } from '~/components/featured-products-carousel/fragment';\nimport { ProductVariantsInventoryFragment } from '~/components/product-variants-inventory/fragment';\n\nimport { ProductSchemaFragment } from './_components/product-schema/fragment';\nimport { ProductViewedFragment } from './_components/product-viewed/fragment';\n\nconst MultipleChoiceFieldFragment = graphql(`\n  fragment MultipleChoiceFieldFragment on MultipleChoiceOption {\n    entityId\n    displayName\n    displayStyle\n    isRequired\n    values(first: 50) {\n      edges {\n        node {\n          entityId\n          label\n          isDefault\n          isSelected\n          ... on SwatchOptionValue {\n            __typename\n            hexColors\n            imageUrl(lossy: true, width: 40)\n          }\n          ... on ProductPickListOptionValue {\n            __typename\n            defaultImage {\n              altText\n              url: urlTemplate(lossy: true)\n            }\n          }\n        }\n      }\n    }\n  }\n`);\n\nconst CheckboxFieldFragment = graphql(`\n  fragment CheckboxFieldFragment on CheckboxOption {\n    entityId\n    isRequired\n    displayName\n    checkedByDefault\n    label\n    checkedOptionValueEntityId\n    uncheckedOptionValueEntityId\n  }\n`);\n\nconst NumberFieldFragment = graphql(`\n  fragment NumberFieldFragment on NumberFieldOption {\n    entityId\n    displayName\n    isRequired\n    defaultNumber: defaultValue\n    highest\n    isIntegerOnly\n    limitNumberBy\n    lowest\n  }\n`);\n\nconst TextFieldFragment = graphql(`\n  fragment TextFieldFragment on TextFieldOption {\n    entityId\n    displayName\n    isRequired\n    defaultText: defaultValue\n    maxLength\n    minLength\n  }\n`);\n\nconst MultiLineTextFieldFragment = graphql(`\n  fragment MultiLineTextFieldFragment on MultiLineTextFieldOption {\n    entityId\n    displayName\n    isRequired\n    defaultText: defaultValue\n    maxLength\n    minLength\n    maxLines\n  }\n`);\n\nconst DateFieldFragment = graphql(`\n  fragment DateFieldFragment on DateFieldOption {\n    entityId\n    displayName\n    isRequired\n    defaultDate: defaultValue\n    earliest\n    latest\n    limitDateBy\n  }\n`);\n\nexport const ProductOptionsFragment = graphql(\n  `\n    fragment ProductOptionsFragment on Product {\n      entityId\n      productOptions(first: 50) {\n        edges {\n          node {\n            __typename\n            entityId\n            displayName\n            isRequired\n            ...MultipleChoiceFieldFragment\n            ...CheckboxFieldFragment\n            ...NumberFieldFragment\n            ...TextFieldFragment\n            ...MultiLineTextFieldFragment\n            ...DateFieldFragment\n          }\n        }\n      }\n    }\n  `,\n  [\n    MultipleChoiceFieldFragment,\n    CheckboxFieldFragment,\n    NumberFieldFragment,\n    TextFieldFragment,\n    MultiLineTextFieldFragment,\n    DateFieldFragment,\n  ],\n);\n\nconst ProductPageMetadataQuery = graphql(`\n  query ProductPageMetadataQuery($entityId: Int!) {\n    site {\n      product(entityId: $entityId) {\n        name\n        path\n        defaultImage {\n          altText\n          url: urlTemplate(lossy: true)\n        }\n        seo {\n          pageTitle\n          metaDescription\n          metaKeywords\n        }\n        plainTextDescription(characterLimit: 1200)\n      }\n    }\n  }\n`);\n\nexport const getProductPageMetadata = cache(\n  async (entityId: number, customerAccessToken?: string) => {\n    const { data } = await client.fetch({\n      document: ProductPageMetadataQuery,\n      variables: { entityId },\n      customerAccessToken,\n      fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },\n    });\n\n    return data.site.product;\n  },\n);\n\nconst ProductQuery = graphql(\n  `\n    query ProductQuery($entityId: Int!) {\n      site {\n        settings {\n          reviews {\n            enabled\n          }\n          display {\n            showProductRating\n          }\n        }\n        product(entityId: $entityId) {\n          entityId\n          name\n          description\n          path\n          brand {\n            name\n          }\n          reviewSummary {\n            averageRating\n            numberOfReviews\n          }\n          description\n          ...ProductOptionsFragment\n        }\n      }\n    }\n  `,\n  [ProductOptionsFragment],\n);\n\nexport const getProduct = cache(async (entityId: number, customerAccessToken?: string) => {\n  const { data } = await client.fetch({\n    document: ProductQuery,\n    variables: { entityId },\n    customerAccessToken,\n    fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },\n  });\n\n  return data.site;\n});\n\nconst StreamableProductVariantInventoryBySkuQuery = graphql(`\n  query ProductVariantBySkuQuery($productId: Int!, $sku: String!) {\n    site {\n      product(entityId: $productId) {\n        variants(skus: [$sku]) {\n          edges {\n            node {\n              id\n              entityId\n              sku\n              inventory {\n                aggregated {\n                  availableToSell\n                  warningLevel\n                  availableOnHand\n                  availableForBackorder\n                  unlimitedBackorder\n                }\n                byLocation {\n                  edges {\n                    node {\n                      locationEntityId\n                      backorderMessage\n                    }\n                  }\n                }\n                isInStock\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n`);\n\ntype VariantInventoryVariables = VariablesOf<typeof StreamableProductVariantInventoryBySkuQuery>;\n\nexport const getStreamableProductVariantInventory = cache(\n  async (variables: VariantInventoryVariables, customerAccessToken?: string) => {\n    const { data } = await client.fetch({\n      document: StreamableProductVariantInventoryBySkuQuery,\n      variables,\n      customerAccessToken,\n      fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate: 60 } },\n    });\n\n    return data.site.product?.variants;\n  },\n);\n\nconst StreamableProductQuery = graphql(\n  `\n    query StreamableProductQuery(\n      $entityId: Int!\n      $optionValueIds: [OptionValueId!]\n      $useDefaultOptionSelections: Boolean\n    ) {\n      site {\n        product(\n          entityId: $entityId\n          optionValueIds: $optionValueIds\n          useDefaultOptionSelections: $useDefaultOptionSelections\n        ) {\n          entityId\n          images(first: 12) {\n            pageInfo {\n              hasNextPage\n              endCursor\n            }\n            edges {\n              node {\n                altText\n                url: urlTemplate(lossy: true)\n                isDefault\n              }\n            }\n          }\n          defaultImage {\n            altText\n            url: urlTemplate(lossy: true)\n          }\n          sku\n          weight {\n            value\n            unit\n          }\n          condition\n          customFields {\n            edges {\n              node {\n                entityId\n                name\n                value\n              }\n            }\n          }\n          minPurchaseQuantity\n          maxPurchaseQuantity\n          warranty\n          ...ProductViewedFragment\n          ...ProductSchemaFragment\n        }\n      }\n    }\n  `,\n  [ProductViewedFragment, ProductSchemaFragment],\n);\n\ntype Variables = VariablesOf<typeof StreamableProductQuery>;\n\nexport const getStreamableProduct = cache(\n  async (variables: Variables, customerAccessToken?: string) => {\n    const { data } = await client.fetch({\n      document: StreamableProductQuery,\n      variables,\n      customerAccessToken,\n      fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },\n    });\n\n    return data.site.product;\n  },\n);\n\nconst StreamableProductInventoryQuery = graphql(\n  `\n    query StreamableProductInventoryQuery($entityId: Int!) {\n      site {\n        product(entityId: $entityId) {\n          sku\n          inventory {\n            hasVariantInventory\n            isInStock\n            aggregated {\n              availableToSell\n              warningLevel\n              availableOnHand\n              availableForBackorder\n              unlimitedBackorder\n            }\n          }\n          availabilityV2 {\n            status\n          }\n          ...ProductVariantsInventoryFragment\n        }\n      }\n    }\n  `,\n  [ProductVariantsInventoryFragment],\n);\n\ntype ProductInventoryVariables = VariablesOf<typeof StreamableProductQuery>;\n\nexport const getStreamableProductInventory = cache(\n  async (variables: ProductInventoryVariables, customerAccessToken?: string) => {\n    const { data } = await client.fetch({\n      document: StreamableProductInventoryQuery,\n      variables,\n      customerAccessToken,\n      fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate: 60 } },\n    });\n\n    return data.site.product;\n  },\n);\n\n// Fields that require currencyCode as a query variable\n// Separated from the rest to cache separately\nconst ProductPricingAndRelatedProductsQuery = graphql(\n  `\n    query ProductPricingAndRelatedProductsQuery(\n      $entityId: Int!\n      $optionValueIds: [OptionValueId!]\n      $useDefaultOptionSelections: Boolean\n      $currencyCode: currencyCode\n    ) {\n      site {\n        product(\n          entityId: $entityId\n          optionValueIds: $optionValueIds\n          useDefaultOptionSelections: $useDefaultOptionSelections\n        ) {\n          ...PricingFragment\n          relatedProducts(first: 8) {\n            edges {\n              node {\n                ...FeaturedProductsCarouselFragment\n              }\n            }\n          }\n        }\n      }\n    }\n  `,\n  [PricingFragment, FeaturedProductsCarouselFragment],\n);\n\nexport const getProductPricingAndRelatedProducts = cache(\n  async (variables: Variables, customerAccessToken?: string) => {\n    const { data } = await client.fetch({\n      document: ProductPricingAndRelatedProductsQuery,\n      variables,\n      customerAccessToken,\n      fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },\n    });\n\n    return data.site.product;\n  },\n);\n\nconst InventorySettingsQuery = graphql(`\n  query InventorySettingsQuery {\n    site {\n      settings {\n        inventory {\n          defaultOutOfStockMessage\n          showOutOfStockMessage\n          stockLevelDisplay\n          showBackorderAvailabilityPrompt\n          backorderAvailabilityPrompt\n          showQuantityOnBackorder\n          showBackorderMessage\n        }\n      }\n    }\n  }\n`);\n\nexport const getStreamableInventorySettingsQuery = cache(async (customerAccessToken?: string) => {\n  const { data } = await client.fetch({\n    document: InventorySettingsQuery,\n    customerAccessToken,\n    fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },\n  });\n\n  return data.site.settings?.inventory;\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/product/[slug]/page.tsx",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\nimport { SearchParams } from 'nuqs/server';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { FeaturedProductCarousel } from '@/vibes/soul/sections/featured-product-carousel';\nimport { ProductDetail } from '@/vibes/soul/sections/product-detail';\nimport { auth, getSessionCustomerAccessToken } from '~/auth';\nimport { pricesTransformer } from '~/data-transformers/prices-transformer';\nimport { productCardTransformer } from '~/data-transformers/product-card-transformer';\nimport { productOptionsTransformer } from '~/data-transformers/product-options-transformer';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\nimport { getRecaptchaSiteKey } from '~/lib/recaptcha';\nimport { getMetadataAlternates } from '~/lib/seo/canonical';\n\nimport { addToCart } from './_actions/add-to-cart';\nimport { getMoreProductImages } from './_actions/get-more-images';\nimport { submitReview } from './_actions/submit-review';\nimport { ProductAnalyticsProvider } from './_components/product-analytics-provider';\nimport { ProductSchema } from './_components/product-schema';\nimport { ProductViewed } from './_components/product-viewed';\nimport { Reviews } from './_components/reviews';\nimport { WishlistButton } from './_components/wishlist-button';\nimport { WishlistButtonForm } from './_components/wishlist-button/form';\nimport {\n  getProduct,\n  getProductPageMetadata,\n  getProductPricingAndRelatedProducts,\n  getStreamableInventorySettingsQuery,\n  getStreamableProduct,\n  getStreamableProductInventory,\n  getStreamableProductVariantInventory,\n} from './page-data';\n\ninterface Props {\n  params: Promise<{ slug: string; locale: string }>;\n  searchParams: Promise<SearchParams>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { slug, locale } = await params;\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const productId = Number(slug);\n\n  const product = await getProductPageMetadata(productId, customerAccessToken);\n\n  if (!product) {\n    return notFound();\n  }\n\n  const { pageTitle, metaDescription, metaKeywords } = product.seo;\n  const { url, altText: alt } = product.defaultImage || {};\n\n  return {\n    title: pageTitle || product.name,\n    description:\n      metaDescription ||\n      `${product.plainTextDescription.replaceAll(/\\s+/g, ' ').trim().slice(0, 150)}...`,\n    ...(metaKeywords && { keywords: metaKeywords.split(',') }),\n    alternates: await getMetadataAlternates({ path: product.path, locale }),\n    ...(url && { openGraph: { images: [{ url, alt }] } }),\n  };\n}\n\nexport default async function Product({ params, searchParams }: Props) {\n  const { locale, slug } = await params;\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  const detachedWishlistFormId = 'product-add-to-wishlist-form';\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Product');\n  const format = await getFormatter();\n\n  const productId = Number(slug);\n\n  const [{ product: baseProduct, settings }, recaptchaSiteKey] = await Promise.all([\n    getProduct(productId, customerAccessToken),\n    getRecaptchaSiteKey(),\n  ]);\n\n  const reviewsEnabled = Boolean(settings?.reviews.enabled && !settings.display.showProductRating);\n  const showRating = Boolean(settings?.reviews.enabled && settings.display.showProductRating);\n\n  if (!baseProduct) {\n    return notFound();\n  }\n\n  const streamableProduct = Streamable.from(async () => {\n    const options = await searchParams;\n\n    const optionValueIds = Object.keys(options)\n      .map((option) => ({\n        optionEntityId: Number(option),\n        valueEntityId: Number(options[option]),\n      }))\n      .filter(\n        (option) => !Number.isNaN(option.optionEntityId) && !Number.isNaN(option.valueEntityId),\n      );\n\n    const variables = {\n      entityId: Number(productId),\n      optionValueIds,\n      useDefaultOptionSelections: true,\n    };\n\n    const product = await getStreamableProduct(variables, customerAccessToken);\n\n    if (!product) {\n      return notFound();\n    }\n\n    return product;\n  });\n\n  const streamableProductSku = Streamable.from(async () => (await streamableProduct).sku);\n\n  const streamableProductInventory = Streamable.from(async () => {\n    const variables = {\n      entityId: Number(productId),\n    };\n\n    const product = await getStreamableProductInventory(variables, customerAccessToken);\n\n    if (!product) {\n      return notFound();\n    }\n\n    return product;\n  });\n\n  const streamableProductVariantInventory = Streamable.from(async () => {\n    const product = await streamableProductInventory;\n\n    if (!product.inventory.hasVariantInventory) {\n      return undefined;\n    }\n\n    const variables = {\n      productId,\n      sku: product.sku,\n    };\n\n    const variants = await getStreamableProductVariantInventory(variables, customerAccessToken);\n\n    if (!variants) {\n      return undefined;\n    }\n\n    return removeEdgesAndNodes(variants).find((v) => v.sku === product.sku);\n  });\n\n  const streamableProductPricingAndRelatedProducts = Streamable.from(async () => {\n    const options = await searchParams;\n\n    const optionValueIds = Object.keys(options)\n      .map((option) => ({\n        optionEntityId: Number(option),\n        valueEntityId: Number(options[option]),\n      }))\n      .filter(\n        (option) => !Number.isNaN(option.optionEntityId) && !Number.isNaN(option.valueEntityId),\n      );\n\n    const currencyCode = await getPreferredCurrencyCode();\n\n    const variables = {\n      entityId: Number(productId),\n      optionValueIds,\n      useDefaultOptionSelections: true,\n      currencyCode,\n    };\n\n    return await getProductPricingAndRelatedProducts(variables, customerAccessToken);\n  });\n\n  const streamablePrices = Streamable.from(async () => {\n    const product = await streamableProductPricingAndRelatedProducts;\n\n    if (!product) {\n      return null;\n    }\n\n    return pricesTransformer(product.prices, format) ?? null;\n  });\n\n  const streamableImages = Streamable.from(async () => {\n    const product = await streamableProduct;\n\n    const images = removeEdgesAndNodes(product.images)\n      .filter((image) => image.url !== product.defaultImage?.url)\n      .map((image) => ({\n        src: image.url,\n        alt: image.altText,\n      }));\n\n    return {\n      images: product.defaultImage\n        ? [{ src: product.defaultImage.url, alt: product.defaultImage.altText }, ...images]\n        : images,\n      pageInfo: product.images.pageInfo,\n    };\n  });\n\n  const streameableCtaLabel = Streamable.from(async () => {\n    const product = await streamableProductInventory;\n\n    if (product.availabilityV2.status === 'Unavailable') {\n      return t('ProductDetails.Submit.unavailable');\n    }\n\n    if (product.availabilityV2.status === 'Preorder') {\n      return t('ProductDetails.Submit.preorder');\n    }\n\n    if (!product.inventory.isInStock) {\n      return t('ProductDetails.Submit.outOfStock');\n    }\n\n    return t('ProductDetails.Submit.addToCart');\n  });\n\n  const streameableCtaDisabled = Streamable.from(async () => {\n    const product = await streamableProductInventory;\n\n    if (product.availabilityV2.status === 'Unavailable') {\n      return true;\n    }\n\n    if (product.availabilityV2.status === 'Preorder') {\n      return false;\n    }\n\n    if (!product.inventory.isInStock) {\n      return true;\n    }\n\n    return false;\n  });\n\n  const streamableInventorySettings = Streamable.from(async () => {\n    return await getStreamableInventorySettingsQuery(customerAccessToken);\n  });\n\n  const getBackorderAvailabilityPrompt = ({\n    showBackorderAvailabilityPrompt,\n    backorderAvailabilityPrompt,\n    availableForBackorder,\n    unlimitedBackorder,\n  }: {\n    showBackorderAvailabilityPrompt: boolean;\n    backorderAvailabilityPrompt: string | null;\n    availableForBackorder?: number | null;\n    unlimitedBackorder?: boolean;\n  }) => {\n    if (!showBackorderAvailabilityPrompt || !backorderAvailabilityPrompt) {\n      return null;\n    }\n\n    const hasBackorderAvailablity = !!availableForBackorder || unlimitedBackorder;\n\n    if (!hasBackorderAvailablity) {\n      return null;\n    }\n\n    return backorderAvailabilityPrompt;\n  };\n\n  const streamableStockDisplayData = Streamable.from(async () => {\n    const [product, variant, inventorySetting] = await Streamable.all([\n      streamableProductInventory,\n      streamableProductVariantInventory,\n      streamableInventorySettings,\n    ]);\n\n    if (!inventorySetting) {\n      return null;\n    }\n\n    let inventory;\n\n    if (product.inventory.hasVariantInventory) {\n      inventory = variant?.inventory;\n    } else {\n      inventory = product.inventory;\n    }\n\n    if (!inventory) {\n      return null;\n    }\n\n    const {\n      showOutOfStockMessage,\n      stockLevelDisplay,\n      defaultOutOfStockMessage,\n      showBackorderAvailabilityPrompt,\n      showBackorderMessage,\n      showQuantityOnBackorder,\n      backorderAvailabilityPrompt,\n    } = inventorySetting;\n\n    if (!inventory.isInStock) {\n      return showOutOfStockMessage\n        ? { stockLevelMessage: defaultOutOfStockMessage, backorderAvailabilityPrompt: null }\n        : null;\n    }\n\n    const {\n      availableToSell,\n      warningLevel,\n      availableOnHand,\n      availableForBackorder,\n      unlimitedBackorder,\n    } = inventory.aggregated ?? {};\n\n    if (stockLevelDisplay === 'DONT_SHOW') {\n      return null;\n    }\n\n    const showsBackorderInfo =\n      showBackorderAvailabilityPrompt || showBackorderMessage || showQuantityOnBackorder;\n\n    // if no backorder info is to be displayed, then availableToSell is the stock quantity to be used\n    const stockQuantity = showsBackorderInfo ? availableOnHand : availableToSell;\n\n    if (!showsBackorderInfo && !stockQuantity) {\n      return null;\n    }\n\n    if (stockLevelDisplay === 'SHOW_WHEN_LOW') {\n      if (!warningLevel) {\n        return null;\n      }\n\n      if (stockQuantity && stockQuantity > warningLevel) {\n        return null;\n      }\n    }\n\n    const availabilityMessage = getBackorderAvailabilityPrompt({\n      showBackorderAvailabilityPrompt,\n      backorderAvailabilityPrompt,\n      availableForBackorder,\n      unlimitedBackorder,\n    });\n\n    if (!availabilityMessage && stockQuantity === undefined) {\n      return null;\n    }\n\n    return {\n      stockLevelMessage: t('ProductDetails.currentStock', {\n        quantity: stockQuantity ?? 0,\n      }),\n      backorderAvailabilityPrompt: availabilityMessage,\n    };\n  });\n\n  const streamableBackorderDisplayData = Streamable.from(async () => {\n    const [product, variant, inventorySetting] = await Streamable.all([\n      streamableProductInventory,\n      streamableProductVariantInventory,\n      streamableInventorySettings,\n    ]);\n\n    let inventory;\n\n    if (!product.inventory.hasVariantInventory) {\n      inventory = product.inventory;\n    } else {\n      inventory = variant?.inventory;\n    }\n\n    if (!inventory?.aggregated || !inventorySetting) {\n      return {\n        availableOnHand: 0,\n        availableForBackorder: 0,\n        unlimitedBackorder: false,\n        showQuantityOnBackorder: false,\n        backorderMessage: null,\n      };\n    }\n\n    const inventoryData = {\n      availableOnHand: inventory.aggregated.availableOnHand,\n      availableForBackorder: inventory.aggregated.availableForBackorder ?? 0,\n      unlimitedBackorder: inventory.aggregated.unlimitedBackorder,\n    };\n\n    const { showQuantityOnBackorder, showBackorderMessage } = inventorySetting;\n\n    const hasBackorderAvailablity =\n      inventoryData.availableForBackorder > 0 || inventoryData.unlimitedBackorder;\n\n    if (!hasBackorderAvailablity || !showBackorderMessage) {\n      return {\n        ...inventoryData,\n        showQuantityOnBackorder: showQuantityOnBackorder && hasBackorderAvailablity,\n        backorderMessage: null,\n      };\n    }\n\n    let variantLocations;\n\n    if (product.inventory.hasVariantInventory) {\n      variantLocations = variant?.inventory?.byLocation;\n    } else {\n      const variants = removeEdgesAndNodes(product.variants);\n      const baseVariant = variants.find((v) => v.sku === product.sku);\n\n      variantLocations = baseVariant?.inventory?.byLocation;\n    }\n\n    if (!variantLocations) {\n      return {\n        ...inventoryData,\n        showQuantityOnBackorder,\n        backorderMessage: null,\n      };\n    }\n\n    const inventoryByLocation = removeEdgesAndNodes(variantLocations).at(0);\n\n    return {\n      ...inventoryData,\n      showQuantityOnBackorder,\n      backorderMessage: inventoryByLocation?.backorderMessage || null,\n    };\n  });\n\n  const streameableAccordions = Streamable.from(async () => {\n    const product = await streamableProduct;\n\n    const customFields = removeEdgesAndNodes(product.customFields);\n\n    const specifications = [\n      {\n        name: t('ProductDetails.Accordions.sku'),\n        value: product.sku,\n      },\n      {\n        name: t('ProductDetails.Accordions.weight'),\n        value: `${product.weight?.value} ${product.weight?.unit}`,\n      },\n      {\n        name: t('ProductDetails.Accordions.condition'),\n        value: product.condition,\n      },\n      ...customFields.map((field) => ({\n        name: field.name,\n        value: field.value,\n      })),\n    ];\n\n    return [\n      ...(specifications.length\n        ? [\n            {\n              title: t('ProductDetails.Accordions.specifications'),\n              content: (\n                <div className=\"prose @container\">\n                  <dl className=\"flex flex-col gap-4\">\n                    {specifications.map((field, index) => (\n                      <div className=\"grid grid-cols-1 gap-2 @lg:grid-cols-2\" key={index}>\n                        <dt>\n                          <strong>{field.name}</strong>\n                        </dt>\n                        <dd>{field.value}</dd>\n                      </div>\n                    ))}\n                  </dl>\n                </div>\n              ),\n            },\n          ]\n        : []),\n      ...(product.warranty\n        ? [\n            {\n              title: t('ProductDetails.Accordions.warranty'),\n              content: (\n                <div className=\"prose\" dangerouslySetInnerHTML={{ __html: product.warranty }} />\n              ),\n            },\n          ]\n        : []),\n    ];\n  });\n\n  const streameableRelatedProducts = Streamable.from(async () => {\n    const product = await streamableProductPricingAndRelatedProducts;\n\n    if (!product) {\n      return [];\n    }\n\n    const relatedProducts = removeEdgesAndNodes(product.relatedProducts);\n\n    return productCardTransformer(relatedProducts, format);\n  });\n\n  const streamableMinQuantity = Streamable.from(async () => {\n    const product = await streamableProduct;\n\n    return product.minPurchaseQuantity;\n  });\n\n  const streamableMaxQuantity = Streamable.from(async () => {\n    const product = await streamableProduct;\n\n    return product.maxPurchaseQuantity;\n  });\n\n  const streamableAnalyticsData = Streamable.from(async () => {\n    const [extendedProduct, pricingProduct] = await Streamable.all([\n      streamableProduct,\n      streamableProductPricingAndRelatedProducts,\n    ]);\n\n    return {\n      id: extendedProduct.entityId,\n      name: extendedProduct.name,\n      sku: extendedProduct.sku,\n      brand: extendedProduct.brand?.name ?? '',\n      price: pricingProduct?.prices?.price.value ?? 0,\n      currency: pricingProduct?.prices?.price.currencyCode ?? '',\n    };\n  });\n\n  const streamableUser = Streamable.from(async () => {\n    const session = await auth();\n    const firstName = session?.user?.firstName ?? '';\n    const lastName = session?.user?.lastName ?? '';\n\n    if (!firstName || !lastName) {\n      return { email: session?.user?.email ?? '', name: '' };\n    }\n\n    const lastInitial = lastName.charAt(0).toUpperCase();\n    const obfuscatedName = `${firstName} ${lastInitial}.`;\n\n    return { email: session?.user?.email ?? '', name: obfuscatedName };\n  });\n\n  return (\n    <>\n      <ProductAnalyticsProvider data={streamableAnalyticsData}>\n        <ProductDetail\n          action={addToCart}\n          additionalActions={\n            <WishlistButton\n              formId={detachedWishlistFormId}\n              productId={productId}\n              productSku={streamableProductSku}\n            />\n          }\n          additionalInformationTitle={t('ProductDetails.additionalInformation')}\n          ctaDisabled={streameableCtaDisabled}\n          ctaLabel={streameableCtaLabel}\n          decrementLabel={t('ProductDetails.decreaseQuantity')}\n          emptySelectPlaceholder={t('ProductDetails.emptySelectPlaceholder')}\n          fields={productOptionsTransformer(baseProduct.productOptions)}\n          incrementLabel={t('ProductDetails.increaseQuantity')}\n          loadMoreImagesAction={getMoreProductImages}\n          prefetch={true}\n          product={{\n            id: baseProduct.entityId.toString(),\n            title: baseProduct.name,\n            description: <div dangerouslySetInnerHTML={{ __html: baseProduct.description }} />,\n            href: baseProduct.path,\n            images: streamableImages,\n            price: streamablePrices,\n            reviewsEnabled,\n            showRating,\n            numberOfReviews: baseProduct.reviewSummary.numberOfReviews,\n            subtitle: baseProduct.brand?.name,\n            rating: baseProduct.reviewSummary.averageRating,\n            accordions: streameableAccordions,\n            minQuantity: streamableMinQuantity,\n            maxQuantity: streamableMaxQuantity,\n            stockDisplayData: streamableStockDisplayData,\n            backorderDisplayData: streamableBackorderDisplayData,\n          }}\n          quantityLabel={t('ProductDetails.quantity')}\n          recaptchaSiteKey={recaptchaSiteKey}\n          reviewFormAction={submitReview}\n          thumbnailLabel={t('ProductDetails.thumbnail')}\n          user={streamableUser}\n        />\n      </ProductAnalyticsProvider>\n\n      <FeaturedProductCarousel\n        cta={{ label: t('RelatedProducts.cta'), href: '/shop-all' }}\n        emptyStateSubtitle={t('RelatedProducts.browseCatalog')}\n        emptyStateTitle={t('RelatedProducts.noRelatedProducts')}\n        nextLabel={t('RelatedProducts.nextProducts')}\n        previousLabel={t('RelatedProducts.previousProducts')}\n        products={streameableRelatedProducts}\n        scrollbarLabel={t('RelatedProducts.scrollbar')}\n        title={t('RelatedProducts.title')}\n      />\n\n      {showRating && (\n        <div id=\"reviews\">\n          <Reviews\n            productId={productId}\n            recaptchaSiteKey={recaptchaSiteKey}\n            searchParams={searchParams}\n            streamableImages={streamableImages}\n            streamableProduct={streamableProduct}\n          />\n        </div>\n      )}\n\n      <Stream\n        fallback={null}\n        value={Streamable.from(async () =>\n          Streamable.all([streamableProduct, streamableProductPricingAndRelatedProducts]),\n        )}\n      >\n        {([extendedProduct, pricingProduct]) => (\n          <>\n            <ProductSchema\n              product={{ ...extendedProduct, prices: pricingProduct?.prices ?? null }}\n            />\n            <ProductViewed\n              product={{ ...extendedProduct, prices: pricingProduct?.prices ?? null }}\n            />\n          </>\n        )}\n      </Stream>\n\n      <WishlistButtonForm\n        formId={detachedWishlistFormId}\n        productId={productId}\n        productSku={streamableProductSku}\n        searchParams={searchParams}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/webpages/[id]/_components/web-page.tsx",
    "content": "import { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Breadcrumb, Breadcrumbs, BreadcrumbsSkeleton } from '@/vibes/soul/sections/breadcrumbs';\n\nexport interface WebPage {\n  title: string;\n  content: string;\n  breadcrumbs: Breadcrumb[];\n  seo: {\n    pageTitle: string;\n    metaDescription: string;\n    metaKeywords: string;\n  };\n}\n\ninterface Props {\n  webPage: Streamable<WebPage>;\n  breadcrumbs?: Streamable<Breadcrumb[]>;\n  children?: React.ReactNode;\n}\n\nexport function WebPageContent({ webPage: streamableWebPage, breadcrumbs, children }: Props) {\n  return (\n    <section className=\"w-full max-w-4xl\">\n      <Stream fallback={<WebPageContentSkeleton />} value={streamableWebPage}>\n        {(webPage) => {\n          const { title, content } = webPage;\n\n          return (\n            <>\n              <header className=\"pb-8 @2xl:pb-12 @4xl:pb-16\">\n                {breadcrumbs && <Breadcrumbs breadcrumbs={breadcrumbs} />}\n\n                <h1 className=\"mb-4 mt-8 font-heading text-4xl font-medium leading-none @xl:text-5xl @4xl:text-6xl\">\n                  {title}\n                </h1>\n              </header>\n\n              <div\n                className=\"@-xl:[&_h2]:text-4xl prose space-y-4 [&_h2]:font-heading [&_h2]:text-3xl [&_h2]:font-normal [&_h2]:leading-none [&_img]:mx-auto [&_img]:max-h-[600px] [&_img]:w-fit [&_img]:rounded-2xl [&_img]:object-cover\"\n                dangerouslySetInnerHTML={{ __html: content }}\n              />\n              {children}\n            </>\n          );\n        }}\n      </Stream>\n    </section>\n  );\n}\n\nfunction WebPageTitleSkeleton() {\n  return (\n    <div className=\"mb-4 mt-8 animate-pulse\">\n      <div className=\"h-9 w-5/6 rounded-lg bg-contrast-100 @xl:h-12 @4xl:h-[3.75rem]\" />\n    </div>\n  );\n}\n\nfunction WebPageBodySkeleton() {\n  return (\n    <div className=\"mx-auto w-full max-w-4xl animate-pulse pb-8 @2xl:pb-12 @4xl:pb-16\">\n      <div className=\"mb-8 h-[1lh] w-3/5 rounded-lg bg-contrast-100\" />\n      <div className=\"mb-4 h-[0.5lh] w-full rounded-lg bg-contrast-100\" />\n      <div className=\"mb-4 h-[0.5lh] w-full rounded-lg bg-contrast-100\" />\n      <div className=\"mb-4 h-[0.5lh] w-full rounded-lg bg-contrast-100\" />\n      <div className=\"mb-4 h-[0.5lh] w-full rounded-lg bg-contrast-100\" />\n      <div className=\"mb-4 h-[0.5lh] w-full rounded-lg bg-contrast-100\" />\n      <div className=\"mb-4 h-[0.5lh] w-3/4 rounded-lg bg-contrast-100\" />\n    </div>\n  );\n}\n\nfunction WebPageContentSkeleton() {\n  return (\n    <div>\n      <div className=\"mx-auto w-full max-w-4xl pb-8 @2xl:pb-12 @4xl:pb-16\">\n        <BreadcrumbsSkeleton />\n        <WebPageTitleSkeleton />\n      </div>\n      <WebPageBodySkeleton />\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/webpages/[id]/contact/_actions/submit-contact-form.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getLocale, getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { DynamicFormActionArgs } from '@/vibes/soul/form/dynamic-form';\nimport { Field, schema } from '@/vibes/soul/form/dynamic-form/schema';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { redirect } from '~/i18n/routing';\nimport { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha';\n\nconst inputSchema = z.object({\n  data: z.object({\n    companyName: z.string().optional(),\n    fullName: z.string().optional(),\n    phoneNumber: z.string().optional(),\n    orderNumber: z.string().optional(),\n    rmaNumber: z.string().optional(),\n    email: z.string().email(),\n    comments: z.string().trim(),\n  }),\n  pageEntityId: z.number(),\n});\n\nconst SubmitContactUsMutation = graphql(`\n  mutation SubmitContactUsMutation($input: SubmitContactUsInput!, $reCaptchaV2: ReCaptchaV2Input) {\n    submitContactUs(input: $input, reCaptchaV2: $reCaptchaV2) {\n      __typename\n      errors {\n        __typename\n        ... on Error {\n          message\n        }\n      }\n    }\n  }\n`);\n\nfunction parseContactFormInput(\n  value: Record<string, string | number | string[] | undefined>,\n): VariablesOf<typeof SubmitContactUsMutation>['input'] {\n  const mappedInput = {\n    data: {\n      companyName: value.companyname,\n      fullName: value.fullname,\n      phoneNumber: value.phone,\n      orderNumber: value.orderno,\n      rmaNumber: value.rma,\n      email: value.email,\n      comments: value.comments,\n    },\n    pageEntityId: Number(value.pageId),\n  };\n\n  return inputSchema.parse(mappedInput);\n}\n\nexport async function submitContactForm<F extends Field>(\n  { fields }: DynamicFormActionArgs<F>,\n  _prevState: { lastResult: SubmissionResult | null },\n  formData: FormData,\n) {\n  const t = await getTranslations('WebPages.ContactUs.Form');\n  const locale = await getLocale();\n\n  const submission = parseWithZod(formData, { schema: schema(fields) });\n\n  if (submission.status !== 'success') {\n    return {\n      lastResult: submission.reply(),\n    };\n  }\n\n  const { siteKey, token } = await getRecaptchaFromForm(formData);\n  const recaptchaValidation = assertRecaptchaTokenPresent(siteKey, token, t('recaptchaRequired'));\n\n  if (!recaptchaValidation.success) {\n    return {\n      lastResult: submission.reply({ formErrors: recaptchaValidation.formErrors }),\n    };\n  }\n\n  try {\n    const input = parseContactFormInput(submission.value);\n    const response = await client.fetch({\n      document: SubmitContactUsMutation,\n      variables: {\n        input,\n        reCaptchaV2:\n          recaptchaValidation.token != null ? { token: recaptchaValidation.token } : undefined,\n      },\n      fetchOptions: { cache: 'no-store' },\n    });\n\n    const result = response.data.submitContactUs;\n\n    if (result.errors.length > 0) {\n      return {\n        lastResult: submission.reply({ formErrors: result.errors.map((error) => error.message) }),\n      };\n    }\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => message),\n        }),\n      };\n    }\n\n    if (error instanceof Error) {\n      return {\n        lastResult: submission.reply({ formErrors: [error.message] }),\n      };\n    }\n\n    return {\n      lastResult: submission.reply({ formErrors: [t('somethingWentWrong')] }),\n    };\n  }\n\n  return redirect({\n    href: {\n      pathname: String(submission.value.pagePath),\n      query: { success: 'true' },\n    },\n    locale,\n  });\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/webpages/[id]/contact/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { BreadcrumbsWebPageFragment } from '~/components/breadcrumbs/fragment';\n\nconst ContactPageQuery = graphql(\n  `\n    query ContactPageQuery($id: ID!) {\n      node(id: $id) {\n        __typename\n        ... on ContactPage {\n          entityId\n          name\n          ...BreadcrumbsFragment\n          path\n          contactFields\n          htmlBody\n          seo {\n            pageTitle\n            metaKeywords\n            metaDescription\n          }\n        }\n      }\n    }\n  `,\n  [BreadcrumbsWebPageFragment],\n);\n\ntype Variables = VariablesOf<typeof ContactPageQuery>;\n\nexport const getWebpageData = cache(async (variables: Variables) => {\n  const { data } = await client.fetch({\n    document: ContactPageQuery,\n    variables,\n    fetchOptions: { next: { revalidate } },\n  });\n\n  return data;\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/webpages/[id]/contact/page.tsx",
    "content": "import type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { getTranslations, setRequestLocale } from 'next-intl/server';\nimport { cache } from 'react';\n\nimport { DynamicForm } from '@/vibes/soul/form/dynamic-form';\nimport type { Field, FieldGroup } from '@/vibes/soul/form/dynamic-form/schema';\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { ButtonLink } from '@/vibes/soul/primitives/button-link';\nimport { Breadcrumb } from '@/vibes/soul/sections/breadcrumbs';\nimport {\n  breadcrumbsTransformer,\n  truncateBreadcrumbs,\n} from '~/data-transformers/breadcrumbs-transformer';\nimport { getRecaptchaSiteKey } from '~/lib/recaptcha';\nimport { getMetadataAlternates } from '~/lib/seo/canonical';\n\nimport { WebPage, WebPageContent } from '../_components/web-page';\n\nimport { submitContactForm } from './_actions/submit-contact-form';\nimport { getWebpageData } from './page-data';\n\ninterface Props {\n  params: Promise<{ id: string; locale: string }>;\n  searchParams: Promise<{ success?: string }>;\n}\n\ninterface ContactPage extends WebPage {\n  entityId: number;\n  path: string;\n  contactFields: string[];\n}\n\nconst fieldMapping = {\n  fullname: 'fullName',\n  companyname: 'companyName',\n  phone: 'phone',\n  orderno: 'orderNo',\n  rma: 'rma',\n} as const;\n\ntype ContactField = keyof typeof fieldMapping;\n\nconst getWebPage = cache(async (id: string): Promise<ContactPage> => {\n  const data = await getWebpageData({ id: decodeURIComponent(id) });\n  const webpage = data.node?.__typename === 'ContactPage' ? data.node : null;\n\n  if (!webpage) {\n    return notFound();\n  }\n\n  const breadcrumbs = breadcrumbsTransformer(webpage.breadcrumbs);\n\n  return {\n    entityId: webpage.entityId,\n    title: webpage.name,\n    path: webpage.path,\n    breadcrumbs,\n    content: webpage.htmlBody,\n    contactFields: webpage.contactFields,\n    seo: webpage.seo,\n  };\n});\n\nasync function getWebPageBreadcrumbs(id: string): Promise<Breadcrumb[]> {\n  const t = await getTranslations('WebPages.ContactUs');\n\n  const webpage = await getWebPage(id);\n  const [, ...rest] = webpage.breadcrumbs.reverse();\n  const breadcrumbs = [\n    {\n      label: t('home'),\n      href: '/',\n    },\n    ...rest.reverse(),\n    {\n      label: webpage.title,\n      href: '#',\n    },\n  ];\n\n  return truncateBreadcrumbs(breadcrumbs, 5);\n}\n\nasync function getWebPageWithSuccessContent(id: string, message: string) {\n  const webpage = await getWebPage(id);\n\n  return {\n    ...webpage,\n    content: message,\n  };\n}\n\nasync function getContactFields(id: string) {\n  const t = await getTranslations('WebPages.ContactUs.Form');\n  const { entityId, path, contactFields } = await getWebPage(id);\n  const toGroupsOfTwo = (fields: Field[]) =>\n    fields.reduce<Array<FieldGroup<Field>>>((acc, _, i) => {\n      if (i % 2 === 0) {\n        acc.push(fields.slice(i, i + 2));\n      }\n\n      return acc;\n    }, []);\n\n  const pageIdField: Field = {\n    id: 'pageId',\n    name: 'pageId',\n    type: 'hidden',\n    label: 'Page ID',\n    defaultValue: String(entityId),\n  };\n\n  // Used for redirect to self with query params\n  const pagePathField: Field = {\n    id: 'pagePath',\n    name: 'pagePath',\n    type: 'hidden',\n    label: 'Page Path',\n    defaultValue: path,\n  };\n\n  const emailField: Field = {\n    id: 'email',\n    name: 'email',\n    label: `${t('email')} *`,\n    type: 'email',\n    required: true,\n  };\n\n  const commentsField: Field = {\n    id: 'comments',\n    name: 'comments',\n    label: `${t('comments')} *`,\n    type: 'textarea',\n    required: true,\n  };\n\n  const optionalFields = contactFields\n    .filter((field): field is ContactField => Object.hasOwn(fieldMapping, field))\n    .map<Field>((field) => ({\n      id: field,\n      name: field,\n      label: t(fieldMapping[field]),\n      type: 'text',\n      required: false,\n    }));\n\n  return [\n    ...toGroupsOfTwo([emailField, ...optionalFields]),\n    commentsField,\n    pageIdField,\n    pagePathField,\n  ];\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { id, locale } = await params;\n  const webpage = await getWebPage(id);\n  const { pageTitle, metaDescription, metaKeywords } = webpage.seo;\n\n  return {\n    title: pageTitle || webpage.title,\n    ...(metaDescription && { description: metaDescription }),\n    ...(metaKeywords && { keywords: metaKeywords.split(',') }),\n    ...(webpage.path && {\n      alternates: await getMetadataAlternates({ path: webpage.path, locale }),\n    }),\n  };\n}\n\nexport default async function ContactPage({ params, searchParams }: Props) {\n  const { id, locale } = await params;\n  const { success } = await searchParams;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('WebPages.ContactUs.Form');\n\n  if (success === 'true') {\n    return (\n      <WebPageContent\n        breadcrumbs={Streamable.from(() => getWebPageBreadcrumbs(id))}\n        webPage={Streamable.from(() => getWebPageWithSuccessContent(id, t('success')))}\n      >\n        <ButtonLink\n          className=\"mt-8 @2xl:mt-12 @4xl:mt-16\"\n          href=\"/\"\n          size=\"large\"\n          type=\"submit\"\n          variant=\"primary\"\n        >\n          {t('successCta')}\n        </ButtonLink>\n      </WebPageContent>\n    );\n  }\n\n  const recaptchaSiteKey = await getRecaptchaSiteKey();\n\n  return (\n    <WebPageContent\n      breadcrumbs={Streamable.from(() => getWebPageBreadcrumbs(id))}\n      webPage={Streamable.from(() => getWebPage(id))}\n    >\n      <div className=\"mt-8 @2xl:mt-12 @4xl:mt-16\">\n        <DynamicForm\n          action={submitContactForm}\n          fields={await getContactFields(id)}\n          recaptchaSiteKey={recaptchaSiteKey}\n          submitLabel={t('cta')}\n        />\n      </div>\n    </WebPageContent>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/webpages/[id]/layout.tsx",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { setRequestLocale } from 'next-intl/server';\nimport { cache } from 'react';\n\nimport { SidebarMenu } from '@/vibes/soul/sections/sidebar-menu';\nimport { StickySidebarLayout } from '@/vibes/soul/sections/sticky-sidebar-layout';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\n\ninterface Props extends React.PropsWithChildren {\n  params: Promise<{ locale: string; id: string }>;\n}\n\nconst WebPageChildrenQuery = graphql(`\n  query WebPageChildrenQuery($id: ID!) {\n    node(id: $id) {\n      ... on WebPage {\n        children(first: 20) {\n          edges {\n            node {\n              name\n              ... on NormalPage {\n                path\n              }\n              ... on ContactPage {\n                path\n              }\n              ... on RawHtmlPage {\n                path\n              }\n              ... on ExternalLinkPage {\n                link\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n`);\n\ninterface PageLink {\n  label: string;\n  href: string;\n}\n\nconst getWebPageChildren = cache(async (id: string): Promise<PageLink[]> => {\n  const { data } = await client.fetch({\n    document: WebPageChildrenQuery,\n    variables: { id: decodeURIComponent(id) },\n    fetchOptions: { next: { revalidate } },\n  });\n\n  if (!data.node) {\n    return [];\n  }\n\n  if (!('children' in data.node)) {\n    return [];\n  }\n\n  const { children } = data.node;\n\n  return removeEdgesAndNodes(children).reduce((acc: PageLink[], child) => {\n    if ('path' in child) {\n      return [...acc, { label: child.name, href: child.path }];\n    }\n\n    if ('link' in child) {\n      return [...acc, { label: child.name, href: child.link }];\n    }\n\n    return acc;\n  }, []);\n});\n\nexport default async function WebPageLayout({ params, children }: Props) {\n  const { locale, id } = await params;\n\n  setRequestLocale(locale);\n\n  return (\n    <StickySidebarLayout\n      sidebar={<SidebarMenu links={getWebPageChildren(id)} />}\n      sidebarSize=\"small\"\n    >\n      {children}\n    </StickySidebarLayout>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/webpages/[id]/normal/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { BreadcrumbsWebPageFragment } from '~/components/breadcrumbs/fragment';\n\nconst NormalPageQuery = graphql(\n  `\n    query NormalPageQuery($id: ID!) {\n      node(id: $id) {\n        ... on NormalPage {\n          __typename\n          name\n          ...BreadcrumbsFragment\n          htmlBody\n          entityId\n          seo {\n            pageTitle\n            metaDescription\n            metaKeywords\n          }\n        }\n      }\n    }\n  `,\n  [BreadcrumbsWebPageFragment],\n);\n\ntype Variables = VariablesOf<typeof NormalPageQuery>;\n\nexport const getWebpageData = cache(async (variables: Variables) => {\n  const { data } = await client.fetch({\n    document: NormalPageQuery,\n    variables,\n    fetchOptions: { next: { revalidate } },\n  });\n\n  return data;\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/webpages/[id]/normal/page.tsx",
    "content": "import type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { getTranslations, setRequestLocale } from 'next-intl/server';\nimport { cache } from 'react';\n\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { Breadcrumb } from '@/vibes/soul/sections/breadcrumbs';\nimport {\n  breadcrumbsTransformer,\n  truncateBreadcrumbs,\n} from '~/data-transformers/breadcrumbs-transformer';\nimport { getMetadataAlternates } from '~/lib/seo/canonical';\n\nimport { WebPageContent, WebPage as WebPageData } from '../_components/web-page';\n\nimport { getWebpageData } from './page-data';\n\ninterface Props {\n  params: Promise<{ locale: string; id: string }>;\n}\n\nconst getWebPage = cache(async (id: string): Promise<WebPageData> => {\n  const data = await getWebpageData({ id: decodeURIComponent(id) });\n  const webpage = data.node?.__typename === 'NormalPage' ? data.node : null;\n\n  if (!webpage) {\n    return notFound();\n  }\n\n  const breadcrumbs = breadcrumbsTransformer(webpage.breadcrumbs);\n\n  return {\n    title: webpage.name,\n    breadcrumbs,\n    content: webpage.htmlBody,\n    seo: webpage.seo,\n  };\n});\n\nasync function getWebPageBreadcrumbs(id: string): Promise<Breadcrumb[]> {\n  const t = await getTranslations('WebPages.Normal');\n\n  const webpage = await getWebPage(id);\n  const [, ...rest] = webpage.breadcrumbs.reverse();\n  const breadcrumbs = [\n    {\n      label: t('home'),\n      href: '/',\n    },\n    ...rest.reverse(),\n    {\n      label: webpage.title,\n      href: '#',\n    },\n  ];\n\n  return truncateBreadcrumbs(breadcrumbs, 5);\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { id, locale } = await params;\n  const webpage = await getWebPage(id);\n  const { pageTitle, metaDescription, metaKeywords } = webpage.seo;\n\n  // Get the path from the last breadcrumb\n  const pagePath = webpage.breadcrumbs[webpage.breadcrumbs.length - 1]?.href;\n\n  return {\n    title: pageTitle || webpage.title,\n    ...(metaDescription && { description: metaDescription }),\n    ...(metaKeywords && { keywords: metaKeywords.split(',') }),\n    ...(pagePath && { alternates: await getMetadataAlternates({ path: pagePath, locale }) }),\n  };\n}\n\nexport default async function WebPage({ params }: Props) {\n  const { locale, id } = await params;\n\n  setRequestLocale(locale);\n\n  return (\n    <WebPageContent\n      breadcrumbs={Streamable.from(() => getWebPageBreadcrumbs(id))}\n      webPage={Streamable.from(() => getWebPage(id))}\n    />\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/(default)/wishlist/[token]/page-data.ts",
    "content": "import { cache } from 'react';\n\nimport { client } from '~/client';\nimport { PaginationFragment } from '~/client/fragments/pagination';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { TAGS } from '~/client/tags';\nimport { ProductCardFragment } from '~/components/product-card/fragment';\nimport { WishlistItemFragment } from '~/components/wishlist/fragment';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\n\nconst PublicWishlistQuery = graphql(\n  `\n    query PublicWishlistQuery(\n      $first: Int\n      $after: String\n      $last: Int\n      $before: String\n      $token: String!\n      $currencyCode: currencyCode\n    ) {\n      site {\n        publicWishlist(token: $token) {\n          entityId\n          name\n          token\n          items(first: $first, after: $after, last: $last, before: $before) {\n            edges {\n              node {\n                ...WishlistItemFragment\n              }\n            }\n            pageInfo {\n              ...PaginationFragment\n            }\n            collectionInfo {\n              totalItems\n            }\n          }\n        }\n      }\n    }\n  `,\n  [WishlistItemFragment, ProductCardFragment, PaginationFragment],\n);\n\ninterface Pagination {\n  limit?: number;\n  before?: string | null;\n  after?: string | null;\n}\n\nexport const getPublicWishlist = cache(async (token: string, pagination: Pagination) => {\n  const { before, after, limit = 9 } = pagination;\n  const currencyCode = await getPreferredCurrencyCode();\n  const paginationArgs = before ? { last: limit, before } : { first: limit, after };\n  const response = await client.fetch({\n    document: PublicWishlistQuery,\n    variables: { ...paginationArgs, currencyCode, token },\n    // Since the wishlist is public, it's okay that we cache this request\n    fetchOptions: { next: { revalidate, tags: [TAGS.customer] } },\n  });\n\n  const wishlist = response.data.site.publicWishlist;\n\n  if (!wishlist) {\n    return null;\n  }\n\n  return wishlist;\n});\n"
  },
  {
    "path": "core/app/[locale]/(default)/wishlist/[token]/page.tsx",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { getFormatter, getTranslations, setRequestLocale } from 'next-intl/server';\nimport { SearchParams } from 'nuqs';\nimport { createSearchParamsCache, parseAsInteger, parseAsString } from 'nuqs/server';\n\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination';\nimport { Breadcrumb, Breadcrumbs } from '@/vibes/soul/sections/breadcrumbs';\nimport { SectionLayout } from '@/vibes/soul/sections/section-layout';\nimport { Wishlist, WishlistDetails } from '@/vibes/soul/sections/wishlist-details';\nimport { addWishlistItemToCart } from '~/app/[locale]/(default)/account/wishlists/[id]/_actions/add-to-cart';\nimport { WishlistAnalyticsProvider } from '~/app/[locale]/(default)/account/wishlists/[id]/_components/wishlist-analytics-provider';\nimport { ExistingResultType } from '~/client/util';\nimport {\n  WishlistShareButton,\n  WishlistShareButtonSkeleton,\n} from '~/components/wishlist/share-button';\nimport { defaultPageInfo, pageInfoTransformer } from '~/data-transformers/page-info-transformer';\nimport { publicWishlistDetailsTransformer } from '~/data-transformers/wishlists-transformer';\nimport { getMetadataAlternates } from '~/lib/seo/canonical';\nimport { isMobileUser } from '~/lib/user-agent';\n\nimport { getPublicWishlist } from './page-data';\n\ninterface Props {\n  params: Promise<{ locale: string; token: string }>;\n  searchParams: Promise<SearchParams>;\n}\n\nconst defaultWishlistItemsLimit = 12;\nconst searchParamsCache = createSearchParamsCache({\n  tag: parseAsString,\n  before: parseAsString,\n  after: parseAsString,\n  limit: parseAsInteger.withDefault(defaultWishlistItemsLimit),\n});\n\nasync function getWishlist(\n  token: string,\n  t: ExistingResultType<typeof getTranslations<'Wishlist'>>,\n  pt: ExistingResultType<typeof getTranslations<'Product.ProductDetails'>>,\n  searchParams: Promise<SearchParams>,\n): Promise<Wishlist> {\n  const searchParamsParsed = searchParamsCache.parse(await searchParams);\n  const formatter = await getFormatter();\n  const wishlist = await getPublicWishlist(token, searchParamsParsed);\n\n  if (!wishlist) {\n    return notFound();\n  }\n\n  return publicWishlistDetailsTransformer(wishlist, t, pt, formatter);\n}\n\nasync function getPaginationInfo(\n  token: string,\n  searchParams: Promise<SearchParams>,\n): Promise<CursorPaginationInfo> {\n  const searchParamsParsed = searchParamsCache.parse(await searchParams);\n  const wishlist = await getPublicWishlist(token, searchParamsParsed);\n\n  return pageInfoTransformer(wishlist?.items.pageInfo ?? defaultPageInfo);\n}\n\nexport async function generateMetadata({ params, searchParams }: Props): Promise<Metadata> {\n  const { locale, token } = await params;\n  // Even though we don't need paginated data during metadata generation, we should still pass the parameters\n  // to make sure we aren't bypassing an existing cache just for the metadata generation.\n  const searchParamsParsed = searchParamsCache.parse(await searchParams);\n  const t = await getTranslations({ locale, namespace: 'PublicWishlist' });\n  const wishlist = await getPublicWishlist(token, searchParamsParsed);\n\n  return {\n    title: wishlist?.name ?? t('title'),\n    alternates: await getMetadataAlternates({ path: `/wishlist/${token}`, locale }),\n  };\n}\n\nconst getAnalyticsData = async (token: string, searchParamsPromise: Promise<SearchParams>) => {\n  const searchParamsParsed = searchParamsCache.parse(await searchParamsPromise);\n  const wishlist = await getPublicWishlist(token, searchParamsParsed);\n\n  if (!wishlist) {\n    return [];\n  }\n\n  return removeEdgesAndNodes(wishlist.items)\n    .map(({ product }) => product)\n    .filter((product) => product !== null)\n    .map((product) => {\n      return {\n        id: product.entityId,\n        name: product.name,\n        sku: product.sku,\n        brand: product.brand?.name ?? '',\n        price: product.prices?.price.value ?? 0,\n        currency: product.prices?.price.currencyCode ?? '',\n      };\n    });\n};\n\nasync function getBreadcrumbs(\n  token: string,\n  searchParams: Promise<SearchParams>,\n): Promise<Breadcrumb[]> {\n  const t = await getTranslations('PublicWishlist');\n  const searchParamsParsed = searchParamsCache.parse(await searchParams);\n  const wishlist = await getPublicWishlist(token, searchParamsParsed);\n\n  return [\n    { href: '/', label: 'Home' },\n    { href: '#', label: wishlist?.name ?? t('defaultName') },\n  ];\n}\n\nexport default async function PublicWishlist({ params, searchParams }: Props) {\n  const { locale, token } = await params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Wishlist');\n  const pwt = await getTranslations('PublicWishlist');\n  const pt = await getTranslations('Product.ProductDetails');\n  const wishlistActions = (wishlist?: Wishlist) => {\n    if (!wishlist) {\n      return (\n        <div className=\"flex items-center\">\n          <WishlistShareButtonSkeleton size=\"medium\" />\n        </div>\n      );\n    }\n\n    const { publicUrl } = wishlist;\n\n    if (!publicUrl || publicUrl === '') {\n      return null;\n    }\n\n    return (\n      <div className=\"flex items-center\">\n        <WishlistShareButton\n          closeLabel={t('Modal.close')}\n          copiedMessage={t('shareCopied')}\n          copyLabel={t('Modal.copy')}\n          disabledTooltip={t('shareDisabled')}\n          isMobileUser={Streamable.from(isMobileUser)}\n          isPublic={wishlist.visibility.isPublic}\n          label={t('share')}\n          modalTitle={t('Modal.shareTitle', { name: wishlist.name })}\n          publicUrl={publicUrl}\n          size=\"medium\"\n          successMessage={t('shareSuccess')}\n          wishlistName={wishlist.name}\n        />\n      </div>\n    );\n  };\n\n  return (\n    <WishlistAnalyticsProvider data={Streamable.from(() => getAnalyticsData(token, searchParams))}>\n      <SectionLayout>\n        <Breadcrumbs breadcrumbs={Streamable.from(() => getBreadcrumbs(token, searchParams))} />\n\n        <WishlistDetails\n          action={addWishlistItemToCart}\n          className=\"mt-8\"\n          emptyStateText={pwt('emptyWishlist')}\n          headerActions={wishlistActions}\n          paginationInfo={Streamable.from(() => getPaginationInfo(token, searchParams))}\n          wishlist={Streamable.from(() => getWishlist(token, t, pt, searchParams))}\n        />\n      </SectionLayout>\n    </WishlistAnalyticsProvider>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/error.tsx",
    "content": "'use client';\n\nimport { useTranslations } from 'next-intl';\n\nimport { Error as ErrorSection } from '@/vibes/soul/sections/error';\n\ninterface Props {\n  error: Error & { digest?: string };\n  reset: () => void;\n}\n\nexport default function Error({ reset }: Props) {\n  const t = useTranslations('Error');\n\n  return <ErrorSection ctaAction={reset} subtitle={t('subtitle')} title={t('title')} />;\n}\n"
  },
  {
    "path": "core/app/[locale]/layout.tsx",
    "content": "import { Analytics } from '@vercel/analytics/react';\nimport { SpeedInsights } from '@vercel/speed-insights/next';\nimport { clsx } from 'clsx';\nimport type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { NextIntlClientProvider } from 'next-intl';\nimport { setRequestLocale } from 'next-intl/server';\nimport { NuqsAdapter } from 'nuqs/adapters/next/app';\nimport { cache, PropsWithChildren } from 'react';\n\nimport '../../globals.css';\n\nimport { fonts } from '~/app/fonts';\nimport { CookieNotifications } from '~/app/notifications';\nimport { Providers } from '~/app/providers';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { WebAnalyticsFragment } from '~/components/analytics/fragment';\nimport { AnalyticsProvider } from '~/components/analytics/provider';\nimport { ConsentManager } from '~/components/consent-manager';\nimport { ScriptsFragment } from '~/components/consent-manager/scripts-fragment';\nimport { ContainerQueryPolyfill } from '~/components/polyfills/container-query';\nimport { scriptsTransformer } from '~/data-transformers/scripts-transformer';\nimport { routing } from '~/i18n/routing';\nimport { getToastNotification } from '~/lib/server-toast';\n\nconst RootLayoutMetadataQuery = graphql(\n  `\n    query RootLayoutMetadataQuery {\n      site {\n        settings {\n          url {\n            vanityUrl\n          }\n          privacy {\n            cookieConsentEnabled\n            privacyPolicyUrl\n          }\n          storeName\n          seo {\n            pageTitle\n            metaDescription\n            metaKeywords\n          }\n          ...WebAnalyticsFragment\n        }\n        content {\n          ...ScriptsFragment\n        }\n      }\n      channel {\n        entityId\n      }\n    }\n  `,\n  [WebAnalyticsFragment, ScriptsFragment],\n);\n\nconst fetchRootLayoutMetadata = cache(async () => {\n  return await client.fetch({\n    document: RootLayoutMetadataQuery,\n    fetchOptions: { next: { revalidate } },\n  });\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n  const { data } = await fetchRootLayoutMetadata();\n\n  const storeName = data.site.settings?.storeName ?? '';\n\n  const { pageTitle, metaDescription, metaKeywords } = data.site.settings?.seo || {};\n\n  const vanityUrl = data.site.settings?.url.vanityUrl;\n\n  // Use preview deployment URL so metadataBase (canonical, og:url) points at the preview, not production.\n  let baseUrl: URL | undefined;\n  const previewUrl =\n    process.env.VERCEL_ENV === 'preview' ? `https://${process.env.VERCEL_URL}` : undefined;\n\n  if (previewUrl && URL.canParse(previewUrl)) {\n    baseUrl = new URL(previewUrl);\n  } else if (vanityUrl && URL.canParse(vanityUrl)) {\n    baseUrl = new URL(vanityUrl);\n  }\n\n  return {\n    metadataBase: baseUrl,\n    title: {\n      template: `%s - ${storeName}`,\n      default: pageTitle || storeName,\n    },\n    icons: {\n      icon: '/favicon.ico', // app/favicon.ico/route.ts\n    },\n    description: metaDescription,\n    keywords: metaKeywords ? metaKeywords.split(',') : null,\n    other: {\n      platform: 'bigcommerce.catalyst',\n      build_sha: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA ?? '',\n      store_hash: process.env.BIGCOMMERCE_STORE_HASH ?? '',\n    },\n  };\n}\n\nconst VercelComponents = () => {\n  if (process.env.VERCEL !== '1') {\n    return null;\n  }\n\n  return (\n    <>\n      {process.env.DISABLE_VERCEL_ANALYTICS !== 'true' && <Analytics />}\n      {process.env.DISABLE_VERCEL_SPEED_INSIGHTS !== 'true' && <SpeedInsights />}\n    </>\n  );\n};\n\ninterface Props extends PropsWithChildren {\n  params: Promise<{ locale: string }>;\n}\n\nexport default async function RootLayout({ params, children }: Props) {\n  const { locale } = await params;\n\n  const rootData = await fetchRootLayoutMetadata();\n  const toastNotificationCookieData = await getToastNotification();\n\n  if (!routing.locales.includes(locale)) {\n    notFound();\n  }\n\n  // need to call this method everywhere where static rendering is enabled\n  // https://next-intl-docs.vercel.app/docs/getting-started/app-router#add-setRequestLocale-to-all-layouts-and-pages\n  setRequestLocale(locale);\n\n  const scripts = scriptsTransformer(rootData.data.site.content.scripts);\n  const isCookieConsentEnabled =\n    rootData.data.site.settings?.privacy?.cookieConsentEnabled ?? false;\n  const privacyPolicyUrl = rootData.data.site.settings?.privacy?.privacyPolicyUrl;\n\n  return (\n    <html className={clsx(fonts.map((f) => f.variable))} lang={locale}>\n      <body className=\"flex min-h-screen flex-col\">\n        <NextIntlClientProvider>\n          <ConsentManager\n            isCookieConsentEnabled={isCookieConsentEnabled}\n            privacyPolicyUrl={privacyPolicyUrl}\n            scripts={scripts}\n          >\n            <NuqsAdapter>\n              <AnalyticsProvider\n                channelId={rootData.data.channel.entityId}\n                isCookieConsentEnabled={isCookieConsentEnabled}\n                settings={rootData.data.site.settings}\n              >\n                <Providers>\n                  {toastNotificationCookieData && (\n                    <CookieNotifications {...toastNotificationCookieData} />\n                  )}\n                  {children}\n                </Providers>\n              </AnalyticsProvider>\n            </NuqsAdapter>\n          </ConsentManager>\n        </NextIntlClientProvider>\n        <VercelComponents />\n        <ContainerQueryPolyfill />\n      </body>\n    </html>\n  );\n}\n\nexport function generateStaticParams() {\n  return routing.locales.map((locale) => ({ locale }));\n}\n\nexport const fetchCache = 'default-cache';\n"
  },
  {
    "path": "core/app/[locale]/maintenance/page.tsx",
    "content": "import { Metadata } from 'next';\nimport { getTranslations, setRequestLocale } from 'next-intl/server';\nimport { ReactNode } from 'react';\n\nimport { Maintenance as MaintenanceSection } from '@/vibes/soul/sections/maintenance';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { StoreLogoFragment } from '~/components/store-logo/fragment';\nimport { logoTransformer } from '~/data-transformers/logo-transformer';\n\nconst MaintenancePageQuery = graphql(\n  `\n    query MaintenancePageQuery {\n      site {\n        settings {\n          contact {\n            phone\n            email\n          }\n          statusMessage\n          ...StoreLogoFragment\n        }\n      }\n    }\n  `,\n  [StoreLogoFragment],\n);\n\ninterface Props {\n  params: Promise<{ locale: string }>;\n}\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { locale } = await params;\n\n  const t = await getTranslations({ locale, namespace: 'Maintenance' });\n\n  return {\n    title: t('title'),\n  };\n}\n\nconst Container = ({ children }: { children: ReactNode }) => (\n  <main className=\"mx-auto flex h-screen w-full flex-col items-center justify-center px-4 md:px-10\">\n    {children}\n  </main>\n);\n\nexport default async function Maintenance({ params }: Props) {\n  const { locale } = await params;\n\n  setRequestLocale(locale);\n\n  const t = await getTranslations('Maintenance');\n\n  const { data } = await client.fetch({\n    document: MaintenancePageQuery,\n  });\n\n  const storeSettings = data.site.settings;\n\n  if (!storeSettings) {\n    return (\n      <Container>\n        <MaintenanceSection className=\"w-full\" />\n      </Container>\n    );\n  }\n\n  const { contact, statusMessage } = storeSettings;\n  const logo = data.site.settings ? logoTransformer(data.site.settings) : '';\n\n  return (\n    <Container>\n      <MaintenanceSection\n        className=\"w-full\"\n        contactEmail={contact?.email}\n        contactPhone={contact?.phone}\n        contactText={t('contactUs')}\n        logo={logo}\n        statusMessage={statusMessage ?? undefined}\n        title={t('message')}\n      />\n    </Container>\n  );\n}\n"
  },
  {
    "path": "core/app/[locale]/not-found.tsx",
    "content": "import { getTranslations } from 'next-intl/server';\n\nimport { NotFound as NotFoundSection } from '@/vibes/soul/sections/not-found';\nimport { Footer } from '~/components/footer';\nimport { Header } from '~/components/header';\n\nexport default async function NotFound() {\n  const t = await getTranslations('NotFound');\n\n  return (\n    <>\n      <Header />\n\n      <NotFoundSection\n        className=\"flex-1 place-content-center\"\n        ctaLabel={t('search')}\n        subtitle={t('subtitle')}\n        title={t('title')}\n      />\n\n      <Footer />\n    </>\n  );\n}\n"
  },
  {
    "path": "core/app/admin/route.ts",
    "content": "import { defaultLocale } from '~/i18n/locales';\nimport { redirect } from '~/i18n/routing';\n\nconst canonicalDomain: string = process.env.BIGCOMMERCE_GRAPHQL_API_DOMAIN ?? 'mybigcommerce.com';\nconst BIGCOMMERCE_STORE_HASH = process.env.BIGCOMMERCE_STORE_HASH;\nconst ENABLE_ADMIN_ROUTE = process.env.ENABLE_ADMIN_ROUTE;\n\nexport const GET = () => {\n  // This route should not work unless explicitly enabled\n  if (ENABLE_ADMIN_ROUTE !== 'true') {\n    return redirect({ href: '/', locale: defaultLocale });\n  }\n\n  return redirect({\n    href: BIGCOMMERCE_STORE_HASH\n      ? `https://store-${BIGCOMMERCE_STORE_HASH}.${canonicalDomain}/admin`\n      : 'https://login.bigcommerce.com',\n    locale: defaultLocale,\n  });\n};\n"
  },
  {
    "path": "core/app/api/auth/[...nextauth]/route.ts",
    "content": "import { handlers } from '~/auth';\n\nexport const { GET, POST } = handlers;\n"
  },
  {
    "path": "core/app/fonts.ts",
    "content": "import { DM_Serif_Text, Inter, Roboto_Mono } from 'next/font/google';\n\nconst inter = Inter({\n  display: 'swap',\n  subsets: ['latin'],\n  variable: '--font-family-body',\n});\n\nconst dmSerifText = DM_Serif_Text({\n  display: 'swap',\n  subsets: ['latin'],\n  weight: '400',\n  variable: '--font-family-heading',\n});\n\nconst robotoMono = Roboto_Mono({\n  subsets: ['latin'],\n  display: 'swap',\n  variable: '--font-family-mono',\n});\n\nexport const fonts = [inter, dmSerifText, robotoMono];\n"
  },
  {
    "path": "core/app/layout.tsx",
    "content": "import { PropsWithChildren } from 'react';\n\n// Since we have a `not-found.tsx` at the root, a layout file is required even if\n// it just passes children through. Ownership of <html>/<body> lives in\n// app/[locale]/layout.tsx (to set lang={locale}) and app/not-found.tsx (for\n// non-localized 404s). See: https://next-intl.dev/docs/environments/error-files\n// TODO: Move <html>/<body> back here and set lang via Next.js `rootParams`\n// once it is available on Native Hosting (Next.js 16.2+).\nexport default function RootLayout({ children }: PropsWithChildren) {\n  return children;\n}\n"
  },
  {
    "path": "core/app/not-found.tsx",
    "content": "import { clsx } from 'clsx';\n\nimport '../globals.css';\n\nimport { fonts } from '~/app/fonts';\n\n// Renders for non-localized requests that don't match any route (e.g. /unknown.txt)\n// or when app/[locale]/layout.tsx calls notFound() for an invalid locale. Since\n// app/layout.tsx is a passthrough, this page must own its own <html>/<body>.\nexport default function RootNotFound() {\n  return (\n    <html className={clsx(fonts.map((f) => f.variable))} lang=\"en\">\n      <body className=\"flex min-h-screen flex-col\">\n        <main className=\"flex flex-1 items-center justify-center font-[family-name:var(--not-found-font-family,var(--font-family-body))]\">\n          <div className=\"mx-auto w-full max-w-screen-2xl px-3 py-10 @container @xl:px-6 @4xl:px-20\">\n            <header className=\"text-center\">\n              <h1 className=\"mb-3 font-[family-name:var(--not-found-title-font-family,var(--font-family-heading))] text-3xl font-medium leading-none text-[var(--not-found-title,hsl(var(--foreground)))] @xl:text-4xl @4xl:text-5xl\">\n                Not found\n              </h1>\n              <p className=\"mb-4 text-lg text-[var(--not-found-subtitle,hsl(var(--contrast-500)))]\">\n                The page you are looking for could not be found.\n              </p>\n              <a\n                className=\"relative z-0 inline-flex min-h-14 select-none items-center justify-center gap-x-3 overflow-hidden rounded-full border border-[var(--button-primary-border,hsl(var(--primary)))] bg-[var(--button-primary-background,hsl(var(--primary)))] px-6 py-4 text-center font-[family-name:var(--button-font-family)] text-base font-semibold leading-normal text-[var(--button-primary-text)] after:absolute after:inset-0 after:-z-10 after:-translate-x-[105%] after:rounded-full after:bg-[var(--button-primary-background-hover,color-mix(in_oklab,hsl(var(--primary)),white_75%))] after:transition-[opacity,transform] after:duration-300 after:[animation-timing-function:cubic-bezier(0,0.25,0,1)] hover:after:translate-x-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--button-focus,hsl(var(--primary)))] focus-visible:ring-offset-2\"\n                href=\"/\"\n              >\n                <span>Go to homepage</span>\n              </a>\n            </header>\n          </div>\n        </main>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "core/app/notifications.tsx",
    "content": "'use client';\n\nimport { useEffect, useRef } from 'react';\n\nimport { toast } from '@/vibes/soul/primitives/toaster';\nimport { type ServerToastData } from '~/lib/server-toast';\n\nexport const CookieNotifications = (notification: ServerToastData) => {\n  const lastRendered = useRef<ServerToastData>(null);\n\n  useEffect(() => {\n    const { message, variant, position, description } = notification;\n\n    if (notification.id !== lastRendered.current?.id) {\n      toast[variant](message, { position, description });\n      lastRendered.current = notification;\n    }\n  }, [notification]);\n\n  return null;\n};\n"
  },
  {
    "path": "core/app/providers.tsx",
    "content": "'use client';\n\nimport { PropsWithChildren } from 'react';\n\nimport { Toaster } from '@/vibes/soul/primitives/toaster';\nimport { SearchProvider } from '~/lib/search';\n\nexport function Providers({ children }: PropsWithChildren) {\n  return (\n    <SearchProvider>\n      <Toaster position=\"top-right\" />\n      {children}\n    </SearchProvider>\n  );\n}\n"
  },
  {
    "path": "core/app/robots.txt/route.ts",
    "content": "/* eslint-disable check-file/folder-naming-convention */\n/*\n * Robots.txt route\n *\n * This route pulls robots.txt content from the channel settings.\n *\n * If you would like to configure this in code instead, delete this file and follow this guide:\n *\n * https://nextjs.org/docs/app/api-reference/file-conventions/metadata/robots\n *\n */\n\nimport { getChannelIdFromLocale } from '~/channels.config';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { defaultLocale } from '~/i18n/locales';\n\nconst RobotsTxtQuery = graphql(`\n  query RobotsTxtQuery {\n    site {\n      settings {\n        robotsTxt\n      }\n    }\n  }\n`);\n\nfunction parseUrl(url?: string): URL {\n  let incomingUrl = '';\n  const defaultUrl = new URL('http://localhost:3000/');\n\n  if (url && !url.startsWith('http')) {\n    incomingUrl = `https://${url}`;\n  }\n\n  return new URL(incomingUrl || defaultUrl);\n}\n\nconst baseUrl = parseUrl(\n  process.env.NEXTAUTH_URL || process.env.VERCEL_PROJECT_PRODUCTION_URL || '',\n);\n\nexport const GET = async () => {\n  const { data } = await client.fetch({\n    document: RobotsTxtQuery,\n    channelId: getChannelIdFromLocale(defaultLocale),\n    fetchOptions: { cache: 'no-store' }, // disable caching to get the latest robots.txt at build time\n  });\n\n  const robotsTxt = `${data.site.settings?.robotsTxt ?? ''}\\nSitemap: ${baseUrl.origin}/sitemap.xml\\n`;\n\n  return new Response(robotsTxt, {\n    headers: {\n      'Content-Type': 'text/plain; charset=UTF-8',\n    },\n  });\n};\n\nexport const dynamic = 'force-static';\n"
  },
  {
    "path": "core/app/sitemap.xml/route.ts",
    "content": "/* eslint-disable check-file/folder-naming-convention */\n/*\n * Proxy to the existing BigCommerce sitemap index on the canonical URL\n */\n\nimport { getChannelIdFromLocale } from '~/channels.config';\nimport { client } from '~/client';\nimport { defaultLocale } from '~/i18n/locales';\n\nexport const GET = async () => {\n  const sitemapIndex = await client.fetchSitemapIndex(getChannelIdFromLocale(defaultLocale));\n\n  return new Response(sitemapIndex, {\n    headers: {\n      'Content-Type': 'application/xml',\n    },\n  });\n};\n"
  },
  {
    "path": "core/app/xmlsitemap.php/route.ts",
    "content": "/* eslint-disable check-file/folder-naming-convention */\nimport { defaultLocale } from '~/i18n/locales';\nimport { permanentRedirect } from '~/i18n/routing';\n\n/*\n * This route is used to redirect the legacy Stencil sitemap that lives on /xmlsitemap.php\n * to Catalyst's new location on /sitemap.xml\n * This is for the benefit of websites who already have a sitemap submitted to Webmaster Tools\n * on /xmlsitemap.php\n */\n\nexport const GET = () => {\n  permanentRedirect({ href: '/sitemap.xml', locale: defaultLocale });\n};\n"
  },
  {
    "path": "core/auth/anonymous-session.ts",
    "content": "import { cookies, headers } from 'next/headers';\nimport { AnonymousUser } from 'next-auth';\nimport { decode, encode } from 'next-auth/jwt';\n\nconst anonymousCookieName = 'authjs.anonymous-session-token';\n\nconst shouldUseSecureCookie = async () => {\n  const headersList = await headers();\n\n  return headersList.get('x-forwarded-proto') === 'https';\n};\n\nexport const anonymousSignIn = async (user: Partial<AnonymousUser> = { cartId: null }) => {\n  const secret = process.env.AUTH_SECRET ?? process.env.NEXTAUTH_SECRET;\n  const useSecureCookies = await shouldUseSecureCookie();\n  const cookiePrefix = useSecureCookies ? '__Secure-' : '';\n\n  if (!secret) {\n    throw new Error('AUTH_SECRET is not set');\n  }\n\n  const cookieJar = await cookies();\n  const jwt = await encode({\n    salt: `${cookiePrefix}${anonymousCookieName}`,\n    secret,\n    token: {\n      user,\n    },\n  });\n\n  cookieJar.set(`${cookiePrefix}${anonymousCookieName}`, jwt, {\n    secure: useSecureCookies,\n    sameSite: 'lax',\n    // We set the maxAge to 7 days as a good default for anonymous sessions.\n    // This can be adjusted based on your application's needs.\n    maxAge: 60 * 60 * 24 * 7, // 7 days\n    httpOnly: true,\n  });\n};\n\nexport const getAnonymousSession = async () => {\n  const cookieJar = await cookies();\n  const useSecureCookies = await shouldUseSecureCookie();\n  const cookiePrefix = useSecureCookies ? '__Secure-' : '';\n  const jwt = cookieJar.get(`${cookiePrefix}${anonymousCookieName}`);\n\n  if (!jwt) {\n    return null;\n  }\n\n  const secret = process.env.AUTH_SECRET ?? process.env.NEXTAUTH_SECRET;\n\n  if (!secret) {\n    throw new Error('AUTH_SECRET is not set');\n  }\n\n  try {\n    const session = await decode({\n      secret,\n      salt: `${cookiePrefix}${anonymousCookieName}`,\n      token: jwt.value,\n    });\n\n    return session;\n  } catch (err) {\n    // eslint-disable-next-line no-console\n    console.error('Failed to decode anonymous session cookie', err);\n\n    return null;\n  }\n};\n\nexport const clearAnonymousSession = async () => {\n  const cookieJar = await cookies();\n  const useSecureCookies = await shouldUseSecureCookie();\n  const cookiePrefix = useSecureCookies ? '__Secure-' : '';\n\n  cookieJar.delete({\n    name: `${cookiePrefix}${anonymousCookieName}`,\n    secure: useSecureCookies,\n    sameSite: 'lax',\n    httpOnly: true,\n  });\n};\n\nexport const updateAnonymousSession = async (user: AnonymousUser) => {\n  const session = await getAnonymousSession();\n\n  if (!session) {\n    return null;\n  }\n\n  await anonymousSignIn(user);\n};\n"
  },
  {
    "path": "core/auth/customer-login-api.ts",
    "content": "import { randomUUID } from 'crypto';\nimport { SignJWT } from 'jose';\n\n/**\n * Build a Customer Login API JWT which can be used in auth/index.ts to log in a customer\n * using the LoginWithTokenMutation, or used as a redirect to /login/token/[token]\n *\n * This is a stub intended to be used when implementing 3rd party authentication callbacks\n *\n * Requires that BIGCOMMERCE_CLIENT_SECRET and BIGCOMMERCE_CLIENT_ID are set in the environment\n * from a client that has the Customer Login scope enabled\n *\n * @param {number} customerId - The BigCommerce customer ID to generate the login token for\n * @param {number} [channelId] - Channel ID that the customer will be logged into\n * @param {string} [redirectTo] - Relative URL to redirect to after successful login\n * @param {Record<string, any>} [additionalClaims] - Optional additional claims to include in the JWT\n * @returns {Promise<string>} A JWT token that can be used to authenticate the customer\n * @throws {Error} If BIGCOMMERCE_CLIENT_SECRET is not set in environment variables\n * @throws {Error} If BIGCOMMERCE_CLIENT_ID is not set in environment variables\n */\nexport const generateCustomerLoginApiJwt = async (\n  customerId: number,\n  channelId: number,\n  redirectTo = '/account/orders',\n  additionalClaims?: Record<string, unknown>,\n): Promise<string> => {\n  const clientId = process.env.BIGCOMMERCE_CLIENT_ID;\n  const clientSecret = process.env.BIGCOMMERCE_CLIENT_SECRET;\n  const storeHash = process.env.BIGCOMMERCE_STORE_HASH;\n\n  if (!clientSecret) {\n    throw new Error('BIGCOMMERCE_CLIENT_SECRET is not set in environment variables');\n  }\n\n  if (!clientId) {\n    throw new Error('BIGCOMMERCE_CLIENT_ID is not set in environment variables');\n  }\n\n  if (!storeHash) {\n    throw new Error('BIGCOMMERCE_STORE_HASH is not set in environment variables');\n  }\n\n  const payload = {\n    iss: clientId,\n    iat: Math.floor(Date.now() / 1000),\n    jti: randomUUID(),\n    operation: 'customer_login',\n    store_hash: storeHash,\n    customer_id: Math.floor(customerId),\n    ...(channelId && { channel_id: channelId }),\n    ...(redirectTo && { redirect_to: redirectTo }),\n    ...(additionalClaims || {}),\n  };\n\n  // Convert client secret to Uint8Array for jose library\n  const secretKey = new TextEncoder().encode(clientSecret);\n\n  // Create and sign the JWT\n  return await new SignJWT(payload)\n    .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })\n    .sign(secretKey);\n};\n"
  },
  {
    "path": "core/auth/index.ts",
    "content": "import { decodeJwt } from 'jose';\nimport NextAuth, { type NextAuthConfig, User } from 'next-auth';\nimport 'next-auth/jwt';\nimport CredentialsProvider from 'next-auth/providers/credentials';\nimport { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { anonymousSignIn, clearAnonymousSession } from '~/auth/anonymous-session';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { clearCartId, setCartId } from '~/lib/cart';\nimport { serverToast } from '~/lib/server-toast';\n\nconst LoginMutation = graphql(`\n  mutation LoginMutation($email: String!, $password: String!, $cartEntityId: String) {\n    login(email: $email, password: $password, guestCartEntityId: $cartEntityId) {\n      customerAccessToken {\n        value\n      }\n      customer {\n        entityId\n        firstName\n        lastName\n        email\n      }\n      cart {\n        entityId\n      }\n    }\n  }\n`);\n\nconst LoginWithTokenMutation = graphql(`\n  mutation LoginWithCustomerLoginJwtMutation($jwt: String!, $cartEntityId: String) {\n    loginWithCustomerLoginJwt(jwt: $jwt, guestCartEntityId: $cartEntityId) {\n      customerAccessToken {\n        value\n      }\n      customer {\n        entityId\n        firstName\n        lastName\n        email\n      }\n      cart {\n        entityId\n      }\n    }\n  }\n`);\n\nconst LogoutMutation = graphql(`\n  mutation LogoutMutation($cartEntityId: String) {\n    logout(cartEntityId: $cartEntityId) {\n      result\n      cartUnassignResult {\n        cart {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\nconst cartIdSchema = z\n  .string()\n  .uuid()\n  .or(z.literal('undefined')) // auth.js seems to pass the cart id as a string literal 'undefined' when not set.\n  .optional()\n  .transform((val) => (val === 'undefined' ? undefined : val));\n\nconst PasswordCredentials = z.object({\n  email: z.string().email(),\n  password: z.string().min(1),\n  cartId: cartIdSchema,\n});\n\nconst JwtCredentials = z.object({\n  jwt: z.string(),\n  cartId: cartIdSchema,\n});\n\nconst SessionUpdate = z.object({\n  user: z.object({\n    cartId: cartIdSchema,\n  }),\n});\n\nasync function handleLoginCart(guestCartId?: string, loginResultCartId?: string) {\n  const t = await getTranslations('Cart');\n\n  if (guestCartId === undefined && loginResultCartId !== undefined) {\n    await serverToast.info(t('cartRestored'), { position: 'top-center' });\n  }\n\n  if (loginResultCartId && guestCartId && loginResultCartId !== guestCartId) {\n    await serverToast.info(t('cartCombined'), { position: 'top-center' });\n  }\n\n  if (loginResultCartId) {\n    await setCartId(loginResultCartId);\n  }\n}\n\nasync function loginWithPassword(credentials: unknown): Promise<User | null> {\n  const { email, password, cartId } = PasswordCredentials.parse(credentials);\n\n  const response = await client.fetch({\n    document: LoginMutation,\n    variables: { email, password, cartEntityId: cartId },\n    fetchOptions: {\n      cache: 'no-store',\n    },\n  });\n\n  if (response.errors && response.errors.length > 0) {\n    return null;\n  }\n\n  const result = response.data.login;\n\n  if (!result.customer || !result.customerAccessToken) {\n    return null;\n  }\n\n  await handleLoginCart(cartId, result.cart?.entityId);\n  await clearAnonymousSession();\n\n  return {\n    firstName: result.customer.firstName,\n    lastName: result.customer.lastName,\n    email: result.customer.email,\n    customerAccessToken: result.customerAccessToken.value,\n    cartId: result.cart?.entityId,\n  };\n}\n\nasync function loginWithJwt(credentials: unknown): Promise<User | null> {\n  const { jwt, cartId } = JwtCredentials.parse(credentials);\n\n  const claims = decodeJwt(jwt);\n  const channelId = claims.channel_id?.toString() ?? process.env.BIGCOMMERCE_CHANNEL_ID;\n  const impersonatorId = claims.impersonator_id?.toString() ?? null;\n  const response = await client.fetch({\n    document: LoginWithTokenMutation,\n    variables: { jwt, cartEntityId: cartId },\n    channelId,\n    fetchOptions: {\n      cache: 'no-store',\n    },\n  });\n\n  if (response.errors && response.errors.length > 0) {\n    return null;\n  }\n\n  const result = response.data.loginWithCustomerLoginJwt;\n\n  if (!result.customer || !result.customerAccessToken) {\n    return null;\n  }\n\n  await handleLoginCart(cartId, result.cart?.entityId);\n  await clearAnonymousSession();\n\n  return {\n    firstName: result.customer.firstName,\n    lastName: result.customer.lastName,\n    email: result.customer.email,\n    customerAccessToken: result.customerAccessToken.value,\n    impersonatorId,\n    cartId: result.cart?.entityId,\n  };\n}\n\nconst config = {\n  // Explicitly setting this value to be undefined. We want the library to handle CSRF checks when taking sensitive actions.\n  // When handling sensitive actions like sign in, sign out, etc., the library will automatically check for CSRF tokens.\n  // If you need to implement your own sensitive actions, you will need to implement CSRF checks yourself.\n  skipCSRFCheck: undefined,\n  // Set this environment variable if you want to trust the host when using `next build` & `next start`.\n  // Otherwise, this will be controlled by process.env.NODE_ENV within the library.\n  trustHost: process.env.AUTH_TRUST_HOST === 'true' ? true : undefined,\n  session: {\n    strategy: 'jwt',\n  },\n  pages: {\n    signIn: '/login',\n    signOut: '/logout',\n  },\n  callbacks: {\n    jwt: ({ token, user, session, trigger }) => {\n      // user can actually be undefined\n      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n      if (user?.customerAccessToken) {\n        token.user = {\n          ...token.user,\n          customerAccessToken: user.customerAccessToken,\n        };\n      }\n\n      // user can actually be undefined\n      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n      if (user?.cartId) {\n        token.user = {\n          ...token.user,\n          cartId: user.cartId,\n        };\n      }\n\n      // user can actually be undefined\n      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n      if (user?.firstName !== undefined) {\n        token.user = {\n          ...token.user,\n          firstName: user.firstName,\n        };\n      }\n\n      // user can actually be undefined\n      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n      if (user?.lastName !== undefined) {\n        token.user = {\n          ...token.user,\n          lastName: user.lastName,\n        };\n      }\n\n      if (trigger === 'update') {\n        const parsedSession = SessionUpdate.safeParse(session);\n\n        if (parsedSession.success) {\n          token.user = {\n            ...token.user,\n            cartId: parsedSession.data.user.cartId,\n          };\n        }\n      }\n\n      return token;\n    },\n    session({ session, token }) {\n      if (token.user?.customerAccessToken) {\n        session.user.customerAccessToken = token.user.customerAccessToken;\n      }\n\n      if (token.user?.cartId !== undefined) {\n        session.user.cartId = token.user.cartId;\n      }\n\n      if (token.user?.firstName !== undefined) {\n        session.user.firstName = token.user.firstName;\n      }\n\n      if (token.user?.lastName !== undefined) {\n        session.user.lastName = token.user.lastName;\n      }\n\n      return session;\n    },\n  },\n  events: {\n    async signOut(message) {\n      const cartEntityId = 'token' in message ? message.token?.user?.cartId : null;\n      const customerAccessToken =\n        'token' in message ? message.token?.user?.customerAccessToken : null;\n\n      if (customerAccessToken) {\n        try {\n          const logoutResponse = await client.fetch({\n            document: LogoutMutation,\n            variables: {\n              cartEntityId,\n            },\n            customerAccessToken,\n            fetchOptions: {\n              cache: 'no-store',\n            },\n          });\n\n          // If the logout is successful, we want to establish a new anonymous session.\n          // This will allow us to restore the cart if persistent cart is disabled.\n          await anonymousSignIn();\n\n          // If persistent cart is disabled, we can restore the cart back to the anonymous session.\n          if (logoutResponse.data.logout.cartUnassignResult.cart) {\n            await setCartId(logoutResponse.data.logout.cartUnassignResult.cart.entityId);\n\n            return;\n          }\n\n          await clearCartId();\n        } catch (error) {\n          // eslint-disable-next-line no-console\n          console.error(error);\n        }\n      }\n    },\n  },\n  providers: [\n    CredentialsProvider({\n      id: 'password',\n      credentials: {\n        email: { label: 'Email', type: 'email' },\n        password: { label: 'Password', type: 'password' },\n        cartId: { type: 'text' },\n      },\n      authorize: loginWithPassword,\n    }),\n    CredentialsProvider({\n      id: 'jwt',\n      credentials: {\n        jwt: { type: 'text' },\n        cartId: { type: 'text' },\n      },\n      authorize: loginWithJwt,\n    }),\n  ],\n} satisfies NextAuthConfig;\n\nexport const { handlers, auth, signIn, signOut, unstable_update: updateSession } = NextAuth(config);\n\nexport const getSessionCustomerAccessToken = async () => {\n  try {\n    const session = await auth();\n\n    return session?.user?.customerAccessToken;\n  } catch {\n    // No empty\n  }\n};\n\nexport const isLoggedIn = async () => {\n  const cat = await getSessionCustomerAccessToken();\n\n  return Boolean(cat);\n};\n\nexport {\n  anonymousSignIn,\n  clearAnonymousSession,\n  getAnonymousSession,\n  updateAnonymousSession,\n} from './anonymous-session';\n"
  },
  {
    "path": "core/auth/types.ts",
    "content": "import { User } from 'next-auth';\n\ndeclare module 'next-auth' {\n  interface Session {\n    user?: User;\n  }\n\n  interface User {\n    firstName?: string | null;\n    lastName?: string | null;\n    email?: string | null;\n    cartId?: string | null;\n    customerAccessToken?: string;\n    impersonatorId?: string | null;\n  }\n\n  interface AnonymousUser {\n    cartId?: string | null;\n  }\n}\n\ndeclare module 'next-auth/jwt' {\n  interface JWT {\n    id?: string;\n    user?: User;\n  }\n}\n"
  },
  {
    "path": "core/build-config/reader.ts",
    "content": "import rawBuildConfig from './build-config.json';\nimport { buildConfigSchema, BuildConfigSchema } from './schema';\n\nclass BuildConfig {\n  private config = buildConfigSchema.parse(rawBuildConfig);\n\n  get<K extends keyof BuildConfigSchema>(key: K): BuildConfigSchema[K] {\n    if (key in this.config) {\n      return this.config[key];\n    }\n\n    throw new Error(`Key \"${key}\" not found in BuildConfig`);\n  }\n}\n\nexport const buildConfig = new BuildConfig();\n"
  },
  {
    "path": "core/build-config/schema.ts",
    "content": "import { z } from 'zod';\n\nexport const buildConfigSchema = z.object({\n  locales: z.array(\n    z.object({\n      code: z.string(),\n      isDefault: z.boolean(),\n    }),\n  ),\n  urls: z.object({\n    vanityUrl: z.string(),\n    cdnUrls: z.array(z.string()).default(['cdn11.bigcommerce.com']),\n    checkoutUrl: z.string(),\n  }),\n});\n\nexport type BuildConfigSchema = z.infer<typeof buildConfigSchema>;\n"
  },
  {
    "path": "core/build-config/writer.ts",
    "content": "/* eslint-disable no-console */\nimport { writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { z } from 'zod';\n\nimport { buildConfigSchema } from './schema';\n\nconst destinationPath = dirname(fileURLToPath(import.meta.url));\nconst CONFIG_FILE = join(destinationPath, 'build-config.json');\n\n// This fn is only intended to be used in the build process (next.config.ts)\nexport async function writeBuildConfig(data: unknown) {\n  try {\n    const parsedData = buildConfigSchema.parse(data);\n\n    await writeFile(CONFIG_FILE, JSON.stringify(data), 'utf8');\n\n    return parsedData;\n  } catch (error) {\n    if (error instanceof z.ZodError) {\n      console.error('Data validation failed:', error.errors);\n    } else {\n      console.error('Error writing build-config.json:', error);\n    }\n\n    throw error;\n  }\n}\n"
  },
  {
    "path": "core/channels.config.ts",
    "content": "// Set overrides per locale\nconst localeToChannelsMappings: Record<string, string> = {\n  // es: '12345',\n};\n\nfunction getChannelIdFromLocale(locale = '') {\n  return localeToChannelsMappings[locale] ?? process.env.BIGCOMMERCE_CHANNEL_ID;\n}\n\nexport { getChannelIdFromLocale };\n"
  },
  {
    "path": "core/client/correlation-id.ts",
    "content": "import { cache } from 'react';\n\nexport const getCorrelationId = cache(() => crypto.randomUUID());\n"
  },
  {
    "path": "core/client/fragments/pagination.ts",
    "content": "import { graphql } from '../graphql';\n\nexport const PaginationFragment = graphql(`\n  fragment PaginationFragment on PageInfo {\n    hasNextPage\n    hasPreviousPage\n    startCursor\n    endCursor\n  }\n`);\n"
  },
  {
    "path": "core/client/fragments/pricing.ts",
    "content": "import { graphql } from '../graphql';\n\nexport const PricingFragment = graphql(`\n  fragment PricingFragment on Product {\n    prices(currencyCode: $currencyCode) {\n      price {\n        value\n        currencyCode\n      }\n      basePrice {\n        value\n        currencyCode\n      }\n      retailPrice {\n        value\n        currencyCode\n      }\n      salePrice {\n        value\n        currencyCode\n      }\n      priceRange {\n        min {\n          value\n          currencyCode\n        }\n        max {\n          value\n          currencyCode\n        }\n      }\n    }\n  }\n`);\n"
  },
  {
    "path": "core/client/graphql.ts",
    "content": "import { initGraphQLTada } from 'gql.tada';\n\nimport type { introspection } from '~/bigcommerce-graphql';\n\nexport const graphql = initGraphQLTada<{\n  introspection: introspection;\n  scalars: {\n    DateTime: string;\n    Long: number;\n    BigDecimal: number;\n    UUID: string;\n  };\n  disableMasking: true;\n}>();\n\nexport type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada';\nexport { readFragment } from 'gql.tada';\n"
  },
  {
    "path": "core/client/index.ts",
    "content": "import { BigCommerceAuthError, createClient } from '@bigcommerce/catalyst-client';\n\nimport { getChannelIdFromLocale } from '../channels.config';\nimport { backendUserAgent } from '../user-agent';\n\n// next/headers, next/navigation, and next-intl/server are imported dynamically\n// (via `import()`) rather than statically. Static imports cause these modules to\n// be evaluated during module graph resolution when next.config.ts imports this\n// file, which poisons the process-wide AsyncLocalStorage context (pnpm symlinks\n// create two separate singleton instances of next/headers). Dynamic imports\n// defer module loading to call time, after Next.js has fully initialized.\n//\n// During config resolution, the dynamic import of next-intl/server succeeds but\n// getLocale() throws (\"not supported in Client Components\") — the try/catch\n// below absorbs this gracefully, and getChannelId falls back to defaultChannelId.\n\nconst getLocale = async () => {\n  try {\n    const { getLocale: getServerLocale } = await import('next-intl/server');\n\n    return await getServerLocale();\n  } catch {\n    /**\n     * Next-intl `getLocale` only works on the server, and when the proxy has run.\n     *\n     * Instances when `getLocale` will not work:\n     * - Requests during next.config.ts resolution\n     * - Requests in proxies\n     * - Requests in `generateStaticParams`\n     * - Request in api routes\n     * - Requests in static sites without `setRequestLocale`\n     */\n  }\n};\n\nexport const client = createClient({\n  storefrontToken: process.env.BIGCOMMERCE_STOREFRONT_TOKEN ?? '',\n  storeHash: process.env.BIGCOMMERCE_STORE_HASH ?? '',\n  channelId: process.env.BIGCOMMERCE_CHANNEL_ID,\n  backendUserAgentExtensions: backendUserAgent,\n  logger:\n    (process.env.NODE_ENV !== 'production' && process.env.CLIENT_LOGGER !== 'false') ||\n    process.env.CLIENT_LOGGER === 'true',\n  getChannelId: async (defaultChannelId: string) => {\n    const locale = await getLocale();\n\n    // We use the default channelId as a fallback, but it is not ideal in some scenarios.\n    return getChannelIdFromLocale(locale) ?? defaultChannelId;\n  },\n  beforeRequest: async (fetchOptions) => {\n    // We can't serialize a `Headers` object within this method so we have to opt into using a plain object\n    const requestHeaders: Record<string, string> = {};\n    const locale = await getLocale();\n\n    try {\n      const { getCorrelationId } = await import('./correlation-id');\n\n      requestHeaders['X-Correlation-ID'] = getCorrelationId();\n    } catch {\n      // correlation-id imports React.cache which is unavailable during next.config.ts resolution\n    }\n\n    if (fetchOptions?.cache && ['no-store', 'no-cache'].includes(fetchOptions.cache)) {\n      const { headers } = await import('next/headers');\n      const ipAddress = (await headers()).get('X-Forwarded-For');\n\n      if (ipAddress) {\n        requestHeaders['X-Forwarded-For'] = ipAddress;\n        requestHeaders['True-Client-IP'] = ipAddress;\n      }\n    }\n\n    if (locale) {\n      requestHeaders['Accept-Language'] = locale;\n    }\n\n    return {\n      headers: requestHeaders,\n    };\n  },\n  onError: async (error, queryType) => {\n    if (error instanceof BigCommerceAuthError && queryType === 'query') {\n      const { redirect } = await import('next/navigation');\n\n      redirect('/api/auth/signout');\n    }\n  },\n});\n"
  },
  {
    "path": "core/client/revalidate-target.ts",
    "content": "export const revalidate = process.env.DEFAULT_REVALIDATE_TARGET\n  ? Number(process.env.DEFAULT_REVALIDATE_TARGET)\n  : 3600;\n"
  },
  {
    "path": "core/client/tags.ts",
    "content": "export const TAGS = {\n  cart: 'cart',\n  checkout: 'checkout',\n  customer: 'customer',\n} as const;\n"
  },
  {
    "path": "core/client/util/index.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ExistingResultType<T extends (...args: any) => any> = NonNullable<\n  Awaited<ReturnType<T>>\n>;\n"
  },
  {
    "path": "core/components/analytics/events.tsx",
    "content": "import { createContext, PropsWithChildren, useContext } from 'react';\n\ninterface EventsContext {\n  onAddToCart?: (formData?: FormData) => void;\n  onRemoveFromCart?: (formData?: FormData) => void;\n}\n\nconst EventsContext = createContext<EventsContext>({\n  onAddToCart: undefined,\n  onRemoveFromCart: undefined,\n});\n\nexport const EventsProvider = ({ children, ...props }: PropsWithChildren<EventsContext>) => {\n  return <EventsContext.Provider value={props}>{children}</EventsContext.Provider>;\n};\n\nexport const useEvents = () => {\n  const context = useContext(EventsContext);\n\n  return context;\n};\n"
  },
  {
    "path": "core/components/analytics/fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nexport const WebAnalyticsFragment = graphql(`\n  fragment WebAnalyticsFragment on Settings {\n    webAnalytics {\n      ga4 {\n        tagId\n      }\n    }\n  }\n`);\n"
  },
  {
    "path": "core/components/analytics/provider.tsx",
    "content": "'use client';\n\nimport { useConsentManager } from '@c15t/nextjs/client';\nimport { PropsWithChildren, useEffect, useRef } from 'react';\n\nimport { FragmentOf } from '~/client/graphql';\nimport { Analytics } from '~/lib/analytics';\nimport { GoogleAnalyticsProvider } from '~/lib/analytics/providers/google-analytics';\nimport { AnalyticsProvider as AnalyticsProviderLib } from '~/lib/analytics/react';\nimport { getConsentCookie } from '~/lib/consent-manager/cookies/client';\n\nimport { WebAnalyticsFragment } from './fragment';\n\ninterface Props {\n  channelId: number;\n  isCookieConsentEnabled: boolean;\n  settings?: FragmentOf<typeof WebAnalyticsFragment> | null;\n}\n\nconst getConsent = () => {\n  const consentCookie = getConsentCookie();\n\n  if (!consentCookie) {\n    return null;\n  }\n\n  return {\n    functionality: consentCookie['c.functionality'],\n    marketing: consentCookie['c.marketing'],\n    measurement: consentCookie['c.measurement'],\n    necessary: consentCookie['c.necessary'],\n  };\n};\n\nconst getAnalytics = ({ channelId, isCookieConsentEnabled, settings }: Props): Analytics | null => {\n  if (settings?.webAnalytics?.ga4?.tagId && channelId) {\n    const googleAnalytics = new GoogleAnalyticsProvider({\n      gaId: settings.webAnalytics.ga4.tagId,\n      consentModeEnabled: isCookieConsentEnabled,\n      developerId: 'dMjk3Nj',\n      getConsent,\n    });\n\n    return new Analytics({\n      channelId,\n      providers: [googleAnalytics],\n    });\n  }\n\n  return null;\n};\n\nexport function AnalyticsProvider({\n  channelId,\n  isCookieConsentEnabled,\n  settings,\n  children,\n}: PropsWithChildren<Props>) {\n  const { consents } = useConsentManager();\n  const prevConsentsRef = useRef<Record<string, boolean> | null>(null);\n\n  const analytics = getAnalytics({\n    channelId,\n    isCookieConsentEnabled,\n    settings,\n  });\n\n  // Update consent when user changes preferences\n  useEffect(() => {\n    if (!isCookieConsentEnabled || !analytics) {\n      return;\n    }\n\n    const currentConsents = consents;\n    const prevConsents = prevConsentsRef.current;\n\n    // Check if consents have changed\n    if (prevConsents && JSON.stringify(currentConsents) !== JSON.stringify(prevConsents)) {\n      const consentState = getConsent();\n\n      if (consentState) {\n        analytics.consent.consentUpdated(consentState);\n      }\n    }\n\n    prevConsentsRef.current = currentConsents;\n  }, [isCookieConsentEnabled, analytics, consents]);\n\n  return <AnalyticsProviderLib analytics={analytics ?? null}>{children}</AnalyticsProviderLib>;\n}\n"
  },
  {
    "path": "core/components/breadcrumbs/fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nexport const BreadcrumbsCategoryFragment = graphql(`\n  fragment BreadcrumbsFragment on Category {\n    breadcrumbs(depth: 5) {\n      edges {\n        node {\n          name\n          path\n          entityId\n        }\n      }\n    }\n  }\n`);\n\nexport const BreadcrumbsWebPageFragment = graphql(`\n  fragment BreadcrumbsFragment on WebPage {\n    breadcrumbs(depth: 8) {\n      edges {\n        node {\n          name\n          path\n        }\n      }\n    }\n  }\n`);\n"
  },
  {
    "path": "core/components/consent-manager/consent-manager-dialog.tsx",
    "content": "'use client';\n\nimport {\n  ConsentManagerDialog as C15TConsentManagerDialog,\n  ConsentManagerWidget as C15TConsentManagerWidget,\n  ConsentManagerDialogProps,\n  ConsentManagerWidgetProps,\n  useConsentManager,\n} from '@c15t/nextjs/client';\nimport { useTranslations } from 'next-intl';\nimport { useCallback } from 'react';\n\nimport { Checkbox } from '@/vibes/soul/form/checkbox';\nimport { Button } from '@/vibes/soul/primitives/button';\n\nfunction ConsentManagerDialogHeaderTitle() {\n  const t = useTranslations('Components.ConsentManager.Dialog');\n\n  return (\n    <C15TConsentManagerDialog.HeaderTitle asChild>\n      <div className=\"font-heading !text-2xl !tracking-normal\">{t('title')}</div>\n    </C15TConsentManagerDialog.HeaderTitle>\n  );\n}\n\nfunction ConsentManagerDialogHeaderDescription() {\n  const t = useTranslations('Components.ConsentManager.Dialog');\n\n  return (\n    <C15TConsentManagerDialog.HeaderDescription asChild>\n      <div className=\"font-body\">{t('description')}</div>\n    </C15TConsentManagerDialog.HeaderDescription>\n  );\n}\n\nexport function ConsentManagerDialog(props: ConsentManagerDialogProps) {\n  return (\n    <C15TConsentManagerDialog.Root {...props}>\n      <C15TConsentManagerDialog.Card>\n        <C15TConsentManagerDialog.Header>\n          <ConsentManagerDialogHeaderTitle />\n          <ConsentManagerDialogHeaderDescription />\n        </C15TConsentManagerDialog.Header>\n        <C15TConsentManagerDialog.Content>\n          <ConsentManagerWidget />\n        </C15TConsentManagerDialog.Content>\n      </C15TConsentManagerDialog.Card>\n    </C15TConsentManagerDialog.Root>\n  );\n}\n\nfunction ConsentManagerWidgetRejectButton() {\n  const t = useTranslations('Components.ConsentManager.Common');\n\n  return (\n    <C15TConsentManagerWidget.RejectButton asChild noStyle themeKey=\"widget.footer.reject-button\">\n      <Button size=\"small\" variant=\"tertiary\">\n        {t('rejectAll')}\n      </Button>\n    </C15TConsentManagerWidget.RejectButton>\n  );\n}\n\nfunction ConsentManagerWidgetAcceptAllButton() {\n  const t = useTranslations('Components.ConsentManager.Common');\n\n  return (\n    <C15TConsentManagerWidget.AcceptAllButton\n      asChild\n      noStyle\n      themeKey=\"widget.footer.accept-button\"\n    >\n      <Button size=\"small\" variant=\"primary\">\n        {t('acceptAll')}\n      </Button>\n    </C15TConsentManagerWidget.AcceptAllButton>\n  );\n}\n\nfunction ConsentManagerWidgetSaveButton() {\n  const t = useTranslations('Components.ConsentManager.Common');\n\n  return (\n    <C15TConsentManagerWidget.SaveButton asChild noStyle themeKey=\"widget.footer.save-button\">\n      <Button size=\"small\" variant=\"secondary\">\n        {t('save')}\n      </Button>\n    </C15TConsentManagerWidget.SaveButton>\n  );\n}\n\nfunction ConsentManagerAccordionItems() {\n  const { selectedConsents, setSelectedConsent, getDisplayedConsents } = useConsentManager();\n  const t = useTranslations('Components.ConsentManager.ConsentTypes');\n  const handleConsentChange = useCallback(\n    (\n      name: 'necessary' | 'functionality' | 'marketing' | 'measurement' | 'experience',\n      checked: boolean,\n    ) => {\n      setSelectedConsent(name, checked);\n    },\n    [setSelectedConsent],\n  );\n\n  return getDisplayedConsents().map((consent) => (\n    <C15TConsentManagerWidget.AccordionItem\n      key={consent.name}\n      themeKey=\"widget.accordion.item\"\n      value={consent.name}\n    >\n      <C15TConsentManagerWidget.AccordionTrigger themeKey=\"widget.accordion.trigger\">\n        <C15TConsentManagerWidget.AccordionTriggerInner themeKey=\"widget.accordion.trigger-inner\">\n          <C15TConsentManagerWidget.AccordionArrow />\n          {t(`${consent.name}.title`)}\n        </C15TConsentManagerWidget.AccordionTriggerInner>\n\n        <Checkbox\n          checked={selectedConsents[consent.name]}\n          disabled={consent.disabled}\n          onCheckedChange={(checked: boolean) => handleConsentChange(consent.name, checked)}\n          onClick={(e: React.MouseEvent<HTMLButtonElement>) => e.stopPropagation()}\n          onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) => e.stopPropagation()}\n          onKeyUp={(e: React.KeyboardEvent<HTMLButtonElement>) => e.stopPropagation()}\n        />\n      </C15TConsentManagerWidget.AccordionTrigger>\n      <C15TConsentManagerWidget.AccordionContent\n        theme={{\n          content: { themeKey: 'widget.accordion.content' },\n          contentInner: { themeKey: 'widget.accordion.content-inner' },\n        }}\n      >\n        {t(`${consent.name}.description`)}\n      </C15TConsentManagerWidget.AccordionContent>\n    </C15TConsentManagerWidget.AccordionItem>\n  ));\n}\n\nfunction ConsentManagerWidget(props: ConsentManagerWidgetProps) {\n  return (\n    <C15TConsentManagerWidget.Root {...props}>\n      <C15TConsentManagerWidget.Accordion themeKey=\"widget.accordion\" type=\"multiple\">\n        <ConsentManagerAccordionItems />\n      </C15TConsentManagerWidget.Accordion>\n      <C15TConsentManagerWidget.Footer>\n        <C15TConsentManagerWidget.FooterSubGroup themeKey=\"widget.footer.sub-group\">\n          <ConsentManagerWidgetRejectButton />\n          <ConsentManagerWidgetAcceptAllButton />\n        </C15TConsentManagerWidget.FooterSubGroup>\n        <ConsentManagerWidgetSaveButton />\n      </C15TConsentManagerWidget.Footer>\n    </C15TConsentManagerWidget.Root>\n  );\n}\n"
  },
  {
    "path": "core/components/consent-manager/consent-providers.tsx",
    "content": "import { ConsentManagerProvider as C15TConsentManagerProvider } from '@c15t/nextjs';\nimport { ClientSideOptionsProvider } from '@c15t/nextjs/client';\nimport type { ComponentProps, PropsWithChildren } from 'react';\n\nimport { CONSENT_COOKIE_NAME } from '~/lib/consent-manager/cookies/constants';\n\nexport type C15tScripts = NonNullable<ComponentProps<typeof ClientSideOptionsProvider>['scripts']>;\n\ninterface ConsentManagerProviderProps extends PropsWithChildren {\n  scripts: C15tScripts;\n  isCookieConsentEnabled: boolean;\n  privacyPolicyUrl?: string | null;\n}\n\nexport function ConsentManagerProvider({\n  children,\n  scripts,\n  isCookieConsentEnabled,\n}: ConsentManagerProviderProps) {\n  return (\n    <C15TConsentManagerProvider\n      options={{\n        mode: 'offline',\n        storageConfig: {\n          storageKey: CONSENT_COOKIE_NAME,\n          crossSubdomain: true,\n        },\n        consentCategories: ['necessary', 'functionality', 'marketing', 'measurement'],\n        enabled: isCookieConsentEnabled,\n      }}\n    >\n      <ClientSideOptionsProvider scripts={scripts}>{children}</ClientSideOptionsProvider>\n    </C15TConsentManagerProvider>\n  );\n}\n"
  },
  {
    "path": "core/components/consent-manager/cookie-banner.tsx",
    "content": "'use client';\n\nimport { CookieBanner as C15TCookieBanner, CookieBannerProps } from '@c15t/nextjs/client';\nimport { useTranslations } from 'next-intl';\nimport { PropsWithChildren } from 'react';\n\nimport { Button } from '@/vibes/soul/primitives/button';\n\nimport { Link } from '../link';\n\nfunction CookieBannerTitle() {\n  const t = useTranslations('Components.ConsentManager.CookieBanner');\n\n  return (\n    <C15TCookieBanner.Title asChild>\n      <div className=\"font-heading !text-xl\">{t('title')}</div>\n    </C15TCookieBanner.Title>\n  );\n}\n\nfunction CookieBannerDescription({ privacyPolicyUrl }: { privacyPolicyUrl?: string | null }) {\n  const t = useTranslations('Components.ConsentManager.CookieBanner');\n\n  return (\n    <C15TCookieBanner.Description asChild>\n      <div className=\"prose font-body\">\n        {t('description')}\n        {typeof privacyPolicyUrl === 'string' && (\n          <>\n            {' '}\n            <Link\n              className=\"rounded-lg ring-primary ring-offset-4 focus:outline-0 focus-visible:ring-2\"\n              href={privacyPolicyUrl}\n              target=\"_blank\"\n            >\n              {t('privacyPolicy')}\n            </Link>\n          </>\n        )}\n      </div>\n    </C15TCookieBanner.Description>\n  );\n}\n\nfunction CookieBannerFooter({ children }: PropsWithChildren) {\n  return (\n    <C15TCookieBanner.Footer asChild>\n      <div className=\"!border-none !bg-transparent !pt-0\">{children}</div>\n    </C15TCookieBanner.Footer>\n  );\n}\n\nfunction CookieBannerRejectButton() {\n  const t = useTranslations('Components.ConsentManager.Common');\n\n  return (\n    <C15TCookieBanner.RejectButton asChild noStyle themeKey=\"banner.footer.reject-button\">\n      <Button size=\"small\" variant=\"tertiary\">\n        {t('rejectAll')}\n      </Button>\n    </C15TCookieBanner.RejectButton>\n  );\n}\n\nfunction CookieBannerAcceptButton() {\n  const t = useTranslations('Components.ConsentManager.Common');\n\n  return (\n    <C15TCookieBanner.AcceptButton asChild noStyle themeKey=\"banner.footer.accept-button\">\n      <Button size=\"small\" variant=\"primary\">\n        {t('acceptAll')}\n      </Button>\n    </C15TCookieBanner.AcceptButton>\n  );\n}\n\nfunction CookieBannerCustomizeButton() {\n  const t = useTranslations('Components.ConsentManager.Common');\n\n  return (\n    <C15TCookieBanner.CustomizeButton asChild noStyle themeKey=\"banner.footer.customize-button\">\n      <Button size=\"small\" variant=\"secondary\">\n        {t('customize')}\n      </Button>\n    </C15TCookieBanner.CustomizeButton>\n  );\n}\n\nexport function CookieBanner({\n  theme,\n  noStyle,\n  disableAnimation,\n  scrollLock,\n  trapFocus,\n  privacyPolicyUrl,\n}: CookieBannerProps & { privacyPolicyUrl?: string | null }) {\n  return (\n    <C15TCookieBanner.Root\n      disableAnimation={disableAnimation}\n      noStyle={noStyle}\n      scrollLock={scrollLock}\n      theme={theme}\n      trapFocus={trapFocus}\n    >\n      <C15TCookieBanner.Card className=\"!max-w-lg\">\n        <C15TCookieBanner.Header>\n          <CookieBannerTitle />\n          <CookieBannerDescription privacyPolicyUrl={privacyPolicyUrl} />\n        </C15TCookieBanner.Header>\n        <CookieBannerFooter>\n          <C15TCookieBanner.FooterSubGroup>\n            <CookieBannerRejectButton />\n            <CookieBannerAcceptButton />\n          </C15TCookieBanner.FooterSubGroup>\n          <CookieBannerCustomizeButton />\n        </CookieBannerFooter>\n      </C15TCookieBanner.Card>\n    </C15TCookieBanner.Root>\n  );\n}\n"
  },
  {
    "path": "core/components/consent-manager/index.tsx",
    "content": "import type { PropsWithChildren } from 'react';\n\nimport { ConsentManagerDialog } from './consent-manager-dialog';\nimport { type C15tScripts, ConsentManagerProvider } from './consent-providers';\nimport { CookieBanner } from './cookie-banner';\n\ninterface ConsentManagerProps extends PropsWithChildren {\n  scripts: C15tScripts;\n  isCookieConsentEnabled: boolean;\n  privacyPolicyUrl?: string | null;\n}\n\nexport function ConsentManager({\n  children,\n  scripts,\n  isCookieConsentEnabled,\n  privacyPolicyUrl,\n}: ConsentManagerProps) {\n  return (\n    <ConsentManagerProvider isCookieConsentEnabled={isCookieConsentEnabled} scripts={scripts}>\n      <ConsentManagerDialog />\n      <CookieBanner privacyPolicyUrl={privacyPolicyUrl} />\n      {children}\n    </ConsentManagerProvider>\n  );\n}\n"
  },
  {
    "path": "core/components/consent-manager/scripts-fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nexport const ScriptsFragment = graphql(`\n  fragment ScriptsFragment on Content {\n    scripts(first: 50, filters: { visibilities: [ALL_PAGES, STOREFRONT] }) {\n      edges {\n        node {\n          __typename\n          integrityHashes {\n            hash\n          }\n          entityId\n          consentCategory\n          visibility\n          ... on InlineScript {\n            scriptTag\n          }\n          ... on SrcScript {\n            src\n          }\n          location\n        }\n      }\n    }\n  }\n`);\n"
  },
  {
    "path": "core/components/featured-products-carousel/fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nimport { ProductCardFragment } from '../product-card/fragment';\n\nexport const FeaturedProductsCarouselFragment = graphql(\n  `\n    fragment FeaturedProductsCarouselFragment on Product {\n      ...ProductCardFragment\n    }\n  `,\n  [ProductCardFragment],\n);\n"
  },
  {
    "path": "core/components/featured-products-list/fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nimport { ProductCardFragment } from '../product-card/fragment';\n\nexport const FeaturedProductsListFragment = graphql(\n  `\n    fragment FeaturedProductsListFragment on Product {\n      ...ProductCardFragment\n    }\n  `,\n  [ProductCardFragment],\n);\n"
  },
  {
    "path": "core/components/footer/fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nexport const FooterFragment = graphql(`\n  fragment FooterFragment on Site {\n    settings {\n      storeName\n      contact {\n        address\n        phone\n      }\n      socialMediaLinks {\n        name\n        url\n      }\n      logoV2 {\n        __typename\n        ... on StoreTextLogo {\n          text\n        }\n        ... on StoreImageLogo {\n          image {\n            url: urlTemplate(lossy: true)\n            altText\n          }\n        }\n      }\n    }\n  }\n`);\n\nexport const FooterSectionsFragment = graphql(`\n  fragment FooterSectionsFragment on Site {\n    settings {\n      giftCertificates(currencyCode: $currencyCode) {\n        currencyCode\n        isEnabled\n      }\n    }\n    content {\n      pages(filters: { parentEntityIds: [0] }) {\n        edges {\n          node {\n            __typename\n            name\n            ... on RawHtmlPage {\n              path\n            }\n            ... on ContactPage {\n              path\n            }\n            ... on NormalPage {\n              path\n            }\n            ... on BlogIndexPage {\n              path\n            }\n            ... on ExternalLinkPage {\n              link\n            }\n          }\n        }\n      }\n    }\n    brands(first: 5) {\n      edges {\n        node {\n          entityId\n          name\n          path\n        }\n      }\n    }\n    categoryTree {\n      name\n      path\n    }\n  }\n`);\n"
  },
  {
    "path": "core/components/footer/index.tsx",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport {\n  SiFacebook,\n  SiInstagram,\n  SiPinterest,\n  SiX,\n  SiYoutube,\n} from '@icons-pack/react-simple-icons';\nimport { getTranslations } from 'next-intl/server';\nimport { cache, JSX } from 'react';\n\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { Footer as FooterSection } from '@/vibes/soul/sections/footer';\nimport { GetLinksAndSectionsQuery, LayoutQuery } from '~/app/[locale]/(default)/page-data';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { readFragment } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { CurrencyCode } from '~/components/header/fragment';\nimport { logoTransformer } from '~/data-transformers/logo-transformer';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\n\nimport { FooterFragment, FooterSectionsFragment } from './fragment';\nimport { AmazonIcon } from './payment-icons/amazon';\nimport { AmericanExpressIcon } from './payment-icons/american-express';\nimport { ApplePayIcon } from './payment-icons/apple-pay';\nimport { MastercardIcon } from './payment-icons/mastercard';\nimport { PayPalIcon } from './payment-icons/paypal';\nimport { VisaIcon } from './payment-icons/visa';\n\nconst paymentIcons = [\n  <AmazonIcon key=\"amazon\" />,\n  <AmericanExpressIcon key=\"americanExpress\" />,\n  <ApplePayIcon key=\"apple\" />,\n  <MastercardIcon key=\"mastercard\" />,\n  <PayPalIcon key=\"paypal\" />,\n  <VisaIcon key=\"visa\" />,\n];\n\nconst socialIcons: Record<string, { icon: JSX.Element }> = {\n  Facebook: { icon: <SiFacebook title=\"Facebook\" /> },\n  Twitter: { icon: <SiX title=\"Twitter\" /> },\n  X: { icon: <SiX title=\"X\" /> },\n  Pinterest: { icon: <SiPinterest title=\"Pinterest\" /> },\n  Instagram: { icon: <SiInstagram title=\"Instagram\" /> },\n  YouTube: { icon: <SiYoutube title=\"YouTube\" /> },\n};\n\nconst getFooterSections = cache(\n  async (customerAccessToken?: string, currencyCode?: CurrencyCode) => {\n    const { data: response } = await client.fetch({\n      document: GetLinksAndSectionsQuery,\n      customerAccessToken,\n      variables: { currencyCode },\n      // Since this query is needed on every page, it's a good idea not to validate the customer access token.\n      // The 'cache' function also caches errors, so we might get caught in a redirect loop if the cache saves an invalid token error response.\n      validateCustomerAccessToken: false,\n      fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },\n    });\n\n    return readFragment(FooterSectionsFragment, response).site;\n  },\n);\n\nconst getFooterData = cache(async () => {\n  const { data: response } = await client.fetch({\n    document: LayoutQuery,\n    fetchOptions: { next: { revalidate } },\n  });\n\n  return readFragment(FooterFragment, response).site;\n});\n\nexport const Footer = async () => {\n  const t = await getTranslations('Components.Footer');\n  const data = await getFooterData();\n\n  const logo = data.settings ? logoTransformer(data.settings) : '';\n\n  const copyright = `© ${new Date().getFullYear()} ${data.settings?.storeName} – Powered by BigCommerce`;\n\n  const contactInformation = data.settings?.contact\n    ? {\n        address: data.settings.contact.address,\n        phone: data.settings.contact.phone,\n      }\n    : undefined;\n\n  const socialMediaLinks = data.settings?.socialMediaLinks\n    .filter((socialMediaLink) => Boolean(socialIcons[socialMediaLink.name]))\n    .map((socialMediaLink) => ({\n      href: socialMediaLink.url,\n      icon: socialIcons[socialMediaLink.name]?.icon,\n    }));\n\n  const streamableSections = Streamable.from(async () => {\n    const customerAccessToken = await getSessionCustomerAccessToken();\n    const currencyCode = await getPreferredCurrencyCode();\n    const sectionsData = await getFooterSections(customerAccessToken, currencyCode);\n\n    return [\n      {\n        title: t('categories'),\n        links: sectionsData.categoryTree.map((category) => ({\n          label: category.name,\n          href: category.path,\n        })),\n      },\n      {\n        title: t('brands'),\n        links: removeEdgesAndNodes(sectionsData.brands).map((brand) => ({\n          label: brand.name,\n          href: brand.path,\n        })),\n      },\n      {\n        title: t('navigate'),\n        links: [\n          ...(sectionsData.settings?.giftCertificates?.isEnabled\n            ? [\n                {\n                  label: t('giftCertificates'),\n                  href: '/gift-certificates',\n                },\n              ]\n            : []),\n          ...removeEdgesAndNodes(sectionsData.content.pages).map((page) => ({\n            label: page.name,\n            href: page.__typename === 'ExternalLinkPage' ? page.link : page.path,\n          })),\n        ],\n      },\n    ];\n  });\n\n  return (\n    <FooterSection\n      contactInformation={contactInformation}\n      contactTitle={t('contactUs')}\n      copyright={copyright}\n      logo={logo}\n      logoHref=\"/\"\n      logoLabel={t('home')}\n      paymentIcons={paymentIcons}\n      sections={streamableSections}\n      socialMediaLinks={socialMediaLinks}\n    />\n  );\n};\n"
  },
  {
    "path": "core/components/footer/payment-icons/amazon.tsx",
    "content": "import React from 'react';\n\nexport const AmazonIcon: React.FC<React.ComponentPropsWithoutRef<'svg'>> = ({ ...props }) => (\n  <svg\n    aria-label=\"Amazon\"\n    fill=\"currentColor\"\n    height=\"24\"\n    role=\"img\"\n    viewBox=\"0 0 24 24\"\n    width=\"24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <path d=\"M2 4a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H2zM0 5a2 2 0 0 1 2-2h20a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V5z\" />\n    <path d=\"M14.173 14.328c-1.046.772-2.563 1.183-3.87 1.183-1.83 0-3.48-.677-4.727-1.804-.098-.088-.01-.209.107-.14 1.347.783 3.011 1.254 4.73 1.254 1.16 0 2.435-.24 3.608-.738.177-.075.325.118.152.245z\" />\n    <path\n      clipRule=\"evenodd\"\n      d=\"M14.609 13.83c-.134-.17-.885-.08-1.223-.04-.101.012-.118-.077-.025-.141.599-.421 1.58-.3 1.694-.16.115.142-.03 1.127-.591 1.596-.087.073-.169.034-.131-.061.127-.315.41-1.022.276-1.193zm-1.199-3.154v-.409a.1.1 0 0 1 .104-.103h1.832c.059 0 .106.042.106.103v.351c0 .058-.05.134-.139.257l-.948 1.355c.352-.008.725.044 1.045.224.072.04.092.1.097.16v.436c0 .06-.066.13-.134.094a2.107 2.107 0 0 0-1.937.004c-.064.033-.13-.035-.13-.095v-.416c0-.066.002-.179.068-.28l1.1-1.578h-.958a.102.102 0 0 1-.106-.103zM6.727 13.23H6.17a.104.104 0 0 1-.1-.093v-2.861c0-.058.049-.104.108-.104h.519a.106.106 0 0 1 .101.096v.374h.01c.136-.361.39-.53.735-.53.348 0 .566.168.723.53a.79.79 0 0 1 .769-.53c.234 0 .49.096.646.313.176.24.14.59.14.897v1.805a.106.106 0 0 1-.108.103h-.556a.104.104 0 0 1-.1-.102V11.61c0-.12.01-.421-.016-.536-.042-.193-.167-.247-.328-.247a.37.37 0 0 0-.333.235c-.057.144-.052.385-.052.548v1.516a.105.105 0 0 1-.107.103h-.557a.104.104 0 0 1-.1-.102V11.61c0-.319.051-.789-.345-.789-.4 0-.385.457-.385.79v1.515a.105.105 0 0 1-.107.103zm10.302-3.118c.827 0 1.275.71 1.275 1.613 0 .873-.494 1.566-1.275 1.566-.812 0-1.254-.711-1.254-1.595 0-.892.448-1.584 1.254-1.584zm.005.584c-.41 0-.437.56-.437.909 0 .35-.005 1.096.432 1.096.432 0 .453-.602.453-.97 0-.24-.01-.53-.083-.759-.063-.198-.188-.276-.365-.276zm2.343 2.535h-.556a.105.105 0 0 1-.099-.104l-.001-2.861a.106.106 0 0 1 .107-.094h.517c.048.002.09.035.099.08v.438h.01c.157-.391.375-.578.76-.578.25 0 .495.09.65.337.146.23.146.614.146.89v1.802a.106.106 0 0 1-.107.09h-.558a.105.105 0 0 1-.1-.09v-1.554c0-.313.036-.771-.349-.771-.135 0-.26.09-.322.229-.078.174-.089.35-.089.542v1.54a.107.107 0 0 1-.108.104zm-7.427-1.367c0 .217.005.399-.104.591-.09.157-.23.254-.387.254-.213 0-.339-.164-.339-.404 0-.476.426-.562.83-.562v.121zm.562 1.359a.115.115 0 0 1-.132.013c-.185-.154-.218-.224-.32-.371-.306.312-.523.405-.918.405-.47 0-.835-.29-.835-.869 0-.453.245-.76.595-.911.302-.133.724-.157 1.048-.194v-.072c0-.133.01-.289-.068-.403-.068-.103-.198-.146-.313-.146-.212 0-.401.11-.448.336-.01.05-.046.099-.097.102l-.54-.059c-.046-.01-.096-.046-.083-.116.124-.656.716-.854 1.246-.854.271 0 .626.073.84.277.27.254.245.591.245.96v.868c0 .262.109.376.21.518.036.05.044.11-.001.147-.115.096-.316.272-.428.37l-.001-.001zm-7.87-1.359c0 .217.005.399-.104.591-.089.157-.23.254-.386.254-.214 0-.339-.164-.339-.404 0-.476.427-.562.83-.562v.121zm.562 1.359a.114.114 0 0 1-.131.013c-.185-.154-.218-.224-.32-.371-.306.312-.523.405-.92.405-.469 0-.833-.29-.833-.869 0-.453.245-.76.595-.911.302-.133.724-.157 1.047-.194v-.072c0-.133.011-.289-.067-.403-.068-.103-.199-.146-.313-.146-.213 0-.402.11-.448.336-.01.05-.047.099-.097.102l-.54-.059c-.046-.01-.097-.046-.084-.116.124-.656.716-.854 1.247-.854.271 0 .626.073.84.277.27.254.244.591.244.96v.868c0 .262.11.376.211.518.035.05.043.11-.002.147a24.13 24.13 0 0 0-.428.37l-.001-.001z\"\n      fillRule=\"evenodd\"\n    />\n  </svg>\n);\n"
  },
  {
    "path": "core/components/footer/payment-icons/american-express.tsx",
    "content": "import React from 'react';\n\nexport const AmericanExpressIcon: React.FC<React.ComponentPropsWithoutRef<'svg'>> = ({\n  ...props\n}) => (\n  <svg\n    aria-label=\"American Express\"\n    fill=\"currentColor\"\n    height=\"24\"\n    role=\"img\"\n    viewBox=\"0 0 24 24\"\n    width=\"24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <path d=\"m20.0538 8.5h-.5478c-.2046 0-.3708.19392-.3708.43278v.2706l-.2547-.49383c-.0668-.12993-.1866-.20906-.3165-.20906h-.8155c-.2046 0-.3703.19392-.3703.43277v.36781l-.1967-.53828c-.0589-.15875-.1921-.2623-.3399-.2623h-.8021c-.0818 0-.1591.03126-.2213.08597-.0613-.05422-.1386-.08597-.2209-.08597h-.5181c-.4393 0-.7658.12505-1.0055.36781-.0271-.20662-.1741-.36781-.3591-.36781h-.5465c-.1458 0-.2694.10111-.3295.24374-.0229-.01954-.0413-.0425-.0668-.06057-.2518-.16998-.5152-.18317-.8338-.18317h-1.2318c-.0693 0-.1303.02833-.1858.06692-.0547-.03859-.1161-.06692-.1854-.06692h-1.78876c-.07557 0-.14113.03273-.19958.07766-.05888-.04493-.12443-.07766-.19959-.07766h-.89272c-.14781 0-.28226.10404-.33988.26426l-.31817.87727-.35032-.88997c-.06096-.15289-.19249-.25156-.33612-.25156h-.87476c-.2046 0-.36995.19392-.36995.43277v.37661l-.20084-.54805c-.05887-.15875-.19207-.26182-.3403-.26182h-.79501c-.14823 0-.28226.10355-.3403.2623l-.95868 2.6216c-.04927.1333-.03716.2867.0309.4088.06847.1211.18455.1944.30898.1944h.58206c.14865 0 .28393-.1055.34072-.2667l.11315-.3185h.62047l.11149.3166c.0572.1621.19207.2681.34239.2681h1.08938c.20418 0 .36994-.1939.36994-.4327v-.0103l.07015.1861c.05887.1573.19165.2584.33779.2584h.47058c.14697 0 .27892-.1011.33821-.2579l.07098-.1856v.0107c0 .2389.16535.4323.36995.4323h.54907c.07516 0 .14072-.0332.19959-.0777.05846.0445.12401.0777.19959.0777h1.78879c.0693 0 .1307-.0283.1854-.0669.0559.0386.1165.0669.1858.0669h.5361c.2042 0 .37-.1939.37-.4328v-.5207h.2041c.0464 0 .0794.0015.104.0034.0025.0445.0017.0973.0017.1324l-.0009.0577.0021.3307c.0017.2373.167.4293.3704.4293h.5403c.061 0 .1157-.0215.1662-.0523.0513.0308.1069.0523.1674.0523h.547c.1791 0 .3211-.1514.3557-.3488.3044.2985.7312.3488.9696.3488h.6271c.1499 0 .2861-.1075.3416-.2711l.1094-.3141h.6171l.1132.3185c.0584.1612.1925.2662.3415.2662h1.0902c.2038 0 .37-.1939.37-.4328v-.4137l.3286.6375c.0672.1299.1871.209.3178.209h.7582c.2038 0 .37-.1939.37-.4328v-2.62152c0-.23886-.1654-.43278-.37-.43278zm-9.3484 3.0548v-2.62154h1.2317c.2735 0 .4752.00831.6489.12554.1687.11723.2706.28819.2706.58078 0 .41762-.238.63402-.3775.69852.1173.0522.2176.1446.2652.2208.0759.1304.0889.2466.0889.4806v.5153h-.5403l-.0025-.3307c0-.1577.0129-.3844-.0844-.5104-.078-.0918-.1975-.1119-.3904-.1119h-.5741v.953zm-2.16041 0v-2.62154h1.78881v.5461h-1.25352v.47234h1.22382v.5373h-1.22382v.5236h1.25352v.5422zm10.96101-.7952v-1.82585h.5478v2.62155h-.7582l-1.0164-1.9675v1.9675h-1.0902l-.2087-.5847h-1.1124l-.2012.5847h-.6272c-.261-.0005-.5904-.0679-.7766-.2901-.1888-.2228-.2873-.5246-.2873-1.0009 0-.3888.0589-.7444.2885-1.02526.1725-.20906.4443-.30529.8134-.30529h.5182v.56173h-.5073c-.1967 0-.3065.03419-.4126.15533-.0914.11039-.1536.31848-.1536.59399 0 .2804.0467.4831.1474.6149.0822.1041.2334.1354.3732.1354h.2401l.7566-2.06135h.8021l.9069 2.47945v-2.47945h.8155zm-6.3392.7957v-2.62155h.5466v2.62155zm-6.5563-.8431.64344-1.77845h.89271v2.62155h-.54907l-.00209-2.05298-.77664 2.05298-.47141-.0005-.77788-2.05443v2.05493h-1.0898l-.20585-.5847h-1.11485l-.20794.5847h-.58164l.95911-2.62155h.795l.91151 2.48235v-2.48235h.87434zm8.4298-.1494c-.0517 0-.0864-.0069-.1052-.0127-.0179-.0391-.0426-.1256-.0426-.3053 0-.1817.0376-.276.0426-.2843.0152-.01684.028-.03225.1488-.03225.0019 0 .0038 0 .0057.00001h.1829zm1.3871-1.18357-.3678 1.04477h.739zm-12.71342 0-.3641 1.04477h.73029zm8.18682.10062h-.6597l-.0004.58175h.6509c.104 0 .1908-.0015.2652-.0537.0689-.04252.1102-.13484.1102-.24914 0-.11235-.043-.19343-.1123-.23153-.0635-.04201-.1612-.04738-.2539-.04738zm9.6207 3.53205h-1.0739c-.3345 0-.5854.1148-.7692.2892-.0513-.1676-.1841-.2892-.3457-.2892h-1.0731c-.2948 0-.5286.086-.7048.2257-.0635-.1324-.1804-.2257-.319-.2257h-1.7871c-.1445 0-.2651.0987-.3261.2389-.0259-.0205-.0438-.0455-.0718-.0645-.2585-.1617-.5341-.1744-.8188-.1744h-1.2368c-.1449 0-.2668.0992-.3274.2408-.1874-.1436-.4513-.2408-.8263-.2408h-1.7111c-.1031 0-.20125.0498-.27139.1382l-.40544.512-.38915-.5066c-.06973-.0918-.16953-.1436-.27517-.1436h-2.1537c-.20418 0-.36994.1934-.36994.4333v2.6215c0 .2389.16534.4333.36994.4333h2.12155c.10314 0 .20126-.0508.27141-.1392l.40878-.5178.3879.5109c.07014.0924.17119.1451.27681.1451h1.041c.2046 0 .3703-.1934.3703-.4328v-.4469h.2977c.3616 0 .6239-.0869.8138-.2193v.6662c0 .2389.1654.4328.3704.4328h.5382c.2042 0 .37-.1934.37-.4328v-.525h.2021c.0492 0 .0839.0019.1085.0039.0025.0464.0025.103.0017.1402v.3809c0 .2389.1653.4328.3704.4328h.5373c.0593 0 .1132-.02.1629-.0488.0492.0298.1039.0488.1637.0488h1.787c.0585 0 .1115-.019.1604-.0469.0493.0279.1019.0469.1607.0469h1.0431c.3328 0 .6017-.0947.805-.2633.0568.1544.1875.2633.3403.2633h1.043.0024c.7569 0 1.2081-.4711 1.2081-1.2592 0-.3664-.0835-.6389-.2672-.8597-.0038-.0044-.008-.0088-.0109-.0127.1023-.0772.1691-.2115.1691-.3644v-.5578c0-.2394-.1653-.4333-.3695-.4333zm-1.7733 3.0539v-.5623h1.0384c.1011 0 .1733-.0151.2188-.0649.0359-.0401.0622-.0997.0622-.171 0-.0767-.028-.1363-.0647-.1734-.0418-.04-.0973-.0581-.1908-.0581-.5007-.0191-1.1286.0181-1.1286-.8109 0-.381.2046-.7805.7657-.7805h1.074v.5578h-.9825c-.0977 0-.1612.0049-.2151.0474-.0588.043-.0805.106-.0805.189 0 .0992.0501.1656.1164.1954.0577.023.1182.0298.2088.0298l.2881.0098c.2906.0083.4906.0664.613.209.1052.1261.1607.2863.1607.5554 0 .5642-.301.8275-.8409.8275zm-2.1888 0v-.5623h1.0389c.101 0 .1732-.0151.2183-.0649.0368-.0401.0627-.0997.0627-.171 0-.0767-.0276-.1363-.0652-.1734-.0409-.04-.0968-.0581-.1908-.0581-.5002-.0191-1.1278.0181-1.1278-.8109 0-.381.2038-.7805.7658-.7805h1.0731v.5578h-.9821c-.0981 0-.1612.0049-.2146.0474-.0593.043-.081.106-.081.189 0 .0992.0497.1656.1169.1954.0572.023.1173.0298.2088.0298l.2877.0098c.2906.0083.4902.0664.6133.209.1044.1261.1608.2863.1608.5554 0 .5642-.3019.8275-.8418.8275zm-2.1099.0009v-2.6215h1.7871v.5417h-1.2543v.4772h1.2234v.5349h-1.2234v.5212l1.2543.0029v.5436zm-2.4526 0v-2.6215h1.2363c.271 0 .4681.0132.6451.1231.1683.1192.2748.2833.2748.5822-.0009.4196-.2401.6331-.3804.698.1186.0508.2167.1441.2618.2203.0764.128.0873.2472.0889.4782v.5197h-.5373v-.3277c0-.1578.0129-.3913-.0865-.5134-.0785-.0938-.1975-.1162-.3929-.1162h-.572v.9573zm-3.54792-1.7623.68142-.8587h1.7111.0023c.4245-.0001.9.1389.9.8636 0 .7282-.4643.8787-.9323.8787h-.6681v.8792h-1.041l-.65969-.868-.68561.868h-2.12155v-2.6215h2.1537zm1.16082-.5779-.82261 1.0336.82261 1.0688zm1.2317.2609h-.6981v.6677h.6914.0013c.2055 0 .3328-.1196.3328-.3458 0-.2296-.1336-.3219-.3274-.3219zm2.3521.0005h-.6598v.589h.651c.1044 0 .1933-.0044.2651-.0542.0685-.0488.1098-.1397.1098-.2515 0-.1119-.0413-.1935-.1106-.236-.0651-.0449-.1612-.0473-.2555-.0473zm-5.68118 0h-1.34241v.4767h1.17539v.5353h-1.17539v.5217h1.3161l.61213-.7703z\" />\n    <path d=\"m2 4.5c-.55228 0-1 .44772-1 1v14c0 .5523.44772 1 1 1h20c.5523 0 1-.4477 1-1v-14c0-.55228-.4477-1-1-1zm-2 1c0-1.10457.895431-2 2-2h20c1.1046 0 2 .89543 2 2v14c0 1.1046-.8954 2-2 2h-20c-1.10457 0-2-.8954-2-2z\" />\n  </svg>\n);\n"
  },
  {
    "path": "core/components/footer/payment-icons/apple-pay.tsx",
    "content": "import React from 'react';\n\nexport const ApplePayIcon: React.FC<React.ComponentPropsWithoutRef<'svg'>> = ({ ...props }) => (\n  <svg\n    aria-label=\"Apple Pay\"\n    fill=\"currentColor\"\n    height=\"24\"\n    role=\"img\"\n    viewBox=\"0 0 24 24\"\n    width=\"24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <path d=\"m2 4c-.55228 0-1 .44772-1 1v14c0 .5523.44772 1 1 1h20c.5523 0 1-.4477 1-1v-14c0-.55228-.4477-1-1-1zm-2 1c0-1.10457.895431-2 2-2h20c1.1046 0 2 .89543 2 2v14c0 1.1046-.8954 2-2 2h-20c-1.10457 0-2-.8954-2-2z\" />\n    <path d=\"m12.7914 10.4263c.8849 0 1.5108.6115 1.5108 1.5036s-.6331 1.5108-1.5324 1.5108h-1.0576v1.5611h-.7122v-4.5755zm-1.0792 2.4172h.8921c.6115 0 .9712-.3309.9712-.9064 0-.5756-.3525-.9137-.9712-.9137h-.8921z\" />\n    <path d=\"m14.439 14.0163c0-.6043.4676-.9496 1.3165-1.0072l.9281-.0504v-.2661c0-.3957-.2662-.6116-.7195-.6116-.3741 0-.6474.1943-.705.4892h-.6403c.0216-.6115.5971-1.0575 1.3669-1.0575.8418 0 1.3813.446 1.3813 1.1223v2.3597h-.6547v-.5755h-.0143c-.1871.3741-.6187.6115-1.0864.6115-.6978.0072-1.1726-.4029-1.1726-1.0144zm2.2446-.3094v-.2661l-.8346.0575c-.4676.0288-.7122.2014-.7122.5108 0 .295.259.482.6547.482.5036-.0072.8921-.3309.8921-.7842z\" />\n    <path d=\"m18.1151 16.2323v-.5612c.0503.0144.1583.0144.2158.0144.3238 0 .5108-.1367.6259-.5036l.0504-.1798-1.2518-3.4533h.741l.8849 2.813h.0144l.8849-2.813h.7194l-1.2878 3.6043c-.295.8274-.6187 1.0936-1.3237 1.0936-.0504.0072-.2159-.0072-.2734-.0144z\" />\n    <path d=\"m8.44045 12.25c-.00966-1.0117.82659-1.4993.86433-1.5224-.46959-.6884-1.20221-.78263-1.46312-.79365-.62282-.06253-1.2155.36715-1.53171.36715-.31535 0-.80363-.35762-1.31977-.34745-.6796.00945-1.30562.39455-1.65552 1.00245-.70512 1.2243-.18024 3.0375.5074 4.0308.33561.485.73675 1.0321 1.26276 1.0123.5074-.0199.69845-.3272 1.31039-.3272.61244 0 .78444.3272 1.32112.3171.54449-.0098.89069-.4953 1.22424-.9823.38558-.5641.54435-1.1102.5536-1.1386-.01173-.0042-1.0627-.4077-1.07372-1.6182z\" />\n    <path d=\"m7.43399 9.27776c.27953-.33881.4671-.80896.41571-1.27776-.40235.01598-.8902.26858-1.1784.60633-.25907.29972-.48579.77833-.42431 1.23731.44883.0349.90732-.22772 1.187-.56588z\" />\n  </svg>\n);\n"
  },
  {
    "path": "core/components/footer/payment-icons/mastercard.tsx",
    "content": "import React from 'react';\n\nexport const MastercardIcon: React.FC<React.ComponentPropsWithoutRef<'svg'>> = ({ ...props }) => (\n  <svg\n    aria-label=\"Mastercard\"\n    fill=\"currentColor\"\n    height=\"24\"\n    role=\"img\"\n    viewBox=\"0 0 24 24\"\n    width=\"24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <path d=\"M2 4a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H2zM0 5a2 2 0 0 1 2-2h20a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V5z\" />\n    <path d=\"M6.313 17.963v-.915c0-.345-.22-.577-.574-.58a.565.565 0 0 0-.512.26.536.536 0 0 0-.482-.26.482.482 0 0 0-.428.217v-.18H4v1.458h.32v-.808c0-.254.14-.388.357-.388.211 0 .318.137.318.385v.811h.32v-.808c0-.254.147-.388.357-.388.217 0 .32.137.32.385v.811h.321zm4.743-1.458h-.52v-.443h-.32v.443H9.92v.29h.296v.665c0 .339.132.54.507.54a.745.745 0 0 0 .397-.113l-.092-.271a.586.586 0 0 1-.28.082c-.16 0-.211-.098-.211-.244v-.66h.518v-.29zm2.706-.037a.43.43 0 0 0-.384.214v-.177h-.315v1.458h.318v-.818c0-.24.104-.375.311-.375.064 0 .131.01.198.037l.098-.3a.68.68 0 0 0-.226-.039zm-4.092.153a1.09 1.09 0 0 0-.595-.153c-.37 0-.607.177-.607.467 0 .238.177.385.503.43l.15.022c.174.024.256.07.256.152 0 .113-.116.177-.333.177a.777.777 0 0 1-.485-.152l-.15.247c.175.128.394.189.632.189.422 0 .666-.198.666-.476 0-.256-.193-.39-.51-.437l-.15-.02c-.137-.02-.247-.047-.247-.144 0-.107.104-.171.278-.171a.94.94 0 0 1 .455.125l.137-.256zm8.502-.153a.43.43 0 0 0-.384.214v-.177h-.315v1.458h.318v-.818c0-.24.103-.375.31-.375.065 0 .132.01.2.037l.097-.3a.68.68 0 0 0-.226-.039zm-4.09.766c0 .442.309.766.779.766.22 0 .366-.049.525-.174l-.153-.256a.642.642 0 0 1-.381.13c-.254-.002-.44-.185-.44-.466 0-.28.186-.464.44-.467.137 0 .262.046.381.131l.153-.256a.764.764 0 0 0-.525-.174c-.47 0-.778.323-.778.766zm2.973-.73h-.317v.178a.553.553 0 0 0-.461-.214c-.409 0-.73.32-.73.766 0 .445.321.766.73.766.207 0 .36-.082.46-.214v.177h.318v-1.458zm-1.18.73c0-.256.167-.467.442-.467.262 0 .44.201.44.467 0 .265-.178.467-.44.467-.275 0-.443-.21-.443-.467zm-3.83-.766c-.428 0-.727.311-.727.766 0 .464.311.766.748.766.22 0 .42-.055.598-.204l-.156-.235a.695.695 0 0 1-.424.152c-.205 0-.39-.095-.436-.357h1.083c.003-.04.006-.08.006-.122-.003-.455-.284-.766-.693-.766zm-.007.284c.205 0 .336.128.37.354h-.757c.033-.21.161-.354.387-.354zM20 15.919h-.317v.763a.553.553 0 0 0-.461-.214c-.41 0-.73.32-.73.766 0 .445.32.766.73.766.207 0 .36-.082.46-.214v.177H20V15.92zm-1.181 1.315c0-.256.168-.467.443-.467.262 0 .439.201.439.467 0 .265-.177.467-.44.467-.274 0-.442-.21-.442-.467zm-10.718-.73h-.317v.178a.553.553 0 0 0-.46-.214c-.41 0-.73.32-.73.766 0 .445.32.766.73.766.207 0 .36-.082.46-.214v.177h.317v-1.458zm-1.18.73c0-.256.167-.467.442-.467.262 0 .44.201.44.467 0 .265-.178.467-.44.467-.275 0-.443-.21-.443-.467z\" />\n    <path d=\"M8.75 7a3.75 3.75 0 1 0 0 7.5 3.75 3.75 0 0 0 0-7.5zM4 10.75a4.75 4.75 0 1 1 9.5 0 4.75 4.75 0 0 1-9.5 0z\" />\n    <path d=\"M15.25 7a3.75 3.75 0 1 0 0 7.5 3.75 3.75 0 0 0 0-7.5zm-4.75 3.75a4.75 4.75 0 1 1 9.5 0 4.75 4.75 0 0 1-9.5 0z\" />\n  </svg>\n);\n"
  },
  {
    "path": "core/components/footer/payment-icons/paypal.tsx",
    "content": "import React from 'react';\n\nexport const PayPalIcon: React.FC<React.ComponentPropsWithoutRef<'svg'>> = ({ ...props }) => (\n  <svg\n    aria-label=\"PayPal\"\n    fill=\"currentColor\"\n    height=\"24\"\n    role=\"img\"\n    viewBox=\"0 0 24 24\"\n    width=\"24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <path d=\"m2 4c-.55228 0-1 .44772-1 1v14c0 .5523.44772 1 1 1h20c.5523 0 1-.4477 1-1v-14c0-.55228-.4477-1-1-1zm-2 1c0-1.10457.895431-2 2-2h20c1.1046 0 2 .89543 2 2v14c0 1.1046-.8954 2-2 2h-20c-1.10457 0-2-.8954-2-2z\" />\n    <path d=\"m9.90233 8.88636c.05681-.50752.48757-.88636.99437-.88636h4.085c1.6635 0 3.0183 1.33971 3.0183 3 0 2.2126-1.8058 4-4.0256 4h-.5508l-.4304 2.5822c-.0402.2411-.2488.4178-.4932.4178h-2.4995c-.59238 0-1.06083-.5127-.99428-1.1073l.89611-8.00634.49387.05527zm.99357.11369-.8954 7.99995h2.0759l.4304-2.5822c.0402-.2411.2488-.4178.4932-.4178h.9744c1.6744 0 3.0256-1.3466 3.0256-3 0-1.10113-.9002-2-2.0183-2z\" />\n    <path d=\"m8.3383 7.00204c-.00002.00002.00003-.00002 0 0 .00122-.00104.00304-.00204.00641-.00204h4.62439c1.1275 0 2.0309.90119 2.0309 1.99999 0 1.65111-1.3567 3.00001-3.0432 3.00001h-1.96845c-.27614 0-.5.2239-.5.5s.22386.5.5.5h1.96845c2.2272 0 4.0432-1.7851 4.0432-4.00001 0-1.66261-1.3628-2.99999-3.0309-2.99999h-4.62439c-.49433 0-.9218.35885-.99537.85386l-1.3323 8.96374c-.11624.6207.36647 1.1824.9893 1.1824h2.4196c.27614 0 .5-.2239.5-.5s-.22386-.5-.5-.5h-2.4196c-.00214 0-.00327-.0002-.00368-.0003-.00031-.0002-.00166-.0007-.00209-.0011.00151-.0077.00285-.0154.004-.0231z\" />\n  </svg>\n);\n"
  },
  {
    "path": "core/components/footer/payment-icons/visa.tsx",
    "content": "import React from 'react';\n\nexport const VisaIcon: React.FC<React.ComponentPropsWithoutRef<'svg'>> = ({ ...props }) => (\n  <svg\n    aria-label=\"Visa\"\n    fill=\"currentColor\"\n    height=\"24\"\n    role=\"img\"\n    viewBox=\"0 0 24 24\"\n    width=\"24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <path d=\"m2 4c-.55228 0-1 .44772-1 1v14c0 .5523.44772 1 1 1h20c.5523 0 1-.4477 1-1v-14c0-.55228-.4477-1-1-1zm-2 1c0-1.10457.895431-2 2-2h20c1.1046 0 2 .89543 2 2v14c0 1.1046-.8954 2-2 2h-20c-1.10457 0-2-.8954-2-2z\" />\n    <path d=\"m9.7694 10.0652c-.02435-.0342-.06567-.0547-.10997-.0547l-.95908.0004c-.05415 0-.10281.0307-.12258.0772l-1.12195 2.6444-.12066-.403c-.00156-.0053-.00359-.0106-.00603-.0157-.17708-.3792-.65298-.9726-1.31701-1.4434-.04526-.0322-.10765-.0337-.15463-.004-.04699.0297-.06801.0839-.05201.1344l.92225 2.9073c.01642.0517.06788.0873.12639.0873l1.0622-.0013c.05253 0 .1-.029.12078-.0735l1.74329-3.7406c.01749-.0375.01337-.0807-.01099-.1148zm-2.89766 1.3202c.02555.031.06471.0479.10489.0479.01851 0 .03714-.0035.05475-.0109.05601-.0237.08681-.0801.0741-.1357l-.21954-.9577c-.00047-.0021-.00101-.0041-.00161-.0061-.07893-.2735-.32885-.3142-.50993-.321-.00173 0-.00346 0-.00513 0l-1.23731-.0019c-.06394 0-.11893.0424-.12997.1005-.01105.0581.02483.1153.08514.1353.75441.2505 1.37151.648 1.78461 1.1496zm4.48766-1.3782h-.9122c-.0642 0-.1191.0428-.1298.1011l-.69212 3.7481c-.00651.0353.00418.0715.02914.0988.02501.0274.06185.0431.10066.0431h.91162c.0642 0 .119-.0426.1298-.1011l.6927-3.7481c.0065-.0352-.0041-.0714-.0291-.0988-.025-.0273-.0619-.0431-.1007-.0431zm6.7414.0993c-.0134-.0557-.0668-.0953-.1286-.0953h-.805c-.322 0-.5067.099-.6176.3311l-1.6687 3.487c-.018.0377-.0142.0811.0101.1156.0243.0344.0658.0551.1104.0551h.9449c.0548 0 .1038-.0312.1232-.0784.1162-.2827.1907-.4628.2153-.521.0627 0 .3529.0003.6641.0008h.0175c.3576.0004.7375.0008.8285.0008.0268.1059.0933.3865.1207.5021.0132.0559.0668.0956.1287.0956h.824c.0399 0 .0777-.0167.1027-.0454.0249-.0287.0345-.0663.0259-.1022zm-1.4767 2.4773.6269-1.4966.0797.3448.2729 1.1518zm-2.7387-1.7678c.0103-.0001.0226-.0002.0347-.0002.3571 0 .6093.0747.7678.1308.0369.0131.0781.0102.1125-.0078.0343-.018.058-.0493.0647-.0852l.1065-.5783c.011-.0596-.0272-.1176-.0897-.1361-.1959-.0578-.5128-.1268-.9097-.1268-1.125 0-1.9151.5222-1.9213 1.2694-.0071.5533.5648.8619.996 1.046.4422.1882.5907.3086.5887.4772-.0031.2581-.3545.3756-.6797.3756-.4628 0-.7122-.0644-1.0651-.1998-.037-.0142-.079-.0121-.1142.0058-.0351.0178-.0594.0495-.0662.0859l-.113.6089c-.0108.0585.0256.1157.0862.1353.311.1006.7451.1631 1.1614.1673h.0014c1.1936-.0003 1.9703-.5163 1.9788-1.3149.0045-.4391-.299-.7711-.9517-1.0435-.3995-.1791-.6443-.298-.6419-.4801 0-.1637.2238-.3295.6538-.3295z\" />\n  </svg>\n);\n"
  },
  {
    "path": "core/components/force-refresh/index.tsx",
    "content": "'use client';\n\nimport { useEffect } from 'react';\n\nimport { useRouter } from '~/i18n/routing';\nimport { FORCE_REFRESH_COOKIE, getCookieValue, setCookie } from '~/lib/client-cookies';\n\nexport const ForceRefresh = () => {\n  const router = useRouter();\n\n  useEffect(() => {\n    const shouldRefresh = getCookieValue(FORCE_REFRESH_COOKIE) === 'true';\n\n    if (shouldRefresh) {\n      setCookie(FORCE_REFRESH_COOKIE, 'false', { path: '/' });\n      router.refresh();\n    }\n  });\n\n  return null;\n};\n"
  },
  {
    "path": "core/components/header/_actions/fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\nimport { ProductCardFragment } from '~/components/product-card/fragment';\n\nexport const SearchProductFragment = graphql(\n  `\n    fragment SearchProductFragment on Product {\n      categories {\n        edges {\n          node {\n            name\n            path\n          }\n        }\n      }\n      ...ProductCardFragment\n    }\n  `,\n  [ProductCardFragment],\n);\n"
  },
  {
    "path": "core/components/header/_actions/search.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError, removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { SearchResult } from '@/vibes/soul/primitives/navigation';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { searchResultsTransformer } from '~/data-transformers/search-results-transformer';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\n\nimport { SearchProductFragment } from './fragment';\n\nconst GetQuickSearchResultsQuery = graphql(\n  `\n    query getQuickSearchResults(\n      $filters: SearchProductsFiltersInput!\n      $currencyCode: currencyCode\n    ) {\n      site {\n        search {\n          searchProducts(filters: $filters) {\n            products(first: 5) {\n              edges {\n                node {\n                  ...SearchProductFragment\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  `,\n  [SearchProductFragment],\n);\n\nexport async function search(\n  prevState: {\n    lastResult: SubmissionResult | null;\n    searchResults: SearchResult[] | null;\n    emptyStateTitle?: string;\n    emptyStateSubtitle?: string;\n  },\n  formData: FormData,\n): Promise<{\n  lastResult: SubmissionResult | null;\n  searchResults: SearchResult[] | null;\n  emptyStateTitle: string;\n  emptyStateSubtitle: string;\n}> {\n  const t = await getTranslations('Components.Header.Search');\n  const submission = parseWithZod(formData, { schema: z.object({ term: z.string() }) });\n  const emptyStateTitle = t('noSearchResultsTitle', {\n    term: submission.status === 'success' ? submission.value.term : '',\n  });\n  const emptyStateSubtitle = t('noSearchResultsSubtitle');\n\n  if (submission.status !== 'success') {\n    return {\n      lastResult: submission.reply(),\n      searchResults: prevState.searchResults,\n      emptyStateTitle,\n      emptyStateSubtitle,\n    };\n  }\n\n  if (submission.value.term.length < 3) {\n    return {\n      lastResult: submission.reply(),\n      searchResults: null,\n      emptyStateTitle,\n      emptyStateSubtitle,\n    };\n  }\n\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const currencyCode = await getPreferredCurrencyCode();\n\n  try {\n    const response = await client.fetch({\n      document: GetQuickSearchResultsQuery,\n      variables: { filters: { searchTerm: submission.value.term }, currencyCode },\n      customerAccessToken,\n      fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },\n    });\n\n    const { products } = response.data.site.search.searchProducts;\n\n    return {\n      lastResult: submission.reply(),\n      searchResults: await searchResultsTransformer(removeEdgesAndNodes(products)),\n      emptyStateTitle,\n      emptyStateSubtitle,\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => message),\n        }),\n        searchResults: prevState.searchResults,\n        emptyStateTitle,\n        emptyStateSubtitle,\n      };\n    }\n\n    if (error instanceof Error) {\n      return {\n        lastResult: submission.reply({ formErrors: [error.message] }),\n        searchResults: prevState.searchResults,\n        emptyStateTitle,\n        emptyStateSubtitle,\n      };\n    }\n\n    return {\n      lastResult: submission.reply({\n        formErrors: [t('somethingWentWrong')],\n      }),\n      searchResults: prevState.searchResults,\n      emptyStateTitle,\n      emptyStateSubtitle,\n    };\n  }\n}\n"
  },
  {
    "path": "core/components/header/_actions/switch-currency.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { revalidatePath, revalidateTag } from 'next/cache';\nimport { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\nimport { getCartId, setCartId } from '~/lib/cart';\nimport { setPreferredCurrencyCode } from '~/lib/currency';\n\nimport { CurrencyCode } from '../fragment';\nimport { CurrencyCodeSchema } from '../schema';\n\nconst currencySwitchSchema = z.object({\n  id: CurrencyCodeSchema,\n});\n\n// Note: this results in a new cart being created in the new currency, so the cart ID will change\nconst UpdateCartCurrencyMutation = graphql(`\n  mutation UpdateCartCurrencyMutation($input: UpdateCartCurrencyInput!) {\n    cart {\n      updateCartCurrency(input: $input) {\n        cart {\n          currencyCode\n          entityId\n        }\n      }\n    }\n  }\n`);\n\nconst updateCartCurrency = async (cartId: string, currencyCode: CurrencyCode) => {\n  const result = await client.fetch({\n    document: UpdateCartCurrencyMutation,\n    variables: { input: { data: { currencyCode }, cartEntityId: cartId } },\n  });\n  const newCartId = result.data.cart.updateCartCurrency?.cart?.entityId;\n\n  if (newCartId) {\n    await setCartId(newCartId);\n  } else {\n    throw new Error('Failed to update cart currency', { cause: result });\n  }\n};\n\nexport const switchCurrency = async (_prevState: SubmissionResult | null, payload: FormData) => {\n  const t = await getTranslations('Components.Header.SwitchCurrency');\n\n  const submission = parseWithZod(payload, { schema: currencySwitchSchema });\n\n  if (submission.status !== 'success') {\n    return submission.reply({ formErrors: [t('invalidCurrency')] });\n  }\n\n  await setPreferredCurrencyCode(submission.value.id);\n\n  const cartId = await getCartId();\n\n  if (cartId) {\n    await updateCartCurrency(cartId, submission.value.id)\n      .then(() => {\n        revalidateTag(TAGS.cart, { expire: 0 });\n      })\n      .catch((error: unknown) => {\n        // eslint-disable-next-line no-console\n        console.error('Error updating cart currency', error);\n\n        if (error instanceof BigCommerceGQLError) {\n          return submission.reply({\n            formErrors: error.errors.map(({ message }) => message),\n          });\n        }\n\n        if (error instanceof Error) {\n          return submission.reply({ formErrors: [error.message] });\n        }\n\n        return submission.reply({ formErrors: [t('errorUpdatingCurrency')] });\n      });\n  }\n\n  revalidatePath('/');\n\n  return submission.reply({ resetForm: true });\n};\n"
  },
  {
    "path": "core/components/header/fragment.ts",
    "content": "import { FragmentOf, graphql } from '~/client/graphql';\n\nexport const HeaderFragment = graphql(`\n  fragment HeaderFragment on Site {\n    settings {\n      storeName\n      logoV2 {\n        __typename\n        ... on StoreTextLogo {\n          text\n        }\n        ... on StoreImageLogo {\n          image {\n            url: urlTemplate(lossy: true)\n            altText\n          }\n        }\n      }\n    }\n    currencies(first: 25) {\n      edges {\n        node {\n          code\n          isTransactional\n          isDefault\n        }\n      }\n    }\n  }\n`);\n\nexport const HeaderLinksFragment = graphql(`\n  fragment HeaderLinksFragment on Site {\n    categoryTree {\n      name\n      path\n      children {\n        name\n        path\n        children {\n          name\n          path\n        }\n      }\n    }\n  }\n`);\n\ntype Currency = NonNullable<\n  NonNullable<FragmentOf<typeof HeaderFragment>>['currencies']['edges']\n>[number]['node'];\nexport type CurrencyCode = Currency['code'];\n"
  },
  {
    "path": "core/components/header/index.tsx",
    "content": "import { getLocale, getTranslations } from 'next-intl/server';\nimport { cache } from 'react';\n\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { HeaderSection } from '@/vibes/soul/sections/header-section';\nimport { GetLinksAndSectionsQuery, LayoutQuery } from '~/app/[locale]/(default)/page-data';\nimport { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, readFragment } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { TAGS } from '~/client/tags';\nimport { logoTransformer } from '~/data-transformers/logo-transformer';\nimport { routing } from '~/i18n/routing';\nimport { getCartId } from '~/lib/cart';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\n\nimport { search } from './_actions/search';\nimport { switchCurrency } from './_actions/switch-currency';\nimport { CurrencyCode, HeaderFragment, HeaderLinksFragment } from './fragment';\n\nconst GetCartCountQuery = graphql(`\n  query GetCartCountQuery($cartId: String) {\n    site {\n      cart(entityId: $cartId) {\n        entityId\n        lineItems {\n          totalQuantity\n        }\n      }\n    }\n  }\n`);\n\nconst getCartCount = cache(async (cartId: string, customerAccessToken?: string) => {\n  const response = await client.fetch({\n    document: GetCartCountQuery,\n    variables: { cartId },\n    customerAccessToken,\n    fetchOptions: {\n      cache: 'no-store',\n      next: {\n        tags: [TAGS.cart],\n      },\n    },\n  });\n\n  return response.data.site.cart?.lineItems.totalQuantity ?? null;\n});\n\nconst getHeaderLinks = cache(async (customerAccessToken?: string, currencyCode?: CurrencyCode) => {\n  const { data: response } = await client.fetch({\n    document: GetLinksAndSectionsQuery,\n    customerAccessToken,\n    variables: { currencyCode },\n    // Since this query is needed on every page, it's a good idea not to validate the customer access token.\n    // The 'cache' function also caches errors, so we might get caught in a redirect loop if the cache saves an invalid token error response.\n    validateCustomerAccessToken: false,\n    fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },\n  });\n\n  return readFragment(HeaderLinksFragment, response).site;\n});\n\nconst getHeaderData = cache(async () => {\n  const { data: response } = await client.fetch({\n    document: LayoutQuery,\n    fetchOptions: { next: { revalidate } },\n  });\n\n  return readFragment(HeaderFragment, response).site;\n});\n\nexport const Header = async () => {\n  const t = await getTranslations('Components.Header');\n  const locale = await getLocale();\n\n  const data = await getHeaderData();\n\n  const logo = data.settings ? logoTransformer(data.settings) : '';\n\n  const locales = routing.locales.map((enabledLocales) => ({\n    id: enabledLocales,\n    label: enabledLocales.toLocaleUpperCase(),\n  }));\n\n  const currencies = data.currencies.edges\n    ? data.currencies.edges\n        // only show transactional currencies for now until cart prices can be rendered in display currencies\n        .filter(({ node }) => node.isTransactional)\n        .map(({ node }) => ({\n          id: node.code,\n          label: node.code,\n          isDefault: node.isDefault,\n        }))\n    : [];\n\n  const streamableLinks = Streamable.from(async () => {\n    const [customerAccessToken, currencyCode] = await Promise.all([\n      getSessionCustomerAccessToken(),\n      getPreferredCurrencyCode(),\n    ]);\n    // const customerAccessToken = await getSessionCustomerAccessToken();\n    // const currencyCode = await getPreferredCurrencyCode();\n    const categoryTree = (await getHeaderLinks(customerAccessToken, currencyCode)).categoryTree;\n\n    /**  To prevent the navigation menu from overflowing, we limit the number of categories to 6.\n   To show a full list of categories, modify the `slice` method to remove the limit.\n   Will require modification of navigation menu styles to accommodate the additional categories.\n   */\n    const slicedTree = categoryTree.slice(0, 6);\n\n    return slicedTree.map(({ name, path, children }) => ({\n      label: name,\n      href: path,\n      groups: children.map((firstChild) => ({\n        label: firstChild.name,\n        href: firstChild.path,\n        links: firstChild.children.map((secondChild) => ({\n          label: secondChild.name,\n          href: secondChild.path,\n        })),\n      })),\n    }));\n  });\n\n  const streamableGiftCertificatesEnabled = Streamable.from(async () => {\n    const [customerAccessToken, currencyCode] = await Promise.all([\n      getSessionCustomerAccessToken(),\n      getPreferredCurrencyCode(),\n    ]);\n    const giftCertificateSettings = (await getHeaderLinks(customerAccessToken, currencyCode))\n      .settings?.giftCertificates;\n\n    return giftCertificateSettings?.isEnabled ?? false;\n  });\n\n  const streamableCartCount = Streamable.from(async () => {\n    const cartId = await getCartId();\n    const customerAccessToken = await getSessionCustomerAccessToken();\n\n    if (!cartId) {\n      return null;\n    }\n\n    return getCartCount(cartId, customerAccessToken);\n  });\n\n  const streamableActiveCurrencyId = Streamable.from(async (): Promise<string | undefined> => {\n    const currencyCode = await getPreferredCurrencyCode();\n\n    const defaultCurrency = currencies.find(({ isDefault }) => isDefault);\n\n    return currencyCode ?? defaultCurrency?.id;\n  });\n\n  return (\n    <HeaderSection\n      navigation={{\n        accountHref: '/login',\n        accountLabel: t('Icons.account'),\n        cartHref: '/cart',\n        cartLabel: t('Icons.cart'),\n        giftCertificatesLabel: t('Icons.giftCertificates'),\n        giftCertificatesHref: '/gift-certificates',\n        giftCertificatesEnabled: streamableGiftCertificatesEnabled,\n        searchHref: '/search',\n        searchParamName: 'term',\n        searchAction: search,\n        searchInputPlaceholder: t('Search.inputPlaceholder'),\n        searchSubmitLabel: t('Search.submitLabel'),\n        links: streamableLinks,\n        logo,\n        mobileMenuTriggerLabel: t('toggleNavigation'),\n        openSearchPopupLabel: t('Icons.search'),\n        logoLabel: t('home'),\n        cartCount: streamableCartCount,\n        activeLocaleId: locale,\n        locales,\n        currencies,\n        activeCurrencyId: streamableActiveCurrencyId,\n        currencyAction: switchCurrency,\n        switchCurrencyLabel: t('SwitchCurrency.label'),\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "core/components/header/schema.ts",
    "content": "import { z } from 'zod';\n\nimport type { CurrencyCode } from './fragment';\n\nexport const CurrencyCodeSchema = z\n  .string()\n  .length(3)\n  .toUpperCase()\n  .refine((val): val is CurrencyCode => /^[A-Z]{3}$/.test(val), {\n    message: 'Must be a valid currency code',\n  });\n"
  },
  {
    "path": "core/components/image/index.tsx",
    "content": "'use client';\n\n// eslint-disable-next-line @typescript-eslint/no-restricted-imports\nimport NextImage, { ImageProps } from 'next/image';\n\nimport { buildConfig } from '~/build-config/reader';\nimport bcCdnImageLoader from '~/lib/cdn-image-loader';\n\nfunction shouldUseLoaderProp(props: ImageProps): boolean {\n  if (typeof props.src !== 'string') return false;\n\n  const { src } = props;\n\n  return buildConfig.get('urls').cdnUrls.some((cdn) => src.startsWith(`https://${cdn}`));\n}\n\n/**\n * This component should be used in place of Next's `Image` component for images from the\n * BigCommerce platform, which will reduce load on the Next.js application for image assets.\n *\n * It defaults to use the default loader in Next.js if it's an image not from the BigCommerce CDN.\n *\n * @returns {React.ReactElement} The `<Image>` component\n */\nexport const Image = ({ ...props }: ImageProps) => {\n  const loader = shouldUseLoaderProp(props) ? bcCdnImageLoader : undefined;\n\n  return <NextImage loader={loader} {...props} />;\n};\n"
  },
  {
    "path": "core/components/link/index.tsx",
    "content": "'use client';\n\nimport { ComponentPropsWithRef, ComponentRef, forwardRef, useReducer } from 'react';\n\nimport { Link as NavLink, useRouter } from '../../i18n/routing';\n\ntype NextLinkProps = Omit<ComponentPropsWithRef<typeof NavLink>, 'prefetch'>;\n\ninterface PrefetchOptions {\n  prefetch?: 'hover' | 'viewport' | 'none';\n  prefetchKind?: 'auto' | 'full';\n}\n\ntype Props = NextLinkProps & PrefetchOptions;\n\n/**\n * This custom `Link` is based on  Next-Intl's `Link` component\n * https://next-intl-docs.vercel.app/docs/routing/navigation#link\n * which adds automatically prefixes for the href with the current locale as necessary\n * and extends with additional prefetching controls, making navigation\n * prefetching more adaptable to different use cases. By offering `prefetch` and `prefetchKind`\n * props, it grants explicit management over when and how prefetching occurs, defaulting to 'hover' for\n * prefetch behavior and 'auto' for prefetch kind. This approach provides a balance between optimizing\n * page load performance and resource usage. https://nextjs.org/docs/app/api-reference/components/link#prefetch\n */\nexport const Link = forwardRef<ComponentRef<'a'>, Props>(\n  ({ href, prefetch = 'hover', prefetchKind = 'auto', children, className, ...rest }, ref) => {\n    const router = useRouter();\n    const [prefetched, setPrefetched] = useReducer(() => true, false);\n    const computedPrefetch = computePrefetchProp({ prefetch, prefetchKind });\n\n    const triggerPrefetch = () => {\n      if (prefetched) {\n        return;\n      }\n\n      if (typeof href === 'string') {\n        // PrefetchKind enum is not exported\n        // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n        // @ts-expect-error\n        router.prefetch(href, { kind: prefetchKind });\n      } else {\n        // PrefetchKind enum is not exported\n        // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n        // @ts-expect-error\n        router.prefetch(href.href, { kind: prefetchKind });\n      }\n\n      setPrefetched();\n    };\n\n    return (\n      <NavLink\n        className={className}\n        href={href}\n        onMouseEnter={prefetch === 'hover' ? triggerPrefetch : undefined}\n        onTouchStart={prefetch === 'hover' ? triggerPrefetch : undefined}\n        prefetch={computedPrefetch}\n        ref={ref}\n        {...rest}\n      >\n        {children}\n      </NavLink>\n    );\n  },\n);\n\nfunction computePrefetchProp({\n  prefetch,\n  prefetchKind,\n}: Required<PrefetchOptions>): boolean | undefined {\n  if (prefetch !== 'viewport') {\n    return false;\n  }\n\n  if (prefetchKind === 'auto') {\n    return undefined;\n  }\n\n  return true;\n}\n"
  },
  {
    "path": "core/components/modal/index.tsx",
    "content": "'use client';\n\nimport { getFormProps, SubmissionResult, useForm } from '@conform-to/react';\nimport { getZodConstraint, parseWithZod } from '@conform-to/zod';\nimport { Close } from '@radix-ui/react-dialog';\nimport { useActionState, useEffect, useRef, useState } from 'react';\nimport { useFormStatus } from 'react-dom';\nimport { z } from 'zod';\n\nimport { Button, ButtonProps } from '@/vibes/soul/primitives/button';\nimport {\n  Modal as ModalPrimitive,\n  ModalProps as ModalPrimitiveProps,\n} from '@/vibes/soul/primitives/modal';\n\nimport { ModalFormContext } from './modal-form-provider';\n\nexport interface ModalButton {\n  label: string;\n  className?: string;\n  variant?: ButtonProps['variant'];\n  type?: 'submit' | 'cancel';\n  action?: () => void | Promise<void>;\n}\n\ntype Action<S, P> = (state: Awaited<S>, payload: P) => S | Promise<S>;\n\nexport interface ModalFormState {\n  lastResult: SubmissionResult | null;\n  successMessage?: string;\n  errorMessage?: string;\n}\n\nexport type ModalFormAction = Action<ModalFormState, FormData>;\n\ninterface ModalFormProps {\n  action: ModalFormAction;\n  onSuccess?: (state: ModalFormState) => void;\n  onError?: (state: ModalFormState) => void;\n}\n\ninterface ModalProps extends ModalPrimitiveProps {\n  buttons?: ModalButton[];\n  form?: ModalFormProps;\n}\n\nexport const Modal = ({ buttons = [], form, children, ...props }: ModalProps) => {\n  return (\n    <ModalPrimitive {...props}>\n      <ModalFormWrapper form={form}>\n        <div>{children}</div>\n        {buttons.length > 0 && (\n          <div className=\"mt-5 flex flex-row justify-end gap-2\">\n            {buttons.map((button, index) => (\n              <ModalButton key={index} {...button} />\n            ))}\n          </div>\n        )}\n      </ModalFormWrapper>\n    </ModalPrimitive>\n  );\n};\n\nfunction ModalButton({ label, className, variant, type, action }: ModalButton) {\n  switch (type) {\n    case 'cancel':\n      return (\n        <Close asChild>\n          <Button className={className} size=\"small\" variant=\"ghost\">\n            {label}\n          </Button>\n        </Close>\n      );\n\n    case 'submit':\n      return (\n        <ModalSubmitButton\n          className={className}\n          label={label}\n          type=\"submit\"\n          variant={variant ?? 'primary'}\n        />\n      );\n\n    default:\n      return (\n        <Button className={className} onClick={action} size=\"small\" variant={variant ?? 'primary'}>\n          {label}\n        </Button>\n      );\n  }\n}\n\nfunction ModalSubmitButton({ label, className, variant }: ModalButton) {\n  const { pending } = useFormStatus();\n\n  return (\n    <Button\n      className={className}\n      loading={pending}\n      size=\"small\"\n      type=\"submit\"\n      variant={variant ?? 'primary'}\n    >\n      {label}\n    </Button>\n  );\n}\n\nfunction ModalFormWrapper({\n  form,\n  children,\n}: {\n  children: React.ReactNode;\n  form?: ModalFormProps;\n}) {\n  if (form) {\n    return <ModalForm {...form}>{children}</ModalForm>;\n  }\n\n  return children;\n}\n\nfunction ModalForm({\n  children,\n  action,\n  onSuccess,\n  onError,\n}: { children: React.ReactNode } & ModalFormProps) {\n  const shouldCallback = useRef(false);\n  const [schema, setSchema] = useState<z.ZodTypeAny | undefined>();\n  const [state, formAction] = useActionState(action, {\n    lastResult: null,\n  });\n\n  const [form, fields] = useForm({\n    constraint: schema ? getZodConstraint(schema) : undefined,\n    shouldValidate: 'onBlur',\n    shouldRevalidate: 'onInput',\n    lastResult: state.lastResult,\n    onValidate: schema ? ({ formData }) => parseWithZod(formData, { schema }) : undefined,\n    onSubmit: () => {\n      shouldCallback.current = true;\n    },\n  });\n\n  useEffect(() => {\n    if (!shouldCallback.current) {\n      return;\n    }\n\n    switch (state.lastResult?.status) {\n      case 'success': {\n        onSuccess?.(state);\n        break;\n      }\n\n      case 'error': {\n        onError?.(state);\n        break;\n      }\n    }\n\n    shouldCallback.current = false;\n  }, [onSuccess, onError, state]);\n\n  return (\n    <ModalFormContext.Provider value={{ form, fields, state, schema, setSchema }}>\n      <form {...getFormProps(form)} action={formAction}>\n        {children}\n      </form>\n    </ModalFormContext.Provider>\n  );\n}\n"
  },
  {
    "path": "core/components/modal/modal-form-provider.tsx",
    "content": "import { FieldMetadata, FormMetadata } from '@conform-to/react';\nimport { createContext, useContext, useEffect } from 'react';\nimport { z } from 'zod';\n\nimport { ModalFormState } from '.';\n\ninterface ModalFormContext<T extends z.ZodTypeAny> {\n  state: ModalFormState;\n  fields: Record<keyof z.infer<T>, FieldMetadata<unknown, z.infer<T>>>;\n  form: FormMetadata<z.infer<T>>;\n  schema?: z.ZodTypeAny;\n  setSchema?: (schema?: z.infer<T>) => void;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const ModalFormContext = createContext<ModalFormContext<any> | undefined>(undefined);\n\nexport function useModalForm<T extends z.ZodTypeAny>(\n  schema: T,\n): Omit<ModalFormContext<T>, 'schema' | 'setSchema'> {\n  const context = useContext<ModalFormContext<T> | undefined>(ModalFormContext);\n\n  if (context === undefined) {\n    throw new Error('useModalForm must be used within a Modal form');\n  }\n\n  useEffect(() => {\n    if (!context.schema) {\n      context.setSchema?.(schema);\n    }\n  }, [context, schema]);\n\n  return { fields: context.fields, state: context.state, form: context.form };\n}\n"
  },
  {
    "path": "core/components/polyfills/container-query/index.tsx",
    "content": "import { headers } from 'next/headers';\nimport Script from 'next/script';\nimport { userAgent } from 'next/server';\n\nfunction checkBrowserSupport(name: string | undefined, version: string | undefined): boolean {\n  if (!name || !version) return false;\n\n  const versionNumber = parseFloat(version);\n\n  switch (name) {\n    case 'Chrome':\n      return versionNumber <= 105;\n\n    case 'Edge':\n      return versionNumber <= 105;\n\n    case 'Safari':\n      return versionNumber <= 15.6;\n\n    case 'Firefox':\n      return versionNumber <= 109;\n\n    default:\n      return false;\n  }\n}\n\nexport async function ContainerQueryPolyfill() {\n  const headersList = await headers();\n  const { browser } = userAgent({ headers: headersList });\n\n  const { version, name } = browser;\n\n  const isUnsupported = checkBrowserSupport(name, version);\n\n  if (isUnsupported) {\n    return (\n      // eslint-disable-next-line @next/next/no-before-interactive-script-outside-document\n      <Script\n        src=\"https://cdn.jsdelivr.net/npm/container-query-polyfill@1/dist/container-query-polyfill.modern.js\"\n        strategy=\"beforeInteractive\"\n      />\n    );\n  }\n}\n"
  },
  {
    "path": "core/components/product-card/fragment.ts",
    "content": "import { PricingFragment } from '~/client/fragments/pricing';\nimport { graphql } from '~/client/graphql';\n\nexport const ProductCardFragment = graphql(\n  `\n    fragment ProductCardFragment on Product {\n      entityId\n      name\n      defaultImage {\n        altText\n        url: urlTemplate(lossy: true)\n      }\n      path\n      brand {\n        name\n        path\n      }\n      inventory {\n        hasVariantInventory\n        isInStock\n        aggregated {\n          availableForBackorder\n          unlimitedBackorder\n          availableOnHand\n        }\n      }\n      reviewSummary {\n        numberOfReviews\n        averageRating\n      }\n      variants(first: 1) {\n        edges {\n          node {\n            entityId\n            sku\n            inventory {\n              byLocation {\n                edges {\n                  node {\n                    locationEntityId\n                    backorderMessage\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n      ...PricingFragment\n    }\n  `,\n  [PricingFragment],\n);\n"
  },
  {
    "path": "core/components/product-variants-inventory/fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nexport const ProductVariantsInventoryFragment = graphql(`\n  fragment ProductVariantsInventoryFragment on Product {\n    variants {\n      edges {\n        node {\n          entityId\n          sku\n          inventory {\n            byLocation {\n              edges {\n                node {\n                  locationEntityId\n                  backorderMessage\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n`);\n"
  },
  {
    "path": "core/components/store-logo/fragment.ts",
    "content": "import { graphql } from '~/client/graphql';\n\nexport const StoreLogoFragment = graphql(`\n  fragment StoreLogoFragment on Settings {\n    storeName\n    logoV2 {\n      __typename\n      ... on StoreTextLogo {\n        text\n      }\n      ... on StoreImageLogo {\n        image {\n          url: urlTemplate(lossy: true)\n          altText\n        }\n      }\n    }\n  }\n`);\n"
  },
  {
    "path": "core/components/subscribe/_actions/subscribe.ts",
    "content": "'use server';\n\nimport { BigCommerceGQLError } from '@bigcommerce/catalyst-client';\nimport { SubmissionResult } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { getTranslations } from 'next-intl/server';\n\nimport { schema } from '@/vibes/soul/primitives/inline-email-form/schema';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\n\nconst SubscribeToNewsletterMutation = graphql(`\n  mutation SubscribeToNewsletterMutation($input: CreateSubscriberInput!) {\n    newsletter {\n      subscribe(input: $input) {\n        errors {\n          __typename\n          ... on CreateSubscriberEmailInvalidError {\n            message\n          }\n          ... on CreateSubscriberAlreadyExistsError {\n            message\n          }\n          ... on CreateSubscriberUnexpectedError {\n            message\n          }\n        }\n      }\n    }\n  }\n`);\n\nexport const subscribe = async (\n  _lastResult: { lastResult: SubmissionResult | null },\n  formData: FormData,\n) => {\n  const t = await getTranslations('Components.Subscribe');\n  const subscribeSchema = schema({\n    requiredMessage: t('Errors.emailRequired'),\n    invalidMessage: t('Errors.invalidEmail'),\n  });\n  const submission = parseWithZod(formData, { schema: subscribeSchema });\n\n  if (submission.status !== 'success') {\n    return { lastResult: submission.reply() };\n  }\n\n  try {\n    const response = await client.fetch({\n      document: SubscribeToNewsletterMutation,\n      variables: {\n        input: {\n          email: submission.value.email,\n        },\n      },\n      fetchOptions: {\n        cache: 'no-store',\n      },\n    });\n\n    const errors = response.data.newsletter.subscribe.errors;\n\n    // If subscriber already exists, treat it as success for privacy reasons\n    // We don't want to reveal that the email is already subscribed\n    const subscriberAlreadyExists = errors.some(\n      ({ __typename }) => __typename === 'CreateSubscriberAlreadyExistsError',\n    );\n\n    if (subscriberAlreadyExists) {\n      return {\n        lastResult: submission.reply(),\n        successMessage: t('subscribedToNewsletter'),\n      };\n    }\n\n    if (errors.length > 0) {\n      // If there are other errors, we want to show the error message to the user\n      return {\n        lastResult: submission.reply({\n          formErrors: errors.map(({ __typename }) => {\n            switch (__typename) {\n              case 'CreateSubscriberEmailInvalidError':\n                return t('Errors.invalidEmail');\n\n              default:\n                return t('Errors.somethingWentWrong');\n            }\n          }),\n        }),\n      };\n    }\n\n    // If there are no errors, we want to show the success message to the user\n    return {\n      lastResult: submission.reply(),\n      successMessage: t('subscribedToNewsletter'),\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    if (error instanceof BigCommerceGQLError) {\n      return {\n        lastResult: submission.reply({\n          formErrors: error.errors.map(({ message }) => message),\n        }),\n      };\n    }\n\n    if (error instanceof Error) {\n      return { lastResult: submission.reply({ formErrors: [error.message] }) };\n    }\n\n    return { lastResult: submission.reply({ formErrors: [String(error)] }) };\n  }\n};\n"
  },
  {
    "path": "core/components/subscribe/index.tsx",
    "content": "import { useTranslations } from 'next-intl';\n\nimport { Subscribe as SubscribeSection } from '@/vibes/soul/sections/subscribe';\n\nimport { subscribe } from './_actions/subscribe';\n\nexport const Subscribe = () => {\n  const t = useTranslations('Components.Subscribe');\n\n  return (\n    <SubscribeSection\n      action={subscribe}\n      description={t('description')}\n      placeholder={t('placeholder')}\n      title={t('title')}\n    />\n  );\n};\n"
  },
  {
    "path": "core/components/wishlist/error.ts",
    "content": "export class WishlistMutationError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = 'WishlistError';\n  }\n}\n"
  },
  {
    "path": "core/components/wishlist/fragment.ts",
    "content": "import { PaginationFragment } from '~/client/fragments/pagination';\nimport { PricingFragment } from '~/client/fragments/pricing';\nimport { graphql } from '~/client/graphql';\n\nexport const WishlistItemProductFragment = graphql(\n  `\n    fragment WishlistItemProductFragment on Product {\n      entityId\n      name\n      defaultImage {\n        altText\n        url: urlTemplate(lossy: true)\n      }\n      path\n      brand {\n        name\n        path\n      }\n      reviewSummary {\n        numberOfReviews\n        averageRating\n      }\n      sku\n      showCartAction\n      inventory {\n        isInStock\n      }\n      availabilityV2 {\n        status\n      }\n      ...PricingFragment\n    }\n  `,\n  [PricingFragment],\n);\n\nexport const WishlistItemFragment = graphql(\n  `\n    fragment WishlistItemFragment on WishlistItem {\n      entityId\n      productEntityId\n      variantEntityId\n      product {\n        ...WishlistItemProductFragment\n      }\n    }\n  `,\n  [WishlistItemProductFragment],\n);\n\nexport const WishlistFragment = graphql(\n  `\n    fragment WishlistFragment on Wishlist {\n      entityId\n      name\n      isPublic\n      token\n      items(first: 6) {\n        edges {\n          node {\n            ...WishlistItemFragment\n          }\n        }\n        collectionInfo {\n          totalItems\n        }\n      }\n    }\n  `,\n  [WishlistItemFragment],\n);\n\nexport const WishlistsFragment = graphql(\n  `\n    fragment WishlistsFragment on WishlistConnection {\n      edges {\n        node {\n          ...WishlistFragment\n        }\n      }\n      pageInfo {\n        ...PaginationFragment\n      }\n    }\n  `,\n  [WishlistFragment, PaginationFragment],\n);\n\nexport const WishlistPaginatedItemsFragment = graphql(\n  `\n    fragment WishlistPaginatedItemsFragment on Wishlist {\n      entityId\n      name\n      isPublic\n      token\n      items(first: $first, after: $after, last: $last, before: $before) {\n        edges {\n          node {\n            ...WishlistItemFragment\n          }\n        }\n        pageInfo {\n          ...PaginationFragment\n        }\n        collectionInfo {\n          totalItems\n        }\n      }\n    }\n  `,\n  [WishlistItemFragment, PaginationFragment],\n);\n\nexport const PublicWishlistFragment = graphql(\n  `\n    fragment PublicWishlistFragment on PublicWishlist {\n      entityId\n      name\n      token\n      items(first: $first, after: $after, last: $last, before: $before) {\n        edges {\n          node {\n            ...WishlistItemFragment\n          }\n        }\n        pageInfo {\n          ...PaginationFragment\n        }\n        collectionInfo {\n          totalItems\n        }\n      }\n    }\n  `,\n  [WishlistItemFragment, PaginationFragment],\n);\n"
  },
  {
    "path": "core/components/wishlist/modals/change-visibility.tsx",
    "content": "'use client';\n\nimport { FormStatus } from '@/vibes/soul/form/form-status';\nimport { Wishlist } from '@/vibes/soul/sections/wishlist-details';\nimport { useModalForm } from '~/components/modal/modal-form-provider';\n\nimport { toggleWishlistVisibilitySchema } from '../../../app/[locale]/(default)/account/wishlists/_actions/schema';\n\nexport const ChangeWishlistVisibilityModal = ({\n  id,\n  visibility: { isPublic },\n  message,\n}: Wishlist & { message: React.ReactNode }) => {\n  const { state, form } = useModalForm(toggleWishlistVisibilitySchema);\n\n  return (\n    <>\n      {message}\n      <input name=\"wishlistId\" type=\"hidden\" value={id} />\n      <input name=\"wishlistIsPublic\" type=\"hidden\" value={String(!isPublic)} />\n      {state.lastResult?.status === 'error' && (\n        <div className=\"mt-4\">\n          {form.errors?.map((error, index) => (\n            <FormStatus key={index} type=\"error\">\n              {error}\n            </FormStatus>\n          ))}\n        </div>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "core/components/wishlist/modals/delete.tsx",
    "content": "'use client';\n\nimport { FormStatus } from '@/vibes/soul/form/form-status';\nimport { Wishlist } from '@/vibes/soul/sections/wishlist-details';\nimport { useModalForm } from '~/components/modal/modal-form-provider';\n\nimport { deleteWishlistSchema } from '../../../app/[locale]/(default)/account/wishlists/_actions/schema';\n\nexport const DeleteWishlistModal = ({ id, message }: Wishlist & { message: React.ReactNode }) => {\n  const { state, form } = useModalForm(deleteWishlistSchema);\n\n  return (\n    <>\n      {message}\n      <input name=\"wishlistId\" type=\"hidden\" value={id} />\n      {state.lastResult?.status === 'error' && (\n        <div className=\"mt-4\">\n          {form.errors?.map((error, index) => (\n            <FormStatus key={index} type=\"error\">\n              {error}\n            </FormStatus>\n          ))}\n        </div>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "core/components/wishlist/modals/new.tsx",
    "content": "'use client';\n\nimport { getInputProps } from '@conform-to/react';\nimport { useRef } from 'react';\n\nimport { FormStatus } from '@/vibes/soul/form/form-status';\nimport { Input } from '@/vibes/soul/form/input';\nimport { useModalForm } from '~/components/modal/modal-form-provider';\n\nimport { newWishlistSchema } from '../../../app/[locale]/(default)/account/wishlists/_actions/schema';\n\nexport const NewWishlistModal = ({\n  nameLabel = 'Name',\n  requiredError,\n}: {\n  requiredError: string;\n  nameLabel: string;\n}) => {\n  const defaultValue = useRef<string>('');\n  const { form, fields, state } = useModalForm(\n    newWishlistSchema({ required_error: requiredError }),\n  );\n\n  return (\n    <>\n      <Input\n        {...getInputProps(fields.wishlistName, { type: 'text' })}\n        defaultValue={defaultValue.current}\n        errors={fields.wishlistName.errors}\n        key={fields.wishlistName.id}\n        label={nameLabel}\n        onChange={(e) => {\n          defaultValue.current = e.target.value;\n        }}\n        required\n      />\n      {state.lastResult?.status === 'error' && (\n        <div className=\"mt-4\">\n          {form.errors?.map((error, index) => (\n            <FormStatus key={index} type=\"error\">\n              {error}\n            </FormStatus>\n          ))}\n        </div>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "core/components/wishlist/modals/rename.tsx",
    "content": "'use client';\n\nimport { getInputProps } from '@conform-to/react';\nimport { useRef } from 'react';\n\nimport { FormStatus } from '@/vibes/soul/form/form-status';\nimport { Input } from '@/vibes/soul/form/input';\nimport { Wishlist } from '@/vibes/soul/sections/wishlist-details';\nimport { useModalForm } from '~/components/modal/modal-form-provider';\n\nimport { renameWishlistSchema } from '../../../app/[locale]/(default)/account/wishlists/_actions/schema';\n\nexport const RenameWishlistModal = ({\n  id,\n  name,\n  nameLabel = 'Name',\n  requiredError,\n}: Wishlist & { requiredError: string; nameLabel: string }) => {\n  const defaultValue = useRef<string>(name);\n  const { form, fields, state } = useModalForm(\n    renameWishlistSchema({ required_error: requiredError }),\n  );\n\n  return (\n    <>\n      <input name=\"wishlistId\" type=\"hidden\" value={id} />\n      <Input\n        {...getInputProps(fields.wishlistName, { type: 'text' })}\n        defaultValue={defaultValue.current}\n        errors={fields.wishlistName.errors}\n        key={fields.wishlistName.id}\n        label={nameLabel}\n        onChange={(e) => {\n          defaultValue.current = e.target.value;\n        }}\n        required\n      />\n      {state.lastResult?.status === 'error' && (\n        <div className=\"mt-4\">\n          {form.errors?.map((error, index) => (\n            <FormStatus key={index} type=\"error\">\n              {error}\n            </FormStatus>\n          ))}\n        </div>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "core/components/wishlist/modals/share.tsx",
    "content": "'use client';\n\nimport { Input } from '@/vibes/soul/form/input';\n\nexport const ShareWishlistModal = ({ publicUrl }: { publicUrl: string }) => {\n  const shareUrl = String(new URL(publicUrl, window.location.origin));\n\n  return <Input defaultValue={shareUrl} readOnly type=\"text\" />;\n};\n"
  },
  {
    "path": "core/components/wishlist/share-button.tsx",
    "content": "'use client';\n\nimport { clsx } from 'clsx';\nimport { useState } from 'react';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Button, ButtonProps } from '@/vibes/soul/primitives/button';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\nimport { toast } from '@/vibes/soul/primitives/toaster';\nimport { Tooltip } from '@/vibes/soul/primitives/tooltip';\nimport { Modal } from '~/components/modal';\n\nimport { getShareWishlistModal } from '../../app/[locale]/(default)/account/wishlists/modals';\n\nimport { ShareWishlistModal } from './modals/share';\n\ninterface Props {\n  wishlistName: string;\n  publicUrl: string;\n  label: string;\n  isMobileUser: Streamable<boolean>;\n  isPublic: boolean;\n  modalTitle: string;\n  successMessage: string;\n  copiedMessage: string;\n  disabledTooltip: string;\n  closeLabel: string;\n  copyLabel: string;\n  size?: ButtonProps['size'];\n}\n\nexport const WishlistShareButton = ({\n  wishlistName,\n  publicUrl,\n  label,\n  isMobileUser: streamableIsMobileUser,\n  isPublic,\n  modalTitle,\n  successMessage,\n  copiedMessage,\n  disabledTooltip,\n  closeLabel,\n  copyLabel,\n  size = 'small',\n}: Props) => {\n  const [open, setOpen] = useState(false);\n  const [tooltipOpen, setTooltipOpen] = useState(false);\n  const getShareUrl = () => String(new URL(publicUrl, window.location.origin));\n  const nativeShare = async () => {\n    try {\n      await navigator.share({ url: getShareUrl(), title: wishlistName });\n      toast.success(successMessage);\n    } catch {\n      // noop\n    }\n  };\n\n  const copyToClipboard = async () => {\n    try {\n      await navigator.clipboard.writeText(getShareUrl());\n      toast.success(copiedMessage);\n      setOpen(false);\n    } catch {\n      // noop\n    }\n  };\n\n  return (\n    <Stream fallback={<WishlistShareButtonSkeleton size={size} />} value={streamableIsMobileUser}>\n      {(isMobileUser) => {\n        const ShareButton = (\n          <Button\n            disabled={!isPublic}\n            onClick={isMobileUser ? nativeShare : () => null}\n            onTouchStart={() => (!isPublic && isMobileUser ? setTooltipOpen(!tooltipOpen) : null)}\n            size={size}\n            variant=\"secondary\"\n          >\n            {label}\n          </Button>\n        );\n\n        if (!isPublic) {\n          return (\n            <Tooltip\n              className=\"max-w-52 pl-4 text-sm\"\n              delayDuration={0}\n              open={tooltipOpen}\n              setOpen={setTooltipOpen}\n              side=\"bottom\"\n              trigger={ShareButton}\n            >\n              {disabledTooltip}\n            </Tooltip>\n          );\n        }\n\n        if (!isMobileUser) {\n          return (\n            <Modal\n              className=\"min-w-64 max-w-lg @lg:min-w-96\"\n              isOpen={open}\n              setOpen={setOpen}\n              trigger={ShareButton}\n              {...getShareWishlistModal(\n                modalTitle,\n                copyLabel,\n                closeLabel,\n                publicUrl,\n                copyToClipboard,\n              )}\n            >\n              <ShareWishlistModal publicUrl={publicUrl} />\n            </Modal>\n          );\n        }\n\n        return ShareButton;\n      }}\n    </Stream>\n  );\n};\n\nexport function WishlistShareButtonSkeleton({ size = 'small' }: { size?: Props['size'] }) {\n  return (\n    <Skeleton.Box\n      className={clsx(\n        'rounded-full',\n        {\n          'x-small': 'min-h-8 min-w-[7ch]',\n          small: 'min-h-10 min-w-[7ch]',\n          medium: 'min-h-12 min-w-[8ch]',\n          large: 'min-h-14 min-w-[9ch]',\n        }[size],\n      )}\n    />\n  );\n}\n"
  },
  {
    "path": "core/data-transformers/breadcrumbs-transformer.ts",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { ResultOf } from 'gql.tada';\n\nimport {\n  BreadcrumbsCategoryFragment,\n  BreadcrumbsWebPageFragment,\n} from '~/components/breadcrumbs/fragment';\nimport { Breadcrumb } from '~/vibes/soul/sections/breadcrumbs';\n\ntype BreadcrumbsResult =\n  | ResultOf<typeof BreadcrumbsWebPageFragment>\n  | ResultOf<typeof BreadcrumbsCategoryFragment>;\n\nexport const breadcrumbsTransformer = (breadcrumbs: BreadcrumbsResult['breadcrumbs']) => {\n  return removeEdgesAndNodes(breadcrumbs).reduce<Breadcrumb[]>((acc, crumb) => {\n    if (crumb.path) {\n      return [...acc, { label: crumb.name, href: crumb.path, id: crumb.path }];\n    }\n\n    return acc;\n  }, []);\n};\n\nexport function truncateBreadcrumbs(breadcrumbs: Breadcrumb[], length: number): Breadcrumb[] {\n  if (breadcrumbs.length < length) {\n    return breadcrumbs;\n  }\n\n  const middleIndex = Math.floor(breadcrumbs.length / 2);\n  const dropCount = breadcrumbs.length - length;\n  const dropEach = Math.ceil(dropCount / 2);\n  const dropLast = Math.floor(dropCount / 2);\n  const [first, last] = [\n    breadcrumbs.slice(0, middleIndex - dropEach),\n    breadcrumbs.slice(middleIndex + dropLast),\n  ];\n\n  last[0] = { label: '...', href: '#' };\n\n  return [...first, ...last];\n}\n"
  },
  {
    "path": "core/data-transformers/facets-transformer.ts",
    "content": "/* eslint-disable complexity */\nimport { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport {\n  fetchFacetedSearch,\n  PublicSearchParamsSchema,\n  PublicToPrivateParams,\n} from '~/app/[locale]/(default)/(faceted)/fetch-faceted-search';\nimport { ExistingResultType } from '~/client/util';\n\nexport const facetsTransformer = async ({\n  refinedFacets,\n  allFacets,\n  searchParams,\n}: {\n  refinedFacets: ExistingResultType<typeof fetchFacetedSearch>['facets']['items'];\n  allFacets: ExistingResultType<typeof fetchFacetedSearch>['facets']['items'];\n  searchParams: z.input<typeof PublicSearchParamsSchema>;\n}) => {\n  const t = await getTranslations('Faceted.FacetedSearch.Facets');\n  const { filters } = PublicToPrivateParams.parse(searchParams);\n\n  return allFacets.map((facet) => {\n    const refinedFacet = refinedFacets.find((f) => f.displayName === facet.displayName);\n\n    if (refinedFacet == null) {\n      return null;\n    }\n\n    if (facet.__typename === 'CategorySearchFilter') {\n      const refinedCategorySearchFilter =\n        refinedFacet.__typename === 'CategorySearchFilter' ? refinedFacet : null;\n\n      return {\n        type: 'toggle-group' as const,\n        paramName: 'categoryIn',\n        label: facet.displayName,\n        defaultCollapsed: facet.isCollapsedByDefault,\n        options: facet.categories.map((category) => {\n          const refinedCategory = refinedCategorySearchFilter?.categories.find(\n            (c) => c.entityId === category.entityId,\n          );\n          const isSelected = filters.categoryEntityIds?.includes(category.entityId) === true;\n          const disabled = refinedCategory == null && !isSelected;\n          const productCountLabel = disabled ? '' : ` (${category.productCount})`;\n          const label = facet.displayProductCount\n            ? `${category.name}${productCountLabel}`\n            : category.name;\n\n          return {\n            label,\n            value: category.entityId.toString(),\n            disabled,\n          };\n        }),\n      };\n    }\n\n    if (facet.__typename === 'BrandSearchFilter') {\n      const refinedBrandSearchFilter =\n        refinedFacet.__typename === 'BrandSearchFilter' ? refinedFacet : null;\n\n      return {\n        type: 'toggle-group' as const,\n        paramName: 'brand',\n        label: facet.displayName,\n        defaultCollapsed: facet.isCollapsedByDefault,\n        options: facet.brands.map((brand) => {\n          const refinedBrand = refinedBrandSearchFilter?.brands.find(\n            (b) => b.entityId === brand.entityId,\n          );\n          const isSelected = filters.brandEntityIds?.includes(brand.entityId) === true;\n          const disabled = refinedBrand == null && !isSelected;\n          const productCountLabel = disabled ? '' : ` (${brand.productCount})`;\n          const label = facet.displayProductCount\n            ? `${brand.name}${productCountLabel}`\n            : brand.name;\n\n          return {\n            label,\n            value: brand.entityId.toString(),\n            disabled,\n          };\n        }),\n      };\n    }\n\n    if (facet.__typename === 'ProductAttributeSearchFilter') {\n      const refinedProductAttributeSearchFilter =\n        refinedFacet.__typename === 'ProductAttributeSearchFilter' ? refinedFacet : null;\n\n      return {\n        type: 'toggle-group' as const,\n        paramName: `attr_${facet.filterKey}`,\n        label: facet.displayName,\n        defaultCollapsed: facet.isCollapsedByDefault,\n        options: facet.attributes.map((attribute) => {\n          const refinedAttribute = refinedProductAttributeSearchFilter?.attributes.find(\n            (a) => a.value === attribute.value,\n          );\n\n          const isSelected =\n            filters.productAttributes?.some((attr) => attr.values.includes(attribute.value)) ===\n            true;\n\n          const disabled = refinedAttribute == null && !isSelected;\n          const productCountLabel = disabled ? '' : ` (${attribute.productCount})`;\n          const label = facet.displayProductCount\n            ? `${attribute.value}${productCountLabel}`\n            : attribute.value;\n\n          return {\n            label,\n            value: attribute.value,\n            disabled,\n          };\n        }),\n      };\n    }\n\n    if (facet.__typename === 'RatingSearchFilter') {\n      const refinedRatingSearchFilter =\n        refinedFacet.__typename === 'RatingSearchFilter' ? refinedFacet : null;\n      const isSelected = filters.rating?.minRating != null;\n\n      return {\n        type: 'rating' as const,\n        paramName: 'minRating',\n        label: facet.displayName,\n        disabled: refinedRatingSearchFilter == null && !isSelected,\n        defaultCollapsed: facet.isCollapsedByDefault,\n      };\n    }\n\n    if (facet.__typename === 'PriceSearchFilter') {\n      const refinedPriceSearchFilter =\n        refinedFacet.__typename === 'PriceSearchFilter' ? refinedFacet : null;\n      const isSelected = filters.price?.minPrice != null || filters.price?.maxPrice != null;\n\n      return {\n        type: 'range' as const,\n        minParamName: 'minPrice',\n        maxParamName: 'maxPrice',\n        label: facet.displayName,\n        min: facet.selected?.minPrice ?? undefined,\n        max: facet.selected?.maxPrice ?? undefined,\n        disabled: refinedPriceSearchFilter == null && !isSelected,\n        defaultCollapsed: facet.isCollapsedByDefault,\n      };\n    }\n\n    if (facet.freeShipping) {\n      const refinedFreeShippingSearchFilter =\n        refinedFacet.__typename === 'OtherSearchFilter' && refinedFacet.freeShipping\n          ? refinedFacet\n          : null;\n      const isSelected = filters.isFreeShipping === true;\n\n      return {\n        type: 'toggle-group' as const,\n        paramName: `shipping`,\n        label: t('freeShippingLabel'),\n        defaultCollapsed: facet.isCollapsedByDefault,\n        options: [\n          {\n            label: t('freeShippingLabel'),\n            value: 'free_shipping',\n            disabled: refinedFreeShippingSearchFilter == null && !isSelected,\n          },\n        ],\n      };\n    }\n\n    if (facet.isFeatured) {\n      const refinedIsFeaturedSearchFilter =\n        refinedFacet.__typename === 'OtherSearchFilter' && refinedFacet.isFeatured\n          ? refinedFacet\n          : null;\n      const isSelected = filters.isFeatured === true;\n\n      return {\n        type: 'toggle-group' as const,\n        paramName: `isFeatured`,\n        label: t('isFeaturedLabel'),\n        defaultCollapsed: facet.isCollapsedByDefault,\n        options: [\n          {\n            label: t('isFeaturedLabel'),\n            value: 'on',\n            disabled: refinedIsFeaturedSearchFilter == null && !isSelected,\n          },\n        ],\n      };\n    }\n\n    if (facet.isInStock) {\n      const refinedIsInStockSearchFilter =\n        refinedFacet.__typename === 'OtherSearchFilter' && refinedFacet.isInStock\n          ? refinedFacet\n          : null;\n      const isSelected = filters.hideOutOfStock === true;\n\n      return {\n        type: 'toggle-group' as const,\n        paramName: `stock`,\n        label: t('inStockLabel'),\n        defaultCollapsed: facet.isCollapsedByDefault,\n        options: [\n          {\n            label: t('inStockLabel'),\n            value: 'in_stock',\n            disabled: refinedIsInStockSearchFilter == null && !isSelected,\n          },\n        ],\n      };\n    }\n\n    return null;\n  });\n};\n"
  },
  {
    "path": "core/data-transformers/form-field-transformer/fragment.ts",
    "content": "import { FragmentOf, graphql } from '~/client/graphql';\n\nexport type FormField = NonNullable<FragmentOf<typeof FormFieldsFragment>>;\n\nexport const FormFieldsFragment = graphql(`\n  fragment FormFieldsFragment on FormField {\n    entityId\n    label\n    sortOrder\n    isBuiltIn\n    isRequired\n    __typename\n    ... on CheckboxesFormField {\n      options {\n        entityId\n        label\n      }\n    }\n    ... on DateFormField {\n      defaultDate\n      minDate\n      maxDate\n    }\n    ... on MultilineTextFormField {\n      defaultText\n      rows\n    }\n    ... on NumberFormField {\n      defaultNumber\n      maxLength\n      minNumber\n      maxNumber\n    }\n    ... on PasswordFormField {\n      defaultText\n      maxLength\n    }\n    ... on PicklistFormField {\n      choosePrefix\n      options {\n        entityId\n        label\n      }\n    }\n    ... on RadioButtonsFormField {\n      options {\n        entityId\n        label\n      }\n    }\n    ... on TextFormField {\n      defaultText\n      maxLength\n    }\n  }\n`);\n\nexport type FormFieldValue = NonNullable<FragmentOf<typeof FormFieldValuesFragment>>;\n\nexport const FormFieldValuesFragment = graphql(`\n  fragment FormFieldValuesFragment on CustomerFormFieldValue {\n    entityId\n    __typename\n    name\n    ... on CheckboxesFormFieldValue {\n      valueEntityIds\n      values\n    }\n    ... on DateFormFieldValue {\n      date {\n        utc\n      }\n    }\n    ... on MultipleChoiceFormFieldValue {\n      valueEntityId\n      value\n    }\n    ... on NumberFormFieldValue {\n      number\n    }\n    ... on PasswordFormFieldValue {\n      password\n    }\n    ... on TextFormFieldValue {\n      text\n    }\n    ... on MultilineTextFormFieldValue {\n      multilineText\n    }\n  }\n`);\n"
  },
  {
    "path": "core/data-transformers/form-field-transformer/index.ts",
    "content": "import { FragmentOf } from 'gql.tada';\n\nimport { Field } from '@/vibes/soul/form/dynamic-form/schema';\n\nimport { FormFieldsFragment } from './fragment';\nimport { FieldNameToFieldId } from './utils';\n\nexport const formFieldTransformer = (\n  field: FragmentOf<typeof FormFieldsFragment> & { name?: string },\n): Field | null => {\n  // If the field name is provided, use it; otherwise, fallback to the entityId mapped name or label.\n  const name = field.name ?? FieldNameToFieldId[Number(field.entityId)] ?? field.label;\n\n  switch (field.__typename) {\n    case 'CheckboxesFormField':\n      return {\n        id: String(field.entityId),\n        type: 'checkbox-group',\n        name,\n        label: field.label,\n        required: field.isRequired,\n        options: field.options.map((option) => ({\n          label: option.label,\n          value: String(option.entityId),\n        })),\n      };\n\n    case 'DateFormField':\n      return {\n        id: String(field.entityId),\n        type: 'date',\n        name,\n        label: field.label,\n        required: field.isRequired,\n        minDate: field.minDate ?? undefined,\n        maxDate: field.maxDate ?? undefined,\n      };\n\n    case 'MultilineTextFormField':\n      return {\n        id: String(field.entityId),\n        type: 'textarea',\n        name,\n        label: field.label,\n        required: field.isRequired,\n      };\n\n    case 'NumberFormField':\n      return {\n        id: String(field.entityId),\n        type: 'number',\n        name,\n        label: field.label,\n        required: field.isRequired,\n      };\n\n    case 'PasswordFormField':\n      return {\n        id: String(field.entityId),\n        type:\n          field.entityId === FieldNameToFieldId.confirmPassword ? 'confirm-password' : 'password',\n        name,\n        label: field.label,\n        required: field.isRequired,\n      };\n\n    case 'PicklistFormField':\n      if (field.entityId === FieldNameToFieldId.countryCode) {\n        return {\n          id: String(field.entityId),\n          type: 'select',\n          name,\n          label: field.label,\n          required: field.isRequired,\n          options: field.options.map((option) => ({\n            label: option.label,\n            value: String(option.entityId),\n          })),\n        };\n      }\n\n      return {\n        id: String(field.entityId),\n        type: 'button-radio-group',\n        name,\n        label: field.label,\n        required: field.isRequired,\n        options: field.options.map((option) => ({\n          label: option.label,\n          value: String(option.entityId),\n        })),\n      };\n\n    case 'RadioButtonsFormField':\n      return {\n        id: String(field.entityId),\n        type: 'radio-group',\n        name,\n        label: field.label,\n        required: field.isRequired,\n        options: field.options.map((option) => ({\n          label: option.label,\n          value: String(option.entityId),\n        })),\n      };\n\n    case 'PicklistOrTextFormField':\n    case 'TextFormField':\n      return {\n        id: String(field.entityId),\n        type: field.entityId === FieldNameToFieldId.email ? 'email' : 'text',\n        name,\n        label: field.label,\n        required: field.isRequired,\n      };\n\n    default:\n      return null;\n  }\n};\n\nexport const injectCountryCodeOptions = (\n  field: Field,\n  countries: Array<{ code: string; name: string }>,\n): Field => {\n  if (field.type === 'select' && field.id === String(FieldNameToFieldId.countryCode)) {\n    return {\n      ...field,\n      options: countries.map((country) => ({\n        label: country.name,\n        value: country.code,\n      })),\n    };\n  }\n\n  return field;\n};\n"
  },
  {
    "path": "core/data-transformers/form-field-transformer/utils.ts",
    "content": "import { exists } from '~/lib/utils';\n\nimport { FormField, FormFieldValue } from './fragment';\n\n/* This mapping needed for aligning built-in fields names to their ids\n for creating valid register customer request object\n that will be sent in mutation */\nexport enum FieldNameToFieldId {\n  email = 1,\n  password,\n  confirmPassword,\n  firstName,\n  lastName,\n  company,\n  phone,\n  address1,\n  address2,\n  city,\n  countryCode,\n  stateOrProvince,\n  postalCode,\n  currentPassword = 24,\n  exclusiveOffers = 25,\n}\n\nexport const CUSTOMER_FIELDS_TO_EXCLUDE = [FieldNameToFieldId.currentPassword];\n\nexport const REGISTER_CUSTOMER_FORM_LAYOUT = [\n  [FieldNameToFieldId.firstName, FieldNameToFieldId.lastName],\n  FieldNameToFieldId.email,\n  FieldNameToFieldId.password,\n  FieldNameToFieldId.confirmPassword,\n  FieldNameToFieldId.company,\n  FieldNameToFieldId.phone,\n  FieldNameToFieldId.address1,\n  FieldNameToFieldId.address2,\n  [FieldNameToFieldId.city, FieldNameToFieldId.stateOrProvince],\n  [FieldNameToFieldId.postalCode, FieldNameToFieldId.countryCode],\n];\n\nexport const ADDRESS_FORM_LAYOUT = [\n  [FieldNameToFieldId.firstName, FieldNameToFieldId.lastName],\n  FieldNameToFieldId.company,\n  FieldNameToFieldId.phone,\n  FieldNameToFieldId.address1,\n  FieldNameToFieldId.address2,\n  [FieldNameToFieldId.city, FieldNameToFieldId.stateOrProvince],\n  [FieldNameToFieldId.postalCode, FieldNameToFieldId.countryCode],\n];\n\nexport const mapFormFieldValueToName = (field: FormFieldValue): Record<string, unknown> => {\n  switch (field.__typename) {\n    case 'CheckboxesFormFieldValue':\n      return {\n        [field.name]: field.valueEntityIds,\n      };\n\n    case 'DateFormFieldValue':\n      return {\n        [field.name]: field.date.utc,\n      };\n\n    case 'MultipleChoiceFormFieldValue':\n      return {\n        [field.name]: field.valueEntityId,\n      };\n\n    case 'NumberFormFieldValue':\n      return {\n        [field.name]: field.number,\n      };\n\n    case 'PasswordFormFieldValue':\n      return {\n        [field.name]: field.password,\n      };\n\n    case 'TextFormFieldValue':\n      return {\n        [field.name]: field.text,\n      };\n\n    case 'MultilineTextFormFieldValue':\n      return {\n        [field.name]: field.multilineText,\n      };\n\n    default:\n      return {};\n  }\n};\n\nexport const transformFieldsToLayout = (\n  fields: FormField[],\n  layout: Array<FieldNameToFieldId | FieldNameToFieldId[]>,\n): Array<FormField | FormField[]> => {\n  const fieldMap = new Map(fields.map((field) => [field.entityId, field]));\n\n  const presetLayout = layout\n    .map((row) => {\n      if (Array.isArray(row)) {\n        return row.map((fieldId) => fieldMap.get(fieldId)).filter<FormField>(exists);\n      }\n\n      return fieldMap.get(row);\n    })\n    .filter<FormField | FormField[]>(exists);\n\n  const usedFieldIds = new Set(layout.flatMap((row) => (Array.isArray(row) ? row : [row])));\n\n  const remainingLayout: FormField[] = fields.filter((field) => !usedFieldIds.has(field.entityId));\n\n  return [...presetLayout, ...remainingLayout];\n};\n"
  },
  {
    "path": "core/data-transformers/logo-transformer.ts",
    "content": "import { ResultOf } from 'gql.tada';\n\nimport { StoreLogoFragment } from '~/components/store-logo/fragment';\n\nexport const logoTransformer = (data: ResultOf<typeof StoreLogoFragment>) => {\n  const { logoV2: logo } = data;\n\n  if (logo.__typename === 'StoreTextLogo') {\n    return logo.text;\n  }\n\n  return { src: logo.image.url, alt: logo.image.altText };\n};\n"
  },
  {
    "path": "core/data-transformers/order-details-transformer.ts",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { getFormatter, getTranslations } from 'next-intl/server';\n\nimport { Order } from '@/vibes/soul/sections/order-details-section';\nimport { ExistingResultType } from '~/client/util';\n\nimport { getCustomerOrderDetails } from '../app/[locale]/(default)/account/orders/[id]/page-data';\n\nfunction getStatusColor(\n  status: ExistingResultType<typeof getCustomerOrderDetails>['status']['value'],\n) {\n  switch (status) {\n    case 'AWAITING_FULFILLMENT':\n    case 'AWAITING_SHIPMENT':\n    case 'AWAITING_PICKUP':\n    case 'PARTIALLY_SHIPPED':\n    case 'PENDING':\n    case 'SHIPPED':\n      return 'info';\n\n    case 'AWAITING_PAYMENT':\n    case 'DISPUTED':\n    case 'INCOMPLETE':\n    case 'MANUAL_VERIFICATION_REQUIRED':\n      return 'warning';\n\n    case 'CANCELLED':\n    case 'DECLINED':\n      return 'error';\n\n    case 'COMPLETED':\n    case 'PARTIALLY_REFUNDED':\n    case 'REFUNDED':\n      return 'success';\n  }\n}\n\nexport const orderDetailsTransformer = (\n  order: ExistingResultType<typeof getCustomerOrderDetails>,\n  t: ExistingResultType<typeof getTranslations<'Account.Orders.Details'>>,\n  format: ExistingResultType<typeof getFormatter>,\n): Order => {\n  const paymentMethods = removeEdgesAndNodes(order.payments).map((payment) => {\n    if (payment.detail?.__typename === 'CreditCardPaymentInstrument') {\n      return {\n        title: t('PaymentMethods.creditCard'),\n        subtitle: `${payment.detail.brand} ${t('paymentEndingInLabel')} ${payment.detail.last4}`,\n        amount: format.number(payment.amount.value, {\n          style: 'currency',\n          currency: payment.amount.currencyCode,\n        }),\n      };\n    }\n\n    let paymentMethod: string;\n\n    // Attempt to use translated names for known payment methods\n    if (payment.detail?.__typename === 'GiftCertificatePaymentInstrument') {\n      paymentMethod = t('PaymentMethods.giftCertificate');\n    } else if (payment.paymentMethodName === 'Store Credit') {\n      paymentMethod = t('PaymentMethods.storeCredit');\n    } else if (payment.paymentMethodName !== '') {\n      paymentMethod = payment.paymentMethodName;\n    } else {\n      paymentMethod = t('PaymentMethods.other');\n    }\n\n    return {\n      title: paymentMethod,\n      subtitle:\n        payment.detail?.__typename === 'GiftCertificatePaymentInstrument'\n          ? payment.detail.code\n          : undefined,\n      amount: format.number(payment.amount.value, {\n        style: 'currency',\n        currency: payment.amount.currencyCode,\n      }),\n    };\n  });\n\n  return {\n    date: format.dateTime(new Date(order.orderedAt.utc)),\n    id: String(order.entityId),\n    status: order.status.label,\n    statusColor: getStatusColor(order.status.value),\n    destinations:\n      order.consignments.shipping?.map((consignment, index, arr) => {\n        return {\n          id: String(consignment.entityId),\n          lineItems: consignment.lineItems.map((lineItem) => {\n            const price = lineItem.catalogProductWithOptionSelections?.prices?.price\n              ? format.number(lineItem.catalogProductWithOptionSelections.prices.price.value, {\n                  style: 'currency',\n                  currency: lineItem.catalogProductWithOptionSelections.prices.price.currencyCode,\n                })\n              : format.number(lineItem.subTotalListPrice.value / lineItem.quantity, {\n                  style: 'currency',\n                  currency: lineItem.subTotalListPrice.currencyCode,\n                });\n\n            return {\n              id: String(lineItem.entityId),\n              title: lineItem.name,\n              subtitle: lineItem.brand ?? '',\n              price,\n              totalPrice: format.number(lineItem.subTotalListPrice.value, {\n                style: 'currency',\n                currency: lineItem.subTotalListPrice.currencyCode,\n              }),\n              href: lineItem.baseCatalogProduct?.path ?? undefined,\n              image: lineItem.image\n                ? {\n                    src: lineItem.image.url,\n                    alt: lineItem.image.altText,\n                  }\n                : undefined,\n              quantity: lineItem.quantity,\n              metadata: lineItem.productOptions.map((option) => ({\n                label: option.name,\n                value: option.value,\n              })),\n            };\n          }),\n          title:\n            arr.length > 1\n              ? t('destinationWithCount', { number: index + 1, total: arr.length })\n              : t('destination'),\n          address: {\n            city: consignment.shippingAddress.city ?? '',\n            country: consignment.shippingAddress.country,\n            state: consignment.shippingAddress.stateOrProvince ?? '',\n            street1: consignment.shippingAddress.address1 ?? '',\n            street2: consignment.shippingAddress.address2 ?? '',\n            name: `${consignment.shippingAddress.firstName} ${consignment.shippingAddress.lastName}`,\n            zipcode: consignment.shippingAddress.postalCode,\n          },\n          shipments: consignment.shipments.map((shipment) => {\n            return {\n              name: shipment.shippingMethodName,\n              status: format.dateTime(new Date(shipment.shippedAt.utc)),\n              tracking: shipment.tracking ?? undefined,\n            };\n          }),\n        };\n      }) ?? [],\n    emailDestinations:\n      order.consignments.email?.map(({ email, lineItems }) => ({\n        title: t('digitalDelivery', { email }),\n        email,\n        lineItems: lineItems.map((item) => ({\n          id: String(item.entityId),\n          title: item.name,\n          price: format.number(item.salePrice.value, {\n            style: 'currency',\n            currency: item.salePrice.currencyCode,\n          }),\n          totalPrice: format.number(item.salePrice.value, {\n            style: 'currency',\n            currency: item.salePrice.currencyCode,\n          }),\n          quantity: 1,\n        })),\n      })) ?? [],\n    summary: {\n      total: format.number(order.totalIncTax.value, {\n        style: 'currency',\n        currency: order.totalIncTax.currencyCode,\n      }),\n      lineItems: [\n        {\n          label: t('subtotal'),\n          value: format.number(order.subTotal.value, {\n            style: 'currency',\n            currency: order.subTotal.currencyCode,\n          }),\n        },\n        ...order.discounts.couponDiscounts.map((discount) => {\n          return {\n            label: discount.couponCode,\n            value: `-${format.number(discount.discountedAmount.value, {\n              style: 'currency',\n              currency: discount.discountedAmount.currencyCode,\n            })}`,\n          };\n        }),\n        {\n          label: t('shipping'),\n          value: format.number(order.shippingCostTotal.value, {\n            style: 'currency',\n            currency: order.shippingCostTotal.currencyCode,\n          }),\n        },\n        {\n          label: t('tax'),\n          value: format.number(order.taxTotal.value, {\n            style: 'currency',\n            currency: order.taxTotal.currencyCode,\n          }),\n        },\n      ],\n    },\n    paymentsSummary: {\n      title: t('paymentMethodsLabel', { count: paymentMethods.length }),\n      payments: paymentMethods,\n    },\n  };\n};\n"
  },
  {
    "path": "core/data-transformers/orders-transformer.ts",
    "content": "import { getFormatter } from 'next-intl/server';\n\nimport { Order } from '@/vibes/soul/sections/order-list';\nimport { getCustomerOrders } from '~/app/[locale]/(default)/account/orders/page-data';\nimport { ExistingResultType } from '~/client/util';\n\nexport const ordersTransformer = (\n  orders: ExistingResultType<typeof getCustomerOrders>['orders'],\n  format: ExistingResultType<typeof getFormatter>,\n): Order[] => {\n  return orders.map((order) => {\n    const lineItems =\n      order.consignments.shipping?.flatMap((consignment) => {\n        return consignment.lineItems.map((lineItem) => {\n          const price = lineItem.catalogProductWithOptionSelections?.prices?.price\n            ? format.number(lineItem.catalogProductWithOptionSelections.prices.price.value, {\n                style: 'currency',\n                currency: lineItem.catalogProductWithOptionSelections.prices.price.currencyCode,\n              })\n            : format.number(lineItem.subTotalListPrice.value / lineItem.quantity, {\n                style: 'currency',\n                currency: lineItem.subTotalListPrice.currencyCode,\n              });\n\n          return {\n            id: lineItem.entityId.toString(),\n            href: lineItem.baseCatalogProduct?.path ?? '#',\n            title: lineItem.name,\n            subtitle: lineItem.brand ?? undefined,\n            price,\n            totalPrice: format.number(lineItem.subTotalListPrice.value, {\n              style: 'currency',\n              currency: lineItem.subTotalListPrice.currencyCode,\n            }),\n            image: lineItem.image\n              ? {\n                  src: lineItem.image.url,\n                  alt: lineItem.image.altText,\n                }\n              : undefined,\n          };\n        });\n      }) ?? [];\n\n    const giftCertificates =\n      order.consignments.email?.flatMap((consignment) => {\n        return consignment.lineItems.map((lineItem) => {\n          return {\n            id: lineItem.entityId.toString(),\n            href: '#',\n            title: lineItem.name,\n            price: '',\n            totalPrice: '',\n            image: undefined,\n          };\n        });\n      }) ?? [];\n\n    return {\n      id: order.entityId.toString(),\n      href: `/account/orders/${order.entityId}`,\n      status: order.status.label,\n      totalPrice: format.number(order.totalIncTax.value, {\n        style: 'currency',\n        currency: order.totalIncTax.currencyCode,\n      }),\n      lineItems: [...lineItems, ...giftCertificates].flat(),\n    } satisfies Order;\n  });\n};\n"
  },
  {
    "path": "core/data-transformers/page-info-transformer.ts",
    "content": "import { ResultOf } from 'gql.tada';\n\nimport { CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination';\nimport { PaginationFragment } from '~/client/fragments/pagination';\n\nexport const defaultPageInfo = {\n  hasNextPage: false,\n  hasPreviousPage: false,\n  startCursor: null,\n  endCursor: null,\n};\n\nexport function pageInfoTransformer(\n  pageInfo: ResultOf<typeof PaginationFragment>,\n  {\n    startCursorParamName = 'before',\n    endCursorParamName = 'after',\n  }: { startCursorParamName?: string; endCursorParamName?: string } = {},\n): CursorPaginationInfo {\n  return {\n    startCursorParamName,\n    startCursor: pageInfo.hasPreviousPage ? pageInfo.startCursor : null,\n    endCursorParamName,\n    endCursor: pageInfo.hasNextPage ? pageInfo.endCursor : null,\n  };\n}\n"
  },
  {
    "path": "core/data-transformers/prices-transformer.ts",
    "content": "import { ResultOf } from 'gql.tada';\nimport { getFormatter } from 'next-intl/server';\n\nimport { Price } from '@/vibes/soul/primitives/price-label';\nimport { PricingFragment } from '~/client/fragments/pricing';\nimport { ExistingResultType } from '~/client/util';\n\nexport const pricesTransformer = (\n  prices: ResultOf<typeof PricingFragment>['prices'],\n  format: ExistingResultType<typeof getFormatter>,\n): Price | undefined => {\n  if (!prices) {\n    return undefined;\n  }\n\n  const isPriceRange = prices.priceRange.min.value !== prices.priceRange.max.value;\n  const isSalePrice = prices.salePrice?.value !== prices.basePrice?.value;\n\n  if (isPriceRange) {\n    return {\n      type: 'range',\n      minValue: format.number(prices.priceRange.min.value, {\n        style: 'currency',\n        currency: prices.price.currencyCode,\n      }),\n      maxValue: format.number(prices.priceRange.max.value, {\n        style: 'currency',\n        currency: prices.price.currencyCode,\n      }),\n    };\n  }\n\n  if (isSalePrice && prices.salePrice && prices.basePrice) {\n    return {\n      type: 'sale',\n      previousValue: format.number(prices.basePrice.value, {\n        style: 'currency',\n        currency: prices.price.currencyCode,\n      }),\n      currentValue: format.number(prices.price.value, {\n        style: 'currency',\n        currency: prices.price.currencyCode,\n      }),\n    };\n  }\n\n  return format.number(prices.price.value, {\n    style: 'currency',\n    currency: prices.price.currencyCode,\n  });\n};\n"
  },
  {
    "path": "core/data-transformers/product-card-transformer.ts",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { ResultOf } from 'gql.tada';\nimport { getFormatter } from 'next-intl/server';\n\nimport { Product } from '@/vibes/soul/primitives/product-card';\nimport { ExistingResultType } from '~/client/util';\nimport { ProductCardFragment } from '~/components/product-card/fragment';\nimport { WishlistItemProductFragment } from '~/components/wishlist/fragment';\n\nimport { pricesTransformer } from './prices-transformer';\n\nconst getInventoryMessage = (\n  product: ResultOf<typeof ProductCardFragment>,\n  outOfStockMessage?: string,\n  showBackorderMessage?: boolean,\n) => {\n  if (!product.inventory.isInStock) {\n    return outOfStockMessage;\n  }\n\n  if (!showBackorderMessage || product.inventory.hasVariantInventory) {\n    return undefined;\n  }\n\n  const { availableForBackorder, unlimitedBackorder, availableOnHand } =\n    product.inventory.aggregated ?? {};\n\n  if (availableOnHand) {\n    return undefined;\n  }\n\n  const hasBackorderAvailablity = !!availableForBackorder || unlimitedBackorder;\n\n  if (!hasBackorderAvailablity) {\n    return undefined;\n  }\n\n  const baseVariant = removeEdgesAndNodes(product.variants).at(0);\n\n  if (!baseVariant?.inventory?.byLocation) {\n    return undefined;\n  }\n\n  const inventoryByLocation = removeEdgesAndNodes(baseVariant.inventory.byLocation).at(0);\n\n  return inventoryByLocation?.backorderMessage ?? undefined;\n};\n\nexport const singleProductCardTransformer = (\n  product: ResultOf<typeof ProductCardFragment | typeof WishlistItemProductFragment>,\n  format: ExistingResultType<typeof getFormatter>,\n  outOfStockMessage?: string,\n  showBackorderMessage?: boolean,\n): Product => {\n  return {\n    id: product.entityId.toString(),\n    title: product.name,\n    href: product.path,\n    image: product.defaultImage\n      ? { src: product.defaultImage.url, alt: product.defaultImage.altText }\n      : undefined,\n    price: pricesTransformer(product.prices, format),\n    subtitle: product.brand?.name ?? undefined,\n    rating: product.reviewSummary.averageRating,\n    numberOfReviews: product.reviewSummary.numberOfReviews,\n    inventoryMessage:\n      'variants' in product\n        ? getInventoryMessage(product, outOfStockMessage, showBackorderMessage)\n        : undefined,\n  };\n};\n\nexport const productCardTransformer = (\n  products: Array<ResultOf<typeof ProductCardFragment | typeof WishlistItemProductFragment>>,\n  format: ExistingResultType<typeof getFormatter>,\n  outOfStockMessage?: string,\n  showBackorderMessage?: boolean,\n): Product[] => {\n  return products.map((product) =>\n    singleProductCardTransformer(product, format, outOfStockMessage, showBackorderMessage),\n  );\n};\n"
  },
  {
    "path": "core/data-transformers/product-options-transformer.ts",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { ResultOf } from 'gql.tada';\nimport { getTranslations } from 'next-intl/server';\n\nimport { Field } from '@/vibes/soul/sections/product-detail/schema';\nimport { ProductOptionsFragment } from '~/app/[locale]/(default)/product/[slug]/page-data';\n\nexport const productOptionsTransformer = async (\n  productOptions: ResultOf<typeof ProductOptionsFragment>['productOptions'],\n) => {\n  const t = await getTranslations('Product.ProductDetails');\n\n  return removeEdgesAndNodes(productOptions)\n    .map<Field | null>((option) => {\n      if (option.__typename === 'MultipleChoiceOption') {\n        const values = removeEdgesAndNodes(option.values);\n\n        switch (option.displayStyle) {\n          case 'Swatch': {\n            return {\n              persist: true,\n              type: 'swatch-radio-group',\n              label: option.displayName,\n              required: option.isRequired,\n              name: option.entityId.toString(),\n              defaultValue: values.find((value) => value.isDefault)?.entityId.toString(),\n              options: values\n                .filter(\n                  (value) => '__typename' in value && value.__typename === 'SwatchOptionValue',\n                )\n                .map((value) => {\n                  if (value.imageUrl) {\n                    return {\n                      type: 'image',\n                      label: value.label,\n                      value: value.entityId.toString(),\n                      image: { src: value.imageUrl, alt: value.label },\n                    };\n                  }\n\n                  return {\n                    type: 'color',\n                    label: value.label,\n                    value: value.entityId.toString(),\n                    color: value.hexColors[0] ?? '',\n                  };\n                }),\n            };\n          }\n\n          case 'RectangleBoxes': {\n            return {\n              persist: true,\n              type: 'button-radio-group',\n              label: option.displayName,\n              required: option.isRequired,\n              name: option.entityId.toString(),\n              defaultValue: values.find((value) => value.isDefault)?.entityId.toString(),\n              options: values.map((value) => ({\n                label: value.label,\n                value: value.entityId.toString(),\n              })),\n            };\n          }\n\n          case 'RadioButtons': {\n            return {\n              persist: true,\n              type: 'radio-group',\n              label: option.displayName,\n              required: option.isRequired,\n              name: option.entityId.toString(),\n              defaultValue: values.find((value) => value.isDefault)?.entityId.toString(),\n              options: values.map((value) => ({\n                label: value.label,\n                value: value.entityId.toString(),\n              })),\n            };\n          }\n\n          case 'DropdownList': {\n            return {\n              persist: true,\n              type: 'select',\n              label: option.displayName,\n              required: option.isRequired,\n              name: option.entityId.toString(),\n              defaultValue: values.find((value) => value.isDefault)?.entityId.toString(),\n              options: values.map((value) => ({\n                label: value.label,\n                value: value.entityId.toString(),\n              })),\n            };\n          }\n\n          case 'ProductPickList': {\n            return {\n              persist: true,\n              type: 'card-radio-group',\n              label: option.displayName,\n              required: option.isRequired,\n              name: option.entityId.toString(),\n              defaultValue: values.find((value) => value.isDefault)?.entityId.toString(),\n              options: values\n                .filter(\n                  (value) =>\n                    '__typename' in value && value.__typename === 'ProductPickListOptionValue',\n                )\n                .map((value) => ({\n                  label: value.label,\n                  value: value.entityId.toString(),\n                })),\n            };\n          }\n\n          case 'ProductPickListWithImages': {\n            return {\n              persist: true,\n              type: 'card-radio-group',\n              label: option.displayName,\n              required: option.isRequired,\n              name: option.entityId.toString(),\n              defaultValue: values.find((value) => value.isDefault)?.entityId.toString(),\n              options: values\n                .filter(\n                  (value) =>\n                    '__typename' in value && value.__typename === 'ProductPickListOptionValue',\n                )\n                .map((value) => ({\n                  label: value.label,\n                  value: value.entityId.toString(),\n                  image: {\n                    src: value.defaultImage?.url ?? '',\n                    alt: value.defaultImage?.altText ?? '',\n                  },\n                })),\n            };\n          }\n\n          default:\n            return null;\n        }\n      }\n\n      if (option.__typename === 'CheckboxOption') {\n        return {\n          persist: true,\n          type: 'checkbox',\n          label: option.displayName,\n          required: option.isRequired,\n          name: option.entityId.toString(),\n          defaultValue: option.checkedByDefault.toString(),\n          uncheckedValue: option.uncheckedOptionValueEntityId.toString(),\n          checkedValue: option.checkedOptionValueEntityId.toString(),\n        };\n      }\n\n      if (option.__typename === 'NumberFieldOption') {\n        return {\n          persist: false,\n          type: 'number',\n          label: option.displayName,\n          required: option.isRequired,\n          name: option.entityId.toString(),\n          defaultValue: option.defaultNumber?.toString(),\n          min: option.lowest ?? undefined,\n          max: option.highest ?? undefined,\n          incrementLabel: t('increaseNumber'),\n          decrementLabel: t('decreaseNumber'),\n          // TODO: should we take into account other properties from API like isIntegerOnly, limitNumberBy?\n          // https://developer.bigcommerce.com/graphql-storefront/reference#definition-LimitInputBy\n        };\n      }\n\n      if (option.__typename === 'MultiLineTextFieldOption') {\n        return {\n          persist: false,\n          type: 'textarea',\n          label: option.displayName,\n          required: option.isRequired,\n          name: option.entityId.toString(),\n          defaultValue: option.defaultText ?? undefined,\n          minLength: option.minLength ?? undefined,\n          maxLength: option.maxLength ?? undefined,\n        };\n      }\n\n      if (option.__typename === 'TextFieldOption') {\n        return {\n          persist: false,\n          type: 'text',\n          label: option.displayName,\n          required: option.isRequired,\n          name: option.entityId.toString(),\n          defaultValue: option.defaultText ?? undefined,\n        };\n      }\n\n      if (option.__typename === 'DateFieldOption') {\n        return {\n          persist: false,\n          type: 'date',\n          label: option.displayName,\n          required: option.isRequired,\n          name: option.entityId.toString(),\n          defaultValue: option.defaultDate ?? undefined,\n        };\n      }\n\n      return null;\n    })\n    .filter((field) => field !== null);\n};\n"
  },
  {
    "path": "core/data-transformers/scripts-transformer.ts",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport type { ConsentManagerProviderProps } from '@c15t/nextjs/client';\n\nimport type { ResultOf } from '~/client/graphql';\nimport type { ScriptsFragment } from '~/components/consent-manager/scripts-fragment';\n\ntype BigCommerceScriptsResult = ResultOf<typeof ScriptsFragment>;\ntype BigCommerceScripts = BigCommerceScriptsResult['scripts'] | null;\ntype C15tScripts = NonNullable<ConsentManagerProviderProps['options']['scripts']>;\ntype C15tScript = C15tScripts[number];\n\nconst BC_TO_C15T_CONSENT_CATEGORY_MAP = {\n  ESSENTIAL: 'necessary',\n  UNKNOWN: 'necessary',\n  FUNCTIONAL: 'functionality',\n  ANALYTICS: 'measurement',\n  TARGETING: 'marketing',\n} as const;\n\ntype ScriptInfo = { textContent: string } | { src: string } | null;\n\n/**\n * Extracts HTML attributes from a script tag's opening element.\n * Handles both boolean attributes (async, defer) and key-value attributes (data-*, crossorigin).\n * @param {string} scriptTag - The full script tag HTML string\n * @returns {Record<string, string>} Record of attribute name-value pairs (boolean attrs get empty string value)\n */\nfunction extractAttributes(scriptTag: string): Record<string, string> {\n  // Extract the opening tag content (everything between <script and >)\n  const openingTagMatch = /<script([^>]*)>/i.exec(scriptTag);\n  const openingTagContent = openingTagMatch?.[1];\n\n  if (!openingTagContent) {\n    return {};\n  }\n\n  // Match attributes in formats: name=\"value\", name='value', or name (boolean)\n  const attributePattern = /([a-zA-Z][\\w-]*)\\s*(?:=\\s*[\"']([^\"']*)[\"'])?/g;\n  const matches = Array.from(openingTagContent.matchAll(attributePattern));\n\n  // Convert matches to record, filtering out 'src' which is handled separately\n  return matches.reduce<Record<string, string>>((acc, match) => {\n    const name = match[1];\n    const value = match[2];\n\n    // Skip if no name or if it's 'src' (handled separately)\n    if (name && name.toLowerCase() !== 'src') {\n      // Boolean attributes get empty string, others get their value\n      acc[name] = value ?? '';\n    }\n\n    return acc;\n  }, {});\n}\n\n// C15T's ClientSideOptionsProvider creates the <script> element at runtime. BigCommerce's API\n// returns inline scripts wrapped in <script> tags, which would result in nested <script> elements\n// and cause errors. This function extracts both inline content and src attributes to handle cases\n// where InlineScript types may contain external script references.\nfunction extractScriptInfo(scriptTag: string): ScriptInfo {\n  // Extract attributes first (works for both inline and src scripts)\n  // Extract text content (for inline scripts)\n  const scriptMatch = /<script[^>]*>([\\s\\S]*?)<\\/script>/i.exec(scriptTag);\n  const textContent = scriptMatch?.[1]?.trim();\n\n  if (textContent) {\n    return {\n      textContent,\n    };\n  }\n\n  // Extract src attribute (for external scripts that may be misclassified as inline)\n  const srcMatch = /<script[^>]*\\ssrc=[\"']([^\"']+)[\"']/i.exec(scriptTag);\n  const src = srcMatch?.[1];\n\n  if (src) {\n    return {\n      src,\n    };\n  }\n\n  return null;\n}\n\nfunction isValidConsentCategory(key: string): key is keyof typeof BC_TO_C15T_CONSENT_CATEGORY_MAP {\n  return key in BC_TO_C15T_CONSENT_CATEGORY_MAP;\n}\n\nfunction mapConsentCategory(\n  bigCommerceCategory: string,\n): 'necessary' | 'functionality' | 'marketing' | 'measurement' | 'experience' {\n  if (isValidConsentCategory(bigCommerceCategory)) {\n    return BC_TO_C15T_CONSENT_CATEGORY_MAP[bigCommerceCategory];\n  }\n\n  return BC_TO_C15T_CONSENT_CATEGORY_MAP.UNKNOWN;\n}\n\nexport function scriptsTransformer(scripts: BigCommerceScripts): C15tScripts {\n  if (!scripts?.edges) return [];\n\n  const scriptNodes = removeEdgesAndNodes(scripts);\n\n  return scriptNodes.map((script) => {\n    const baseConfig: C15tScript = {\n      category: mapConsentCategory(script.consentCategory),\n      id: script.entityId,\n      target: script.location === 'HEAD' ? 'head' : 'body',\n    };\n\n    const integrityHashes = script.integrityHashes.map((h) => h.hash).filter(Boolean);\n    const attributes = integrityHashes.length\n      ? { integrity: integrityHashes.join(' ') }\n      : undefined;\n\n    if (script.__typename === 'InlineScript' && script.scriptTag) {\n      const scriptInfo = extractScriptInfo(script.scriptTag);\n      const additionalAttributes = extractAttributes(script.scriptTag);\n\n      // Prefer textContent if available (true inline script)\n      if (scriptInfo !== null && 'textContent' in scriptInfo) {\n        return {\n          ...baseConfig,\n          textContent: scriptInfo.textContent,\n          attributes: { ...additionalAttributes, ...attributes },\n        };\n      }\n\n      if (scriptInfo !== null && 'src' in scriptInfo) {\n        return {\n          ...baseConfig,\n          src: scriptInfo.src,\n          attributes: { ...additionalAttributes, ...attributes },\n        };\n      }\n    }\n\n    if (script.__typename === 'SrcScript' && script.src) {\n      return { ...baseConfig, src: script.src, attributes };\n    }\n\n    return { ...baseConfig, attributes };\n  });\n}\n"
  },
  {
    "path": "core/data-transformers/search-results-transformer.ts",
    "content": "import { ResultOf } from 'gql.tada';\nimport { getFormatter, getTranslations } from 'next-intl/server';\n\nimport { SearchResult } from '@/vibes/soul/primitives/navigation';\nimport { SearchProductFragment } from '~/components/header/_actions/fragment';\n\nimport { pricesTransformer } from './prices-transformer';\n\nexport async function searchResultsTransformer(\n  searchProducts: Array<ResultOf<typeof SearchProductFragment>>,\n): Promise<SearchResult[]> {\n  const format = await getFormatter();\n  const t = await getTranslations('Components.Header.Search');\n\n  const productResults: SearchResult = {\n    type: 'products',\n    title: t('products'),\n    products: searchProducts.map((product) => {\n      const price = pricesTransformer(product.prices, format);\n\n      return {\n        id: product.entityId.toString(),\n        title: product.name,\n        href: product.path,\n        image: product.defaultImage\n          ? { src: product.defaultImage.url, alt: product.defaultImage.altText }\n          : undefined,\n        price,\n      };\n    }),\n  };\n\n  const categoryResults: SearchResult = {\n    type: 'links',\n    title: t('categories'),\n    links:\n      searchProducts.length > 0\n        ? Object.entries(\n            searchProducts.reduce<Record<string, string>>((categories, product) => {\n              product.categories.edges?.forEach((category) => {\n                categories[category.node.name] = category.node.path;\n              });\n\n              return categories;\n            }, {}),\n          ).map(([name, path]) => {\n            return { label: name, href: path };\n          })\n        : [],\n  };\n\n  const brandResults: SearchResult = {\n    type: 'links',\n    title: t('brands'),\n    links:\n      searchProducts.length > 0\n        ? Object.entries(\n            searchProducts.reduce<Record<string, string>>((brands, product) => {\n              if (product.brand) {\n                brands[product.brand.name] = product.brand.path;\n              }\n\n              return brands;\n            }, {}),\n          ).map(([name, path]) => {\n            return { label: name, href: path };\n          })\n        : [],\n  };\n\n  const results = [];\n\n  if (categoryResults.links.length > 0) {\n    results.push(categoryResults);\n  }\n\n  if (brandResults.links.length > 0) {\n    results.push(brandResults);\n  }\n\n  if (productResults.products.length > 0) {\n    results.push(productResults);\n  }\n\n  return results;\n}\n"
  },
  {
    "path": "core/data-transformers/wishlists-transformer.ts",
    "content": "import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';\nimport { getFormatter, getTranslations } from 'next-intl/server';\n\nimport { WishlistItem } from '@/vibes/soul/primitives/wishlist-item-card';\nimport { Wishlist } from '@/vibes/soul/sections/wishlist-details';\nimport { ResultOf } from '~/client/graphql';\nimport { ExistingResultType } from '~/client/util';\nimport {\n  PublicWishlistFragment,\n  WishlistFragment,\n  WishlistItemProductFragment,\n  WishlistPaginatedItemsFragment,\n  WishlistsFragment,\n} from '~/components/wishlist/fragment';\n\nimport { singleProductCardTransformer } from './product-card-transformer';\n\nconst getCtaLabel = (\n  product: ResultOf<typeof WishlistItemProductFragment>,\n  pt: ExistingResultType<typeof getTranslations<'Product.ProductDetails'>>,\n): string => {\n  if (product.availabilityV2.status === 'Unavailable') {\n    return pt('Submit.unavailable');\n  }\n\n  if (product.availabilityV2.status === 'Preorder') {\n    return pt('Submit.preorder');\n  }\n\n  if (!product.inventory.isInStock) {\n    return pt('Submit.outOfStock');\n  }\n\n  return pt('Submit.addToCart');\n};\n\nconst getCtaDisabled = (product: ResultOf<typeof WishlistItemProductFragment>): boolean => {\n  if (product.availabilityV2.status === 'Unavailable') {\n    return true;\n  }\n\n  if (product.availabilityV2.status === 'Preorder') {\n    return false;\n  }\n\n  if (!product.inventory.isInStock) {\n    return true;\n  }\n\n  return false;\n};\n\nfunction wishlistItemsTransformer(\n  wishlistItems: ResultOf<typeof WishlistFragment | typeof WishlistPaginatedItemsFragment>['items'],\n  formatter: ExistingResultType<typeof getFormatter>,\n  pt?: ExistingResultType<typeof getTranslations<'Product.ProductDetails'>>,\n): WishlistItem[] {\n  return removeEdgesAndNodes(wishlistItems)\n    .filter(\n      (item): item is typeof item & { product: NonNullable<typeof item.product> } =>\n        item.product !== null,\n    )\n    .map((item) => ({\n      itemId: item.entityId.toString(),\n      productId: item.productEntityId.toString(),\n      variantId: item.variantEntityId?.toString() ?? undefined,\n      callToAction: pt\n        ? {\n            label: getCtaLabel(item.product, pt),\n            disabled: getCtaDisabled(item.product),\n          }\n        : undefined,\n      product: singleProductCardTransformer(item.product, formatter),\n    }));\n}\n\nfunction wishlistTransformer(\n  wishlist: ResultOf<typeof WishlistFragment | typeof WishlistPaginatedItemsFragment>,\n  t: ExistingResultType<typeof getTranslations<'Wishlist'>>,\n  formatter: ExistingResultType<typeof getFormatter>,\n  pt?: ExistingResultType<typeof getTranslations<'Product.ProductDetails'>>,\n): Wishlist {\n  const totalItems = wishlist.items.collectionInfo?.totalItems ?? 0;\n\n  return {\n    id: wishlist.entityId.toString(),\n    name: wishlist.name,\n    publicUrl: `/wishlist/${wishlist.token}`,\n    visibility: {\n      isPublic: wishlist.isPublic,\n      label: wishlist.isPublic ? t('Visibility.public') : t('Visibility.private'),\n      publicLabel: t('Visibility.public'),\n      privateLabel: t('Visibility.private'),\n    },\n    href: `/account/wishlists/${wishlist.entityId}`,\n    items: wishlistItemsTransformer(wishlist.items, formatter, pt),\n    totalItems: {\n      value: totalItems,\n      label: t('items', { count: totalItems }),\n    },\n  };\n}\n\nexport const wishlistsTransformer = (\n  wishlists: ResultOf<typeof WishlistsFragment>,\n  t: ExistingResultType<typeof getTranslations<'Wishlist'>>,\n  formatter: ExistingResultType<typeof getFormatter>,\n): Wishlist[] =>\n  removeEdgesAndNodes(wishlists).map((wishlist) => wishlistTransformer(wishlist, t, formatter));\n\nexport const wishlistDetailsTransformer = (\n  wishlist: ResultOf<typeof WishlistPaginatedItemsFragment>,\n  t: ExistingResultType<typeof getTranslations<'Wishlist'>>,\n  pt: ExistingResultType<typeof getTranslations<'Product.ProductDetails'>>,\n  formatter: ExistingResultType<typeof getFormatter>,\n): Wishlist => wishlistTransformer(wishlist, t, formatter, pt);\n\nexport const publicWishlistDetailsTransformer = (\n  wishlist: ResultOf<typeof PublicWishlistFragment>,\n  t: ExistingResultType<typeof getTranslations<'Wishlist'>>,\n  pt: ExistingResultType<typeof getTranslations<'Product.ProductDetails'>>,\n  formatter: ExistingResultType<typeof getFormatter>,\n): Wishlist => wishlistTransformer({ ...wishlist, isPublic: true }, t, formatter, pt);\n"
  },
  {
    "path": "core/global.ts",
    "content": "import { routing } from '~/i18n/routing';\nimport messages from '~/messages/en.json';\n\ndeclare module 'next-intl' {\n  interface AppConfig {\n    Locale: (typeof routing.locales)[number];\n    Messages: typeof messages;\n  }\n}\n"
  },
  {
    "path": "core/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n  --primary: 96 100% 68%;\n  --accent: 96 100% 88%;\n  --background: 0 0% 100%;\n  --foreground: 0 0% 7%;\n  --success: 116 78% 65%;\n  --error: 0 100% 60%;\n  --warning: 40 100% 60%;\n  --info: 220 70% 45%;\n  --contrast-100: 0 0% 93%;\n  --contrast-200: 0 0% 82%;\n  --contrast-300: 0 0% 70%;\n  --contrast-400: 0 0% 54%;\n  --contrast-500: 0 0% 34%;\n  --font-variation-settings-body: 'slnt' 0;\n  --font-variation-settings-heading: 'slnt' 0;\n  --font-size-xs: 0.75rem;\n  --font-size-sm: 0.875rem;\n  --font-size-base: 1rem;\n  --font-size-lg: 1.125rem;\n  --font-size-xl: 1.25rem;\n  --font-size-2xl: 1.5rem;\n  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n  --shadow-base: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);\n  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);\n  --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);\n}\n"
  },
  {
    "path": "core/i18n/locales.ts",
    "content": "import { buildConfig } from '~/build-config/reader';\n\nconst localeNodes = buildConfig.get('locales');\n\nexport const locales = localeNodes.map((locale) => locale.code);\nexport const defaultLocale = localeNodes.find((locale) => locale.isDefault)?.code ?? 'en';\n"
  },
  {
    "path": "core/i18n/request.ts",
    "content": "import deepmerge from 'deepmerge';\nimport { notFound } from 'next/navigation';\nimport { getRequestConfig } from 'next-intl/server';\n\nimport { locales } from './locales';\n\n// The language to fall back to if the requested message string is not available.\nconst fallbackLocale = 'en';\n\nexport default getRequestConfig(async ({ requestLocale }) => {\n  const locale = await requestLocale;\n\n  if (!locale || !locales.includes(locale)) {\n    notFound();\n  }\n\n  if (locale === fallbackLocale) {\n    return {\n      locale,\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access\n      messages: (await import(`../messages/${locale}.json`)).default,\n    };\n  }\n\n  return {\n    locale,\n    messages: deepmerge(\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access\n      (await import(`../messages/${fallbackLocale}.json`)).default,\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access\n      (await import(`../messages/${locale}.json`)).default,\n    ),\n  };\n});\n"
  },
  {
    "path": "core/i18n/routing.ts",
    "content": "import { createNavigation } from 'next-intl/navigation';\nimport { defineRouting } from 'next-intl/routing';\n\nimport { defaultLocale, locales } from './locales';\n\nenum LocalePrefixes {\n  ALWAYS = 'always',\n  // Don't use NEVER as there is a issue that causes cache problems and returns the wrong messages.\n  // More info: https://github.com/amannn/next-intl/issues/786\n  // NEVER = 'never',\n  ASNEEDED = 'as-needed', // removes prefix on default locale\n}\n\nconst localePrefix = LocalePrefixes.ASNEEDED;\n\nexport const routing = defineRouting({\n  locales,\n  defaultLocale,\n  localePrefix,\n});\n\n// Lightweight wrappers around Next.js' navigation APIs\n// that will consider the routing configuration\n// Redirect will append locale prefix even when in default locale\n// More info: https://github.com/amannn/next-intl/issues/1335\nexport const { Link, redirect, usePathname, useRouter, permanentRedirect } =\n  createNavigation(routing);\n"
  },
  {
    "path": "core/i18n/utils.ts",
    "content": "import { parseWithZod as conformParseWithZod } from '@conform-to/zod';\nimport { z, ZodIssueOptionalMessage } from 'zod';\n\nimport { FormErrorTranslationMap } from '@/vibes/soul/form/dynamic-form/schema';\n\nexport function createErrorMap(errorTranslations?: FormErrorTranslationMap) {\n  return (issue: ZodIssueOptionalMessage) => {\n    const field = issue.path[0];\n    const fieldKey = typeof field === 'string' ? field : '';\n    const errorMessage = errorTranslations?.[fieldKey]?.[issue.code];\n\n    return { message: errorMessage ?? issue.message ?? 'Invalid input' };\n  };\n}\n\nexport function parseWithZodTranslatedErrors<Schema extends z.ZodType>(\n  formData: FormData,\n  options: {\n    schema: Schema;\n    errorTranslations?: FormErrorTranslationMap;\n  },\n) {\n  const errorMap = createErrorMap(options.errorTranslations);\n\n  return conformParseWithZod(formData, {\n    schema: options.schema,\n    errorMap,\n  });\n}\n"
  },
  {
    "path": "core/instrumentation.ts",
    "content": "import { registerOTel } from '@vercel/otel';\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * Initializes OpenTelemetry instrumentation for the Next.js application.\n *\n * This function is automatically called by Next.js during application startup\n * to set up distributed tracing and observability.\n *\n * @see https://nextjs.org/docs/app/guides/instrumentation\n * @see https://nextjs.org/docs/app/guides/open-telemetry\n *\n * Configuration:\n * - Uses @vercel/otel for simplified OpenTelemetry setup\n * - Automatically instruments Next.js internals (requests, rendering, fetches)\n * - Service name is read from OTEL_SERVICE_NAME environment variable\n * - If OTEL_SERVICE_NAME is not set, defaults to 'next-app'\n *\n * Environment Variables:\n * - OTEL_SERVICE_NAME: Custom service name (e.g., 'catalyst-storefront')\n * - NEXT_OTEL_VERBOSE: Set to '1' to see all spans (default: essential spans only)\n * - NEXT_OTEL_FETCH_DISABLED: Set to '1' to disable automatic fetch instrumentation\n *\n * What's automatically instrumented:\n * - All HTTP requests (route, method, status)\n * - App Router page and layout rendering\n * - API route handler execution\n * - fetch() calls to external APIs\n * - Metadata generation (generateMetadata)\n *\n * Custom instrumentation:\n * Import the tracer from ~/lib/otel/tracer to create custom spans:\n *\n * @example\n * import { tracer } from '~/lib/otel/tracer';\n *\n * export async function myFunction() {\n *   return await tracer.startActiveSpan('myFunction', async (span) => {\n *     try {\n *       const result = await doWork();\n *       span.setAttribute('result.count', result.length);\n *       return result;\n *     } finally {\n *       span.end();\n *     }\n *   });\n * }\n *\n * For more information about using OpenTelemetry in Catalyst, see:\n * - OPENTELEMETRY.md in this directory\n * - Official Next.js guide: https://nextjs.org/docs/app/guides/open-telemetry\n */\nexport function register() {\n  // Service name is pulled from the OTEL_SERVICE_NAME env var.\n  // If you wish to manually set it, you can remove the env var and set it here:\n  // registerOTel({ serviceName: 'catalyst-storefront' });\n  registerOTel();\n}\n"
  },
  {
    "path": "core/lib/analytics/analytics.d.ts",
    "content": "declare namespace Analytics {\n  interface Metadata {\n    channelId: number;\n    eventUuid: string;\n  }\n\n  interface Product {\n    // Required fields by most analytics providers\n\n    id: string;\n    name: string;\n\n    // Optional fields and may not be provided in some cases\n\n    brand?: string;\n    sku?: string;\n    /**\n     * A breadcrumb of the category names\n     *\n     * @example [\"Electronics\", \"Computers\", \"Laptops\"]\n     */\n    categories?: string[];\n    /** Individual item price, without multiplying the quantity */\n    price?: number;\n    quantity?: number;\n    variant_id?: number;\n  }\n\n  namespace Navigation {\n    interface ProductViewedPayload {\n      currency: string;\n      value: number;\n      items: Product[];\n    }\n\n    interface CategoryViewedPayload {\n      id: number;\n      name: string;\n      currency: string;\n      items: Product[];\n    }\n\n    interface ProviderEvents {\n      categoryViewed: (payload: CategoryViewedPayload, metadata: Metadata) => void;\n      productViewed: (payload: ProductViewedPayload, metadata: Metadata) => void;\n    }\n\n    export interface Events {\n      categoryViewed: (payload: CategoryViewedPayload) => void;\n      productViewed: (payload: ProductViewedPayload) => void;\n    }\n  }\n\n  export namespace Cart {\n    interface ProductAddedPayload {\n      currency: string;\n      /**\n       * Price of the item multiplied by the quantity\n       * @example 25.0 * 3 = 75.0 // Use 75.0\n       */\n      value: number;\n      items: Product[];\n    }\n\n    interface CartViewedPayload {\n      currency: string;\n      value: number;\n      items: Product[];\n    }\n\n    interface ProductRemovedPayload {\n      currency: string;\n      value: number;\n      items: Product[];\n    }\n\n    interface ProviderEvents {\n      cartViewed: (payload: CartViewedPayload, metadata: Metadata) => void;\n      productAdded: (payload: ProductAddedPayload, metadata: Metadata) => void;\n      productRemoved: (payload: ProductRemovedPayload, metadata: Metadata) => void;\n    }\n\n    export interface Events {\n      cartViewed: (payload: CartViewedPayload) => void;\n      productAdded: (payload: ProductAddedPayload) => void;\n      productRemoved: (payload: ProductRemovedPayload) => void;\n    }\n  }\n\n  export namespace Consent {\n    type ConsentNames = 'necessary' | 'functionality' | 'marketing' | 'measurement';\n    type ConsentValues = Record<ConsentNames, boolean>;\n\n    interface ProviderEvents {\n      consentUpdated: (consent: ConsentValues, metadata: Metadata) => void;\n    }\n\n    export interface Events {\n      consentUpdated: (consent: ConsentValues) => void;\n    }\n  }\n}\n"
  },
  {
    "path": "core/lib/analytics/bigcommerce/data-events.ts",
    "content": "import { client } from '~/client';\nimport { graphql } from '~/client/graphql';\n\nconst VisitStartedMutation = graphql(`\n  mutation VisitStarted($input: VisitStartedEventInput!) {\n    analytics {\n      visitStartedEvent(input: $input) {\n        executed\n      }\n    }\n  }\n`);\n\nconst ProductViewedMutation = graphql(`\n  mutation ProductViewed($input: ProductViewedEventInput!) {\n    analytics {\n      productViewedEvent(input: $input) {\n        executed\n      }\n    }\n  }\n`);\n\ninterface AnalyticsInitiator {\n  visitId: string;\n  visitorId: string;\n}\n\ninterface AnalyticsRequest {\n  url: string;\n  refererUrl: string;\n  userAgent: string;\n}\n\ninterface VisitStartedEvent {\n  initiator: AnalyticsInitiator;\n  request: AnalyticsRequest;\n}\n\ninterface ProductViewedEvent {\n  initiator: AnalyticsInitiator;\n  request: AnalyticsRequest;\n  productId: number;\n}\n\nexport async function sendVisitStartedEvent({ initiator, request }: VisitStartedEvent) {\n  const input = { commonInput: preareCommonInput(initiator, request) };\n\n  return client.fetch({\n    document: VisitStartedMutation,\n    variables: { input },\n    fetchOptions: { cache: 'no-store' },\n  });\n}\n\nexport async function sendProductViewedEvent({\n  productId,\n  initiator,\n  request,\n}: ProductViewedEvent) {\n  const input = {\n    commonInput: preareCommonInput(initiator, request),\n    productInput: { productEntityId: Number(productId) },\n  };\n\n  return await client.fetch({\n    document: ProductViewedMutation,\n    variables: { input },\n    fetchOptions: { cache: 'no-store' },\n  });\n}\n\nfunction preareCommonInput(initiator: AnalyticsInitiator, request: AnalyticsRequest) {\n  return {\n    initiator,\n    request: {\n      url: request.url,\n      refererUrl: request.refererUrl,\n      userAgent: request.userAgent,\n    },\n  };\n}\n"
  },
  {
    "path": "core/lib/analytics/bigcommerce/index.ts",
    "content": "import { cookies } from 'next/headers';\n\nconst VISITOR_COOKIE_NAME = 'catalyst.visitorId';\nconst VISIT_COOKIE_NAME = 'catalyst.visitId';\nconst VISITOR_DURATION = 400 * 24 * 60 * 60; // 400 days\nconst VISIT_DURATION = 30 * 60; // 30 minutes\n\nexport async function getVisitorIdCookie(): Promise<string | undefined> {\n  const cookieStore = await cookies();\n\n  return cookieStore.get(VISITOR_COOKIE_NAME)?.value;\n}\n\nexport async function setVisitorIdCookie(visitorId: string): Promise<void> {\n  const cookieStore = await cookies();\n\n  cookieStore.set(VISITOR_COOKIE_NAME, visitorId, {\n    httpOnly: true,\n    secure: true,\n    path: '/',\n    maxAge: VISITOR_DURATION,\n  });\n}\n\nexport async function getVisitIdCookie(): Promise<string | undefined> {\n  const cookieStore = await cookies();\n\n  return cookieStore.get(VISIT_COOKIE_NAME)?.value;\n}\n\nexport async function setVisitIdCookie(visitId: string): Promise<void> {\n  const cookieStore = await cookies();\n\n  cookieStore.set(VISIT_COOKIE_NAME, visitId, {\n    httpOnly: true,\n    secure: true,\n    path: '/',\n    maxAge: VISIT_DURATION,\n  });\n}\n"
  },
  {
    "path": "core/lib/analytics/index.ts",
    "content": "import { v4 as uuidV4 } from 'uuid';\n\nimport type { AnalyticsConfig, Analytics as IAnalytics } from './types';\n\nexport class Analytics implements IAnalytics {\n  static #instance: Analytics | null = null;\n\n  readonly cart = this.bindCartEvents();\n  readonly navigation = this.bindNavigationEvents();\n  readonly consent = this.bindConsentEvents();\n\n  constructor(private readonly config: AnalyticsConfig) {\n    if (!Analytics.#instance) {\n      Analytics.#instance = this;\n    }\n\n    return Analytics.#instance;\n  }\n\n  initialize() {\n    this.config.providers.forEach((provider) => {\n      provider.initialize();\n    });\n  }\n\n  private bindCartEvents() {\n    return {\n      cartViewed: (payload) => {\n        this.config.providers.forEach((provider) => {\n          provider.cart.cartViewed(payload, {\n            channelId: this.config.channelId,\n            eventUuid: uuidV4(),\n          });\n        });\n      },\n      productAdded: (payload) => {\n        this.config.providers.forEach((provider) => {\n          provider.cart.productAdded(payload, {\n            channelId: this.config.channelId,\n            eventUuid: uuidV4(),\n          });\n        });\n      },\n      productRemoved: (payload) => {\n        this.config.providers.forEach((provider) => {\n          provider.cart.productRemoved(payload, {\n            channelId: this.config.channelId,\n            eventUuid: uuidV4(),\n          });\n        });\n      },\n    } satisfies Analytics.Cart.Events;\n  }\n\n  private bindNavigationEvents() {\n    return {\n      categoryViewed: (payload) => {\n        this.config.providers.forEach((provider) => {\n          provider.navigation.categoryViewed(payload, {\n            channelId: this.config.channelId,\n            eventUuid: uuidV4(),\n          });\n        });\n      },\n      productViewed: (payload) => {\n        this.config.providers.forEach((provider) => {\n          provider.navigation.productViewed(payload, {\n            channelId: this.config.channelId,\n            eventUuid: uuidV4(),\n          });\n        });\n      },\n    } satisfies Analytics.Navigation.Events;\n  }\n\n  private bindConsentEvents() {\n    return {\n      consentUpdated: (payload) => {\n        this.config.providers.forEach((provider) => {\n          provider.consent.consentUpdated(payload, {\n            channelId: this.config.channelId,\n            eventUuid: uuidV4(),\n          });\n        });\n      },\n    } satisfies Analytics.Consent.Events;\n  }\n}\n"
  },
  {
    "path": "core/lib/analytics/providers/google-analytics/index.ts",
    "content": "import { AnalyticsProvider } from '~/lib/analytics/types';\n\nexport interface GoogleAnalyticsConfig {\n  gaId: string;\n  consentModeEnabled?: boolean;\n  developerId?: string;\n  dataLayerName?: string;\n  debugMode?: boolean;\n  nonce?: string;\n  getConsent?: () => Analytics.Consent.ConsentValues | null;\n}\n\nexport class GoogleAnalyticsProvider implements AnalyticsProvider {\n  static #instance: GoogleAnalyticsProvider | null = null;\n\n  readonly cart = this.getCartEvents();\n  readonly navigation = this.getNavigationEvents();\n  readonly consent = this.getConsentEvents();\n\n  private readonly dataLayerScriptId = 'data-layer-script';\n  private readonly gtagScriptId = 'gtag-script';\n\n  constructor(private readonly config: GoogleAnalyticsConfig) {\n    this.validateConfig();\n\n    if (GoogleAnalyticsProvider.#instance) {\n      return GoogleAnalyticsProvider.#instance;\n    }\n\n    GoogleAnalyticsProvider.#instance = this;\n  }\n\n  initialize() {\n    if (typeof window === 'undefined') {\n      throw new Error('Google Analytics is only available in the browser environment');\n    }\n\n    this.initializeDataLayer();\n    this.initializeConsent();\n    this.initializeGTM();\n  }\n\n  private validateConfig() {\n    if (!this.config.gaId) {\n      throw new Error('Google Analytics requires a Google Analytics ID');\n    }\n\n    if (!this.config.dataLayerName) {\n      this.config.dataLayerName = 'dataLayer';\n    }\n  }\n\n  private initializeDataLayer() {\n    const existingScript = document.getElementById(this.dataLayerScriptId);\n\n    if (existingScript) {\n      return;\n    }\n\n    const script = document.createElement('script');\n\n    script.id = this.dataLayerScriptId;\n    script.type = 'text/javascript';\n    script.nonce = this.config.nonce;\n    script.innerHTML = `\n      window['${this.config.dataLayerName}'] = window['${this.config.dataLayerName}'] || [];\n      function gtag(){window['${this.config.dataLayerName}'].push(arguments);}\n      gtag('js', new Date());\n\n      ${this.config.developerId ? `gtag('set', 'developer_id.${this.config.developerId}', true)` : ''};\n      gtag('config', '${this.config.gaId}' ${this.config.debugMode ? \",{ 'debug_mode': true }\" : ''});\n    `;\n\n    document.body.appendChild(script);\n  }\n\n  private initializeConsent() {\n    if (!this.config.consentModeEnabled || !this.config.getConsent) {\n      return;\n    }\n\n    const consent = this.config.getConsent();\n\n    if (!consent) {\n      // Set default consent to denied if no consent is available\n      gtag('consent', 'default', {\n        ad_storage: 'denied',\n        ad_user_data: 'denied',\n        ad_personalization: 'denied',\n        analytics_storage: 'denied',\n        wait_for_update: 500,\n      });\n\n      return;\n    }\n\n    gtag('consent', 'default', {\n      ad_storage: consent.marketing ? 'granted' : 'denied',\n      ad_user_data: consent.marketing ? 'granted' : 'denied',\n      ad_personalization: consent.marketing ? 'granted' : 'denied',\n      analytics_storage: consent.measurement ? 'granted' : 'denied',\n      wait_for_update: 500,\n    });\n  }\n\n  private initializeGTM() {\n    const existingScript = document.getElementById(this.gtagScriptId);\n\n    if (existingScript) {\n      return;\n    }\n\n    const script = document.createElement('script');\n\n    script.id = this.gtagScriptId;\n    script.nonce = this.config.nonce;\n    script.src = `https://www.googletagmanager.com/gtag/js?id=${this.config.gaId}`;\n\n    document.head.appendChild(script);\n  }\n\n  private getCartEvents() {\n    return {\n      cartViewed: (payload, metadata) => {\n        gtag('event', 'view_cart', {\n          event_id: metadata.eventUuid,\n          channel_id: metadata.channelId,\n          currency: payload.currency,\n          value: payload.value,\n          items: payload.items.map((item) => {\n            return {\n              item_name: item.name,\n              item_id: item.sku ?? item.id,\n              price: item.price,\n              quantity: item.quantity,\n              currency: payload.currency,\n              item_brand: item.brand,\n              variant_id: item.variant_id,\n              item_category: item.categories?.at(0),\n              item_category2: item.categories?.at(1),\n              item_category3: item.categories?.at(2),\n              item_category4: item.categories?.at(3),\n              item_category5: item.categories?.at(4),\n            };\n          }),\n        });\n      },\n      productAdded: (payload, metadata) => {\n        gtag('event', 'add_to_cart', {\n          event_id: metadata.eventUuid,\n          channel_id: metadata.channelId,\n          currency: payload.currency,\n          value: payload.value,\n          items: payload.items.map((item) => {\n            return {\n              item_name: item.name,\n              item_id: item.sku ?? item.id,\n              price: item.price,\n              quantity: item.quantity,\n              currency: payload.currency,\n              item_brand: item.brand,\n              variant_id: item.variant_id,\n              item_category: item.categories?.at(0),\n              item_category2: item.categories?.at(1),\n              item_category3: item.categories?.at(2),\n              item_category4: item.categories?.at(3),\n              item_category5: item.categories?.at(4),\n            };\n          }),\n        });\n      },\n      productRemoved: (payload, metadata) => {\n        gtag('event', 'remove_from_cart', {\n          event_id: metadata.eventUuid,\n          channel_id: metadata.channelId,\n          currency: payload.currency,\n          value: payload.value,\n          items: payload.items.map((item) => {\n            return {\n              item_name: item.name,\n              item_id: item.sku ?? item.id,\n              price: item.price,\n              quantity: item.quantity,\n              currency: payload.currency,\n              item_brand: item.brand,\n              variant_id: item.variant_id,\n              item_category: item.categories?.at(0),\n              item_category2: item.categories?.at(1),\n              item_category3: item.categories?.at(2),\n              item_category4: item.categories?.at(3),\n              item_category5: item.categories?.at(4),\n            };\n          }),\n        });\n      },\n    } satisfies Analytics.Cart.ProviderEvents;\n  }\n\n  private getNavigationEvents() {\n    return {\n      categoryViewed: (payload, metadata) => {\n        gtag('event', 'view_item_list', {\n          event_id: metadata.eventUuid,\n          channel_id: metadata.channelId,\n          item_list_id: payload.id,\n          item_list_name: payload.name,\n          items: payload.items.map((item) => {\n            return {\n              item_name: item.name,\n              item_id: item.sku ?? item.id,\n              price: item.price,\n              quantity: item.quantity,\n              currency: payload.currency,\n              item_brand: item.brand,\n              variant_id: item.variant_id,\n              item_category: item.categories?.at(0),\n              item_category2: item.categories?.at(1),\n              item_category3: item.categories?.at(2),\n              item_category4: item.categories?.at(3),\n              item_category5: item.categories?.at(4),\n            };\n          }),\n        });\n      },\n      productViewed: (payload, metadata) => {\n        gtag('event', 'view_item', {\n          event_id: metadata.eventUuid,\n          channel_id: metadata.channelId,\n          currency: payload.currency,\n          value: payload.value,\n          items: payload.items.map((item) => {\n            return {\n              item_name: item.name,\n              item_id: item.sku ?? item.id,\n              price: item.price,\n              quantity: item.quantity,\n              currency: payload.currency,\n              item_brand: item.brand,\n              variant_id: item.variant_id,\n              item_category: item.categories?.at(0),\n              item_category2: item.categories?.at(1),\n              item_category3: item.categories?.at(2),\n              item_category4: item.categories?.at(3),\n              item_category5: item.categories?.at(4),\n            };\n          }),\n        });\n      },\n    } satisfies Analytics.Navigation.ProviderEvents;\n  }\n\n  private getConsentEvents() {\n    return {\n      consentUpdated: (consent) => {\n        gtag('consent', 'update', {\n          ad_storage: consent.marketing ? 'granted' : 'denied',\n          ad_user_data: consent.marketing ? 'granted' : 'denied',\n          ad_personalization: consent.marketing ? 'granted' : 'denied',\n          analytics_storage: consent.measurement ? 'granted' : 'denied',\n        });\n      },\n    } satisfies Analytics.Consent.ProviderEvents;\n  }\n}\n"
  },
  {
    "path": "core/lib/analytics/react/index.tsx",
    "content": "'use client';\n\nimport { createContext, PropsWithChildren, useContext, useEffect } from 'react';\n\nimport { type Analytics } from '../types';\n\nconst AnalyticsContext = createContext<Analytics | null>(null);\n\ninterface AnalyticsProviderProps {\n  analytics: Analytics | null;\n}\n\nexport const AnalyticsProvider = ({\n  children,\n  analytics,\n}: PropsWithChildren<AnalyticsProviderProps>) => {\n  useEffect(() => {\n    analytics?.initialize();\n  }, [analytics]);\n\n  return <AnalyticsContext.Provider value={analytics}>{children}</AnalyticsContext.Provider>;\n};\n\nexport const useAnalytics = () => {\n  return useContext(AnalyticsContext);\n};\n"
  },
  {
    "path": "core/lib/analytics/types.ts",
    "content": "export interface AnalyticsProvider {\n  cart: Analytics.Cart.ProviderEvents;\n  navigation: Analytics.Navigation.ProviderEvents;\n  consent: Analytics.Consent.ProviderEvents;\n\n  initialize: () => void;\n}\n\nexport interface AnalyticsConfig {\n  channelId: number;\n  providers: AnalyticsProvider[];\n}\n\nexport interface Analytics {\n  readonly cart: Analytics.Cart.Events;\n  readonly navigation: Analytics.Navigation.Events;\n  readonly consent: Analytics.Consent.Events;\n\n  initialize(): void;\n}\n"
  },
  {
    "path": "core/lib/cart/add-cart-line-item.ts",
    "content": "import { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\n\nconst AddCartLineItemMutation = graphql(`\n  mutation AddCartLineItemMutation($input: AddCartLineItemsInput!) {\n    cart {\n      addCartLineItems(input: $input) {\n        cart {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\ntype Variables = VariablesOf<typeof AddCartLineItemMutation>;\nexport type AddCartLineItemsInput = Variables['input'];\n\nexport const addCartLineItem = async (\n  cartEntityId: AddCartLineItemsInput['cartEntityId'],\n  data: AddCartLineItemsInput['data'],\n) => {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  return await client.fetch({\n    document: AddCartLineItemMutation,\n    variables: { input: { cartEntityId, data } },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n};\n"
  },
  {
    "path": "core/lib/cart/create-cart.ts",
    "content": "import { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql, VariablesOf } from '~/client/graphql';\nimport { getPreferredCurrencyCode } from '~/lib/currency';\n\nconst CreateCartMutation = graphql(`\n  mutation CreateCartMutation($createCartInput: CreateCartInput!) {\n    cart {\n      createCart(input: $createCartInput) {\n        cart {\n          entityId\n        }\n      }\n    }\n  }\n`);\n\ntype Variables = VariablesOf<typeof CreateCartMutation>;\nexport type CreateCartInput = Variables['createCartInput'];\n\nexport const createCart = async (data: CreateCartInput) => {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n  const currencyCode = await getPreferredCurrencyCode();\n\n  return await client.fetch({\n    document: CreateCartMutation,\n    variables: {\n      createCartInput: {\n        ...data,\n        currencyCode,\n      },\n    },\n    customerAccessToken,\n    fetchOptions: { cache: 'no-store' },\n  });\n};\n"
  },
  {
    "path": "core/lib/cart/error.ts",
    "content": "export class MissingCartError extends Error {\n  constructor() {\n    super('Cart was not returned in response');\n    this.name = 'MissingCartError';\n  }\n}\n"
  },
  {
    "path": "core/lib/cart/index.ts",
    "content": "'use server';\n\nimport { revalidateTag } from 'next/cache';\n\nimport { auth, getAnonymousSession, updateAnonymousSession, updateSession } from '~/auth';\nimport { TAGS } from '~/client/tags';\nimport { addCartLineItem, AddCartLineItemsInput } from '~/lib/cart/add-cart-line-item';\nimport { createCart, CreateCartInput } from '~/lib/cart/create-cart';\nimport { validateCartId } from '~/lib/cart/validate-cart';\n\nimport { MissingCartError } from './error';\n\nexport async function getCartId(): Promise<string | undefined> {\n  const anonymousSession = await getAnonymousSession();\n\n  if (anonymousSession) {\n    return anonymousSession.user?.cartId ?? undefined;\n  }\n\n  const session = await auth();\n\n  return session?.user?.cartId ?? undefined;\n}\n\nexport async function setCartId(cartId: string): Promise<void> {\n  const anonymousSession = await getAnonymousSession();\n\n  if (anonymousSession) {\n    await updateAnonymousSession({ cartId });\n\n    return;\n  }\n\n  await updateSession({ user: { cartId } });\n}\n\nexport async function clearCartId(): Promise<void> {\n  const anonymousSession = await getAnonymousSession();\n\n  if (anonymousSession) {\n    await updateAnonymousSession({ cartId: null });\n\n    return;\n  }\n\n  await updateSession({ user: { cartId: null } });\n}\n\nexport async function addToOrCreateCart(\n  data: CreateCartInput | AddCartLineItemsInput['data'],\n): Promise<void> {\n  const cartId = await getCartId();\n  const cart = await validateCartId(cartId);\n\n  if (cart) {\n    const response = await addCartLineItem(cart.entityId, data);\n\n    if (!response.data.cart.addCartLineItems?.cart?.entityId) {\n      throw new MissingCartError();\n    }\n\n    revalidateTag(TAGS.cart, { expire: 0 });\n\n    return;\n  }\n\n  const createResponse = await createCart(data);\n\n  if (!createResponse.data.cart.createCart?.cart?.entityId) {\n    throw new MissingCartError();\n  }\n\n  await setCartId(createResponse.data.cart.createCart.cart.entityId);\n\n  revalidateTag(TAGS.cart, { expire: 0 });\n}\n"
  },
  {
    "path": "core/lib/cart/validate-cart.ts",
    "content": "import { getSessionCustomerAccessToken } from '~/auth';\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { TAGS } from '~/client/tags';\n\nconst ValidateCartQuery = graphql(`\n  query ValidateCartQuery($cartId: String) {\n    site {\n      cart(entityId: $cartId) {\n        entityId\n      }\n    }\n  }\n`);\n\nexport async function validateCartId(cartId?: string) {\n  const customerAccessToken = await getSessionCustomerAccessToken();\n\n  const response = await client.fetch({\n    document: ValidateCartQuery,\n    variables: { cartId },\n    customerAccessToken,\n    fetchOptions: {\n      cache: 'no-store',\n      next: {\n        tags: [TAGS.cart],\n      },\n    },\n  });\n\n  return response.data.site.cart;\n}\n"
  },
  {
    "path": "core/lib/cdn-image-loader.ts",
    "content": "'use client';\n\nimport { ImageLoaderProps } from 'next/image';\n\nexport default function bcCdnImageLoader({ src, width }: ImageLoaderProps): string {\n  const url = src.replace('{:size}', `${width}w`);\n\n  return url;\n}\n"
  },
  {
    "path": "core/lib/client-cookies.ts",
    "content": "export const FORCE_REFRESH_COOKIE = 'force-refresh';\n\ninterface ClientCookieOptions {\n  expires: Date;\n  path: string;\n  domain: string;\n  secure: boolean;\n  sameSite: 'Strict' | 'Lax' | 'None';\n  maxAge: number;\n}\n\nconst cookiePropertyMap: Record<string, string> = {\n  expires: 'Expires',\n  path: 'Path',\n  domain: 'Domain',\n  secure: 'Secure',\n  sameSite: 'SameSite',\n  maxAge: 'Max-Age',\n};\n\nexport function getCookieValue(name: string): string | null {\n  const cookies = document.cookie.split('; ');\n  const cookie = cookies.find((c) => c.startsWith(`${name}=`));\n\n  if (!cookie) {\n    return null;\n  }\n\n  const cookieValue = cookie.split('=')[1];\n\n  return cookieValue ?? null;\n}\n\nexport function setCookie(\n  name: string,\n  value: string,\n  options?: Partial<ClientCookieOptions>,\n): void {\n  const opts = options ?? {};\n  const cookieOptions = Object.entries(opts)\n    .reduce<string[]>((acc, [key, optValue]) => {\n      const propKey = cookiePropertyMap[key];\n\n      if (!propKey) {\n        return acc;\n      }\n\n      if (propKey === 'Secure' && optValue === true) {\n        return [...acc, propKey];\n      }\n\n      return [...acc, `${propKey}=${optValue.toString()}`];\n    }, [])\n    .join('; ');\n\n  document.cookie = `${name}=${value}; ${cookieOptions};`;\n}\n"
  },
  {
    "path": "core/lib/consent-manager/cookies/client.ts",
    "content": "import { ConsentCookieSchema } from '../schema';\n\nimport { CONSENT_COOKIE_NAME } from './constants';\nimport { parseCompactFormat } from './parse-compact-format';\n\nconst getCookieValueByName = (name: string) => {\n  if (typeof document === 'undefined') return null;\n\n  const pair = document.cookie.split('; ').find((c) => c.startsWith(`${name}=`));\n\n  return pair ? pair.slice(name.length + 1) : null;\n};\n\nexport const getConsentCookie = () => {\n  const cookie = getCookieValueByName(CONSENT_COOKIE_NAME);\n\n  if (!cookie) return null;\n\n  try {\n    const consent = parseCompactFormat(cookie);\n\n    return ConsentCookieSchema.parse(consent);\n  } catch {\n    return null;\n  }\n};\n"
  },
  {
    "path": "core/lib/consent-manager/cookies/constants.ts",
    "content": "export const CONSENT_COOKIE_NAME = 'c15t-consent';\n"
  },
  {
    "path": "core/lib/consent-manager/cookies/parse-compact-format.ts",
    "content": "/**\n * Parses the compact format: i.t:1765485149496,c.necessary:1,c.functionality:1,etc.\n *\n * @param {string} raw - The raw compact format string\n * @returns {Record<string, string | number>} Parsed object with keys and values\n */\nexport function parseCompactFormat(raw: string): Record<string, string | number> {\n  const pairs = raw.split(',');\n  const result: Record<string, string | number> = {};\n\n  pairs.forEach((pair) => {\n    const [key, value] = pair.split(':');\n\n    if (key && value !== undefined) {\n      // Try to parse as number, otherwise keep as string\n      const numValue = Number(value);\n      const trimmedKey = key.trim();\n      const trimmedValue = value.trim();\n\n      result[trimmedKey] = Number.isNaN(numValue) ? trimmedValue : numValue;\n    }\n  });\n\n  return result;\n}\n"
  },
  {
    "path": "core/lib/consent-manager/cookies/server.ts",
    "content": "import { cookies } from 'next/headers';\n\nimport { ConsentCookieSchema } from '../schema';\n\nimport { CONSENT_COOKIE_NAME } from './constants';\nimport { parseCompactFormat } from './parse-compact-format';\n\nexport const getConsentCookie = async () => {\n  const cookieStore = await cookies();\n  const cookie = cookieStore.get(CONSENT_COOKIE_NAME)?.value;\n\n  if (!cookie) return null;\n\n  try {\n    const consent = parseCompactFormat(cookie);\n\n    return ConsentCookieSchema.parse(consent);\n  } catch {\n    return null;\n  }\n};\n"
  },
  {
    "path": "core/lib/consent-manager/schema.ts",
    "content": "import { z } from 'zod';\n\n// Optional consent that can only be 1 (if present), returns false if absent\nconst optionalConsent = z\n  .literal(1)\n  .optional()\n  .transform((val) => val === 1);\n\nexport const ConsentCookieSchema = z.object({\n  // timestamp in milliseconds\n  'i.t': z.number().int().positive(),\n  // required consent, returns true\n  'c.necessary': z.literal(1).transform(() => true),\n  // optional consents (if present, must be 1, returns true)\n  'c.functionality': optionalConsent,\n  'c.marketing': optionalConsent,\n  'c.measurement': optionalConsent,\n});\n"
  },
  {
    "path": "core/lib/content-security-policy.ts",
    "content": "import builder from 'content-security-policy-builder';\n\nconst makeswiftEnabled = !!process.env.MAKESWIFT_SITE_API_KEY;\n\nconst makeswiftBaseUrl = process.env.MAKESWIFT_BASE_URL || 'https://app.makeswift.com';\n\nconst frameAncestors = makeswiftEnabled ? makeswiftBaseUrl : 'none';\n\n// customize the directives as needed\nexport const cspHeader = builder({\n  directives: {\n    baseUri: ['self'],\n    frameAncestors: [frameAncestors],\n    // formAction: ['self'],\n    // defaultSrc: ['self'],\n    // scriptSrc: ['self'],\n    // styleSrc: ['self'],\n    // imgSrc: ['self'],\n    // connectSrc: ['self'],\n    // fontSrc: ['self'],\n    // objectSrc: ['none'],\n    // mediaSrc: ['self'],\n    // frameSrc: ['self'],\n    // childSrc: ['self'],\n    // manifestSrc: ['self'],\n    // workerSrc: ['self'],\n    // prefetchSrc: ['self'],\n    // navigateTo: ['self'],\n    // reportUri: ['none'],\n  },\n});\n"
  },
  {
    "path": "core/lib/currency.ts",
    "content": "'use server';\n\nimport { cookies } from 'next/headers';\n\nimport type { CurrencyCode } from '~/components/header/fragment';\nimport { CurrencyCodeSchema } from '~/components/header/schema';\n\nexport async function getPreferredCurrencyCode(): Promise<CurrencyCode | undefined> {\n  const cookieStore = await cookies();\n  const currencyCode = cookieStore.get('currencyCode')?.value;\n\n  if (!currencyCode) {\n    return undefined;\n  }\n\n  const result = CurrencyCodeSchema.safeParse(currencyCode);\n\n  return result.success ? result.data : undefined;\n}\n\nexport async function setPreferredCurrencyCode(currencyCode: CurrencyCode): Promise<void> {\n  const cookieStore = await cookies();\n\n  cookieStore.set('currencyCode', currencyCode, {\n    httpOnly: true,\n    secure: process.env.NODE_ENV === 'production',\n    sameSite: 'lax',\n    path: '/',\n  });\n}\n"
  },
  {
    "path": "core/lib/force-refresh.ts",
    "content": "import { cookies } from 'next/headers';\n\nimport { FORCE_REFRESH_COOKIE } from './client-cookies';\n\nexport async function setForceRefreshCookie() {\n  const cookieStore = await cookies();\n  const forceRefreshCookie = cookieStore.get(FORCE_REFRESH_COOKIE);\n\n  if (!forceRefreshCookie) {\n    cookieStore.set(FORCE_REFRESH_COOKIE, 'true', {\n      httpOnly: false,\n      secure: false,\n    });\n  } else if (forceRefreshCookie.value === 'false') {\n    cookieStore.delete(FORCE_REFRESH_COOKIE);\n  }\n}\n"
  },
  {
    "path": "core/lib/kv/adapters/memory.ts",
    "content": "/* eslint-disable @typescript-eslint/require-await */\nimport { LRUCache } from 'lru-cache';\n\nimport { KvAdapter } from '../types';\n\ninterface CacheEntry {\n  value: unknown;\n  expiresAt: number;\n}\n\nexport class MemoryKvAdapter implements KvAdapter {\n  private kv = new LRUCache<string, CacheEntry>({\n    max: 500,\n  });\n\n  async mget<Data>(...keys: string[]) {\n    const entries = keys.map((key) => this.kv.get(key)?.value);\n\n    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n    return entries as Data[];\n  }\n\n  async set<Data>(key: string, value: Data, options: { ex?: number } = {}) {\n    this.kv.set(key, {\n      value,\n      expiresAt: options.ex ? Date.now() + options.ex * 1_000 : Number.MAX_SAFE_INTEGER,\n    });\n\n    return value;\n  }\n\n  private async get<Data>(key: string) {\n    const entry = this.kv.get(key);\n\n    if (!entry) {\n      return null;\n    }\n\n    if (entry.expiresAt < Date.now()) {\n      return null;\n    }\n\n    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n    return entry.value as Data;\n  }\n}\n"
  },
  {
    "path": "core/lib/kv/adapters/upstash.ts",
    "content": "import { Redis } from '@upstash/redis';\n\nimport { KvAdapter, SetCommandOptions } from '../types';\n\nexport class UpstashKvAdapter implements KvAdapter {\n  private upstashKv = Redis.fromEnv();\n\n  async mget<Data>(...keys: string[]) {\n    return this.upstashKv.mget<Data[]>(keys);\n  }\n\n  async set<Data>(key: string, value: Data, opts?: SetCommandOptions) {\n    const response = await this.upstashKv.set(key, value, opts);\n\n    if (response === 'OK') {\n      return null;\n    }\n\n    return response;\n  }\n}\n"
  },
  {
    "path": "core/lib/kv/adapters/vercel-runtime-cache.ts",
    "content": "import { getCache } from '@vercel/functions';\n\nimport { KvAdapter, SetCommandOptions } from '../types';\n\nexport class RuntimeCacheAdapter implements KvAdapter {\n  private cache = getCache();\n\n  async mget<Data>(...keys: string[]): Promise<Array<Data | null>> {\n    this.logger(\n      `MGET - Keys: ${keys.toString()} - Source: RUNTIME_CACHE - Fetching ${keys.length} keys`,\n    );\n\n    try {\n      const values = await Promise.all(\n        keys.map(async (key) => {\n          try {\n            // Use the Runtime Cache API\n            const cachedValue = await this.cache.get(key);\n\n            if (cachedValue !== null) {\n              this.logger(`RUNTIME_CACHE GET - Key: ${key} - Found: true`);\n\n              return cachedValue;\n            }\n\n            this.logger(`RUNTIME_CACHE GET - Key: ${key} - Found: false`);\n\n            return null;\n          } catch (error) {\n            this.logger(\n              `RUNTIME_CACHE GET ERROR - Key: ${key} - Error: ${error instanceof Error ? error.message : 'Unknown error'}`,\n            );\n\n            return null;\n          }\n        }),\n      );\n\n      // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n      return values as Array<Data | null>;\n    } catch (error) {\n      this.logger(\n        `RUNTIME_CACHE ACCESS ERROR - Returning null values: ${error instanceof Error ? error.message : 'Unknown error'}`,\n      );\n\n      // Return null for all keys if Runtime Cache is unavailable\n      return keys.map(() => null);\n    }\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  async set<Data>(key: string, value: Data, _opts?: SetCommandOptions): Promise<Data | null> {\n    this.logger(`SET - Key: ${key} - Setting in runtime cache`);\n\n    try {\n      await this.cache.set(key, value);\n      this.logger(`RUNTIME_CACHE SET - Key: ${key} - Success`);\n    } catch (error) {\n      this.logger(\n        `RUNTIME_CACHE SET ERROR - Key: ${key} - Error: ${error instanceof Error ? error.message : 'Unknown error'}`,\n      );\n    }\n\n    return value;\n  }\n\n  private logger(message: string) {\n    // Check if logging is enabled using the same logic as the main KV class\n    const loggingEnabled =\n      (process.env.NODE_ENV !== 'production' && process.env.KV_LOGGER !== 'false') ||\n      process.env.KV_LOGGER === 'true';\n\n    if (loggingEnabled) {\n      // eslint-disable-next-line no-console\n      console.log(`[BigCommerce] Runtime Cache ${message}`);\n    }\n  }\n}\n"
  },
  {
    "path": "core/lib/kv/index.ts",
    "content": "import { MemoryKvAdapter } from './adapters/memory';\nimport { KvAdapter, SetCommandOptions } from './types';\n\ninterface Config {\n  logger?: boolean;\n}\n\nconst memoryKv = new MemoryKvAdapter();\n\nclass KV<Adapter extends KvAdapter> implements KvAdapter {\n  private kv?: Adapter;\n  private memoryKv = memoryKv;\n\n  constructor(\n    private createAdapter: () => Promise<Adapter>,\n    private config: Config = {},\n  ) {}\n\n  async get<Data>(key: string) {\n    const [value] = await this.mget<Data>(key);\n\n    return value ?? null;\n  }\n\n  async mget<Data>(...keys: string[]) {\n    const kv = await this.getKv();\n\n    const memoryValues = (await this.memoryKv.mget<Data>(...keys)).filter(Boolean);\n\n    if (memoryValues.length === keys.length) {\n      this.logger(\n        `MGET - Keys: ${keys.toString()} - Value: ${JSON.stringify(memoryValues, null, 2)}`,\n      );\n\n      return memoryValues;\n    }\n\n    const values = await kv.mget<Data>(...keys);\n\n    this.logger(`MGET - Keys: ${keys.toString()} - Value: ${JSON.stringify(values, null, 2)}`);\n\n    // Store the values in memory kv\n    await Promise.all(\n      values.map(async (value, index) => {\n        const key = keys[index];\n\n        if (!key) {\n          return;\n        }\n\n        await this.memoryKv.set(key, value);\n      }),\n    );\n\n    return values;\n  }\n\n  async set<Data>(key: string, value: Data, opts?: SetCommandOptions) {\n    const kv = await this.getKv();\n\n    this.logger(`SET - Key: ${key} - Value: ${JSON.stringify(value, null, 2)}`);\n\n    await Promise.all([this.memoryKv.set(key, value, opts), kv.set(key, value, opts)]);\n\n    return value;\n  }\n\n  private async getKv() {\n    if (!this.kv) {\n      this.kv = await this.createAdapter();\n    }\n\n    return this.kv;\n  }\n\n  private logger(message: string) {\n    if (this.config.logger) {\n      // eslint-disable-next-line no-console\n      console.log(`[BigCommerce] KV ${message}`);\n    }\n  }\n}\n\nasync function createKVAdapter() {\n  // Prioritize Runtime Cache for Vercel environments\n  if (process.env.VERCEL === '1') {\n    const { RuntimeCacheAdapter } = await import('./adapters/vercel-runtime-cache');\n\n    return new RuntimeCacheAdapter();\n  }\n\n  if (process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN) {\n    const { UpstashKvAdapter } = await import('./adapters/upstash');\n\n    return new UpstashKvAdapter();\n  }\n\n  return new MemoryKvAdapter();\n}\n\nconst adapterInstance = new KV(createKVAdapter, {\n  logger:\n    (process.env.NODE_ENV !== 'production' && process.env.KV_LOGGER !== 'false') ||\n    process.env.KV_LOGGER === 'true',\n});\n\nexport { adapterInstance as kv };\n"
  },
  {
    "path": "core/lib/kv/keys.ts",
    "content": "const VERSION = 'v3';\n\nexport const STORE_STATUS_KEY = 'storeStatus';\n\nexport const kvKey = (key: string, channelId?: string) => {\n  const namespace = process.env.KV_NAMESPACE ?? process.env.BIGCOMMERCE_STORE_HASH ?? 'store';\n  const id = channelId ?? process.env.BIGCOMMERCE_CHANNEL_ID ?? '1';\n\n  return `${namespace}_${id}_${VERSION}_${key}`;\n};\n"
  },
  {
    "path": "core/lib/kv/types.ts",
    "content": "export type SetCommandOptions = Record<string, unknown>;\n\nexport interface KvAdapter {\n  mget<Data>(...keys: string[]): Promise<Array<Data | null>>;\n  set<Data>(key: string, value: Data, opts?: SetCommandOptions): Promise<Data | null>;\n}\n"
  },
  {
    "path": "core/lib/otel/tracer.ts",
    "content": "import { trace } from '@opentelemetry/api';\n\n/**\n * OpenTelemetry tracer for creating custom spans in the Catalyst application.\n *\n * Use this tracer to instrument important operations and track their performance.\n * Spans created with this tracer will appear in your observability dashboard\n * nested under the appropriate HTTP request trace.\n *\n * @see https://nextjs.org/docs/app/guides/open-telemetry#custom-spans\n * @see OPENTELEMETRY.md for detailed usage guide\n *\n * @example Basic usage\n * import { tracer } from '~/lib/otel/tracer';\n *\n * export async function fetchProductRecommendations(productId: string) {\n *   return await tracer.startActiveSpan('fetchProductRecommendations', async (span) => {\n *     try {\n *       // Add attributes for context\n *       span.setAttribute('product.id', productId);\n *\n *       const recommendations = await fetch(`/api/recommendations/${productId}`);\n *\n *       span.setAttribute('recommendations.count', recommendations.length);\n *\n *       return recommendations;\n *     } finally {\n *       // Always end the span, even if an error occurs\n *       span.end();\n *     }\n *   });\n * }\n *\n * @example With error handling\n * import { tracer } from '~/lib/otel/tracer';\n * import { SpanStatusCode } from '@opentelemetry/api';\n *\n * export async function processOrder(orderId: string) {\n *   return await tracer.startActiveSpan('processOrder', async (span) => {\n *     try {\n *       span.setAttribute('order.id', orderId);\n *\n *       const result = await submitOrder(orderId);\n *\n *       return result;\n *     } catch (error) {\n *       // Record the exception and mark span as error\n *       span.recordException(error);\n *       span.setStatus({ code: SpanStatusCode.ERROR });\n *       throw error;\n *     } finally {\n *       span.end();\n *     }\n *   });\n * }\n *\n * @example Data transformation\n * export async function transformCartData(rawCart: RawCart) {\n *   return await tracer.startActiveSpan('transformCartData', async (span) => {\n *     try {\n *       span.setAttribute('cart.itemCount', rawCart.lineItems.length);\n *\n *       const transformed = {\n *         items: rawCart.lineItems.map(transformLineItem),\n *         total: calculateTotal(rawCart),\n *       };\n *\n *       return transformed;\n *     } finally {\n *       span.end();\n *     }\n *   });\n * }\n *\n * When to use custom spans:\n * - Operations that might be slow (> 100ms)\n * - Critical business logic (pricing, inventory, checkout)\n * - External API integrations\n * - Data transformations with variable performance\n * - Any operation you want to monitor and optimize\n *\n * When NOT to use custom spans:\n * - Operations already instrumented by Next.js (fetch calls, route rendering)\n * - Simple utility functions\n * - Trivial operations (< 10ms)\n *\n * Best practices:\n * - Use descriptive span names (e.g., 'cart.calculateDiscounts')\n * - Add meaningful attributes for filtering and debugging\n * - Always end spans in finally blocks\n * - Use hierarchical naming (e.g., 'cart.validate', 'cart.addItem')\n */\nexport const tracer = trace.getTracer('default');\n"
  },
  {
    "path": "core/lib/recaptcha/constants.ts",
    "content": "export interface ReCaptchaSettings {\n  isEnabledOnStorefront: boolean;\n  siteKey: string;\n}\n\nexport const RECAPTCHA_TOKEN_FORM_KEY = 'g-recaptcha-response';\n"
  },
  {
    "path": "core/lib/recaptcha.ts",
    "content": "import 'server-only';\n\nimport { cache } from 'react';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\n\nimport { RECAPTCHA_TOKEN_FORM_KEY, type ReCaptchaSettings } from './recaptcha/constants';\n\nexport { RECAPTCHA_TOKEN_FORM_KEY } from './recaptcha/constants';\nexport type { ReCaptchaSettings } from './recaptcha/constants';\n\nexport const ReCaptchaSettingsQuery = graphql(`\n  query ReCaptchaSettingsQuery {\n    site {\n      settings {\n        reCaptcha {\n          isEnabledOnStorefront\n          siteKey\n        }\n      }\n    }\n  }\n`);\n\nexport const getReCaptchaSettings = cache(async (): Promise<ReCaptchaSettings | null> => {\n  const { data } = await client.fetch({\n    document: ReCaptchaSettingsQuery,\n    fetchOptions: { next: { revalidate } },\n  });\n\n  const reCaptcha = data.site.settings?.reCaptcha;\n\n  if (!reCaptcha?.siteKey) {\n    return null;\n  }\n\n  return {\n    isEnabledOnStorefront: reCaptcha.isEnabledOnStorefront,\n    siteKey: reCaptcha.siteKey,\n  };\n});\n\nexport const getRecaptchaSiteKey = cache(async (): Promise<string | undefined> => {\n  const settings = await getReCaptchaSettings();\n\n  return settings?.isEnabledOnStorefront === true && settings.siteKey\n    ? settings.siteKey\n    : undefined;\n});\n\nexport async function getRecaptchaFromForm(\n  formData: FormData,\n): Promise<{ siteKey: string | undefined; token: string }> {\n  const siteKey = await getRecaptchaSiteKey();\n  const raw = formData.get(RECAPTCHA_TOKEN_FORM_KEY);\n  const token = typeof raw === 'string' ? raw : '';\n\n  return { siteKey, token };\n}\n\nexport function assertRecaptchaTokenPresent(\n  siteKey: string | undefined,\n  token: string,\n  recaptchaRequiredMessage: string,\n): { success: true; token: string | undefined } | { success: false; formErrors: [string] } {\n  if (!siteKey) {\n    return { success: true, token: undefined };\n  }\n\n  const tokenValue = token.trim();\n\n  if (!tokenValue) {\n    return { success: false, formErrors: [recaptchaRequiredMessage] };\n  }\n\n  return { success: true, token: tokenValue };\n}\n"
  },
  {
    "path": "core/lib/search.tsx",
    "content": "import React, {\n  createContext,\n  Dispatch,\n  ReactNode,\n  SetStateAction,\n  useContext,\n  useState,\n} from 'react';\n\ninterface SearchContextProps {\n  isSearchOpen: boolean;\n  setIsSearchOpen: Dispatch<SetStateAction<boolean>>;\n}\n\nconst SearchContext = createContext<SearchContextProps | undefined>(undefined);\n\nexport const SearchProvider = ({ children }: { children: ReactNode }) => {\n  const [isSearchOpen, setIsSearchOpen] = useState(false);\n\n  return (\n    <SearchContext.Provider value={{ isSearchOpen, setIsSearchOpen }}>\n      {children}\n    </SearchContext.Provider>\n  );\n};\n\nexport const useSearch = (): SearchContextProps => {\n  const context = useContext(SearchContext);\n\n  if (context === undefined) {\n    throw new Error('useSearch must be used within a SearchProvider');\n  }\n\n  return context;\n};\n"
  },
  {
    "path": "core/lib/seo/canonical.ts",
    "content": "import { cache } from 'react';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { defaultLocale, locales } from '~/i18n/locales';\n\ninterface CanonicalUrlOptions {\n  /**\n   * The path from BigCommerce (e.g., product.path, category.path)\n   * or a manually constructed path for static pages (e.g., '/')\n   */\n  path: string;\n  /**\n   * Current locale from params\n   */\n  locale: string;\n  /**\n   * Whether to include hreflang alternates for all locales\n   * @default true\n   */\n  includeAlternates?: boolean;\n}\n\n/**\n * Generates metadata alternates object for Next.js Metadata API\n *\n * Rules:\n * - Default locale: no prefix (e.g., https://example.com/product/)\n * - Other locales: with prefix (e.g., https://example.com/fr/product/)\n * - Respects TRAILING_SLASH environment variable\n *\n * @param {CanonicalUrlOptions} options - The options for generating canonical URLs\n * @returns {object} The metadata alternates object with canonical URL and optional language alternates\n */\nconst VanityUrlQuery = graphql(`\n  query VanityUrlQuery {\n    site {\n      settings {\n        url {\n          vanityUrl\n        }\n      }\n    }\n  }\n`);\n\nconst getVanityUrl = cache(async () => {\n  const { data } = await client.fetch({\n    document: VanityUrlQuery,\n    fetchOptions: { next: { revalidate } },\n  });\n\n  const vanityUrl = data.site.settings?.url.vanityUrl;\n\n  if (!vanityUrl) {\n    throw new Error('Vanity URL not found in site settings');\n  }\n\n  return vanityUrl;\n});\n\nexport async function getMetadataAlternates(options: CanonicalUrlOptions) {\n  const { path, locale, includeAlternates = true } = options;\n\n  // Use preview deployment URL so canonical/hreflang URLs point at the preview, not production.\n  const previewUrl =\n    process.env.VERCEL_ENV === 'preview' ? `https://${process.env.VERCEL_URL}` : undefined;\n  const baseUrl = previewUrl && URL.canParse(previewUrl) ? previewUrl : await getVanityUrl();\n\n  const canonical = buildLocalizedUrl(baseUrl, path, locale);\n\n  if (!includeAlternates) {\n    return { canonical };\n  }\n\n  const languages = locales.reduce<Record<string, string>>((acc, loc) => {\n    acc[loc] = buildLocalizedUrl(baseUrl, path, loc);\n\n    return acc;\n  }, {});\n\n  languages['x-default'] = buildLocalizedUrl(baseUrl, path, defaultLocale);\n\n  return { canonical, languages };\n}\n\nfunction buildLocalizedUrl(baseUrl: string, pathname: string, locale: string): string {\n  const trailingSlash = process.env.TRAILING_SLASH !== 'false';\n\n  const url = new URL(pathname, baseUrl);\n\n  url.pathname = locale === defaultLocale ? url.pathname : `/${locale}${url.pathname}`;\n\n  if (trailingSlash && !url.pathname.endsWith('/')) {\n    url.pathname += '/';\n  } else if (!trailingSlash && url.pathname.endsWith('/') && url.pathname !== '/') {\n    url.pathname = url.pathname.slice(0, -1);\n  }\n\n  return url.href;\n}\n"
  },
  {
    "path": "core/lib/server-toast.ts",
    "content": "import { cookies } from 'next/headers';\nimport { z } from 'zod';\nimport 'server-only';\n\nconst serverToastSchema = z.object({\n  id: z.number(),\n  message: z.string(),\n  description: z.string().optional(),\n  variant: z.enum(['success', 'error', 'warning', 'info']),\n  position: z\n    .enum(['top-left', 'top-right', 'top-center', 'bottom-left', 'bottom-right', 'bottom-center'])\n    .optional(),\n});\n\nexport type ServerToastData = z.infer<typeof serverToastSchema>;\n\ntype ServerToastOptions = Pick<ServerToastData, 'position' | 'description'>;\n\nconst TOAST_COOKIE = 'toast-notification';\n\nfunction encode(data: ServerToastData): string {\n  return btoa(JSON.stringify(data));\n}\n\nfunction decode(data: string): ServerToastData {\n  return serverToastSchema.parse(JSON.parse(atob(data)));\n}\n\nasync function setToastCookie(data: ServerToastData) {\n  const cookieStore = await cookies();\n\n  cookieStore.set(TOAST_COOKIE, encode(data), {\n    httpOnly: true,\n    secure: true,\n    sameSite: 'strict',\n    path: '/',\n    partitioned: true,\n    maxAge: 1,\n  });\n}\n\n/**\n * Server-side toast message propagator.\n * Allows queuing a toast message from a server action which will be displayed when the next route renders.\n * Only allows data serializable as JSON.\n */\nexport const serverToast = {\n  success: async (message: string, options?: ServerToastOptions) => {\n    await setToastCookie({ id: Date.now(), message, variant: 'success', ...options });\n  },\n  error: async (message: string, options?: ServerToastOptions) => {\n    await setToastCookie({ id: Date.now(), message, variant: 'error', ...options });\n  },\n  warning: async (message: string, options?: ServerToastOptions) => {\n    await setToastCookie({ id: Date.now(), message, variant: 'warning', ...options });\n  },\n  info: async (message: string, options?: ServerToastOptions) => {\n    await setToastCookie({ id: Date.now(), message, variant: 'info', ...options });\n  },\n};\n\nexport const getToastNotification = async (): Promise<ServerToastData | null> => {\n  const cookieStore = await cookies();\n  const cookie = cookieStore.get(TOAST_COOKIE);\n\n  if (!cookie) {\n    return null;\n  }\n\n  try {\n    return decode(cookie.value);\n  } catch (err) {\n    // eslint-disable-next-line no-console\n    console.error('Failed to decode toast notification cookie', err);\n\n    return null;\n  }\n};\n"
  },
  {
    "path": "core/lib/store-assets.ts",
    "content": "import { buildConfig } from '~/build-config/reader';\n\nconst storeHash = process.env.BIGCOMMERCE_STORE_HASH ?? '';\n\n/**\n * Build the CDN image URL.\n * A query parameter containing the commit SHA is appended to the URL to ensure\n * that the asset is invalidated when the storefront app is deployed.\n *\n * @param {string} sizeSegment - The size segment of the URL. Can be of the form `{:size}` (to make it a urlTemplate) or `original` or `123w` or `123x123`.\n * @param {string} source - The source of the image. Can be either `content` or `image-manager`.\n * @param {string} path - The path of the image relative to the source.\n * @param {number} cdnIndex - The index of the CDN URL to use. Defaults to 0.\n * @returns {string} The CDN image URL.\n */\nconst cdnImageUrlBuilder = (\n  sizeSegment: string,\n  source: string,\n  path: string,\n  cdnIndex = 0,\n): string => {\n  return `https://${buildConfig.get('urls').cdnUrls.at(cdnIndex)}/s-${storeHash}/images/stencil/${sizeSegment}/${source}/${path}`;\n};\n\n/**\n * Given a path, return the full URL to the content asset.\n * These assets are accessible via the /content folder in WebDAV on the store.\n * A query parameter containing the commit SHA is appended to the URL to ensure\n * that the asset is invalidated when the storefront app is deployed.\n *\n * @param {string} path - The path of the content asset.\n * @param {number} cdnIndex - The index of the CDN URL to use. Defaults to 0.\n * @returns {string} The full URL to the content asset.\n */\nexport const contentAssetUrl = (path: string, cdnIndex = 0): string => {\n  return `https://${buildConfig.get('urls').cdnUrls.at(cdnIndex)}/s-${storeHash}/content/${path}`;\n};\n\n/**\n * Build a URL or resizable URL template for an image in the /content folder in WebDAV.\n *\n * @param {string} path - The path of the image relative to the /content folder.\n * @param {string} sizeParam - The optional size parameter. Can be of the form `{:size}` (to make it a urlTemplate) or `original` or `123w` or `123x123`. If omitted, will return the templated string containing `{:size}`.\n * @returns {string} The resizeable URL template for the image, which can be used with `<Image>`.\n */\nexport const contentImageUrl = (path: string, sizeParam?: string): string => {\n  // return a urlTemplate that can be used with the <Image> component\n  return cdnImageUrlBuilder(sizeParam || '{:size}', 'content', path);\n};\n\n/**\n * Build a URL or resizable URL template for an image in the Image Manager.\n *\n * @param {string} filename - The filename of the image managed by the image manager.\n * @param {string} sizeParam - The optional size parameter. Can be of the form `{:size}` (to make it a urlTemplate) or `original` or `123w` or `123x123`. If omitted, will return the templated string containing `{:size}`.\n * @returns {string} The resizeable URL template for the image, which can be used with `<Image>`.\n */\nexport const imageManagerImageUrl = (filename: string, sizeParam?: string): string => {\n  // return a urlTemplate that can be used with the <Image> component\n  return cdnImageUrlBuilder(sizeParam || '{:size}', 'image-manager', filename);\n};\n"
  },
  {
    "path": "core/lib/user-agent.ts",
    "content": "'use server';\n\nimport { headers } from 'next/headers';\nimport { userAgent } from 'next/server';\n\nasync function getUserAgent() {\n  return userAgent({ headers: await headers() });\n}\n\nexport async function isMobileUser() {\n  const { device } = await getUserAgent();\n\n  if (!device.type) {\n    return false;\n  }\n\n  return ['mobile', 'tablet'].includes(device.type);\n}\n"
  },
  {
    "path": "core/lib/utils.ts",
    "content": "export function exists<T>(value: T | null | undefined): value is T {\n  return value != null;\n}\n"
  },
  {
    "path": "core/messages/da.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Friske fund til enhver lejlighed\",\n                \"description\": \"Gå på opdagelse i vores seneste nyheder, som er sammensat for at give dig stil, funktionalitet og inspiration. Køb nu, og opdag din næste favorit.\",\n                \"alt\": \"Fem små potteplanter udstillet på beige stablede blokke, med en række forskellige grønne blade i mørkegrå potter mod en neutral baggrund.\",\n                \"cta\": \"Køb nu\"\n            },\n            \"Slide02\": {\n                \"title\": \"Se nyhederne\",\n                \"description\": \"Køb vores nyeste varer, og find noget friskt og spændende til dit hjem.\",\n                \"alt\": \"Hænder, der rækker ud for at holde en grøn bregne i en flettet kurv med en dekorativ sløjfe, mod en beige baggrund med bløde skygger.\",\n                \"cta\": \"Køb nu\"\n            },\n            \"Slide03\": {\n                \"title\": \"Noget for enhver\",\n                \"description\": \"Gå ikke glip af eksklusive tilbud på vores bedst sælgende produkter. Køb i dag og spar stort på de varer, du elsker.\",\n                \"alt\": \"Nærbillede af et flot grønt blad med perforeringer, der fremhæver dets glatte tekstur og naturlige detaljer.\",\n                \"cta\": \"Køb nu\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Fremhævet kollektion\",\n            \"description\": \"Udforsk vores populære valg i denne fremhævede samling. Find den perfekte gave, eller forkæl dig selv!\",\n            \"cta\": \"Se mere\",\n            \"emptyStateTitle\": \"Ingen produkter fundet\",\n            \"emptyStateSubtitle\": \"Prøv at gennemse vores komplette produktkatalog.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Nye varer\",\n            \"description\": \"Vores nyeste produkter er her. Se butikkens nye produkter.\",\n            \"cta\": \"Se alle\",\n            \"emptyStateTitle\": \"Ingen produkter fundet\",\n            \"emptyStateSubtitle\": \"Prøv at gennemse vores komplette produktkatalog.\",\n            \"previousProducts\": \"Forrige produkter\",\n            \"nextProducts\": \"Næste produkter\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Skift adgangskode\",\n            \"newPassword\": \"Den nye adgangskode\",\n            \"confirmPassword\": \"Bekræft adgangskode\",\n            \"passwordUpdated\": \"Adgangskoden er blevet opdateret.\",\n            \"somethingWentWrong\": \"Noget gik galt. Prøv igen senere.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Adgangskode er påkrævet\",\n                \"passwordTooSmall\": \"Adgangskoden skal være på mindst {minLength, plural, =1 {1 character} other {# characters}}\",\n                \"passwordLowercaseRequired\": \"Adgangskoden skal indeholde mindst ét lille bogstav\",\n                \"passwordUppercaseRequired\": \"Adgangskoden skal indeholde mindst ét stort bogstav\",\n                \"passwordNumberRequired\": \"Adgangskoden skal indeholde mindst {minNumbers, plural, =1 {one number} other {# numbers}}\",\n                \"passwordSpecialCharacterRequired\": \"Adgangskoden skal indeholde mindst ét specialtegn\",\n                \"passwordsMustMatch\": \"Adgangskoderne stemmer ikke overens\",\n                \"confirmPasswordRequired\": \"Bekræft din adgangskode\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Log på\",\n            \"heading\": \"Log ind\",\n            \"forgotPassword\": \"Har du glemt din adgangskode?\",\n            \"cta\": \"Log ind\",\n            \"email\": \"E-mail\",\n            \"password\": \"Adgangskode\",\n            \"invalidCredentials\": \"Din e-mailadresse eller adgangskode er forkert. Prøv at logge ind igen, eller nulstil din adgangskode\",\n            \"somethingWentWrong\": \"Noget gik galt. Prøv igen senere.\",\n            \"passwordResetRequired\": \"Nulstilling af adgangskode påkrævet. Kontrollér din e-mail for instruktioner til at nulstille din adgangskode.\",\n            \"invalidToken\": \"Dit login-link er ugyldigt eller udløbet. Prøv at logge ind igen.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"E-mail er påkrævet\",\n                \"emailInvalid\": \"Indtast en gyldig e-mailadresse\",\n                \"passwordRequired\": \"Adgangskode er påkrævet\",\n                \"invalidInput\": \"Tjek din indtastning, og prøv igen.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"Ny kunde?\",\n                \"accountBenefits\": \"Opret en konto hos os, og du vil kunne:\",\n                \"fastCheckout\": \"Betale hurtigere\",\n                \"multipleAddresses\": \"Gemme flere forsendelsesadresser\",\n                \"ordersHistory\": \"Få adgang til din ordrehistorik\",\n                \"ordersTracking\": \"Spore nye ordrer\",\n                \"wishlists\": \"Gemme varer til din ønskeliste\",\n                \"cta\": \"Opret konto\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Glemt adgangskode\",\n                \"subtitle\": \"Indtast den e-mail, der er knyttet til din konto, nedenfor. Vi sender dig instruktioner til nulstilling af din adgangskode.\",\n                \"confirmResetPassword\": \"Hvis e-mailadressen {email} er knyttet til en konto i vores butik, har vi sendt dig en e-mail til nulstilling af adgangskode. Tjek din indbakke og spam-mappe, hvis du ikke kan se den.\",\n                \"somethingWentWrong\": \"Noget gik galt. Prøv igen senere.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"E-mail er påkrævet\",\n                    \"emailInvalid\": \"Indtast en gyldig e-mailadresse\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Registrer konto\",\n            \"heading\": \"Ny konto\",\n            \"cta\": \"Opret konto\",\n            \"somethingWentWrong\": \"Noget gik galt. Prøv igen senere.\",\n            \"recaptchaRequired\": \"Udfyld reCAPTCHA-bekræftelsen.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Fornavnet er påkrævet\",\n                \"lastNameRequired\": \"Efternavnet er påkrævet\",\n                \"emailRequired\": \"E-mail er påkrævet\",\n                \"emailInvalid\": \"Indtast en gyldig e-mailadresse\",\n                \"passwordRequired\": \"Adgangskode er påkrævet\",\n                \"passwordTooSmall\": \"Adgangskoden skal være på mindst {minLength, plural, =1 {1 character} other {# characters}}\",\n                \"passwordLowercaseRequired\": \"Adgangskoden skal indeholde mindst ét lille bogstav\",\n                \"passwordUppercaseRequired\": \"Adgangskoden skal indeholde mindst ét stort bogstav\",\n                \"passwordNumberRequired\": \"Adgangskoden skal indeholde mindst {minNumbers, plural, =1 {one number} other {# numbers}}\",\n                \"passwordSpecialCharacterRequired\": \"Adgangskoden skal indeholde mindst ét specialtegn\",\n                \"passwordsMustMatch\": \"Adgangskoderne stemmer ikke overens\",\n                \"addressLine1Required\": \"Adresselinje 1 er påkrævet\",\n                \"cityRequired\": \"Byen er påkrævet\",\n                \"countryRequired\": \"Landet er påkrævet\",\n                \"stateRequired\": \"Stat/region er påkrævet\",\n                \"postalCodeRequired\": \"Postnummeret er påkrævet\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"Der er ingen produkter i dette mærke\",\n                \"subtitle\": \"Prøv at bruge andre filtre.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Kategorier\",\n            \"Empty\": {\n                \"title\": \"Ingen produkter i denne kategori\",\n                \"subtitle\": \"Prøv at bruge andre filtre.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Søgeresultater\",\n            \"searchResults\": \"Søgeresultater for\",\n            \"subCategories\": \"Kategorier\",\n            \"Breadcrumbs\": {\n                \"home\": \"Hjem\",\n                \"search\": \"Søg\"\n            },\n            \"Empty\": {\n                \"title\": \"Beklager, der var ingen resultater for “{term}”.\",\n                \"subtitle\": \"Prøv en anden søgning.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtre\",\n            \"resetFilters\": \"Nulstil filtre\",\n            \"Range\": {\n                \"apply\": \"Anvend\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Gratis fragt\",\n                \"isFeaturedLabel\": \"Er udvalgt\",\n                \"inStockLabel\": \"På lager\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Sortér efter:\",\n            \"featuredItems\": \"Udvalgte varer\",\n            \"bestSellingItems\": \"Bedst sælgende varer\",\n            \"newestItems\": \"Nyeste varer\",\n            \"aToZ\": \"A til Å\",\n            \"zToA\": \"Å til A\",\n            \"byReview\": \"Efter anmeldelse\",\n            \"priceAscending\": \"Pris: Stigende\",\n            \"priceDescending\": \"Pris: Faldende\",\n            \"relevance\": \"Relevans\"\n        },\n        \"Compare\": {\n            \"compare\": \"Sammenlign\",\n            \"remove\": \"Fjern\",\n            \"maxCompareLimit\": \"Du har nået det maksimale antal produkter til sammenligning. Fjern et produkt for at tilføje et nyt.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Adresser\",\n            \"logout\": \"Log ud\",\n            \"orders\": \"Ordrer\",\n            \"settings\": \"Konto\",\n            \"wishlists\": \"Ønskelister\"\n        },\n        \"Orders\": {\n            \"title\": \"Ordrer\",\n            \"orderNumber\": \"Ordrenr.\",\n            \"totalPrice\": \"I alt\",\n            \"viewDetails\": \"Se detaljer\",\n            \"EmptyState\": {\n                \"title\": \"Du har ikke nogen ordrer\",\n                \"cta\": \"Køb nu\"\n            },\n            \"Details\": {\n                \"title\": \"Ordrenr. {orderNumber}\",\n                \"shippingAddress\": \"Leveringsadresse\",\n                \"shippingMethod\": \"Forsendelsesmetode\",\n                \"summaryTotal\": \"I alt\",\n                \"destination\": \"Destination\",\n                \"destinationWithCount\": \"Destination {number, number}/{total, number}\",\n                \"digitalDelivery\": \"Digital levering til {email}\",\n                \"subtotal\": \"Subtotal\",\n                \"shipping\": \"Forsendelse\",\n                \"tax\": \"Moms\",\n                \"orderSummary\": \"Ordreoversigt\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Betalingsmetode} other {Betalingsmetoder}}\",\n                \"paymentEndingInLabel\": \"Slutter på\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Kreditkort\",\n                    \"giftCertificate\": \"Gavekort\",\n                    \"storeCredit\": \"Butikskredit\",\n                    \"other\": \"Andet\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Adresser\",\n            \"cta\": \"Tilføj adresse\",\n            \"edit\": \"Rediger\",\n            \"delete\": \"Slet\",\n            \"cancel\": \"Annuller\",\n            \"create\": \"Opret\",\n            \"update\": \"Opdater\",\n            \"setDefault\": \"Indstil som standard\",\n            \"somethingWentWrong\": \"Noget gik galt. Prøv igen senere.\",\n            \"EmptyState\": {\n                \"title\": \"Du har ingen adresser\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Fornavnet er påkrævet\",\n                \"lastNameRequired\": \"Efternavnet er påkrævet\",\n                \"addressLine1Required\": \"Adresselinje 1 er påkrævet\",\n                \"cityRequired\": \"Byen er påkrævet\",\n                \"countryRequired\": \"Landet er påkrævet\",\n                \"stateRequired\": \"Stat/region er påkrævet\",\n                \"postalCodeRequired\": \"Postnummeret er påkrævet\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"Kontoindstillinger\",\n            \"changePassword\": \"Skift adgangskode\",\n            \"passwordUpdated\": \"Adgangskoden er blevet opdateret.\",\n            \"settingsUpdated\": \"Kontoindstillingerne er blevet opdateret!\",\n            \"somethingWentWrong\": \"Noget gik galt. Prøv igen senere.\",\n            \"currentPassword\": \"Nuværende adgangskode\",\n            \"newPassword\": \"Den nye adgangskode\",\n            \"confirmPassword\": \"Bekræft adgangskode\",\n            \"cta\": \"Opdater\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Markedsføringspræferencer\",\n                \"label\": \"Abonner på vores nyhedsbrev.\",\n                \"marketingPreferencesUpdated\": \"Markedsføringspræferencerne er blevet opdateret!\",\n                \"somethingWentWrong\": \"Noget gik galt. Prøv igen senere.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Fornavnet er påkrævet\",\n                \"firstNameTooSmall\": \"Fornavnet skal være på mindst 2 tegn\",\n                \"lastNameRequired\": \"Efternavnet er påkrævet\",\n                \"lastNameTooSmall\": \"Efternavnet skal være på mindst 2 tegn\",\n                \"emailRequired\": \"E-mail er påkrævet\",\n                \"emailInvalid\": \"Indtast en gyldig e-mailadresse\",\n                \"currentPasswordRequired\": \"Aktuel adgangskode er påkrævet\",\n                \"passwordRequired\": \"Adgangskode er påkrævet\",\n                \"passwordTooSmall\": \"Adgangskoden skal være på mindst {minLength, plural, =1 {1 character} other {# characters}}\",\n                \"passwordLowercaseRequired\": \"Adgangskoden skal indeholde mindst ét lille bogstav\",\n                \"passwordUppercaseRequired\": \"Adgangskoden skal indeholde mindst ét stort bogstav\",\n                \"passwordNumberRequired\": \"Adgangskoden skal indeholde mindst {minNumbers, plural, =1 {one number} other {# numbers}}\",\n                \"passwordSpecialCharacterRequired\": \"Adgangskoden skal indeholde mindst ét specialtegn\",\n                \"passwordsMustMatch\": \"Adgangskoderne stemmer ikke overens\",\n                \"confirmPasswordRequired\": \"Bekræft din adgangskode\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Handlinger for ønskeliste\",\n        \"title\": \"Ønskelister\",\n        \"new\": \"Ny ønskeliste\",\n        \"items\": \"{count, plural, =1 {1 vare} other {# varer}}\",\n        \"viewWishlist\": \"Se liste\",\n        \"noWishlists\": \"Du har ingen ønskelister\",\n        \"noWishlistsCallToAction\": \"Opret en ønskeliste\",\n        \"emptyWishlist\": \"Du har ikke tilføjet produkter til denne ønskeliste.\",\n        \"share\": \"Del\",\n        \"shareSuccess\": \"Ønskelisten er delt.\",\n        \"shareCopied\": \"Ønskelistens offentlige URL er blevet kopieret til din udklipsholder.\",\n        \"shareDisabled\": \"Din ønskeliste skal være offentlig for at kunne dele den.\",\n        \"makePublic\": \"Gør offentlig\",\n        \"makePrivate\": \"Gør privat\",\n        \"rename\": \"Omdøb\",\n        \"delete\": \"Slet\",\n        \"removeButtonTitle\": \"Fjern produktet fra ønskelisten\",\n        \"Visibility\": {\n            \"public\": \"Offentlig\",\n            \"private\": \"Privat\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Annuller\",\n            \"close\": \"Luk\",\n            \"copy\": \"Kopi\",\n            \"create\": \"Opret\",\n            \"save\": \"Gem\",\n            \"delete\": \"Slet\",\n            \"newTitle\": \"Opret en ny ønskeliste\",\n            \"shareTitle\": \"Del {name}\",\n            \"renameTitle\": \"Omdøb {name}\",\n            \"deleteTitle\": \"Slet {name}\",\n            \"changeVisibilityPublicTitle\": \"Vil du gøre {name} offentlig?\",\n            \"changeVisibilityPrivateTitle\": \"Vil du gøre {name} privat?\",\n            \"makePublicContent\": \"Er du sikker på du vil gøre <bold>{name}</bold> offentlig? Dette giver andre mulighed for at se din ønskeliste, hvis de har linket.\",\n            \"makePrivateContent\": \"Er du sikker på du vil gøre <bold>{name}</bold> privat? Hvis du har delt din ønskeliste med andre, vil de ikke længere kunne se den.\",\n            \"deleteContent\": \"Er du sikker på, at du vil slette <bold>{name}</bold>? Denne handling kan ikke fortrydes.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Navn\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"Navnet på ønskelisten må ikke være tomt.\",\n            \"updateFailed\": \"Din ønskeliste kunne ikke opdateres. Prøv igen.\",\n            \"deleteFailed\": \"Din ønskeliste kunne ikke slettes. Prøv igen.\",\n            \"removeProductFailed\": \"Produktet kunne ikke fjernes fra din ønskeliste. Prøv igen.\",\n            \"unauthorized\": \"Du er ikke autoriseret til at udføre denne handling. Log ind, og prøv igen.\",\n            \"unexpected\": \"Der opstod en uventet fejl. Prøv igen.\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"Ønskelisten er oprettet.\",\n            \"updateSuccess\": \"Ønskelisten er opdateret.\",\n            \"deleteSuccess\": \"Ønskelisten er slettet.\",\n            \"removeItemSuccess\": \"Varen er blevet fjernet fra din ønskeliste.\"\n        },\n        \"Button\": {\n            \"label\": \"Føj til ønskeliste\",\n            \"addToNewWishlist\": \"Føj til ny ønskeliste\",\n            \"defaultWishlistName\": \"Min ønskeliste\",\n            \"addSuccessMessage\": \"Produktet er blevet føjet til din ønskeliste\",\n            \"removeSuccessMessage\": \"Produktet er blevet fjernet fra din ønskeliste\",\n            \"Errors\": {\n                \"addProductFailed\": \"Produktet kunne ikke føjes til din ønskeliste. Prøv igen.\",\n                \"removeProductFailed\": \"Produktet kunne ikke fjernes fra din ønskeliste. Prøv igen.\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Offentlig ønskeliste\",\n        \"defaultName\": \"Offentlig ønskeliste\",\n        \"emptyWishlist\": \"Denne ønskeliste har endnu ingen produkter.\"\n    },\n    \"Blog\": {\n        \"title\": \"blog\",\n        \"home\": \"Hjem\",\n        \"Empty\": {\n            \"title\": \"Der blev ikke fundet et blogindlæg\",\n            \"subtitle\": \"Kom tilbage senere for mere indhold.\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Del\",\n            \"email\": \"E-mail\",\n            \"print\": \"Udskriv\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Indkøbskurv\",\n        \"heading\": \"Din indkøbskurv\",\n        \"proceedToCheckout\": \"Gå til kassen\",\n        \"increment\": \"Øg mængden\",\n        \"decrement\": \"Reducer mængden\",\n        \"removeItem\": \"Fjern vare\",\n        \"cartCombined\": \"Vi bemærkede, at du havde gemt varer i en tidligere indkøbskurv, så vi har føjet dem til din nuværende indkøbskurv for dig.\",\n        \"cartRestored\": \"Du startede en indkøbskurv på en anden enhed, og vi har gendannet den her, så du kan fortsætte, hvor du slap.\",\n        \"cartUpdateInProgress\": \"Du har en igangværende opdatering af din indkøbskurv. Er du sikker på, at du vil forlade denne side? Dine ændringer kan gå tabt.\",\n        \"originalPrice\": \"Den oprindelige pris var {price}.\",\n        \"currentPrice\": \"Den aktuelle pris er {price}.\",\n        \"quantityReadyToShip\": \"{quantity, number} klar til forsendelse\",\n        \"quantityOnBackorder\": \"{antal, number} vil være i restordre\",\n        \"partiallyAvailable\": \"Kun {quantity, number} tilgængelig(e)\",\n        \"CheckoutSummary\": {\n            \"title\": \"Oversigt\",\n            \"subTotal\": \"Subtotal\",\n            \"discounts\": \"Rabatter\",\n            \"tax\": \"Moms\",\n            \"total\": \"I alt\",\n            \"CouponCode\": {\n                \"apply\": \"Anvend\",\n                \"couponCode\": \"Kuponkode\",\n                \"removeCouponCode\": \"Fjern kuponkode\",\n                \"invalidCouponCode\": \"Indtast en gyldig kuponkode\",\n                \"cartNotFound\": \"Der opstod en fejl ved hentning af din indkøbskurv\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Forsendelse\",\n                \"add\": \"Tilføj\",\n                \"change\": \"Ændr\",\n                \"cancel\": \"Annuller\",\n                \"country\": \"Land\",\n                \"city\": \"By\",\n                \"state\": \"Landsdel/kommune\",\n                \"postalCode\": \"Postnummer\",\n                \"updatedShippingOptions\": \"Opdater forsendelsesmuligheder\",\n                \"viewShippingOptions\": \"Se forsendelsesmuligheder\",\n                \"editAddress\": \"Rediger adresse\",\n                \"shippingOptions\": \"Forsendelsesmuligheder\",\n                \"updateShipping\": \"Opdater forsendelse\",\n                \"addShipping\": \"Tilføj forsendelse\",\n                \"cartNotFound\": \"Der opstod en fejl ved hentning af din indkøbskurv\",\n                \"noShippingOptions\": \"Der er ingen tilgængelige forsendelsesmuligheder for din adresse\",\n                \"countryRequired\": \"Landet er påkrævet\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Gavekort\",\n            \"giftCertificateCode\": \"Gavekortkode\",\n            \"removeGiftCertificate\": \"Fjern gavekortet\",\n            \"apply\": \"Anvend\",\n            \"to\": \"Til\",\n            \"message\": \"Meddelelse\",\n            \"invalidGiftCertificate\": \"Indtast en gyldig gavekortkode\",\n            \"cartNotFound\": \"Der opstod en fejl ved hentning af din indkøbskurv\"\n        },\n        \"Empty\": {\n            \"title\": \"Din indkøbskurv er tom.\",\n            \"subtitle\": \"Tilføj nogle produkter for at komme i gang.\",\n            \"cta\": \"Fortsæt med at handle\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Der opstod en fejl ved hentning af din indkøbskurv\",\n            \"lineItemNotFound\": \"Linjeelement ikke fundet.\",\n            \"failedToUpdateQuantity\": \"Antallet kunne ikke opdateres.\",\n            \"somethingWentWrong\": \"Noget gik galt. Prøv igen senere.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Sammenlign produkter\",\n        \"addToCart\": \"Føj til kurv\",\n        \"next\": \"Næste produkter\",\n        \"previous\": \"Forrige produkter\",\n        \"noProductsToCompare\": \"Der er ingen produkter at sammenligne\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Vægt\",\n        \"description\": \"Beskrivelse\",\n        \"noDescription\": \"Der er ingen beskrivelse tilgængelig.\",\n        \"rating\": \"Bedømmelse\",\n        \"noRatings\": \"Der er ingen anmeldelser.\",\n        \"otherDetails\": \"Andre oplysninger\",\n        \"noOtherDetails\": \"Der er ingen andre oplysninger.\",\n        \"viewOptions\": \"Se muligheder\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 vare} other {# varer}} tilføjet til <cartLink>din indkøbskurv</cartLink>\",\n        \"missingCart\": \"Indkøbskurven blev ikke fundet. Prøv igen senere.\",\n        \"unknownError\": \"Ukendt fejl. Prøv igen senere.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Antal\",\n            \"increaseQuantity\": \"Øg mængden\",\n            \"decreaseQuantity\": \"Reducer mængden\",\n            \"emptySelectPlaceholder\": \"Vælg en valgmulighed\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 vare} other {# varer}} tilføjet til <cartLink>din indkøbskurv</cartLink>\",\n            \"missingCart\": \"Indkøbskurven blev ikke fundet. Prøv igen senere.\",\n            \"unknownError\": \"Ukendt fejl. Prøv igen senere.\",\n            \"variantRequiredError\": \"Dette produkt kræver, at der vælges muligheder for at kunne lægges i indkøbskurven.\",\n            \"increaseNumber\": \"Øg antallet\",\n            \"decreaseNumber\": \"Reducer antallet\",\n            \"thumbnail\": \"Vis billednummer\",\n            \"additionalInformation\": \"Yderligere oplysninger\",\n            \"currentStock\": \"{antal, number} på lager\",\n            \"backorderQuantity\": \"{antal, number} vil være i restordre\",\n            \"loadingMoreImages\": \"Indlæser flere billeder\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 yderligere billede indlæst} other {# yderligere billeder indlæst}}\",\n            \"Submit\": {\n                \"addToCart\": \"Føj til kurv\",\n                \"outOfStock\": \"Udsolgt\",\n                \"preorder\": \"Forudbestil\",\n                \"unavailable\": \"Ikke tilgængelig\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Specifikationer\",\n                \"warranty\": \"Garanti\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Vægt\",\n                \"condition\": \"Tilstand\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Relaterede produkter\",\n            \"noRelatedProducts\": \"Der blev ikke fundet nogen relaterede produkter\",\n            \"browseCatalog\": \"Prøv at gennemse vores komplette produktkatalog.\",\n            \"cta\": \"Se alle\",\n            \"previousProducts\": \"Forrige produkter\",\n            \"nextProducts\": \"Næste produkter\",\n            \"scrollbar\": \"Rullepanel med relaterede produkter\"\n        },\n        \"Reviews\": {\n            \"title\": \"Anmeldelser\",\n            \"empty\": \"Der er ikke tilføjet nogen anmeldelser for dette produkt.\",\n            \"previous\": \"Foregående anmeldelser\",\n            \"next\": \"Næste anmeldelser\",\n            \"Form\": {\n                \"button\": \"Skriv en anmeldelse\",\n                \"title\": \"Skriv en anmeldelse\",\n                \"submit\": \"Indsend\",\n                \"cancel\": \"Annuller\",\n                \"ratingLabel\": \"Bedømmelse\",\n                \"titleLabel\": \"Titel\",\n                \"reviewLabel\": \"Anmeldelse\",\n                \"nameLabel\": \"Navn\",\n                \"emailLabel\": \"E-mail\",\n                \"successMessage\": \"Din anmeldelse er blevet indsendt!\",\n                \"somethingWentWrong\": \"Noget gik galt. Prøv igen senere.\",\n                \"recaptchaRequired\": \"Udfyld reCAPTCHA-bekræftelsen.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Titel er påkrævet\",\n                    \"authorRequired\": \"Navn er påkrævet\",\n                    \"emailRequired\": \"E-mail er påkrævet\",\n                    \"emailInvalid\": \"Indtast en gyldig e-mailadresse\",\n                    \"textRequired\": \"Anmeldelse er påkrævet\",\n                    \"ratingRequired\": \"Bedømmelse er påkrævet\",\n                    \"ratingTooSmall\": \"Bedømmelsen skal være mindst 1\",\n                    \"ratingTooLarge\": \"Bedømmelsen må højst være 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Hjem\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Hjem\",\n            \"Form\": {\n                \"success\": \"Tak for din henvendelse. Vi vender snart tilbage til dig.\",\n                \"successCta\": \"Fortsæt med at handle\",\n                \"fullName\": \"Fulde navn\",\n                \"companyName\": \"Firmanavn\",\n                \"phone\": \"Telefon\",\n                \"orderNo\": \"Ordrenummer\",\n                \"rma\": \"RMA-nummer\",\n                \"email\": \"E-mail\",\n                \"comments\": \"Kommentarer/spørgsmål\",\n                \"cta\": \"Indsend formular\",\n                \"somethingWentWrong\": \"Noget gik galt. Prøv igen senere.\",\n                \"recaptchaRequired\": \"Udfyld reCAPTCHA-bekræftelsen.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"Vedligeholdelse\",\n        \"message\": \"Vi er nede på grund af vedligeholdelse\",\n        \"contactUs\": \"Du kan kontakte os på:\"\n    },\n    \"Error\": {\n        \"title\": \"Der opstod en serverfejl.\",\n        \"subtitle\": \"Prøv igen senere.\",\n        \"cta\": \"Prøv igen\"\n    },\n    \"NotFound\": {\n        \"title\": \"Vi kunne ikke finde den side.\",\n        \"subtitle\": \"Prøv at søge efter noget andet, eller gå tilbage til startsiden.\",\n        \"featuredProducts\": \"Udvalgte produkter\",\n        \"search\": \"Søg\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Hjem\",\n            \"toggleNavigation\": \"Skift navigation\",\n            \"Icons\": {\n                \"account\": \"Profil\",\n                \"cart\": \"Indkøbskurv\",\n                \"search\": \"Åbn søge-pop op\",\n                \"giftCertificates\": \"Gavekort\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Skift valuta\",\n                \"invalidCurrency\": \"Ugyldig valuta\",\n                \"errorUpdatingCurrency\": \"Fejl ved opdatering af valuta for din indkøbskurv. Prøv igen.\"\n            },\n            \"Search\": {\n                \"products\": \"produkter\",\n                \"categories\": \"Kategorier\",\n                \"brands\": \"mærker\",\n                \"noSearchResultsTitle\": \"Beklager, der var ingen resultater for “{term}”.\",\n                \"noSearchResultsSubtitle\": \"Prøv en anden søgning.\",\n                \"somethingWentWrong\": \"Noget gik galt. Prøv igen.\",\n                \"inputPlaceholder\": \"Søg efter produkter, kategorier, mærker ...\",\n                \"submitLabel\": \"Søg\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Hjem\",\n            \"contactUs\": \"kontakte os\",\n            \"socialMediaLinks\": \"Links til sociale medier\",\n            \"categories\": \"Kategorier\",\n            \"brands\": \"mærker\",\n            \"navigate\": \"Navigér\",\n            \"giftCertificates\": \"Gavekort\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Tilmeld dig vores nyhedsbrev\",\n            \"placeholder\": \"Indtast din e-mailadresse\",\n            \"description\": \"Hold dig opdateret med de seneste nyheder og tilbud fra vores butik.\",\n            \"subscribedToNewsletter\": \"Du er blevet tilmeldt vores nyhedsbrev!\",\n            \"Errors\": {\n                \"emailRequired\": \"E-mail er påkrævet\",\n                \"invalidEmail\": \"Indtast en gyldig e-mailadresse\",\n                \"somethingWentWrong\": \"Noget gik galt. Prøv igen senere.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Afvis alle\",\n                \"acceptAll\": \"Accepter alle\",\n                \"customize\": \"Tilpas\",\n                \"save\": \"Gem indstillinger\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"Vi værdsætter dit privatliv\",\n                \"description\": \"Denne hjemmeside bruger cookies til at forbedre din browseroplevelse, analysere webstedstrafik og vise personligt tilpasset indhold.\",\n                \"privacyPolicy\": \"Politik om beskyttelse af personlige oplysninger\"\n            },\n            \"Dialog\": {\n                \"title\": \"Privatlivsindstillinger\",\n                \"description\": \"Tilpas dine privatlivsindstillinger her. Du kan vælge, hvilke typer cookies og tracking-teknologier du vil tillade.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Strengt nødvendigt\",\n                    \"description\": \"Disse cookies er afgørende for, at hjemmesiden fungerer korrekt og kan ikke deaktiveres.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Funktionalitet\",\n                    \"description\": \"Disse cookies muliggør forbedret funktionalitet og personalisering af hjemmesiden.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Markedsføring\",\n                    \"description\": \"Disse cookies anvendes til at levere relevante annoncer og spore deres effektivitet.\"\n                },\n                \"measurement\": {\n                    \"title\": \"Analyse\",\n                    \"description\": \"Disse cookies hjælper os med at forstå, hvordan besøgende interagerer med hjemmesiden og forbedrer dens ydeevne.\"\n                },\n                \"experience\": {\n                    \"title\": \"Oplevelse\",\n                    \"description\": \"Disse cookies hjælper os med at give en bedre brugeroplevelse og teste nye funktioner.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"Den oprindelige pris var {price}.\",\n            \"currentPrice\": \"Den aktuelle pris er {price}.\",\n            \"range\": \"Pris fra {minValue} til {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Gavekort\",\n        \"description\": \"Giv den perfekte gave, der aldrig går af mode. Lad venner og familie vælge præcis, hvad de ønsker sig fra hele vores kollektion.\",\n        \"purchaseLabel\": \"Køb nu\",\n        \"checkBalanceLabel\": \"Tjek saldo\",\n        \"expiresAtLabel\": \"Gyldig til\",\n        \"CheckBalance\": {\n            \"title\": \"Tjek saldo\",\n            \"description\": \"Du kan tjekke saldoen og få oplysninger om dit gavekort ved at indtaste koden i feltet nedenfor.\",\n            \"inputLabel\": \"Kode\",\n            \"inputPlaceholder\": \"XXX-XXX-XXX-XXX\",\n            \"purchasedDateLabel\": \"Købt\",\n            \"senderLabel\": \"Fra\",\n            \"Errors\": {\n                \"invalidCode\": \"Gavekortkoden, du indtastede, er ugyldig. Kontroller koden, og prøv igen.\",\n                \"codeRequired\": \"Indtast en gavekortkode.\",\n                \"somethingWentWrong\": \"Noget gik galt. Prøv igen senere.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Køb et gavekort\",\n            \"title\": \"Digitalt gavekort\",\n            \"description\": \"Udforsk vores gavekort, som er perfekte til enhver lejlighed. Vælg beløbet, og personaliser din besked.\",\n            \"successMessage\": \"Gavekort er blevet tilføjet til <cartLink> din kurv</cartLink>\",\n            \"missingCart\": \"Indkøbskurven blev ikke fundet. Prøv igen senere.\",\n            \"unknownError\": \"Ukendt fejl. Prøv igen senere.\",\n            \"Form\": {\n                \"amountLabel\": \"Beløb\",\n                \"customAmountLabel\": \"Beløb (mellem {minAmount} og {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Vælg et beløb\",\n                \"customAmountPlaceholder\": \"Indtast et brugerdefineret beløb\",\n                \"senderNameLabel\": \"Dit navn\",\n                \"senderEmailLabel\": \"Din email\",\n                \"recipientNameLabel\": \"Modtagers navn\",\n                \"recipientEmailLabel\": \"Modtagers e-mail\",\n                \"namePlaceholder\": \"Indtast navn\",\n                \"emailPlaceholder\": \"Indtast e-mail\",\n                \"messageLabel\": \"Meddelelse\",\n                \"messagePlaceholder\": \"Indtast din besked (valgfrit)\",\n                \"nonRefundableCheckboxLabel\": \"Jeg accepterer, at gavekort ikke kan refunderes\",\n                \"expiryCheckboxLabel\": \"Jeg accepterer, at dette gavekort udløber den {expiryDate}\",\n                \"ctaLabel\": \"Føj til kurv\",\n                \"Errors\": {\n                    \"amountRequired\": \"Vælg eller indtast et gavekortbeløb\",\n                    \"amountInvalid\": \"Vælg et gyldigt gavekortbeløb\",\n                    \"amountOutOfRange\": \"Indtast et beløb mellem {minAmount} og {maxAmount}\",\n                    \"unexpectedSettingsError\": \"Der opstod en uventet fejl under hentning af indstillingerne for gavekort. Prøv igen senere.\",\n                    \"senderNameRequired\": \"Dit navn er påkrævet\",\n                    \"senderEmailRequired\": \"Din e-mailadresse er påkrævet\",\n                    \"recipientNameRequired\": \"Modtagerens navn er påkrævet\",\n                    \"recipientEmailRequired\": \"Modtagerens e-mailadresse er påkrævet\",\n                    \"emailInvalid\": \"Indtast en gyldig e-mailadresse\",\n                    \"checkboxRequired\": \"Du skal sætte kryds i dette felt for at fortsætte\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"Valgfrit\",\n        \"recaptchaRequired\": \"Udfyld reCAPTCHA-bekræftelsen.\",\n        \"Errors\": {\n            \"invalidInput\": \"Tjek din indtastning, og prøv igen\",\n            \"invalidFormat\": \"Den indtastede værdi stemmer ikke overens med det krævede format\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/de.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Frische Entdeckungen für jeden Anlass\",\n                \"description\": \"Entdecken Sie unsere neuesten Produkte, die Sie mit Stil, Funktionalität und Inspiration begeistern werden. Kaufen Sie jetzt ein und entdecken Sie Ihren neuesten Lieblingsartikel.\",\n                \"alt\": \"Fünf kleine Topfpflanzen auf beigefarbenen, gestapelten Blöcken mit einer Vielzahl von grünen Blättern in dunkelgrauen Töpfen vor einem neutralen Hintergrund.\",\n                \"cta\": \"Jetzt shoppen\"\n            },\n            \"Slide02\": {\n                \"title\": \"Neues entdecken\",\n                \"description\": \"Entdecken Sie unsere neuesten Produkte und finden Sie etwas Originelles und Aufregendes für Ihr Zuhause.\",\n                \"alt\": \"Hände, die ausgestreckt sind, um einen grünen Farn in einem geflochtenen Korb mit einer dekorativen Schleife zu halten, vor einem beigefarbenen Hintergrund mit weichen Schatten.\",\n                \"cta\": \"Jetzt shoppen\"\n            },\n            \"Slide03\": {\n                \"title\": \"Etwas für jeden Geschmack\",\n                \"description\": \"Verpassen Sie nicht die exklusiven Angebote für unsere Bestseller-Produkte. Kaufen Sie noch heute und sparen Sie bei Ihren Lieblingsartikeln.\",\n                \"alt\": \"Nahaufnahme eines leuchtend grünen Blattes mit Perforationen, das seine glatte Struktur und natürlichen Details zur Geltung bringt.\",\n                \"cta\": \"Jetzt shoppen\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Vorgestellte Kollektion\",\n            \"description\": \"Entdecken Sie unsere Top-Auswahl in dieser vorgestellten Kollektion. Finden Sie das perfekte Geschenk oder gönnen Sie sich selbst etwas!\",\n            \"cta\": \"Mehr anzeigen\",\n            \"emptyStateTitle\": \"Keine Produkte gefunden\",\n            \"emptyStateSubtitle\": \"Entdecken Sie unseren kompletten Produktkatalog.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Neu eingetroffen\",\n            \"description\": \"Unsere neuesten Produkte sind hier. Entdecken Sie unsere neuesten Produkte.\",\n            \"cta\": \"Alle anzeigen\",\n            \"emptyStateTitle\": \"Keine Produkte gefunden\",\n            \"emptyStateSubtitle\": \"Entdecken Sie unseren kompletten Produktkatalog.\",\n            \"previousProducts\": \"Vorherige Produkte\",\n            \"nextProducts\": \"Nächste Produkte\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Passwort ändern\",\n            \"newPassword\": \"Neues Passwort\",\n            \"confirmPassword\": \"Passwort bestätigen\",\n            \"passwordUpdated\": \"Passwort wurde erfolgreich aktualisiert!\",\n            \"somethingWentWrong\": \"Es ist etwas schiefgegangen Bitte versuchen Sie es später erneut.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Es muss ein Passwort angegeben werden\",\n                \"passwordTooSmall\": \"Das Passwort muss mindestens {minLength, plural, =1 {1 Zeichen} other {# Zeichen}} lang sein\",\n                \"passwordLowercaseRequired\": \"Das Passwort muss mindestens einen Kleinbuchstaben enthalten\",\n                \"passwordUppercaseRequired\": \"Das Passwort muss mindestens einen Großbuchstaben enthalten\",\n                \"passwordNumberRequired\": \"Das Passwort muss mindestens {minNumbers, plural, =1 {eine Zahl} other {# Zahlen}} enthalten\",\n                \"passwordSpecialCharacterRequired\": \"Das Passwort muss mindestens ein Sonderzeichen enthalten\",\n                \"passwordsMustMatch\": \"Die Passwörter stimmen nicht überein\",\n                \"confirmPasswordRequired\": \"Bitte bestätigen Sie Ihr Passwort\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Anmelden\",\n            \"heading\": \"Anmelden\",\n            \"forgotPassword\": \"Passwort vergessen?\",\n            \"cta\": \"Anmelden\",\n            \"email\": \"E-Mail\",\n            \"password\": \"Passwort\",\n            \"invalidCredentials\": \"Ihre E-Mail-Adresse oder Ihr Passwort ist falsch. Versuchen Sie, sich erneut anzumelden, oder setzen Sie Ihr Passwort zurück\",\n            \"somethingWentWrong\": \"Es ist etwas schiefgegangen Bitte versuchen Sie es später erneut.\",\n            \"passwordResetRequired\": \"Passwortzurücksetzung erforderlich. Bitte überprüfen Sie Ihre E-Mails für die Anleitung zum Zurücksetzen Ihres Passworts.\",\n            \"invalidToken\": \"Ihr Anmeldelink ist ungültig oder abgelaufen. Bitte versuchen Sie erneut, sich anzumelden.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"Die E-Mail-Adresse muss angegeben werden\",\n                \"emailInvalid\": \"Bitte geben Sie eine gültige E-Mail-Adresse an\",\n                \"passwordRequired\": \"Es muss ein Passwort angegeben werden\",\n                \"invalidInput\": \"Bitte überprüfen Sie Ihre Eingabe und versuchen Sie es erneut\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"Neuer Kunde?\",\n                \"accountBenefits\": \"Wenn Sie ein Konto bei uns erstellen, haben Sie folgende Möglichkeiten:\",\n                \"fastCheckout\": \"Schnellerer Bezahlvorgang\",\n                \"multipleAddresses\": \"Speicherung mehrerer Lieferadressen\",\n                \"ordersHistory\": \"Zugang zu Ihrem Bestellverlauf\",\n                \"ordersTracking\": \"Tracking von neuen Bestellungen\",\n                \"wishlists\": \"Hinzufügen von Artikeln zu Ihrer Wunschliste\",\n                \"cta\": \"Konto erstellen\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Passwort vergessen\",\n                \"subtitle\": \"Geben Sie unten die mit Ihrem Konto verknüpfte E-Mail-Adresse ein. Wir senden Ihnen Anweisungen zum Zurücksetzen Ihres Passworts.\",\n                \"confirmResetPassword\": \"Wenn die E-Mail-Adresse {email} mit einem Konto in unserem Geschäft verknüpft ist, haben wir Ihnen eine E-Mail zum Zurücksetzen des Passworts gesendet. Bitte überprüfen Sie Ihren Posteingang und Spam-Ordner, wenn Sie sie nicht sehen.\",\n                \"somethingWentWrong\": \"Es ist etwas schiefgegangen Bitte versuchen Sie es später erneut.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"Die E-Mail-Adresse muss angegeben werden\",\n                    \"emailInvalid\": \"Bitte geben Sie eine gültige E-Mail-Adresse an\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Konto registrieren\",\n            \"heading\": \"Neues Konto\",\n            \"cta\": \"Konto erstellen\",\n            \"somethingWentWrong\": \"Es ist etwas schiefgegangen Bitte versuchen Sie es später erneut.\",\n            \"recaptchaRequired\": \"Bitte führen Sie die reCAPTCHA-Verifizierung durch.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Es muss ein Vorname angegeben werden\",\n                \"lastNameRequired\": \"Es muss ein Nachname angegeben werden\",\n                \"emailRequired\": \"Die E-Mail-Adresse muss angegeben werden\",\n                \"emailInvalid\": \"Bitte geben Sie eine gültige E-Mail-Adresse an\",\n                \"passwordRequired\": \"Es muss ein Passwort angegeben werden\",\n                \"passwordTooSmall\": \"Das Passwort muss mindestens {minLength, plural, =1 {1 Zeichen} other {# Zeichen}} lang sein\",\n                \"passwordLowercaseRequired\": \"Das Passwort muss mindestens einen Kleinbuchstaben enthalten\",\n                \"passwordUppercaseRequired\": \"Das Passwort muss mindestens einen Großbuchstaben enthalten\",\n                \"passwordNumberRequired\": \"Das Passwort muss mindestens {minNumbers, plural, =1 {eine Zahl} other {# Zahlen}} enthalten\",\n                \"passwordSpecialCharacterRequired\": \"Das Passwort muss mindestens ein Sonderzeichen enthalten\",\n                \"passwordsMustMatch\": \"Die Passwörter stimmen nicht überein\",\n                \"addressLine1Required\": \"Adresszeile 1 ist eine Pflichtangabe\",\n                \"cityRequired\": \"Ort ist eine Pflichtangabe\",\n                \"countryRequired\": \"Land ist eine Pflichtangabe\",\n                \"stateRequired\": \"Bundesstaat/Provinz ist eine Pflichtangabe\",\n                \"postalCodeRequired\": \"Postleitzahl ist erforderlich\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"Keine Produkte für diese Marke\",\n                \"subtitle\": \"Verwenden Sie verschiedene Filter.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Kategorien\",\n            \"Empty\": {\n                \"title\": \"Keine Produkte in dieser Kategorie\",\n                \"subtitle\": \"Verwenden Sie verschiedene Filter.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Suchergebnisse\",\n            \"searchResults\": \"Suchergebnisse für\",\n            \"subCategories\": \"Kategorien\",\n            \"Breadcrumbs\": {\n                \"home\": \"Startseite\",\n                \"search\": \"Suchen\"\n            },\n            \"Empty\": {\n                \"title\": \"Leider gibt es keine Ergebnisse für „{term}“.\",\n                \"subtitle\": \"Bitte versuchen Sie es mit einer anderen Suchanfrage.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filter\",\n            \"resetFilters\": \"Filter zurücksetzen\",\n            \"Range\": {\n                \"apply\": \"Anwenden\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Kostenloser Versand\",\n                \"isFeaturedLabel\": \"Wird empfohlen\",\n                \"inStockLabel\": \"Auf Lager\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Sortieren nach:\",\n            \"featuredItems\": \"Empfohlene Artikel\",\n            \"bestSellingItems\": \"Meistverkaufte Artikel\",\n            \"newestItems\": \"Neueste Artikel\",\n            \"aToZ\": \"A bis Z\",\n            \"zToA\": \"Z bis A\",\n            \"byReview\": \"Nach Bewertung\",\n            \"priceAscending\": \"Preis: Aufsteigend\",\n            \"priceDescending\": \"Preis: Absteigend\",\n            \"relevance\": \"Relevanz\"\n        },\n        \"Compare\": {\n            \"compare\": \"Vergleichen\",\n            \"remove\": \"Entfernen\",\n            \"maxCompareLimit\": \"Sie haben die maximale Anzahl an Produkten zum Vergleich erreicht. Entfernen Sie ein Produkt, um ein neues hinzuzufügen.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Adressen\",\n            \"logout\": \"Abmelden\",\n            \"orders\": \"Bestellungen\",\n            \"settings\": \"Konto\",\n            \"wishlists\": \"Wunschlisten\"\n        },\n        \"Orders\": {\n            \"title\": \"Bestellungen\",\n            \"orderNumber\": \"Bestellnr.\",\n            \"totalPrice\": \"Summe\",\n            \"viewDetails\": \"Details anzeigen\",\n            \"EmptyState\": {\n                \"title\": \"Sie haben keine Bestellungen\",\n                \"cta\": \"Jetzt shoppen\"\n            },\n            \"Details\": {\n                \"title\": \"Bestellung #{orderNumber}\",\n                \"shippingAddress\": \"Versandadresse\",\n                \"shippingMethod\": \"Versandmethode\",\n                \"summaryTotal\": \"Summe\",\n                \"destination\": \"Zielort\",\n                \"destinationWithCount\": \"Zielort {Nummer, Nummer}/{Gesamt, Nummer}\",\n                \"digitalDelivery\": \"Digitale Lieferung an {email}\",\n                \"subtotal\": \"Zwischensumme\",\n                \"shipping\": \"Versand\",\n                \"tax\": \"Steuern\",\n                \"orderSummary\": \"Bestellübersicht\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Zahlungsmethode} other {Zahlungsmethoden}}\",\n                \"paymentEndingInLabel\": \"Endet auf\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Kreditkarte\",\n                    \"giftCertificate\": \"Geschenkgutschein\",\n                    \"storeCredit\": \"Shop-Guthaben\",\n                    \"other\": \"Sonstige\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Adressen\",\n            \"cta\": \"Adresse hinzufügen\",\n            \"edit\": \"Bearbeiten\",\n            \"delete\": \"Löschen\",\n            \"cancel\": \"Abbrechen\",\n            \"create\": \"erstellen\",\n            \"update\": \"Update\",\n            \"setDefault\": \"Als Standard festlegen\",\n            \"somethingWentWrong\": \"Es ist etwas schiefgegangen Bitte versuchen Sie es später erneut.\",\n            \"EmptyState\": {\n                \"title\": \"Sie haben keine Adressen\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Es muss ein Vorname angegeben werden\",\n                \"lastNameRequired\": \"Es muss ein Nachname angegeben werden\",\n                \"addressLine1Required\": \"Adresszeile 1 ist eine Pflichtangabe\",\n                \"cityRequired\": \"Ort ist eine Pflichtangabe\",\n                \"countryRequired\": \"Land ist eine Pflichtangabe\",\n                \"stateRequired\": \"Bundesstaat/Provinz ist eine Pflichtangabe\",\n                \"postalCodeRequired\": \"Postleitzahl ist erforderlich\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"Kontoeinstellungen\",\n            \"changePassword\": \"Passwort ändern\",\n            \"passwordUpdated\": \"Passwort wurde erfolgreich aktualisiert!\",\n            \"settingsUpdated\": \"Die Kontoeinstellungen wurden erfolgreich aktualisiert!\",\n            \"somethingWentWrong\": \"Es ist etwas schiefgegangen Bitte versuchen Sie es später erneut.\",\n            \"currentPassword\": \"Aktuelles Passwort\",\n            \"newPassword\": \"Neues Passwort\",\n            \"confirmPassword\": \"Passwort bestätigen\",\n            \"cta\": \"Update\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Marketingpräferenzen\",\n                \"label\": \"Abonnieren Sie unseren Newsletter.\",\n                \"marketingPreferencesUpdated\": \"Marketingeinstellungen wurden erfolgreich aktualisiert!\",\n                \"somethingWentWrong\": \"Es ist etwas schiefgegangen Bitte versuchen Sie es später erneut.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Es muss ein Vorname angegeben werden\",\n                \"firstNameTooSmall\": \"„Vorname“ muss mindestens 2 Zeichen lang sei\",\n                \"lastNameRequired\": \"Es muss ein Nachname angegeben werden\",\n                \"lastNameTooSmall\": \"„Nachname“ muss mindestens 2 Zeichen lang sein\",\n                \"emailRequired\": \"Die E-Mail-Adresse muss angegeben werden\",\n                \"emailInvalid\": \"Bitte geben Sie eine gültige E-Mail-Adresse an\",\n                \"currentPasswordRequired\": \"Aktuelles Passwort ist erforderlich\",\n                \"passwordRequired\": \"Es muss ein Passwort angegeben werden\",\n                \"passwordTooSmall\": \"Das Passwort muss mindestens {minLength, plural, =1 {1 Zeichen} other {# Zeichen}} lang sein\",\n                \"passwordLowercaseRequired\": \"Das Passwort muss mindestens einen Kleinbuchstaben enthalten\",\n                \"passwordUppercaseRequired\": \"Das Passwort muss mindestens einen Großbuchstaben enthalten\",\n                \"passwordNumberRequired\": \"Das Passwort muss mindestens {minNumbers, plural, =1 {eine Zahl} other {# Zahlen}} enthalten\",\n                \"passwordSpecialCharacterRequired\": \"Das Passwort muss mindestens ein Sonderzeichen enthalten\",\n                \"passwordsMustMatch\": \"Die Passwörter stimmen nicht überein\",\n                \"confirmPasswordRequired\": \"Bitte bestätigen Sie Ihr Passwort\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Wunschlistenaktionen\",\n        \"title\": \"Wunschlisten\",\n        \"new\": \"Neue Wunschliste\",\n        \"items\": \"{count, plural, =1 {1 Artikel} other {# Artikel}}\",\n        \"viewWishlist\": \"Liste anzeigen\",\n        \"noWishlists\": \"Sie haben keine Wunschlisten.\",\n        \"noWishlistsCallToAction\": \"Eine Wunschliste erstellen\",\n        \"emptyWishlist\": \"Sie haben keine Produkte zu dieser Wunschliste hinzugefügt.\",\n        \"share\": \"Freigeben\",\n        \"shareSuccess\": \"Die Wunschliste wurde erfolgreich geteilt.\",\n        \"shareCopied\": \"Die öffentliche URL der Wunschliste wurde in Ihre Zwischenablage kopiert.\",\n        \"shareDisabled\": \"Um Ihre Wunschliste teilen zu können, muss sie öffentlich sein.\",\n        \"makePublic\": \"Öffentlich machen\",\n        \"makePrivate\": \"Privat machen\",\n        \"rename\": \"Umbenennen\",\n        \"delete\": \"Löschen\",\n        \"removeButtonTitle\": \"Produkt von der Wunschliste entfernen\",\n        \"Visibility\": {\n            \"public\": \"Öffentlich\",\n            \"private\": \"Privat\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Abbrechen\",\n            \"close\": \"Schließen\",\n            \"copy\": \"Kopieren\",\n            \"create\": \"erstellen\",\n            \"save\": \"Speichern\",\n            \"delete\": \"Löschen\",\n            \"newTitle\": \"Eine neue Wunschliste erstellen\",\n            \"shareTitle\": \"Teilen Sie {name}\",\n            \"renameTitle\": \"{name} umbenennen\",\n            \"deleteTitle\": \"{name} löschen\",\n            \"changeVisibilityPublicTitle\": \"Möchten Sie {name} öffentlich machen?\",\n            \"changeVisibilityPrivateTitle\": \"Soll {name} auf privat gesetzt werden?\",\n            \"makePublicContent\": \"Möchten Sie <bold>{name}</bold> wirklich auf öffentlich setzen? Dadurch können andere Ihre Wunschliste sehen, wenn sie den Link haben.\",\n            \"makePrivateContent\": \"Möchten Sie <bold>{name}</bold> wirklich auf privat setzen? Wenn Sie Ihre Wunschliste mit anderen geteilt haben, können diese sie nicht mehr sehen.\",\n            \"deleteContent\": \"Möchten Sie <bold>{name}</bold> wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Name\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"Der Wunschlistenname darf nicht leer sein.\",\n            \"updateFailed\": \"Die Aktualisierung Ihrer Wunschliste ist fehlgeschlagen. Bitte versuchen Sie es erneut.\",\n            \"deleteFailed\": \"Das Löschen Ihrer Wunschliste konnte nicht durchgeführt werden. Bitte versuchen Sie es erneut.\",\n            \"removeProductFailed\": \"Das Produkt konnte nicht von Ihrer Wunschliste entfernt werden. Bitte versuchen Sie es erneut.\",\n            \"unauthorized\": \"Sie sind nicht berechtigt, diese Aktion auszuführen, bitte melden Sie sich an und versuchen Sie es erneut.\",\n            \"unexpected\": \"Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"Die Wunschliste wurde erfolgreich erstellt.\",\n            \"updateSuccess\": \"Die Wunschliste wurde erfolgreich aktualisiert.\",\n            \"deleteSuccess\": \"Die Wunschliste wurde erfolgreich gelöscht.\",\n            \"removeItemSuccess\": \"Der Artikel wurde von Ihrer Wunschliste entfernt.\"\n        },\n        \"Button\": {\n            \"label\": \"Zur Wunschliste hinzufügen\",\n            \"addToNewWishlist\": \"Zu neuer Wunschliste hinzufügen\",\n            \"defaultWishlistName\": \"Meine Wunschliste\",\n            \"addSuccessMessage\": \"Das Produkt wurde Ihrer Wunschliste hinzugefügt.\",\n            \"removeSuccessMessage\": \"Das Produkt wurde von Ihrer Wunschliste entfernt\",\n            \"Errors\": {\n                \"addProductFailed\": \"Das Produkt konnte nicht zu Ihrer Wunschliste hinzugefügt werden. Bitte versuchen Sie es erneut.\",\n                \"removeProductFailed\": \"Das Produkt konnte nicht von Ihrer Wunschliste entfernt werden, bitte versuchen Sie es erneut.\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Öffentliche Wunschliste\",\n        \"defaultName\": \"Öffentliche Wunschliste\",\n        \"emptyWishlist\": \"Diese Wunschliste enthält noch keine Produkte.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Startseite\",\n        \"Empty\": {\n            \"title\": \"Keine Blog-Beiträge gefunden\",\n            \"subtitle\": \"Kommen Sie später wieder, um mehr Inhalte zu sehen.\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Freigeben\",\n            \"email\": \"E-Mail\",\n            \"print\": \"Drucken\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Warenkorb\",\n        \"heading\": \"Ihr Warenkorb\",\n        \"proceedToCheckout\": \"Weiter zur Kasse\",\n        \"increment\": \"Menge erhöhen\",\n        \"decrement\": \"Menge verringern\",\n        \"removeItem\": \"Artikel entfernen\",\n        \"cartCombined\": \"Wir haben festgestellt, dass Sie Artikel in einem früheren Warenkorb gespeichert hatten, also haben wir diese Ihrem aktuellen Warenkorb hinzugefügt.\",\n        \"cartRestored\": \"Sie haben einen Warenkorb auf einem anderen Gerät angelegt, und wir haben ihn hier wiederhergestellt, damit Sie dort weitermachen können, wo Sie aufgehört haben.\",\n        \"cartUpdateInProgress\": \"Sie führen derzeit eine Aktualisierung Ihres Warenkorbs durch. Sind Sie sicher, dass Sie diese Seite verlassen möchten? Ihre Änderungen könnten verloren gehen.\",\n        \"originalPrice\": \"Der ursprüngliche Preis war {price}.\",\n        \"currentPrice\": \"Der aktuelle Preis beträgt {price}.\",\n        \"quantityReadyToShip\": \"{quantity, number} bereit zum Versand\",\n        \"quantityOnBackorder\": \"{quantity, number} wird nachbestellt\",\n        \"partiallyAvailable\": \"Nur {quantity, number} verfügbar\",\n        \"CheckoutSummary\": {\n            \"title\": \"Übersicht\",\n            \"subTotal\": \"Zwischensumme\",\n            \"discounts\": \"Rabatte\",\n            \"tax\": \"Steuern\",\n            \"total\": \"Summe\",\n            \"CouponCode\": {\n                \"apply\": \"Anwenden\",\n                \"couponCode\": \"Gutschein-Code\",\n                \"removeCouponCode\": \"Gutschein-Code entfernen\",\n                \"invalidCouponCode\": \"Bitte geben Sie einen gültigen Coupon-Code ein\",\n                \"cartNotFound\": \"Beim Abrufen des Warenkorbs ist ein Fehler aufgetreten\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Versand\",\n                \"add\": \"Hinzufügen\",\n                \"change\": \"Ändern\",\n                \"cancel\": \"Abbrechen\",\n                \"country\": \"Land\",\n                \"city\": \"Ort\",\n                \"state\": \"Bundesstaat/Provinz\",\n                \"postalCode\": \"Postleitzahl\",\n                \"updatedShippingOptions\": \"Versandoptionen aktualisieren\",\n                \"viewShippingOptions\": \"Versandoptionen anzeigen\",\n                \"editAddress\": \"Adresse bearbeiten\",\n                \"shippingOptions\": \"Versandoptionen\",\n                \"updateShipping\": \"Versand aktualisieren\",\n                \"addShipping\": \"Versand hinzufügen\",\n                \"cartNotFound\": \"Beim Abrufen des Warenkorbs ist ein Fehler aufgetreten\",\n                \"noShippingOptions\": \"Für Ihre Adresse sind keine Versandoptionen verfügbar\",\n                \"countryRequired\": \"Land ist eine Pflichtangabe\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Geschenkgutschein\",\n            \"giftCertificateCode\": \"Geschenkgutscheincode\",\n            \"removeGiftCertificate\": \"Geschenkgutschein entfernen\",\n            \"apply\": \"Anwenden\",\n            \"to\": \"Nach\",\n            \"message\": \"Nachricht\",\n            \"invalidGiftCertificate\": \"Bitte geben Sie einen gültigen Geschenkgutscheincode ein.\",\n            \"cartNotFound\": \"Beim Abrufen des Warenkorbs ist ein Fehler aufgetreten\"\n        },\n        \"Empty\": {\n            \"title\": \"Ihr Warenkorb ist leer.\",\n            \"subtitle\": \"Fügen Sie einige Produkte hinzu, um zu beginnen.\",\n            \"cta\": \"Weiter einkaufen\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Beim Abrufen des Warenkorbs ist ein Fehler aufgetreten\",\n            \"lineItemNotFound\": \"Einzelposten nicht gefunden.\",\n            \"failedToUpdateQuantity\": \"Menge konnte nicht aktualisiert werden.\",\n            \"somethingWentWrong\": \"Es ist etwas schiefgegangen Bitte versuchen Sie es später erneut.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Produkte vergleichen\",\n        \"addToCart\": \"Zum Warenkorb hinzufügen\",\n        \"next\": \"Nächste Produkte\",\n        \"previous\": \"Vorherige Produkte\",\n        \"noProductsToCompare\": \"Keine Produkte zum Vergleichen\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Gewicht\",\n        \"description\": \"Beschreibung\",\n        \"noDescription\": \"Es ist keine Beschreibung verfügbar.\",\n        \"rating\": \"Bewertung\",\n        \"noRatings\": \"Es gibt keine Bewertungen.\",\n        \"otherDetails\": \"Weitere Angaben\",\n        \"noOtherDetails\": \"Es gibt keine weiteren Details.\",\n        \"viewOptions\": \"Optionen anzeigen\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 Item} other {# Items}} wurde zu<cartLink> Ihrem Warenkorb</cartLink> hinzugefügt\",\n        \"missingCart\": \"Warenkorb nicht gefunden. Bitte versuchen Sie es später erneut.\",\n        \"unknownError\": \"Unbekannter Fehler. Bitte versuchen Sie es später erneut.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Menge\",\n            \"increaseQuantity\": \"Menge erhöhen\",\n            \"decreaseQuantity\": \"Menge verringern\",\n            \"emptySelectPlaceholder\": \"Eine Option auswählen\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 Item} other {# Items}} wurde zu<cartLink> Ihrem Warenkorb</cartLink> hinzugefügt\",\n            \"missingCart\": \"Warenkorb nicht gefunden. Bitte versuchen Sie es später erneut!\",\n            \"unknownError\": \"Unbekannter Fehler. Bitte versuchen Sie es später erneut.\",\n            \"variantRequiredError\": \"Für dieses Produkt müssen Optionen ausgewählt werden, um es in den Warenkorb zu legen.\",\n            \"increaseNumber\": \"Anzahl erhöhen\",\n            \"decreaseNumber\": \"Anzahl verringern\",\n            \"thumbnail\": \"Bildnummer anzeigen\",\n            \"additionalInformation\": \"Weitere Informationen\",\n            \"currentStock\": \"{quantity, number} auf Lager\",\n            \"backorderQuantity\": \"{quantity, number} wird nachbestellt\",\n            \"loadingMoreImages\": \"Weitere Bilder werden geladen\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 weiteres Bild geladen, } other {# weitere Bilder geladen, }}\",\n            \"Submit\": {\n                \"addToCart\": \"Zum Warenkorb hinzufügen\",\n                \"outOfStock\": \"Kein Lagerbestand\",\n                \"preorder\": \"Vorbestellung\",\n                \"unavailable\": \"Nicht verfügbar\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Produktdetails\",\n                \"warranty\": \"Garantie\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Gewicht\",\n                \"condition\": \"Zustand\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Verwandte Produkte\",\n            \"noRelatedProducts\": \"Keine zugehörigen Produkte gefunden\",\n            \"browseCatalog\": \"Entdecken Sie unseren kompletten Produktkatalog.\",\n            \"cta\": \"Alles shoppen\",\n            \"previousProducts\": \"Vorherige Produkte\",\n            \"nextProducts\": \"Nächste Produkte\",\n            \"scrollbar\": \"Bildlaufleiste für verwandte Produkte\"\n        },\n        \"Reviews\": {\n            \"title\": \"Bewertungen\",\n            \"empty\": \"Für dieses Produkt wurden keine Bewertungen hinzugefügt.\",\n            \"previous\": \"Vorherige Bewertungen\",\n            \"next\": \"Nächste Bewertungen\",\n            \"Form\": {\n                \"button\": \"Eine Bewertung schreiben\",\n                \"title\": \"Eine Bewertung schreiben\",\n                \"submit\": \"Einreichen\",\n                \"cancel\": \"Abbrechen\",\n                \"ratingLabel\": \"Bewertung\",\n                \"titleLabel\": \"Titel\",\n                \"reviewLabel\": \"Bewertung\",\n                \"nameLabel\": \"Name\",\n                \"emailLabel\": \"E-Mail\",\n                \"successMessage\": \"Ihre Bewertung wurde erfolgreich übermittelt!\",\n                \"somethingWentWrong\": \"Es ist etwas schiefgegangen Bitte versuchen Sie es später erneut.\",\n                \"recaptchaRequired\": \"Bitte führen Sie die reCAPTCHA-Verifizierung durch.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Titel ist erforderlich\",\n                    \"authorRequired\": \"Es muss ein Name angegeben werden\",\n                    \"emailRequired\": \"Die E-Mail-Adresse muss angegeben werden\",\n                    \"emailInvalid\": \"Bitte geben Sie eine gültige E-Mail-Adresse an\",\n                    \"textRequired\": \"Überprüfung ist erforderlich\",\n                    \"ratingRequired\": \"Bewertung ist erforderlich.\",\n                    \"ratingTooSmall\": \"Die Bewertung muss mindestens 1 betragen\",\n                    \"ratingTooLarge\": \"Die Bewertung darf höchstens 5 betragen\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Startseite\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Startseite\",\n            \"Form\": {\n                \"success\": \"Vielen Dank für Ihre Kontaktaufnahme. Wir melden uns bald bei Ihnen.\",\n                \"successCta\": \"Weiter einkaufen\",\n                \"fullName\": \"Vollständiger Name\",\n                \"companyName\": \"Firmenname\",\n                \"phone\": \"Telefon\",\n                \"orderNo\": \"Bestellnummer\",\n                \"rma\": \"RMA-Nummer\",\n                \"email\": \"E-Mail\",\n                \"comments\": \"Kommentare/Fragen\",\n                \"cta\": \"Formular einreichen\",\n                \"somethingWentWrong\": \"Es ist etwas schiefgegangen Bitte versuchen Sie es später erneut.\",\n                \"recaptchaRequired\": \"Bitte führen Sie die reCAPTCHA-Verifizierung durch.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"WARTUNG\",\n        \"message\": \"Wir sind wegen Wartungsarbeiten nicht erreichbar\",\n        \"contactUs\": \"Sie können uns kontaktieren unter:\"\n    },\n    \"Error\": {\n        \"title\": \"Es ist ein Serverfehler aufgetreten!\",\n        \"subtitle\": \"Bitte versuchen Sie es später erneut.\",\n        \"cta\": \"Erneut versuchen\"\n    },\n    \"NotFound\": {\n        \"title\": \"Wir konnten diese Seite nicht finden!\",\n        \"subtitle\": \"Versuchen Sie, nach etwas anderem zu suchen oder zur Startseite zurückzukehren.\",\n        \"featuredProducts\": \"Empfohlene Produkte\",\n        \"search\": \"Suchen\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Startseite\",\n            \"toggleNavigation\": \"Navigation ein-/ausschalten\",\n            \"Icons\": {\n                \"account\": \"Profil\",\n                \"cart\": \"Warenkorb\",\n                \"search\": \"Popup-Fenster zur Suche öffnen\",\n                \"giftCertificates\": \"Geschenkgutscheine\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Währung ändern\",\n                \"invalidCurrency\": \"Ungültige Währung\",\n                \"errorUpdatingCurrency\": \"Fehler beim Aktualisieren der Währung für Ihren Warenkorb. Bitte versuchen Sie es erneut.\"\n            },\n            \"Search\": {\n                \"products\": \"Produkte\",\n                \"categories\": \"Kategorien\",\n                \"brands\": \"Marken\",\n                \"noSearchResultsTitle\": \"Leider gibt es keine Ergebnisse für „{term}“.\",\n                \"noSearchResultsSubtitle\": \"Bitte versuchen Sie es mit einer anderen Suchanfrage.\",\n                \"somethingWentWrong\": \"Es ist etwas schiefgegangen. Bitte versuchen Sie es erneut.\",\n                \"inputPlaceholder\": \"Produkte, Kategorien, Marken suchen ...\",\n                \"submitLabel\": \"Suchen\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Startseite\",\n            \"contactUs\": \"Kontakt\",\n            \"socialMediaLinks\": \"Social-Media-Links\",\n            \"categories\": \"Kategorien\",\n            \"brands\": \"Marken\",\n            \"navigate\": \"Navigieren\",\n            \"giftCertificates\": \"Geschenkgutscheine\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Melden Sie sich für unseren Newsletter an\",\n            \"placeholder\": \"Geben Sie Ihre E-Mail-Adresse ein\",\n            \"description\": \"Bleiben Sie auf dem Laufenden über die aktuellen Neuigkeiten und Angebote in unserem Shop.\",\n            \"subscribedToNewsletter\": \"Sie haben unseren Newsletter abonniert!\",\n            \"Errors\": {\n                \"emailRequired\": \"Die E-Mail-Adresse muss angegeben werden\",\n                \"invalidEmail\": \"Bitte geben Sie eine gültige E-Mail-Adresse an\",\n                \"somethingWentWrong\": \"Es ist etwas schiefgegangen Bitte versuchen Sie es später erneut.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Alle ablehnen\",\n                \"acceptAll\": \"Alle akzeptieren\",\n                \"customize\": \"Benutzerdefiniert anpassen\",\n                \"save\": \"Einstellungen speichern\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"Wir respektieren Ihre Privatsphäre.\",\n                \"description\": \"Diese Website verwendet Cookies, um Ihr Surferlebnis zu verbessern, den Website-Traffic zu analysieren und personalisierte Inhalte anzuzeigen.\",\n                \"privacyPolicy\": \"Datenschutzerklärung\"\n            },\n            \"Dialog\": {\n                \"title\": \"Datenschutzeinstellungen\",\n                \"description\": \"Passen Sie hier Ihre Datenschutzeinstellungen an. Sie können auswählen, welche Arten von Cookies und Tracking-Technologien Sie zulassen möchten.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Unbedingt notwendig\",\n                    \"description\": \"Diese Cookies sind für die ordnungsgemäße Funktion der Website unbedingt erforderlich und können nicht deaktiviert werden.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Funktionalität\",\n                    \"description\": \"Diese Cookies ermöglichen erweiterte Funktionalität und Personalisierung der Website.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"Diese Cookies werden verwendet, um relevante Werbung zu liefern und deren Wirksamkeit zu verfolgen.\"\n                },\n                \"measurement\": {\n                    \"title\": \"Analysen\",\n                    \"description\": \"Diese Cookies helfen uns zu verstehen, wie Besucher mit der Website interagieren und ihre Leistung zu verbessern.\"\n                },\n                \"experience\": {\n                    \"title\": \"Erlebnis\",\n                    \"description\": \"Diese Cookies helfen uns, eine bessere Benutzererfahrung zu bieten und neue Funktionen zu testen.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"Der ursprüngliche Preis war {price}.\",\n            \"currentPrice\": \"Der aktuelle Preis beträgt {price}.\",\n            \"range\": \"Preis von {minValue} bis {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Geschenkgutscheine\",\n        \"description\": \"Verschenken Sie das perfekte Geschenk, das nie aus der Mode kommt. Lassen Sie Freunde und Bekannte aus unserer gesamten Kollektion genau das auswählen, was sie wollen.\",\n        \"purchaseLabel\": \"Jetzt shoppen\",\n        \"checkBalanceLabel\": \"Guthaben abfragen\",\n        \"expiresAtLabel\": \"Gültig bis\",\n        \"CheckBalance\": {\n            \"title\": \"Guthaben abfragen\",\n            \"description\": \"Sie können das Guthaben überprüfen und Informationen zu Ihrem Geschenkgutschein erhalten, indem Sie den Code in das Feld unten eingeben.\",\n            \"inputLabel\": \"Code\",\n            \"inputPlaceholder\": \"XXX-XXX-XXX-XXX\",\n            \"purchasedDateLabel\": \"Gekauft\",\n            \"senderLabel\": \"Von\",\n            \"Errors\": {\n                \"invalidCode\": \"Der von Ihnen eingegebene Geschenkgutscheincode ist ungültig. Bitte überprüfen Sie den Code und versuchen Sie es erneut.\",\n                \"codeRequired\": \"Bitte geben Sie einen Geschenkgutscheincode ein.\",\n                \"somethingWentWrong\": \"Es ist etwas schiefgegangen Bitte versuchen Sie es später erneut.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Einen Geschenkgutschein kaufen\",\n            \"title\": \"Digitaler Geschenkgutschein\",\n            \"description\": \"Entdecken Sie unsere Geschenkgutscheine – perfekt für jeden Anlass. Wählen Sie den Betrag und personalisieren Sie Ihre Nachricht.\",\n            \"successMessage\": \"Der Geschenkgutschein wurde <cartLink> Ihrem Warenkorb</cartLink> hinzugefügt.\",\n            \"missingCart\": \"Warenkorb nicht gefunden. Bitte versuchen Sie es später erneut.\",\n            \"unknownError\": \"Unbekannter Fehler. Bitte versuchen Sie es später erneut.\",\n            \"Form\": {\n                \"amountLabel\": \"Betrag\",\n                \"customAmountLabel\": \"Betrag (zwischen {minAmount} und {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Wählen Sie einen Betrag aus\",\n                \"customAmountPlaceholder\": \"Geben Sie einen Benutzer: innen definierten Betrag ein\",\n                \"senderNameLabel\": \"Ihr Name\",\n                \"senderEmailLabel\": \"Ihre E-Mail-Adresse\",\n                \"recipientNameLabel\": \"Name des Empfängers\",\n                \"recipientEmailLabel\": \"E-Mail-Adresse des Empfängers\",\n                \"namePlaceholder\": \"Geben Sie den Namen ein\",\n                \"emailPlaceholder\": \"E-Mail-Adresse eingeben\",\n                \"messageLabel\": \"Nachricht\",\n                \"messagePlaceholder\": \"Geben Sie Ihre Nachricht ein (optional)\",\n                \"nonRefundableCheckboxLabel\": \"Ich erkläre mich damit einverstanden, dass Geschenkgutscheine nicht erstattungsfähig sind\",\n                \"expiryCheckboxLabel\": \"Ich bestätige, dass dieser Geschenkgutschein am {expiryDate} abläuft.\",\n                \"ctaLabel\": \"Zum Warenkorb hinzufügen\",\n                \"Errors\": {\n                    \"amountRequired\": \"Bitte wählen Sie einen Geschenkgutscheinbetrag aus oder geben Sie ihn ein\",\n                    \"amountInvalid\": \"Bitte wählen Sie einen gültigen Betrag für diesen Geschenkgutschein aus\",\n                    \"amountOutOfRange\": \"Bitte geben Sie einen Betrag zwischen {minAmount} und {maxAmount} ein\",\n                    \"unexpectedSettingsError\": \"Beim Abrufen der Geschenkgutscheineinstellungen ist ein unerwarteter Fehler aufgetreten. Bitte versuchen Sie es später erneut.\",\n                    \"senderNameRequired\": \"Ihr Name ist erforderlich\",\n                    \"senderEmailRequired\": \"Ihre E-Mail-Adresse ist erforderlich\",\n                    \"recipientNameRequired\": \"Der Name des Empfängers ist erforderlich\",\n                    \"recipientEmailRequired\": \"Die E-Mail-Adresse des Empfängers ist erforderlich\",\n                    \"emailInvalid\": \"Bitte geben Sie eine gültige E-Mail-Adresse an\",\n                    \"checkboxRequired\": \"Sie müssen dieses Feld ankreuzen, um fortzufahren\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"optional\",\n        \"recaptchaRequired\": \"Bitte führen Sie die reCAPTCHA-Verifizierung durch.\",\n        \"Errors\": {\n            \"invalidInput\": \"Bitte überprüfen Sie Ihre Eingabe und versuchen Sie es erneut\",\n            \"invalidFormat\": \"Der eingegebene Wert entspricht nicht dem erforderlichen Format\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/en.json",
    "content": "{\n  \"Home\": {\n    \"Slideshow\": {\n      \"Slide01\": {\n        \"title\": \"Fresh finds for every occasion\",\n        \"description\": \"Explore our latest arrivals, curated to bring you style, functionality, and inspiration. Shop now and discover your next favorite.\",\n        \"alt\": \"Five small potted plants displayed on beige stacked blocks, featuring a variety of green foliage in dark gray pots against a neutral background.\",\n        \"cta\": \"Shop Now\"\n      },\n      \"Slide02\": {\n        \"title\": \"Discover what's new\",\n        \"description\": \"Shop our latest arrivals and find something fresh and exciting for your home.\",\n        \"alt\": \"Hands reaching out to hold a green fern in a woven basket with a decorative bow, against a beige background with soft shadows.\",\n        \"cta\": \"Shop Now\"\n      },\n      \"Slide03\": {\n        \"title\": \"Something for everyone\",\n        \"description\": \"Don’t miss out on exclusive offers across our best-selling products. Shop today and save big on the items you love.\",\n        \"alt\": \"Close-up of a vibrant green leaf with perforations, showcasing its smooth texture and natural details.\",\n        \"cta\": \"Shop Now\"\n      }\n    },\n    \"FeaturedProducts\": {\n      \"title\": \"Featured collection\",\n      \"description\": \"Explore our top picks in this featured collection. Find the perfect gift or treat yourself!\",\n      \"cta\": \"View more\",\n      \"emptyStateTitle\": \"No products found\",\n      \"emptyStateSubtitle\": \"Try browsing our complete catalog of products.\"\n    },\n    \"NewestProducts\": {\n      \"title\": \"New arrivals\",\n      \"description\": \"Our latest products are here. Check out what's new in store.\",\n      \"cta\": \"See all\",\n      \"emptyStateTitle\": \"No products found\",\n      \"emptyStateSubtitle\": \"Try browsing our complete catalog of products.\",\n      \"previousProducts\": \"Previous products\",\n      \"nextProducts\": \"Next products\"\n    }\n  },\n  \"Auth\": {\n    \"ChangePassword\": {\n      \"title\": \"Change password\",\n      \"newPassword\": \"New password\",\n      \"confirmPassword\": \"Confirm password\",\n      \"passwordUpdated\": \"Password has been updated successfully!\",\n      \"somethingWentWrong\": \"Something went wrong. Please try again later.\",\n      \"FieldErrors\": {\n        \"passwordRequired\": \"Password is required\",\n        \"passwordTooSmall\": \"Password must be at least {minLength, plural, =1 {1 character} other {# characters}} long\",\n        \"passwordLowercaseRequired\": \"Password must contain at least one lowercase letter\",\n        \"passwordUppercaseRequired\": \"Password must contain at least one uppercase letter\",\n        \"passwordNumberRequired\": \"Password must contain at least {minNumbers, plural, =1 {one number} other {# numbers}}\",\n        \"passwordSpecialCharacterRequired\": \"Password must contain at least one special character\",\n        \"passwordsMustMatch\": \"The passwords do not match\",\n        \"confirmPasswordRequired\": \"Please confirm your password\"\n      }\n    },\n    \"Login\": {\n      \"title\": \"Login\",\n      \"heading\": \"Log in\",\n      \"forgotPassword\": \"Forgot your password?\",\n      \"cta\": \"Log in\",\n      \"email\": \"Email\",\n      \"password\": \"Password\",\n      \"invalidCredentials\": \"Your email address or password is incorrect. Try signing in again or reset your password\",\n      \"somethingWentWrong\": \"Something went wrong. Please try again later.\",\n      \"passwordResetRequired\": \"Password reset required. Please check your email for instructions to reset your password.\",\n      \"invalidToken\": \"Your login link is invalid or has expired. Please try logging in again.\",\n      \"FieldErrors\": {\n        \"emailRequired\": \"Email is required\",\n        \"emailInvalid\": \"Please enter a valid email address\",\n        \"passwordRequired\": \"Password is required\",\n        \"invalidInput\": \"Please check your input and try again.\"\n      },\n      \"CreateAccount\": {\n        \"title\": \"New customer?\",\n        \"accountBenefits\": \"Create an account with us and you'll be able to:\",\n        \"fastCheckout\": \"Check out faster\",\n        \"multipleAddresses\": \"Save multiple shipping addresses\",\n        \"ordersHistory\": \"Access your order history\",\n        \"ordersTracking\": \"Track new orders\",\n        \"wishlists\": \"Save items to your Wishlist\",\n        \"cta\": \"Create account\"\n      },\n      \"ForgotPassword\": {\n        \"title\": \"Forgot password\",\n        \"subtitle\": \"Enter the email associated with your account below. We'll send you instructions to reset your password.\",\n        \"confirmResetPassword\": \"If the email address {email} is linked to an account in our store, we have sent you a password reset email. Please check your inbox and spam folder if you don't see it.\",\n        \"somethingWentWrong\": \"Something went wrong. Please try again later.\",\n        \"FieldErrors\": {\n          \"emailRequired\": \"Email is required\",\n          \"emailInvalid\": \"Please enter a valid email address\"\n        }\n      }\n    },\n    \"Register\": {\n      \"title\": \"Register account\",\n      \"heading\": \"New account\",\n      \"cta\": \"Create account\",\n      \"somethingWentWrong\": \"Something went wrong. Please try again later.\",\n      \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n      \"FieldErrors\": {\n        \"firstNameRequired\": \"First name is required\",\n        \"lastNameRequired\": \"Last name is required\",\n        \"emailRequired\": \"Email is required\",\n        \"emailInvalid\": \"Please enter a valid email address\",\n        \"passwordRequired\": \"Password is required\",\n        \"passwordTooSmall\": \"Password must be at least {minLength, plural, =1 {1 character} other {# characters}} long\",\n        \"passwordLowercaseRequired\": \"Password must contain at least one lowercase letter\",\n        \"passwordUppercaseRequired\": \"Password must contain at least one uppercase letter\",\n        \"passwordNumberRequired\": \"Password must contain at least {minNumbers, plural, =1 {one number} other {# numbers}}\",\n        \"passwordSpecialCharacterRequired\": \"Password must contain at least one special character\",\n        \"passwordsMustMatch\": \"The passwords do not match\",\n        \"addressLine1Required\": \"Address line 1 is required\",\n        \"cityRequired\": \"City is required\",\n        \"countryRequired\": \"Country is required\",\n        \"stateRequired\": \"State/Province is required\",\n        \"postalCodeRequired\": \"Postal code is required\"\n      }\n    }\n  },\n  \"Faceted\": {\n    \"Brand\": {\n      \"Empty\": {\n        \"title\": \"No products in this brand\",\n        \"subtitle\": \"Try using different filters.\"\n      }\n    },\n    \"Category\": {\n      \"subCategories\": \"Categories\",\n      \"Empty\": {\n        \"title\": \"No products in this category\",\n        \"subtitle\": \"Try using different filters.\"\n      }\n    },\n    \"Search\": {\n      \"title\": \"Search results\",\n      \"searchResults\": \"Search results for\",\n      \"subCategories\": \"Categories\",\n      \"Breadcrumbs\": {\n        \"home\": \"Home\",\n        \"search\": \"Search\"\n      },\n      \"Empty\": {\n        \"title\": \"Sorry, no results for \\\"{term}\\\".\",\n        \"subtitle\": \"Please try another search.\"\n      }\n    },\n    \"FacetedSearch\": {\n      \"filters\": \"Filters\",\n      \"resetFilters\": \"Reset filters\",\n      \"Range\": {\n        \"apply\": \"Apply\"\n      },\n      \"Facets\": {\n        \"freeShippingLabel\": \"Free shipping\",\n        \"isFeaturedLabel\": \"Is featured\",\n        \"inStockLabel\": \"In stock\"\n      }\n    },\n    \"SortBy\": {\n      \"sortBy\": \"Sort by:\",\n      \"featuredItems\": \"Featured items\",\n      \"bestSellingItems\": \"Best selling items\",\n      \"newestItems\": \"Newest items\",\n      \"aToZ\": \"A to Z\",\n      \"zToA\": \"Z to A\",\n      \"byReview\": \"By review\",\n      \"priceAscending\": \"Price: ascending\",\n      \"priceDescending\": \"Price: descending\",\n      \"relevance\": \"Relevance\"\n    },\n    \"Compare\": {\n      \"compare\": \"Compare\",\n      \"remove\": \"Remove\",\n      \"maxCompareLimit\": \"You've reached the maximum number of products for comparison. Remove a product to add a new one.\"\n    }\n  },\n  \"Account\": {\n    \"Layout\": {\n      \"addresses\": \"Addresses\",\n      \"logout\": \"Logout\",\n      \"orders\": \"Orders\",\n      \"settings\": \"Account\",\n      \"wishlists\": \"Wish lists\"\n    },\n    \"Orders\": {\n      \"title\": \"Orders\",\n      \"orderNumber\": \"Order #\",\n      \"totalPrice\": \"Total\",\n      \"viewDetails\": \"View Details\",\n      \"EmptyState\": {\n        \"title\": \"You don't have any orders\",\n        \"cta\": \"Shop now\"\n      },\n      \"Details\": {\n        \"title\": \"Order #{orderNumber}\",\n        \"shippingAddress\": \"Shipping address\",\n        \"shippingMethod\": \"Shipping method\",\n        \"summaryTotal\": \"Total\",\n        \"destination\": \"Destination\",\n        \"destinationWithCount\": \"Destination {number, number}/{total, number}\",\n        \"digitalDelivery\": \"Digital delivery to {email}\",\n        \"subtotal\": \"Subtotal\",\n        \"shipping\": \"Shipping\",\n        \"tax\": \"Tax\",\n        \"orderSummary\": \"Order summary\",\n        \"paymentMethodsLabel\": \"{count, plural, =1 {Payment method} other {Payment methods}}\",\n        \"paymentEndingInLabel\": \"ending in\",\n        \"PaymentMethods\": {\n          \"creditCard\": \"Credit card\",\n          \"giftCertificate\": \"Gift certificate\",\n          \"storeCredit\": \"Store credit\",\n          \"other\": \"Other\"\n        }\n      }\n    },\n    \"Addresses\": {\n      \"title\": \"Addresses\",\n      \"cta\": \"Add address\",\n      \"edit\": \"Edit\",\n      \"delete\": \"Delete\",\n      \"cancel\": \"Cancel\",\n      \"create\": \"Create\",\n      \"update\": \"Update\",\n      \"setDefault\": \"Set as default\",\n      \"somethingWentWrong\": \"Something went wrong. Please try again later.\",\n      \"EmptyState\": {\n        \"title\": \"You don't have any addresses\"\n      },\n      \"FieldErrors\": {\n        \"firstNameRequired\": \"First name is required\",\n        \"lastNameRequired\": \"Last name is required\",\n        \"addressLine1Required\": \"Address line 1 is required\",\n        \"cityRequired\": \"City is required\",\n        \"countryRequired\": \"Country is required\",\n        \"stateRequired\": \"State/Province is required\",\n        \"postalCodeRequired\": \"Postal code is required\"\n      }\n    },\n    \"Settings\": {\n      \"title\": \"Account settings\",\n      \"changePassword\": \"Change password\",\n      \"passwordUpdated\": \"Password has been updated successfully!\",\n      \"settingsUpdated\": \"Account settings have been updated successfully!\",\n      \"somethingWentWrong\": \"Something went wrong. Please try again later.\",\n      \"currentPassword\": \"Current password\",\n      \"newPassword\": \"New password\",\n      \"confirmPassword\": \"Confirm password\",\n      \"cta\": \"Update\",\n      \"NewsletterSubscription\": {\n        \"title\": \"Marketing preferences\",\n        \"label\": \"Subscribe to our newsletter.\",\n        \"marketingPreferencesUpdated\": \"Marketing preferences have been updated successfully!\",\n        \"somethingWentWrong\": \"Something went wrong. Please try again later.\"\n      },\n      \"FieldErrors\": {\n        \"firstNameRequired\": \"First name is required\",\n        \"firstNameTooSmall\": \"First name must be at least 2 characters long\",\n        \"lastNameRequired\": \"Last name is required\",\n        \"lastNameTooSmall\": \"Last name must be at least 2 characters long\",\n        \"emailRequired\": \"Email is required\",\n        \"emailInvalid\": \"Please enter a valid email address\",\n        \"currentPasswordRequired\": \"Current password is required\",\n        \"passwordRequired\": \"Password is required\",\n        \"passwordTooSmall\": \"Password must be at least {minLength, plural, =1 {1 character} other {# characters}} long\",\n        \"passwordLowercaseRequired\": \"Password must contain at least one lowercase letter\",\n        \"passwordUppercaseRequired\": \"Password must contain at least one uppercase letter\",\n        \"passwordNumberRequired\": \"Password must contain at least {minNumbers, plural, =1 {one number} other {# numbers}}\",\n        \"passwordSpecialCharacterRequired\": \"Password must contain at least one special character\",\n        \"passwordsMustMatch\": \"The passwords do not match\",\n        \"confirmPasswordRequired\": \"Please confirm your password\"\n      }\n    }\n  },\n  \"Wishlist\": {\n    \"actionsTitle\": \"Wishlist actions\",\n    \"title\": \"Wish Lists\",\n    \"new\": \"New wish list\",\n    \"items\": \"{count, plural, =1 {1 item} other {# items}}\",\n    \"viewWishlist\": \"View list\",\n    \"noWishlists\": \"You don't have any wish lists\",\n    \"noWishlistsCallToAction\": \"Create a wishlist\",\n    \"emptyWishlist\": \"You haven't added products to this wish list.\",\n    \"share\": \"Share\",\n    \"shareSuccess\": \"Wish list has been shared successfully.\",\n    \"shareCopied\": \"Wish list public URL has been copied to your clipboard.\",\n    \"shareDisabled\": \"Your wish list must be public in order to share it.\",\n    \"makePublic\": \"Make public\",\n    \"makePrivate\": \"Make private\",\n    \"rename\": \"Rename\",\n    \"delete\": \"Delete\",\n    \"removeButtonTitle\": \"Remove product from wish list\",\n    \"Visibility\": {\n      \"public\": \"Public\",\n      \"private\": \"Private\"\n    },\n    \"Modal\": {\n      \"cancel\": \"Cancel\",\n      \"close\": \"Close\",\n      \"copy\": \"Copy\",\n      \"create\": \"Create\",\n      \"save\": \"Save\",\n      \"delete\": \"Delete\",\n      \"newTitle\": \"Create a new wish list\",\n      \"shareTitle\": \"Share {name}\",\n      \"renameTitle\": \"Rename {name}\",\n      \"deleteTitle\": \"Delete {name}\",\n      \"changeVisibilityPublicTitle\": \"Make {name} public?\",\n      \"changeVisibilityPrivateTitle\": \"Make {name} private?\",\n      \"makePublicContent\": \"Are you sure you want to make <bold>{name}</bold> public? This will allow others to see your wish list if they have the link.\",\n      \"makePrivateContent\": \"Are you sure you want to make <bold>{name}</bold> private? If you've shared your wish list with others, they will no longer be able to see it.\",\n      \"deleteContent\": \"Are you sure you want to delete <bold>{name}</bold>? This action cannot be undone.\"\n    },\n    \"Form\": {\n      \"nameLabel\": \"Name\"\n    },\n    \"Errors\": {\n      \"nameRequired\": \"Wish list name cannot be empty.\",\n      \"updateFailed\": \"Failed to update your wish list. Please try again.\",\n      \"deleteFailed\": \"Failed to delete your wish list. Please try again.\",\n      \"removeProductFailed\": \"Failed to remove the product from your wish list. Please try again.\",\n      \"unauthorized\": \"You are not authorized to perform this action, please sign in and try again\",\n      \"unexpected\": \"An unexpected error occurred, please try again\"\n    },\n    \"Result\": {\n      \"createSuccess\": \"Wish list has been created successfully.\",\n      \"updateSuccess\": \"Wish list has been updated successfully.\",\n      \"deleteSuccess\": \"Wish list has been deleted successfully.\",\n      \"removeItemSuccess\": \"Item has been removed from your wish list.\"\n    },\n    \"Button\": {\n      \"label\": \"Add to wish list\",\n      \"addToNewWishlist\": \"Add to new wish list\",\n      \"defaultWishlistName\": \"My wish list\",\n      \"addSuccessMessage\": \"Product has been added to your wish list\",\n      \"removeSuccessMessage\": \"Product has been removed from your wish list\",\n      \"Errors\": {\n        \"addProductFailed\": \"Failed to add the product from your wish list, please try again\",\n        \"removeProductFailed\": \"Failed to remove the product from your wish list, please try again\"\n      }\n    }\n  },\n  \"PublicWishlist\": {\n    \"title\": \"Public Wish List\",\n    \"defaultName\": \"Public wish list\",\n    \"emptyWishlist\": \"This wish list doesn't have any products yet.\"\n  },\n  \"Blog\": {\n    \"title\": \"Blog\",\n    \"home\": \"Home\",\n    \"Empty\": {\n      \"title\": \"No blog posts found\",\n      \"subtitle\": \"Check back later for more content\"\n    },\n    \"SharingLinks\": {\n      \"share\": \"Share\",\n      \"email\": \"Email\",\n      \"print\": \"Print\"\n    }\n  },\n  \"Cart\": {\n    \"title\": \"Cart\",\n    \"heading\": \"Your cart\",\n    \"proceedToCheckout\": \"Proceed to checkout\",\n    \"increment\": \"Increase quantity\",\n    \"decrement\": \"Decrease quantity\",\n    \"removeItem\": \"Remove item\",\n    \"cartCombined\": \"We noticed you had items saved in a previous cart, so we've added them to your current cart for you.\",\n    \"cartRestored\": \"You started a cart on another device, and we've restored it here so you can pick up where you left off.\",\n    \"cartUpdateInProgress\": \"You have a cart update in progress. Are you sure you want to leave this page? Your changes may be lost.\",\n    \"originalPrice\": \"Original price was {price}.\",\n    \"currentPrice\": \"Current price is {price}.\",\n    \"quantityReadyToShip\": \"{quantity, number} ready to ship\",\n    \"quantityOnBackorder\": \"{quantity, number} will be backordered\",\n    \"partiallyAvailable\": \"Only {quantity, number} available\",\n    \"CheckoutSummary\": {\n      \"title\": \"Summary\",\n      \"subTotal\": \"Subtotal\",\n      \"discounts\": \"Discounts\",\n      \"tax\": \"Tax\",\n      \"total\": \"Total\",\n      \"CouponCode\": {\n        \"apply\": \"Apply\",\n        \"couponCode\": \"Coupon code\",\n        \"removeCouponCode\": \"Remove coupon code\",\n        \"invalidCouponCode\": \"Please enter a valid coupon code\",\n        \"cartNotFound\": \"An error occurred when retrieving your cart\"\n      },\n      \"Shipping\": {\n        \"shipping\": \"Shipping\",\n        \"add\": \"Add\",\n        \"change\": \"Change\",\n        \"cancel\": \"Cancel\",\n        \"country\": \"Country\",\n        \"city\": \"City\",\n        \"state\": \"State/Province\",\n        \"postalCode\": \"Postal code\",\n        \"updatedShippingOptions\": \"Update shipping options\",\n        \"viewShippingOptions\": \"View shipping options\",\n        \"editAddress\": \"Edit address\",\n        \"shippingOptions\": \"Shipping options\",\n        \"updateShipping\": \"Update shipping\",\n        \"addShipping\": \"Add shipping\",\n        \"cartNotFound\": \"An error occurred when retrieving your cart\",\n        \"noShippingOptions\": \"There are no shipping options available for your address\",\n        \"countryRequired\": \"Country is required\"\n      }\n    },\n    \"GiftCertificate\": {\n      \"giftCertificate\": \"Gift certificate\",\n      \"giftCertificateCode\": \"Gift certificate code\",\n      \"removeGiftCertificate\": \"Remove gift certificate\",\n      \"apply\": \"Apply\",\n      \"to\": \"To\",\n      \"message\": \"Message\",\n      \"invalidGiftCertificate\": \"Please enter a valid gift certificate code\",\n      \"cartNotFound\": \"An error occurred when retrieving your cart\"\n    },\n    \"Empty\": {\n      \"title\": \"Your cart is empty.\",\n      \"subtitle\": \"Add some products to get started.\",\n      \"cta\": \"Continue shopping\"\n    },\n    \"Errors\": {\n      \"cartNotFound\": \"An error occurred when retrieving your cart\",\n      \"lineItemNotFound\": \"Line item not found.\",\n      \"failedToUpdateQuantity\": \"Failed to update quantity.\",\n      \"somethingWentWrong\": \"Something went wrong. Please try again later.\"\n    }\n  },\n  \"Compare\": {\n    \"title\": \"Compare products\",\n    \"addToCart\": \"Add to cart\",\n    \"next\": \"Next products\",\n    \"previous\": \"Previous products\",\n    \"noProductsToCompare\": \"No products to compare\",\n    \"sku\": \"SKU\",\n    \"weight\": \"Weight\",\n    \"description\": \"Description\",\n    \"noDescription\": \"There is no description available.\",\n    \"rating\": \"Rating\",\n    \"noRatings\": \"There are no reviews.\",\n    \"otherDetails\": \"Other details\",\n    \"noOtherDetails\": \"There are no other details.\",\n    \"viewOptions\": \"View options\",\n    \"successMessage\": \"{cartItems, plural, =1 {1 item} other {# items}} added to <cartLink> your cart</cartLink>\",\n    \"missingCart\": \"Cart not found. Please try again later.\",\n    \"unknownError\": \"Unknown error. Please try again later.\"\n  },\n  \"Product\": {\n    \"ProductDetails\": {\n      \"quantity\": \"Quantity\",\n      \"increaseQuantity\": \"Increase quantity\",\n      \"decreaseQuantity\": \"Decrease quantity\",\n      \"emptySelectPlaceholder\": \"Select an option\",\n      \"successMessage\": \"{cartItems, plural, =1 {1 item} other {# items}} added to <cartLink> your cart</cartLink>\",\n      \"missingCart\": \"Cart not found. Please try again later!\",\n      \"unknownError\": \"Unknown error. Please try again later.\",\n      \"variantRequiredError\": \"This product requires options to be selected in order to add to the cart.\",\n      \"increaseNumber\": \"Increase number\",\n      \"decreaseNumber\": \"Decrease number\",\n      \"thumbnail\": \"View image number\",\n      \"additionalInformation\": \"Additional information\",\n      \"currentStock\": \"{quantity, number} in stock\",\n      \"backorderQuantity\": \"{quantity, number} will be on backorder\",\n      \"loadingMoreImages\": \"Loading more images\",\n      \"imagesLoaded\": \"{count, plural, =1 {1 more image loaded} other {# more images loaded}}\",\n      \"Submit\": {\n        \"addToCart\": \"Add to cart\",\n        \"outOfStock\": \"Out of stock\",\n        \"preorder\": \"Preorder\",\n        \"unavailable\": \"Unavailable\"\n      },\n      \"Accordions\": {\n        \"specifications\": \"Specifications\",\n        \"warranty\": \"Warranty\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Weight\",\n        \"condition\": \"Condition\"\n      }\n    },\n    \"RelatedProducts\": {\n      \"title\": \"Related products\",\n      \"noRelatedProducts\": \"No related products found\",\n      \"browseCatalog\": \"Try browsing our complete catalog of products.\",\n      \"cta\": \"Shop all\",\n      \"previousProducts\": \"Previous products\",\n      \"nextProducts\": \"Next products\",\n      \"scrollbar\": \"Related products scrollbar\"\n    },\n    \"Reviews\": {\n      \"title\": \"Reviews\",\n      \"empty\": \"No reviews have been added for this product.\",\n      \"previous\": \"Previous reviews\",\n      \"next\": \"Next reviews\",\n      \"Form\": {\n        \"button\": \"Write a review\",\n        \"title\": \"Write a review\",\n        \"submit\": \"Submit\",\n        \"cancel\": \"Cancel\",\n        \"ratingLabel\": \"Rating\",\n        \"titleLabel\": \"Title\",\n        \"reviewLabel\": \"Review\",\n        \"nameLabel\": \"Name\",\n        \"emailLabel\": \"Email\",\n        \"successMessage\": \"Your review has been submitted successfully!\",\n        \"somethingWentWrong\": \"Something went wrong. Please try again later.\",\n        \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n        \"FieldErrors\": {\n          \"titleRequired\": \"Title is required\",\n          \"authorRequired\": \"Name is required\",\n          \"emailRequired\": \"Email is required\",\n          \"emailInvalid\": \"Please enter a valid email address\",\n          \"textRequired\": \"Review is required\",\n          \"ratingRequired\": \"Rating is required\",\n          \"ratingTooSmall\": \"Rating must be at least 1\",\n          \"ratingTooLarge\": \"Rating must be at most 5\"\n        }\n      }\n    }\n  },\n  \"WebPages\": {\n    \"Normal\": {\n      \"home\": \"Home\"\n    },\n    \"ContactUs\": {\n      \"home\": \"Home\",\n      \"Form\": {\n        \"success\": \"Thanks for reaching out. We'll get back to you soon.\",\n        \"successCta\": \"Continue shopping\",\n        \"fullName\": \"Full name\",\n        \"companyName\": \"Company name\",\n        \"phone\": \"Phone\",\n        \"orderNo\": \"Order number\",\n        \"rma\": \"RMA number\",\n        \"email\": \"Email\",\n        \"comments\": \"Comments/questions\",\n        \"cta\": \"Submit form\",\n        \"somethingWentWrong\": \"Something went wrong. Please try again later.\",\n        \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\"\n      }\n    }\n  },\n  \"Maintenance\": {\n    \"title\": \"Maintenance\",\n    \"message\": \"We are down for maintenance\",\n    \"contactUs\": \"You can contact us at:\"\n  },\n  \"Error\": {\n    \"title\": \"There was a server error!\",\n    \"subtitle\": \"Please try again later.\",\n    \"cta\": \"Try again\"\n  },\n  \"NotFound\": {\n    \"title\": \"We couldn't find that page!\",\n    \"subtitle\": \"Try searching for something else or go back to the home page.\",\n    \"featuredProducts\": \"Featured products\",\n    \"search\": \"Search\"\n  },\n  \"Components\": {\n    \"Header\": {\n      \"home\": \"Home\",\n      \"toggleNavigation\": \"Toggle navigation\",\n      \"Icons\": {\n        \"account\": \"Profile\",\n        \"cart\": \"Cart\",\n        \"search\": \"Open search popup\",\n        \"giftCertificates\": \"Gift certificates\"\n      },\n      \"SwitchCurrency\": {\n        \"label\": \"Switch currency\",\n        \"invalidCurrency\": \"Invalid currency\",\n        \"errorUpdatingCurrency\": \"Error updating currency for your cart. Please try again.\"\n      },\n      \"Search\": {\n        \"products\": \"Products\",\n        \"categories\": \"Categories\",\n        \"brands\": \"Brands\",\n        \"noSearchResultsTitle\": \"Sorry, no results for \\\"{term}\\\".\",\n        \"noSearchResultsSubtitle\": \"Please try another search.\",\n        \"somethingWentWrong\": \"Something went wrong. Please try again.\",\n        \"inputPlaceholder\": \"Search products, categories, brands...\",\n        \"submitLabel\": \"Search\"\n      }\n    },\n    \"Footer\": {\n      \"home\": \"Home\",\n      \"contactUs\": \"Contact Us\",\n      \"socialMediaLinks\": \"Social media links\",\n      \"categories\": \"Categories\",\n      \"brands\": \"Brands\",\n      \"navigate\": \"Navigate\",\n      \"giftCertificates\": \"Gift certificates\"\n    },\n    \"Subscribe\": {\n      \"title\": \"Sign up for our newsletter\",\n      \"placeholder\": \"Enter your email\",\n      \"description\": \"Stay up to date with the latest news and offers from our store.\",\n      \"subscribedToNewsletter\": \"You have been subscribed to our newsletter!\",\n      \"Errors\": {\n        \"emailRequired\": \"Email is required\",\n        \"invalidEmail\": \"Please enter a valid email address\",\n        \"somethingWentWrong\": \"Something went wrong. Please try again later.\"\n      }\n    },\n    \"ConsentManager\": {\n      \"Common\": {\n        \"rejectAll\": \"Reject All\",\n        \"acceptAll\": \"Accept All\",\n        \"customize\": \"Customize\",\n        \"save\": \"Save Settings\"\n      },\n      \"CookieBanner\": {\n        \"title\": \"We value your privacy\",\n        \"description\": \"This site uses cookies to improve your browsing experience, analyze site traffic, and show personalized content.\",\n        \"privacyPolicy\": \"Privacy Policy\"\n      },\n      \"Dialog\": {\n        \"title\": \"Privacy Settings\",\n        \"description\": \"Customize your privacy settings here. You can choose which types of cookies and tracking technologies you would like to allow.\"\n      },\n      \"ConsentTypes\": {\n        \"necessary\": {\n          \"title\": \"Strictly Necessary\",\n          \"description\": \"These cookies are essential for the website to function properly and cannot be disabled.\"\n        },\n        \"functionality\": {\n          \"title\": \"Functionality\",\n          \"description\": \"These cookies enable enhanced functionality and personalization of the website.\"\n        },\n        \"marketing\": {\n          \"title\": \"Marketing\",\n          \"description\": \"These cookies are used to deliver relevant advertisements and track their effectiveness.\"\n        },\n        \"measurement\": {\n          \"title\": \"Analytics\",\n          \"description\": \"These cookies help us understand how visitors interact with the website and improve its performance.\"\n        },\n        \"experience\": {\n          \"title\": \"Experience\",\n          \"description\": \"These cookies help us provide a better user experience and test new features.\"\n        }\n      }\n    },\n    \"Price\": {\n      \"originalPrice\": \"Original price was {price}.\",\n      \"currentPrice\": \"Current price is {price}.\",\n      \"range\": \"Price from {minValue} to {maxValue}.\"\n    }\n  },\n  \"GiftCertificates\": {\n    \"title\": \"Gift certificates\",\n    \"description\": \"Give the perfect gift that never goes out of style. Let friends and loved ones choose exactly what they want from our entire collection.\",\n    \"purchaseLabel\": \"Shop now\",\n    \"checkBalanceLabel\": \"Check balance\",\n    \"expiresAtLabel\": \"Valid thru\",\n    \"CheckBalance\": {\n      \"title\": \"Check balance\",\n      \"description\": \"You can check the balance and get the information about your gift certificate by typing the code in the box below.\",\n      \"inputLabel\": \"Code\",\n      \"inputPlaceholder\": \"xxx-xxx-xxx-xxx\",\n      \"purchasedDateLabel\": \"Purchased\",\n      \"senderLabel\": \"From\",\n      \"Errors\": {\n        \"invalidCode\": \"The gift certificate code you entered is invalid. Please check the code and try again.\",\n        \"codeRequired\": \"Please enter a gift certificate code.\",\n        \"somethingWentWrong\": \"Something went wrong. Please try again later.\"\n      }\n    },\n    \"Purchase\": {\n      \"breadcrumbTitle\": \"Purchase a gift certificate\",\n      \"title\": \"Digital gift certificate\",\n      \"description\": \"Explore our gift certificates, perfect for any occasion. Choose the amount and personalize your message.\",\n      \"successMessage\": \"Gift certificate has been added to <cartLink> your cart</cartLink>\",\n      \"missingCart\": \"Cart not found. Please try again later.\",\n      \"unknownError\": \"Unknown error. Please try again later.\",\n      \"Form\": {\n        \"amountLabel\": \"Amount\",\n        \"customAmountLabel\": \"Amount (between {minAmount} and {maxAmount})\",\n        \"selectAmountPlaceholder\": \"Select an amount\",\n        \"customAmountPlaceholder\": \"Enter custom amount\",\n        \"senderNameLabel\": \"Your name\",\n        \"senderEmailLabel\": \"Your email\",\n        \"recipientNameLabel\": \"Recipient's name\",\n        \"recipientEmailLabel\": \"Recipient's email\",\n        \"namePlaceholder\": \"Enter name\",\n        \"emailPlaceholder\": \"Enter email\",\n        \"messageLabel\": \"Message\",\n        \"messagePlaceholder\": \"Enter your message (optional)\",\n        \"nonRefundableCheckboxLabel\": \"I agree that Gift Certificates are non-refundable\",\n        \"expiryCheckboxLabel\": \"I acknowledge that this Gift Certificate will expire on {expiryDate}\",\n        \"ctaLabel\": \"Add to cart\",\n        \"Errors\": {\n          \"amountRequired\": \"Please select or enter a gift certificate amount\",\n          \"amountInvalid\": \"Please select a valid gift certificate amount\",\n          \"amountOutOfRange\": \"Please enter an amount between {minAmount} and {maxAmount}\",\n          \"unexpectedSettingsError\": \"An unexpected error occurred while retrieving gift certificate settings. Please try again later.\",\n          \"senderNameRequired\": \"Your name is required\",\n          \"senderEmailRequired\": \"Your email is required\",\n          \"recipientNameRequired\": \"Recipient's name is required\",\n          \"recipientEmailRequired\": \"Recipient's email is required\",\n          \"emailInvalid\": \"Please enter a valid email address\",\n          \"checkboxRequired\": \"You must check this box to continue\"\n        }\n      }\n    }\n  },\n  \"Form\": {\n    \"optional\": \"optional\",\n    \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n    \"Errors\": {\n      \"invalidInput\": \"Please check your input and try again\",\n      \"invalidFormat\": \"The value entered does not match the required format\"\n    }\n  }\n}\n"
  },
  {
    "path": "core/messages/es-419.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Nuevos hallazgos para cada ocasión\",\n                \"description\": \"Explora nuestras últimas novedades seleccionadas para ofrecerte estilo, funcionalidad e inspiración. Compra ahora y descubre tu próximo favorito.\",\n                \"alt\": \"Cinco plantas pequeñas en macetas exhibidas sobre bloques apilados de color beige, con una variedad de follaje verde en macetas de color gris oscuro contra un fondo neutro.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide02\": {\n                \"title\": \"Descubre las novedades\",\n                \"description\": \"Compra nuestras últimas novedades y encuentra algo fresco y emocionante para tu hogar.\",\n                \"alt\": \"Manos extendiéndose para sostener un helecho verde en una canasta tejida con un lazo decorativo, sobre un fondo beige con sombras suaves.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide03\": {\n                \"title\": \"Algo para todos\",\n                \"description\": \"No te pierdas las ofertas exclusivas en nuestros productos más vendidos. Compra hoy y ahorra mucho dinero en los artículos que te gustan.\",\n                \"alt\": \"Primer plano de una hoja verde vibrante con perforaciones, que muestra su textura suave y detalles naturales.\",\n                \"cta\": \"Comprar ahora\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Colección destacada\",\n            \"description\": \"Explora nuestras mejores selecciones en esta colección destacada. ¡Encuentra el regalo perfecto o date un capricho!\",\n            \"cta\": \"Ver más\",\n            \"emptyStateTitle\": \"No se encontraron productos\",\n            \"emptyStateSubtitle\": \"Intenta navegar por nuestro catálogo completo de productos.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Novedades\",\n            \"description\": \"Nuestros últimos productos están aquí. Descubre lo nuevo de la tienda.\",\n            \"cta\": \"Ver todas\",\n            \"emptyStateTitle\": \"No se encontraron productos\",\n            \"emptyStateSubtitle\": \"Intenta navegar por nuestro catálogo completo de productos.\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Cambiar contraseña\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"passwordUpdated\": \"¡La contraseña se actualizó correctamente!\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Por favor, confirme tu contraseña\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Inicio de sesión\",\n            \"heading\": \"Inicio de sesión\",\n            \"forgotPassword\": \"¿Olvidaste tu contraseña?\",\n            \"cta\": \"Inicio de sesión\",\n            \"email\": \"Correo electrónico\",\n            \"password\": \"Contraseña\",\n            \"invalidCredentials\": \"Tu dirección de correo electrónico o tu contraseña son incorrectas. Intenta iniciar sesión de nuevo o restablece tu contraseña\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"passwordResetRequired\": \"Se requiere restablecimiento de contraseña. Por favor, revisa tu email para ver instrucciones para restablecer tu contraseña.\",\n            \"invalidToken\": \"Tu enlace de acceso es inválido o caducó. Por favor, intenta iniciar sesión de nuevo.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"invalidInput\": \"Por favor, revisa tu opinión y vuelve a intentarlo.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"¿Nuevo cliente?\",\n                \"accountBenefits\": \"Crea una cuenta con nosotros y podrás:\",\n                \"fastCheckout\": \"Pagar más rápido\",\n                \"multipleAddresses\": \"Guardar varias direcciones de envío\",\n                \"ordersHistory\": \"Acceder a tu historial de pedidos\",\n                \"ordersTracking\": \"Rastrear nuevos pedidos\",\n                \"wishlists\": \"Guardar artículos en tu lista de deseos\",\n                \"cta\": \"Crear cuenta\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Olvidé la contraseña\",\n                \"subtitle\": \"A continuación, ingresa el correo electrónico asociado a tu cuenta. Te enviaremos instrucciones para restablecer tu contraseña.\",\n                \"confirmResetPassword\": \"Si la dirección de correo electrónico {email} está vinculada a una cuenta en nuestra tienda, te hemos enviado un correo electrónico de restablecimiento de contraseña. Revisa tu bandeja de entrada y tu carpeta de correo no deseado si no lo ves.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Registrar cuenta\",\n            \"heading\": \"Cuenta nueva\",\n            \"cta\": \"Crear cuenta\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"addressLine1Required\": \"La línea 1 es obligatoria\",\n                \"cityRequired\": \"La ciudad es obligatoria\",\n                \"countryRequired\": \"País es un campo obligatorio\",\n                \"stateRequired\": \"Estado/Provincia es un campo obligatorio.\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio.\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"No hay productos en esta marca\",\n                \"subtitle\": \"Intenta usar diferentes filtros.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Categorías\",\n            \"Empty\": {\n                \"title\": \"Ningún producto en esta categoría\",\n                \"subtitle\": \"Intenta usar diferentes filtros.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Resultados de búsqueda\",\n            \"searchResults\": \"Resultados de búsqueda para\",\n            \"subCategories\": \"Categorías\",\n            \"Breadcrumbs\": {\n                \"home\": \"Inicio\",\n                \"search\": \"Buscar\"\n            },\n            \"Empty\": {\n                \"title\": \"Lo sentimos, no hay resultados para “{term}”.\",\n                \"subtitle\": \"Prueba con otra búsqueda.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtros\",\n            \"resetFilters\": \"Restablecer filtros\",\n            \"Range\": {\n                \"apply\": \"Aplicar\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Envío gratuito\",\n                \"isFeaturedLabel\": \"Destacado\",\n                \"inStockLabel\": \"En existencias\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Ordenar por:\",\n            \"featuredItems\": \"Elementos destacados\",\n            \"bestSellingItems\": \"Artículos más vendidos\",\n            \"newestItems\": \"Artículos más nuevos\",\n            \"aToZ\": \"A a la Z\",\n            \"zToA\": \"Z a la A\",\n            \"byReview\": \"Por opinión\",\n            \"priceAscending\": \"Precio: ascendente\",\n            \"priceDescending\": \"Precio: descendente\",\n            \"relevance\": \"Relevancia\"\n        },\n        \"Compare\": {\n            \"compare\": \"Comparar\",\n            \"remove\": \"Quitar\",\n            \"maxCompareLimit\": \"Alcanzaste el número máximo de productos para comparar. Elimina un producto para agregar uno nuevo.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Direcciones\",\n            \"logout\": \"Cerrar sesión\",\n            \"orders\": \"Pedidos\",\n            \"settings\": \"Cuenta\",\n            \"wishlists\": \"Listas de deseos\"\n        },\n        \"Orders\": {\n            \"title\": \"Pedidos\",\n            \"orderNumber\": \"Núm. de pedido\",\n            \"totalPrice\": \"Total\",\n            \"viewDetails\": \"Ver detalles\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ningún pedido\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Details\": {\n                \"title\": \"Pedido núm.{orderNumber}\",\n                \"shippingAddress\": \"Dirección de envío\",\n                \"shippingMethod\": \"Método de envío\",\n                \"summaryTotal\": \"Total\",\n                \"destination\": \"Destino\",\n                \"destinationWithCount\": \"Destino {número, número}/{total, número}\",\n                \"digitalDelivery\": \"Entrega digital a {email}\",\n                \"subtotal\": \"Subtotal\",\n                \"shipping\": \"Envío\",\n                \"tax\": \"Impuesto\",\n                \"orderSummary\": \"Resumen del pedido\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Método de pago} other {Métodos de pago}}\",\n                \"paymentEndingInLabel\": \"Con terminación en\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Tarjeta de crédito\",\n                    \"giftCertificate\": \"Certificado de regalo\",\n                    \"storeCredit\": \"Crédito de la tienda\",\n                    \"other\": \"Otro\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Direcciones\",\n            \"cta\": \"Agregar dirección\",\n            \"edit\": \"Editar\",\n            \"delete\": \"Eliminar\",\n            \"cancel\": \"Cancelar\",\n            \"create\": \"Crear\",\n            \"update\": \"Actualizar\",\n            \"setDefault\": \"Establecer como predeterminado\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ninguna dirección\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"addressLine1Required\": \"La línea 1 es obligatoria\",\n                \"cityRequired\": \"La ciudad es obligatoria\",\n                \"countryRequired\": \"País es un campo obligatorio\",\n                \"stateRequired\": \"Estado/Provincia es un campo obligatorio.\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio.\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"CONFIGURACIÓN DE CUENTA\",\n            \"changePassword\": \"Cambiar contraseña\",\n            \"passwordUpdated\": \"¡La contraseña se actualizó correctamente!\",\n            \"settingsUpdated\": \"La configuración de la cuenta se actualizó correctamente.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"currentPassword\": \"Contraseña actual\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"cta\": \"Actualizar\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Preferencias de marketing\",\n                \"label\": \"Suscríbete a nuestro boletín informativo.\",\n                \"marketingPreferencesUpdated\": \"¡Las preferencias de marketing se actualizaron con éxito!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"firstNameTooSmall\": \"El nombre de pila debe tener al menos 2 caracteres\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"lastNameTooSmall\": \"El apellido debe tener al menos 2 caracteres\",\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"currentPasswordRequired\": \"La contraseña actual es obligatoria\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Por favor, confirme tu contraseña\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Acciones de la lista de deseos\",\n        \"title\": \"Listas de deseos\",\n        \"new\": \"Nueva lista de deseos\",\n        \"items\": \"{count, plural, =1 {1 artículo} other {# artículos}}\",\n        \"viewWishlist\": \"Ver lista\",\n        \"noWishlists\": \"No tienes ninguna lista de deseos\",\n        \"noWishlistsCallToAction\": \"Crea una lista de deseos\",\n        \"emptyWishlist\": \"No has agregado productos a esta lista de deseos.\",\n        \"share\": \"Compartir\",\n        \"shareSuccess\": \"La lista de deseos se compartió correctamente.\",\n        \"shareCopied\": \"La URL pública de la lista de deseos se copió a tu portapapeles.\",\n        \"shareDisabled\": \"Tu lista de deseos debe ser pública para que puedas compartirla.\",\n        \"makePublic\": \"Haz público\",\n        \"makePrivate\": \"Configurar como privada\",\n        \"rename\": \"Cambiar el nombre\",\n        \"delete\": \"Eliminar\",\n        \"removeButtonTitle\": \"Eliminar el producto de tu lista de deseos\",\n        \"Visibility\": {\n            \"public\": \"Público\",\n            \"private\": \"Privado\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Cancelar\",\n            \"close\": \"Cerrar\",\n            \"copy\": \"Copiar\",\n            \"create\": \"Crear\",\n            \"save\": \"Guardar\",\n            \"delete\": \"Eliminar\",\n            \"newTitle\": \"Crea una nueva lista de deseos\",\n            \"shareTitle\": \"Comparte {name}\",\n            \"renameTitle\": \"Renombrar {name}\",\n            \"deleteTitle\": \"Borrar {name}\",\n            \"changeVisibilityPublicTitle\": \"¿Hacer {name} público?\",\n            \"changeVisibilityPrivateTitle\": \"¿Hacer {name} privado?\",\n            \"makePublicContent\": \"¿Confirmas que quieres configurar <bold>{name}</bold> como pública? Si lo haces, otras personas podrán ver tu lista de deseos si tienen un enlace para acceder a ella.\",\n            \"makePrivateContent\": \"¿Confirmas que quieres configurar <bold>{name}</bold> como privada? Las personas con quienes hayas compartido tu lista de deseos ya no podrán verla.\",\n            \"deleteContent\": \"¿Confirmas que quieres eliminar <bold>{name}</bold>? Esta acción no puede deshacerse.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Nombre\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"El nombre de la lista de deseos no puede estar vacío.\",\n            \"updateFailed\": \"No se pudo actualizar tu lista de deseos. Vuelve a intentarlo.\",\n            \"deleteFailed\": \"No se pudo eliminar tu lista de deseos. Vuelve a intentarlo.\",\n            \"removeProductFailed\": \"No se pudo eliminar el producto de tu lista de deseos. Vuelve a intentarlo.\",\n            \"unauthorized\": \"No tienes autorización para realizar esta acción. Inicia sesión e inténtalo de nuevo.\",\n            \"unexpected\": \"Se produjo un error inesperado, inténtalo de nuevo\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"La lista de deseos se creó correctamente.\",\n            \"updateSuccess\": \"La lista de deseos se actualizó correctamente.\",\n            \"deleteSuccess\": \"La lista de deseos se eliminó correctamente.\",\n            \"removeItemSuccess\": \"Se eliminó el artículo de tu lista de deseos.\"\n        },\n        \"Button\": {\n            \"label\": \"Agregar a la lista de deseos\",\n            \"addToNewWishlist\": \"Agregar a una nueva lista de deseos\",\n            \"defaultWishlistName\": \"Mi lista de deseos\",\n            \"addSuccessMessage\": \"El producto se ha agregado a tu lista de deseos.\",\n            \"removeSuccessMessage\": \"El producto se ha eliminado de tu lista de deseos.\",\n            \"Errors\": {\n                \"addProductFailed\": \"No se pudo agregar el producto de tu lista de deseos, inténtalo de nuevo\",\n                \"removeProductFailed\": \"No se pudo eliminar el producto de tu lista de deseos, inténtalo de nuevo\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Lista de deseos pública\",\n        \"defaultName\": \"Lista pública de deseos\",\n        \"emptyWishlist\": \"Aún no hay productos en esta lista de deseos.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Inicio\",\n        \"Empty\": {\n            \"title\": \"No se encontraron publicaciones de blog\",\n            \"subtitle\": \"Vuelve a consultar más tarde para obtener más contenido\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Compartir\",\n            \"email\": \"Correo electrónico\",\n            \"print\": \"Imprimir\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Carrito\",\n        \"heading\": \"Tu carrito\",\n        \"proceedToCheckout\": \"Continuar con el proceso de pago\",\n        \"increment\": \"Aumenta la cantidad\",\n        \"decrement\": \"Disminuir cantidad\",\n        \"removeItem\": \"Eliminar artículo\",\n        \"cartCombined\": \"Notamos que tenías artículos guardados en un carrito anterior, así que los hemos agregado a tu carrito actual.\",\n        \"cartRestored\": \"Iniciaste un carrito en otro dispositivo y lo hemos restaurado aquí para que puedas continuar desde donde lo dejaste.\",\n        \"cartUpdateInProgress\": \"Tiene una actualización de carrito en proceso. ¿Estás seguro de que quieres salir de esta página? Es posible que se pierdan sus cambios.\",\n        \"originalPrice\": \"El precio original era {price}.\",\n        \"currentPrice\": \"El precio actual es {price}.\",\n        \"quantityReadyToShip\": \"{cantidad, número} listo para enviar\",\n        \"quantityOnBackorder\": \"{cantidad, número} estará en pedido pendiente\",\n        \"partiallyAvailable\": \"Solo {cantidad, número} disponible\",\n        \"CheckoutSummary\": {\n            \"title\": \"Resumen\",\n            \"subTotal\": \"Subtotal\",\n            \"discounts\": \"Descuentos\",\n            \"tax\": \"Impuesto\",\n            \"total\": \"Total\",\n            \"CouponCode\": {\n                \"apply\": \"Aplicar\",\n                \"couponCode\": \"Código de cupón\",\n                \"removeCouponCode\": \"Elimina el código de cupón\",\n                \"invalidCouponCode\": \"Ingresa un código de cupón válido\",\n                \"cartNotFound\": \"Se produjo un error al recuperar su carrito\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Envío\",\n                \"add\": \"Agregar\",\n                \"change\": \"Cambiar\",\n                \"cancel\": \"Cancelar\",\n                \"country\": \"País\",\n                \"city\": \"Ciudad\",\n                \"state\": \"Estado/Provincia\",\n                \"postalCode\": \"Código postal\",\n                \"updatedShippingOptions\": \"Actualiza las opciones de envío\",\n                \"viewShippingOptions\": \"Ver opciones de envío\",\n                \"editAddress\": \"Editar dirección\",\n                \"shippingOptions\": \"Opciones de envío\",\n                \"updateShipping\": \"Actualizar envío\",\n                \"addShipping\": \"Agregar envío\",\n                \"cartNotFound\": \"Se produjo un error al recuperar su carrito\",\n                \"noShippingOptions\": \"No hay opciones de envío disponibles para tu dirección\",\n                \"countryRequired\": \"País es un campo obligatorio\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Certificado de regalo\",\n            \"giftCertificateCode\": \"Código del certificado de regalo\",\n            \"removeGiftCertificate\": \"Eliminar certificado de regalo\",\n            \"apply\": \"Aplicar\",\n            \"to\": \"De\",\n            \"message\": \"Mensaje\",\n            \"invalidGiftCertificate\": \"Ingrese un código de certificado de regalo válido\",\n            \"cartNotFound\": \"Se produjo un error al recuperar su carrito\"\n        },\n        \"Empty\": {\n            \"title\": \"Su carrito está vacío.\",\n            \"subtitle\": \"Agrega algunos productos para comenzar.\",\n            \"cta\": \"Seguir comprando\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Se produjo un error al recuperar su carrito\",\n            \"lineItemNotFound\": \"Artículo no encontrado.\",\n            \"failedToUpdateQuantity\": \"No se pudo actualizar la cantidad.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Comparar productos\",\n        \"addToCart\": \"Agregar al carrito\",\n        \"next\": \"Siguientes productos\",\n        \"previous\": \"Productos anteriores\",\n        \"noProductsToCompare\": \"No hay productos para comparar\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Peso\",\n        \"description\": \"Descripción\",\n        \"noDescription\": \"No hay ninguna descripción disponible.\",\n        \"rating\": \"Calificación\",\n        \"noRatings\": \"No hay reseñas.\",\n        \"otherDetails\": \"Otros detalles\",\n        \"noOtherDetails\": \"No hay otros detalles.\",\n        \"viewOptions\": \"Opciones de visualización\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 artículo} other {# artículos}} agregado(s) a <cartLink>tu carrito</cartLink>\",\n        \"missingCart\": \"Carrito no encontrado. Vuelva a intentarlo más tarde.\",\n        \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Cantidad\",\n            \"increaseQuantity\": \"Aumenta la cantidad\",\n            \"decreaseQuantity\": \"Disminuir cantidad\",\n            \"emptySelectPlaceholder\": \"Seleccione una opción\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 artículo} other {# artículos}} agregado(s) a <cartLink>tu carrito</cartLink>\",\n            \"missingCart\": \"Carrito no encontrado. Intenta de nuevo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"variantRequiredError\": \"Para añadir este producto al carrito, debes seleccionar opciones.\",\n            \"increaseNumber\": \"Aumentar número\",\n            \"decreaseNumber\": \"Disminuir número\",\n            \"thumbnail\": \"Ver número de imagen\",\n            \"additionalInformation\": \"Información adicional\",\n            \"currentStock\": \"{cantidad, número} en stock\",\n            \"backorderQuantity\": \"{cantidad, número} estará en espera\",\n            \"loadingMoreImages\": \"Cargando más imágenes\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 imagen más cargada} other {# más imágenes cargadas}}\",\n            \"Submit\": {\n                \"addToCart\": \"Agregar al carrito\",\n                \"outOfStock\": \"Agotado/a\",\n                \"preorder\": \"Pedido anticipado\",\n                \"unavailable\": \"No disponible\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Especificaciones\",\n                \"warranty\": \"Garantía\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Peso\",\n                \"condition\": \"Condición\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Productos relacionados\",\n            \"noRelatedProducts\": \"No se encontraron productos relacionados\",\n            \"browseCatalog\": \"Intenta navegar por nuestro catálogo completo de productos.\",\n            \"cta\": \"Comprar todos\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\",\n            \"scrollbar\": \"Barra de desplazamiento de productos relacionados\"\n        },\n        \"Reviews\": {\n            \"title\": \"Reseñas\",\n            \"empty\": \"No se han agregado reseñas para este producto.\",\n            \"previous\": \"Reseñas anteriores\",\n            \"next\": \"Próximas reseñas\",\n            \"Form\": {\n                \"button\": \"Escribe una opinión\",\n                \"title\": \"Escribe una opinión\",\n                \"submit\": \"Enviar\",\n                \"cancel\": \"Cancelar\",\n                \"ratingLabel\": \"Calificación\",\n                \"titleLabel\": \"Título\",\n                \"reviewLabel\": \"Revisar\",\n                \"nameLabel\": \"Nombre\",\n                \"emailLabel\": \"Correo electrónico\",\n                \"successMessage\": \"¡Tu reseña fue enviada con éxito!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Se requiere título\",\n                    \"authorRequired\": \"El campo Nombre es obligatorio\",\n                    \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                    \"textRequired\": \"Es necesario revisar\",\n                    \"ratingRequired\": \"Se requiere una calificación\",\n                    \"ratingTooSmall\": \"La puntuación debe ser al menos 1\",\n                    \"ratingTooLarge\": \"El puntaje debe ser como máximo 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Inicio\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Inicio\",\n            \"Form\": {\n                \"success\": \"Gracias por ponerte en contacto. Nos comunicaremos contigo pronto.\",\n                \"successCta\": \"Seguir comprando\",\n                \"fullName\": \"Nombre completo\",\n                \"companyName\": \"Nombre de la empresa\",\n                \"phone\": \"Teléfono\",\n                \"orderNo\": \"Número de pedido\",\n                \"rma\": \"Número de RMA (autorización de devolución)\",\n                \"email\": \"Correo electrónico\",\n                \"comments\": \"Comentarios/Preguntas\",\n                \"cta\": \"Enviar formulario\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"MANTENIMIENTO\",\n        \"message\": \"Estamos fuera de servicio por mantenimiento\",\n        \"contactUs\": \"Puedes contactarnos en:\"\n    },\n    \"Error\": {\n        \"title\": \"Hubo un error en el servidor.\",\n        \"subtitle\": \"Vuelva a intentarlo más tarde.\",\n        \"cta\": \"Volver a intentarlo\"\n    },\n    \"NotFound\": {\n        \"title\": \"No pudimos encontrar esa página.\",\n        \"subtitle\": \"Intenta buscar algo más o regresa a la página de inicio.\",\n        \"featuredProducts\": \"Productos destacados\",\n        \"search\": \"Buscar\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Inicio\",\n            \"toggleNavigation\": \"Alternar navegación\",\n            \"Icons\": {\n                \"account\": \"Perfil\",\n                \"cart\": \"Carrito\",\n                \"search\": \"Abrir ventana emergente de búsqueda\",\n                \"giftCertificates\": \"Certificados de regalo\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Cambiar moneda\",\n                \"invalidCurrency\": \"Moneda inválida\",\n                \"errorUpdatingCurrency\": \"Error al actualizar la moneda de tu carrito. Vuelve a intentarlo.\"\n            },\n            \"Search\": {\n                \"products\": \"Productos\",\n                \"categories\": \"Categorías\",\n                \"brands\": \"Marcas\",\n                \"noSearchResultsTitle\": \"Lo sentimos, no hay resultados para “{term}”.\",\n                \"noSearchResultsSubtitle\": \"Prueba con otra búsqueda.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo.\",\n                \"inputPlaceholder\": \"Buscar productos, categorías, marcas...\",\n                \"submitLabel\": \"Buscar\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Inicio\",\n            \"contactUs\": \"Contacto\",\n            \"socialMediaLinks\": \"Enlaces a redes sociales\",\n            \"categories\": \"Categorías\",\n            \"brands\": \"Marcas\",\n            \"navigate\": \"Navegar\",\n            \"giftCertificates\": \"Certificados de regalo\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Suscríbete a nuestro boletín informativo\",\n            \"placeholder\": \"Ingresa tu correo electrónico\",\n            \"description\": \"Mantente al día con las últimas noticias y ofertas de nuestra tienda.\",\n            \"subscribedToNewsletter\": \"¡Te suscribiste a nuestro boletín!\",\n            \"Errors\": {\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"invalidEmail\": \"Por favor, introduzca una dirección de email válida\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Rechazar todo\",\n                \"acceptAll\": \"Aceptar todo\",\n                \"customize\": \"Personalizar\",\n                \"save\": \"Guardar la configuración\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"Valoramos su privacidad\",\n                \"description\": \"Este sitio emplea cookies para mejorar su experiencia de navegación, analizar el tráfico del sitio y mostrar contenido personalizado.\",\n                \"privacyPolicy\": \"Política de Privacidad\"\n            },\n            \"Dialog\": {\n                \"title\": \"Configuración de privacidad\",\n                \"description\": \"Personaliza tu configuración de privacidad aquí. Puede elegir qué tipos de cookies y tecnologías de seguimiento le gustaría permitir.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Estrictamente necesario\",\n                    \"description\": \"Estas cookies son Essentials para que el sitio web funcione correctamente y no se pueden desactivar.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Funcionalidad\",\n                    \"description\": \"Estas cookies permiten mejorar la funcionalidad y la personalización del sitio web.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"Estas cookies se emplean para ofrecer anuncios relevantes y realizar un seguimiento de su efectividad.\"\n                },\n                \"measurement\": {\n                    \"title\": \"DATOS ANALÍTICOS\",\n                    \"description\": \"Estas cookies nos ayudan a comprender cómo interactúan los visitantes con el sitio web y mejorar su rendimiento.\"\n                },\n                \"experience\": {\n                    \"title\": \"Experiencia\",\n                    \"description\": \"Estas cookies nos ayudan a brindar una mejor experiencia de usuario y probar nuevas funciones.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"El precio original era {price}.\",\n            \"currentPrice\": \"El precio actual es {price}.\",\n            \"range\": \"Precio de {minValue} a {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Certificados de regalo\",\n        \"description\": \"Regala el regalo perfecto que nunca pasa de moda. Deja que tus colegas y seres queridos elijan exactamente lo que quieren de toda nuestra colección.\",\n        \"purchaseLabel\": \"Comprar ahora\",\n        \"checkBalanceLabel\": \"Consultar saldo\",\n        \"expiresAtLabel\": \"Válido hasta\",\n        \"CheckBalance\": {\n            \"title\": \"Consultar saldo\",\n            \"description\": \"Puede consultar el saldo y obtener la información sobre su certificado de regalo escribiendo el código en el cuadro a continuación.\",\n            \"inputLabel\": \"Código\",\n            \"inputPlaceholder\": \"xxx-xxx-xxx-xxx\",\n            \"purchasedDateLabel\": \"Comprado\",\n            \"senderLabel\": \"Para\",\n            \"Errors\": {\n                \"invalidCode\": \"El código del certificado de regalo que ingresó no es válido. Verifique el código e inténtelo de nuevo.\",\n                \"codeRequired\": \"Ingresa el código del certificado de regalo.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Comprar certificado de regalo\",\n            \"title\": \"Certificado de regalo digital\",\n            \"description\": \"Explore nuestros certificados de regalo, perfectos para cualquier ocasión. Elige la cantidad y personaliza tu mensaje.\",\n            \"successMessage\": \"Se ha agregado un certificado de regalo a <cartLink> su carrito</cartLink>\",\n            \"missingCart\": \"Carrito no encontrado. Vuelva a intentarlo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"Form\": {\n                \"amountLabel\": \"Monto\",\n                \"customAmountLabel\": \"Importe (entre {minAmount} y {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Selecciona una cantidad\",\n                \"customAmountPlaceholder\": \"Ingrese la cantidad personalizada\",\n                \"senderNameLabel\": \"Tu nombre\",\n                \"senderEmailLabel\": \"Tu correo electrónico\",\n                \"recipientNameLabel\": \"Nombre del destinatario\",\n                \"recipientEmailLabel\": \"Correo electrónico del destinatario\",\n                \"namePlaceholder\": \"Ingrese el nombre\",\n                \"emailPlaceholder\": \"Ingresa el correo electrónico\",\n                \"messageLabel\": \"Mensaje\",\n                \"messagePlaceholder\": \"Ingrese su mensaje (opcional)\",\n                \"nonRefundableCheckboxLabel\": \"Acepto que los certificados de regalo no son reembolsables\",\n                \"expiryCheckboxLabel\": \"Reconozco que este Certificado de Regalo caducará el {expiryDate}\",\n                \"ctaLabel\": \"Agregar al carrito\",\n                \"Errors\": {\n                    \"amountRequired\": \"Por favor, selecciona o introduce el importe de un vale regalo\",\n                    \"amountInvalid\": \"Por favor, seleccione un importe válido de un certificado regalo\",\n                    \"amountOutOfRange\": \"Por favor, introduzca una cantidad entre {minAmount} y {maxAmount}\",\n                    \"unexpectedSettingsError\": \"Se produjo un error inesperado al recuperar la configuración del certificado de regalo. Intente de nuevo más tarde.\",\n                    \"senderNameRequired\": \"Tu nombre es obligatorio\",\n                    \"senderEmailRequired\": \"Tu correo electrónico es obligatorio\",\n                    \"recipientNameRequired\": \"Se requiere el nombre del destinatario\",\n                    \"recipientEmailRequired\": \"Se requiere el email del destinatario\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                    \"checkboxRequired\": \"Debes marcar esta casilla para continuar\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"Opcional\",\n        \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n        \"Errors\": {\n            \"invalidInput\": \"Por favor, revisa tu opinión y vuelve a intentarlo\",\n            \"invalidFormat\": \"El valor introducido no coincide con el formato requerido\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/es-AR.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Nuevos hallazgos para cada ocasión\",\n                \"description\": \"Explora nuestras últimas novedades seleccionadas para ofrecerte estilo, funcionalidad e inspiración. Compra ahora y descubre tu próximo favorito.\",\n                \"alt\": \"Cinco plantas pequeñas en macetas exhibidas sobre bloques apilados de color beige, con una variedad de follaje verde en macetas de color gris oscuro contra un fondo neutro.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide02\": {\n                \"title\": \"Descubre las novedades\",\n                \"description\": \"Compra nuestras últimas novedades y encuentra algo fresco y emocionante para tu hogar.\",\n                \"alt\": \"Manos extendiéndose para sostener un helecho verde en una canasta tejida con un lazo decorativo, sobre un fondo beige con sombras suaves.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide03\": {\n                \"title\": \"Algo para todos\",\n                \"description\": \"No te pierdas las ofertas exclusivas en nuestros productos más vendidos. Compra hoy y ahorra mucho dinero en los artículos que te gustan.\",\n                \"alt\": \"Primer plano de una hoja verde vibrante con perforaciones, que muestra su textura suave y detalles naturales.\",\n                \"cta\": \"Comprar ahora\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Colección destacada\",\n            \"description\": \"Explora nuestras mejores selecciones en esta colección destacada. ¡Encuentra el regalo perfecto o date un capricho!\",\n            \"cta\": \"Ver más\",\n            \"emptyStateTitle\": \"No se encontraron productos\",\n            \"emptyStateSubtitle\": \"Intenta navegar por nuestro catálogo completo de productos.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Novedades\",\n            \"description\": \"Nuestros últimos productos están aquí. Descubre lo nuevo de la tienda.\",\n            \"cta\": \"Ver todas\",\n            \"emptyStateTitle\": \"No se encontraron productos\",\n            \"emptyStateSubtitle\": \"Intenta navegar por nuestro catálogo completo de productos.\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Cambiar contraseña\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"passwordUpdated\": \"¡La contraseña se actualizó correctamente!\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Por favor, confirme tu contraseña\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Inicio de sesión\",\n            \"heading\": \"Inicio de sesión\",\n            \"forgotPassword\": \"¿Olvidaste tu contraseña?\",\n            \"cta\": \"Inicio de sesión\",\n            \"email\": \"Correo electrónico\",\n            \"password\": \"Contraseña\",\n            \"invalidCredentials\": \"Tu dirección de correo electrónico o tu contraseña son incorrectas. Intenta iniciar sesión de nuevo o restablece tu contraseña\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"passwordResetRequired\": \"Se requiere restablecimiento de contraseña. Por favor, revisa tu email para ver instrucciones para restablecer tu contraseña.\",\n            \"invalidToken\": \"Tu enlace de acceso es inválido o caducó. Por favor, intenta iniciar sesión de nuevo.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"invalidInput\": \"Por favor, revisa tu opinión y vuelve a intentarlo.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"¿Nuevo cliente?\",\n                \"accountBenefits\": \"Crea una cuenta con nosotros y podrás:\",\n                \"fastCheckout\": \"Pagar más rápido\",\n                \"multipleAddresses\": \"Guardar varias direcciones de envío\",\n                \"ordersHistory\": \"Acceder a tu historial de pedidos\",\n                \"ordersTracking\": \"Rastrear nuevos pedidos\",\n                \"wishlists\": \"Guardar artículos en tu lista de deseos\",\n                \"cta\": \"Crear cuenta\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Olvidé la contraseña\",\n                \"subtitle\": \"A continuación, ingresa el correo electrónico asociado a tu cuenta. Te enviaremos instrucciones para restablecer tu contraseña.\",\n                \"confirmResetPassword\": \"Si la dirección de correo electrónico {email} está vinculada a una cuenta en nuestra tienda, te hemos enviado un correo electrónico de restablecimiento de contraseña. Revisa tu bandeja de entrada y tu carpeta de correo no deseado si no lo ves.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Registrar cuenta\",\n            \"heading\": \"Cuenta nueva\",\n            \"cta\": \"Crear cuenta\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"addressLine1Required\": \"La línea 1 es obligatoria\",\n                \"cityRequired\": \"La ciudad es obligatoria\",\n                \"countryRequired\": \"País es un campo obligatorio\",\n                \"stateRequired\": \"Estado/Provincia es un campo obligatorio.\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio.\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"No hay productos en esta marca\",\n                \"subtitle\": \"Intenta usar diferentes filtros.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Categorías\",\n            \"Empty\": {\n                \"title\": \"Ningún producto en esta categoría\",\n                \"subtitle\": \"Intenta usar diferentes filtros.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Resultados de búsqueda\",\n            \"searchResults\": \"Resultados de búsqueda para\",\n            \"subCategories\": \"Categorías\",\n            \"Breadcrumbs\": {\n                \"home\": \"Inicio\",\n                \"search\": \"Buscar\"\n            },\n            \"Empty\": {\n                \"title\": \"Lo sentimos, no hay resultados para “{term}”.\",\n                \"subtitle\": \"Prueba con otra búsqueda.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtros\",\n            \"resetFilters\": \"Restablecer filtros\",\n            \"Range\": {\n                \"apply\": \"Aplicar\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Envío gratuito\",\n                \"isFeaturedLabel\": \"Destacado\",\n                \"inStockLabel\": \"En existencias\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Ordenar por:\",\n            \"featuredItems\": \"Elementos destacados\",\n            \"bestSellingItems\": \"Artículos más vendidos\",\n            \"newestItems\": \"Artículos más nuevos\",\n            \"aToZ\": \"A a la Z\",\n            \"zToA\": \"Z a la A\",\n            \"byReview\": \"Por opinión\",\n            \"priceAscending\": \"Precio: ascendente\",\n            \"priceDescending\": \"Precio: descendente\",\n            \"relevance\": \"Relevancia\"\n        },\n        \"Compare\": {\n            \"compare\": \"Comparar\",\n            \"remove\": \"Quitar\",\n            \"maxCompareLimit\": \"Alcanzaste el número máximo de productos para comparar. Elimina un producto para agregar uno nuevo.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Direcciones\",\n            \"logout\": \"Cerrar sesión\",\n            \"orders\": \"Pedidos\",\n            \"settings\": \"Cuenta\",\n            \"wishlists\": \"Listas de deseos\"\n        },\n        \"Orders\": {\n            \"title\": \"Pedidos\",\n            \"orderNumber\": \"Núm. de pedido\",\n            \"totalPrice\": \"Total\",\n            \"viewDetails\": \"Ver detalles\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ningún pedido\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Details\": {\n                \"title\": \"Pedido núm.{orderNumber}\",\n                \"shippingAddress\": \"Dirección de envío\",\n                \"shippingMethod\": \"Método de envío\",\n                \"summaryTotal\": \"Total\",\n                \"destination\": \"Destino\",\n                \"destinationWithCount\": \"Destino {número, número}/{total, número}\",\n                \"digitalDelivery\": \"Entrega digital a {email}\",\n                \"subtotal\": \"Subtotal\",\n                \"shipping\": \"Envío\",\n                \"tax\": \"Impuesto\",\n                \"orderSummary\": \"Resumen del pedido\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Método de pago} other {Métodos de pago}}\",\n                \"paymentEndingInLabel\": \"Con terminación en\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Tarjeta de crédito\",\n                    \"giftCertificate\": \"Certificado de regalo\",\n                    \"storeCredit\": \"Crédito de la tienda\",\n                    \"other\": \"Otro\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Direcciones\",\n            \"cta\": \"Agregar dirección\",\n            \"edit\": \"Editar\",\n            \"delete\": \"Eliminar\",\n            \"cancel\": \"Cancelar\",\n            \"create\": \"Crear\",\n            \"update\": \"Actualizar\",\n            \"setDefault\": \"Establecer como predeterminado\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ninguna dirección\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"addressLine1Required\": \"La línea 1 es obligatoria\",\n                \"cityRequired\": \"La ciudad es obligatoria\",\n                \"countryRequired\": \"País es un campo obligatorio\",\n                \"stateRequired\": \"Estado/Provincia es un campo obligatorio.\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio.\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"CONFIGURACIÓN DE CUENTA\",\n            \"changePassword\": \"Cambiar contraseña\",\n            \"passwordUpdated\": \"¡La contraseña se actualizó correctamente!\",\n            \"settingsUpdated\": \"La configuración de la cuenta se actualizó correctamente.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"currentPassword\": \"Contraseña actual\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"cta\": \"Actualizar\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Preferencias de marketing\",\n                \"label\": \"Suscríbete a nuestro boletín informativo.\",\n                \"marketingPreferencesUpdated\": \"¡Las preferencias de marketing se actualizaron con éxito!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"firstNameTooSmall\": \"El nombre de pila debe tener al menos 2 caracteres\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"lastNameTooSmall\": \"El apellido debe tener al menos 2 caracteres\",\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"currentPasswordRequired\": \"La contraseña actual es obligatoria\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Por favor, confirme tu contraseña\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Acciones de la lista de deseos\",\n        \"title\": \"Listas de deseos\",\n        \"new\": \"Nueva lista de deseos\",\n        \"items\": \"{count, plural, =1 {1 artículo} other {# artículos}}\",\n        \"viewWishlist\": \"Ver lista\",\n        \"noWishlists\": \"No tienes ninguna lista de deseos\",\n        \"noWishlistsCallToAction\": \"Crea una lista de deseos\",\n        \"emptyWishlist\": \"No has agregado productos a esta lista de deseos.\",\n        \"share\": \"Compartir\",\n        \"shareSuccess\": \"La lista de deseos se compartió correctamente.\",\n        \"shareCopied\": \"La URL pública de la lista de deseos se copió a tu portapapeles.\",\n        \"shareDisabled\": \"Tu lista de deseos debe ser pública para que puedas compartirla.\",\n        \"makePublic\": \"Haz público\",\n        \"makePrivate\": \"Configurar como privada\",\n        \"rename\": \"Cambiar el nombre\",\n        \"delete\": \"Eliminar\",\n        \"removeButtonTitle\": \"Eliminar el producto de tu lista de deseos\",\n        \"Visibility\": {\n            \"public\": \"Público\",\n            \"private\": \"Privado\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Cancelar\",\n            \"close\": \"Cerrar\",\n            \"copy\": \"Copiar\",\n            \"create\": \"Crear\",\n            \"save\": \"Guardar\",\n            \"delete\": \"Eliminar\",\n            \"newTitle\": \"Crea una nueva lista de deseos\",\n            \"shareTitle\": \"Comparte {name}\",\n            \"renameTitle\": \"Renombrar {name}\",\n            \"deleteTitle\": \"Borrar {name}\",\n            \"changeVisibilityPublicTitle\": \"¿Hacer {name} público?\",\n            \"changeVisibilityPrivateTitle\": \"¿Hacer {name} privado?\",\n            \"makePublicContent\": \"¿Confirmas que quieres configurar <bold>{name}</bold> como pública? Si lo haces, otras personas podrán ver tu lista de deseos si tienen un enlace para acceder a ella.\",\n            \"makePrivateContent\": \"¿Confirmas que quieres configurar <bold>{name}</bold> como privada? Las personas con quienes hayas compartido tu lista de deseos ya no podrán verla.\",\n            \"deleteContent\": \"¿Confirmas que quieres eliminar <bold>{name}</bold>? Esta acción no puede deshacerse.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Nombre\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"El nombre de la lista de deseos no puede estar vacío.\",\n            \"updateFailed\": \"No se pudo actualizar tu lista de deseos. Vuelve a intentarlo.\",\n            \"deleteFailed\": \"No se pudo eliminar tu lista de deseos. Vuelve a intentarlo.\",\n            \"removeProductFailed\": \"No se pudo eliminar el producto de tu lista de deseos. Vuelve a intentarlo.\",\n            \"unauthorized\": \"No tienes autorización para realizar esta acción. Inicia sesión e inténtalo de nuevo.\",\n            \"unexpected\": \"Se produjo un error inesperado, inténtalo de nuevo\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"La lista de deseos se creó correctamente.\",\n            \"updateSuccess\": \"La lista de deseos se actualizó correctamente.\",\n            \"deleteSuccess\": \"La lista de deseos se eliminó correctamente.\",\n            \"removeItemSuccess\": \"Se eliminó el artículo de tu lista de deseos.\"\n        },\n        \"Button\": {\n            \"label\": \"Agregar a la lista de deseos\",\n            \"addToNewWishlist\": \"Agregar a una nueva lista de deseos\",\n            \"defaultWishlistName\": \"Mi lista de deseos\",\n            \"addSuccessMessage\": \"El producto se ha agregado a tu lista de deseos.\",\n            \"removeSuccessMessage\": \"El producto se ha eliminado de tu lista de deseos.\",\n            \"Errors\": {\n                \"addProductFailed\": \"No se pudo agregar el producto de tu lista de deseos, inténtalo de nuevo\",\n                \"removeProductFailed\": \"No se pudo eliminar el producto de tu lista de deseos, inténtalo de nuevo\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Lista de deseos pública\",\n        \"defaultName\": \"Lista pública de deseos\",\n        \"emptyWishlist\": \"Aún no hay productos en esta lista de deseos.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Inicio\",\n        \"Empty\": {\n            \"title\": \"No se encontraron publicaciones de blog\",\n            \"subtitle\": \"Vuelve a consultar más tarde para obtener más contenido\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Compartir\",\n            \"email\": \"Correo electrónico\",\n            \"print\": \"Imprimir\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Carrito\",\n        \"heading\": \"Tu carrito\",\n        \"proceedToCheckout\": \"Continuar con el proceso de pago\",\n        \"increment\": \"Aumenta la cantidad\",\n        \"decrement\": \"Disminuir cantidad\",\n        \"removeItem\": \"Eliminar artículo\",\n        \"cartCombined\": \"Notamos que tenías artículos guardados en un carrito anterior, así que los hemos agregado a tu carrito actual.\",\n        \"cartRestored\": \"Iniciaste un carrito en otro dispositivo y lo hemos restaurado aquí para que puedas continuar desde donde lo dejaste.\",\n        \"cartUpdateInProgress\": \"Tiene una actualización de carrito en proceso. ¿Estás seguro de que quieres salir de esta página? Es posible que se pierdan sus cambios.\",\n        \"originalPrice\": \"El precio original era {price}.\",\n        \"currentPrice\": \"El precio actual es {price}.\",\n        \"quantityReadyToShip\": \"{cantidad, número} listo para enviar\",\n        \"quantityOnBackorder\": \"{cantidad, número} estará en pedido pendiente\",\n        \"partiallyAvailable\": \"Solo {cantidad, número} disponible\",\n        \"CheckoutSummary\": {\n            \"title\": \"Resumen\",\n            \"subTotal\": \"Subtotal\",\n            \"discounts\": \"Descuentos\",\n            \"tax\": \"Impuesto\",\n            \"total\": \"Total\",\n            \"CouponCode\": {\n                \"apply\": \"Aplicar\",\n                \"couponCode\": \"Código de cupón\",\n                \"removeCouponCode\": \"Elimina el código de cupón\",\n                \"invalidCouponCode\": \"Ingresa un código de cupón válido\",\n                \"cartNotFound\": \"Se produjo un error al recuperar su carrito\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Envío\",\n                \"add\": \"Agregar\",\n                \"change\": \"Cambiar\",\n                \"cancel\": \"Cancelar\",\n                \"country\": \"País\",\n                \"city\": \"Ciudad\",\n                \"state\": \"Estado/Provincia\",\n                \"postalCode\": \"Código postal\",\n                \"updatedShippingOptions\": \"Actualiza las opciones de envío\",\n                \"viewShippingOptions\": \"Ver opciones de envío\",\n                \"editAddress\": \"Editar dirección\",\n                \"shippingOptions\": \"Opciones de envío\",\n                \"updateShipping\": \"Actualizar envío\",\n                \"addShipping\": \"Agregar envío\",\n                \"cartNotFound\": \"Se produjo un error al recuperar su carrito\",\n                \"noShippingOptions\": \"No hay opciones de envío disponibles para tu dirección\",\n                \"countryRequired\": \"País es un campo obligatorio\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Certificado de regalo\",\n            \"giftCertificateCode\": \"Código del certificado de regalo\",\n            \"removeGiftCertificate\": \"Eliminar certificado de regalo\",\n            \"apply\": \"Aplicar\",\n            \"to\": \"De\",\n            \"message\": \"Mensaje\",\n            \"invalidGiftCertificate\": \"Ingrese un código de certificado de regalo válido\",\n            \"cartNotFound\": \"Se produjo un error al recuperar su carrito\"\n        },\n        \"Empty\": {\n            \"title\": \"Su carrito está vacío.\",\n            \"subtitle\": \"Agrega algunos productos para comenzar.\",\n            \"cta\": \"Seguir comprando\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Se produjo un error al recuperar su carrito\",\n            \"lineItemNotFound\": \"Artículo no encontrado.\",\n            \"failedToUpdateQuantity\": \"No se pudo actualizar la cantidad.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Comparar productos\",\n        \"addToCart\": \"Agregar al carrito\",\n        \"next\": \"Siguientes productos\",\n        \"previous\": \"Productos anteriores\",\n        \"noProductsToCompare\": \"No hay productos para comparar\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Peso\",\n        \"description\": \"Descripción\",\n        \"noDescription\": \"No hay ninguna descripción disponible.\",\n        \"rating\": \"Calificación\",\n        \"noRatings\": \"No hay reseñas.\",\n        \"otherDetails\": \"Otros detalles\",\n        \"noOtherDetails\": \"No hay otros detalles.\",\n        \"viewOptions\": \"Opciones de visualización\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 artículo} other {# artículos}} agregado(s) a <cartLink>tu carrito</cartLink>\",\n        \"missingCart\": \"Carrito no encontrado. Vuelva a intentarlo más tarde.\",\n        \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Cantidad\",\n            \"increaseQuantity\": \"Aumenta la cantidad\",\n            \"decreaseQuantity\": \"Disminuir cantidad\",\n            \"emptySelectPlaceholder\": \"Seleccione una opción\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 artículo} other {# artículos}} agregado(s) a <cartLink>tu carrito</cartLink>\",\n            \"missingCart\": \"Carrito no encontrado. Intenta de nuevo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"variantRequiredError\": \"Para añadir este producto al carrito, debes seleccionar opciones.\",\n            \"increaseNumber\": \"Aumentar número\",\n            \"decreaseNumber\": \"Disminuir número\",\n            \"thumbnail\": \"Ver número de imagen\",\n            \"additionalInformation\": \"Información adicional\",\n            \"currentStock\": \"{cantidad, número} en stock\",\n            \"backorderQuantity\": \"{cantidad, número} estará en espera\",\n            \"loadingMoreImages\": \"Cargando más imágenes\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 imagen más cargada} other {# más imágenes cargadas}}\",\n            \"Submit\": {\n                \"addToCart\": \"Agregar al carrito\",\n                \"outOfStock\": \"Agotado/a\",\n                \"preorder\": \"Pedido anticipado\",\n                \"unavailable\": \"No disponible\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Especificaciones\",\n                \"warranty\": \"Garantía\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Peso\",\n                \"condition\": \"Condición\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Productos relacionados\",\n            \"noRelatedProducts\": \"No se encontraron productos relacionados\",\n            \"browseCatalog\": \"Intenta navegar por nuestro catálogo completo de productos.\",\n            \"cta\": \"Comprar todos\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\",\n            \"scrollbar\": \"Barra de desplazamiento de productos relacionados\"\n        },\n        \"Reviews\": {\n            \"title\": \"Reseñas\",\n            \"empty\": \"No se han agregado reseñas para este producto.\",\n            \"previous\": \"Reseñas anteriores\",\n            \"next\": \"Próximas reseñas\",\n            \"Form\": {\n                \"button\": \"Escribe una opinión\",\n                \"title\": \"Escribe una opinión\",\n                \"submit\": \"Enviar\",\n                \"cancel\": \"Cancelar\",\n                \"ratingLabel\": \"Calificación\",\n                \"titleLabel\": \"Título\",\n                \"reviewLabel\": \"Revisar\",\n                \"nameLabel\": \"Nombre\",\n                \"emailLabel\": \"Correo electrónico\",\n                \"successMessage\": \"¡Tu reseña fue enviada con éxito!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Se requiere título\",\n                    \"authorRequired\": \"El campo Nombre es obligatorio\",\n                    \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                    \"textRequired\": \"Es necesario revisar\",\n                    \"ratingRequired\": \"Se requiere una calificación\",\n                    \"ratingTooSmall\": \"La puntuación debe ser al menos 1\",\n                    \"ratingTooLarge\": \"El puntaje debe ser como máximo 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Inicio\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Inicio\",\n            \"Form\": {\n                \"success\": \"Gracias por ponerte en contacto. Nos comunicaremos contigo pronto.\",\n                \"successCta\": \"Seguir comprando\",\n                \"fullName\": \"Nombre completo\",\n                \"companyName\": \"Nombre de la empresa\",\n                \"phone\": \"Teléfono\",\n                \"orderNo\": \"Número de pedido\",\n                \"rma\": \"Número de RMA (autorización de devolución)\",\n                \"email\": \"Correo electrónico\",\n                \"comments\": \"Comentarios/Preguntas\",\n                \"cta\": \"Enviar formulario\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"MANTENIMIENTO\",\n        \"message\": \"Estamos fuera de servicio por mantenimiento\",\n        \"contactUs\": \"Puedes contactarnos en:\"\n    },\n    \"Error\": {\n        \"title\": \"Hubo un error en el servidor.\",\n        \"subtitle\": \"Vuelva a intentarlo más tarde.\",\n        \"cta\": \"Volver a intentarlo\"\n    },\n    \"NotFound\": {\n        \"title\": \"No pudimos encontrar esa página.\",\n        \"subtitle\": \"Intenta buscar algo más o regresa a la página de inicio.\",\n        \"featuredProducts\": \"Productos destacados\",\n        \"search\": \"Buscar\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Inicio\",\n            \"toggleNavigation\": \"Alternar navegación\",\n            \"Icons\": {\n                \"account\": \"Perfil\",\n                \"cart\": \"Carrito\",\n                \"search\": \"Abrir ventana emergente de búsqueda\",\n                \"giftCertificates\": \"Certificados de regalo\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Cambiar moneda\",\n                \"invalidCurrency\": \"Moneda inválida\",\n                \"errorUpdatingCurrency\": \"Error al actualizar la moneda de tu carrito. Vuelve a intentarlo.\"\n            },\n            \"Search\": {\n                \"products\": \"Productos\",\n                \"categories\": \"Categorías\",\n                \"brands\": \"Marcas\",\n                \"noSearchResultsTitle\": \"Lo sentimos, no hay resultados para “{term}”.\",\n                \"noSearchResultsSubtitle\": \"Prueba con otra búsqueda.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo.\",\n                \"inputPlaceholder\": \"Buscar productos, categorías, marcas...\",\n                \"submitLabel\": \"Buscar\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Inicio\",\n            \"contactUs\": \"Contacto\",\n            \"socialMediaLinks\": \"Enlaces a redes sociales\",\n            \"categories\": \"Categorías\",\n            \"brands\": \"Marcas\",\n            \"navigate\": \"Navegar\",\n            \"giftCertificates\": \"Certificados de regalo\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Suscríbete a nuestro boletín informativo\",\n            \"placeholder\": \"Ingresa tu correo electrónico\",\n            \"description\": \"Mantente al día con las últimas noticias y ofertas de nuestra tienda.\",\n            \"subscribedToNewsletter\": \"¡Te suscribiste a nuestro boletín!\",\n            \"Errors\": {\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"invalidEmail\": \"Por favor, introduzca una dirección de email válida\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Rechazar todo\",\n                \"acceptAll\": \"Aceptar todo\",\n                \"customize\": \"Personalizar\",\n                \"save\": \"Guardar la configuración\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"Valoramos su privacidad\",\n                \"description\": \"Este sitio emplea cookies para mejorar su experiencia de navegación, analizar el tráfico del sitio y mostrar contenido personalizado.\",\n                \"privacyPolicy\": \"Política de Privacidad\"\n            },\n            \"Dialog\": {\n                \"title\": \"Configuración de privacidad\",\n                \"description\": \"Personaliza tu configuración de privacidad aquí. Puede elegir qué tipos de cookies y tecnologías de seguimiento le gustaría permitir.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Estrictamente necesario\",\n                    \"description\": \"Estas cookies son Essentials para que el sitio web funcione correctamente y no se pueden desactivar.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Funcionalidad\",\n                    \"description\": \"Estas cookies permiten mejorar la funcionalidad y la personalización del sitio web.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"Estas cookies se emplean para ofrecer anuncios relevantes y realizar un seguimiento de su efectividad.\"\n                },\n                \"measurement\": {\n                    \"title\": \"DATOS ANALÍTICOS\",\n                    \"description\": \"Estas cookies nos ayudan a comprender cómo interactúan los visitantes con el sitio web y mejorar su rendimiento.\"\n                },\n                \"experience\": {\n                    \"title\": \"Experiencia\",\n                    \"description\": \"Estas cookies nos ayudan a brindar una mejor experiencia de usuario y probar nuevas funciones.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"El precio original era {price}.\",\n            \"currentPrice\": \"El precio actual es {price}.\",\n            \"range\": \"Precio de {minValue} a {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Certificados de regalo\",\n        \"description\": \"Regala el regalo perfecto que nunca pasa de moda. Deja que tus colegas y seres queridos elijan exactamente lo que quieren de toda nuestra colección.\",\n        \"purchaseLabel\": \"Comprar ahora\",\n        \"checkBalanceLabel\": \"Consultar saldo\",\n        \"expiresAtLabel\": \"Válido hasta\",\n        \"CheckBalance\": {\n            \"title\": \"Consultar saldo\",\n            \"description\": \"Puede consultar el saldo y obtener la información sobre su certificado de regalo escribiendo el código en el cuadro a continuación.\",\n            \"inputLabel\": \"Código\",\n            \"inputPlaceholder\": \"xxx-xxx-xxx-xxx\",\n            \"purchasedDateLabel\": \"Comprado\",\n            \"senderLabel\": \"Para\",\n            \"Errors\": {\n                \"invalidCode\": \"El código del certificado de regalo que ingresó no es válido. Verifique el código e inténtelo de nuevo.\",\n                \"codeRequired\": \"Ingresa el código del certificado de regalo.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Comprar certificado de regalo\",\n            \"title\": \"Certificado de regalo digital\",\n            \"description\": \"Explore nuestros certificados de regalo, perfectos para cualquier ocasión. Elige la cantidad y personaliza tu mensaje.\",\n            \"successMessage\": \"Se ha agregado un certificado de regalo a <cartLink> su carrito</cartLink>\",\n            \"missingCart\": \"Carrito no encontrado. Vuelva a intentarlo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"Form\": {\n                \"amountLabel\": \"Monto\",\n                \"customAmountLabel\": \"Importe (entre {minAmount} y {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Selecciona una cantidad\",\n                \"customAmountPlaceholder\": \"Ingrese la cantidad personalizada\",\n                \"senderNameLabel\": \"Tu nombre\",\n                \"senderEmailLabel\": \"Tu correo electrónico\",\n                \"recipientNameLabel\": \"Nombre del destinatario\",\n                \"recipientEmailLabel\": \"Correo electrónico del destinatario\",\n                \"namePlaceholder\": \"Ingrese el nombre\",\n                \"emailPlaceholder\": \"Ingresa el correo electrónico\",\n                \"messageLabel\": \"Mensaje\",\n                \"messagePlaceholder\": \"Ingrese su mensaje (opcional)\",\n                \"nonRefundableCheckboxLabel\": \"Acepto que los certificados de regalo no son reembolsables\",\n                \"expiryCheckboxLabel\": \"Reconozco que este Certificado de Regalo caducará el {expiryDate}\",\n                \"ctaLabel\": \"Agregar al carrito\",\n                \"Errors\": {\n                    \"amountRequired\": \"Por favor, selecciona o introduce el importe de un vale regalo\",\n                    \"amountInvalid\": \"Por favor, seleccione un importe válido de un certificado regalo\",\n                    \"amountOutOfRange\": \"Por favor, introduzca una cantidad entre {minAmount} y {maxAmount}\",\n                    \"unexpectedSettingsError\": \"Se produjo un error inesperado al recuperar la configuración del certificado de regalo. Intente de nuevo más tarde.\",\n                    \"senderNameRequired\": \"Tu nombre es obligatorio\",\n                    \"senderEmailRequired\": \"Tu correo electrónico es obligatorio\",\n                    \"recipientNameRequired\": \"Se requiere el nombre del destinatario\",\n                    \"recipientEmailRequired\": \"Se requiere el email del destinatario\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                    \"checkboxRequired\": \"Debes marcar esta casilla para continuar\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"Opcional\",\n        \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n        \"Errors\": {\n            \"invalidInput\": \"Por favor, revisa tu opinión y vuelve a intentarlo\",\n            \"invalidFormat\": \"El valor introducido no coincide con el formato requerido\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/es-CL.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Nuevos hallazgos para cada ocasión\",\n                \"description\": \"Explora nuestras últimas novedades seleccionadas para ofrecerte estilo, funcionalidad e inspiración. Compra ahora y descubre tu próximo favorito.\",\n                \"alt\": \"Cinco plantas pequeñas en macetas exhibidas sobre bloques apilados de color beige, con una variedad de follaje verde en macetas de color gris oscuro contra un fondo neutro.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide02\": {\n                \"title\": \"Descubre las novedades\",\n                \"description\": \"Compra nuestras últimas novedades y encuentra algo fresco y emocionante para tu hogar.\",\n                \"alt\": \"Manos extendiéndose para sostener un helecho verde en una canasta tejida con un lazo decorativo, sobre un fondo beige con sombras suaves.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide03\": {\n                \"title\": \"Algo para todos\",\n                \"description\": \"No te pierdas las ofertas exclusivas en nuestros productos más vendidos. Compra hoy y ahorra mucho dinero en los artículos que te gustan.\",\n                \"alt\": \"Primer plano de una hoja verde vibrante con perforaciones, que muestra su textura suave y detalles naturales.\",\n                \"cta\": \"Comprar ahora\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Colección destacada\",\n            \"description\": \"Explora nuestras mejores selecciones en esta colección destacada. ¡Encuentra el regalo perfecto o date un capricho!\",\n            \"cta\": \"Ver más\",\n            \"emptyStateTitle\": \"No se encontraron productos\",\n            \"emptyStateSubtitle\": \"Intenta navegar por nuestro catálogo completo de productos.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Novedades\",\n            \"description\": \"Nuestros últimos productos están aquí. Descubre lo nuevo de la tienda.\",\n            \"cta\": \"Ver todas\",\n            \"emptyStateTitle\": \"No se encontraron productos\",\n            \"emptyStateSubtitle\": \"Intenta navegar por nuestro catálogo completo de productos.\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Cambiar contraseña\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"passwordUpdated\": \"¡La contraseña se actualizó correctamente!\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Por favor, confirme tu contraseña\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Inicio de sesión\",\n            \"heading\": \"Inicio de sesión\",\n            \"forgotPassword\": \"¿Olvidaste tu contraseña?\",\n            \"cta\": \"Inicio de sesión\",\n            \"email\": \"Correo electrónico\",\n            \"password\": \"Contraseña\",\n            \"invalidCredentials\": \"Tu dirección de correo electrónico o tu contraseña son incorrectas. Intenta iniciar sesión de nuevo o restablece tu contraseña\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"passwordResetRequired\": \"Se requiere restablecimiento de contraseña. Por favor, revisa tu email para ver instrucciones para restablecer tu contraseña.\",\n            \"invalidToken\": \"Tu enlace de acceso es inválido o caducó. Por favor, intenta iniciar sesión de nuevo.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"invalidInput\": \"Por favor, revisa tu opinión y vuelve a intentarlo.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"¿Nuevo cliente?\",\n                \"accountBenefits\": \"Crea una cuenta con nosotros y podrás:\",\n                \"fastCheckout\": \"Pagar más rápido\",\n                \"multipleAddresses\": \"Guardar varias direcciones de envío\",\n                \"ordersHistory\": \"Acceder a tu historial de pedidos\",\n                \"ordersTracking\": \"Rastrear nuevos pedidos\",\n                \"wishlists\": \"Guardar artículos en tu lista de deseos\",\n                \"cta\": \"Crear cuenta\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Olvidé la contraseña\",\n                \"subtitle\": \"A continuación, ingresa el correo electrónico asociado a tu cuenta. Te enviaremos instrucciones para restablecer tu contraseña.\",\n                \"confirmResetPassword\": \"Si la dirección de correo electrónico {email} está vinculada a una cuenta en nuestra tienda, te hemos enviado un correo electrónico de restablecimiento de contraseña. Revisa tu bandeja de entrada y tu carpeta de correo no deseado si no lo ves.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Registrar cuenta\",\n            \"heading\": \"Cuenta nueva\",\n            \"cta\": \"Crear cuenta\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"addressLine1Required\": \"La línea 1 es obligatoria\",\n                \"cityRequired\": \"La ciudad es obligatoria\",\n                \"countryRequired\": \"País es un campo obligatorio\",\n                \"stateRequired\": \"Estado/Provincia es un campo obligatorio.\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio.\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"No hay productos en esta marca\",\n                \"subtitle\": \"Intenta usar diferentes filtros.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Categorías\",\n            \"Empty\": {\n                \"title\": \"Ningún producto en esta categoría\",\n                \"subtitle\": \"Intenta usar diferentes filtros.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Resultados de búsqueda\",\n            \"searchResults\": \"Resultados de búsqueda para\",\n            \"subCategories\": \"Categorías\",\n            \"Breadcrumbs\": {\n                \"home\": \"Inicio\",\n                \"search\": \"Buscar\"\n            },\n            \"Empty\": {\n                \"title\": \"Lo sentimos, no hay resultados para “{term}”.\",\n                \"subtitle\": \"Prueba con otra búsqueda.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtros\",\n            \"resetFilters\": \"Restablecer filtros\",\n            \"Range\": {\n                \"apply\": \"Aplicar\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Envío gratuito\",\n                \"isFeaturedLabel\": \"Destacado\",\n                \"inStockLabel\": \"En existencias\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Ordenar por:\",\n            \"featuredItems\": \"Elementos destacados\",\n            \"bestSellingItems\": \"Artículos más vendidos\",\n            \"newestItems\": \"Artículos más nuevos\",\n            \"aToZ\": \"A a la Z\",\n            \"zToA\": \"Z a la A\",\n            \"byReview\": \"Por opinión\",\n            \"priceAscending\": \"Precio: ascendente\",\n            \"priceDescending\": \"Precio: descendente\",\n            \"relevance\": \"Relevancia\"\n        },\n        \"Compare\": {\n            \"compare\": \"Comparar\",\n            \"remove\": \"Quitar\",\n            \"maxCompareLimit\": \"Alcanzaste el número máximo de productos para comparar. Elimina un producto para agregar uno nuevo.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Direcciones\",\n            \"logout\": \"Cerrar sesión\",\n            \"orders\": \"Pedidos\",\n            \"settings\": \"Cuenta\",\n            \"wishlists\": \"Listas de deseos\"\n        },\n        \"Orders\": {\n            \"title\": \"Pedidos\",\n            \"orderNumber\": \"Núm. de pedido\",\n            \"totalPrice\": \"Total\",\n            \"viewDetails\": \"Ver detalles\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ningún pedido\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Details\": {\n                \"title\": \"Pedido núm.{orderNumber}\",\n                \"shippingAddress\": \"Dirección de envío\",\n                \"shippingMethod\": \"Método de envío\",\n                \"summaryTotal\": \"Total\",\n                \"destination\": \"Destino\",\n                \"destinationWithCount\": \"Destino {número, número}/{total, número}\",\n                \"digitalDelivery\": \"Entrega digital a {email}\",\n                \"subtotal\": \"Subtotal\",\n                \"shipping\": \"Envío\",\n                \"tax\": \"Impuesto\",\n                \"orderSummary\": \"Resumen del pedido\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Método de pago} other {Métodos de pago}}\",\n                \"paymentEndingInLabel\": \"Con terminación en\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Tarjeta de crédito\",\n                    \"giftCertificate\": \"Certificado de regalo\",\n                    \"storeCredit\": \"Crédito de la tienda\",\n                    \"other\": \"Otro\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Direcciones\",\n            \"cta\": \"Agregar dirección\",\n            \"edit\": \"Editar\",\n            \"delete\": \"Eliminar\",\n            \"cancel\": \"Cancelar\",\n            \"create\": \"Crear\",\n            \"update\": \"Actualizar\",\n            \"setDefault\": \"Establecer como predeterminado\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ninguna dirección\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"addressLine1Required\": \"La línea 1 es obligatoria\",\n                \"cityRequired\": \"La ciudad es obligatoria\",\n                \"countryRequired\": \"País es un campo obligatorio\",\n                \"stateRequired\": \"Estado/Provincia es un campo obligatorio.\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio.\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"CONFIGURACIÓN DE CUENTA\",\n            \"changePassword\": \"Cambiar contraseña\",\n            \"passwordUpdated\": \"¡La contraseña se actualizó correctamente!\",\n            \"settingsUpdated\": \"La configuración de la cuenta se actualizó correctamente.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"currentPassword\": \"Contraseña actual\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"cta\": \"Actualizar\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Preferencias de marketing\",\n                \"label\": \"Suscríbete a nuestro boletín informativo.\",\n                \"marketingPreferencesUpdated\": \"¡Las preferencias de marketing se actualizaron con éxito!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"firstNameTooSmall\": \"El nombre de pila debe tener al menos 2 caracteres\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"lastNameTooSmall\": \"El apellido debe tener al menos 2 caracteres\",\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"currentPasswordRequired\": \"La contraseña actual es obligatoria\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Por favor, confirme tu contraseña\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Acciones de la lista de deseos\",\n        \"title\": \"Listas de deseos\",\n        \"new\": \"Nueva lista de deseos\",\n        \"items\": \"{count, plural, =1 {1 artículo} other {# artículos}}\",\n        \"viewWishlist\": \"Ver lista\",\n        \"noWishlists\": \"No tienes ninguna lista de deseos\",\n        \"noWishlistsCallToAction\": \"Crea una lista de deseos\",\n        \"emptyWishlist\": \"No has agregado productos a esta lista de deseos.\",\n        \"share\": \"Compartir\",\n        \"shareSuccess\": \"La lista de deseos se compartió correctamente.\",\n        \"shareCopied\": \"La URL pública de la lista de deseos se copió a tu portapapeles.\",\n        \"shareDisabled\": \"Tu lista de deseos debe ser pública para que puedas compartirla.\",\n        \"makePublic\": \"Haz público\",\n        \"makePrivate\": \"Configurar como privada\",\n        \"rename\": \"Cambiar el nombre\",\n        \"delete\": \"Eliminar\",\n        \"removeButtonTitle\": \"Eliminar el producto de tu lista de deseos\",\n        \"Visibility\": {\n            \"public\": \"Público\",\n            \"private\": \"Privado\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Cancelar\",\n            \"close\": \"Cerrar\",\n            \"copy\": \"Copiar\",\n            \"create\": \"Crear\",\n            \"save\": \"Guardar\",\n            \"delete\": \"Eliminar\",\n            \"newTitle\": \"Crea una nueva lista de deseos\",\n            \"shareTitle\": \"Comparte {name}\",\n            \"renameTitle\": \"Renombrar {name}\",\n            \"deleteTitle\": \"Borrar {name}\",\n            \"changeVisibilityPublicTitle\": \"¿Hacer {name} público?\",\n            \"changeVisibilityPrivateTitle\": \"¿Hacer {name} privado?\",\n            \"makePublicContent\": \"¿Confirmas que quieres configurar <bold>{name}</bold> como pública? Si lo haces, otras personas podrán ver tu lista de deseos si tienen un enlace para acceder a ella.\",\n            \"makePrivateContent\": \"¿Confirmas que quieres configurar <bold>{name}</bold> como privada? Las personas con quienes hayas compartido tu lista de deseos ya no podrán verla.\",\n            \"deleteContent\": \"¿Confirmas que quieres eliminar <bold>{name}</bold>? Esta acción no puede deshacerse.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Nombre\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"El nombre de la lista de deseos no puede estar vacío.\",\n            \"updateFailed\": \"No se pudo actualizar tu lista de deseos. Vuelve a intentarlo.\",\n            \"deleteFailed\": \"No se pudo eliminar tu lista de deseos. Vuelve a intentarlo.\",\n            \"removeProductFailed\": \"No se pudo eliminar el producto de tu lista de deseos. Vuelve a intentarlo.\",\n            \"unauthorized\": \"No tienes autorización para realizar esta acción. Inicia sesión e inténtalo de nuevo.\",\n            \"unexpected\": \"Se produjo un error inesperado, inténtalo de nuevo\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"La lista de deseos se creó correctamente.\",\n            \"updateSuccess\": \"La lista de deseos se actualizó correctamente.\",\n            \"deleteSuccess\": \"La lista de deseos se eliminó correctamente.\",\n            \"removeItemSuccess\": \"Se eliminó el artículo de tu lista de deseos.\"\n        },\n        \"Button\": {\n            \"label\": \"Agregar a la lista de deseos\",\n            \"addToNewWishlist\": \"Agregar a una nueva lista de deseos\",\n            \"defaultWishlistName\": \"Mi lista de deseos\",\n            \"addSuccessMessage\": \"El producto se ha agregado a tu lista de deseos.\",\n            \"removeSuccessMessage\": \"El producto se ha eliminado de tu lista de deseos.\",\n            \"Errors\": {\n                \"addProductFailed\": \"No se pudo agregar el producto de tu lista de deseos, inténtalo de nuevo\",\n                \"removeProductFailed\": \"No se pudo eliminar el producto de tu lista de deseos, inténtalo de nuevo\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Lista de deseos pública\",\n        \"defaultName\": \"Lista pública de deseos\",\n        \"emptyWishlist\": \"Aún no hay productos en esta lista de deseos.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Inicio\",\n        \"Empty\": {\n            \"title\": \"No se encontraron publicaciones de blog\",\n            \"subtitle\": \"Vuelve a consultar más tarde para obtener más contenido\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Compartir\",\n            \"email\": \"Correo electrónico\",\n            \"print\": \"Imprimir\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Carrito\",\n        \"heading\": \"Tu carrito\",\n        \"proceedToCheckout\": \"Continuar con el proceso de pago\",\n        \"increment\": \"Aumenta la cantidad\",\n        \"decrement\": \"Disminuir cantidad\",\n        \"removeItem\": \"Eliminar artículo\",\n        \"cartCombined\": \"Notamos que tenías artículos guardados en un carrito anterior, así que los hemos agregado a tu carrito actual.\",\n        \"cartRestored\": \"Iniciaste un carrito en otro dispositivo y lo hemos restaurado aquí para que puedas continuar desde donde lo dejaste.\",\n        \"cartUpdateInProgress\": \"Tiene una actualización de carrito en proceso. ¿Estás seguro de que quieres salir de esta página? Es posible que se pierdan sus cambios.\",\n        \"originalPrice\": \"El precio original era {price}.\",\n        \"currentPrice\": \"El precio actual es {price}.\",\n        \"quantityReadyToShip\": \"{cantidad, número} listo para enviar\",\n        \"quantityOnBackorder\": \"{cantidad, número} estará en pedido pendiente\",\n        \"partiallyAvailable\": \"Solo {cantidad, número} disponible\",\n        \"CheckoutSummary\": {\n            \"title\": \"Resumen\",\n            \"subTotal\": \"Subtotal\",\n            \"discounts\": \"Descuentos\",\n            \"tax\": \"Impuesto\",\n            \"total\": \"Total\",\n            \"CouponCode\": {\n                \"apply\": \"Aplicar\",\n                \"couponCode\": \"Código de cupón\",\n                \"removeCouponCode\": \"Elimina el código de cupón\",\n                \"invalidCouponCode\": \"Ingresa un código de cupón válido\",\n                \"cartNotFound\": \"Se produjo un error al recuperar su carrito\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Envío\",\n                \"add\": \"Agregar\",\n                \"change\": \"Cambiar\",\n                \"cancel\": \"Cancelar\",\n                \"country\": \"País\",\n                \"city\": \"Ciudad\",\n                \"state\": \"Estado/Provincia\",\n                \"postalCode\": \"Código postal\",\n                \"updatedShippingOptions\": \"Actualiza las opciones de envío\",\n                \"viewShippingOptions\": \"Ver opciones de envío\",\n                \"editAddress\": \"Editar dirección\",\n                \"shippingOptions\": \"Opciones de envío\",\n                \"updateShipping\": \"Actualizar envío\",\n                \"addShipping\": \"Agregar envío\",\n                \"cartNotFound\": \"Se produjo un error al recuperar su carrito\",\n                \"noShippingOptions\": \"No hay opciones de envío disponibles para tu dirección\",\n                \"countryRequired\": \"País es un campo obligatorio\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Certificado de regalo\",\n            \"giftCertificateCode\": \"Código del certificado de regalo\",\n            \"removeGiftCertificate\": \"Eliminar certificado de regalo\",\n            \"apply\": \"Aplicar\",\n            \"to\": \"De\",\n            \"message\": \"Mensaje\",\n            \"invalidGiftCertificate\": \"Ingrese un código de certificado de regalo válido\",\n            \"cartNotFound\": \"Se produjo un error al recuperar su carrito\"\n        },\n        \"Empty\": {\n            \"title\": \"Su carrito está vacío.\",\n            \"subtitle\": \"Agrega algunos productos para comenzar.\",\n            \"cta\": \"Seguir comprando\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Se produjo un error al recuperar su carrito\",\n            \"lineItemNotFound\": \"Artículo no encontrado.\",\n            \"failedToUpdateQuantity\": \"No se pudo actualizar la cantidad.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Comparar productos\",\n        \"addToCart\": \"Agregar al carrito\",\n        \"next\": \"Siguientes productos\",\n        \"previous\": \"Productos anteriores\",\n        \"noProductsToCompare\": \"No hay productos para comparar\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Peso\",\n        \"description\": \"Descripción\",\n        \"noDescription\": \"No hay ninguna descripción disponible.\",\n        \"rating\": \"Calificación\",\n        \"noRatings\": \"No hay reseñas.\",\n        \"otherDetails\": \"Otros detalles\",\n        \"noOtherDetails\": \"No hay otros detalles.\",\n        \"viewOptions\": \"Opciones de visualización\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 artículo} other {# artículos}} agregado(s) a <cartLink>tu carrito</cartLink>\",\n        \"missingCart\": \"Carrito no encontrado. Vuelva a intentarlo más tarde.\",\n        \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Cantidad\",\n            \"increaseQuantity\": \"Aumenta la cantidad\",\n            \"decreaseQuantity\": \"Disminuir cantidad\",\n            \"emptySelectPlaceholder\": \"Seleccione una opción\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 artículo} other {# artículos}} agregado(s) a <cartLink>tu carrito</cartLink>\",\n            \"missingCart\": \"Carrito no encontrado. Intenta de nuevo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"variantRequiredError\": \"Para añadir este producto al carrito, debes seleccionar opciones.\",\n            \"increaseNumber\": \"Aumentar número\",\n            \"decreaseNumber\": \"Disminuir número\",\n            \"thumbnail\": \"Ver número de imagen\",\n            \"additionalInformation\": \"Información adicional\",\n            \"currentStock\": \"{cantidad, número} en stock\",\n            \"backorderQuantity\": \"{cantidad, número} estará en espera\",\n            \"loadingMoreImages\": \"Cargando más imágenes\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 imagen más cargada} other {# más imágenes cargadas}}\",\n            \"Submit\": {\n                \"addToCart\": \"Agregar al carrito\",\n                \"outOfStock\": \"Agotado/a\",\n                \"preorder\": \"Pedido anticipado\",\n                \"unavailable\": \"No disponible\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Especificaciones\",\n                \"warranty\": \"Garantía\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Peso\",\n                \"condition\": \"Condición\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Productos relacionados\",\n            \"noRelatedProducts\": \"No se encontraron productos relacionados\",\n            \"browseCatalog\": \"Intenta navegar por nuestro catálogo completo de productos.\",\n            \"cta\": \"Comprar todos\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\",\n            \"scrollbar\": \"Barra de desplazamiento de productos relacionados\"\n        },\n        \"Reviews\": {\n            \"title\": \"Reseñas\",\n            \"empty\": \"No se han agregado reseñas para este producto.\",\n            \"previous\": \"Reseñas anteriores\",\n            \"next\": \"Próximas reseñas\",\n            \"Form\": {\n                \"button\": \"Escribe una opinión\",\n                \"title\": \"Escribe una opinión\",\n                \"submit\": \"Enviar\",\n                \"cancel\": \"Cancelar\",\n                \"ratingLabel\": \"Calificación\",\n                \"titleLabel\": \"Título\",\n                \"reviewLabel\": \"Revisar\",\n                \"nameLabel\": \"Nombre\",\n                \"emailLabel\": \"Correo electrónico\",\n                \"successMessage\": \"¡Tu reseña fue enviada con éxito!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Se requiere título\",\n                    \"authorRequired\": \"El campo Nombre es obligatorio\",\n                    \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                    \"textRequired\": \"Es necesario revisar\",\n                    \"ratingRequired\": \"Se requiere una calificación\",\n                    \"ratingTooSmall\": \"La puntuación debe ser al menos 1\",\n                    \"ratingTooLarge\": \"El puntaje debe ser como máximo 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Inicio\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Inicio\",\n            \"Form\": {\n                \"success\": \"Gracias por ponerte en contacto. Nos comunicaremos contigo pronto.\",\n                \"successCta\": \"Seguir comprando\",\n                \"fullName\": \"Nombre completo\",\n                \"companyName\": \"Nombre de la empresa\",\n                \"phone\": \"Teléfono\",\n                \"orderNo\": \"Número de pedido\",\n                \"rma\": \"Número de RMA (autorización de devolución)\",\n                \"email\": \"Correo electrónico\",\n                \"comments\": \"Comentarios/Preguntas\",\n                \"cta\": \"Enviar formulario\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"MANTENIMIENTO\",\n        \"message\": \"Estamos fuera de servicio por mantenimiento\",\n        \"contactUs\": \"Puedes contactarnos en:\"\n    },\n    \"Error\": {\n        \"title\": \"Hubo un error en el servidor.\",\n        \"subtitle\": \"Vuelva a intentarlo más tarde.\",\n        \"cta\": \"Volver a intentarlo\"\n    },\n    \"NotFound\": {\n        \"title\": \"No pudimos encontrar esa página.\",\n        \"subtitle\": \"Intenta buscar algo más o regresa a la página de inicio.\",\n        \"featuredProducts\": \"Productos destacados\",\n        \"search\": \"Buscar\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Inicio\",\n            \"toggleNavigation\": \"Alternar navegación\",\n            \"Icons\": {\n                \"account\": \"Perfil\",\n                \"cart\": \"Carrito\",\n                \"search\": \"Abrir ventana emergente de búsqueda\",\n                \"giftCertificates\": \"Certificados de regalo\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Cambiar moneda\",\n                \"invalidCurrency\": \"Moneda inválida\",\n                \"errorUpdatingCurrency\": \"Error al actualizar la moneda de tu carrito. Vuelve a intentarlo.\"\n            },\n            \"Search\": {\n                \"products\": \"Productos\",\n                \"categories\": \"Categorías\",\n                \"brands\": \"Marcas\",\n                \"noSearchResultsTitle\": \"Lo sentimos, no hay resultados para “{term}”.\",\n                \"noSearchResultsSubtitle\": \"Prueba con otra búsqueda.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo.\",\n                \"inputPlaceholder\": \"Buscar productos, categorías, marcas...\",\n                \"submitLabel\": \"Buscar\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Inicio\",\n            \"contactUs\": \"Contacto\",\n            \"socialMediaLinks\": \"Enlaces a redes sociales\",\n            \"categories\": \"Categorías\",\n            \"brands\": \"Marcas\",\n            \"navigate\": \"Navegar\",\n            \"giftCertificates\": \"Certificados de regalo\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Suscríbete a nuestro boletín informativo\",\n            \"placeholder\": \"Ingresa tu correo electrónico\",\n            \"description\": \"Mantente al día con las últimas noticias y ofertas de nuestra tienda.\",\n            \"subscribedToNewsletter\": \"¡Te suscribiste a nuestro boletín!\",\n            \"Errors\": {\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"invalidEmail\": \"Por favor, introduzca una dirección de email válida\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Rechazar todo\",\n                \"acceptAll\": \"Aceptar todo\",\n                \"customize\": \"Personalizar\",\n                \"save\": \"Guardar la configuración\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"Valoramos su privacidad\",\n                \"description\": \"Este sitio emplea cookies para mejorar su experiencia de navegación, analizar el tráfico del sitio y mostrar contenido personalizado.\",\n                \"privacyPolicy\": \"Política de Privacidad\"\n            },\n            \"Dialog\": {\n                \"title\": \"Configuración de privacidad\",\n                \"description\": \"Personaliza tu configuración de privacidad aquí. Puede elegir qué tipos de cookies y tecnologías de seguimiento le gustaría permitir.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Estrictamente necesario\",\n                    \"description\": \"Estas cookies son Essentials para que el sitio web funcione correctamente y no se pueden desactivar.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Funcionalidad\",\n                    \"description\": \"Estas cookies permiten mejorar la funcionalidad y la personalización del sitio web.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"Estas cookies se emplean para ofrecer anuncios relevantes y realizar un seguimiento de su efectividad.\"\n                },\n                \"measurement\": {\n                    \"title\": \"DATOS ANALÍTICOS\",\n                    \"description\": \"Estas cookies nos ayudan a comprender cómo interactúan los visitantes con el sitio web y mejorar su rendimiento.\"\n                },\n                \"experience\": {\n                    \"title\": \"Experiencia\",\n                    \"description\": \"Estas cookies nos ayudan a brindar una mejor experiencia de usuario y probar nuevas funciones.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"El precio original era {price}.\",\n            \"currentPrice\": \"El precio actual es {price}.\",\n            \"range\": \"Precio de {minValue} a {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Certificados de regalo\",\n        \"description\": \"Regala el regalo perfecto que nunca pasa de moda. Deja que tus colegas y seres queridos elijan exactamente lo que quieren de toda nuestra colección.\",\n        \"purchaseLabel\": \"Comprar ahora\",\n        \"checkBalanceLabel\": \"Consultar saldo\",\n        \"expiresAtLabel\": \"Válido hasta\",\n        \"CheckBalance\": {\n            \"title\": \"Consultar saldo\",\n            \"description\": \"Puede consultar el saldo y obtener la información sobre su certificado de regalo escribiendo el código en el cuadro a continuación.\",\n            \"inputLabel\": \"Código\",\n            \"inputPlaceholder\": \"xxx-xxx-xxx-xxx\",\n            \"purchasedDateLabel\": \"Comprado\",\n            \"senderLabel\": \"Para\",\n            \"Errors\": {\n                \"invalidCode\": \"El código del certificado de regalo que ingresó no es válido. Verifique el código e inténtelo de nuevo.\",\n                \"codeRequired\": \"Ingresa el código del certificado de regalo.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Comprar certificado de regalo\",\n            \"title\": \"Certificado de regalo digital\",\n            \"description\": \"Explore nuestros certificados de regalo, perfectos para cualquier ocasión. Elige la cantidad y personaliza tu mensaje.\",\n            \"successMessage\": \"Se ha agregado un certificado de regalo a <cartLink> su carrito</cartLink>\",\n            \"missingCart\": \"Carrito no encontrado. Vuelva a intentarlo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"Form\": {\n                \"amountLabel\": \"Monto\",\n                \"customAmountLabel\": \"Importe (entre {minAmount} y {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Selecciona una cantidad\",\n                \"customAmountPlaceholder\": \"Ingrese la cantidad personalizada\",\n                \"senderNameLabel\": \"Tu nombre\",\n                \"senderEmailLabel\": \"Tu correo electrónico\",\n                \"recipientNameLabel\": \"Nombre del destinatario\",\n                \"recipientEmailLabel\": \"Correo electrónico del destinatario\",\n                \"namePlaceholder\": \"Ingrese el nombre\",\n                \"emailPlaceholder\": \"Ingresa el correo electrónico\",\n                \"messageLabel\": \"Mensaje\",\n                \"messagePlaceholder\": \"Ingrese su mensaje (opcional)\",\n                \"nonRefundableCheckboxLabel\": \"Acepto que los certificados de regalo no son reembolsables\",\n                \"expiryCheckboxLabel\": \"Reconozco que este Certificado de Regalo caducará el {expiryDate}\",\n                \"ctaLabel\": \"Agregar al carrito\",\n                \"Errors\": {\n                    \"amountRequired\": \"Por favor, selecciona o introduce el importe de un vale regalo\",\n                    \"amountInvalid\": \"Por favor, seleccione un importe válido de un certificado regalo\",\n                    \"amountOutOfRange\": \"Por favor, introduzca una cantidad entre {minAmount} y {maxAmount}\",\n                    \"unexpectedSettingsError\": \"Se produjo un error inesperado al recuperar la configuración del certificado de regalo. Intente de nuevo más tarde.\",\n                    \"senderNameRequired\": \"Tu nombre es obligatorio\",\n                    \"senderEmailRequired\": \"Tu correo electrónico es obligatorio\",\n                    \"recipientNameRequired\": \"Se requiere el nombre del destinatario\",\n                    \"recipientEmailRequired\": \"Se requiere el email del destinatario\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                    \"checkboxRequired\": \"Debes marcar esta casilla para continuar\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"Opcional\",\n        \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n        \"Errors\": {\n            \"invalidInput\": \"Por favor, revisa tu opinión y vuelve a intentarlo\",\n            \"invalidFormat\": \"El valor introducido no coincide con el formato requerido\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/es-CO.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Nuevos hallazgos para cada ocasión\",\n                \"description\": \"Explora nuestras últimas novedades seleccionadas para ofrecerte estilo, funcionalidad e inspiración. Compra ahora y descubre tu próximo favorito.\",\n                \"alt\": \"Cinco plantas pequeñas en macetas exhibidas sobre bloques apilados de color beige, con una variedad de follaje verde en macetas de color gris oscuro contra un fondo neutro.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide02\": {\n                \"title\": \"Descubre las novedades\",\n                \"description\": \"Compra nuestras últimas novedades y encuentra algo fresco y emocionante para tu hogar.\",\n                \"alt\": \"Manos extendiéndose para sostener un helecho verde en una canasta tejida con un lazo decorativo, sobre un fondo beige con sombras suaves.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide03\": {\n                \"title\": \"Algo para todos\",\n                \"description\": \"No te pierdas las ofertas exclusivas en nuestros productos más vendidos. Compra hoy y ahorra mucho dinero en los artículos que te gustan.\",\n                \"alt\": \"Primer plano de una hoja verde vibrante con perforaciones, que muestra su textura suave y detalles naturales.\",\n                \"cta\": \"Comprar ahora\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Colección destacada\",\n            \"description\": \"Explora nuestras mejores selecciones en esta colección destacada. ¡Encuentra el regalo perfecto o date un capricho!\",\n            \"cta\": \"Ver más\",\n            \"emptyStateTitle\": \"No se encontraron productos\",\n            \"emptyStateSubtitle\": \"Intenta navegar por nuestro catálogo completo de productos.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Novedades\",\n            \"description\": \"Nuestros últimos productos están aquí. Descubre lo nuevo de la tienda.\",\n            \"cta\": \"Ver todas\",\n            \"emptyStateTitle\": \"No se encontraron productos\",\n            \"emptyStateSubtitle\": \"Intenta navegar por nuestro catálogo completo de productos.\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Cambiar contraseña\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"passwordUpdated\": \"¡La contraseña se actualizó correctamente!\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Por favor, confirme tu contraseña\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Inicio de sesión\",\n            \"heading\": \"Inicio de sesión\",\n            \"forgotPassword\": \"¿Olvidaste tu contraseña?\",\n            \"cta\": \"Inicio de sesión\",\n            \"email\": \"Correo electrónico\",\n            \"password\": \"Contraseña\",\n            \"invalidCredentials\": \"Tu dirección de correo electrónico o tu contraseña son incorrectas. Intenta iniciar sesión de nuevo o restablece tu contraseña\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"passwordResetRequired\": \"Se requiere restablecimiento de contraseña. Por favor, revisa tu email para ver instrucciones para restablecer tu contraseña.\",\n            \"invalidToken\": \"Tu enlace de acceso es inválido o caducó. Por favor, intenta iniciar sesión de nuevo.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"invalidInput\": \"Por favor, revisa tu opinión y vuelve a intentarlo.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"¿Nuevo cliente?\",\n                \"accountBenefits\": \"Crea una cuenta con nosotros y podrás:\",\n                \"fastCheckout\": \"Pagar más rápido\",\n                \"multipleAddresses\": \"Guardar varias direcciones de envío\",\n                \"ordersHistory\": \"Acceder a tu historial de pedidos\",\n                \"ordersTracking\": \"Rastrear nuevos pedidos\",\n                \"wishlists\": \"Guardar artículos en tu lista de deseos\",\n                \"cta\": \"Crear cuenta\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Olvidé la contraseña\",\n                \"subtitle\": \"A continuación, ingresa el correo electrónico asociado a tu cuenta. Te enviaremos instrucciones para restablecer tu contraseña.\",\n                \"confirmResetPassword\": \"Si la dirección de correo electrónico {email} está vinculada a una cuenta en nuestra tienda, te hemos enviado un correo electrónico de restablecimiento de contraseña. Revisa tu bandeja de entrada y tu carpeta de correo no deseado si no lo ves.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Registrar cuenta\",\n            \"heading\": \"Cuenta nueva\",\n            \"cta\": \"Crear cuenta\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"addressLine1Required\": \"La línea 1 es obligatoria\",\n                \"cityRequired\": \"La ciudad es obligatoria\",\n                \"countryRequired\": \"País es un campo obligatorio\",\n                \"stateRequired\": \"Estado/Provincia es un campo obligatorio.\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio.\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"No hay productos en esta marca\",\n                \"subtitle\": \"Intenta usar diferentes filtros.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Categorías\",\n            \"Empty\": {\n                \"title\": \"Ningún producto en esta categoría\",\n                \"subtitle\": \"Intenta usar diferentes filtros.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Resultados de búsqueda\",\n            \"searchResults\": \"Resultados de búsqueda para\",\n            \"subCategories\": \"Categorías\",\n            \"Breadcrumbs\": {\n                \"home\": \"Inicio\",\n                \"search\": \"Buscar\"\n            },\n            \"Empty\": {\n                \"title\": \"Lo sentimos, no hay resultados para “{term}”.\",\n                \"subtitle\": \"Prueba con otra búsqueda.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtros\",\n            \"resetFilters\": \"Restablecer filtros\",\n            \"Range\": {\n                \"apply\": \"Aplicar\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Envío gratuito\",\n                \"isFeaturedLabel\": \"Destacado\",\n                \"inStockLabel\": \"En existencias\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Ordenar por:\",\n            \"featuredItems\": \"Elementos destacados\",\n            \"bestSellingItems\": \"Artículos más vendidos\",\n            \"newestItems\": \"Artículos más nuevos\",\n            \"aToZ\": \"A a la Z\",\n            \"zToA\": \"Z a la A\",\n            \"byReview\": \"Por opinión\",\n            \"priceAscending\": \"Precio: ascendente\",\n            \"priceDescending\": \"Precio: descendente\",\n            \"relevance\": \"Relevancia\"\n        },\n        \"Compare\": {\n            \"compare\": \"Comparar\",\n            \"remove\": \"Quitar\",\n            \"maxCompareLimit\": \"Alcanzaste el número máximo de productos para comparar. Elimina un producto para agregar uno nuevo.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Direcciones\",\n            \"logout\": \"Cerrar sesión\",\n            \"orders\": \"Pedidos\",\n            \"settings\": \"Cuenta\",\n            \"wishlists\": \"Listas de deseos\"\n        },\n        \"Orders\": {\n            \"title\": \"Pedidos\",\n            \"orderNumber\": \"Núm. de pedido\",\n            \"totalPrice\": \"Total\",\n            \"viewDetails\": \"Ver detalles\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ningún pedido\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Details\": {\n                \"title\": \"Pedido núm.{orderNumber}\",\n                \"shippingAddress\": \"Dirección de envío\",\n                \"shippingMethod\": \"Método de envío\",\n                \"summaryTotal\": \"Total\",\n                \"destination\": \"Destino\",\n                \"destinationWithCount\": \"Destino {número, número}/{total, número}\",\n                \"digitalDelivery\": \"Entrega digital a {email}\",\n                \"subtotal\": \"Subtotal\",\n                \"shipping\": \"Envío\",\n                \"tax\": \"Impuesto\",\n                \"orderSummary\": \"Resumen del pedido\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Método de pago} other {Métodos de pago}}\",\n                \"paymentEndingInLabel\": \"Con terminación en\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Tarjeta de crédito\",\n                    \"giftCertificate\": \"Certificado de regalo\",\n                    \"storeCredit\": \"Crédito de la tienda\",\n                    \"other\": \"Otro\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Direcciones\",\n            \"cta\": \"Agregar dirección\",\n            \"edit\": \"Editar\",\n            \"delete\": \"Eliminar\",\n            \"cancel\": \"Cancelar\",\n            \"create\": \"Crear\",\n            \"update\": \"Actualizar\",\n            \"setDefault\": \"Establecer como predeterminado\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ninguna dirección\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"addressLine1Required\": \"La línea 1 es obligatoria\",\n                \"cityRequired\": \"La ciudad es obligatoria\",\n                \"countryRequired\": \"País es un campo obligatorio\",\n                \"stateRequired\": \"Estado/Provincia es un campo obligatorio.\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio.\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"CONFIGURACIÓN DE CUENTA\",\n            \"changePassword\": \"Cambiar contraseña\",\n            \"passwordUpdated\": \"¡La contraseña se actualizó correctamente!\",\n            \"settingsUpdated\": \"La configuración de la cuenta se actualizó correctamente.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"currentPassword\": \"Contraseña actual\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"cta\": \"Actualizar\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Preferencias de marketing\",\n                \"label\": \"Suscríbete a nuestro boletín informativo.\",\n                \"marketingPreferencesUpdated\": \"¡Las preferencias de marketing se actualizaron con éxito!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"firstNameTooSmall\": \"El nombre de pila debe tener al menos 2 caracteres\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"lastNameTooSmall\": \"El apellido debe tener al menos 2 caracteres\",\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"currentPasswordRequired\": \"La contraseña actual es obligatoria\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Por favor, confirme tu contraseña\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Acciones de la lista de deseos\",\n        \"title\": \"Listas de deseos\",\n        \"new\": \"Nueva lista de deseos\",\n        \"items\": \"{count, plural, =1 {1 artículo} other {# artículos}}\",\n        \"viewWishlist\": \"Ver lista\",\n        \"noWishlists\": \"No tienes ninguna lista de deseos\",\n        \"noWishlistsCallToAction\": \"Crea una lista de deseos\",\n        \"emptyWishlist\": \"No has agregado productos a esta lista de deseos.\",\n        \"share\": \"Compartir\",\n        \"shareSuccess\": \"La lista de deseos se compartió correctamente.\",\n        \"shareCopied\": \"La URL pública de la lista de deseos se copió a tu portapapeles.\",\n        \"shareDisabled\": \"Tu lista de deseos debe ser pública para que puedas compartirla.\",\n        \"makePublic\": \"Haz público\",\n        \"makePrivate\": \"Configurar como privada\",\n        \"rename\": \"Cambiar el nombre\",\n        \"delete\": \"Eliminar\",\n        \"removeButtonTitle\": \"Eliminar el producto de tu lista de deseos\",\n        \"Visibility\": {\n            \"public\": \"Público\",\n            \"private\": \"Privado\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Cancelar\",\n            \"close\": \"Cerrar\",\n            \"copy\": \"Copiar\",\n            \"create\": \"Crear\",\n            \"save\": \"Guardar\",\n            \"delete\": \"Eliminar\",\n            \"newTitle\": \"Crea una nueva lista de deseos\",\n            \"shareTitle\": \"Comparte {name}\",\n            \"renameTitle\": \"Renombrar {name}\",\n            \"deleteTitle\": \"Borrar {name}\",\n            \"changeVisibilityPublicTitle\": \"¿Hacer {name} público?\",\n            \"changeVisibilityPrivateTitle\": \"¿Hacer {name} privado?\",\n            \"makePublicContent\": \"¿Confirmas que quieres configurar <bold>{name}</bold> como pública? Si lo haces, otras personas podrán ver tu lista de deseos si tienen un enlace para acceder a ella.\",\n            \"makePrivateContent\": \"¿Confirmas que quieres configurar <bold>{name}</bold> como privada? Las personas con quienes hayas compartido tu lista de deseos ya no podrán verla.\",\n            \"deleteContent\": \"¿Confirmas que quieres eliminar <bold>{name}</bold>? Esta acción no puede deshacerse.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Nombre\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"El nombre de la lista de deseos no puede estar vacío.\",\n            \"updateFailed\": \"No se pudo actualizar tu lista de deseos. Vuelve a intentarlo.\",\n            \"deleteFailed\": \"No se pudo eliminar tu lista de deseos. Vuelve a intentarlo.\",\n            \"removeProductFailed\": \"No se pudo eliminar el producto de tu lista de deseos. Vuelve a intentarlo.\",\n            \"unauthorized\": \"No tienes autorización para realizar esta acción. Inicia sesión e inténtalo de nuevo.\",\n            \"unexpected\": \"Se produjo un error inesperado, inténtalo de nuevo\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"La lista de deseos se creó correctamente.\",\n            \"updateSuccess\": \"La lista de deseos se actualizó correctamente.\",\n            \"deleteSuccess\": \"La lista de deseos se eliminó correctamente.\",\n            \"removeItemSuccess\": \"Se eliminó el artículo de tu lista de deseos.\"\n        },\n        \"Button\": {\n            \"label\": \"Agregar a la lista de deseos\",\n            \"addToNewWishlist\": \"Agregar a una nueva lista de deseos\",\n            \"defaultWishlistName\": \"Mi lista de deseos\",\n            \"addSuccessMessage\": \"El producto se ha agregado a tu lista de deseos.\",\n            \"removeSuccessMessage\": \"El producto se ha eliminado de tu lista de deseos.\",\n            \"Errors\": {\n                \"addProductFailed\": \"No se pudo agregar el producto de tu lista de deseos, inténtalo de nuevo\",\n                \"removeProductFailed\": \"No se pudo eliminar el producto de tu lista de deseos, inténtalo de nuevo\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Lista de deseos pública\",\n        \"defaultName\": \"Lista pública de deseos\",\n        \"emptyWishlist\": \"Aún no hay productos en esta lista de deseos.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Inicio\",\n        \"Empty\": {\n            \"title\": \"No se encontraron publicaciones de blog\",\n            \"subtitle\": \"Vuelve a consultar más tarde para obtener más contenido\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Compartir\",\n            \"email\": \"Correo electrónico\",\n            \"print\": \"Imprimir\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Carrito\",\n        \"heading\": \"Tu carrito\",\n        \"proceedToCheckout\": \"Continuar con el proceso de pago\",\n        \"increment\": \"Aumenta la cantidad\",\n        \"decrement\": \"Disminuir cantidad\",\n        \"removeItem\": \"Eliminar artículo\",\n        \"cartCombined\": \"Notamos que tenías artículos guardados en un carrito anterior, así que los hemos agregado a tu carrito actual.\",\n        \"cartRestored\": \"Iniciaste un carrito en otro dispositivo y lo hemos restaurado aquí para que puedas continuar desde donde lo dejaste.\",\n        \"cartUpdateInProgress\": \"Tiene una actualización de carrito en proceso. ¿Estás seguro de que quieres salir de esta página? Es posible que se pierdan sus cambios.\",\n        \"originalPrice\": \"El precio original era {price}.\",\n        \"currentPrice\": \"El precio actual es {price}.\",\n        \"quantityReadyToShip\": \"{cantidad, número} listo para enviar\",\n        \"quantityOnBackorder\": \"{cantidad, número} estará en pedido pendiente\",\n        \"partiallyAvailable\": \"Solo {cantidad, número} disponible\",\n        \"CheckoutSummary\": {\n            \"title\": \"Resumen\",\n            \"subTotal\": \"Subtotal\",\n            \"discounts\": \"Descuentos\",\n            \"tax\": \"Impuesto\",\n            \"total\": \"Total\",\n            \"CouponCode\": {\n                \"apply\": \"Aplicar\",\n                \"couponCode\": \"Código de cupón\",\n                \"removeCouponCode\": \"Elimina el código de cupón\",\n                \"invalidCouponCode\": \"Ingresa un código de cupón válido\",\n                \"cartNotFound\": \"Se produjo un error al recuperar su carrito\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Envío\",\n                \"add\": \"Agregar\",\n                \"change\": \"Cambiar\",\n                \"cancel\": \"Cancelar\",\n                \"country\": \"País\",\n                \"city\": \"Ciudad\",\n                \"state\": \"Estado/Provincia\",\n                \"postalCode\": \"Código postal\",\n                \"updatedShippingOptions\": \"Actualiza las opciones de envío\",\n                \"viewShippingOptions\": \"Ver opciones de envío\",\n                \"editAddress\": \"Editar dirección\",\n                \"shippingOptions\": \"Opciones de envío\",\n                \"updateShipping\": \"Actualizar envío\",\n                \"addShipping\": \"Agregar envío\",\n                \"cartNotFound\": \"Se produjo un error al recuperar su carrito\",\n                \"noShippingOptions\": \"No hay opciones de envío disponibles para tu dirección\",\n                \"countryRequired\": \"País es un campo obligatorio\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Certificado de regalo\",\n            \"giftCertificateCode\": \"Código del certificado de regalo\",\n            \"removeGiftCertificate\": \"Eliminar certificado de regalo\",\n            \"apply\": \"Aplicar\",\n            \"to\": \"De\",\n            \"message\": \"Mensaje\",\n            \"invalidGiftCertificate\": \"Ingrese un código de certificado de regalo válido\",\n            \"cartNotFound\": \"Se produjo un error al recuperar su carrito\"\n        },\n        \"Empty\": {\n            \"title\": \"Su carrito está vacío.\",\n            \"subtitle\": \"Agrega algunos productos para comenzar.\",\n            \"cta\": \"Seguir comprando\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Se produjo un error al recuperar su carrito\",\n            \"lineItemNotFound\": \"Artículo no encontrado.\",\n            \"failedToUpdateQuantity\": \"No se pudo actualizar la cantidad.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Comparar productos\",\n        \"addToCart\": \"Agregar al carrito\",\n        \"next\": \"Siguientes productos\",\n        \"previous\": \"Productos anteriores\",\n        \"noProductsToCompare\": \"No hay productos para comparar\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Peso\",\n        \"description\": \"Descripción\",\n        \"noDescription\": \"No hay ninguna descripción disponible.\",\n        \"rating\": \"Calificación\",\n        \"noRatings\": \"No hay reseñas.\",\n        \"otherDetails\": \"Otros detalles\",\n        \"noOtherDetails\": \"No hay otros detalles.\",\n        \"viewOptions\": \"Opciones de visualización\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 artículo} other {# artículos}} agregado(s) a <cartLink>tu carrito</cartLink>\",\n        \"missingCart\": \"Carrito no encontrado. Vuelva a intentarlo más tarde.\",\n        \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Cantidad\",\n            \"increaseQuantity\": \"Aumenta la cantidad\",\n            \"decreaseQuantity\": \"Disminuir cantidad\",\n            \"emptySelectPlaceholder\": \"Seleccione una opción\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 artículo} other {# artículos}} agregado(s) a <cartLink>tu carrito</cartLink>\",\n            \"missingCart\": \"Carrito no encontrado. Intenta de nuevo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"variantRequiredError\": \"Para añadir este producto al carrito, debes seleccionar opciones.\",\n            \"increaseNumber\": \"Aumentar número\",\n            \"decreaseNumber\": \"Disminuir número\",\n            \"thumbnail\": \"Ver número de imagen\",\n            \"additionalInformation\": \"Información adicional\",\n            \"currentStock\": \"{cantidad, número} en stock\",\n            \"backorderQuantity\": \"{cantidad, número} estará en espera\",\n            \"loadingMoreImages\": \"Cargando más imágenes\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 imagen más cargada} other {# más imágenes cargadas}}\",\n            \"Submit\": {\n                \"addToCart\": \"Agregar al carrito\",\n                \"outOfStock\": \"Agotado/a\",\n                \"preorder\": \"Pedido anticipado\",\n                \"unavailable\": \"No disponible\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Especificaciones\",\n                \"warranty\": \"Garantía\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Peso\",\n                \"condition\": \"Condición\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Productos relacionados\",\n            \"noRelatedProducts\": \"No se encontraron productos relacionados\",\n            \"browseCatalog\": \"Intenta navegar por nuestro catálogo completo de productos.\",\n            \"cta\": \"Comprar todos\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\",\n            \"scrollbar\": \"Barra de desplazamiento de productos relacionados\"\n        },\n        \"Reviews\": {\n            \"title\": \"Reseñas\",\n            \"empty\": \"No se han agregado reseñas para este producto.\",\n            \"previous\": \"Reseñas anteriores\",\n            \"next\": \"Próximas reseñas\",\n            \"Form\": {\n                \"button\": \"Escribe una opinión\",\n                \"title\": \"Escribe una opinión\",\n                \"submit\": \"Enviar\",\n                \"cancel\": \"Cancelar\",\n                \"ratingLabel\": \"Calificación\",\n                \"titleLabel\": \"Título\",\n                \"reviewLabel\": \"Revisar\",\n                \"nameLabel\": \"Nombre\",\n                \"emailLabel\": \"Correo electrónico\",\n                \"successMessage\": \"¡Tu reseña fue enviada con éxito!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Se requiere título\",\n                    \"authorRequired\": \"El campo Nombre es obligatorio\",\n                    \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                    \"textRequired\": \"Es necesario revisar\",\n                    \"ratingRequired\": \"Se requiere una calificación\",\n                    \"ratingTooSmall\": \"La puntuación debe ser al menos 1\",\n                    \"ratingTooLarge\": \"El puntaje debe ser como máximo 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Inicio\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Inicio\",\n            \"Form\": {\n                \"success\": \"Gracias por ponerte en contacto. Nos comunicaremos contigo pronto.\",\n                \"successCta\": \"Seguir comprando\",\n                \"fullName\": \"Nombre completo\",\n                \"companyName\": \"Nombre de la empresa\",\n                \"phone\": \"Teléfono\",\n                \"orderNo\": \"Número de pedido\",\n                \"rma\": \"Número de RMA (autorización de devolución)\",\n                \"email\": \"Correo electrónico\",\n                \"comments\": \"Comentarios/Preguntas\",\n                \"cta\": \"Enviar formulario\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"MANTENIMIENTO\",\n        \"message\": \"Estamos fuera de servicio por mantenimiento\",\n        \"contactUs\": \"Puedes contactarnos en:\"\n    },\n    \"Error\": {\n        \"title\": \"Hubo un error en el servidor.\",\n        \"subtitle\": \"Vuelva a intentarlo más tarde.\",\n        \"cta\": \"Volver a intentarlo\"\n    },\n    \"NotFound\": {\n        \"title\": \"No pudimos encontrar esa página.\",\n        \"subtitle\": \"Intenta buscar algo más o regresa a la página de inicio.\",\n        \"featuredProducts\": \"Productos destacados\",\n        \"search\": \"Buscar\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Inicio\",\n            \"toggleNavigation\": \"Alternar navegación\",\n            \"Icons\": {\n                \"account\": \"Perfil\",\n                \"cart\": \"Carrito\",\n                \"search\": \"Abrir ventana emergente de búsqueda\",\n                \"giftCertificates\": \"Certificados de regalo\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Cambiar moneda\",\n                \"invalidCurrency\": \"Moneda inválida\",\n                \"errorUpdatingCurrency\": \"Error al actualizar la moneda de tu carrito. Vuelve a intentarlo.\"\n            },\n            \"Search\": {\n                \"products\": \"Productos\",\n                \"categories\": \"Categorías\",\n                \"brands\": \"Marcas\",\n                \"noSearchResultsTitle\": \"Lo sentimos, no hay resultados para “{term}”.\",\n                \"noSearchResultsSubtitle\": \"Prueba con otra búsqueda.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo.\",\n                \"inputPlaceholder\": \"Buscar productos, categorías, marcas...\",\n                \"submitLabel\": \"Buscar\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Inicio\",\n            \"contactUs\": \"Contacto\",\n            \"socialMediaLinks\": \"Enlaces a redes sociales\",\n            \"categories\": \"Categorías\",\n            \"brands\": \"Marcas\",\n            \"navigate\": \"Navegar\",\n            \"giftCertificates\": \"Certificados de regalo\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Suscríbete a nuestro boletín informativo\",\n            \"placeholder\": \"Ingresa tu correo electrónico\",\n            \"description\": \"Mantente al día con las últimas noticias y ofertas de nuestra tienda.\",\n            \"subscribedToNewsletter\": \"¡Te suscribiste a nuestro boletín!\",\n            \"Errors\": {\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"invalidEmail\": \"Por favor, introduzca una dirección de email válida\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Rechazar todo\",\n                \"acceptAll\": \"Aceptar todo\",\n                \"customize\": \"Personalizar\",\n                \"save\": \"Guardar la configuración\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"Valoramos su privacidad\",\n                \"description\": \"Este sitio emplea cookies para mejorar su experiencia de navegación, analizar el tráfico del sitio y mostrar contenido personalizado.\",\n                \"privacyPolicy\": \"Política de Privacidad\"\n            },\n            \"Dialog\": {\n                \"title\": \"Configuración de privacidad\",\n                \"description\": \"Personaliza tu configuración de privacidad aquí. Puede elegir qué tipos de cookies y tecnologías de seguimiento le gustaría permitir.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Estrictamente necesario\",\n                    \"description\": \"Estas cookies son Essentials para que el sitio web funcione correctamente y no se pueden desactivar.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Funcionalidad\",\n                    \"description\": \"Estas cookies permiten mejorar la funcionalidad y la personalización del sitio web.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"Estas cookies se emplean para ofrecer anuncios relevantes y realizar un seguimiento de su efectividad.\"\n                },\n                \"measurement\": {\n                    \"title\": \"DATOS ANALÍTICOS\",\n                    \"description\": \"Estas cookies nos ayudan a comprender cómo interactúan los visitantes con el sitio web y mejorar su rendimiento.\"\n                },\n                \"experience\": {\n                    \"title\": \"Experiencia\",\n                    \"description\": \"Estas cookies nos ayudan a brindar una mejor experiencia de usuario y probar nuevas funciones.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"El precio original era {price}.\",\n            \"currentPrice\": \"El precio actual es {price}.\",\n            \"range\": \"Precio de {minValue} a {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Certificados de regalo\",\n        \"description\": \"Regala el regalo perfecto que nunca pasa de moda. Deja que tus colegas y seres queridos elijan exactamente lo que quieren de toda nuestra colección.\",\n        \"purchaseLabel\": \"Comprar ahora\",\n        \"checkBalanceLabel\": \"Consultar saldo\",\n        \"expiresAtLabel\": \"Válido hasta\",\n        \"CheckBalance\": {\n            \"title\": \"Consultar saldo\",\n            \"description\": \"Puede consultar el saldo y obtener la información sobre su certificado de regalo escribiendo el código en el cuadro a continuación.\",\n            \"inputLabel\": \"Código\",\n            \"inputPlaceholder\": \"xxx-xxx-xxx-xxx\",\n            \"purchasedDateLabel\": \"Comprado\",\n            \"senderLabel\": \"Para\",\n            \"Errors\": {\n                \"invalidCode\": \"El código del certificado de regalo que ingresó no es válido. Verifique el código e inténtelo de nuevo.\",\n                \"codeRequired\": \"Ingresa el código del certificado de regalo.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Comprar certificado de regalo\",\n            \"title\": \"Certificado de regalo digital\",\n            \"description\": \"Explore nuestros certificados de regalo, perfectos para cualquier ocasión. Elige la cantidad y personaliza tu mensaje.\",\n            \"successMessage\": \"Se ha agregado un certificado de regalo a <cartLink> su carrito</cartLink>\",\n            \"missingCart\": \"Carrito no encontrado. Vuelva a intentarlo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"Form\": {\n                \"amountLabel\": \"Monto\",\n                \"customAmountLabel\": \"Importe (entre {minAmount} y {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Selecciona una cantidad\",\n                \"customAmountPlaceholder\": \"Ingrese la cantidad personalizada\",\n                \"senderNameLabel\": \"Tu nombre\",\n                \"senderEmailLabel\": \"Tu correo electrónico\",\n                \"recipientNameLabel\": \"Nombre del destinatario\",\n                \"recipientEmailLabel\": \"Correo electrónico del destinatario\",\n                \"namePlaceholder\": \"Ingrese el nombre\",\n                \"emailPlaceholder\": \"Ingresa el correo electrónico\",\n                \"messageLabel\": \"Mensaje\",\n                \"messagePlaceholder\": \"Ingrese su mensaje (opcional)\",\n                \"nonRefundableCheckboxLabel\": \"Acepto que los certificados de regalo no son reembolsables\",\n                \"expiryCheckboxLabel\": \"Reconozco que este Certificado de Regalo caducará el {expiryDate}\",\n                \"ctaLabel\": \"Agregar al carrito\",\n                \"Errors\": {\n                    \"amountRequired\": \"Por favor, selecciona o introduce el importe de un vale regalo\",\n                    \"amountInvalid\": \"Por favor, seleccione un importe válido de un certificado regalo\",\n                    \"amountOutOfRange\": \"Por favor, introduzca una cantidad entre {minAmount} y {maxAmount}\",\n                    \"unexpectedSettingsError\": \"Se produjo un error inesperado al recuperar la configuración del certificado de regalo. Intente de nuevo más tarde.\",\n                    \"senderNameRequired\": \"Tu nombre es obligatorio\",\n                    \"senderEmailRequired\": \"Tu correo electrónico es obligatorio\",\n                    \"recipientNameRequired\": \"Se requiere el nombre del destinatario\",\n                    \"recipientEmailRequired\": \"Se requiere el email del destinatario\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                    \"checkboxRequired\": \"Debes marcar esta casilla para continuar\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"Opcional\",\n        \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n        \"Errors\": {\n            \"invalidInput\": \"Por favor, revisa tu opinión y vuelve a intentarlo\",\n            \"invalidFormat\": \"El valor introducido no coincide con el formato requerido\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/es-LA.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Nuevos hallazgos para cada ocasión\",\n                \"description\": \"Explora nuestras últimas novedades seleccionadas para ofrecerte estilo, funcionalidad e inspiración. Compra ahora y descubre tu próximo favorito.\",\n                \"alt\": \"Cinco plantas pequeñas en macetas exhibidas sobre bloques apilados de color beige, con una variedad de follaje verde en macetas de color gris oscuro contra un fondo neutro.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide02\": {\n                \"title\": \"Descubre las novedades\",\n                \"description\": \"Compra nuestras últimas novedades y encuentra algo fresco y emocionante para tu hogar.\",\n                \"alt\": \"Manos extendiéndose para sostener un helecho verde en una canasta tejida con un lazo decorativo, sobre un fondo beige con sombras suaves.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide03\": {\n                \"title\": \"Algo para todos\",\n                \"description\": \"No te pierdas las ofertas exclusivas en nuestros productos más vendidos. Compra hoy y ahorra mucho dinero en los artículos que te gustan.\",\n                \"alt\": \"Primer plano de una hoja verde vibrante con perforaciones, que muestra su textura suave y detalles naturales.\",\n                \"cta\": \"Comprar ahora\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Colección destacada\",\n            \"description\": \"Explora nuestras mejores selecciones en esta colección destacada. ¡Encuentra el regalo perfecto o date un capricho!\",\n            \"cta\": \"Ver más\",\n            \"emptyStateTitle\": \"No se encontraron productos\",\n            \"emptyStateSubtitle\": \"Intenta navegar por nuestro catálogo completo de productos.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Novedades\",\n            \"description\": \"Nuestros últimos productos están aquí. Descubre lo nuevo de la tienda.\",\n            \"cta\": \"Ver todas\",\n            \"emptyStateTitle\": \"No se encontraron productos\",\n            \"emptyStateSubtitle\": \"Intenta navegar por nuestro catálogo completo de productos.\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Cambiar contraseña\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"passwordUpdated\": \"¡La contraseña se actualizó correctamente!\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Por favor, confirme tu contraseña\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Inicio de sesión\",\n            \"heading\": \"Inicio de sesión\",\n            \"forgotPassword\": \"¿Olvidaste tu contraseña?\",\n            \"cta\": \"Inicio de sesión\",\n            \"email\": \"Correo electrónico\",\n            \"password\": \"Contraseña\",\n            \"invalidCredentials\": \"Tu dirección de correo electrónico o tu contraseña son incorrectas. Intenta iniciar sesión de nuevo o restablece tu contraseña\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"passwordResetRequired\": \"Se requiere restablecimiento de contraseña. Por favor, revisa tu email para ver instrucciones para restablecer tu contraseña.\",\n            \"invalidToken\": \"Tu enlace de acceso es inválido o caducó. Por favor, intenta iniciar sesión de nuevo.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"invalidInput\": \"Por favor, revisa tu opinión y vuelve a intentarlo.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"¿Nuevo cliente?\",\n                \"accountBenefits\": \"Crea una cuenta con nosotros y podrás:\",\n                \"fastCheckout\": \"Pagar más rápido\",\n                \"multipleAddresses\": \"Guardar varias direcciones de envío\",\n                \"ordersHistory\": \"Acceder a tu historial de pedidos\",\n                \"ordersTracking\": \"Rastrear nuevos pedidos\",\n                \"wishlists\": \"Guardar artículos en tu lista de deseos\",\n                \"cta\": \"Crear cuenta\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Olvidé la contraseña\",\n                \"subtitle\": \"A continuación, ingresa el correo electrónico asociado a tu cuenta. Te enviaremos instrucciones para restablecer tu contraseña.\",\n                \"confirmResetPassword\": \"Si la dirección de correo electrónico {email} está vinculada a una cuenta en nuestra tienda, te hemos enviado un correo electrónico de restablecimiento de contraseña. Revisa tu bandeja de entrada y tu carpeta de correo no deseado si no lo ves.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Registrar cuenta\",\n            \"heading\": \"Cuenta nueva\",\n            \"cta\": \"Crear cuenta\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"addressLine1Required\": \"La línea 1 es obligatoria\",\n                \"cityRequired\": \"La ciudad es obligatoria\",\n                \"countryRequired\": \"País es un campo obligatorio\",\n                \"stateRequired\": \"Estado/Provincia es un campo obligatorio.\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio.\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"No hay productos en esta marca\",\n                \"subtitle\": \"Intenta usar diferentes filtros.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Categorías\",\n            \"Empty\": {\n                \"title\": \"Ningún producto en esta categoría\",\n                \"subtitle\": \"Intenta usar diferentes filtros.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Resultados de búsqueda\",\n            \"searchResults\": \"Resultados de búsqueda para\",\n            \"subCategories\": \"Categorías\",\n            \"Breadcrumbs\": {\n                \"home\": \"Inicio\",\n                \"search\": \"Buscar\"\n            },\n            \"Empty\": {\n                \"title\": \"Lo sentimos, no hay resultados para “{term}”.\",\n                \"subtitle\": \"Prueba con otra búsqueda.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtros\",\n            \"resetFilters\": \"Restablecer filtros\",\n            \"Range\": {\n                \"apply\": \"Aplicar\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Envío gratuito\",\n                \"isFeaturedLabel\": \"Destacado\",\n                \"inStockLabel\": \"En existencias\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Ordenar por:\",\n            \"featuredItems\": \"Elementos destacados\",\n            \"bestSellingItems\": \"Artículos más vendidos\",\n            \"newestItems\": \"Artículos más nuevos\",\n            \"aToZ\": \"A a la Z\",\n            \"zToA\": \"Z a la A\",\n            \"byReview\": \"Por opinión\",\n            \"priceAscending\": \"Precio: ascendente\",\n            \"priceDescending\": \"Precio: descendente\",\n            \"relevance\": \"Relevancia\"\n        },\n        \"Compare\": {\n            \"compare\": \"Comparar\",\n            \"remove\": \"Quitar\",\n            \"maxCompareLimit\": \"Alcanzaste el número máximo de productos para comparar. Elimina un producto para agregar uno nuevo.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Direcciones\",\n            \"logout\": \"Cerrar sesión\",\n            \"orders\": \"Pedidos\",\n            \"settings\": \"Cuenta\",\n            \"wishlists\": \"Listas de deseos\"\n        },\n        \"Orders\": {\n            \"title\": \"Pedidos\",\n            \"orderNumber\": \"Núm. de pedido\",\n            \"totalPrice\": \"Total\",\n            \"viewDetails\": \"Ver detalles\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ningún pedido\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Details\": {\n                \"title\": \"Pedido núm.{orderNumber}\",\n                \"shippingAddress\": \"Dirección de envío\",\n                \"shippingMethod\": \"Método de envío\",\n                \"summaryTotal\": \"Total\",\n                \"destination\": \"Destino\",\n                \"destinationWithCount\": \"Destino {número, número}/{total, número}\",\n                \"digitalDelivery\": \"Entrega digital a {email}\",\n                \"subtotal\": \"Subtotal\",\n                \"shipping\": \"Envío\",\n                \"tax\": \"Impuesto\",\n                \"orderSummary\": \"Resumen del pedido\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Método de pago} other {Métodos de pago}}\",\n                \"paymentEndingInLabel\": \"Con terminación en\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Tarjeta de crédito\",\n                    \"giftCertificate\": \"Certificado de regalo\",\n                    \"storeCredit\": \"Crédito de la tienda\",\n                    \"other\": \"Otro\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Direcciones\",\n            \"cta\": \"Agregar dirección\",\n            \"edit\": \"Editar\",\n            \"delete\": \"Eliminar\",\n            \"cancel\": \"Cancelar\",\n            \"create\": \"Crear\",\n            \"update\": \"Actualizar\",\n            \"setDefault\": \"Establecer como predeterminado\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ninguna dirección\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"addressLine1Required\": \"La línea 1 es obligatoria\",\n                \"cityRequired\": \"La ciudad es obligatoria\",\n                \"countryRequired\": \"País es un campo obligatorio\",\n                \"stateRequired\": \"Estado/Provincia es un campo obligatorio.\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio.\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"CONFIGURACIÓN DE CUENTA\",\n            \"changePassword\": \"Cambiar contraseña\",\n            \"passwordUpdated\": \"¡La contraseña se actualizó correctamente!\",\n            \"settingsUpdated\": \"La configuración de la cuenta se actualizó correctamente.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"currentPassword\": \"Contraseña actual\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"cta\": \"Actualizar\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Preferencias de marketing\",\n                \"label\": \"Suscríbete a nuestro boletín informativo.\",\n                \"marketingPreferencesUpdated\": \"¡Las preferencias de marketing se actualizaron con éxito!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"firstNameTooSmall\": \"El nombre de pila debe tener al menos 2 caracteres\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"lastNameTooSmall\": \"El apellido debe tener al menos 2 caracteres\",\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"currentPasswordRequired\": \"La contraseña actual es obligatoria\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Por favor, confirme tu contraseña\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Acciones de la lista de deseos\",\n        \"title\": \"Listas de deseos\",\n        \"new\": \"Nueva lista de deseos\",\n        \"items\": \"{count, plural, =1 {1 artículo} other {# artículos}}\",\n        \"viewWishlist\": \"Ver lista\",\n        \"noWishlists\": \"No tienes ninguna lista de deseos\",\n        \"noWishlistsCallToAction\": \"Crea una lista de deseos\",\n        \"emptyWishlist\": \"No has agregado productos a esta lista de deseos.\",\n        \"share\": \"Compartir\",\n        \"shareSuccess\": \"La lista de deseos se compartió correctamente.\",\n        \"shareCopied\": \"La URL pública de la lista de deseos se copió a tu portapapeles.\",\n        \"shareDisabled\": \"Tu lista de deseos debe ser pública para que puedas compartirla.\",\n        \"makePublic\": \"Haz público\",\n        \"makePrivate\": \"Configurar como privada\",\n        \"rename\": \"Cambiar el nombre\",\n        \"delete\": \"Eliminar\",\n        \"removeButtonTitle\": \"Eliminar el producto de tu lista de deseos\",\n        \"Visibility\": {\n            \"public\": \"Público\",\n            \"private\": \"Privado\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Cancelar\",\n            \"close\": \"Cerrar\",\n            \"copy\": \"Copiar\",\n            \"create\": \"Crear\",\n            \"save\": \"Guardar\",\n            \"delete\": \"Eliminar\",\n            \"newTitle\": \"Crea una nueva lista de deseos\",\n            \"shareTitle\": \"Comparte {name}\",\n            \"renameTitle\": \"Renombrar {name}\",\n            \"deleteTitle\": \"Borrar {name}\",\n            \"changeVisibilityPublicTitle\": \"¿Hacer {name} público?\",\n            \"changeVisibilityPrivateTitle\": \"¿Hacer {name} privado?\",\n            \"makePublicContent\": \"¿Confirmas que quieres configurar <bold>{name}</bold> como pública? Si lo haces, otras personas podrán ver tu lista de deseos si tienen un enlace para acceder a ella.\",\n            \"makePrivateContent\": \"¿Confirmas que quieres configurar <bold>{name}</bold> como privada? Las personas con quienes hayas compartido tu lista de deseos ya no podrán verla.\",\n            \"deleteContent\": \"¿Confirmas que quieres eliminar <bold>{name}</bold>? Esta acción no puede deshacerse.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Nombre\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"El nombre de la lista de deseos no puede estar vacío.\",\n            \"updateFailed\": \"No se pudo actualizar tu lista de deseos. Vuelve a intentarlo.\",\n            \"deleteFailed\": \"No se pudo eliminar tu lista de deseos. Vuelve a intentarlo.\",\n            \"removeProductFailed\": \"No se pudo eliminar el producto de tu lista de deseos. Vuelve a intentarlo.\",\n            \"unauthorized\": \"No tienes autorización para realizar esta acción. Inicia sesión e inténtalo de nuevo.\",\n            \"unexpected\": \"Se produjo un error inesperado, inténtalo de nuevo\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"La lista de deseos se creó correctamente.\",\n            \"updateSuccess\": \"La lista de deseos se actualizó correctamente.\",\n            \"deleteSuccess\": \"La lista de deseos se eliminó correctamente.\",\n            \"removeItemSuccess\": \"Se eliminó el artículo de tu lista de deseos.\"\n        },\n        \"Button\": {\n            \"label\": \"Agregar a la lista de deseos\",\n            \"addToNewWishlist\": \"Agregar a una nueva lista de deseos\",\n            \"defaultWishlistName\": \"Mi lista de deseos\",\n            \"addSuccessMessage\": \"El producto se ha agregado a tu lista de deseos.\",\n            \"removeSuccessMessage\": \"El producto se ha eliminado de tu lista de deseos.\",\n            \"Errors\": {\n                \"addProductFailed\": \"No se pudo agregar el producto de tu lista de deseos, inténtalo de nuevo\",\n                \"removeProductFailed\": \"No se pudo eliminar el producto de tu lista de deseos, inténtalo de nuevo\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Lista de deseos pública\",\n        \"defaultName\": \"Lista pública de deseos\",\n        \"emptyWishlist\": \"Aún no hay productos en esta lista de deseos.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Inicio\",\n        \"Empty\": {\n            \"title\": \"No se encontraron publicaciones de blog\",\n            \"subtitle\": \"Vuelve a consultar más tarde para obtener más contenido\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Compartir\",\n            \"email\": \"Correo electrónico\",\n            \"print\": \"Imprimir\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Carrito\",\n        \"heading\": \"Tu carrito\",\n        \"proceedToCheckout\": \"Continuar con el proceso de pago\",\n        \"increment\": \"Aumenta la cantidad\",\n        \"decrement\": \"Disminuir cantidad\",\n        \"removeItem\": \"Eliminar artículo\",\n        \"cartCombined\": \"Notamos que tenías artículos guardados en un carrito anterior, así que los hemos agregado a tu carrito actual.\",\n        \"cartRestored\": \"Iniciaste un carrito en otro dispositivo y lo hemos restaurado aquí para que puedas continuar desde donde lo dejaste.\",\n        \"cartUpdateInProgress\": \"Tiene una actualización de carrito en proceso. ¿Estás seguro de que quieres salir de esta página? Es posible que se pierdan sus cambios.\",\n        \"originalPrice\": \"El precio original era {price}.\",\n        \"currentPrice\": \"El precio actual es {price}.\",\n        \"quantityReadyToShip\": \"{cantidad, número} listo para enviar\",\n        \"quantityOnBackorder\": \"{cantidad, número} estará en pedido pendiente\",\n        \"partiallyAvailable\": \"Solo {cantidad, número} disponible\",\n        \"CheckoutSummary\": {\n            \"title\": \"Resumen\",\n            \"subTotal\": \"Subtotal\",\n            \"discounts\": \"Descuentos\",\n            \"tax\": \"Impuesto\",\n            \"total\": \"Total\",\n            \"CouponCode\": {\n                \"apply\": \"Aplicar\",\n                \"couponCode\": \"Código de cupón\",\n                \"removeCouponCode\": \"Elimina el código de cupón\",\n                \"invalidCouponCode\": \"Ingresa un código de cupón válido\",\n                \"cartNotFound\": \"Se produjo un error al recuperar su carrito\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Envío\",\n                \"add\": \"Agregar\",\n                \"change\": \"Cambiar\",\n                \"cancel\": \"Cancelar\",\n                \"country\": \"País\",\n                \"city\": \"Ciudad\",\n                \"state\": \"Estado/Provincia\",\n                \"postalCode\": \"Código postal\",\n                \"updatedShippingOptions\": \"Actualiza las opciones de envío\",\n                \"viewShippingOptions\": \"Ver opciones de envío\",\n                \"editAddress\": \"Editar dirección\",\n                \"shippingOptions\": \"Opciones de envío\",\n                \"updateShipping\": \"Actualizar envío\",\n                \"addShipping\": \"Agregar envío\",\n                \"cartNotFound\": \"Se produjo un error al recuperar su carrito\",\n                \"noShippingOptions\": \"No hay opciones de envío disponibles para tu dirección\",\n                \"countryRequired\": \"País es un campo obligatorio\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Certificado de regalo\",\n            \"giftCertificateCode\": \"Código del certificado de regalo\",\n            \"removeGiftCertificate\": \"Eliminar certificado de regalo\",\n            \"apply\": \"Aplicar\",\n            \"to\": \"De\",\n            \"message\": \"Mensaje\",\n            \"invalidGiftCertificate\": \"Ingrese un código de certificado de regalo válido\",\n            \"cartNotFound\": \"Se produjo un error al recuperar su carrito\"\n        },\n        \"Empty\": {\n            \"title\": \"Su carrito está vacío.\",\n            \"subtitle\": \"Agrega algunos productos para comenzar.\",\n            \"cta\": \"Seguir comprando\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Se produjo un error al recuperar su carrito\",\n            \"lineItemNotFound\": \"Artículo no encontrado.\",\n            \"failedToUpdateQuantity\": \"No se pudo actualizar la cantidad.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Comparar productos\",\n        \"addToCart\": \"Agregar al carrito\",\n        \"next\": \"Siguientes productos\",\n        \"previous\": \"Productos anteriores\",\n        \"noProductsToCompare\": \"No hay productos para comparar\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Peso\",\n        \"description\": \"Descripción\",\n        \"noDescription\": \"No hay ninguna descripción disponible.\",\n        \"rating\": \"Calificación\",\n        \"noRatings\": \"No hay reseñas.\",\n        \"otherDetails\": \"Otros detalles\",\n        \"noOtherDetails\": \"No hay otros detalles.\",\n        \"viewOptions\": \"Opciones de visualización\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 artículo} other {# artículos}} agregado(s) a <cartLink>tu carrito</cartLink>\",\n        \"missingCart\": \"Carrito no encontrado. Vuelva a intentarlo más tarde.\",\n        \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Cantidad\",\n            \"increaseQuantity\": \"Aumenta la cantidad\",\n            \"decreaseQuantity\": \"Disminuir cantidad\",\n            \"emptySelectPlaceholder\": \"Seleccione una opción\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 artículo} other {# artículos}} agregado(s) a <cartLink>tu carrito</cartLink>\",\n            \"missingCart\": \"Carrito no encontrado. Intenta de nuevo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"variantRequiredError\": \"Para añadir este producto al carrito, debes seleccionar opciones.\",\n            \"increaseNumber\": \"Aumentar número\",\n            \"decreaseNumber\": \"Disminuir número\",\n            \"thumbnail\": \"Ver número de imagen\",\n            \"additionalInformation\": \"Información adicional\",\n            \"currentStock\": \"{cantidad, número} en stock\",\n            \"backorderQuantity\": \"{cantidad, número} estará en espera\",\n            \"loadingMoreImages\": \"Cargando más imágenes\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 imagen más cargada} other {# más imágenes cargadas}}\",\n            \"Submit\": {\n                \"addToCart\": \"Agregar al carrito\",\n                \"outOfStock\": \"Agotado/a\",\n                \"preorder\": \"Pedido anticipado\",\n                \"unavailable\": \"No disponible\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Especificaciones\",\n                \"warranty\": \"Garantía\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Peso\",\n                \"condition\": \"Condición\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Productos relacionados\",\n            \"noRelatedProducts\": \"No se encontraron productos relacionados\",\n            \"browseCatalog\": \"Intenta navegar por nuestro catálogo completo de productos.\",\n            \"cta\": \"Comprar todos\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\",\n            \"scrollbar\": \"Barra de desplazamiento de productos relacionados\"\n        },\n        \"Reviews\": {\n            \"title\": \"Reseñas\",\n            \"empty\": \"No se han agregado reseñas para este producto.\",\n            \"previous\": \"Reseñas anteriores\",\n            \"next\": \"Próximas reseñas\",\n            \"Form\": {\n                \"button\": \"Escribe una opinión\",\n                \"title\": \"Escribe una opinión\",\n                \"submit\": \"Enviar\",\n                \"cancel\": \"Cancelar\",\n                \"ratingLabel\": \"Calificación\",\n                \"titleLabel\": \"Título\",\n                \"reviewLabel\": \"Revisar\",\n                \"nameLabel\": \"Nombre\",\n                \"emailLabel\": \"Correo electrónico\",\n                \"successMessage\": \"¡Tu reseña fue enviada con éxito!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Se requiere título\",\n                    \"authorRequired\": \"El campo Nombre es obligatorio\",\n                    \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                    \"textRequired\": \"Es necesario revisar\",\n                    \"ratingRequired\": \"Se requiere una calificación\",\n                    \"ratingTooSmall\": \"La puntuación debe ser al menos 1\",\n                    \"ratingTooLarge\": \"El puntaje debe ser como máximo 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Inicio\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Inicio\",\n            \"Form\": {\n                \"success\": \"Gracias por ponerte en contacto. Nos comunicaremos contigo pronto.\",\n                \"successCta\": \"Seguir comprando\",\n                \"fullName\": \"Nombre completo\",\n                \"companyName\": \"Nombre de la empresa\",\n                \"phone\": \"Teléfono\",\n                \"orderNo\": \"Número de pedido\",\n                \"rma\": \"Número de RMA (autorización de devolución)\",\n                \"email\": \"Correo electrónico\",\n                \"comments\": \"Comentarios/Preguntas\",\n                \"cta\": \"Enviar formulario\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"MANTENIMIENTO\",\n        \"message\": \"Estamos fuera de servicio por mantenimiento\",\n        \"contactUs\": \"Puedes contactarnos en:\"\n    },\n    \"Error\": {\n        \"title\": \"Hubo un error en el servidor.\",\n        \"subtitle\": \"Vuelva a intentarlo más tarde.\",\n        \"cta\": \"Volver a intentarlo\"\n    },\n    \"NotFound\": {\n        \"title\": \"No pudimos encontrar esa página.\",\n        \"subtitle\": \"Intenta buscar algo más o regresa a la página de inicio.\",\n        \"featuredProducts\": \"Productos destacados\",\n        \"search\": \"Buscar\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Inicio\",\n            \"toggleNavigation\": \"Alternar navegación\",\n            \"Icons\": {\n                \"account\": \"Perfil\",\n                \"cart\": \"Carrito\",\n                \"search\": \"Abrir ventana emergente de búsqueda\",\n                \"giftCertificates\": \"Certificados de regalo\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Cambiar moneda\",\n                \"invalidCurrency\": \"Moneda inválida\",\n                \"errorUpdatingCurrency\": \"Error al actualizar la moneda de tu carrito. Vuelve a intentarlo.\"\n            },\n            \"Search\": {\n                \"products\": \"Productos\",\n                \"categories\": \"Categorías\",\n                \"brands\": \"Marcas\",\n                \"noSearchResultsTitle\": \"Lo sentimos, no hay resultados para “{term}”.\",\n                \"noSearchResultsSubtitle\": \"Prueba con otra búsqueda.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo.\",\n                \"inputPlaceholder\": \"Buscar productos, categorías, marcas...\",\n                \"submitLabel\": \"Buscar\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Inicio\",\n            \"contactUs\": \"Contacto\",\n            \"socialMediaLinks\": \"Enlaces a redes sociales\",\n            \"categories\": \"Categorías\",\n            \"brands\": \"Marcas\",\n            \"navigate\": \"Navegar\",\n            \"giftCertificates\": \"Certificados de regalo\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Suscríbete a nuestro boletín informativo\",\n            \"placeholder\": \"Ingresa tu correo electrónico\",\n            \"description\": \"Mantente al día con las últimas noticias y ofertas de nuestra tienda.\",\n            \"subscribedToNewsletter\": \"¡Te suscribiste a nuestro boletín!\",\n            \"Errors\": {\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"invalidEmail\": \"Por favor, introduzca una dirección de email válida\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Rechazar todo\",\n                \"acceptAll\": \"Aceptar todo\",\n                \"customize\": \"Personalizar\",\n                \"save\": \"Guardar la configuración\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"Valoramos su privacidad\",\n                \"description\": \"Este sitio emplea cookies para mejorar su experiencia de navegación, analizar el tráfico del sitio y mostrar contenido personalizado.\",\n                \"privacyPolicy\": \"Política de Privacidad\"\n            },\n            \"Dialog\": {\n                \"title\": \"Configuración de privacidad\",\n                \"description\": \"Personaliza tu configuración de privacidad aquí. Puede elegir qué tipos de cookies y tecnologías de seguimiento le gustaría permitir.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Estrictamente necesario\",\n                    \"description\": \"Estas cookies son Essentials para que el sitio web funcione correctamente y no se pueden desactivar.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Funcionalidad\",\n                    \"description\": \"Estas cookies permiten mejorar la funcionalidad y la personalización del sitio web.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"Estas cookies se emplean para ofrecer anuncios relevantes y realizar un seguimiento de su efectividad.\"\n                },\n                \"measurement\": {\n                    \"title\": \"DATOS ANALÍTICOS\",\n                    \"description\": \"Estas cookies nos ayudan a comprender cómo interactúan los visitantes con el sitio web y mejorar su rendimiento.\"\n                },\n                \"experience\": {\n                    \"title\": \"Experiencia\",\n                    \"description\": \"Estas cookies nos ayudan a brindar una mejor experiencia de usuario y probar nuevas funciones.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"El precio original era {price}.\",\n            \"currentPrice\": \"El precio actual es {price}.\",\n            \"range\": \"Precio de {minValue} a {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Certificados de regalo\",\n        \"description\": \"Regala el regalo perfecto que nunca pasa de moda. Deja que tus colegas y seres queridos elijan exactamente lo que quieren de toda nuestra colección.\",\n        \"purchaseLabel\": \"Comprar ahora\",\n        \"checkBalanceLabel\": \"Consultar saldo\",\n        \"expiresAtLabel\": \"Válido hasta\",\n        \"CheckBalance\": {\n            \"title\": \"Consultar saldo\",\n            \"description\": \"Puede consultar el saldo y obtener la información sobre su certificado de regalo escribiendo el código en el cuadro a continuación.\",\n            \"inputLabel\": \"Código\",\n            \"inputPlaceholder\": \"xxx-xxx-xxx-xxx\",\n            \"purchasedDateLabel\": \"Comprado\",\n            \"senderLabel\": \"Para\",\n            \"Errors\": {\n                \"invalidCode\": \"El código del certificado de regalo que ingresó no es válido. Verifique el código e inténtelo de nuevo.\",\n                \"codeRequired\": \"Ingresa el código del certificado de regalo.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Comprar certificado de regalo\",\n            \"title\": \"Certificado de regalo digital\",\n            \"description\": \"Explore nuestros certificados de regalo, perfectos para cualquier ocasión. Elige la cantidad y personaliza tu mensaje.\",\n            \"successMessage\": \"Se ha agregado un certificado de regalo a <cartLink> su carrito</cartLink>\",\n            \"missingCart\": \"Carrito no encontrado. Vuelva a intentarlo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"Form\": {\n                \"amountLabel\": \"Monto\",\n                \"customAmountLabel\": \"Importe (entre {minAmount} y {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Selecciona una cantidad\",\n                \"customAmountPlaceholder\": \"Ingrese la cantidad personalizada\",\n                \"senderNameLabel\": \"Tu nombre\",\n                \"senderEmailLabel\": \"Tu correo electrónico\",\n                \"recipientNameLabel\": \"Nombre del destinatario\",\n                \"recipientEmailLabel\": \"Correo electrónico del destinatario\",\n                \"namePlaceholder\": \"Ingrese el nombre\",\n                \"emailPlaceholder\": \"Ingresa el correo electrónico\",\n                \"messageLabel\": \"Mensaje\",\n                \"messagePlaceholder\": \"Ingrese su mensaje (opcional)\",\n                \"nonRefundableCheckboxLabel\": \"Acepto que los certificados de regalo no son reembolsables\",\n                \"expiryCheckboxLabel\": \"Reconozco que este Certificado de Regalo caducará el {expiryDate}\",\n                \"ctaLabel\": \"Agregar al carrito\",\n                \"Errors\": {\n                    \"amountRequired\": \"Por favor, selecciona o introduce el importe de un vale regalo\",\n                    \"amountInvalid\": \"Por favor, seleccione un importe válido de un certificado regalo\",\n                    \"amountOutOfRange\": \"Por favor, introduzca una cantidad entre {minAmount} y {maxAmount}\",\n                    \"unexpectedSettingsError\": \"Se produjo un error inesperado al recuperar la configuración del certificado de regalo. Intente de nuevo más tarde.\",\n                    \"senderNameRequired\": \"Tu nombre es obligatorio\",\n                    \"senderEmailRequired\": \"Tu correo electrónico es obligatorio\",\n                    \"recipientNameRequired\": \"Se requiere el nombre del destinatario\",\n                    \"recipientEmailRequired\": \"Se requiere el email del destinatario\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                    \"checkboxRequired\": \"Debes marcar esta casilla para continuar\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"Opcional\",\n        \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n        \"Errors\": {\n            \"invalidInput\": \"Por favor, revisa tu opinión y vuelve a intentarlo\",\n            \"invalidFormat\": \"El valor introducido no coincide con el formato requerido\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/es-MX.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Nuevos hallazgos para cada ocasión\",\n                \"description\": \"Explora nuestras últimas novedades seleccionadas para ofrecerte estilo, funcionalidad e inspiración. Compra ahora y descubre tu próximo favorito.\",\n                \"alt\": \"Cinco plantas pequeñas en macetas exhibidas sobre bloques apilados de color beige, con una variedad de follaje verde en macetas de color gris oscuro contra un fondo neutro.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide02\": {\n                \"title\": \"Descubre las novedades\",\n                \"description\": \"Compra nuestras últimas novedades y encuentra algo fresco y emocionante para tu hogar.\",\n                \"alt\": \"Manos extendiéndose para sostener un helecho verde en una canasta tejida con un lazo decorativo, sobre un fondo beige con sombras suaves.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide03\": {\n                \"title\": \"Algo para todos\",\n                \"description\": \"No te pierdas las ofertas exclusivas en nuestros productos más vendidos. Compra hoy y ahorra mucho dinero en los artículos que te gustan.\",\n                \"alt\": \"Primer plano de una hoja verde vibrante con perforaciones, que muestra su textura suave y detalles naturales.\",\n                \"cta\": \"Comprar ahora\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Colección destacada\",\n            \"description\": \"Explora nuestras mejores selecciones en esta colección destacada. ¡Encuentra el regalo perfecto o date un capricho!\",\n            \"cta\": \"Ver más\",\n            \"emptyStateTitle\": \"No se encontraron productos\",\n            \"emptyStateSubtitle\": \"Intenta navegar por nuestro catálogo completo de productos.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Novedades\",\n            \"description\": \"Nuestros últimos productos están aquí. Descubre lo nuevo de la tienda.\",\n            \"cta\": \"Ver todas\",\n            \"emptyStateTitle\": \"No se encontraron productos\",\n            \"emptyStateSubtitle\": \"Intenta navegar por nuestro catálogo completo de productos.\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Cambiar contraseña\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"passwordUpdated\": \"¡La contraseña se actualizó correctamente!\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Por favor, confirme tu contraseña\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Inicio de sesión\",\n            \"heading\": \"Inicio de sesión\",\n            \"forgotPassword\": \"¿Olvidaste tu contraseña?\",\n            \"cta\": \"Inicio de sesión\",\n            \"email\": \"Correo electrónico\",\n            \"password\": \"Contraseña\",\n            \"invalidCredentials\": \"Tu dirección de correo electrónico o tu contraseña son incorrectas. Intenta iniciar sesión de nuevo o restablece tu contraseña\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"passwordResetRequired\": \"Se requiere restablecimiento de contraseña. Por favor, revisa tu email para ver instrucciones para restablecer tu contraseña.\",\n            \"invalidToken\": \"Tu enlace de acceso es inválido o caducó. Por favor, intenta iniciar sesión de nuevo.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"invalidInput\": \"Por favor, revisa tu opinión y vuelve a intentarlo.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"¿Nuevo cliente?\",\n                \"accountBenefits\": \"Crea una cuenta con nosotros y podrás:\",\n                \"fastCheckout\": \"Pagar más rápido\",\n                \"multipleAddresses\": \"Guardar varias direcciones de envío\",\n                \"ordersHistory\": \"Acceder a tu historial de pedidos\",\n                \"ordersTracking\": \"Rastrear nuevos pedidos\",\n                \"wishlists\": \"Guardar artículos en tu lista de deseos\",\n                \"cta\": \"Crear cuenta\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Olvidé la contraseña\",\n                \"subtitle\": \"A continuación, ingresa el correo electrónico asociado a tu cuenta. Te enviaremos instrucciones para restablecer tu contraseña.\",\n                \"confirmResetPassword\": \"Si la dirección de correo electrónico {email} está vinculada a una cuenta en nuestra tienda, te hemos enviado un correo electrónico de restablecimiento de contraseña. Revisa tu bandeja de entrada y tu carpeta de correo no deseado si no lo ves.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Registrar cuenta\",\n            \"heading\": \"Cuenta nueva\",\n            \"cta\": \"Crear cuenta\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"addressLine1Required\": \"La línea 1 es obligatoria\",\n                \"cityRequired\": \"La ciudad es obligatoria\",\n                \"countryRequired\": \"País es un campo obligatorio\",\n                \"stateRequired\": \"Estado/Provincia es un campo obligatorio.\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio.\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"No hay productos en esta marca\",\n                \"subtitle\": \"Intenta usar diferentes filtros.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Categorías\",\n            \"Empty\": {\n                \"title\": \"Ningún producto en esta categoría\",\n                \"subtitle\": \"Intenta usar diferentes filtros.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Resultados de búsqueda\",\n            \"searchResults\": \"Resultados de búsqueda para\",\n            \"subCategories\": \"Categorías\",\n            \"Breadcrumbs\": {\n                \"home\": \"Inicio\",\n                \"search\": \"Buscar\"\n            },\n            \"Empty\": {\n                \"title\": \"Lo sentimos, no hay resultados para “{term}”.\",\n                \"subtitle\": \"Prueba con otra búsqueda.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtros\",\n            \"resetFilters\": \"Restablecer filtros\",\n            \"Range\": {\n                \"apply\": \"Aplicar\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Envío gratuito\",\n                \"isFeaturedLabel\": \"Destacado\",\n                \"inStockLabel\": \"En existencias\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Ordenar por:\",\n            \"featuredItems\": \"Elementos destacados\",\n            \"bestSellingItems\": \"Artículos más vendidos\",\n            \"newestItems\": \"Artículos más nuevos\",\n            \"aToZ\": \"A a la Z\",\n            \"zToA\": \"Z a la A\",\n            \"byReview\": \"Por opinión\",\n            \"priceAscending\": \"Precio: ascendente\",\n            \"priceDescending\": \"Precio: descendente\",\n            \"relevance\": \"Relevancia\"\n        },\n        \"Compare\": {\n            \"compare\": \"Comparar\",\n            \"remove\": \"Quitar\",\n            \"maxCompareLimit\": \"Alcanzaste el número máximo de productos para comparar. Elimina un producto para agregar uno nuevo.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Direcciones\",\n            \"logout\": \"Cerrar sesión\",\n            \"orders\": \"Pedidos\",\n            \"settings\": \"Cuenta\",\n            \"wishlists\": \"Listas de deseos\"\n        },\n        \"Orders\": {\n            \"title\": \"Pedidos\",\n            \"orderNumber\": \"Núm. de pedido\",\n            \"totalPrice\": \"Total\",\n            \"viewDetails\": \"Ver detalles\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ningún pedido\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Details\": {\n                \"title\": \"Pedido núm.{orderNumber}\",\n                \"shippingAddress\": \"Dirección de envío\",\n                \"shippingMethod\": \"Método de envío\",\n                \"summaryTotal\": \"Total\",\n                \"destination\": \"Destino\",\n                \"destinationWithCount\": \"Destino {número, número}/{total, número}\",\n                \"digitalDelivery\": \"Entrega digital a {email}\",\n                \"subtotal\": \"Subtotal\",\n                \"shipping\": \"Envío\",\n                \"tax\": \"Impuesto\",\n                \"orderSummary\": \"Resumen del pedido\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Método de pago} other {Métodos de pago}}\",\n                \"paymentEndingInLabel\": \"Con terminación en\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Tarjeta de crédito\",\n                    \"giftCertificate\": \"Certificado de regalo\",\n                    \"storeCredit\": \"Crédito de la tienda\",\n                    \"other\": \"Otro\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Direcciones\",\n            \"cta\": \"Agregar dirección\",\n            \"edit\": \"Editar\",\n            \"delete\": \"Eliminar\",\n            \"cancel\": \"Cancelar\",\n            \"create\": \"Crear\",\n            \"update\": \"Actualizar\",\n            \"setDefault\": \"Establecer como predeterminado\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ninguna dirección\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"addressLine1Required\": \"La línea 1 es obligatoria\",\n                \"cityRequired\": \"La ciudad es obligatoria\",\n                \"countryRequired\": \"País es un campo obligatorio\",\n                \"stateRequired\": \"Estado/Provincia es un campo obligatorio.\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio.\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"CONFIGURACIÓN DE CUENTA\",\n            \"changePassword\": \"Cambiar contraseña\",\n            \"passwordUpdated\": \"¡La contraseña se actualizó correctamente!\",\n            \"settingsUpdated\": \"La configuración de la cuenta se actualizó correctamente.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"currentPassword\": \"Contraseña actual\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"cta\": \"Actualizar\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Preferencias de marketing\",\n                \"label\": \"Suscríbete a nuestro boletín informativo.\",\n                \"marketingPreferencesUpdated\": \"¡Las preferencias de marketing se actualizaron con éxito!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"firstNameTooSmall\": \"El nombre de pila debe tener al menos 2 caracteres\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"lastNameTooSmall\": \"El apellido debe tener al menos 2 caracteres\",\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"currentPasswordRequired\": \"La contraseña actual es obligatoria\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Por favor, confirme tu contraseña\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Acciones de la lista de deseos\",\n        \"title\": \"Listas de deseos\",\n        \"new\": \"Nueva lista de deseos\",\n        \"items\": \"{count, plural, =1 {1 artículo} other {# artículos}}\",\n        \"viewWishlist\": \"Ver lista\",\n        \"noWishlists\": \"No tienes ninguna lista de deseos\",\n        \"noWishlistsCallToAction\": \"Crea una lista de deseos\",\n        \"emptyWishlist\": \"No has agregado productos a esta lista de deseos.\",\n        \"share\": \"Compartir\",\n        \"shareSuccess\": \"La lista de deseos se compartió correctamente.\",\n        \"shareCopied\": \"La URL pública de la lista de deseos se copió a tu portapapeles.\",\n        \"shareDisabled\": \"Tu lista de deseos debe ser pública para que puedas compartirla.\",\n        \"makePublic\": \"Haz público\",\n        \"makePrivate\": \"Configurar como privada\",\n        \"rename\": \"Cambiar el nombre\",\n        \"delete\": \"Eliminar\",\n        \"removeButtonTitle\": \"Eliminar el producto de tu lista de deseos\",\n        \"Visibility\": {\n            \"public\": \"Público\",\n            \"private\": \"Privado\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Cancelar\",\n            \"close\": \"Cerrar\",\n            \"copy\": \"Copiar\",\n            \"create\": \"Crear\",\n            \"save\": \"Guardar\",\n            \"delete\": \"Eliminar\",\n            \"newTitle\": \"Crea una nueva lista de deseos\",\n            \"shareTitle\": \"Comparte {name}\",\n            \"renameTitle\": \"Renombrar {name}\",\n            \"deleteTitle\": \"Borrar {name}\",\n            \"changeVisibilityPublicTitle\": \"¿Hacer {name} público?\",\n            \"changeVisibilityPrivateTitle\": \"¿Hacer {name} privado?\",\n            \"makePublicContent\": \"¿Confirmas que quieres configurar <bold>{name}</bold> como pública? Si lo haces, otras personas podrán ver tu lista de deseos si tienen un enlace para acceder a ella.\",\n            \"makePrivateContent\": \"¿Confirmas que quieres configurar <bold>{name}</bold> como privada? Las personas con quienes hayas compartido tu lista de deseos ya no podrán verla.\",\n            \"deleteContent\": \"¿Confirmas que quieres eliminar <bold>{name}</bold>? Esta acción no puede deshacerse.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Nombre\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"El nombre de la lista de deseos no puede estar vacío.\",\n            \"updateFailed\": \"No se pudo actualizar tu lista de deseos. Vuelve a intentarlo.\",\n            \"deleteFailed\": \"No se pudo eliminar tu lista de deseos. Vuelve a intentarlo.\",\n            \"removeProductFailed\": \"No se pudo eliminar el producto de tu lista de deseos. Vuelve a intentarlo.\",\n            \"unauthorized\": \"No tienes autorización para realizar esta acción. Inicia sesión e inténtalo de nuevo.\",\n            \"unexpected\": \"Se produjo un error inesperado, inténtalo de nuevo\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"La lista de deseos se creó correctamente.\",\n            \"updateSuccess\": \"La lista de deseos se actualizó correctamente.\",\n            \"deleteSuccess\": \"La lista de deseos se eliminó correctamente.\",\n            \"removeItemSuccess\": \"Se eliminó el artículo de tu lista de deseos.\"\n        },\n        \"Button\": {\n            \"label\": \"Agregar a la lista de deseos\",\n            \"addToNewWishlist\": \"Agregar a una nueva lista de deseos\",\n            \"defaultWishlistName\": \"Mi lista de deseos\",\n            \"addSuccessMessage\": \"El producto se ha agregado a tu lista de deseos.\",\n            \"removeSuccessMessage\": \"El producto se ha eliminado de tu lista de deseos.\",\n            \"Errors\": {\n                \"addProductFailed\": \"No se pudo agregar el producto de tu lista de deseos, inténtalo de nuevo\",\n                \"removeProductFailed\": \"No se pudo eliminar el producto de tu lista de deseos, inténtalo de nuevo\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Lista de deseos pública\",\n        \"defaultName\": \"Lista pública de deseos\",\n        \"emptyWishlist\": \"Aún no hay productos en esta lista de deseos.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Inicio\",\n        \"Empty\": {\n            \"title\": \"No se encontraron publicaciones de blog\",\n            \"subtitle\": \"Vuelve a consultar más tarde para obtener más contenido\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Compartir\",\n            \"email\": \"Correo electrónico\",\n            \"print\": \"Imprimir\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Carrito\",\n        \"heading\": \"Tu carrito\",\n        \"proceedToCheckout\": \"Continuar con el proceso de pago\",\n        \"increment\": \"Aumenta la cantidad\",\n        \"decrement\": \"Disminuir cantidad\",\n        \"removeItem\": \"Eliminar artículo\",\n        \"cartCombined\": \"Notamos que tenías artículos guardados en un carrito anterior, así que los hemos agregado a tu carrito actual.\",\n        \"cartRestored\": \"Iniciaste un carrito en otro dispositivo y lo hemos restaurado aquí para que puedas continuar desde donde lo dejaste.\",\n        \"cartUpdateInProgress\": \"Tiene una actualización de carrito en proceso. ¿Estás seguro de que quieres salir de esta página? Es posible que se pierdan sus cambios.\",\n        \"originalPrice\": \"El precio original era {price}.\",\n        \"currentPrice\": \"El precio actual es {price}.\",\n        \"quantityReadyToShip\": \"{cantidad, número} listo para enviar\",\n        \"quantityOnBackorder\": \"{cantidad, número} estará en pedido pendiente\",\n        \"partiallyAvailable\": \"Solo {cantidad, número} disponible\",\n        \"CheckoutSummary\": {\n            \"title\": \"Resumen\",\n            \"subTotal\": \"Subtotal\",\n            \"discounts\": \"Descuentos\",\n            \"tax\": \"Impuesto\",\n            \"total\": \"Total\",\n            \"CouponCode\": {\n                \"apply\": \"Aplicar\",\n                \"couponCode\": \"Código de cupón\",\n                \"removeCouponCode\": \"Elimina el código de cupón\",\n                \"invalidCouponCode\": \"Ingresa un código de cupón válido\",\n                \"cartNotFound\": \"Se produjo un error al recuperar su carrito\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Envío\",\n                \"add\": \"Agregar\",\n                \"change\": \"Cambiar\",\n                \"cancel\": \"Cancelar\",\n                \"country\": \"País\",\n                \"city\": \"Ciudad\",\n                \"state\": \"Estado/Provincia\",\n                \"postalCode\": \"Código postal\",\n                \"updatedShippingOptions\": \"Actualiza las opciones de envío\",\n                \"viewShippingOptions\": \"Ver opciones de envío\",\n                \"editAddress\": \"Editar dirección\",\n                \"shippingOptions\": \"Opciones de envío\",\n                \"updateShipping\": \"Actualizar envío\",\n                \"addShipping\": \"Agregar envío\",\n                \"cartNotFound\": \"Se produjo un error al recuperar su carrito\",\n                \"noShippingOptions\": \"No hay opciones de envío disponibles para tu dirección\",\n                \"countryRequired\": \"País es un campo obligatorio\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Certificado de regalo\",\n            \"giftCertificateCode\": \"Código del certificado de regalo\",\n            \"removeGiftCertificate\": \"Eliminar certificado de regalo\",\n            \"apply\": \"Aplicar\",\n            \"to\": \"De\",\n            \"message\": \"Mensaje\",\n            \"invalidGiftCertificate\": \"Ingrese un código de certificado de regalo válido\",\n            \"cartNotFound\": \"Se produjo un error al recuperar su carrito\"\n        },\n        \"Empty\": {\n            \"title\": \"Su carrito está vacío.\",\n            \"subtitle\": \"Agrega algunos productos para comenzar.\",\n            \"cta\": \"Seguir comprando\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Se produjo un error al recuperar su carrito\",\n            \"lineItemNotFound\": \"Artículo no encontrado.\",\n            \"failedToUpdateQuantity\": \"No se pudo actualizar la cantidad.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Comparar productos\",\n        \"addToCart\": \"Agregar al carrito\",\n        \"next\": \"Siguientes productos\",\n        \"previous\": \"Productos anteriores\",\n        \"noProductsToCompare\": \"No hay productos para comparar\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Peso\",\n        \"description\": \"Descripción\",\n        \"noDescription\": \"No hay ninguna descripción disponible.\",\n        \"rating\": \"Calificación\",\n        \"noRatings\": \"No hay reseñas.\",\n        \"otherDetails\": \"Otros detalles\",\n        \"noOtherDetails\": \"No hay otros detalles.\",\n        \"viewOptions\": \"Opciones de visualización\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 artículo} other {# artículos}} agregado(s) a <cartLink>tu carrito</cartLink>\",\n        \"missingCart\": \"Carrito no encontrado. Vuelva a intentarlo más tarde.\",\n        \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Cantidad\",\n            \"increaseQuantity\": \"Aumenta la cantidad\",\n            \"decreaseQuantity\": \"Disminuir cantidad\",\n            \"emptySelectPlaceholder\": \"Seleccione una opción\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 artículo} other {# artículos}} agregado(s) a <cartLink>tu carrito</cartLink>\",\n            \"missingCart\": \"Carrito no encontrado. Intenta de nuevo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"variantRequiredError\": \"Para añadir este producto al carrito, debes seleccionar opciones.\",\n            \"increaseNumber\": \"Aumentar número\",\n            \"decreaseNumber\": \"Disminuir número\",\n            \"thumbnail\": \"Ver número de imagen\",\n            \"additionalInformation\": \"Información adicional\",\n            \"currentStock\": \"{cantidad, número} en stock\",\n            \"backorderQuantity\": \"{cantidad, número} estará en espera\",\n            \"loadingMoreImages\": \"Cargando más imágenes\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 imagen más cargada} other {# más imágenes cargadas}}\",\n            \"Submit\": {\n                \"addToCart\": \"Agregar al carrito\",\n                \"outOfStock\": \"Agotado/a\",\n                \"preorder\": \"Pedido anticipado\",\n                \"unavailable\": \"No disponible\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Especificaciones\",\n                \"warranty\": \"Garantía\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Peso\",\n                \"condition\": \"Condición\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Productos relacionados\",\n            \"noRelatedProducts\": \"No se encontraron productos relacionados\",\n            \"browseCatalog\": \"Intenta navegar por nuestro catálogo completo de productos.\",\n            \"cta\": \"Comprar todos\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\",\n            \"scrollbar\": \"Barra de desplazamiento de productos relacionados\"\n        },\n        \"Reviews\": {\n            \"title\": \"Reseñas\",\n            \"empty\": \"No se han agregado reseñas para este producto.\",\n            \"previous\": \"Reseñas anteriores\",\n            \"next\": \"Próximas reseñas\",\n            \"Form\": {\n                \"button\": \"Escribe una opinión\",\n                \"title\": \"Escribe una opinión\",\n                \"submit\": \"Enviar\",\n                \"cancel\": \"Cancelar\",\n                \"ratingLabel\": \"Calificación\",\n                \"titleLabel\": \"Título\",\n                \"reviewLabel\": \"Revisar\",\n                \"nameLabel\": \"Nombre\",\n                \"emailLabel\": \"Correo electrónico\",\n                \"successMessage\": \"¡Tu reseña fue enviada con éxito!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Se requiere título\",\n                    \"authorRequired\": \"El campo Nombre es obligatorio\",\n                    \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                    \"textRequired\": \"Es necesario revisar\",\n                    \"ratingRequired\": \"Se requiere una calificación\",\n                    \"ratingTooSmall\": \"La puntuación debe ser al menos 1\",\n                    \"ratingTooLarge\": \"El puntaje debe ser como máximo 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Inicio\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Inicio\",\n            \"Form\": {\n                \"success\": \"Gracias por ponerte en contacto. Nos comunicaremos contigo pronto.\",\n                \"successCta\": \"Seguir comprando\",\n                \"fullName\": \"Nombre completo\",\n                \"companyName\": \"Nombre de la empresa\",\n                \"phone\": \"Teléfono\",\n                \"orderNo\": \"Número de pedido\",\n                \"rma\": \"Número de RMA (autorización de devolución)\",\n                \"email\": \"Correo electrónico\",\n                \"comments\": \"Comentarios/Preguntas\",\n                \"cta\": \"Enviar formulario\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"MANTENIMIENTO\",\n        \"message\": \"Estamos fuera de servicio por mantenimiento\",\n        \"contactUs\": \"Puedes contactarnos en:\"\n    },\n    \"Error\": {\n        \"title\": \"Hubo un error en el servidor.\",\n        \"subtitle\": \"Vuelva a intentarlo más tarde.\",\n        \"cta\": \"Volver a intentarlo\"\n    },\n    \"NotFound\": {\n        \"title\": \"No pudimos encontrar esa página.\",\n        \"subtitle\": \"Intenta buscar algo más o regresa a la página de inicio.\",\n        \"featuredProducts\": \"Productos destacados\",\n        \"search\": \"Buscar\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Inicio\",\n            \"toggleNavigation\": \"Alternar navegación\",\n            \"Icons\": {\n                \"account\": \"Perfil\",\n                \"cart\": \"Carrito\",\n                \"search\": \"Abrir ventana emergente de búsqueda\",\n                \"giftCertificates\": \"Certificados de regalo\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Cambiar moneda\",\n                \"invalidCurrency\": \"Moneda inválida\",\n                \"errorUpdatingCurrency\": \"Error al actualizar la moneda de tu carrito. Vuelve a intentarlo.\"\n            },\n            \"Search\": {\n                \"products\": \"Productos\",\n                \"categories\": \"Categorías\",\n                \"brands\": \"Marcas\",\n                \"noSearchResultsTitle\": \"Lo sentimos, no hay resultados para “{term}”.\",\n                \"noSearchResultsSubtitle\": \"Prueba con otra búsqueda.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo.\",\n                \"inputPlaceholder\": \"Buscar productos, categorías, marcas...\",\n                \"submitLabel\": \"Buscar\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Inicio\",\n            \"contactUs\": \"Contacto\",\n            \"socialMediaLinks\": \"Enlaces a redes sociales\",\n            \"categories\": \"Categorías\",\n            \"brands\": \"Marcas\",\n            \"navigate\": \"Navegar\",\n            \"giftCertificates\": \"Certificados de regalo\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Suscríbete a nuestro boletín informativo\",\n            \"placeholder\": \"Ingresa tu correo electrónico\",\n            \"description\": \"Mantente al día con las últimas noticias y ofertas de nuestra tienda.\",\n            \"subscribedToNewsletter\": \"¡Te suscribiste a nuestro boletín!\",\n            \"Errors\": {\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"invalidEmail\": \"Por favor, introduzca una dirección de email válida\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Rechazar todo\",\n                \"acceptAll\": \"Aceptar todo\",\n                \"customize\": \"Personalizar\",\n                \"save\": \"Guardar la configuración\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"Valoramos su privacidad\",\n                \"description\": \"Este sitio emplea cookies para mejorar su experiencia de navegación, analizar el tráfico del sitio y mostrar contenido personalizado.\",\n                \"privacyPolicy\": \"Política de Privacidad\"\n            },\n            \"Dialog\": {\n                \"title\": \"Configuración de privacidad\",\n                \"description\": \"Personaliza tu configuración de privacidad aquí. Puede elegir qué tipos de cookies y tecnologías de seguimiento le gustaría permitir.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Estrictamente necesario\",\n                    \"description\": \"Estas cookies son Essentials para que el sitio web funcione correctamente y no se pueden desactivar.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Funcionalidad\",\n                    \"description\": \"Estas cookies permiten mejorar la funcionalidad y la personalización del sitio web.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"Estas cookies se emplean para ofrecer anuncios relevantes y realizar un seguimiento de su efectividad.\"\n                },\n                \"measurement\": {\n                    \"title\": \"DATOS ANALÍTICOS\",\n                    \"description\": \"Estas cookies nos ayudan a comprender cómo interactúan los visitantes con el sitio web y mejorar su rendimiento.\"\n                },\n                \"experience\": {\n                    \"title\": \"Experiencia\",\n                    \"description\": \"Estas cookies nos ayudan a brindar una mejor experiencia de usuario y probar nuevas funciones.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"El precio original era {price}.\",\n            \"currentPrice\": \"El precio actual es {price}.\",\n            \"range\": \"Precio de {minValue} a {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Certificados de regalo\",\n        \"description\": \"Regala el regalo perfecto que nunca pasa de moda. Deja que tus colegas y seres queridos elijan exactamente lo que quieren de toda nuestra colección.\",\n        \"purchaseLabel\": \"Comprar ahora\",\n        \"checkBalanceLabel\": \"Consultar saldo\",\n        \"expiresAtLabel\": \"Válido hasta\",\n        \"CheckBalance\": {\n            \"title\": \"Consultar saldo\",\n            \"description\": \"Puede consultar el saldo y obtener la información sobre su certificado de regalo escribiendo el código en el cuadro a continuación.\",\n            \"inputLabel\": \"Código\",\n            \"inputPlaceholder\": \"xxx-xxx-xxx-xxx\",\n            \"purchasedDateLabel\": \"Comprado\",\n            \"senderLabel\": \"Para\",\n            \"Errors\": {\n                \"invalidCode\": \"El código del certificado de regalo que ingresó no es válido. Verifique el código e inténtelo de nuevo.\",\n                \"codeRequired\": \"Ingresa el código del certificado de regalo.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Comprar certificado de regalo\",\n            \"title\": \"Certificado de regalo digital\",\n            \"description\": \"Explore nuestros certificados de regalo, perfectos para cualquier ocasión. Elige la cantidad y personaliza tu mensaje.\",\n            \"successMessage\": \"Se ha agregado un certificado de regalo a <cartLink> su carrito</cartLink>\",\n            \"missingCart\": \"Carrito no encontrado. Vuelva a intentarlo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"Form\": {\n                \"amountLabel\": \"Monto\",\n                \"customAmountLabel\": \"Importe (entre {minAmount} y {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Selecciona una cantidad\",\n                \"customAmountPlaceholder\": \"Ingrese la cantidad personalizada\",\n                \"senderNameLabel\": \"Tu nombre\",\n                \"senderEmailLabel\": \"Tu correo electrónico\",\n                \"recipientNameLabel\": \"Nombre del destinatario\",\n                \"recipientEmailLabel\": \"Correo electrónico del destinatario\",\n                \"namePlaceholder\": \"Ingrese el nombre\",\n                \"emailPlaceholder\": \"Ingresa el correo electrónico\",\n                \"messageLabel\": \"Mensaje\",\n                \"messagePlaceholder\": \"Ingrese su mensaje (opcional)\",\n                \"nonRefundableCheckboxLabel\": \"Acepto que los certificados de regalo no son reembolsables\",\n                \"expiryCheckboxLabel\": \"Reconozco que este Certificado de Regalo caducará el {expiryDate}\",\n                \"ctaLabel\": \"Agregar al carrito\",\n                \"Errors\": {\n                    \"amountRequired\": \"Por favor, selecciona o introduce el importe de un vale regalo\",\n                    \"amountInvalid\": \"Por favor, seleccione un importe válido de un certificado regalo\",\n                    \"amountOutOfRange\": \"Por favor, introduzca una cantidad entre {minAmount} y {maxAmount}\",\n                    \"unexpectedSettingsError\": \"Se produjo un error inesperado al recuperar la configuración del certificado de regalo. Intente de nuevo más tarde.\",\n                    \"senderNameRequired\": \"Tu nombre es obligatorio\",\n                    \"senderEmailRequired\": \"Tu correo electrónico es obligatorio\",\n                    \"recipientNameRequired\": \"Se requiere el nombre del destinatario\",\n                    \"recipientEmailRequired\": \"Se requiere el email del destinatario\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                    \"checkboxRequired\": \"Debes marcar esta casilla para continuar\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"Opcional\",\n        \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n        \"Errors\": {\n            \"invalidInput\": \"Por favor, revisa tu opinión y vuelve a intentarlo\",\n            \"invalidFormat\": \"El valor introducido no coincide con el formato requerido\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/es-PE.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Nuevos hallazgos para cada ocasión\",\n                \"description\": \"Explora nuestras últimas novedades seleccionadas para ofrecerte estilo, funcionalidad e inspiración. Compra ahora y descubre tu próximo favorito.\",\n                \"alt\": \"Cinco plantas pequeñas en macetas exhibidas sobre bloques apilados de color beige, con una variedad de follaje verde en macetas de color gris oscuro contra un fondo neutro.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide02\": {\n                \"title\": \"Descubre las novedades\",\n                \"description\": \"Compra nuestras últimas novedades y encuentra algo fresco y emocionante para tu hogar.\",\n                \"alt\": \"Manos extendiéndose para sostener un helecho verde en una canasta tejida con un lazo decorativo, sobre un fondo beige con sombras suaves.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide03\": {\n                \"title\": \"Algo para todos\",\n                \"description\": \"No te pierdas las ofertas exclusivas en nuestros productos más vendidos. Compra hoy y ahorra mucho dinero en los artículos que te gustan.\",\n                \"alt\": \"Primer plano de una hoja verde vibrante con perforaciones, que muestra su textura suave y detalles naturales.\",\n                \"cta\": \"Comprar ahora\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Colección destacada\",\n            \"description\": \"Explora nuestras mejores selecciones en esta colección destacada. ¡Encuentra el regalo perfecto o date un capricho!\",\n            \"cta\": \"Ver más\",\n            \"emptyStateTitle\": \"No se encontraron productos\",\n            \"emptyStateSubtitle\": \"Intenta navegar por nuestro catálogo completo de productos.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Novedades\",\n            \"description\": \"Nuestros últimos productos están aquí. Descubre lo nuevo de la tienda.\",\n            \"cta\": \"Ver todas\",\n            \"emptyStateTitle\": \"No se encontraron productos\",\n            \"emptyStateSubtitle\": \"Intenta navegar por nuestro catálogo completo de productos.\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Cambiar contraseña\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"passwordUpdated\": \"¡La contraseña se actualizó correctamente!\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Por favor, confirme tu contraseña\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Inicio de sesión\",\n            \"heading\": \"Inicio de sesión\",\n            \"forgotPassword\": \"¿Olvidaste tu contraseña?\",\n            \"cta\": \"Inicio de sesión\",\n            \"email\": \"Correo electrónico\",\n            \"password\": \"Contraseña\",\n            \"invalidCredentials\": \"Tu dirección de correo electrónico o tu contraseña son incorrectas. Intenta iniciar sesión de nuevo o restablece tu contraseña\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"passwordResetRequired\": \"Se requiere restablecimiento de contraseña. Por favor, revisa tu email para ver instrucciones para restablecer tu contraseña.\",\n            \"invalidToken\": \"Tu enlace de acceso es inválido o caducó. Por favor, intenta iniciar sesión de nuevo.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"invalidInput\": \"Por favor, revisa tu opinión y vuelve a intentarlo.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"¿Nuevo cliente?\",\n                \"accountBenefits\": \"Crea una cuenta con nosotros y podrás:\",\n                \"fastCheckout\": \"Pagar más rápido\",\n                \"multipleAddresses\": \"Guardar varias direcciones de envío\",\n                \"ordersHistory\": \"Acceder a tu historial de pedidos\",\n                \"ordersTracking\": \"Rastrear nuevos pedidos\",\n                \"wishlists\": \"Guardar artículos en tu lista de deseos\",\n                \"cta\": \"Crear cuenta\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Olvidé la contraseña\",\n                \"subtitle\": \"A continuación, ingresa el correo electrónico asociado a tu cuenta. Te enviaremos instrucciones para restablecer tu contraseña.\",\n                \"confirmResetPassword\": \"Si la dirección de correo electrónico {email} está vinculada a una cuenta en nuestra tienda, te hemos enviado un correo electrónico de restablecimiento de contraseña. Revisa tu bandeja de entrada y tu carpeta de correo no deseado si no lo ves.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Registrar cuenta\",\n            \"heading\": \"Cuenta nueva\",\n            \"cta\": \"Crear cuenta\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"addressLine1Required\": \"La línea 1 es obligatoria\",\n                \"cityRequired\": \"La ciudad es obligatoria\",\n                \"countryRequired\": \"País es un campo obligatorio\",\n                \"stateRequired\": \"Estado/Provincia es un campo obligatorio.\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio.\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"No hay productos en esta marca\",\n                \"subtitle\": \"Intenta usar diferentes filtros.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Categorías\",\n            \"Empty\": {\n                \"title\": \"Ningún producto en esta categoría\",\n                \"subtitle\": \"Intenta usar diferentes filtros.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Resultados de búsqueda\",\n            \"searchResults\": \"Resultados de búsqueda para\",\n            \"subCategories\": \"Categorías\",\n            \"Breadcrumbs\": {\n                \"home\": \"Inicio\",\n                \"search\": \"Buscar\"\n            },\n            \"Empty\": {\n                \"title\": \"Lo sentimos, no hay resultados para “{term}”.\",\n                \"subtitle\": \"Prueba con otra búsqueda.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtros\",\n            \"resetFilters\": \"Restablecer filtros\",\n            \"Range\": {\n                \"apply\": \"Aplicar\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Envío gratuito\",\n                \"isFeaturedLabel\": \"Destacado\",\n                \"inStockLabel\": \"En existencias\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Ordenar por:\",\n            \"featuredItems\": \"Elementos destacados\",\n            \"bestSellingItems\": \"Artículos más vendidos\",\n            \"newestItems\": \"Artículos más nuevos\",\n            \"aToZ\": \"A a la Z\",\n            \"zToA\": \"Z a la A\",\n            \"byReview\": \"Por opinión\",\n            \"priceAscending\": \"Precio: ascendente\",\n            \"priceDescending\": \"Precio: descendente\",\n            \"relevance\": \"Relevancia\"\n        },\n        \"Compare\": {\n            \"compare\": \"Comparar\",\n            \"remove\": \"Quitar\",\n            \"maxCompareLimit\": \"Alcanzaste el número máximo de productos para comparar. Elimina un producto para agregar uno nuevo.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Direcciones\",\n            \"logout\": \"Cerrar sesión\",\n            \"orders\": \"Pedidos\",\n            \"settings\": \"Cuenta\",\n            \"wishlists\": \"Listas de deseos\"\n        },\n        \"Orders\": {\n            \"title\": \"Pedidos\",\n            \"orderNumber\": \"Núm. de pedido\",\n            \"totalPrice\": \"Total\",\n            \"viewDetails\": \"Ver detalles\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ningún pedido\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Details\": {\n                \"title\": \"Pedido núm.{orderNumber}\",\n                \"shippingAddress\": \"Dirección de envío\",\n                \"shippingMethod\": \"Método de envío\",\n                \"summaryTotal\": \"Total\",\n                \"destination\": \"Destino\",\n                \"destinationWithCount\": \"Destino {número, número}/{total, número}\",\n                \"digitalDelivery\": \"Entrega digital a {email}\",\n                \"subtotal\": \"Subtotal\",\n                \"shipping\": \"Envío\",\n                \"tax\": \"Impuesto\",\n                \"orderSummary\": \"Resumen del pedido\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Método de pago} other {Métodos de pago}}\",\n                \"paymentEndingInLabel\": \"Con terminación en\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Tarjeta de crédito\",\n                    \"giftCertificate\": \"Certificado de regalo\",\n                    \"storeCredit\": \"Crédito de la tienda\",\n                    \"other\": \"Otro\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Direcciones\",\n            \"cta\": \"Agregar dirección\",\n            \"edit\": \"Editar\",\n            \"delete\": \"Eliminar\",\n            \"cancel\": \"Cancelar\",\n            \"create\": \"Crear\",\n            \"update\": \"Actualizar\",\n            \"setDefault\": \"Establecer como predeterminado\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ninguna dirección\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"addressLine1Required\": \"La línea 1 es obligatoria\",\n                \"cityRequired\": \"La ciudad es obligatoria\",\n                \"countryRequired\": \"País es un campo obligatorio\",\n                \"stateRequired\": \"Estado/Provincia es un campo obligatorio.\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio.\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"CONFIGURACIÓN DE CUENTA\",\n            \"changePassword\": \"Cambiar contraseña\",\n            \"passwordUpdated\": \"¡La contraseña se actualizó correctamente!\",\n            \"settingsUpdated\": \"La configuración de la cuenta se actualizó correctamente.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"currentPassword\": \"Contraseña actual\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"cta\": \"Actualizar\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Preferencias de marketing\",\n                \"label\": \"Suscríbete a nuestro boletín informativo.\",\n                \"marketingPreferencesUpdated\": \"¡Las preferencias de marketing se actualizaron con éxito!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"firstNameTooSmall\": \"El nombre de pila debe tener al menos 2 caracteres\",\n                \"lastNameRequired\": \"El campo Apellido es obligatorio.\",\n                \"lastNameTooSmall\": \"El apellido debe tener al menos 2 caracteres\",\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                \"currentPasswordRequired\": \"La contraseña actual es obligatoria\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe ser al menos {minLength, plural, =1 {1 carácter} other {# caracteres}} longitud\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Por favor, confirme tu contraseña\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Acciones de la lista de deseos\",\n        \"title\": \"Listas de deseos\",\n        \"new\": \"Nueva lista de deseos\",\n        \"items\": \"{count, plural, =1 {1 artículo} other {# artículos}}\",\n        \"viewWishlist\": \"Ver lista\",\n        \"noWishlists\": \"No tienes ninguna lista de deseos\",\n        \"noWishlistsCallToAction\": \"Crea una lista de deseos\",\n        \"emptyWishlist\": \"No has agregado productos a esta lista de deseos.\",\n        \"share\": \"Compartir\",\n        \"shareSuccess\": \"La lista de deseos se compartió correctamente.\",\n        \"shareCopied\": \"La URL pública de la lista de deseos se copió a tu portapapeles.\",\n        \"shareDisabled\": \"Tu lista de deseos debe ser pública para que puedas compartirla.\",\n        \"makePublic\": \"Haz público\",\n        \"makePrivate\": \"Configurar como privada\",\n        \"rename\": \"Cambiar el nombre\",\n        \"delete\": \"Eliminar\",\n        \"removeButtonTitle\": \"Eliminar el producto de tu lista de deseos\",\n        \"Visibility\": {\n            \"public\": \"Público\",\n            \"private\": \"Privado\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Cancelar\",\n            \"close\": \"Cerrar\",\n            \"copy\": \"Copiar\",\n            \"create\": \"Crear\",\n            \"save\": \"Guardar\",\n            \"delete\": \"Eliminar\",\n            \"newTitle\": \"Crea una nueva lista de deseos\",\n            \"shareTitle\": \"Comparte {name}\",\n            \"renameTitle\": \"Renombrar {name}\",\n            \"deleteTitle\": \"Borrar {name}\",\n            \"changeVisibilityPublicTitle\": \"¿Hacer {name} público?\",\n            \"changeVisibilityPrivateTitle\": \"¿Hacer {name} privado?\",\n            \"makePublicContent\": \"¿Confirmas que quieres configurar <bold>{name}</bold> como pública? Si lo haces, otras personas podrán ver tu lista de deseos si tienen un enlace para acceder a ella.\",\n            \"makePrivateContent\": \"¿Confirmas que quieres configurar <bold>{name}</bold> como privada? Las personas con quienes hayas compartido tu lista de deseos ya no podrán verla.\",\n            \"deleteContent\": \"¿Confirmas que quieres eliminar <bold>{name}</bold>? Esta acción no puede deshacerse.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Nombre\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"El nombre de la lista de deseos no puede estar vacío.\",\n            \"updateFailed\": \"No se pudo actualizar tu lista de deseos. Vuelve a intentarlo.\",\n            \"deleteFailed\": \"No se pudo eliminar tu lista de deseos. Vuelve a intentarlo.\",\n            \"removeProductFailed\": \"No se pudo eliminar el producto de tu lista de deseos. Vuelve a intentarlo.\",\n            \"unauthorized\": \"No tienes autorización para realizar esta acción. Inicia sesión e inténtalo de nuevo.\",\n            \"unexpected\": \"Se produjo un error inesperado, inténtalo de nuevo\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"La lista de deseos se creó correctamente.\",\n            \"updateSuccess\": \"La lista de deseos se actualizó correctamente.\",\n            \"deleteSuccess\": \"La lista de deseos se eliminó correctamente.\",\n            \"removeItemSuccess\": \"Se eliminó el artículo de tu lista de deseos.\"\n        },\n        \"Button\": {\n            \"label\": \"Agregar a la lista de deseos\",\n            \"addToNewWishlist\": \"Agregar a una nueva lista de deseos\",\n            \"defaultWishlistName\": \"Mi lista de deseos\",\n            \"addSuccessMessage\": \"El producto se ha agregado a tu lista de deseos.\",\n            \"removeSuccessMessage\": \"El producto se ha eliminado de tu lista de deseos.\",\n            \"Errors\": {\n                \"addProductFailed\": \"No se pudo agregar el producto de tu lista de deseos, inténtalo de nuevo\",\n                \"removeProductFailed\": \"No se pudo eliminar el producto de tu lista de deseos, inténtalo de nuevo\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Lista de deseos pública\",\n        \"defaultName\": \"Lista pública de deseos\",\n        \"emptyWishlist\": \"Aún no hay productos en esta lista de deseos.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Inicio\",\n        \"Empty\": {\n            \"title\": \"No se encontraron publicaciones de blog\",\n            \"subtitle\": \"Vuelve a consultar más tarde para obtener más contenido\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Compartir\",\n            \"email\": \"Correo electrónico\",\n            \"print\": \"Imprimir\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Carrito\",\n        \"heading\": \"Tu carrito\",\n        \"proceedToCheckout\": \"Continuar con el proceso de pago\",\n        \"increment\": \"Aumenta la cantidad\",\n        \"decrement\": \"Disminuir cantidad\",\n        \"removeItem\": \"Eliminar artículo\",\n        \"cartCombined\": \"Notamos que tenías artículos guardados en un carrito anterior, así que los hemos agregado a tu carrito actual.\",\n        \"cartRestored\": \"Iniciaste un carrito en otro dispositivo y lo hemos restaurado aquí para que puedas continuar desde donde lo dejaste.\",\n        \"cartUpdateInProgress\": \"Tiene una actualización de carrito en proceso. ¿Estás seguro de que quieres salir de esta página? Es posible que se pierdan sus cambios.\",\n        \"originalPrice\": \"El precio original era {price}.\",\n        \"currentPrice\": \"El precio actual es {price}.\",\n        \"quantityReadyToShip\": \"{cantidad, número} listo para enviar\",\n        \"quantityOnBackorder\": \"{cantidad, número} estará en pedido pendiente\",\n        \"partiallyAvailable\": \"Solo {cantidad, número} disponible\",\n        \"CheckoutSummary\": {\n            \"title\": \"Resumen\",\n            \"subTotal\": \"Subtotal\",\n            \"discounts\": \"Descuentos\",\n            \"tax\": \"Impuesto\",\n            \"total\": \"Total\",\n            \"CouponCode\": {\n                \"apply\": \"Aplicar\",\n                \"couponCode\": \"Código de cupón\",\n                \"removeCouponCode\": \"Elimina el código de cupón\",\n                \"invalidCouponCode\": \"Ingresa un código de cupón válido\",\n                \"cartNotFound\": \"Se produjo un error al recuperar su carrito\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Envío\",\n                \"add\": \"Agregar\",\n                \"change\": \"Cambiar\",\n                \"cancel\": \"Cancelar\",\n                \"country\": \"País\",\n                \"city\": \"Ciudad\",\n                \"state\": \"Estado/Provincia\",\n                \"postalCode\": \"Código postal\",\n                \"updatedShippingOptions\": \"Actualiza las opciones de envío\",\n                \"viewShippingOptions\": \"Ver opciones de envío\",\n                \"editAddress\": \"Editar dirección\",\n                \"shippingOptions\": \"Opciones de envío\",\n                \"updateShipping\": \"Actualizar envío\",\n                \"addShipping\": \"Agregar envío\",\n                \"cartNotFound\": \"Se produjo un error al recuperar su carrito\",\n                \"noShippingOptions\": \"No hay opciones de envío disponibles para tu dirección\",\n                \"countryRequired\": \"País es un campo obligatorio\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Certificado de regalo\",\n            \"giftCertificateCode\": \"Código del certificado de regalo\",\n            \"removeGiftCertificate\": \"Eliminar certificado de regalo\",\n            \"apply\": \"Aplicar\",\n            \"to\": \"De\",\n            \"message\": \"Mensaje\",\n            \"invalidGiftCertificate\": \"Ingrese un código de certificado de regalo válido\",\n            \"cartNotFound\": \"Se produjo un error al recuperar su carrito\"\n        },\n        \"Empty\": {\n            \"title\": \"Su carrito está vacío.\",\n            \"subtitle\": \"Agrega algunos productos para comenzar.\",\n            \"cta\": \"Seguir comprando\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Se produjo un error al recuperar su carrito\",\n            \"lineItemNotFound\": \"Artículo no encontrado.\",\n            \"failedToUpdateQuantity\": \"No se pudo actualizar la cantidad.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Comparar productos\",\n        \"addToCart\": \"Agregar al carrito\",\n        \"next\": \"Siguientes productos\",\n        \"previous\": \"Productos anteriores\",\n        \"noProductsToCompare\": \"No hay productos para comparar\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Peso\",\n        \"description\": \"Descripción\",\n        \"noDescription\": \"No hay ninguna descripción disponible.\",\n        \"rating\": \"Calificación\",\n        \"noRatings\": \"No hay reseñas.\",\n        \"otherDetails\": \"Otros detalles\",\n        \"noOtherDetails\": \"No hay otros detalles.\",\n        \"viewOptions\": \"Opciones de visualización\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 artículo} other {# artículos}} agregado(s) a <cartLink>tu carrito</cartLink>\",\n        \"missingCart\": \"Carrito no encontrado. Vuelva a intentarlo más tarde.\",\n        \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Cantidad\",\n            \"increaseQuantity\": \"Aumenta la cantidad\",\n            \"decreaseQuantity\": \"Disminuir cantidad\",\n            \"emptySelectPlaceholder\": \"Seleccione una opción\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 artículo} other {# artículos}} agregado(s) a <cartLink>tu carrito</cartLink>\",\n            \"missingCart\": \"Carrito no encontrado. Intenta de nuevo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"variantRequiredError\": \"Para añadir este producto al carrito, debes seleccionar opciones.\",\n            \"increaseNumber\": \"Aumentar número\",\n            \"decreaseNumber\": \"Disminuir número\",\n            \"thumbnail\": \"Ver número de imagen\",\n            \"additionalInformation\": \"Información adicional\",\n            \"currentStock\": \"{cantidad, número} en stock\",\n            \"backorderQuantity\": \"{cantidad, número} estará en espera\",\n            \"loadingMoreImages\": \"Cargando más imágenes\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 imagen más cargada} other {# más imágenes cargadas}}\",\n            \"Submit\": {\n                \"addToCart\": \"Agregar al carrito\",\n                \"outOfStock\": \"Agotado/a\",\n                \"preorder\": \"Pedido anticipado\",\n                \"unavailable\": \"No disponible\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Especificaciones\",\n                \"warranty\": \"Garantía\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Peso\",\n                \"condition\": \"Condición\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Productos relacionados\",\n            \"noRelatedProducts\": \"No se encontraron productos relacionados\",\n            \"browseCatalog\": \"Intenta navegar por nuestro catálogo completo de productos.\",\n            \"cta\": \"Comprar todos\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\",\n            \"scrollbar\": \"Barra de desplazamiento de productos relacionados\"\n        },\n        \"Reviews\": {\n            \"title\": \"Reseñas\",\n            \"empty\": \"No se han agregado reseñas para este producto.\",\n            \"previous\": \"Reseñas anteriores\",\n            \"next\": \"Próximas reseñas\",\n            \"Form\": {\n                \"button\": \"Escribe una opinión\",\n                \"title\": \"Escribe una opinión\",\n                \"submit\": \"Enviar\",\n                \"cancel\": \"Cancelar\",\n                \"ratingLabel\": \"Calificación\",\n                \"titleLabel\": \"Título\",\n                \"reviewLabel\": \"Revisar\",\n                \"nameLabel\": \"Nombre\",\n                \"emailLabel\": \"Correo electrónico\",\n                \"successMessage\": \"¡Tu reseña fue enviada con éxito!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Se requiere título\",\n                    \"authorRequired\": \"El campo Nombre es obligatorio\",\n                    \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                    \"textRequired\": \"Es necesario revisar\",\n                    \"ratingRequired\": \"Se requiere una calificación\",\n                    \"ratingTooSmall\": \"La puntuación debe ser al menos 1\",\n                    \"ratingTooLarge\": \"El puntaje debe ser como máximo 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Inicio\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Inicio\",\n            \"Form\": {\n                \"success\": \"Gracias por ponerte en contacto. Nos comunicaremos contigo pronto.\",\n                \"successCta\": \"Seguir comprando\",\n                \"fullName\": \"Nombre completo\",\n                \"companyName\": \"Nombre de la empresa\",\n                \"phone\": \"Teléfono\",\n                \"orderNo\": \"Número de pedido\",\n                \"rma\": \"Número de RMA (autorización de devolución)\",\n                \"email\": \"Correo electrónico\",\n                \"comments\": \"Comentarios/Preguntas\",\n                \"cta\": \"Enviar formulario\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"MANTENIMIENTO\",\n        \"message\": \"Estamos fuera de servicio por mantenimiento\",\n        \"contactUs\": \"Puedes contactarnos en:\"\n    },\n    \"Error\": {\n        \"title\": \"Hubo un error en el servidor.\",\n        \"subtitle\": \"Vuelva a intentarlo más tarde.\",\n        \"cta\": \"Volver a intentarlo\"\n    },\n    \"NotFound\": {\n        \"title\": \"No pudimos encontrar esa página.\",\n        \"subtitle\": \"Intenta buscar algo más o regresa a la página de inicio.\",\n        \"featuredProducts\": \"Productos destacados\",\n        \"search\": \"Buscar\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Inicio\",\n            \"toggleNavigation\": \"Alternar navegación\",\n            \"Icons\": {\n                \"account\": \"Perfil\",\n                \"cart\": \"Carrito\",\n                \"search\": \"Abrir ventana emergente de búsqueda\",\n                \"giftCertificates\": \"Certificados de regalo\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Cambiar moneda\",\n                \"invalidCurrency\": \"Moneda inválida\",\n                \"errorUpdatingCurrency\": \"Error al actualizar la moneda de tu carrito. Vuelve a intentarlo.\"\n            },\n            \"Search\": {\n                \"products\": \"Productos\",\n                \"categories\": \"Categorías\",\n                \"brands\": \"Marcas\",\n                \"noSearchResultsTitle\": \"Lo sentimos, no hay resultados para “{term}”.\",\n                \"noSearchResultsSubtitle\": \"Prueba con otra búsqueda.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo.\",\n                \"inputPlaceholder\": \"Buscar productos, categorías, marcas...\",\n                \"submitLabel\": \"Buscar\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Inicio\",\n            \"contactUs\": \"Contacto\",\n            \"socialMediaLinks\": \"Enlaces a redes sociales\",\n            \"categories\": \"Categorías\",\n            \"brands\": \"Marcas\",\n            \"navigate\": \"Navegar\",\n            \"giftCertificates\": \"Certificados de regalo\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Suscríbete a nuestro boletín informativo\",\n            \"placeholder\": \"Ingresa tu correo electrónico\",\n            \"description\": \"Mantente al día con las últimas noticias y ofertas de nuestra tienda.\",\n            \"subscribedToNewsletter\": \"¡Te suscribiste a nuestro boletín!\",\n            \"Errors\": {\n                \"emailRequired\": \"El campo de correo electrónico es obligatorio\",\n                \"invalidEmail\": \"Por favor, introduzca una dirección de email válida\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Rechazar todo\",\n                \"acceptAll\": \"Aceptar todo\",\n                \"customize\": \"Personalizar\",\n                \"save\": \"Guardar la configuración\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"Valoramos su privacidad\",\n                \"description\": \"Este sitio emplea cookies para mejorar su experiencia de navegación, analizar el tráfico del sitio y mostrar contenido personalizado.\",\n                \"privacyPolicy\": \"Política de Privacidad\"\n            },\n            \"Dialog\": {\n                \"title\": \"Configuración de privacidad\",\n                \"description\": \"Personaliza tu configuración de privacidad aquí. Puede elegir qué tipos de cookies y tecnologías de seguimiento le gustaría permitir.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Estrictamente necesario\",\n                    \"description\": \"Estas cookies son Essentials para que el sitio web funcione correctamente y no se pueden desactivar.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Funcionalidad\",\n                    \"description\": \"Estas cookies permiten mejorar la funcionalidad y la personalización del sitio web.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"Estas cookies se emplean para ofrecer anuncios relevantes y realizar un seguimiento de su efectividad.\"\n                },\n                \"measurement\": {\n                    \"title\": \"DATOS ANALÍTICOS\",\n                    \"description\": \"Estas cookies nos ayudan a comprender cómo interactúan los visitantes con el sitio web y mejorar su rendimiento.\"\n                },\n                \"experience\": {\n                    \"title\": \"Experiencia\",\n                    \"description\": \"Estas cookies nos ayudan a brindar una mejor experiencia de usuario y probar nuevas funciones.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"El precio original era {price}.\",\n            \"currentPrice\": \"El precio actual es {price}.\",\n            \"range\": \"Precio de {minValue} a {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Certificados de regalo\",\n        \"description\": \"Regala el regalo perfecto que nunca pasa de moda. Deja que tus colegas y seres queridos elijan exactamente lo que quieren de toda nuestra colección.\",\n        \"purchaseLabel\": \"Comprar ahora\",\n        \"checkBalanceLabel\": \"Consultar saldo\",\n        \"expiresAtLabel\": \"Válido hasta\",\n        \"CheckBalance\": {\n            \"title\": \"Consultar saldo\",\n            \"description\": \"Puede consultar el saldo y obtener la información sobre su certificado de regalo escribiendo el código en el cuadro a continuación.\",\n            \"inputLabel\": \"Código\",\n            \"inputPlaceholder\": \"xxx-xxx-xxx-xxx\",\n            \"purchasedDateLabel\": \"Comprado\",\n            \"senderLabel\": \"Para\",\n            \"Errors\": {\n                \"invalidCode\": \"El código del certificado de regalo que ingresó no es válido. Verifique el código e inténtelo de nuevo.\",\n                \"codeRequired\": \"Ingresa el código del certificado de regalo.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Comprar certificado de regalo\",\n            \"title\": \"Certificado de regalo digital\",\n            \"description\": \"Explore nuestros certificados de regalo, perfectos para cualquier ocasión. Elige la cantidad y personaliza tu mensaje.\",\n            \"successMessage\": \"Se ha agregado un certificado de regalo a <cartLink> su carrito</cartLink>\",\n            \"missingCart\": \"Carrito no encontrado. Vuelva a intentarlo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"Form\": {\n                \"amountLabel\": \"Monto\",\n                \"customAmountLabel\": \"Importe (entre {minAmount} y {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Selecciona una cantidad\",\n                \"customAmountPlaceholder\": \"Ingrese la cantidad personalizada\",\n                \"senderNameLabel\": \"Tu nombre\",\n                \"senderEmailLabel\": \"Tu correo electrónico\",\n                \"recipientNameLabel\": \"Nombre del destinatario\",\n                \"recipientEmailLabel\": \"Correo electrónico del destinatario\",\n                \"namePlaceholder\": \"Ingrese el nombre\",\n                \"emailPlaceholder\": \"Ingresa el correo electrónico\",\n                \"messageLabel\": \"Mensaje\",\n                \"messagePlaceholder\": \"Ingrese su mensaje (opcional)\",\n                \"nonRefundableCheckboxLabel\": \"Acepto que los certificados de regalo no son reembolsables\",\n                \"expiryCheckboxLabel\": \"Reconozco que este Certificado de Regalo caducará el {expiryDate}\",\n                \"ctaLabel\": \"Agregar al carrito\",\n                \"Errors\": {\n                    \"amountRequired\": \"Por favor, selecciona o introduce el importe de un vale regalo\",\n                    \"amountInvalid\": \"Por favor, seleccione un importe válido de un certificado regalo\",\n                    \"amountOutOfRange\": \"Por favor, introduzca una cantidad entre {minAmount} y {maxAmount}\",\n                    \"unexpectedSettingsError\": \"Se produjo un error inesperado al recuperar la configuración del certificado de regalo. Intente de nuevo más tarde.\",\n                    \"senderNameRequired\": \"Tu nombre es obligatorio\",\n                    \"senderEmailRequired\": \"Tu correo electrónico es obligatorio\",\n                    \"recipientNameRequired\": \"Se requiere el nombre del destinatario\",\n                    \"recipientEmailRequired\": \"Se requiere el email del destinatario\",\n                    \"emailInvalid\": \"Por favor, introduzca una dirección de email válida\",\n                    \"checkboxRequired\": \"Debes marcar esta casilla para continuar\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"Opcional\",\n        \"recaptchaRequired\": \"Por favor, completa la verificación de reCAPTCHA.\",\n        \"Errors\": {\n            \"invalidInput\": \"Por favor, revisa tu opinión y vuelve a intentarlo\",\n            \"invalidFormat\": \"El valor introducido no coincide con el formato requerido\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/es.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Novedades para cada ocasión\",\n                \"description\": \"Nuestras últimas incorporaciones llegan cargadas de estilo, funcionalidad e inspiración. ¡No te las pierdas! Aprovecha y descubre los mejores productos para tu hogar.\",\n                \"alt\": \"Cinco plantas pequeñas expuestas en bloques apilados de color beige, con un follaje verde variado y macetas de color gris oscuro, sobre un fondo neutro.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide02\": {\n                \"title\": \"Descubre las novedades\",\n                \"description\": \"Descubre nuestras últimas novedades y dale un nuevo aire a tu hogar.\",\n                \"alt\": \"Manos tendidas que sostienen un helecho verde en una cesta tejida con un lazo decorativo sobre un fondo beige con sombras suaves.\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Slide03\": {\n                \"title\": \"Para todos los gustos\",\n                \"description\": \"No te pierdas las ofertas exclusivas de nuestros superventas. Compra hoy y ahorra a lo grande en tus productos favoritos.\",\n                \"alt\": \"Primer plano de una hoja verde brillante con agujeros, en el que se aprecia su textura suave y sus detalles naturales.\",\n                \"cta\": \"Comprar ahora\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Colección destacada\",\n            \"description\": \"Explora nuestras mejores selecciones en esta colección destacada. ¡Encuentra el regalo perfecto o date un capricho!\",\n            \"cta\": \"Ver más\",\n            \"emptyStateTitle\": \"No se han encontrado productos\",\n            \"emptyStateSubtitle\": \"Prueba a navegar por nuestro catálogo completo de productos.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Novedades\",\n            \"description\": \"Aquí encontrarás nuestros últimos productos. Echa un vistazo a las novedades de la tienda.\",\n            \"cta\": \"Ver todas\",\n            \"emptyStateTitle\": \"No se han encontrado productos\",\n            \"emptyStateSubtitle\": \"Prueba a navegar por nuestro catálogo completo de productos.\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Cambiar contraseña\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"passwordUpdated\": \"La contraseña se ha actualizado correctamente.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe tener al menos {minLength, plural, =1 {1 carácter} other {# caracteres}}\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Confirma tu contraseña\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Iniciar sesión\",\n            \"heading\": \"Iniciar sesión\",\n            \"forgotPassword\": \"¿Olvidó su contraseña?\",\n            \"cta\": \"Iniciar sesión\",\n            \"email\": \"Correo electrónico\",\n            \"password\": \"Contraseña\",\n            \"invalidCredentials\": \"Tu dirección de correo electrónico o contraseña son incorrectas. Intenta iniciar sesión de nuevo o restablece tu contraseña\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"passwordResetRequired\": \"Es necesario restablecer la contraseña. Consulta tu correo electrónico para recibir instrucciones sobre cómo restablecer tu contraseña.\",\n            \"invalidToken\": \"Tu enlace de inicio de sesión no es válido o ha caducado. Intenta conectarte de nuevo.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"Se requiere un correo electrónico\",\n                \"emailInvalid\": \"Introduce una dirección de correo electrónico válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"invalidInput\": \"Comprueba lo que has escrito e inténtalo de nuevo.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"¿Cliente nuevo?\",\n                \"accountBenefits\": \"Cree una cuenta con nosotros y podrá:\",\n                \"fastCheckout\": \"Agilizar el proceso de pago\",\n                \"multipleAddresses\": \"Guardar varias direcciones de envío\",\n                \"ordersHistory\": \"Acceder a su historial de pedidos\",\n                \"ordersTracking\": \"Hacer seguimiento de pedidos nuevos\",\n                \"wishlists\": \"Guardar artículos en su lista de deseos\",\n                \"cta\": \"Crear cuenta\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Olvidé mi contraseña\",\n                \"subtitle\": \"Introduce a continuación la dirección de correo electrónico asociada a tu cuenta. Te enviaremos instrucciones para restablecer tu contraseña.\",\n                \"confirmResetPassword\": \"Si la dirección de correo electrónico {email} está vinculada a una cuenta en nuestra tienda, te hemos enviado un correo electrónico de restablecimiento de contraseña. Comprueba tu bandeja de entrada y tu carpeta de correo no deseado si no lo ves.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"Se requiere un correo electrónico\",\n                    \"emailInvalid\": \"Introduce una dirección de correo electrónico válida\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Registrar cuenta\",\n            \"heading\": \"Cuenta nueva\",\n            \"cta\": \"Crear cuenta\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"recaptchaRequired\": \"Completa la verificación de reCAPTCHA.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido(s) es obligatorio.\",\n                \"emailRequired\": \"Se requiere un correo electrónico\",\n                \"emailInvalid\": \"Introduce una dirección de correo electrónico válida\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe tener al menos {minLength, plural, =1 {1 carácter} other {# caracteres}}\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"addressLine1Required\": \"El campo Línea de dirección 1 es obligatorio\",\n                \"cityRequired\": \"El campo Ciudad es obligatorio\",\n                \"countryRequired\": \"El campo de país es obligatorio.\",\n                \"stateRequired\": \"El campo de Estado/Provincia es obligatorio\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"No hay productos de esta marca\",\n                \"subtitle\": \"Prueba a utilizar otro tipo de filtros.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Categorías\",\n            \"Empty\": {\n                \"title\": \"No hay productos en esta categoría\",\n                \"subtitle\": \"Prueba a utilizar otro tipo de filtros.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Resultados de la búsqueda\",\n            \"searchResults\": \"Resultados de la búsqueda para\",\n            \"subCategories\": \"Categorías\",\n            \"Breadcrumbs\": {\n                \"home\": \"Inicio\",\n                \"search\": \"Buscar\"\n            },\n            \"Empty\": {\n                \"title\": \"Lo sentimos, no hay resultados para \\\"{term}\\\".\",\n                \"subtitle\": \"Intenta con otra búsqueda.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtros\",\n            \"resetFilters\": \"Restablecer filtros\",\n            \"Range\": {\n                \"apply\": \"Aplicar\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Envío gratis\",\n                \"isFeaturedLabel\": \"Es destacado\",\n                \"inStockLabel\": \"En existencias\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Clasificar por:\",\n            \"featuredItems\": \"Artículos destacados\",\n            \"bestSellingItems\": \"Artículos más vendidos\",\n            \"newestItems\": \"Artículos más recientes\",\n            \"aToZ\": \"A a Z\",\n            \"zToA\": \"Z a A\",\n            \"byReview\": \"Por reseñas\",\n            \"priceAscending\": \"Precio: ascendente\",\n            \"priceDescending\": \"Precio: descendente\",\n            \"relevance\": \"Relevancia\"\n        },\n        \"Compare\": {\n            \"compare\": \"Comparar\",\n            \"remove\": \"Eliminar\",\n            \"maxCompareLimit\": \"Has alcanzado el número máximo de productos para comparar. Elimina un producto para añadir uno nuevo.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Direcciones\",\n            \"logout\": \"Cerrar sesión\",\n            \"orders\": \"Pedidos\",\n            \"settings\": \"Cuenta\",\n            \"wishlists\": \"Listas de deseos\"\n        },\n        \"Orders\": {\n            \"title\": \"Pedidos\",\n            \"orderNumber\": \"Núm. de pedido\",\n            \"totalPrice\": \"Total\",\n            \"viewDetails\": \"Ver detalles\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ningún pedido\",\n                \"cta\": \"Comprar ahora\"\n            },\n            \"Details\": {\n                \"title\": \"Pedido n.º {orderNumber}\",\n                \"shippingAddress\": \"Dirección de envío\",\n                \"shippingMethod\": \"Método de envío\",\n                \"summaryTotal\": \"Total\",\n                \"destination\": \"Destino\",\n                \"destinationWithCount\": \"Destino {número, número}/{total, número}\",\n                \"digitalDelivery\": \"Entrega digital a {email}\",\n                \"subtotal\": \"Subtotal\",\n                \"shipping\": \"Envío\",\n                \"tax\": \"Impuesto\",\n                \"orderSummary\": \"Resumen del pedido\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Método de pago} other {Métodos de pago}}\",\n                \"paymentEndingInLabel\": \"Termina en\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Tarjeta de crédito\",\n                    \"giftCertificate\": \"Certificado de regalo\",\n                    \"storeCredit\": \"Crédito en tienda\",\n                    \"other\": \"Otro\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Direcciones\",\n            \"cta\": \"Añadir dirección\",\n            \"edit\": \"Editar\",\n            \"delete\": \"Eliminar\",\n            \"cancel\": \"Cancelar\",\n            \"create\": \"Crear\",\n            \"update\": \"Actualizar\",\n            \"setDefault\": \"Establecer como predeterminado\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"EmptyState\": {\n                \"title\": \"No tienes ninguna dirección\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"lastNameRequired\": \"El campo Apellido(s) es obligatorio.\",\n                \"addressLine1Required\": \"El campo Línea de dirección 1 es obligatorio\",\n                \"cityRequired\": \"El campo Ciudad es obligatorio\",\n                \"countryRequired\": \"El campo de país es obligatorio.\",\n                \"stateRequired\": \"El campo de Estado/Provincia es obligatorio\",\n                \"postalCodeRequired\": \"El campo Código postal es obligatorio\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"Configuración de la cuenta\",\n            \"changePassword\": \"Cambiar contraseña\",\n            \"passwordUpdated\": \"La contraseña se ha actualizado correctamente.\",\n            \"settingsUpdated\": \"¡La configuración de tu cuenta se ha actualizado correctamente!\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n            \"currentPassword\": \"Contraseña actual\",\n            \"newPassword\": \"Nueva contraseña\",\n            \"confirmPassword\": \"Confirmar contraseña\",\n            \"cta\": \"Actualizar\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Preferencias de marketing\",\n                \"label\": \"Suscríbase a nuestro boletín.\",\n                \"marketingPreferencesUpdated\": \"¡Las preferencias de marketing se han actualizado correctamente!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"El campo Nombre es obligatorio.\",\n                \"firstNameTooSmall\": \"El nombre debe tener al menos 2 caracteres\",\n                \"lastNameRequired\": \"El campo Apellido(s) es obligatorio.\",\n                \"lastNameTooSmall\": \"El apellido debe tener al menos 2 caracteres\",\n                \"emailRequired\": \"Se requiere un correo electrónico\",\n                \"emailInvalid\": \"Introduce una dirección de correo electrónico válida\",\n                \"currentPasswordRequired\": \"Es obligatorio indicar la contraseña actual\",\n                \"passwordRequired\": \"Se requiere la contraseña\",\n                \"passwordTooSmall\": \"La contraseña debe tener al menos {minLength, plural, =1 {1 carácter} other {# caracteres}}\",\n                \"passwordLowercaseRequired\": \"La contraseña debe contener al menos una letra minúscula\",\n                \"passwordUppercaseRequired\": \"La contraseña debe contener al menos una letra mayúscula\",\n                \"passwordNumberRequired\": \"La contraseña debe contener al menos {minNumbers, plural, =1 {un número} other {# números}}\",\n                \"passwordSpecialCharacterRequired\": \"La contraseña debe contener al menos un carácter especial\",\n                \"passwordsMustMatch\": \"Las contraseñas no coinciden\",\n                \"confirmPasswordRequired\": \"Confirma tu contraseña\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Acciones de la lista de deseos\",\n        \"title\": \"Listas de deseos\",\n        \"new\": \"Nueva lista de deseos\",\n        \"items\": \"{count, plural, =1 {1 item} other {# items}}\",\n        \"viewWishlist\": \"Ver lista\",\n        \"noWishlists\": \"No tienes ninguna lista de deseos\",\n        \"noWishlistsCallToAction\": \"Crea una lista de deseos\",\n        \"emptyWishlist\": \"No has añadido productos a esta lista de deseos.\",\n        \"share\": \"Compartir\",\n        \"shareSuccess\": \"La lista de deseos se ha compartido correctamente.\",\n        \"shareCopied\": \"La URL pública de la lista de deseos se ha copiado en tu portapapeles.\",\n        \"shareDisabled\": \"Tu lista de deseos debe ser pública para poder compartirla.\",\n        \"makePublic\": \"Hacer público\",\n        \"makePrivate\": \"Hacer privado\",\n        \"rename\": \"Cambiar nombre\",\n        \"delete\": \"Eliminar\",\n        \"removeButtonTitle\": \"Eliminar producto de lista de deseos\",\n        \"Visibility\": {\n            \"public\": \"Público\",\n            \"private\": \"Privado\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Cancelar\",\n            \"close\": \"Cerrar\",\n            \"copy\": \"Copiar\",\n            \"create\": \"Crear\",\n            \"save\": \"Guardar\",\n            \"delete\": \"Eliminar\",\n            \"newTitle\": \"Crea una nueva lista de deseos\",\n            \"shareTitle\": \"Comparte {name}\",\n            \"renameTitle\": \"Renombrar {name}\",\n            \"deleteTitle\": \"Eliminar {name}\",\n            \"changeVisibilityPublicTitle\": \"¿Hacer {name} público?\",\n            \"changeVisibilityPrivateTitle\": \"¿Hacer {name} privado?\",\n            \"makePublicContent\": \"¿Seguro que quieres que <bold>{name}</bold> sea público? Esto permitirá que otras personas vean tu lista de deseos si tienen el enlace.\",\n            \"makePrivateContent\": \"¿Seguro que quieres que <bold>{name}</bold> sea privado? Si has compartido tu lista de deseos con otras personas, estas ya no podrán verla.\",\n            \"deleteContent\": \"¿Está seguro de que desea borrar <bold>{name}</bold>? Esta acción no se puede deshacer.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Nombre\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"El nombre de la lista de deseos no puede estar vacío.\",\n            \"updateFailed\": \"No se ha podido actualizar tu lista de deseos. Inténtalo de nuevo.\",\n            \"deleteFailed\": \"No se ha podido borrar tu lista de deseos. Inténtalo de nuevo.\",\n            \"removeProductFailed\": \"No se ha podido eliminar el producto de tu lista de deseos. Inténtalo de nuevo.\",\n            \"unauthorized\": \"No tienes autorización para realizar esta acción. Inicia sesión e inténtalo de nuevo.\",\n            \"unexpected\": \"Se ha producido un error inesperado. Vuelve a intentarlo.\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"La lista de deseos se ha creado correctamente.\",\n            \"updateSuccess\": \"La lista de deseos se ha actualizado correctamente.\",\n            \"deleteSuccess\": \"La lista de deseos se ha eliminado correctamente.\",\n            \"removeItemSuccess\": \"Se ha eliminado el artículo de tu lista de deseos.\"\n        },\n        \"Button\": {\n            \"label\": \"Añadir a la lista de deseos\",\n            \"addToNewWishlist\": \"Añadir a una nueva lista de deseos\",\n            \"defaultWishlistName\": \"Mi lista de deseos\",\n            \"addSuccessMessage\": \"El producto se ha añadido a tu lista de deseos.\",\n            \"removeSuccessMessage\": \"El producto se ha eliminado de tu lista de deseos.\",\n            \"Errors\": {\n                \"addProductFailed\": \"No se ha podido añadir el producto de tu lista de deseos. Inténtalo de nuevo.\",\n                \"removeProductFailed\": \"No se ha podido eliminar el producto de tu lista de deseos. Inténtalo de nuevo.\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Lista de deseos pública\",\n        \"defaultName\": \"Lista de deseos pública\",\n        \"emptyWishlist\": \"Esta lista de deseos aún no tiene ningún producto.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Inicio\",\n        \"Empty\": {\n            \"title\": \"No se encontraron entradas de blog\",\n            \"subtitle\": \"Comprueba más tarde si hay contenido nuevo\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Compartir\",\n            \"email\": \"Correo electrónico\",\n            \"print\": \"Imprimir\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Carrito\",\n        \"heading\": \"Su carrito\",\n        \"proceedToCheckout\": \"Ir al proceso de pago\",\n        \"increment\": \"Aumenta la cantidad\",\n        \"decrement\": \"Disminuir cantidad\",\n        \"removeItem\": \"Eliminar artículo\",\n        \"cartCombined\": \"Nos hemos dado cuenta de que tenías artículos guardados en un carrito anterior, así que los hemos añadido a tu carrito actual.\",\n        \"cartRestored\": \"Empezaste a llenar un carrito en otro dispositivo y lo hemos restaurado aquí para que puedas seguir donde lo dejaste.\",\n        \"cartUpdateInProgress\": \"Tienes una actualización del carrito en curso. ¿Seguro que quieres abandonar esta página? Es posible que se pierdan los cambios.\",\n        \"originalPrice\": \"El precio original era {price}.\",\n        \"currentPrice\": \"El precio actual es {price}.\",\n        \"quantityReadyToShip\": \"{quantity, number} listo para enviar\",\n        \"quantityOnBackorder\": \"Cantidad que se añadirá a pedidos pendientes: {quantity, number}\",\n        \"partiallyAvailable\": \"Cantidad disponible: {quantity, number}\",\n        \"CheckoutSummary\": {\n            \"title\": \"Resumen\",\n            \"subTotal\": \"Subtotal\",\n            \"discounts\": \"Descuentos\",\n            \"tax\": \"Impuesto\",\n            \"total\": \"Total\",\n            \"CouponCode\": {\n                \"apply\": \"Aplicar\",\n                \"couponCode\": \"Código de cupón\",\n                \"removeCouponCode\": \"Elimina el código de cupón\",\n                \"invalidCouponCode\": \"Introduce un código de cupón válido\",\n                \"cartNotFound\": \"Se ha producido un error al recuperar tu carrito\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Envío\",\n                \"add\": \"Añadir\",\n                \"change\": \"Cambiar\",\n                \"cancel\": \"Cancelar\",\n                \"country\": \"País\",\n                \"city\": \"Ciudad\",\n                \"state\": \"Estado/Provincia\",\n                \"postalCode\": \"Código postal\",\n                \"updatedShippingOptions\": \"Actualiza las opciones de envío\",\n                \"viewShippingOptions\": \"Ver opciones de envío\",\n                \"editAddress\": \"Editar dirección\",\n                \"shippingOptions\": \"Opciones de envío\",\n                \"updateShipping\": \"Actualizar envío\",\n                \"addShipping\": \"Añadir envío\",\n                \"cartNotFound\": \"Se ha producido un error al recuperar tu carrito\",\n                \"noShippingOptions\": \"No hay opciones de envío disponibles para tu dirección\",\n                \"countryRequired\": \"El campo de país es obligatorio.\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Certificado de regalo\",\n            \"giftCertificateCode\": \"Código del certificado de regalo\",\n            \"removeGiftCertificate\": \"Eliminar certificado de regalo\",\n            \"apply\": \"Aplicar\",\n            \"to\": \"a\",\n            \"message\": \"Mensaje\",\n            \"invalidGiftCertificate\": \"Introduce un código de cupón de regalo válido\",\n            \"cartNotFound\": \"Se ha producido un error al recuperar tu carrito\"\n        },\n        \"Empty\": {\n            \"title\": \"Su carrito está vacío.\",\n            \"subtitle\": \"Añade algunos productos para empezar.\",\n            \"cta\": \"Seguir comprando\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Se ha producido un error al recuperar tu carrito\",\n            \"lineItemNotFound\": \"No se ha encontrado el elemento.\",\n            \"failedToUpdateQuantity\": \"No se pudo actualizar la cantidad.\",\n            \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Comparar productos\",\n        \"addToCart\": \"Añadir al carrito\",\n        \"next\": \"Siguientes productos\",\n        \"previous\": \"Productos anteriores\",\n        \"noProductsToCompare\": \"No hay productos para comparar\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Peso\",\n        \"description\": \"Descripción\",\n        \"noDescription\": \"No hay descripción disponible.\",\n        \"rating\": \"Calificación\",\n        \"noRatings\": \"No hay reseñas.\",\n        \"otherDetails\": \"Otros detalles\",\n        \"noOtherDetails\": \"No hay más detalles.\",\n        \"viewOptions\": \"Ver opciones\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 Item} other {# Items}} añadidos <cartLink> al carrito</cartLink>\",\n        \"missingCart\": \"No se ha encontrado el carrito. Vuelva a intentarlo más tarde.\",\n        \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Cantidad\",\n            \"increaseQuantity\": \"Aumenta la cantidad\",\n            \"decreaseQuantity\": \"Disminuir cantidad\",\n            \"emptySelectPlaceholder\": \"Seleccionar una opción\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 Item} other {# Items}} añadidos <cartLink> al carrito</cartLink>\",\n            \"missingCart\": \"No se ha encontrado el carrito. Vuelve a intentarlo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"variantRequiredError\": \"Este producto requiere que se seleccionen opciones para poder añadirlo al carrito.\",\n            \"increaseNumber\": \"Aumentar número\",\n            \"decreaseNumber\": \"Disminuir número\",\n            \"thumbnail\": \"Ver número de imagen\",\n            \"additionalInformation\": \"Información adicional\",\n            \"currentStock\": \"{cantidad, número} en existencias\",\n            \"backorderQuantity\": \"{cantidad, número} en pedidos pendientes\",\n            \"loadingMoreImages\": \"Cargando más imágenes\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 imagen más cargada} other {# imágenes más cargadas}}\",\n            \"Submit\": {\n                \"addToCart\": \"Añadir al carrito\",\n                \"outOfStock\": \"Sin existencias\",\n                \"preorder\": \"Pedido en preventa\",\n                \"unavailable\": \"No disponible\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Especificaciones\",\n                \"warranty\": \"Garantía\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Peso\",\n                \"condition\": \"Condición\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Productos relacionados\",\n            \"noRelatedProducts\": \"No se encontraron productos relacionados\",\n            \"browseCatalog\": \"Prueba a navegar por nuestro catálogo completo de productos.\",\n            \"cta\": \"Comprar todo\",\n            \"previousProducts\": \"Productos anteriores\",\n            \"nextProducts\": \"Siguientes productos\",\n            \"scrollbar\": \"Barra de desplazamiento de productos relacionados\"\n        },\n        \"Reviews\": {\n            \"title\": \"Reseñas\",\n            \"empty\": \"No se han escrito opiniones para este producto.\",\n            \"previous\": \"Reseñas anteriores\",\n            \"next\": \"Próximas reseñas\",\n            \"Form\": {\n                \"button\": \"Escribir una reseña\",\n                \"title\": \"Escribir una reseña\",\n                \"submit\": \"Enviar\",\n                \"cancel\": \"Cancelar\",\n                \"ratingLabel\": \"Calificación\",\n                \"titleLabel\": \"Título\",\n                \"reviewLabel\": \"Reseña\",\n                \"nameLabel\": \"Nombre\",\n                \"emailLabel\": \"Correo electrónico\",\n                \"successMessage\": \"¡Tu reseña se ha enviado correctamente!\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Completa la verificación de reCAPTCHA.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"El título es obligatorio\",\n                    \"authorRequired\": \"El campo Nombre es obligatorio\",\n                    \"emailRequired\": \"Se requiere un correo electrónico\",\n                    \"emailInvalid\": \"Introduce una dirección de correo electrónico válida\",\n                    \"textRequired\": \"La revisión es obligatoria\",\n                    \"ratingRequired\": \"La calificación es obligatoria\",\n                    \"ratingTooSmall\": \"La puntuación debe ser como mínimo 1\",\n                    \"ratingTooLarge\": \"La puntuación debe ser como máximo 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Inicio\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Inicio\",\n            \"Form\": {\n                \"success\": \"Gracias por ponerte en contacto con nosotros. Nos pondremos en contacto contigo pronto.\",\n                \"successCta\": \"Seguir comprando\",\n                \"fullName\": \"Nombre completo\",\n                \"companyName\": \"Nombre de la compañía\",\n                \"phone\": \"Teléfono\",\n                \"orderNo\": \"Número de pedido\",\n                \"rma\": \"Número RMA\",\n                \"email\": \"Correo electrónico\",\n                \"comments\": \"Comentarios/Preguntas\",\n                \"cta\": \"Enviar formulario\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\",\n                \"recaptchaRequired\": \"Completa la verificación de reCAPTCHA.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"Mantenimiento\",\n        \"message\": \"Estamos fuera de servicio por mantenimiento\",\n        \"contactUs\": \"Puedes ponerte en contacto con nosotros en:\"\n    },\n    \"Error\": {\n        \"title\": \"Se ha producido un error en el servidor.\",\n        \"subtitle\": \"Vuelva a intentarlo más tarde.\",\n        \"cta\": \"Volver a intentarlo\"\n    },\n    \"NotFound\": {\n        \"title\": \"No hemos podido encontrar esa página.\",\n        \"subtitle\": \"Prueba a buscar otra cosa o vuelve a la página de inicio.\",\n        \"featuredProducts\": \"Productos destacados\",\n        \"search\": \"Buscar\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Inicio\",\n            \"toggleNavigation\": \"Alternar navegación\",\n            \"Icons\": {\n                \"account\": \"Perfil\",\n                \"cart\": \"Carrito\",\n                \"search\": \"Abrir ventana de búsqueda\",\n                \"giftCertificates\": \"Certificados de regalo\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Cambiar divisa\",\n                \"invalidCurrency\": \"Moneda no válida\",\n                \"errorUpdatingCurrency\": \"Error al actualizar la moneda de tu carrito. Inténtalo de nuevo.\"\n            },\n            \"Search\": {\n                \"products\": \"Productos\",\n                \"categories\": \"Categorías\",\n                \"brands\": \"Marcas\",\n                \"noSearchResultsTitle\": \"Lo sentimos, no hay resultados para \\\"{term}\\\".\",\n                \"noSearchResultsSubtitle\": \"Intenta con otra búsqueda.\",\n                \"somethingWentWrong\": \"Algo ha salido mal. Vuelva a intentarlo.\",\n                \"inputPlaceholder\": \"Busca productos, categorías, marcas...\",\n                \"submitLabel\": \"Buscar\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Inicio\",\n            \"contactUs\": \"Contacto\",\n            \"socialMediaLinks\": \"Vínculos a redes sociales\",\n            \"categories\": \"Categorías\",\n            \"brands\": \"Marcas\",\n            \"navigate\": \"Navegar\",\n            \"giftCertificates\": \"Certificados de regalo\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Suscribirse a nuestro boletín\",\n            \"placeholder\": \"Introduzca su correo electrónico\",\n            \"description\": \"Entérate de todas las novedades y ofertas de nuestra tienda.\",\n            \"subscribedToNewsletter\": \"¡Te has suscrito a nuestro boletín!\",\n            \"Errors\": {\n                \"emailRequired\": \"Se requiere un correo electrónico\",\n                \"invalidEmail\": \"Introduce una dirección de correo electrónico válida\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Rechazar todo\",\n                \"acceptAll\": \"Aceptar todo\",\n                \"customize\": \"Personalizar\",\n                \"save\": \"Guardar la configuración\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"Valoramos tu privacidad\",\n                \"description\": \"Este sitio utiliza cookies para mejorar tu experiencia de navegación, analizar el tráfico del sitio y mostrar contenido personalizado.\",\n                \"privacyPolicy\": \"Política de privacidad\"\n            },\n            \"Dialog\": {\n                \"title\": \"Configuración de privacidad\",\n                \"description\": \"Personaliza tu configuración de privacidad aquí. Puedes elegir qué tipos de cookies y tecnologías de seguimiento te gustaría permitir.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Estrictamente necesario\",\n                    \"description\": \"Estas cookies son esenciales para que el sitio web funcione correctamente y no se pueden desactivar.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Funcionalidad\",\n                    \"description\": \"Estas cookies permiten mejorar la funcionalidad y la personalización del sitio web.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"Estas cookies se utilizan para ofrecer anuncios relevantes y realizar un seguimiento de su efectividad.\"\n                },\n                \"measurement\": {\n                    \"title\": \"Análisis\",\n                    \"description\": \"Estas cookies nos ayudan a comprender cómo interactúan los visitantes con el sitio web y a mejorar su rendimiento.\"\n                },\n                \"experience\": {\n                    \"title\": \"Experiencia\",\n                    \"description\": \"Estas cookies nos ayudan a brindar una mejor experiencia de usuario y probar nuevas funciones.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"El precio original era {price}.\",\n            \"currentPrice\": \"El precio actual es {price}.\",\n            \"range\": \"Precios entre {minValue} y {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Certificados de regalo\",\n        \"description\": \"Haz el regalo perfecto que nunca pasa de moda. Deja que tus amigos y seres queridos elijan exactamente lo que quieren de toda nuestra colección.\",\n        \"purchaseLabel\": \"Comprar ahora\",\n        \"checkBalanceLabel\": \"Verificar saldo\",\n        \"expiresAtLabel\": \"Válido hasta\",\n        \"CheckBalance\": {\n            \"title\": \"Verificar saldo\",\n            \"description\": \"Puedes comprobar el saldo y obtener la información sobre tu cupón de regalo escribiendo el código en la casilla de abajo.\",\n            \"inputLabel\": \"Código\",\n            \"inputPlaceholder\": \"XXX-XXX-XXX-XXX\",\n            \"purchasedDateLabel\": \"Comprado\",\n            \"senderLabel\": \"de\",\n            \"Errors\": {\n                \"invalidCode\": \"El código del cupón de regalo que has introducido no es válido. Compruébalo e inténtalo de nuevo.\",\n                \"codeRequired\": \"Introduce un código de cupón de regalo.\",\n                \"somethingWentWrong\": \"Algo salió mal. Vuelva a intentarlo más tarde.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Comprar un cupón de regalo\",\n            \"title\": \"Certificado de regalo digital\",\n            \"description\": \"Explora nuestros cupones de regalo, perfectos para cualquier ocasión. Elige el importe y personaliza tu mensaje.\",\n            \"successMessage\": \"Se ha añadido un cupón de regalo a <cartLink>tu carrito</cartLink>\",\n            \"missingCart\": \"No se ha encontrado el carrito. Vuelva a intentarlo más tarde.\",\n            \"unknownError\": \"Error desconocido. Vuelva a intentarlo más tarde.\",\n            \"Form\": {\n                \"amountLabel\": \"Cantidad\",\n                \"customAmountLabel\": \"Importe (entre {minAmount} y {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Selecciona una cantidad\",\n                \"customAmountPlaceholder\": \"Introduce un importe personalizado\",\n                \"senderNameLabel\": \"Su nombre\",\n                \"senderEmailLabel\": \"Su correo electrónico\",\n                \"recipientNameLabel\": \"Nombre del destinatario\",\n                \"recipientEmailLabel\": \"Correo electrónico del destinatario\",\n                \"namePlaceholder\": \"Introduce el nombre\",\n                \"emailPlaceholder\": \"Introduzca el correo electrónico\",\n                \"messageLabel\": \"Mensaje\",\n                \"messagePlaceholder\": \"Ingresa tu mensaje (opcional)\",\n                \"nonRefundableCheckboxLabel\": \"Acepto que los cupones de regalo no son reembolsables\",\n                \"expiryCheckboxLabel\": \"Reconozco que este vale de regalo caducará el {expiryDate}\",\n                \"ctaLabel\": \"Añadir al carrito\",\n                \"Errors\": {\n                    \"amountRequired\": \"Selecciona o introduce un importe para el cupón de regalo\",\n                    \"amountInvalid\": \"Selecciona un importe válido para el cupón de regalo\",\n                    \"amountOutOfRange\": \"Introduce una cantidad entre {minAmount} y {maxAmount}\",\n                    \"unexpectedSettingsError\": \"Se ha producido un error inesperado al recuperar la configuración del cupón de regalo. Vuelve a intentarlo más tarde.\",\n                    \"senderNameRequired\": \"Tu nombre es obligatorio\",\n                    \"senderEmailRequired\": \"Tu correo electrónico es obligatorio\",\n                    \"recipientNameRequired\": \"El nombre del destinatario es obligatorio\",\n                    \"recipientEmailRequired\": \"El correo electrónico del destinatario es obligatorio\",\n                    \"emailInvalid\": \"Introduce una dirección de correo electrónico válida\",\n                    \"checkboxRequired\": \"Debes marcar esta casilla para continuar\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"Opcional\",\n        \"recaptchaRequired\": \"Completa la verificación de reCAPTCHA.\",\n        \"Errors\": {\n            \"invalidInput\": \"Comprueba lo que has escrito e inténtalo de nuevo\",\n            \"invalidFormat\": \"El valor introducido no coincide con el formato requerido\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/fr.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Des plantes naturelles pour chaque occasion\",\n                \"description\": \"Découvrez nos dernières nouveautés mêlant esthétique, fonctionnalité et inspiration. Explorez notre boutique et découvrez votre prochain coup de cœur.\",\n                \"alt\": \"Cinq petites plantes dans des pots gris foncé placés sur des blocs beiges empilés, mettant en valeur différents feuillages verts, le tout sur un fond neutre.\",\n                \"cta\": \"Acheter\"\n            },\n            \"Slide02\": {\n                \"title\": \"Découvrez les nouveautés\",\n                \"description\": \"Parcourez nos dernières nouveautés et trouvez quelque chose qui vous intéresse pour votre habitation.\",\n                \"alt\": \"Les mains d'une personne tendent à une autre personne une fougère verte dans un panier tressé avec un nœud décoratif, le tout sur un fond beige avec des ombres douces.\",\n                \"cta\": \"Acheter\"\n            },\n            \"Slide03\": {\n                \"title\": \"Quelque chose pour tous\",\n                \"description\": \"Ne manquez pas les offres exclusives valables sur nos produits les plus vendus. Passez commande dès aujourd’hui et faites de grosses économies sur les articles que vous aimez.\",\n                \"alt\": \"Gros plan sur une feuille d’un vert éclatant avec des perforations, qui met en valeur sa texture lisse et ses détails naturels.\",\n                \"cta\": \"Acheter\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Collection en vedette\",\n            \"description\": \"Découvrez nos meilleurs choix dans cette collection en vedette. Trouvez le cadeau parfait ou faites-vous plaisir !\",\n            \"cta\": \"Afficher plus\",\n            \"emptyStateTitle\": \"Aucun produit trouvé\",\n            \"emptyStateSubtitle\": \"Parcourez l'intégralité de notre catalogue de produits.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Nouveautés\",\n            \"description\": \"Nos derniers produits sont arrivés. Découvrez ce qui est nouveau en magasin.\",\n            \"cta\": \"Tout voir\",\n            \"emptyStateTitle\": \"Aucun produit trouvé\",\n            \"emptyStateSubtitle\": \"Parcourez l'intégralité de notre catalogue de produits.\",\n            \"previousProducts\": \"Produits précédents\",\n            \"nextProducts\": \"Prochains produits\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Modifier le mot de passe\",\n            \"newPassword\": \"Nouveau mot de passe\",\n            \"confirmPassword\": \"Confirmer le mot de passe\",\n            \"passwordUpdated\": \"Le mot de passe a bien été mis à jour !\",\n            \"somethingWentWrong\": \"Une erreur s'est produite. Veuillez réessayer plus tard.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Un mot de passe est requis\",\n                \"passwordTooSmall\": \"Le mot de passe doit comporter au moins {minLength, plural, =1 {1 character} other {# characters}} caractères\",\n                \"passwordLowercaseRequired\": \"Le mot de passe doit comporter au moins une lettre minuscule\",\n                \"passwordUppercaseRequired\": \"Le mot de passe doit comporter au moins une lettre majuscule\",\n                \"passwordNumberRequired\": \"Le mot de passe doit comporter au moins {minNumbers, plural, =1 {one number} other {# numbers}} chiffres\",\n                \"passwordSpecialCharacterRequired\": \"Le mot de passe doit comporter au moins un caractère spécial\",\n                \"passwordsMustMatch\": \"Les mots de passe ne correspondent pas\",\n                \"confirmPasswordRequired\": \"Veuillez confirmer votre mot de passe\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Connexion\",\n            \"heading\": \"Connexion\",\n            \"forgotPassword\": \"Mot de passe oublié ?\",\n            \"cta\": \"Connexion\",\n            \"email\": \"E-mail\",\n            \"password\": \"Mot de passe\",\n            \"invalidCredentials\": \"Votre adresse e-mail ou votre mot de passe est incorrect. Essayez de vous reconnecter ou réinitialisez votre mot de passe\",\n            \"somethingWentWrong\": \"Une erreur s'est produite. Veuillez réessayer plus tard.\",\n            \"passwordResetRequired\": \"Réinitialisation du mot de passe requise. Veuillez consulter votre e-mail pour les instructions de réinitialisation de votre mot de passe.\",\n            \"invalidToken\": \"Votre lien de connexion n'est pas valide ou a expiré. Veuillez réessayer de vous connecter.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"L'adresse e-mail est requise\",\n                \"emailInvalid\": \"Veuillez saisir une adresse e-mail valide\",\n                \"passwordRequired\": \"Un mot de passe est requis\",\n                \"invalidInput\": \"Veuillez vérifier votre saisie et réessayer.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"Nouveau client ?\",\n                \"accountBenefits\": \"Créez un compte sur notre site et vous pourrez :\",\n                \"fastCheckout\": \"Accélérer le processus de paiement\",\n                \"multipleAddresses\": \"Enregistrer plusieurs adresses de livraison\",\n                \"ordersHistory\": \"Accéder à votre historique de commande\",\n                \"ordersTracking\": \"Suivre les nouvelles commandes\",\n                \"wishlists\": \"Enregistrer les articles dans votre liste d'envies\",\n                \"cta\": \"Créer un compte\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Mot de passe oublié\",\n                \"subtitle\": \"Veuillez saisir ci-dessous l'adresse e-mail associée à votre compte. Nous vous enverrons des instructions pour réinitialiser votre mot de passe.\",\n                \"confirmResetPassword\": \"Si l'adresse e-mail {email} est liée à un compte dans notre boutique, vous recevrez un e-mail pour réinitialiser votre mot de passe. Veuillez consulter votre boîte de réception, ainsi que votre dossier de courrier indésirable si vous ne trouvez pas cet e-mail.\",\n                \"somethingWentWrong\": \"Une erreur s'est produite. Veuillez réessayer plus tard.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"L'adresse e-mail est requise\",\n                    \"emailInvalid\": \"Veuillez saisir une adresse e-mail valide\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Enregistrer un compte\",\n            \"heading\": \"Nouveau compte\",\n            \"cta\": \"Créer un compte\",\n            \"somethingWentWrong\": \"Une erreur s'est produite. Veuillez réessayer plus tard.\",\n            \"recaptchaRequired\": \"Veuillez compléter la vérification reCAPTCHA.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Le prénom est requis\",\n                \"lastNameRequired\": \"Le nom de famille est requis\",\n                \"emailRequired\": \"L'adresse e-mail est requise\",\n                \"emailInvalid\": \"Veuillez saisir une adresse e-mail valide\",\n                \"passwordRequired\": \"Un mot de passe est requis\",\n                \"passwordTooSmall\": \"Le mot de passe doit comporter au moins {minLength, plural, =1 {1 character} other {# characters}} caractères\",\n                \"passwordLowercaseRequired\": \"Le mot de passe doit comporter au moins une lettre minuscule\",\n                \"passwordUppercaseRequired\": \"Le mot de passe doit comporter au moins une lettre majuscule\",\n                \"passwordNumberRequired\": \"Le mot de passe doit comporter au moins {minNumbers, plural, =1 {one number} other {# numbers}} chiffres\",\n                \"passwordSpecialCharacterRequired\": \"Le mot de passe doit comporter au moins un caractère spécial\",\n                \"passwordsMustMatch\": \"Les mots de passe ne correspondent pas\",\n                \"addressLine1Required\": \"Ligne d'adresse 1 est requise\",\n                \"cityRequired\": \"La ville est requise\",\n                \"countryRequired\": \"Le pays est requis\",\n                \"stateRequired\": \"État/province est requis\",\n                \"postalCodeRequired\": \"Le code postal est requis\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"Aucun produit dans cette marque\",\n                \"subtitle\": \"Essayez différents filtres.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Catégories\",\n            \"Empty\": {\n                \"title\": \"Aucun produit dans cette catégorie\",\n                \"subtitle\": \"Essayez différents filtres.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Résultats de recherche\",\n            \"searchResults\": \"Résultats de recherche pour\",\n            \"subCategories\": \"Catégories\",\n            \"Breadcrumbs\": {\n                \"home\": \"Accueil\",\n                \"search\": \"Rechercher\"\n            },\n            \"Empty\": {\n                \"title\": \"Désolé, aucun résultat pour « {term} ».\",\n                \"subtitle\": \"Veuillez modifier votre recherche.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtres\",\n            \"resetFilters\": \"Réinitialiser le filtre\",\n            \"Range\": {\n                \"apply\": \"Appliquer\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Livraison gratuite\",\n                \"isFeaturedLabel\": \"En vedette\",\n                \"inStockLabel\": \"En stock\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Trier par :\",\n            \"featuredItems\": \"Articles en vedette\",\n            \"bestSellingItems\": \"Articles les plus vendus\",\n            \"newestItems\": \"Nouveautés\",\n            \"aToZ\": \"A à Z\",\n            \"zToA\": \"Z à A\",\n            \"byReview\": \"Par avis\",\n            \"priceAscending\": \"Prix : par ordre croissant\",\n            \"priceDescending\": \"Prix : par ordre décroissant\",\n            \"relevance\": \"Pertinence\"\n        },\n        \"Compare\": {\n            \"compare\": \"Comparer\",\n            \"remove\": \"Supprimer\",\n            \"maxCompareLimit\": \"Vous avez atteint le nombre maximal de produits pouvant être comparés. Pour en ajouter un autre, supprimez d'abord un produit.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Adresses\",\n            \"logout\": \"Déconnexion\",\n            \"orders\": \"Commandes\",\n            \"settings\": \"Compte\",\n            \"wishlists\": \"Listes d'envies\"\n        },\n        \"Orders\": {\n            \"title\": \"Commandes\",\n            \"orderNumber\": \"Commande n° \",\n            \"totalPrice\": \"Total\",\n            \"viewDetails\": \"Afficher les détails\",\n            \"EmptyState\": {\n                \"title\": \"Vous n'avez aucune commande\",\n                \"cta\": \"Acheter\"\n            },\n            \"Details\": {\n                \"title\": \"Commande n° {orderNumber}\",\n                \"shippingAddress\": \"Adresse de livraison\",\n                \"shippingMethod\": \"Mode de livraison\",\n                \"summaryTotal\": \"Total\",\n                \"destination\": \"Destination\",\n                \"destinationWithCount\": \"Destination {number, number}/{total, number}\",\n                \"digitalDelivery\": \"Livraison numérique à {email}\",\n                \"subtotal\": \"Sous-total\",\n                \"shipping\": \"Livraison\",\n                \"tax\": \"Taxes\",\n                \"orderSummary\": \"Récapitulatif de la commande\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Payment method} other {Payment methods}}\",\n                \"paymentEndingInLabel\": \"Se terminant par\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Carte de crédit\",\n                    \"giftCertificate\": \"Chèque-cadeau\",\n                    \"storeCredit\": \"Avoir\",\n                    \"other\": \"Autre\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Adresses\",\n            \"cta\": \"Ajouter une adresse\",\n            \"edit\": \"Modifier\",\n            \"delete\": \"Supprimer\",\n            \"cancel\": \"Annuler\",\n            \"create\": \"Créer\",\n            \"update\": \"Mettre à jour\",\n            \"setDefault\": \"Définir par défaut\",\n            \"somethingWentWrong\": \"Une erreur s'est produite. Veuillez réessayer plus tard.\",\n            \"EmptyState\": {\n                \"title\": \"Vous n'avez pas d'adresse\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Le prénom est requis\",\n                \"lastNameRequired\": \"Le nom de famille est requis\",\n                \"addressLine1Required\": \"Ligne d'adresse 1 est requise\",\n                \"cityRequired\": \"La ville est requise\",\n                \"countryRequired\": \"Le pays est requis\",\n                \"stateRequired\": \"État/province est requis\",\n                \"postalCodeRequired\": \"Le code postal est requis\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"Paramètres des comptes\",\n            \"changePassword\": \"Modifier le mot de passe\",\n            \"passwordUpdated\": \"Le mot de passe a bien été mis à jour !\",\n            \"settingsUpdated\": \"Les paramètres du compte ont été mis à jour.\",\n            \"somethingWentWrong\": \"Une erreur s'est produite. Veuillez réessayer plus tard.\",\n            \"currentPassword\": \"Mot de passe actuel\",\n            \"newPassword\": \"Nouveau mot de passe\",\n            \"confirmPassword\": \"Confirmer le mot de passe\",\n            \"cta\": \"Mettre à jour\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Préférences de marketing\",\n                \"label\": \"Abonnez-vous à notre newsletter.\",\n                \"marketingPreferencesUpdated\": \"Les préférences marketing ont bien été mises à jour.\",\n                \"somethingWentWrong\": \"Une erreur s'est produite. Veuillez réessayer plus tard.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Le prénom est requis\",\n                \"firstNameTooSmall\": \"Le prénom doit comporter au moins 2 caractères\",\n                \"lastNameRequired\": \"Le nom de famille est requis\",\n                \"lastNameTooSmall\": \"Le nom de famille doit comporter au moins 2 caractères\",\n                \"emailRequired\": \"L'adresse e-mail est requise\",\n                \"emailInvalid\": \"Veuillez saisir une adresse e-mail valide\",\n                \"currentPasswordRequired\": \"Vous devez saisir le mot de passe actuel\",\n                \"passwordRequired\": \"Un mot de passe est requis\",\n                \"passwordTooSmall\": \"Le mot de passe doit comporter au moins {minLength, plural, =1 {1 character} other {# characters}} caractères\",\n                \"passwordLowercaseRequired\": \"Le mot de passe doit comporter au moins une lettre minuscule\",\n                \"passwordUppercaseRequired\": \"Le mot de passe doit comporter au moins une lettre majuscule\",\n                \"passwordNumberRequired\": \"Le mot de passe doit comporter au moins {minNumbers, plural, =1 {one number} other {# numbers}} chiffres\",\n                \"passwordSpecialCharacterRequired\": \"Le mot de passe doit comporter au moins un caractère spécial\",\n                \"passwordsMustMatch\": \"Les mots de passe ne correspondent pas\",\n                \"confirmPasswordRequired\": \"Veuillez confirmer votre mot de passe\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Actions de la liste d'envies\",\n        \"title\": \"Listes d'envies\",\n        \"new\": \"Nouvelle liste d'envies\",\n        \"items\": \"{count, plural, =1 {1 article} other {# articles}}\",\n        \"viewWishlist\": \"Afficher la liste\",\n        \"noWishlists\": \"Vous n'avez pas de listes d'envies\",\n        \"noWishlistsCallToAction\": \"Créer une liste d'envies\",\n        \"emptyWishlist\": \"Vous n’avez pas ajouté de produits à cette liste d'envies.\",\n        \"share\": \"Partager\",\n        \"shareSuccess\": \"La liste d'envies a été partagée avec succès.\",\n        \"shareCopied\": \"L'URL publique de la liste d'envies a été copiée dans le presse-papier.\",\n        \"shareDisabled\": \"Votre liste de souhaits doit être publique pour pouvoir être partagée.\",\n        \"makePublic\": \"Rendre public\",\n        \"makePrivate\": \"Rendre privé\",\n        \"rename\": \"Renommer\",\n        \"delete\": \"Supprimer\",\n        \"removeButtonTitle\": \"Retirer le produit de la liste d'envies\",\n        \"Visibility\": {\n            \"public\": \"Publique\",\n            \"private\": \"Privé\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Annuler\",\n            \"close\": \"Fermer\",\n            \"copy\": \"Copier\",\n            \"create\": \"Créer\",\n            \"save\": \"Enregistrer\",\n            \"delete\": \"Supprimer\",\n            \"newTitle\": \"Créer une nouvelle liste d’envies\",\n            \"shareTitle\": \"Partager {name}\",\n            \"renameTitle\": \"Renommer {name}\",\n            \"deleteTitle\": \"Supprimer {name}\",\n            \"changeVisibilityPublicTitle\": \"Rendre {name} publique ?\",\n            \"changeVisibilityPrivateTitle\": \"Rendre {name} privé ?\",\n            \"makePublicContent\": \"Voulez-vous vraiment rendre la liste <bold>{name}</bold> publique ? Vous permettrez ainsi aux personnes disposant du lien d'accéder à votre liste d'envies.\",\n            \"makePrivateContent\": \"Voulez-vous vraiment rendre la liste <bold>{name}</bold> privée ? Si vous avez partagé votre liste d'envies avec d'autres personnes, celles-ci ne pourront plus la voir.\",\n            \"deleteContent\": \"Voulez-vous vraiment supprimer <bold>{name}</bold> ? Cette action ne peut pas être annulée.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Nom\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"Votre liste d'envies doit avoir un nom.\",\n            \"updateFailed\": \"Impossible de mettre à jour votre liste d'envies. Veuillez réessayer.\",\n            \"deleteFailed\": \"Échec de la suppression de votre liste d'envies. Veuillez réessayer.\",\n            \"removeProductFailed\": \"Impossible de retirer le produit de votre liste d'envies. Veuillez réessayer.\",\n            \"unauthorized\": \"Vous n'êtes pas autorisé à effectuer cette action. Veuillez vous connecter et réessayer.\",\n            \"unexpected\": \"Une erreur inattendue s'est produite. Veuillez réessayer.\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"La liste d'envies a été créée avec succès.\",\n            \"updateSuccess\": \"La liste d'envies a bien été mise à jour.\",\n            \"deleteSuccess\": \"La liste d'envies a bien été supprimée.\",\n            \"removeItemSuccess\": \"L'article a été supprimé de votre liste d'envies.\"\n        },\n        \"Button\": {\n            \"label\": \"Ajouter à la liste d'envies\",\n            \"addToNewWishlist\": \"Ajouter à une nouvelle liste d'envies\",\n            \"defaultWishlistName\": \"Ma liste d'envies\",\n            \"addSuccessMessage\": \"Le produit a été ajouté à votre liste d'envies.\",\n            \"removeSuccessMessage\": \"Le produit a été retiré de votre liste d'envies.\",\n            \"Errors\": {\n                \"addProductFailed\": \"Impossible d'ajouter le produit de votre liste d'envies. Veuillez réessayer.\",\n                \"removeProductFailed\": \"Impossible de retirer le produit de votre liste d'envies. Veuillez réessayer.\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Liste d'envies publique\",\n        \"defaultName\": \"Liste d'envies publique\",\n        \"emptyWishlist\": \"Cette liste de souhaits ne contient pas encore de produits.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Accueil\",\n        \"Empty\": {\n            \"title\": \"Aucun article de blog n’a été trouvé\",\n            \"subtitle\": \"Pour accéder à d'autres contenus, revenez plus tard\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Partager\",\n            \"email\": \"E-mail\",\n            \"print\": \"Imprimer\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Panier\",\n        \"heading\": \"Votre panier\",\n        \"proceedToCheckout\": \"Procéder au paiement\",\n        \"increment\": \"Augmenter la quantité :\",\n        \"decrement\": \"Réduire la quantité\",\n        \"removeItem\": \"Supprimer l'article\",\n        \"cartCombined\": \"Nous avons remarqué que votre panier précédent contenait des articles enregistrés. Nous avons donc ajouté ces articles à votre panier actuel.\",\n        \"cartRestored\": \"Vous avez débuté vos achats sur un autre appareil. Nous avons restauré votre panier ici pour que vous puissiez reprendre là où vous en étiez.\",\n        \"cartUpdateInProgress\": \"Une mise à jour de votre panier est en cours. Voulez-vous vraiment quitter cette page ? Vos modifications pourraient être perdues.\",\n        \"originalPrice\": \"Le prix initial était de {price}.\",\n        \"currentPrice\": \"Le prix actuel est de {price}.\",\n        \"quantityReadyToShip\": \"{quantity, number} prêts à être expédiés\",\n        \"quantityOnBackorder\": \"{quantity, number} seront en attente de réapprovisionnement\",\n        \"partiallyAvailable\": \"Seulement {quantity, number} disponibles\",\n        \"CheckoutSummary\": {\n            \"title\": \"Récapitulatif\",\n            \"subTotal\": \"Sous-total\",\n            \"discounts\": \"Remises\",\n            \"tax\": \"Taxes\",\n            \"total\": \"Total\",\n            \"CouponCode\": {\n                \"apply\": \"Appliquer\",\n                \"couponCode\": \"Code promotionnel\",\n                \"removeCouponCode\": \"Supprimez le code promotionnel\",\n                \"invalidCouponCode\": \"Veuillez saisir un code promotionnel valide\",\n                \"cartNotFound\": \"Une erreur s’est produite lors de la récupération de votre panier\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Livraison\",\n                \"add\": \"Ajouter\",\n                \"change\": \"Modifier\",\n                \"cancel\": \"Annuler\",\n                \"country\": \"Pays\",\n                \"city\": \"Ville\",\n                \"state\": \"État/Province\",\n                \"postalCode\": \"Code postal\",\n                \"updatedShippingOptions\": \"Mettre à jour les options de livraison\",\n                \"viewShippingOptions\": \"Voir les options de livraison\",\n                \"editAddress\": \"Modifier l'adresse\",\n                \"shippingOptions\": \"Options de livraison\",\n                \"updateShipping\": \"Mettre à jour l’expédition\",\n                \"addShipping\": \"Ajouter l’expédition\",\n                \"cartNotFound\": \"Une erreur s’est produite lors de la récupération de votre panier\",\n                \"noShippingOptions\": \"Aucune option de livraison n'est disponible pour votre adresse\",\n                \"countryRequired\": \"Le pays est requis\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Chèque-cadeau\",\n            \"giftCertificateCode\": \"Code du chèque-cadeau\",\n            \"removeGiftCertificate\": \"Supprimer le chèque-cadeau\",\n            \"apply\": \"Appliquer\",\n            \"to\": \"À\",\n            \"message\": \"Message\",\n            \"invalidGiftCertificate\": \"Veuillez saisir un code de chèque-cadeau valide\",\n            \"cartNotFound\": \"Une erreur s’est produite lors de la récupération de votre panier\"\n        },\n        \"Empty\": {\n            \"title\": \"Votre panier est vide.\",\n            \"subtitle\": \"Ajoutez quelques produits pour commencer.\",\n            \"cta\": \"Continuer mes achats\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Une erreur s’est produite lors de la récupération de votre panier\",\n            \"lineItemNotFound\": \"Ligne introuvable.\",\n            \"failedToUpdateQuantity\": \"Échec de la mise à jour de la quantité.\",\n            \"somethingWentWrong\": \"Une erreur s'est produite. Veuillez réessayer plus tard.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Comparer les produits\",\n        \"addToCart\": \"Ajouter au panier\",\n        \"next\": \"Prochains produits\",\n        \"previous\": \"Produits précédents\",\n        \"noProductsToCompare\": \"Aucun produit à comparer\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Poids\",\n        \"description\": \"Description\",\n        \"noDescription\": \"Aucune description disponible.\",\n        \"rating\": \"Note\",\n        \"noRatings\": \"Il n'existe aucun avis.\",\n        \"otherDetails\": \"Autres informations\",\n        \"noOtherDetails\": \"Aucune information supplémentaire.\",\n        \"viewOptions\": \"Afficher les options\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 article} other {# articles}} ajouté(s) à <cartLink>votre panier</cartLink>\",\n        \"missingCart\": \"Panier introuvable. Veuillez réessayer plus tard.\",\n        \"unknownError\": \"Erreur inconnue. Veuillez réessayer plus tard.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Quantité\",\n            \"increaseQuantity\": \"Augmenter la quantité :\",\n            \"decreaseQuantity\": \"Réduire la quantité\",\n            \"emptySelectPlaceholder\": \"Sélectionner une option\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 article} other {# articles}} ajouté(s) à <cartLink>votre panier</cartLink>\",\n            \"missingCart\": \"Panier introuvable. Veuillez réessayer plus tard !\",\n            \"unknownError\": \"Erreur inconnue. Veuillez réessayer plus tard.\",\n            \"variantRequiredError\": \"Ce produit nécessite de sélectionner des options pour pouvoir être ajouté au panier.\",\n            \"increaseNumber\": \"Augmenter le nombre\",\n            \"decreaseNumber\": \"Réduire le nombre\",\n            \"thumbnail\": \"Voir le numéro de l’image\",\n            \"additionalInformation\": \"Informations supplémentaires\",\n            \"currentStock\": \"{quantity, number} en stock\",\n            \"backorderQuantity\": \"{quantité, nombre} en attente de réapprovisionnement\",\n            \"loadingMoreImages\": \"Chargement de plus d'images\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 image supplémentaire chargée} other {# images supplémentaires chargées}}\",\n            \"Submit\": {\n                \"addToCart\": \"Ajouter au panier\",\n                \"outOfStock\": \"En rupture de stock\",\n                \"preorder\": \"Précommander\",\n                \"unavailable\": \"Indisponible\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Caractéristiques\",\n                \"warranty\": \"Garantie\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Poids\",\n                \"condition\": \"Condition\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Produits associés\",\n            \"noRelatedProducts\": \"Aucun produit associé n’a été trouvé\",\n            \"browseCatalog\": \"Parcourez l'intégralité de notre catalogue de produits.\",\n            \"cta\": \"Tout parcourir\",\n            \"previousProducts\": \"Produits précédents\",\n            \"nextProducts\": \"Prochains produits\",\n            \"scrollbar\": \"Barre de défilement des produits associés\"\n        },\n        \"Reviews\": {\n            \"title\": \"Avis\",\n            \"empty\": \"Aucun avis n’a été ajouté pour ce produit.\",\n            \"previous\": \"Avis précédents\",\n            \"next\": \"Avis suivants\",\n            \"Form\": {\n                \"button\": \"Rédiger un avis\",\n                \"title\": \"Rédiger un avis\",\n                \"submit\": \"Envoyer\",\n                \"cancel\": \"Annuler\",\n                \"ratingLabel\": \"Note\",\n                \"titleLabel\": \"Titre\",\n                \"reviewLabel\": \"Avis\",\n                \"nameLabel\": \"Nom\",\n                \"emailLabel\": \"E-mail\",\n                \"successMessage\": \"Votre avis a bien été envoyé !\",\n                \"somethingWentWrong\": \"Une erreur s'est produite. Veuillez réessayer plus tard.\",\n                \"recaptchaRequired\": \"Veuillez compléter la vérification reCAPTCHA.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Le titre est obligatoire\",\n                    \"authorRequired\": \"Le nom doit être renseigné\",\n                    \"emailRequired\": \"L'adresse e-mail est requise\",\n                    \"emailInvalid\": \"Veuillez saisir une adresse e-mail valide\",\n                    \"textRequired\": \"Une révision est requise\",\n                    \"ratingRequired\": \"La note est requise\",\n                    \"ratingTooSmall\": \"La note doit être supérieure ou égale à 1\",\n                    \"ratingTooLarge\": \"La note doit être au maximum de 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Accueil\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Accueil\",\n            \"Form\": {\n                \"success\": \"Merci de nous avoir contactés. Nous reviendrons vers vous bientôt.\",\n                \"successCta\": \"Continuer mes achats\",\n                \"fullName\": \"Nom complet\",\n                \"companyName\": \"Nom de l'entreprise\",\n                \"phone\": \"Numéro de téléphone\",\n                \"orderNo\": \"Numéro de commande\",\n                \"rma\": \"Numéro Autorisation de retour de marchandise (RMA)\",\n                \"email\": \"E-mail\",\n                \"comments\": \"Commentaires/Questions\",\n                \"cta\": \"Envoyer le formulaire\",\n                \"somethingWentWrong\": \"Une erreur s'est produite. Veuillez réessayer plus tard.\",\n                \"recaptchaRequired\": \"Veuillez compléter la vérification reCAPTCHA.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"Maintenance\",\n        \"message\": \"En raison de procédures de maintenance, nos services sont indisponibles.\",\n        \"contactUs\": \"Vous pouvez nous contacter à :\"\n    },\n    \"Error\": {\n        \"title\": \"Une erreur de serveur s'est produite !\",\n        \"subtitle\": \"Veuillez réessayer plus tard.\",\n        \"cta\": \"Réessayez\"\n    },\n    \"NotFound\": {\n        \"title\": \"Page introuvable !\",\n        \"subtitle\": \"Essayez de rechercher autre chose ou revenez à la page d'accueil.\",\n        \"featuredProducts\": \"Produits en vedette\",\n        \"search\": \"Rechercher\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Accueil\",\n            \"toggleNavigation\": \"Basculer la navigation\",\n            \"Icons\": {\n                \"account\": \"Profil\",\n                \"cart\": \"Panier\",\n                \"search\": \"Ouvrir la fenêtre contextuelle de recherche\",\n                \"giftCertificates\": \"Chèques-cadeaux\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Changer de devise\",\n                \"invalidCurrency\": \"Devise non valide\",\n                \"errorUpdatingCurrency\": \"Erreur lors de la mise à jour de la devise de votre panier. Veuillez réessayer.\"\n            },\n            \"Search\": {\n                \"products\": \"Produits\",\n                \"categories\": \"Catégories\",\n                \"brands\": \"Marques\",\n                \"noSearchResultsTitle\": \"Désolé, aucun résultat pour « {term} ».\",\n                \"noSearchResultsSubtitle\": \"Veuillez modifier votre recherche.\",\n                \"somethingWentWrong\": \"Une erreur s'est produite. Veuillez réessayer.\",\n                \"inputPlaceholder\": \"Recherchez des produits, des catégories, des marques...\",\n                \"submitLabel\": \"Rechercher\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Accueil\",\n            \"contactUs\": \"Nous contacter\",\n            \"socialMediaLinks\": \"Lien vers les médias sociaux\",\n            \"categories\": \"Catégories\",\n            \"brands\": \"Marques\",\n            \"navigate\": \"Parcourir\",\n            \"giftCertificates\": \"Chèques-cadeaux\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Abonnez-vous à notre newsletter\",\n            \"placeholder\": \"Saisissez votre adresse e-mail\",\n            \"description\": \"Restez informé des dernières nouvelles et offres de notre magasin.\",\n            \"subscribedToNewsletter\": \"Votre abonnement à la newsletter a bien été pris en compte !\",\n            \"Errors\": {\n                \"emailRequired\": \"L'adresse e-mail est requise\",\n                \"invalidEmail\": \"Veuillez saisir une adresse e-mail valide\",\n                \"somethingWentWrong\": \"Une erreur s'est produite. Veuillez réessayer plus tard.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Tout rejeter\",\n                \"acceptAll\": \"Accepter tout\",\n                \"customize\": \"Personnaliser\",\n                \"save\": \"Enregistrer les paramètres\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"Nous respectons votre vie privée\",\n                \"description\": \"Ce site utilise des cookies pour améliorer votre expérience de navigation, analyser le trafic du site et afficher du contenu personnalisé.\",\n                \"privacyPolicy\": \"Politique de confidentialité\"\n            },\n            \"Dialog\": {\n                \"title\": \"Paramètres de confidentialité\",\n                \"description\": \"Personnalisez vos paramètres de confidentialité ici. Vous pouvez choisir les types de cookies et de technologies de suivi que vous souhaitez autoriser.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Strictement nécessaire\",\n                    \"description\": \"Ces cookies sont essentiels au bon fonctionnement du site web et ne peuvent pas être désactivés.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Fonctionnalité\",\n                    \"description\": \"Ces cookies permettent d’améliorer la fonctionnalité et la personnalisation du site Web.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"Ces cookies sont utilisés pour diffuser des publicités pertinentes et évaluer leur efficacité.\"\n                },\n                \"measurement\": {\n                    \"title\": \"Statistiques\",\n                    \"description\": \"Ces cookies nous aident à comprendre comment les visiteurs interagissent avec le site Web et à améliorer ses performances.\"\n                },\n                \"experience\": {\n                    \"title\": \"Expérience\",\n                    \"description\": \"Ces cookies nous aident à offrir une meilleure expérience utilisateur et à tester de nouvelles fonctionnalités.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"Le prix initial était de {price}.\",\n            \"currentPrice\": \"Le prix actuel est de {price}.\",\n            \"range\": \"Prix de {minValue} à {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Chèques-cadeaux\",\n        \"description\": \"Offrez le cadeau idéal qui ne se démode jamais. Laissez vos amis et vos proches choisir exactement ce qu’ils veulent parmi toute notre collection.\",\n        \"purchaseLabel\": \"Acheter\",\n        \"checkBalanceLabel\": \"Vérifier le solde\",\n        \"expiresAtLabel\": \"Valable jusqu’au\",\n        \"CheckBalance\": {\n            \"title\": \"Vérifier le solde\",\n            \"description\": \"Vous pouvez vérifier le solde et obtenir des informations sur votre chèque-cadeau en saisissant le code dans la case ci-dessous.\",\n            \"inputLabel\": \"Code\",\n            \"inputPlaceholder\": \"XXX-XXX-XXX-XXX\",\n            \"purchasedDateLabel\": \"Acheté(s\",\n            \"senderLabel\": \"De\",\n            \"Errors\": {\n                \"invalidCode\": \"Le code du chèque-cadeau que vous avez saisi n’est pas valide. Veuillez vérifier le code et réessayer.\",\n                \"codeRequired\": \"Veuillez saisir un code de chèque-cadeau.\",\n                \"somethingWentWrong\": \"Une erreur s'est produite. Veuillez réessayer plus tard.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Acheter un chèque-cadeau\",\n            \"title\": \"Chèque-cadeau numérique\",\n            \"description\": \"Découvrez nos chèques-cadeaux, parfaits pour toutes les occasions. Choisissez le montant et personnalisez votre message.\",\n            \"successMessage\": \"Le chèque-cadeau a été ajouté à <cartLink> votre panier</cartLink>\",\n            \"missingCart\": \"Panier introuvable. Veuillez réessayer plus tard.\",\n            \"unknownError\": \"Erreur inconnue. Veuillez réessayer plus tard.\",\n            \"Form\": {\n                \"amountLabel\": \"Montant\",\n                \"customAmountLabel\": \"Montant (entre {minAmount} et {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Sélectionnez un montant\",\n                \"customAmountPlaceholder\": \"Entrez un montant personnalisé\",\n                \"senderNameLabel\": \"Votre nom\",\n                \"senderEmailLabel\": \"Votre adresse e-mail\",\n                \"recipientNameLabel\": \"Nom du destinataire\",\n                \"recipientEmailLabel\": \"Adresse e-mail du destinataire\",\n                \"namePlaceholder\": \"Saisissez un nom\",\n                \"emailPlaceholder\": \"Saisir l'adresse e-mail\",\n                \"messageLabel\": \"Message\",\n                \"messagePlaceholder\": \"Veuillez saisir votre message (facultatif)\",\n                \"nonRefundableCheckboxLabel\": \"Je reconnais que les chèques-cadeaux ne sont pas remboursables\",\n                \"expiryCheckboxLabel\": \"Je reconnais que ce chèque-cadeau expirera le {expiryDate}\",\n                \"ctaLabel\": \"Ajouter au panier\",\n                \"Errors\": {\n                    \"amountRequired\": \"Veuillez sélectionner ou saisir un montant de chèque-cadeau\",\n                    \"amountInvalid\": \"Veuillez sélectionner un montant de chèque-cadeau valide\",\n                    \"amountOutOfRange\": \"Veuillez saisir un montant entre {minAmount} et {maxAmount}\",\n                    \"unexpectedSettingsError\": \"Une erreur inattendue s’est produite lors de la récupération des paramètres du chèque-cadeau. Veuillez réessayer plus tard.\",\n                    \"senderNameRequired\": \"Votre nom est requis\",\n                    \"senderEmailRequired\": \"Votre adresse e-mail est requise\",\n                    \"recipientNameRequired\": \"Le nom du destinataire est obligatoire\",\n                    \"recipientEmailRequired\": \"L’adresse e-mail du destinataire est obligatoire\",\n                    \"emailInvalid\": \"Veuillez saisir une adresse e-mail valide\",\n                    \"checkboxRequired\": \"Vous devez cocher cette case pour continuer\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"facultatif\",\n        \"recaptchaRequired\": \"Veuillez compléter la vérification reCAPTCHA.\",\n        \"Errors\": {\n            \"invalidInput\": \"Veuillez vérifier votre saisie et réessayer\",\n            \"invalidFormat\": \"La valeur saisie ne correspond pas au format requis\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/it.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Nuove scoperte per ogni occasione\",\n                \"description\": \"Scopri i nostri ultimi arrivi, selezionati per offrirti stile, funzionalità e ispirazione. Acquista ora e scopri il tuo prossimo articolo preferito.\",\n                \"alt\": \"Cinque piccole piante in vaso esposte su blocchi impilati beige, con una varietà di fogliame verde in vasi grigio scuro su uno sfondo neutro.\",\n                \"cta\": \"Acquista ora\"\n            },\n            \"Slide02\": {\n                \"title\": \"Scopri le novità\",\n                \"description\": \"Acquista i nostri ultimi arrivi e trova qualcosa di nuovo ed entusiasmante per la tua casa.\",\n                \"alt\": \"Le mani si allungano per tenere una felce verde in un cesto intrecciato con un fiocco decorativo, su uno sfondo beige con ombre morbide.\",\n                \"cta\": \"Acquista ora\"\n            },\n            \"Slide03\": {\n                \"title\": \"Qualcosa per tutti\",\n                \"description\": \"Non perdere le offerte esclusive sui nostri prodotti più venduti. Acquista oggi e risparmia alla grande sugli articoli che ami.\",\n                \"alt\": \"Primo piano di una foglia verde brillante con perforazioni, che mette in risalto la sua struttura liscia e i dettagli naturali.\",\n                \"cta\": \"Acquista ora\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Collezione in evidenza\",\n            \"description\": \"Esplora le nostre migliori scelte in questa collezione in evidenza. Trova il regalo perfetto o fatti un regalo.\",\n            \"cta\": \"Visualizza altro\",\n            \"emptyStateTitle\": \"Nessun prodotto trovato\",\n            \"emptyStateSubtitle\": \"Prova a sfogliare il nostro catalogo completo di prodotti.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Novità\",\n            \"description\": \"I nostri ultimi prodotti sono arrivati. Scopri le novità in negozio.\",\n            \"cta\": \"Vedi tutto\",\n            \"emptyStateTitle\": \"Nessun prodotto trovato\",\n            \"emptyStateSubtitle\": \"Prova a sfogliare il nostro catalogo completo di prodotti.\",\n            \"previousProducts\": \"Prodotti precedenti\",\n            \"nextProducts\": \"Prodotti successivi\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Cambia password\",\n            \"newPassword\": \"Nuova password\",\n            \"confirmPassword\": \"Conferma password\",\n            \"passwordUpdated\": \"La password è stata aggiornata!\",\n            \"somethingWentWrong\": \"Si è verificato un errore. Riprova più tardi.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"La password è obbligatoria\",\n                \"passwordTooSmall\": \"La password deve essere lunga almeno {minLength, plural, =1 {1 carattere} other {# caratteri}}\",\n                \"passwordLowercaseRequired\": \"La password deve contenere almeno una lettera minuscola\",\n                \"passwordUppercaseRequired\": \"La password deve contenere almeno una lettera maiuscola\",\n                \"passwordNumberRequired\": \"La password deve contenere almeno {minNumbers, plural, =1 {un numero} other {# numeri}}\",\n                \"passwordSpecialCharacterRequired\": \"La password deve contenere almeno un carattere speciale\",\n                \"passwordsMustMatch\": \"Le password non corrispondono\",\n                \"confirmPasswordRequired\": \"Conferma la tua password\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Accedi\",\n            \"heading\": \"Accedi\",\n            \"forgotPassword\": \"Hai dimenticato la password?\",\n            \"cta\": \"Accedi\",\n            \"email\": \"E-mail\",\n            \"password\": \"Password\",\n            \"invalidCredentials\": \"L'indirizzo e-mail o la password non sono corretti. Prova ad accedere di nuovo o reimposta la password\",\n            \"somethingWentWrong\": \"Si è verificato un errore. Riprova più tardi.\",\n            \"passwordResetRequired\": \"È necessario reimpostare la password. Controlla la tua email per le istruzioni su come reimpostare la password.\",\n            \"invalidToken\": \"Il tuo link di accesso non è valido o è scaduto. Riprova ad accedere.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"L'indirizzo email è obbligatorio\",\n                \"emailInvalid\": \"Inserisci un indirizzo e-mail valido\",\n                \"passwordRequired\": \"La password è obbligatoria\",\n                \"invalidInput\": \"Controlla i dati inseriti e riprova.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"Nuovo cliente?\",\n                \"accountBenefits\": \"Crea un account con noi e sarai in grado di:\",\n                \"fastCheckout\": \"Completare gli acquisti più velocemente\",\n                \"multipleAddresses\": \"Salvare molteplici indirizzi di spedizione\",\n                \"ordersHistory\": \"Accedere allo storico dei tuoi ordini\",\n                \"ordersTracking\": \"Tracciare i nuovi ordini\",\n                \"wishlists\": \"Salva articoli nella Lista desideri\",\n                \"cta\": \"Crea account\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Password dimenticata\",\n                \"subtitle\": \"Inserisci di seguito l'e-mail associata al tuo account. Ti invieremo le istruzioni per reimpostare la password.\",\n                \"confirmResetPassword\": \"Se l'indirizzo e-mail {email} è collegato a un account nel nostro negozio, ti abbiamo inviato un'e-mail per reimpostare la password. Se non la trovi, controlla le cartelle di posta in arrivo e indesiderata.\",\n                \"somethingWentWrong\": \"Si è verificato un errore. Riprova più tardi.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"L'indirizzo email è obbligatorio\",\n                    \"emailInvalid\": \"Inserisci un indirizzo e-mail valido\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Registra un account\",\n            \"heading\": \"Nuovo account\",\n            \"cta\": \"Crea account\",\n            \"somethingWentWrong\": \"Si è verificato un errore. Riprova più tardi.\",\n            \"recaptchaRequired\": \"Completa la verifica reCAPTCHA.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Il nome è obbligatorio\",\n                \"lastNameRequired\": \"Il cognome è obbligatorio\",\n                \"emailRequired\": \"L'indirizzo email è obbligatorio\",\n                \"emailInvalid\": \"Inserisci un indirizzo e-mail valido\",\n                \"passwordRequired\": \"La password è obbligatoria\",\n                \"passwordTooSmall\": \"La password deve essere lunga almeno {minLength, plural, =1 {1 carattere} other {# caratteri}}\",\n                \"passwordLowercaseRequired\": \"La password deve contenere almeno una lettera minuscola\",\n                \"passwordUppercaseRequired\": \"La password deve contenere almeno una lettera maiuscola\",\n                \"passwordNumberRequired\": \"La password deve contenere almeno {minNumbers, plural, =1 {un numero} other {# numeri}}\",\n                \"passwordSpecialCharacterRequired\": \"La password deve contenere almeno un carattere speciale\",\n                \"passwordsMustMatch\": \"Le password non corrispondono\",\n                \"addressLine1Required\": \"La riga 1 dell'indirizzo è obbligatoria\",\n                \"cityRequired\": \"La Città è necessaria\",\n                \"countryRequired\": \"Il paese è obbligatorio\",\n                \"stateRequired\": \"Lo Stato/Provincia è obbligatorio\",\n                \"postalCodeRequired\": \"Il CAP è necessario\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"Nessun prodotto in questo marchio\",\n                \"subtitle\": \"Prova a usare filtri diversi.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Categorie\",\n            \"Empty\": {\n                \"title\": \"Non sono presenti prodotti in questa categoria\",\n                \"subtitle\": \"Prova a usare filtri diversi.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Risultati della ricerca\",\n            \"searchResults\": \"Risultati di ricerca per\",\n            \"subCategories\": \"Categorie\",\n            \"Breadcrumbs\": {\n                \"home\": \"Home\",\n                \"search\": \"Cerca\"\n            },\n            \"Empty\": {\n                \"title\": \"Spiacenti, nessun risultato per \\\"{term}\\\".\",\n                \"subtitle\": \"Prova un'altra ricerca.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtri\",\n            \"resetFilters\": \"Reimposta filtri\",\n            \"Range\": {\n                \"apply\": \"Applica\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Spedizione gratuita\",\n                \"isFeaturedLabel\": \"In evidenza\",\n                \"inStockLabel\": \"Disponibili\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Ordina per:\",\n            \"featuredItems\": \"Articoli in primo piano\",\n            \"bestSellingItems\": \"Articoli più venduti\",\n            \"newestItems\": \"Articoli più recenti\",\n            \"aToZ\": \"da A a Z\",\n            \"zToA\": \"da Z a A\",\n            \"byReview\": \"Per recensione\",\n            \"priceAscending\": \"Prezzo: ascendente\",\n            \"priceDescending\": \"Prezzo: discendente\",\n            \"relevance\": \"Rilevanza\"\n        },\n        \"Compare\": {\n            \"compare\": \"Confronta\",\n            \"remove\": \"Rimuovi\",\n            \"maxCompareLimit\": \"È stato raggiunto il numero massimo di prodotti per il confronto. Rimuovi un prodotto per aggiungerne uno nuovo.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Indirizzi\",\n            \"logout\": \"Esci\",\n            \"orders\": \"Ordini\",\n            \"settings\": \"Account\",\n            \"wishlists\": \"Liste desideri\"\n        },\n        \"Orders\": {\n            \"title\": \"Ordini\",\n            \"orderNumber\": \"Ordine #\",\n            \"totalPrice\": \"Totale\",\n            \"viewDetails\": \"Visualizza dettagli\",\n            \"EmptyState\": {\n                \"title\": \"Non hai ordini\",\n                \"cta\": \"Acquista ora\"\n            },\n            \"Details\": {\n                \"title\": \"Ordine n. {orderNumber}\",\n                \"shippingAddress\": \"Indirizzo di spedizione\",\n                \"shippingMethod\": \"Metodo di spedizione\",\n                \"summaryTotal\": \"Totale\",\n                \"destination\": \"Destinazione\",\n                \"destinationWithCount\": \"Destinazione {number, number}/{total, number}\",\n                \"digitalDelivery\": \"Consegna digitale a {email}\",\n                \"subtotal\": \"Subtotale\",\n                \"shipping\": \"Spedizioni\",\n                \"tax\": \"Tasse\",\n                \"orderSummary\": \"Riepilogo ordine\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Metodo di pagamento} other {Metodi di pagamento}}\",\n                \"paymentEndingInLabel\": \"Che termina con\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Carta di credito\",\n                    \"giftCertificate\": \"Buono regalo\",\n                    \"storeCredit\": \"Credito dello store\",\n                    \"other\": \"Altro\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Indirizzi\",\n            \"cta\": \"Aggiungi indirizzo\",\n            \"edit\": \"Modifica\",\n            \"delete\": \"Elimina\",\n            \"cancel\": \"Annulla\",\n            \"create\": \"crea\",\n            \"update\": \"Aggiorna\",\n            \"setDefault\": \"Imposta come predefinito\",\n            \"somethingWentWrong\": \"Si è verificato un errore. Riprova più tardi.\",\n            \"EmptyState\": {\n                \"title\": \"Non hai indirizzi\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Il nome è obbligatorio\",\n                \"lastNameRequired\": \"Il cognome è obbligatorio\",\n                \"addressLine1Required\": \"La riga 1 dell'indirizzo è obbligatoria\",\n                \"cityRequired\": \"La Città è necessaria\",\n                \"countryRequired\": \"Il paese è obbligatorio\",\n                \"stateRequired\": \"Lo Stato/Provincia è obbligatorio\",\n                \"postalCodeRequired\": \"Il CAP è necessario\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"Impostazioni account\",\n            \"changePassword\": \"Cambia password\",\n            \"passwordUpdated\": \"La password è stata aggiornata!\",\n            \"settingsUpdated\": \"Impostazioni dell'account aggiornate.\",\n            \"somethingWentWrong\": \"Si è verificato un errore. Riprova più tardi.\",\n            \"currentPassword\": \"Password attuale\",\n            \"newPassword\": \"Nuova password\",\n            \"confirmPassword\": \"Conferma password\",\n            \"cta\": \"Aggiorna\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Preferenze di marketing\",\n                \"label\": \"Iscriviti alla nostra newsletter.\",\n                \"marketingPreferencesUpdated\": \"Le preferenze di marketing sono state aggiornate.\",\n                \"somethingWentWrong\": \"Si è verificato un errore. Riprova più tardi.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Il nome è obbligatorio\",\n                \"firstNameTooSmall\": \"Il nome deve essere lungo almeno 2 caratteri\",\n                \"lastNameRequired\": \"Il cognome è obbligatorio\",\n                \"lastNameTooSmall\": \"Il cognome deve essere lungo almeno 2 caratteri\",\n                \"emailRequired\": \"L'indirizzo email è obbligatorio\",\n                \"emailInvalid\": \"Inserisci un indirizzo e-mail valido\",\n                \"currentPasswordRequired\": \"È necessario inserire la password attuale\",\n                \"passwordRequired\": \"La password è obbligatoria\",\n                \"passwordTooSmall\": \"La password deve essere lunga almeno {minLength, plural, =1 {1 carattere} other {# caratteri}}\",\n                \"passwordLowercaseRequired\": \"La password deve contenere almeno una lettera minuscola\",\n                \"passwordUppercaseRequired\": \"La password deve contenere almeno una lettera maiuscola\",\n                \"passwordNumberRequired\": \"La password deve contenere almeno {minNumbers, plural, =1 {un numero} other {# numeri}}\",\n                \"passwordSpecialCharacterRequired\": \"La password deve contenere almeno un carattere speciale\",\n                \"passwordsMustMatch\": \"Le password non corrispondono\",\n                \"confirmPasswordRequired\": \"Conferma la tua password\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Azioni della lista dei desideri\",\n        \"title\": \"Liste desideri\",\n        \"new\": \"Nuova Lista desideri\",\n        \"items\": \"{count, plural, =1 {1 item} other {# items}}\",\n        \"viewWishlist\": \"Visualizza elenco\",\n        \"noWishlists\": \"Non hai nessuna lista dei desideri\",\n        \"noWishlistsCallToAction\": \"Crea una lista desideri\",\n        \"emptyWishlist\": \"Non hai aggiunto prodotti a questa lista dei desideri.\",\n        \"share\": \"Condividi\",\n        \"shareSuccess\": \"La lista dei desideri è stata condivisa correttamente.\",\n        \"shareCopied\": \"L'URL pubblico della lista dei desideri è stato copiato negli appunti.\",\n        \"shareDisabled\": \"Per poter condividere la tua lista dei desideri, questa deve essere pubblica.\",\n        \"makePublic\": \"Rendi pubblico\",\n        \"makePrivate\": \"Rendi privato\",\n        \"rename\": \"Rinomina\",\n        \"delete\": \"Elimina\",\n        \"removeButtonTitle\": \"Rimuovi il prodotto dalla lista dei desideri\",\n        \"Visibility\": {\n            \"public\": \"Pubblica\",\n            \"private\": \"Privato\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Annulla\",\n            \"close\": \"Chiudi\",\n            \"copy\": \"Copia\",\n            \"create\": \"crea\",\n            \"save\": \"Salva\",\n            \"delete\": \"Elimina\",\n            \"newTitle\": \"Crea una nuova lista dei desideri\",\n            \"shareTitle\": \"Condividi {name}\",\n            \"renameTitle\": \"Rinomina {name}\",\n            \"deleteTitle\": \"Elimina {name}\",\n            \"changeVisibilityPublicTitle\": \"Rendi pubblico {name}?\",\n            \"changeVisibilityPrivateTitle\": \"Vuoi rendere {name} privato?\",\n            \"makePublicContent\": \"Vuoi davvero rendere <bold>{name}</bold> pubblico? In questo modo, consentirai agli ad altri di vedere la tua lista dei desideri se dispongono del link.\",\n            \"makePrivateContent\": \"Vuoi davvero rendere <bold>{name}</bold> privato? Se hai condiviso la tua lista dei desideri con altri, essi non potranno più vederla.\",\n            \"deleteContent\": \"Eliminare <bold>{name}</bold>? L'azione non può essere annullata.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Nome\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"Il nome della lista dei desideri non può essere vuoto.\",\n            \"updateFailed\": \"Impossibile aggiornare la tua lista dei desideri. Riprova.\",\n            \"deleteFailed\": \"Impossibile eliminare la tua lista dei desideri. Riprova.\",\n            \"removeProductFailed\": \"Impossibile rimuovere il prodotto dalla tua lista dei desideri. Riprova.\",\n            \"unauthorized\": \"Non hai l'autorizzazione a eseguire questa azione. Accedi e riprova\",\n            \"unexpected\": \"Si è verificato un errore imprevisto, riprova\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"La lista dei desideri è stata creata correttamente.\",\n            \"updateSuccess\": \"La lista dei desideri è stata aggiornata correttamente.\",\n            \"deleteSuccess\": \"La lista dei desideri è stata eliminata correttamente.\",\n            \"removeItemSuccess\": \"L'articolo è stato rimosso dalla tua lista dei desideri.\"\n        },\n        \"Button\": {\n            \"label\": \"Aggiungi alla Lista desideri\",\n            \"addToNewWishlist\": \"Aggiungi a nuova lista desideri\",\n            \"defaultWishlistName\": \"La mia lista desideri\",\n            \"addSuccessMessage\": \"Il prodotto è stato aggiunto alla tua lista dei desideri\",\n            \"removeSuccessMessage\": \"Il prodotto è stato rimosso dalla tua lista dei desideri\",\n            \"Errors\": {\n                \"addProductFailed\": \"Impossibile aggiungere il prodotto dalla tua lista dei desideri. Riprova\",\n                \"removeProductFailed\": \"Impossibile rimuovere il prodotto dalla tua lista dei desideri. Riprova\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Lista dei desideri pubblica\",\n        \"defaultName\": \"Lista dei desideri pubblica\",\n        \"emptyWishlist\": \"Questa lista dei desideri non contiene ancora prodotti.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Home\",\n        \"Empty\": {\n            \"title\": \"Nessun articolo del blog trovato\",\n            \"subtitle\": \"Controlla più tardi per ulteriori contenuti\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Condividi\",\n            \"email\": \"E-mail\",\n            \"print\": \"Stampa\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Carrello\",\n        \"heading\": \"Il tuo carrello\",\n        \"proceedToCheckout\": \"Procedi al checkout\",\n        \"increment\": \"Aumenta quantità\",\n        \"decrement\": \"Riduci quantità\",\n        \"removeItem\": \"Rimuovi articolo\",\n        \"cartCombined\": \"Abbiamo notato che avevi salvato degli articoli in un carrello precedente, quindi li abbiamo aggiunti al carrello attuale.\",\n        \"cartRestored\": \"Hai avviato un carrello su un altro dispositivo e lo abbiamo ripristinato qui, così puoi riprendere da dove avevi lasciato.\",\n        \"cartUpdateInProgress\": \"Hai un aggiornamento del carrello attivo in corso. Uscire dalla pagina? Le modifiche potrebbero andare perse.\",\n        \"originalPrice\": \"Il prezzo originale era {price}.\",\n        \"currentPrice\": \"Il prezzo corrente è {price}.\",\n        \"quantityReadyToShip\": \"{quantity, number} pronto per la spedizione\",\n        \"quantityOnBackorder\": \"{quantity, number} sarà in arretrato\",\n        \"partiallyAvailable\": \"Solo {quantity, number} disponibili\",\n        \"CheckoutSummary\": {\n            \"title\": \"Riepilogo\",\n            \"subTotal\": \"Subtotale\",\n            \"discounts\": \"Sconti\",\n            \"tax\": \"Tasse\",\n            \"total\": \"Totale\",\n            \"CouponCode\": {\n                \"apply\": \"Applica\",\n                \"couponCode\": \"Codice coupon\",\n                \"removeCouponCode\": \"Rimuovi il codice coupon\",\n                \"invalidCouponCode\": \"Inserisci un codice coupon valido\",\n                \"cartNotFound\": \"Si è verificato un errore durante il recupero del carrello\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Spedizioni\",\n                \"add\": \"Aggiungi\",\n                \"change\": \"Cambia\",\n                \"cancel\": \"Annulla\",\n                \"country\": \"Paese\",\n                \"city\": \"Città\",\n                \"state\": \"Stato/provincia\",\n                \"postalCode\": \"CAP\",\n                \"updatedShippingOptions\": \"Aggiorna le opzioni di spedizione\",\n                \"viewShippingOptions\": \"Vedi le opzioni di spedizione\",\n                \"editAddress\": \"Modifica indirizzo\",\n                \"shippingOptions\": \"Opzioni di spedizione\",\n                \"updateShipping\": \"Aggiorna la spedizione\",\n                \"addShipping\": \"Aggiungi spedizione\",\n                \"cartNotFound\": \"Si è verificato un errore durante il recupero del carrello\",\n                \"noShippingOptions\": \"Non ci sono opzioni di spedizione disponibili per il tuo indirizzo\",\n                \"countryRequired\": \"Il paese è obbligatorio\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Buono regalo\",\n            \"giftCertificateCode\": \"Codice del buono regalo\",\n            \"removeGiftCertificate\": \"Rimuovi buono regalo\",\n            \"apply\": \"Applica\",\n            \"to\": \"a\",\n            \"message\": \"Messaggio\",\n            \"invalidGiftCertificate\": \"Inserisci un codice del buono regalo valido\",\n            \"cartNotFound\": \"Si è verificato un errore durante il recupero del carrello\"\n        },\n        \"Empty\": {\n            \"title\": \"Il tuo carrello è vuoto.\",\n            \"subtitle\": \"Aggiungi alcuni prodotti per iniziare.\",\n            \"cta\": \"Continua a fare acquisti\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Si è verificato un errore durante il recupero del carrello\",\n            \"lineItemNotFound\": \"Voce di riga non trovata.\",\n            \"failedToUpdateQuantity\": \"Impossibile aggiornare la quantità.\",\n            \"somethingWentWrong\": \"Si è verificato un errore. Riprova più tardi.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Compara prodotti\",\n        \"addToCart\": \"Aggiungi al carrello\",\n        \"next\": \"Prodotti successivi\",\n        \"previous\": \"Prodotti precedenti\",\n        \"noProductsToCompare\": \"Nessun prodotto da confrontare\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Peso\",\n        \"description\": \"Description\",\n        \"noDescription\": \"Non è disponibile alcuna descrizione.\",\n        \"rating\": \"Valutazione\",\n        \"noRatings\": \"Nessuna recensione disponibile.\",\n        \"otherDetails\": \"Altri dettagli\",\n        \"noOtherDetails\": \"Non ci sono altri dettagli.\",\n        \"viewOptions\": \"Visualizza le opzioni\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 articolo} other {# articoli}} aggiunto/i al <cartLink> carrello</cartLink>\",\n        \"missingCart\": \"Carrello non trovato. Riprova più tardi.\",\n        \"unknownError\": \"Errore sconosciuto. Riprova più tardi.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Quantità\",\n            \"increaseQuantity\": \"Aumenta quantità\",\n            \"decreaseQuantity\": \"Riduci quantità\",\n            \"emptySelectPlaceholder\": \"Seleziona un'opzione\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 articolo} other {# articoli}} aggiunto/i al <cartLink> carrello</cartLink>\",\n            \"missingCart\": \"Carrello non trovato. Riprova più tardi.\",\n            \"unknownError\": \"Errore sconosciuto. Riprova più tardi.\",\n            \"variantRequiredError\": \"Questo prodotto richiede che vengano selezionate delle opzioni per poter essere aggiunto al carrello.\",\n            \"increaseNumber\": \"Aumenta numero\",\n            \"decreaseNumber\": \"Diminuisci numero\",\n            \"thumbnail\": \"Visualizza numero immagine\",\n            \"additionalInformation\": \"Informazioni aggiuntive\",\n            \"currentStock\": \"{quantità, numero} in magazzino\",\n            \"backorderQuantity\": \"{quantity, number} sarà in arretrato\",\n            \"loadingMoreImages\": \"Caricamento di altre immagini\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 altra immagine caricata} other {# altre immagini caricate}}\",\n            \"Submit\": {\n                \"addToCart\": \"Aggiungi al carrello\",\n                \"outOfStock\": \"Esaurito\",\n                \"preorder\": \"Pre-ordine\",\n                \"unavailable\": \"Non disponibile\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Specifiche\",\n                \"warranty\": \"Garanzia\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Peso\",\n                \"condition\": \"Condizioni\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Prodotti simili\",\n            \"noRelatedProducts\": \"Nessun prodotto correlato trovato\",\n            \"browseCatalog\": \"Prova a sfogliare il nostro catalogo completo di prodotti.\",\n            \"cta\": \"Acquista tutto\",\n            \"previousProducts\": \"Prodotti precedenti\",\n            \"nextProducts\": \"Prodotti successivi\",\n            \"scrollbar\": \"Barra di scorrimento dei prodotti simili\"\n        },\n        \"Reviews\": {\n            \"title\": \"Recensioni\",\n            \"empty\": \"Non sono state aggiunte recensioni per questo prodotto.\",\n            \"previous\": \"Recensioni precedenti\",\n            \"next\": \"Prossime recensioni\",\n            \"Form\": {\n                \"button\": \"Scrivi una recensione\",\n                \"title\": \"Scrivi una recensione\",\n                \"submit\": \"Invia\",\n                \"cancel\": \"Annulla\",\n                \"ratingLabel\": \"Valutazione\",\n                \"titleLabel\": \"Titolo\",\n                \"reviewLabel\": \"Recensione\",\n                \"nameLabel\": \"Nome\",\n                \"emailLabel\": \"E-mail\",\n                \"successMessage\": \"La tua recensione è stata inviata correttamente.\",\n                \"somethingWentWrong\": \"Si è verificato un errore. Riprova più tardi.\",\n                \"recaptchaRequired\": \"Completa la verifica reCAPTCHA.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Il titolo è obbligatorio\",\n                    \"authorRequired\": \"Il nome è obbligatorio\",\n                    \"emailRequired\": \"L'indirizzo email è obbligatorio\",\n                    \"emailInvalid\": \"Inserisci un indirizzo e-mail valido\",\n                    \"textRequired\": \"La revisione è obbligatoria\",\n                    \"ratingRequired\": \"La valutazione è obbligatoria\",\n                    \"ratingTooSmall\": \"Il punteggio deve essere almeno 1\",\n                    \"ratingTooLarge\": \"Il punteggio deve essere al massimo 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Home\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Home\",\n            \"Form\": {\n                \"success\": \"Grazie per averci contattato. Ti risponderemo al più presto.\",\n                \"successCta\": \"Continua a fare acquisti\",\n                \"fullName\": \"Nome completo\",\n                \"companyName\": \"Azienda\",\n                \"phone\": \"Telefono\",\n                \"orderNo\": \"Numero di ordine\",\n                \"rma\": \"Numero RMA\",\n                \"email\": \"E-mail\",\n                \"comments\": \"Commenti/domande\",\n                \"cta\": \"Invia modulo\",\n                \"somethingWentWrong\": \"Si è verificato un errore. Riprova più tardi.\",\n                \"recaptchaRequired\": \"Completa la verifica reCAPTCHA.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"MANUTENZIONE\",\n        \"message\": \"Stiamo effettuando la manutenzione\",\n        \"contactUs\": \"Puoi contattarci all'indirizzo:\"\n    },\n    \"Error\": {\n        \"title\": \"Si è verificato un errore del server.\",\n        \"subtitle\": \"Riprova più tardi.\",\n        \"cta\": \"Riprova\"\n    },\n    \"NotFound\": {\n        \"title\": \"Non è stato possibile trovare la pagina.\",\n        \"subtitle\": \"Prova a cercare qualcos'altro o torna alla home page.\",\n        \"featuredProducts\": \"Prodotti in primo piano\",\n        \"search\": \"Cerca\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Home\",\n            \"toggleNavigation\": \"Attiva/Disattiva navigazione\",\n            \"Icons\": {\n                \"account\": \"Profilo\",\n                \"cart\": \"Carrello\",\n                \"search\": \"Apri popup di ricerca\",\n                \"giftCertificates\": \"Buoni regalo\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Cambia valuta\",\n                \"invalidCurrency\": \"Valuta non valida\",\n                \"errorUpdatingCurrency\": \"Si è verificato un errore durante l'aggiornamento della valuta per il tuo carrello. Riprova.\"\n            },\n            \"Search\": {\n                \"products\": \"Prodotti\",\n                \"categories\": \"Categorie\",\n                \"brands\": \"Marchi\",\n                \"noSearchResultsTitle\": \"Spiacenti, nessun risultato per \\\"{term}\\\".\",\n                \"noSearchResultsSubtitle\": \"Prova un'altra ricerca.\",\n                \"somethingWentWrong\": \"Si è verificato un errore. Riprova.\",\n                \"inputPlaceholder\": \"Cerca prodotti, categorie, marchi...\",\n                \"submitLabel\": \"Cerca\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Home\",\n            \"contactUs\": \"Contattaci\",\n            \"socialMediaLinks\": \"Link ai social media\",\n            \"categories\": \"Categorie\",\n            \"brands\": \"Marchi\",\n            \"navigate\": \"Naviga\",\n            \"giftCertificates\": \"Buoni regalo\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Iscriviti alla nostra newsletter\",\n            \"placeholder\": \"Inserisci la tua e-mail\",\n            \"description\": \"Ricevi aggiornamenti sulle ultime novità e offerte dal nostro negozio.\",\n            \"subscribedToNewsletter\": \"Hai effettuato l'iscrizione alla nostra newsletter.\",\n            \"Errors\": {\n                \"emailRequired\": \"L'indirizzo email è obbligatorio\",\n                \"invalidEmail\": \"Inserisci un indirizzo e-mail valido\",\n                \"somethingWentWrong\": \"Si è verificato un errore. Riprova più tardi.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Rifiuta tutto\",\n                \"acceptAll\": \"Accetta tutto\",\n                \"customize\": \"Personalizza\",\n                \"save\": \"Salva impostazioni\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"Rispettiamo la tua privacy\",\n                \"description\": \"Questo sito utilizza i cookie per migliorare la tua esperienza di navigazione, analizzare il traffico del sito e mostrare contenuti personalizzati.\",\n                \"privacyPolicy\": \"Informativa sulla privacy\"\n            },\n            \"Dialog\": {\n                \"title\": \"Impostazioni privacy\",\n                \"description\": \"Personalizza le impostazioni sulla privacy qui. Puoi scegliere quali tipi di cookie e tecnologie di tracciamento desideri consentire.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Strettamente necessario\",\n                    \"description\": \"Questi cookie sono essenziali per il corretto funzionamento del sito web e non possono essere disattivati.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Funzionalità\",\n                    \"description\": \"Questi cookie consentono di migliorare la funzionalità e la personalizzazione del sito web.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"Questi cookie vengono utilizzati per fornire annunci pubblicitari pertinenti e monitorarne l'efficacia.\"\n                },\n                \"measurement\": {\n                    \"title\": \"Analisi\",\n                    \"description\": \"Questi cookie ci aiutano a capire come i visitatori interagiscono con il sito web e a migliorarne le prestazioni.\"\n                },\n                \"experience\": {\n                    \"title\": \"Esperienza\",\n                    \"description\": \"Questi cookie ci aiutano a fornire un'esperienza utente migliore e a testare nuove funzionalità.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"Il prezzo originale era {price}.\",\n            \"currentPrice\": \"Il prezzo corrente è {price}.\",\n            \"range\": \"Prezzo da {minValue} a {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Buoni regalo\",\n        \"description\": \"Fai il regalo perfetto che non passa mai di moda. Lascia che amici e persone care scelgano esattamente ciò che vogliono da tutta la nostra collezione.\",\n        \"purchaseLabel\": \"Acquista ora\",\n        \"checkBalanceLabel\": \"Controlla il saldo\",\n        \"expiresAtLabel\": \"Valido fino a\",\n        \"CheckBalance\": {\n            \"title\": \"Controlla il saldo\",\n            \"description\": \"Puoi controllare il saldo e ottenere informazioni sul tuo buono regalo inserendo il codice nella casella sottostante.\",\n            \"inputLabel\": \"Codice\",\n            \"inputPlaceholder\": \"XXX-XXX-XXX-XXX\",\n            \"purchasedDateLabel\": \"Acquistato\",\n            \"senderLabel\": \"Da\",\n            \"Errors\": {\n                \"invalidCode\": \"Il codice del buono regalo che hai inserito non è valido. Controlla il codice e riprova.\",\n                \"codeRequired\": \"Inserisci un buono regalo.\",\n                \"somethingWentWrong\": \"Si è verificato un errore. Riprova più tardi.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Acquista un buono regalo\",\n            \"title\": \"Buono regalo digitale\",\n            \"description\": \"Scopri i nostri buoni regalo, perfetti per ogni occasione. Scegli l'importo e personalizza il tuo messaggio.\",\n            \"successMessage\": \"Il buono regalo è stato aggiunto al <cartLink> tuo carrello</cartLink>\",\n            \"missingCart\": \"Carrello non trovato. Riprova più tardi.\",\n            \"unknownError\": \"Errore sconosciuto. Riprova più tardi.\",\n            \"Form\": {\n                \"amountLabel\": \"Importo\",\n                \"customAmountLabel\": \"Importo (tra {minAmount} e {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Seleziona un importo\",\n                \"customAmountPlaceholder\": \"Inserisci un importo personalizzato\",\n                \"senderNameLabel\": \"Il tuo nome\",\n                \"senderEmailLabel\": \"La tua e-mail\",\n                \"recipientNameLabel\": \"Nome del destinatario\",\n                \"recipientEmailLabel\": \"E-mail del destinatario\",\n                \"namePlaceholder\": \"Inserisci il nome\",\n                \"emailPlaceholder\": \"Inserisci e-mail\",\n                \"messageLabel\": \"Messaggio\",\n                \"messagePlaceholder\": \"Inserisci il tuo messaggio (facoltativo)\",\n                \"nonRefundableCheckboxLabel\": \"Accetto che i buoni regalo non sono rimborsabili\",\n                \"expiryCheckboxLabel\": \"Riconosco che questo buono regalo scadrà il {expiryDate}\",\n                \"ctaLabel\": \"Aggiungi al carrello\",\n                \"Errors\": {\n                    \"amountRequired\": \"Seleziona o inserisci un importo del buono regalo\",\n                    \"amountInvalid\": \"Seleziona un importo valido per il buono regalo\",\n                    \"amountOutOfRange\": \"Inserisci un importo compreso tra {minAmount} e {maxAmount}\",\n                    \"unexpectedSettingsError\": \"Si è verificato un errore imprevisto durante il recupero delle impostazioni del buono regalo. Riprova più tardi.\",\n                    \"senderNameRequired\": \"Il tuo nome è obbligatorio\",\n                    \"senderEmailRequired\": \"La tua e-mail è obbligatoria\",\n                    \"recipientNameRequired\": \"Il nome del destinatario è obbligatorio\",\n                    \"recipientEmailRequired\": \"L'e-mail del destinatario è obbligatoria\",\n                    \"emailInvalid\": \"Inserisci un indirizzo e-mail valido\",\n                    \"checkboxRequired\": \"Devi spuntare questa casella per continuare\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"facoltativo\",\n        \"recaptchaRequired\": \"Completa la verifica reCAPTCHA.\",\n        \"Errors\": {\n            \"invalidInput\": \"Controlla i dati inseriti e riprova\",\n            \"invalidFormat\": \"Il valore inserito non corrisponde al formato richiesto\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/ja.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"あらゆる場面にぴったりの新商品\",\n                \"description\": \"スタイル、機能性、インスピレーションをお届けするために厳選された最新商品をご覧ください。今すぐお買い物をして、お気に入りを見つけましょう。\",\n                \"alt\": \"ニュートラルな背景の前に積み重ねられたベージュ色のブロックの上に、5つの小さな鉢植えの植物が飾られており、ダーク グレーの鉢にはさまざまな緑色の植物が美しく植わっている。\",\n                \"cta\": \"今すぐ購入する\"\n            },\n            \"Slide02\": {\n                \"title\": \"最新情報を見る\",\n                \"description\": \"最新入荷の商品をチェックして、暮らしに新しいワクワクをプラスしましょう。\",\n                \"alt\": \"淡い陰影のあるベージュ色の背景を背に、リボン付きの編みかごに入った緑色のシダを取ろうと誰かが手を伸ばしている。\",\n                \"cta\": \"今すぐ購入する\"\n            },\n            \"Slide03\": {\n                \"title\": \"誰でもきっとお気に入りが見つかるはず\",\n                \"description\": \"当社のベストセラー商品に関する限定オファーをお見逃しなく。今すぐお買い物して、お気に入りの商品をお得にゲットしましょう。\",\n                \"alt\": \"滑らかな質感と自然なディテールを持つ、穴の開いた鮮やかな緑の葉のクローズアップ。\",\n                \"cta\": \"今すぐ購入する\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"注目のコレクション\",\n            \"description\": \"この特集コレクションで、当店のおすすめ商品をご覧ください。パーフェクトなギフトや、自分へのご褒美に！\",\n            \"cta\": \"もっと見る\",\n            \"emptyStateTitle\": \"商品が見つかりません\",\n            \"emptyStateSubtitle\": \"当社の製品カタログをぜひご覧ください。\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"新着商品\",\n            \"description\": \"新商品が到着しました。ストアの最新情報をチェックしてください。\",\n            \"cta\": \"すべてを見る\",\n            \"emptyStateTitle\": \"商品が見つかりません\",\n            \"emptyStateSubtitle\": \"当社の製品カタログをぜひご覧ください。\",\n            \"previousProducts\": \"前の商品\",\n            \"nextProducts\": \"次の商品\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"パスワード変更\",\n            \"newPassword\": \"新しいパスワード\",\n            \"confirmPassword\": \"パスワード確認\",\n            \"passwordUpdated\": \"パスワードが正常に更新されました！\",\n            \"somethingWentWrong\": \"何か問題が発生しました。後でもう一度やり直してください。\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"パスワードが必要です\",\n                \"passwordTooSmall\": \"パスワードは少なくとも {minLength, plural, =1 {1 character } other {# characters }}の長さである必要があります\",\n                \"passwordLowercaseRequired\": \"パスワードには少なくとも1つの小文字を含める必要があります\",\n                \"passwordUppercaseRequired\": \"パスワードには少なくとも1つの大文字を含める必要があります\",\n                \"passwordNumberRequired\": \"パスワードには少なくとも {minNumbers, plural, =1 {one number } other {#個の数字}}が含まれている必要があります\",\n                \"passwordSpecialCharacterRequired\": \"パスワードには少なくとも1つの特殊文字を含める必要があります\",\n                \"passwordsMustMatch\": \"パスワードが一致しません\",\n                \"confirmPasswordRequired\": \"パスワードを確認してください\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"ログイン\",\n            \"heading\": \"ログイン\",\n            \"forgotPassword\": \"パスワードをお忘れですか?\",\n            \"cta\": \"ログイン\",\n            \"email\": \"Eメール\",\n            \"password\": \"パスワード\",\n            \"invalidCredentials\": \"メールアドレスまたはパスワードが間違っています。もう一度サインインするか、パスワードをリセットしてください\",\n            \"somethingWentWrong\": \"何か問題が発生しました。後でもう一度やり直してください。\",\n            \"passwordResetRequired\": \"パスワードのリセットが必要です。パスワードをリセットするための手順については、メールを確認してください。\",\n            \"invalidToken\": \"ログインリンクが無効または期限切れです。もう一度ログインしてください。\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"メールアドレスは必須です\",\n                \"emailInvalid\": \"有効なメールアドレスを入力してください\",\n                \"passwordRequired\": \"パスワードが必要です\",\n                \"invalidInput\": \"入力内容を確認して、もう一度お試しください。\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"新規のお客様ですか？\",\n                \"accountBenefits\": \"アカウントを作成すると、次のことができるようになります:\",\n                \"fastCheckout\": \"スムーズにお支払い\",\n                \"multipleAddresses\": \"複数の配送先住所を保存する\",\n                \"ordersHistory\": \"注文履歴にアクセスする\",\n                \"ordersTracking\": \"新規注文の追跡\",\n                \"wishlists\": \"ウィッシュリストにアイテムを保存\",\n                \"cta\": \"アカウント作成\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"パスワードをお忘れですか\",\n                \"subtitle\": \"アカウントに関連付けられたメールアドレスを以下に入力してください。パスワードをリセットするための手順をお送りいたします。\",\n                \"confirmResetPassword\": \"メールアドレス {email} が当社のアカウントにリンクされている場合、パスワードリセットのメールが送信されました。受信トレイをご確認ください。見つからない場合は、スパムフォルダもご確認ください。\",\n                \"somethingWentWrong\": \"何か問題が発生しました。後でもう一度やり直してください。\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"メールアドレスは必須です\",\n                    \"emailInvalid\": \"有効なメールアドレスを入力してください\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"アカウントを登録\",\n            \"heading\": \"新しいアカウント\",\n            \"cta\": \"アカウント作成\",\n            \"somethingWentWrong\": \"何か問題が発生しました。後でもう一度やり直してください。\",\n            \"recaptchaRequired\": \"reCAPTCHA認証を完了してください。\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"名は必須です\",\n                \"lastNameRequired\": \"姓は必須です\",\n                \"emailRequired\": \"メールアドレスは必須です\",\n                \"emailInvalid\": \"有効なメールアドレスを入力してください\",\n                \"passwordRequired\": \"パスワードが必要です\",\n                \"passwordTooSmall\": \"パスワードは少なくとも {minLength, plural, =1 {1 character } other {# characters }}の長さである必要があります\",\n                \"passwordLowercaseRequired\": \"パスワードには少なくとも1つの小文字を含める必要があります\",\n                \"passwordUppercaseRequired\": \"パスワードには少なくとも1つの大文字を含める必要があります\",\n                \"passwordNumberRequired\": \"パスワードには少なくとも {minNumbers, plural, =1 {one number } other {#個の数字}}が含まれている必要があります\",\n                \"passwordSpecialCharacterRequired\": \"パスワードには少なくとも1つの特殊文字を含める必要があります\",\n                \"passwordsMustMatch\": \"パスワードが一致しません\",\n                \"addressLine1Required\": \"住所1行目は必須です\",\n                \"cityRequired\": \"市区町村が必要です\",\n                \"countryRequired\": \"国名コードが必要です\",\n                \"stateRequired\": \"都道府県が必要です\",\n                \"postalCodeRequired\": \"郵便番号は必要です\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"このブランドの商品はありません\",\n                \"subtitle\": \"別のフィルターをお試しください。\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"カテゴリー\",\n            \"Empty\": {\n                \"title\": \"このカテゴリーの商品はありません\",\n                \"subtitle\": \"別のフィルターをお試しください。\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"検索結果\",\n            \"searchResults\": \"検索結果表示\",\n            \"subCategories\": \"カテゴリー\",\n            \"Breadcrumbs\": {\n                \"home\": \"ホーム\",\n                \"search\": \"検索\"\n            },\n            \"Empty\": {\n                \"title\": \"申し訳ございません。「{term}」に該当する結果は見つかりませんでした。\",\n                \"subtitle\": \"別の検索をお試しください。\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"フィルター\",\n            \"resetFilters\": \"フィルターをリセット\",\n            \"Range\": {\n                \"apply\": \"適用\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"送料無料\",\n                \"isFeaturedLabel\": \"特集されています\",\n                \"inStockLabel\": \"在庫あり\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"並べ替え：\",\n            \"featuredItems\": \"おすすめ商品\",\n            \"bestSellingItems\": \"ベストセラー商品\",\n            \"newestItems\": \"最新商品\",\n            \"aToZ\": \"AからZ\",\n            \"zToA\": \"ZからA\",\n            \"byReview\": \"レビュー別\",\n            \"priceAscending\": \"価格：昇順\",\n            \"priceDescending\": \"価格：降順\",\n            \"relevance\": \"関連性\"\n        },\n        \"Compare\": {\n            \"compare\": \"価格比較\",\n            \"remove\": \"削除\",\n            \"maxCompareLimit\": \"比較対象の商品が最大数に達しました。新しい商品を追加するには、1つ商品を削除してください。\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"住所\",\n            \"logout\": \"ログアウト\",\n            \"orders\": \"注文\",\n            \"settings\": \"アカウント\",\n            \"wishlists\": \"ウィッシュリスト\"\n        },\n        \"Orders\": {\n            \"title\": \"注文\",\n            \"orderNumber\": \"注文 #\",\n            \"totalPrice\": \"合計\",\n            \"viewDetails\": \"詳細一覧\",\n            \"EmptyState\": {\n                \"title\": \"注文がありません\",\n                \"cta\": \"今すぐ購入する\"\n            },\n            \"Details\": {\n                \"title\": \"注文番号 {orderNumber}\",\n                \"shippingAddress\": \"配送先住所\",\n                \"shippingMethod\": \"配送方法\",\n                \"summaryTotal\": \"合計\",\n                \"destination\": \"目的地\",\n                \"destinationWithCount\": \"目的地 {number, number}／{total, number}\",\n                \"digitalDelivery\": \"{email}へのデジタル配信\",\n                \"subtotal\": \"小計\",\n                \"shipping\": \"配送\",\n                \"tax\": \"税金\",\n                \"orderSummary\": \"注文概要\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {支払い方法} other {支払い方法}}\",\n                \"paymentEndingInLabel\": \"終了\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"クレジットカード\",\n                    \"giftCertificate\": \"ギフト券\",\n                    \"storeCredit\": \"ストアクレジット\",\n                    \"other\": \"その他\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"住所\",\n            \"cta\": \"アドレスを追加\",\n            \"edit\": \"編集\",\n            \"delete\": \"削除する\",\n            \"cancel\": \"キャンセル\",\n            \"create\": \"作成\",\n            \"update\": \"アップデート\",\n            \"setDefault\": \"デフォルトとして設定\",\n            \"somethingWentWrong\": \"何か問題が発生しました。後でもう一度やり直してください。\",\n            \"EmptyState\": {\n                \"title\": \"アドレスがありません\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"名は必須です\",\n                \"lastNameRequired\": \"姓は必須です\",\n                \"addressLine1Required\": \"住所1行目は必須です\",\n                \"cityRequired\": \"市区町村が必要です\",\n                \"countryRequired\": \"国名コードが必要です\",\n                \"stateRequired\": \"都道府県が必要です\",\n                \"postalCodeRequired\": \"郵便番号は必要です\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"アカウント設定\",\n            \"changePassword\": \"パスワード変更\",\n            \"passwordUpdated\": \"パスワードが正常に更新されました！\",\n            \"settingsUpdated\": \"アカウント設定が正常に更新されました！\",\n            \"somethingWentWrong\": \"何か問題が発生しました。後でもう一度やり直してください。\",\n            \"currentPassword\": \"現在のパスワード\",\n            \"newPassword\": \"新しいパスワード\",\n            \"confirmPassword\": \"パスワード確認\",\n            \"cta\": \"アップデート\",\n            \"NewsletterSubscription\": {\n                \"title\": \"マーケティングの好み\",\n                \"label\": \"ニュースレターに登録してください。\",\n                \"marketingPreferencesUpdated\": \"マーケティング設定が正常に更新されました。\",\n                \"somethingWentWrong\": \"何か問題が発生しました。後でもう一度やり直してください。\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"名は必須です\",\n                \"firstNameTooSmall\": \"名は2文字以上でなければなりません\",\n                \"lastNameRequired\": \"姓は必須です\",\n                \"lastNameTooSmall\": \"姓は2文字以上でなければなりません\",\n                \"emailRequired\": \"メールアドレスは必須です\",\n                \"emailInvalid\": \"有効なメールアドレスを入力してください\",\n                \"currentPasswordRequired\": \"現在のパスワードは必須です\",\n                \"passwordRequired\": \"パスワードが必要です\",\n                \"passwordTooSmall\": \"パスワードは少なくとも {minLength, plural, =1 {1 character } other {# characters }}の長さである必要があります\",\n                \"passwordLowercaseRequired\": \"パスワードには少なくとも1つの小文字を含める必要があります\",\n                \"passwordUppercaseRequired\": \"パスワードには少なくとも1つの大文字を含める必要があります\",\n                \"passwordNumberRequired\": \"パスワードには少なくとも {minNumbers, plural, =1 {one number } other {#個の数字}}が含まれている必要があります\",\n                \"passwordSpecialCharacterRequired\": \"パスワードには少なくとも1つの特殊文字を含める必要があります\",\n                \"passwordsMustMatch\": \"パスワードが一致しません\",\n                \"confirmPasswordRequired\": \"パスワードを確認してください\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"ウィッシュリストアクション\",\n        \"title\": \"ウィッシュリスト\",\n        \"new\": \"新しいウィッシュリスト\",\n        \"items\": \"{count, plural, =1 {1 item} other {# items}}\",\n        \"viewWishlist\": \"リストを表示\",\n        \"noWishlists\": \"ウィッシュリストがありません\",\n        \"noWishlistsCallToAction\": \"ウィッシュリストを作成\",\n        \"emptyWishlist\": \"このウィッシュリストには商品を追加していません。\",\n        \"share\": \"共有\",\n        \"shareSuccess\": \"ウィッシュリストが正常に共有されました。\",\n        \"shareCopied\": \"ウィッシュリストの公開 URL がクリップボードにコピーされました。\",\n        \"shareDisabled\": \"欲しいものリストを共有するには、公開する必要があります。\",\n        \"makePublic\": \"公開する\",\n        \"makePrivate\": \"非公開にする\",\n        \"rename\": \"名前の変更\",\n        \"delete\": \"削除する\",\n        \"removeButtonTitle\": \"ウィッシュリストから商品を削除する\",\n        \"Visibility\": {\n            \"public\": \"公式\",\n            \"private\": \"プライベート\"\n        },\n        \"Modal\": {\n            \"cancel\": \"キャンセル\",\n            \"close\": \"閉じる\",\n            \"copy\": \"コピー\",\n            \"create\": \"作成\",\n            \"save\": \"保存\",\n            \"delete\": \"削除する\",\n            \"newTitle\": \"新しいウィッシュリストを作成\",\n            \"shareTitle\": \"{name}を共有\",\n            \"renameTitle\": \"{name}の名前を変更\",\n            \"deleteTitle\": \"{name}を削除\",\n            \"changeVisibilityPublicTitle\": \"{name} をしますか？\",\n            \"changeVisibilityPrivateTitle\": \"{name}を非公開にしますか？\",\n            \"makePublicContent\": \"<bold>{name}</bold>を公開してもよろしいですか？これにより、リンクを知っている他のユーザーがあなたのウィッシュリストを閲覧できるようになります。\",\n            \"makePrivateContent\": \"<bold>{name}</bold>を非公開にしてもよろしいですか？ウィッシュリストを他のユーザーと共有している場合、他のユーザーはそのリストを閲覧できなくなります。\",\n            \"deleteContent\": \"<bold>{name}</bold>を削除してもよろしいですか？この操作は元に戻せません。\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"名前\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"ウィッシュリスト名は空欄にできません。\",\n            \"updateFailed\": \"ウィッシュリストの更新に失敗しました。再度お試しください。\",\n            \"deleteFailed\": \"ウィッシュリストを削除できませんでした。再度お試しください。\",\n            \"removeProductFailed\": \"ウィッシュリストから商品を削除することができませんでした。再度お試しください。\",\n            \"unauthorized\": \"この操作を実行する権限がありません。サインインして、もう一度お試しください\",\n            \"unexpected\": \"予期しないエラーが発生しました。もう一度お試しください\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"ウィッシュリストが正常に作成されました。\",\n            \"updateSuccess\": \"ウィッシュリストが正常に更新されました。\",\n            \"deleteSuccess\": \"ウィッシュリストは正常に削除されました。\",\n            \"removeItemSuccess\": \"アイテムがウィッシュリストから削除されました。\"\n        },\n        \"Button\": {\n            \"label\": \"ウィッシュリストに追加\",\n            \"addToNewWishlist\": \"新しいウィッシュリストに追加\",\n            \"defaultWishlistName\": \"私のウィッシュリスト\",\n            \"addSuccessMessage\": \"商品がウィッシュリストに追加されました\",\n            \"removeSuccessMessage\": \"商品がウィッシュリストから削除されました\",\n            \"Errors\": {\n                \"addProductFailed\": \"ウィッシュリストから商品を追加できませんでした。もう一度お試しください。\",\n                \"removeProductFailed\": \"ウィッシュリストから商品を削除できませんでした。再度お試しください\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"公開ウィッシュリスト\",\n        \"defaultName\": \"公開ウィッシュリスト\",\n        \"emptyWishlist\": \"このウィッシュリストにはまだ商品がありません。\"\n    },\n    \"Blog\": {\n        \"title\": \"ブログ\",\n        \"home\": \"ホーム\",\n        \"Empty\": {\n            \"title\": \"ブログ記事が見つかりません\",\n            \"subtitle\": \"後ほど、さらにコンテンツをご確認ください\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"共有\",\n            \"email\": \"Eメール\",\n            \"print\": \"印刷\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"ショッピングカート\",\n        \"heading\": \"カート\",\n        \"proceedToCheckout\": \"支払いに進む\",\n        \"increment\": \"数量を増やす\",\n        \"decrement\": \"数量を減らす\",\n        \"removeItem\": \"アイテムを削除\",\n        \"cartCombined\": \"以前のカートに商品が保存されていたので、それを現在のカートに追加しました。\",\n        \"cartRestored\": \"別のデバイスでカートに商品が追加されていたため、中断されたところからお買い物を再開できるよう、こちらにカートの内容を復元しました。\",\n        \"cartUpdateInProgress\": \"カートの更新が進行中です。このページを離れてもよろしいですか?変更内容が失われる可能性があります。\",\n        \"originalPrice\": \"元の価格は{price}でした。\",\n        \"currentPrice\": \"現在の価格は{price}です。\",\n        \"quantityReadyToShip\": \"{quantity, number} 個、発送準備完了\",\n        \"quantityOnBackorder\": \"{quantity, number} はバックオーダーになります\",\n        \"partiallyAvailable\": \"{quantity, number} 個のみ在庫あり\",\n        \"CheckoutSummary\": {\n            \"title\": \"要約\",\n            \"subTotal\": \"小計\",\n            \"discounts\": \"割引\",\n            \"tax\": \"税金\",\n            \"total\": \"合計\",\n            \"CouponCode\": {\n                \"apply\": \"適用\",\n                \"couponCode\": \"クーポンコード\",\n                \"removeCouponCode\": \"クーポンコードを削除\",\n                \"invalidCouponCode\": \"有効なクーポンコードを入力してください。\",\n                \"cartNotFound\": \"カートの取得中にエラーが発生しました\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"配送\",\n                \"add\": \"追加\",\n                \"change\": \"変更する\",\n                \"cancel\": \"キャンセル\",\n                \"country\": \"国\",\n                \"city\": \"市区町村\",\n                \"state\": \"都道府県\",\n                \"postalCode\": \"郵便番号\",\n                \"updatedShippingOptions\": \"配送オプションを更新\",\n                \"viewShippingOptions\": \"配送オプションを表示\",\n                \"editAddress\": \"住所を編集\",\n                \"shippingOptions\": \"配送オプション\",\n                \"updateShipping\": \"配送情報を更新\",\n                \"addShipping\": \"配送を追加\",\n                \"cartNotFound\": \"カートの取得中にエラーが発生しました\",\n                \"noShippingOptions\": \"ご指定の住所で利用できる配送オプションはありません\",\n                \"countryRequired\": \"国名コードが必要です\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"ギフト券\",\n            \"giftCertificateCode\": \"ギフト券コード\",\n            \"removeGiftCertificate\": \"デモ ギフト証明書\",\n            \"apply\": \"適用\",\n            \"to\": \"送信先\",\n            \"message\": \"メッセージ\",\n            \"invalidGiftCertificate\": \"有効なギフト券コードを入力してください\",\n            \"cartNotFound\": \"カートの取得中にエラーが発生しました\"\n        },\n        \"Empty\": {\n            \"title\": \"お客様のカートは空です。\",\n            \"subtitle\": \"開始するには、商品をいくつか追加してください。\",\n            \"cta\": \"買い物を続ける\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"カートの取得中にエラーが発生しました\",\n            \"lineItemNotFound\": \"行項目が見つかりませんでした。\",\n            \"failedToUpdateQuantity\": \"数量の更新に失敗しました。\",\n            \"somethingWentWrong\": \"何か問題が発生しました。後でもう一度やり直してください。\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"商品を比較\",\n        \"addToCart\": \"カートに追加\",\n        \"next\": \"次の商品\",\n        \"previous\": \"前の商品\",\n        \"noProductsToCompare\": \"比較する商品はありません\",\n        \"sku\": \"SKU\",\n        \"weight\": \"重さ\",\n        \"description\": \"説明\",\n        \"noDescription\": \"説明はありません。\",\n        \"rating\": \"評価\",\n        \"noRatings\": \"レビューがありません。\",\n        \"otherDetails\": \"その他の詳細\",\n        \"noOtherDetails\": \"他に詳細はありません。\",\n        \"viewOptions\": \"表示オプション\",\n        \"successMessage\": \"{cartItems, plural, =1 {1個の商品} other {#個の商品}}が<cartLink>カート</cartLink>に追加されました\",\n        \"missingCart\": \"カートが見つかりません。しばらくしてから再度お試しください。\",\n        \"unknownError\": \"不明なエラーです。しばらくしてから再度お試しください。\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"数量\",\n            \"increaseQuantity\": \"数量を増やす\",\n            \"decreaseQuantity\": \"数量を減らす\",\n            \"emptySelectPlaceholder\": \"オプション選択\",\n            \"successMessage\": \"{cartItems, plural, =1 {1個の商品} other {#個の商品}}が<cartLink>カート</cartLink>に追加されました\",\n            \"missingCart\": \"カートが見つかりません。しばらくしてからもう一度お試しください！\",\n            \"unknownError\": \"不明なエラーです。しばらくしてから再度お試しください。\",\n            \"variantRequiredError\": \"この商品をカートに追加するには、オプションを選択する必要があります。\",\n            \"increaseNumber\": \"数を増やす\",\n            \"decreaseNumber\": \"数を減らす\",\n            \"thumbnail\": \"画像番号を表示\",\n            \"additionalInformation\": \"追加情報\",\n            \"currentStock\": \"{quantity, number} 個の在庫あり\",\n            \"backorderQuantity\": \"{quantity, number} はバックオーダーになります\",\n            \"loadingMoreImages\": \"さらに画像を読み込んでいます\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 枚の画像が読み込まれました} other {#枚の画像が読み込まれました}}\",\n            \"Submit\": {\n                \"addToCart\": \"カートに追加\",\n                \"outOfStock\": \"品切れ\",\n                \"preorder\": \"予約注文\",\n                \"unavailable\": \"利用不可\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"仕様\",\n                \"warranty\": \"保証\",\n                \"sku\": \"SKU\",\n                \"weight\": \"重さ\",\n                \"condition\": \"条件\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"関連商品\",\n            \"noRelatedProducts\": \"関連する商品は見つかりませんでした\",\n            \"browseCatalog\": \"当社の製品カタログをぜひご覧ください。\",\n            \"cta\": \"すべての商品\",\n            \"previousProducts\": \"前の商品\",\n            \"nextProducts\": \"次の商品\",\n            \"scrollbar\": \"関連商品のスクロールバー\"\n        },\n        \"Reviews\": {\n            \"title\": \"レビュー\",\n            \"empty\": \"この商品にはまだレビューがありません\",\n            \"previous\": \"過去のレビュー\",\n            \"next\": \"次のレビュー\",\n            \"Form\": {\n                \"button\": \"レビューを書く\",\n                \"title\": \"レビューを書く\",\n                \"submit\": \"提出\",\n                \"cancel\": \"キャンセル\",\n                \"ratingLabel\": \"評価\",\n                \"titleLabel\": \"タイトル\",\n                \"reviewLabel\": \"レビュー\",\n                \"nameLabel\": \"名前\",\n                \"emailLabel\": \"Eメール\",\n                \"successMessage\": \"レビューは正常に送信されました。\",\n                \"somethingWentWrong\": \"何か問題が発生しました。後でもう一度やり直してください。\",\n                \"recaptchaRequired\": \"reCAPTCHA認証を完了してください。\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"タイトルは必須です\",\n                    \"authorRequired\": \"商品名は必須です\",\n                    \"emailRequired\": \"メールアドレスは必須です\",\n                    \"emailInvalid\": \"有効なメールアドレスを入力してください\",\n                    \"textRequired\": \"レビューが必要です\",\n                    \"ratingRequired\": \"評価は必須です\",\n                    \"ratingTooSmall\": \"評価は1以上である必要があります\",\n                    \"ratingTooLarge\": \"評価は5以下でなければなりません\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"ホーム\"\n        },\n        \"ContactUs\": {\n            \"home\": \"ホーム\",\n            \"Form\": {\n                \"success\": \"お問い合わせありがとうございます。近日中に返信させていただきます。\",\n                \"successCta\": \"買い物を続ける\",\n                \"fullName\": \"氏名\",\n                \"companyName\": \"会社名\",\n                \"phone\": \"電話\",\n                \"orderNo\": \"注文番号\",\n                \"rma\": \"RMA 番号\",\n                \"email\": \"Eメール\",\n                \"comments\": \"コメント/質問\",\n                \"cta\": \"フォームを送信\",\n                \"somethingWentWrong\": \"何か問題が発生しました。後でもう一度やり直してください。\",\n                \"recaptchaRequired\": \"reCAPTCHA認証を完了してください。\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"メンテナンス\",\n        \"message\": \"メンテナンスのため停止中です\",\n        \"contactUs\": \"お問い合わせ先：\"\n    },\n    \"Error\": {\n        \"title\": \"サーバーエラーが発生しました！\",\n        \"subtitle\": \"しばらくしてから再度お試しください。\",\n        \"cta\": \"もう一度お試しください\"\n    },\n    \"NotFound\": {\n        \"title\": \"そのページは見つかりませんでした！\",\n        \"subtitle\": \"他のものを検索するか、ホームページに戻ってください。\",\n        \"featuredProducts\": \"おすすめ商品\",\n        \"search\": \"検索\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"ホーム\",\n            \"toggleNavigation\": \"トグルナビゲーション\",\n            \"Icons\": {\n                \"account\": \"プロフィール\",\n                \"cart\": \"ショッピングカート\",\n                \"search\": \"検索ポップアップを開く\",\n                \"giftCertificates\": \"ギフト券\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"通貨を切り替える\",\n                \"invalidCurrency\": \"無効な通貨\",\n                \"errorUpdatingCurrency\": \"カートの通貨を更新する際にエラーが発生しました。再度お試しください。\"\n            },\n            \"Search\": {\n                \"products\": \"商品\",\n                \"categories\": \"カテゴリー\",\n                \"brands\": \"ブランド\",\n                \"noSearchResultsTitle\": \"申し訳ございません。「{term}」に該当する結果は見つかりませんでした。\",\n                \"noSearchResultsSubtitle\": \"別の検索をお試しください。\",\n                \"somethingWentWrong\": \"問題が発生しました。 もう一度お試しください。\",\n                \"inputPlaceholder\": \"商品、カテゴリ、ブランドを検索...\",\n                \"submitLabel\": \"検索\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"ホーム\",\n            \"contactUs\": \"問い合わせる\",\n            \"socialMediaLinks\": \"ソーシャル·メディア·リンク\",\n            \"categories\": \"カテゴリー\",\n            \"brands\": \"ブランド\",\n            \"navigate\": \"ナビゲート\",\n            \"giftCertificates\": \"ギフト券\"\n        },\n        \"Subscribe\": {\n            \"title\": \"ニュースレターにご登録ください\",\n            \"placeholder\": \"メールアドレスを入力してください\",\n            \"description\": \"当店の最新ニュースやオファーをぜひチェックしてください。\",\n            \"subscribedToNewsletter\": \"ニュースレターを購読されました。\",\n            \"Errors\": {\n                \"emailRequired\": \"メールアドレスは必須です\",\n                \"invalidEmail\": \"有効なメールアドレスを入力してください\",\n                \"somethingWentWrong\": \"何か問題が発生しました。後でもう一度やり直してください。\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"すべて拒否\",\n                \"acceptAll\": \"すべて受け入れる\",\n                \"customize\": \"カスタム\",\n                \"save\": \"設定を保存\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"私たちはあなたのプライバシーを大切にしています\",\n                \"description\": \"このサイトでは、ブラウジング体験の向上、サイトトラフィックの分析、パーソナライズされたコンテンツの表示のために Cookie を使用しています。\",\n                \"privacyPolicy\": \"プライバシーポリシー\"\n            },\n            \"Dialog\": {\n                \"title\": \"プライバシー設定\",\n                \"description\": \"ここでプライバシー設定をカスタマイズします。許可する Cookie と追跡テクノロジーの種類を選択できます。\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"厳密に必要な\",\n                    \"description\": \"これらの Cookie は Web サイトが適切に機能するために不可欠であり、無効にすることはできません。\"\n                },\n                \"functionality\": {\n                    \"title\": \"機能性\",\n                    \"description\": \"これらの Cookie により、Web サイトの機能性とパーソナライズが向上します。\"\n                },\n                \"marketing\": {\n                    \"title\": \"マーケティング\",\n                    \"description\": \"これらの Cookie は、関連性の高い広告を配信し、その効果を追跡するために使用されます。\"\n                },\n                \"measurement\": {\n                    \"title\": \"分析\",\n                    \"description\": \"これらの Cookie は、訪問者が Web サイトとどのようにやり取りしているかを理解し、そのパフォーマンスを向上させるのに役立ちます。\"\n                },\n                \"experience\": {\n                    \"title\": \"環境\",\n                    \"description\": \"これらの Cookie は、より良いユーザー エクスペリエンスを提供し、新しい機能をテストするのに役立ちます。\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"元の価格は{price}でした。\",\n            \"currentPrice\": \"現在の価格は{price}です。\",\n            \"range\": \"価格は{minValue}から{maxValue}までです。\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"ギフト券\",\n        \"description\": \"流行遅れにならない完璧なギフトを贈りましょう。友人や大切な人に、当社の全コレクションから欲しいものを選んでもらいましょう。\",\n        \"purchaseLabel\": \"今すぐ購入する\",\n        \"checkBalanceLabel\": \"残高を確認する\",\n        \"expiresAtLabel\": \"有効期限\",\n        \"CheckBalance\": {\n            \"title\": \"残高を確認する\",\n            \"description\": \"下のボックスにコードを入力すると、残高を確認したり、ギフト券に関する情報を取得したりできます。\",\n            \"inputLabel\": \"コード\",\n            \"inputPlaceholder\": \"xxx-xxx-xxx-xxx\",\n            \"purchasedDateLabel\": \"購入済み\",\n            \"senderLabel\": \"から\",\n            \"Errors\": {\n                \"invalidCode\": \"入力したギフト券コードが無効です。コードを確認してもう一度お試しください。\",\n                \"codeRequired\": \"ギフト券コードを入力してください。\",\n                \"somethingWentWrong\": \"何か問題が発生しました。後でもう一度やり直してください。\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"ギフト券を購入する\",\n            \"title\": \"デジタルギフト券\",\n            \"description\": \"あらゆる機会に最適なギフト券をご覧ください。金額を選択し、メッセージをカスタマイズします。\",\n            \"successMessage\": \"ギフト券が追加されました<cartLink>カート</cartLink>\",\n            \"missingCart\": \"カートが見つかりません。しばらくしてから再度お試しください。\",\n            \"unknownError\": \"不明なエラーです。しばらくしてから再度お試しください。\",\n            \"Form\": {\n                \"amountLabel\": \"金額\",\n                \"customAmountLabel\": \"金額（ {minAmount}から{maxAmount}の間）\",\n                \"selectAmountPlaceholder\": \"金額を選択\",\n                \"customAmountPlaceholder\": \"カスタム金額を入力\",\n                \"senderNameLabel\": \"あなたの名前\",\n                \"senderEmailLabel\": \"あなたのメールアドレス\",\n                \"recipientNameLabel\": \"受取人の名前\",\n                \"recipientEmailLabel\": \"受信者のメールアドレス\",\n                \"namePlaceholder\": \"名前を入力\",\n                \"emailPlaceholder\": \"Eメールを入力してください\",\n                \"messageLabel\": \"メッセージ\",\n                \"messagePlaceholder\": \"メッセージを入力してください（オプション）\",\n                \"nonRefundableCheckboxLabel\": \"ギフト券は返金不可であることに同意します\",\n                \"expiryCheckboxLabel\": \"このギフト券は{expiryDate}に有効期限が切れることに同意します。\",\n                \"ctaLabel\": \"カートに追加\",\n                \"Errors\": {\n                    \"amountRequired\": \"ギフト券の金額を選択または入力してください\",\n                    \"amountInvalid\": \"有効なギフト券の金額を選択してください\",\n                    \"amountOutOfRange\": \"{minAmount}から{maxAmount}の間の金額を入力してください\",\n                    \"unexpectedSettingsError\": \"ギフト券設定の取得中に予期しないエラーが発生しました。しばらくしてから再度お試しください\",\n                    \"senderNameRequired\": \"お名前は必須です\",\n                    \"senderEmailRequired\": \"メールアドレスは必須です\",\n                    \"recipientNameRequired\": \"受取人の名前は必須です\",\n                    \"recipientEmailRequired\": \"受信者のメールアドレスは必須です\",\n                    \"emailInvalid\": \"有効なメールアドレスを入力してください\",\n                    \"checkboxRequired\": \"続行するにはこのボックスにチェックを入れてください\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"オプション\",\n        \"recaptchaRequired\": \"reCAPTCHA認証を完了してください。\",\n        \"Errors\": {\n            \"invalidInput\": \"入力内容を確認してもう一度お試しください\",\n            \"invalidFormat\": \"入力された値は必要な形式と一致しません\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/nl.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Verse vondsten voor elke gelegenheid\",\n                \"description\": \"Ontdek onze nieuwste aanwinsten, speciaal voor jou geselecteerd op basis van stijl en functionaliteit. Shop nu en ontdek je volgende favoriet.\",\n                \"alt\": \"Vijf kleine potplanten tentoongesteld op beige gestapelde blokken, met een verscheidenheid aan groen gebladerte in donkergrijze potten tegen een neutrale achtergrond.\",\n                \"cta\": \"Nu winkelen\"\n            },\n            \"Slide02\": {\n                \"title\": \"Ontdek nieuwe items\",\n                \"description\": \"Shop onze nieuwste aanwinsten en geef je huis een frisse look.\",\n                \"alt\": \"Handen die reiken naar een groene varen in een gevlochten mand met een decoratieve strik, tegen een beige achtergrond met zachte schaduwen.\",\n                \"cta\": \"Nu winkelen\"\n            },\n            \"Slide03\": {\n                \"title\": \"Iets voor iedereen\",\n                \"description\": \"Mis de exclusieve aanbiedingen op onze best verkochte producten niet. Shop vandaag nog en bespaar op je favoriete artikelen.\",\n                \"alt\": \"Close-up van een levendig groen blad met perforaties, dat de gladde textuur en natuurlijke details laat zien.\",\n                \"cta\": \"Nu winkelen\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Uitgelichte collectie\",\n            \"description\": \"Ontdek onze topkeuzes in deze uitgelichte collectie. Vind het perfecte cadeau of verwen jezelf!\",\n            \"cta\": \"Meer weergeven\",\n            \"emptyStateTitle\": \"Geen producten gevonden\",\n            \"emptyStateSubtitle\": \"Probeer onze volledige productcatalogus te bekijken.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Nieuwe artikelen\",\n            \"description\": \"Onze nieuwste producten vind je hier. Bekijk nieuwe items in de winkel.\",\n            \"cta\": \"Alles weergeven\",\n            \"emptyStateTitle\": \"Geen producten gevonden\",\n            \"emptyStateSubtitle\": \"Probeer onze volledige productcatalogus te bekijken.\",\n            \"previousProducts\": \"Vorige producten\",\n            \"nextProducts\": \"Volgende producten\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Wachtwoord wijzigen\",\n            \"newPassword\": \"Nieuw wachtwoord\",\n            \"confirmPassword\": \"Wachtwoord bevestigen\",\n            \"passwordUpdated\": \"Wachtwoord is succesvol bijgewerkt!\",\n            \"somethingWentWrong\": \"Er is iets fout gegaan. Probeer het later opnieuw.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Wachtwoord is vereist\",\n                \"passwordTooSmall\": \"Het wachtwoord moet minimaal {minLength, plural, =1 {1 teken} other {# tekens}} lang zijn\",\n                \"passwordLowercaseRequired\": \"Het wachtwoord moet minstens één kleine letter bevatten\",\n                \"passwordUppercaseRequired\": \"Het wachtwoord moet minimaal één hoofdletter bevatten\",\n                \"passwordNumberRequired\": \"Het wachtwoord moet minimaal {minNumbers, plural, =1 {één cijfer} other {# cijfers}} bevatten\",\n                \"passwordSpecialCharacterRequired\": \"Het wachtwoord moet minimaal één speciaal teken bevatten\",\n                \"passwordsMustMatch\": \"De wachtwoorden komen niet overeen\",\n                \"confirmPasswordRequired\": \"Bevestig je wachtwoord\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Inloggen\",\n            \"heading\": \"Inloggen\",\n            \"forgotPassword\": \"Wachtwoord vergeten?\",\n            \"cta\": \"Inloggen\",\n            \"email\": \"E-mailadres\",\n            \"password\": \"Wachtwoord\",\n            \"invalidCredentials\": \"Je e-mailadres of wachtwoord is niet correct. Probeer opnieuw in te loggen of stel je wachtwoord opnieuw in\",\n            \"somethingWentWrong\": \"Er is iets fout gegaan. Probeer het later opnieuw.\",\n            \"passwordResetRequired\": \"Wachtwoord opnieuw instellen is vereist. Controleer uw e-mail voor instructies om uw wachtwoord opnieuw in te stellen.\",\n            \"invalidToken\": \"Je aanmeldingslink is ongeldig of verlopen. Probeer opnieuw in te loggen.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"E-mailadres is vereist\",\n                \"emailInvalid\": \"Voer een geldig e-mailadres in\",\n                \"passwordRequired\": \"Wachtwoord is vereist\",\n                \"invalidInput\": \"Controleer uw invoer en probeer het opnieuw.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"Nieuwe klant?\",\n                \"accountBenefits\": \"Maak een account aan bij ons om:\",\n                \"fastCheckout\": \"Sneller af te rekenen\",\n                \"multipleAddresses\": \"Meerdere verzendadressen op te slaan\",\n                \"ordersHistory\": \"Uw bestelgeschiedenis te bekijken\",\n                \"ordersTracking\": \"Nieuwe bestellingen te volgen\",\n                \"wishlists\": \"Artikelen in uw verlanglijstje op te slaan\",\n                \"cta\": \"Account aanmaken\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Wachtwoord vergeten\",\n                \"subtitle\": \"Voer hieronder het e-mailadres in dat aan je account is gekoppeld. We sturen je instructies om je wachtwoord opnieuw in te stellen.\",\n                \"confirmResetPassword\": \"Als het e-mailadres {email} is gekoppeld aan een account in onze winkel, hebben we je een e-mail gestuurd om je wachtwoord opnieuw in te stellen. Controleer je inbox en spammap als je de e-mail niet ziet.\",\n                \"somethingWentWrong\": \"Er is iets fout gegaan. Probeer het later opnieuw.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"E-mailadres is vereist\",\n                    \"emailInvalid\": \"Voer een geldig e-mailadres in\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Account registreren\",\n            \"heading\": \"Nieuw account\",\n            \"cta\": \"Account aanmaken\",\n            \"somethingWentWrong\": \"Er is iets fout gegaan. Probeer het later opnieuw.\",\n            \"recaptchaRequired\": \"Voltooi de reCAPTCHA-verificatie.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Voornaam is vereist\",\n                \"lastNameRequired\": \"Achternaam is vereist\",\n                \"emailRequired\": \"E-mailadres is vereist\",\n                \"emailInvalid\": \"Voer een geldig e-mailadres in\",\n                \"passwordRequired\": \"Wachtwoord is vereist\",\n                \"passwordTooSmall\": \"Het wachtwoord moet minimaal {minLength, plural, =1 {1 teken} other {# tekens}} lang zijn\",\n                \"passwordLowercaseRequired\": \"Het wachtwoord moet minstens één kleine letter bevatten\",\n                \"passwordUppercaseRequired\": \"Het wachtwoord moet minimaal één hoofdletter bevatten\",\n                \"passwordNumberRequired\": \"Het wachtwoord moet minimaal {minNumbers, plural, =1 {één cijfer} other {# cijfers}} bevatten\",\n                \"passwordSpecialCharacterRequired\": \"Het wachtwoord moet minimaal één speciaal teken bevatten\",\n                \"passwordsMustMatch\": \"De wachtwoorden komen niet overeen\",\n                \"addressLine1Required\": \"Adresregel 1 is vereist\",\n                \"cityRequired\": \"Plaats is vereist\",\n                \"countryRequired\": \"Land is vereist\",\n                \"stateRequired\": \"Staat/provincie is vereist\",\n                \"postalCodeRequired\": \"Postcode is vereist\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"Geen producten in dit merk\",\n                \"subtitle\": \"Probeer andere filters te gebruiken.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Categorieën\",\n            \"Empty\": {\n                \"title\": \"Er zijn geen producten in deze categorie.\",\n                \"subtitle\": \"Probeer andere filters te gebruiken.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Zoekresultaten\",\n            \"searchResults\": \"Zoekresultaten voor\",\n            \"subCategories\": \"Categorieën\",\n            \"Breadcrumbs\": {\n                \"home\": \"Home\",\n                \"search\": \"Zoeken\"\n            },\n            \"Empty\": {\n                \"title\": \"Sorry, geen resultaten voor '{term}'.\",\n                \"subtitle\": \"Probeer een andere zoekopdracht\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filters\",\n            \"resetFilters\": \"Filters opnieuw instellen\",\n            \"Range\": {\n                \"apply\": \"Toepassen\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Gratis verzending\",\n                \"isFeaturedLabel\": \"Uitgelicht\",\n                \"inStockLabel\": \"Op voorraad\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Sorteren op:\",\n            \"featuredItems\": \"Uitgelichte items\",\n            \"bestSellingItems\": \"Bestverkopende items\",\n            \"newestItems\": \"Nieuwste items\",\n            \"aToZ\": \"A tot Z\",\n            \"zToA\": \"Z tot A\",\n            \"byReview\": \"Beoordeling\",\n            \"priceAscending\": \"Prijs: oplopend\",\n            \"priceDescending\": \"Prijs: aflopend\",\n            \"relevance\": \"Relevantie\"\n        },\n        \"Compare\": {\n            \"compare\": \"Vergelijken\",\n            \"remove\": \"Verwijderen\",\n            \"maxCompareLimit\": \"Je hebt het maximale aantal producten voor vergelijking bereikt. Verwijder een product om een nieuw toe te voegen.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Adressen\",\n            \"logout\": \"Uitloggen\",\n            \"orders\": \"Bestellingen\",\n            \"settings\": \"Account\",\n            \"wishlists\": \"Verlanglijstjes\"\n        },\n        \"Orders\": {\n            \"title\": \"Bestellingen\",\n            \"orderNumber\": \"Bestelnr.\",\n            \"totalPrice\": \"Totaal\",\n            \"viewDetails\": \"Details weergeven\",\n            \"EmptyState\": {\n                \"title\": \"U hebt geen bestellingen\",\n                \"cta\": \"Nu winkelen\"\n            },\n            \"Details\": {\n                \"title\": \"Bestelnummer {orderNumber}\",\n                \"shippingAddress\": \"Verzendadres\",\n                \"shippingMethod\": \"Verzendmethode\",\n                \"summaryTotal\": \"Totaal\",\n                \"destination\": \"Bestemming\",\n                \"destinationWithCount\": \"Bestemming {number, number}/{total, number}\",\n                \"digitalDelivery\": \"Digitale levering naar {email}\",\n                \"subtotal\": \"Subtotaal\",\n                \"shipping\": \"Verzending\",\n                \"tax\": \"Belasting\",\n                \"orderSummary\": \"Besteloverzicht\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Betaalmethode} other {Betaalmethoden}}\",\n                \"paymentEndingInLabel\": \"Eindigend op\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Creditcard\",\n                    \"giftCertificate\": \"Cadeaubon\",\n                    \"storeCredit\": \"Winkeltegoed\",\n                    \"other\": \"Overig\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Adressen\",\n            \"cta\": \"Voeg adres toe\",\n            \"edit\": \"Bewerken\",\n            \"delete\": \"Verwijderen\",\n            \"cancel\": \"Annuleren\",\n            \"create\": \"Aanmaken\",\n            \"update\": \"Bijwerken\",\n            \"setDefault\": \"Instellen als standaard\",\n            \"somethingWentWrong\": \"Er is iets fout gegaan. Probeer het later opnieuw.\",\n            \"EmptyState\": {\n                \"title\": \"Je hebt geen adressen\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Voornaam is vereist\",\n                \"lastNameRequired\": \"Achternaam is vereist\",\n                \"addressLine1Required\": \"Adresregel 1 is vereist\",\n                \"cityRequired\": \"Plaats is vereist\",\n                \"countryRequired\": \"Land is vereist\",\n                \"stateRequired\": \"Staat/provincie is vereist\",\n                \"postalCodeRequired\": \"Postcode is vereist\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"Accountinstellingen\",\n            \"changePassword\": \"Wachtwoord wijzigen\",\n            \"passwordUpdated\": \"Wachtwoord is succesvol bijgewerkt!\",\n            \"settingsUpdated\": \"Accountinstellingen zijn bijgewerkt!\",\n            \"somethingWentWrong\": \"Er is iets fout gegaan. Probeer het later opnieuw.\",\n            \"currentPassword\": \"Huidig wachtwoord\",\n            \"newPassword\": \"Nieuw wachtwoord\",\n            \"confirmPassword\": \"Wachtwoord bevestigen\",\n            \"cta\": \"Bijwerken\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Marketingvoorkeuren\",\n                \"label\": \"Meld u aan voor onze nieuwsbrief.\",\n                \"marketingPreferencesUpdated\": \"Marketingvoorkeuren zijn bijgewerkt!\",\n                \"somethingWentWrong\": \"Er is iets fout gegaan. Probeer het later opnieuw.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Voornaam is vereist\",\n                \"firstNameTooSmall\": \"De voornaam moet minimaal 2 tekens lang zijn\",\n                \"lastNameRequired\": \"Achternaam is vereist\",\n                \"lastNameTooSmall\": \"De achternaam moet minimaal 2 tekens lang zijn\",\n                \"emailRequired\": \"E-mailadres is vereist\",\n                \"emailInvalid\": \"Voer een geldig e-mailadres in\",\n                \"currentPasswordRequired\": \"Huidig wachtwoord is vereist\",\n                \"passwordRequired\": \"Wachtwoord is vereist\",\n                \"passwordTooSmall\": \"Het wachtwoord moet minimaal {minLength, plural, =1 {1 teken} other {# tekens}} lang zijn\",\n                \"passwordLowercaseRequired\": \"Het wachtwoord moet minstens één kleine letter bevatten\",\n                \"passwordUppercaseRequired\": \"Het wachtwoord moet minimaal één hoofdletter bevatten\",\n                \"passwordNumberRequired\": \"Het wachtwoord moet minimaal {minNumbers, plural, =1 {één cijfer} other {# cijfers}} bevatten\",\n                \"passwordSpecialCharacterRequired\": \"Het wachtwoord moet minimaal één speciaal teken bevatten\",\n                \"passwordsMustMatch\": \"De wachtwoorden komen niet overeen\",\n                \"confirmPasswordRequired\": \"Bevestig je wachtwoord\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Verlanglijstacties\",\n        \"title\": \"Verlanglijstjes\",\n        \"new\": \"Nieuw verlanglijstje\",\n        \"items\": \"{count, plural, =1 {1 item} other {# items}}\",\n        \"viewWishlist\": \"Lijstje bekijken\",\n        \"noWishlists\": \"Je hebt geen verlanglijstjes.\",\n        \"noWishlistsCallToAction\": \"Een verlanglijstje maken\",\n        \"emptyWishlist\": \"Je hebt nog geen producten aan dit verlanglijstje toegevoegd.\",\n        \"share\": \"Delen\",\n        \"shareSuccess\": \"Verlanglijstje is succesvol gedeeld.\",\n        \"shareCopied\": \"De openbare URL van het verlanglijstje is naar je klembord gekopieerd.\",\n        \"shareDisabled\": \"Je verlanglijstje moet openbaar zijn om het te kunnen delen.\",\n        \"makePublic\": \"Openbaar maken\",\n        \"makePrivate\": \"Privé maken\",\n        \"rename\": \"Naam wijzigen\",\n        \"delete\": \"Verwijderen\",\n        \"removeButtonTitle\": \"Product van verlanglijstje verwijderen\",\n        \"Visibility\": {\n            \"public\": \"Openbaar\",\n            \"private\": \"Privé\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Annuleren\",\n            \"close\": \"Sluiten\",\n            \"copy\": \"Kopiëren\",\n            \"create\": \"Aanmaken\",\n            \"save\": \"Opslaan\",\n            \"delete\": \"Verwijderen\",\n            \"newTitle\": \"Een nieuw verlanglijstje maken\",\n            \"shareTitle\": \"Deel {name}\",\n            \"renameTitle\": \"Naam van {name} wijzigen\",\n            \"deleteTitle\": \"{name} verwijderen?\",\n            \"changeVisibilityPublicTitle\": \"{name} openbaar maken?\",\n            \"changeVisibilityPrivateTitle\": \"{name} privé maken?\",\n            \"makePublicContent\": \"Weet je zeker dat je <bold>{name}</bold> openbaar wilt maken? Hierdoor kunnen anderen je verlanglijstje zien als ze de link hebben.\",\n            \"makePrivateContent\": \"Weet je zeker dat je <bold>{name}</bold> privé wilt maken? Als je je verlanglijstje met anderen hebt gedeeld, kunnen ze het dan niet meer zien.\",\n            \"deleteContent\": \"Weet u zeker dat u <bold>{name}</bold> wilt verwijderen? Dit kan niet ongedaan worden gemaakt.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Naam\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"De naam van het verlanglijstje mag niet leeg zijn.\",\n            \"updateFailed\": \"Het bijwerken van je verlanglijstje is mislukt. Probeer het opnieuw.\",\n            \"deleteFailed\": \"Het verwijderen van je verlanglijstje is mislukt. Probeer het opnieuw.\",\n            \"removeProductFailed\": \"Het is niet gelukt om het product van je verlanglijstje te verwijderen. Probeer het opnieuw.\",\n            \"unauthorized\": \"Je bent niet bevoegd om deze handeling uit te voeren. Log in en probeer het opnieuw\",\n            \"unexpected\": \"Er is een onverwachte fout opgetreden. Probeer het opnieuw\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"Verlanglijstje is succesvol aangemaakt.\",\n            \"updateSuccess\": \"Het verlanglijstje is bijgewerkt.\",\n            \"deleteSuccess\": \"Verlanglijstje is succesvol verwijderd.\",\n            \"removeItemSuccess\": \"Het artikel is verwijderd van je verlanglijstje.\"\n        },\n        \"Button\": {\n            \"label\": \"Toevoegen aan verlanglijstje\",\n            \"addToNewWishlist\": \"Aan nieuw verlanglijstje toevoegen\",\n            \"defaultWishlistName\": \"Mijn verlanglijstje\",\n            \"addSuccessMessage\": \"Product is toegevoegd aan je verlanglijstje\",\n            \"removeSuccessMessage\": \"Het product is verwijderd van je verlanglijstje\",\n            \"Errors\": {\n                \"addProductFailed\": \"Het toevoegen van het product van je verlanglijstje is mislukt.Probeer het opnieuw\",\n                \"removeProductFailed\": \"Het is niet gelukt om het product van je verlanglijstje te verwijderen. Probeer het opnieuw\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Openbare verlanglijst\",\n        \"defaultName\": \"Openbare verlanglijst\",\n        \"emptyWishlist\": \"Deze verlanglijst bevat nog geen producten.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Home\",\n        \"Empty\": {\n            \"title\": \"Geen blogberichten gevonden\",\n            \"subtitle\": \"Kom later terug voor meer content\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Delen\",\n            \"email\": \"E-mailadres\",\n            \"print\": \"Afdrukken\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Winkelmandje\",\n        \"heading\": \"Uw winkelmandje\",\n        \"proceedToCheckout\": \"Doorgaan naar betaalomgeving\",\n        \"increment\": \"Aantal verhogen\",\n        \"decrement\": \"Aantal verminderen\",\n        \"removeItem\": \"Item verwijderen\",\n        \"cartCombined\": \"We hebben gezien dat je artikelen in een eerder winkelmandje had opgeslagen, dus we hebben ze voor je aan je huidige winkelmandje toegevoegd.\",\n        \"cartRestored\": \"Je bent een winkelmandje begonnen op een ander apparaat en we hebben het hier hersteld, zodat je verder kunt gaan waar je was gebleven.\",\n        \"cartUpdateInProgress\": \"Je winkelwagentje wordt bijgewerkt. Weet je zeker dat je deze pagina wilt verlaten? Je wijzigingen kunnen verloren gaan.\",\n        \"originalPrice\": \"De oorspronkelijke prijs was {price}.\",\n        \"currentPrice\": \"De huidige prijs is {price}.\",\n        \"quantityReadyToShip\": \"{quantity, number} klaar voor verzending\",\n        \"quantityOnBackorder\": \"{quantity, number} staat in backorder\",\n        \"partiallyAvailable\": \"Slechts {quantity, number} beschikbaar\",\n        \"CheckoutSummary\": {\n            \"title\": \"Overzicht\",\n            \"subTotal\": \"Subtotaal\",\n            \"discounts\": \"Kortingen\",\n            \"tax\": \"Belasting\",\n            \"total\": \"Totaal\",\n            \"CouponCode\": {\n                \"apply\": \"Toepassen\",\n                \"couponCode\": \"Couponcode\",\n                \"removeCouponCode\": \"Couponcode verwijderen\",\n                \"invalidCouponCode\": \"Voer een geldige couponcode in\",\n                \"cartNotFound\": \"Er is een fout opgetreden bij het ophalen van je winkelwagen\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Verzending\",\n                \"add\": \"Toevoegen\",\n                \"change\": \"Wijzigen\",\n                \"cancel\": \"Annuleren\",\n                \"country\": \"Land\",\n                \"city\": \"Plaats\",\n                \"state\": \"Staat/provincie\",\n                \"postalCode\": \"Postcode\",\n                \"updatedShippingOptions\": \"Verzendopties bijwerken\",\n                \"viewShippingOptions\": \"Verzendopties bekijken\",\n                \"editAddress\": \"Adres bewerken\",\n                \"shippingOptions\": \"Verzendopties\",\n                \"updateShipping\": \"Verzending bijwerken\",\n                \"addShipping\": \"Verzending toevoegen\",\n                \"cartNotFound\": \"Er is een fout opgetreden bij het ophalen van je winkelwagen\",\n                \"noShippingOptions\": \"Er zijn geen verzendopties beschikbaar voor je adres\",\n                \"countryRequired\": \"Land is vereist\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Cadeaubon\",\n            \"giftCertificateCode\": \"Code cadeaubon\",\n            \"removeGiftCertificate\": \"Cadeaubon verwijderen\",\n            \"apply\": \"Toepassen\",\n            \"to\": \"Naar\",\n            \"message\": \"Bericht\",\n            \"invalidGiftCertificate\": \"Voer een geldige cadeauboncode in\",\n            \"cartNotFound\": \"Er is een fout opgetreden bij het ophalen van je winkelwagen\"\n        },\n        \"Empty\": {\n            \"title\": \"Uw winkelmandje is leeg.\",\n            \"subtitle\": \"Voeg producten toe om te beginnen.\",\n            \"cta\": \"Verdergaan met winkelen\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Er is een fout opgetreden bij het ophalen van je winkelwagen\",\n            \"lineItemNotFound\": \"Regelitem niet gevonden.\",\n            \"failedToUpdateQuantity\": \"Hoeveelheid bijwerken is mislukt.\",\n            \"somethingWentWrong\": \"Er is iets fout gegaan. Probeer het later opnieuw.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Producten vergelijken\",\n        \"addToCart\": \"Toevoegen aan winkelmandje\",\n        \"next\": \"Volgende producten\",\n        \"previous\": \"Vorige producten\",\n        \"noProductsToCompare\": \"Geen producten om te vergelijken\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Gewicht\",\n        \"description\": \"Description\",\n        \"noDescription\": \"Er is geen beschrijving beschikbaar.\",\n        \"rating\": \"Beoordeling\",\n        \"noRatings\": \"Er zijn geen beoordelingen.\",\n        \"otherDetails\": \"Overige details\",\n        \"noOtherDetails\": \"Er zijn geen verdere details.\",\n        \"viewOptions\": \"Opties weergeven\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 item} other {# items}} toegevoegd aan <cartLink> je winkelwagen</cartLink>\",\n        \"missingCart\": \"Winkelmandje niet gevonden. Probeer het later opnieuw.\",\n        \"unknownError\": \"Onbekende fout. Probeer het later opnieuw.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Aantal\",\n            \"increaseQuantity\": \"Aantal verhogen\",\n            \"decreaseQuantity\": \"Aantal verminderen\",\n            \"emptySelectPlaceholder\": \"Selecteer een optie\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 item} other {# items}} toegevoegd aan <cartLink> je winkelwagen</cartLink>\",\n            \"missingCart\": \"Winkelmandje niet gevonden. Probeer het later opnieuw.\",\n            \"unknownError\": \"Onbekende fout. Probeer het later opnieuw.\",\n            \"variantRequiredError\": \"Voor dit product moeten opties worden geselecteerd om het aan de winkelwagen toe te voegen.\",\n            \"increaseNumber\": \"Aantal verhogen\",\n            \"decreaseNumber\": \"Aantal verlagen\",\n            \"thumbnail\": \"Bekijk het nummer van de afbeelding\",\n            \"additionalInformation\": \"Aanvullende informatie\",\n            \"currentStock\": \"{quantity, number} op voorraad\",\n            \"backorderQuantity\": \"{hoeveelheid, aantal} staat in backorder\",\n            \"loadingMoreImages\": \"Meer afbeeldingen laden\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 afbeelding geladen} other {# afbeeldingen geladen}}\",\n            \"Submit\": {\n                \"addToCart\": \"Toevoegen aan winkelmandje\",\n                \"outOfStock\": \"Niet op voorraad\",\n                \"preorder\": \"Vooruitbestellen\",\n                \"unavailable\": \"Niet beschikbaar\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Specificaties\",\n                \"warranty\": \"Garantie\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Gewicht\",\n                \"condition\": \"Staat\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Gerelateerde producten\",\n            \"noRelatedProducts\": \"Geen gerelateerde producten gevonden.\",\n            \"browseCatalog\": \"Probeer onze volledige productcatalogus te bekijken.\",\n            \"cta\": \"Alles kopen\",\n            \"previousProducts\": \"Vorige producten\",\n            \"nextProducts\": \"Volgende producten\",\n            \"scrollbar\": \"Schuifbalk gerelateerde producten\"\n        },\n        \"Reviews\": {\n            \"title\": \"Beoordelingen\",\n            \"empty\": \"Er zijn geen beoordelingen toegevoegd voor dit product.\",\n            \"previous\": \"Vorige beoordelingen\",\n            \"next\": \"Volgende beoordelingen\",\n            \"Form\": {\n                \"button\": \"Schrijf een beoordeling\",\n                \"title\": \"Schrijf een beoordeling\",\n                \"submit\": \"Verzenden\",\n                \"cancel\": \"Annuleren\",\n                \"ratingLabel\": \"Beoordeling\",\n                \"titleLabel\": \"Titel\",\n                \"reviewLabel\": \"Beoordelen\",\n                \"nameLabel\": \"Naam\",\n                \"emailLabel\": \"E-mailadres\",\n                \"successMessage\": \"Je beoordeling is succesvol ingediend!\",\n                \"somethingWentWrong\": \"Er is iets fout gegaan. Probeer het later opnieuw.\",\n                \"recaptchaRequired\": \"Voltooi de reCAPTCHA-verificatie.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Titel is vereist\",\n                    \"authorRequired\": \"Naam is verplicht\",\n                    \"emailRequired\": \"E-mailadres is vereist\",\n                    \"emailInvalid\": \"Voer een geldig e-mailadres in\",\n                    \"textRequired\": \"Beoordeling is vereist\",\n                    \"ratingRequired\": \"Beoordeling is vereist\",\n                    \"ratingTooSmall\": \"De beoordeling moet minimaal 1 zijn\",\n                    \"ratingTooLarge\": \"De beoordeling moet maximaal 5 zijn\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Home\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Home\",\n            \"Form\": {\n                \"success\": \"Bedankt voor je bericht. We nemen snel contact met je op.\",\n                \"successCta\": \"Verdergaan met winkelen\",\n                \"fullName\": \"Volledige naam\",\n                \"companyName\": \"Bedrijfsnaam\",\n                \"phone\": \"Telefoon\",\n                \"orderNo\": \"Bestelnummer\",\n                \"rma\": \"RMA-nummer\",\n                \"email\": \"E-mailadres\",\n                \"comments\": \"Opmerkingen/vragen\",\n                \"cta\": \"Formulier indienen\",\n                \"somethingWentWrong\": \"Er is iets fout gegaan. Probeer het later opnieuw.\",\n                \"recaptchaRequired\": \"Voltooi de reCAPTCHA-verificatie.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"Onderhoud\",\n        \"message\": \"We zijn offline voor onderhoud\",\n        \"contactUs\": \"Je kunt contact met ons opnemen via:\"\n    },\n    \"Error\": {\n        \"title\": \"Er is een serverfout opgetreden!\",\n        \"subtitle\": \"Probeer het later opnieuw.\",\n        \"cta\": \"Probeer het opnieuw\"\n    },\n    \"NotFound\": {\n        \"title\": \"We hebben die pagina niet gevonden!\",\n        \"subtitle\": \"Probeer iets anders te zoeken of ga terug naar de homepage.\",\n        \"featuredProducts\": \"Uitgelichte producten\",\n        \"search\": \"Zoeken\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Home\",\n            \"toggleNavigation\": \"Navigatie in- of uitschakelen\",\n            \"Icons\": {\n                \"account\": \"Profiel\",\n                \"cart\": \"Winkelmandje\",\n                \"search\": \"Popup voor zoeken openen\",\n                \"giftCertificates\": \"Cadeaubonnen\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Valuta wisselen\",\n                \"invalidCurrency\": \"Ongeldige valuta\",\n                \"errorUpdatingCurrency\": \"Er is een fout opgetreden bij het bijwerken van de valuta voor je winkelwagen. Probeer het opnieuw.\"\n            },\n            \"Search\": {\n                \"products\": \"Producten\",\n                \"categories\": \"Categorieën\",\n                \"brands\": \"Merken\",\n                \"noSearchResultsTitle\": \"Sorry, geen resultaten voor '{term}'.\",\n                \"noSearchResultsSubtitle\": \"Probeer een andere zoekopdracht\",\n                \"somethingWentWrong\": \"Er is iets fout gegaan. Probeer het opnieuw.\",\n                \"inputPlaceholder\": \"Zoek producten, categorieën, merken...\",\n                \"submitLabel\": \"Zoeken\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Home\",\n            \"contactUs\": \"Contact\",\n            \"socialMediaLinks\": \"Links naar sociale media\",\n            \"categories\": \"Categorieën\",\n            \"brands\": \"Merken\",\n            \"navigate\": \"Navigeren\",\n            \"giftCertificates\": \"Cadeaubonnen\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Aanmelden voor onze nieuwsbrief\",\n            \"placeholder\": \"Voer uw e-mail in\",\n            \"description\": \"Blijf op de hoogte van het laatste nieuws en aanbiedingen van onze winkel.\",\n            \"subscribedToNewsletter\": \"Je bent nu geabonneerd op onze nieuwsbrief!\",\n            \"Errors\": {\n                \"emailRequired\": \"E-mailadres is vereist\",\n                \"invalidEmail\": \"Voer een geldig e-mailadres in\",\n                \"somethingWentWrong\": \"Er is iets fout gegaan. Probeer het later opnieuw.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Alles afwijzen\",\n                \"acceptAll\": \"Alles aanvaarden\",\n                \"customize\": \"Aanpassen\",\n                \"save\": \"Instellingen opslaan\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"We waarderen je privacy\",\n                \"description\": \"Deze site maakt gebruik van cookies om je browse-ervaring te verbeteren, siteverkeer te analyseren en gepersonaliseerde inhoud weer te geven.\",\n                \"privacyPolicy\": \"Privacybeleid\"\n            },\n            \"Dialog\": {\n                \"title\": \"Privacy-instellingen\",\n                \"description\": \"Pas je privacy-instellingen hier aan. Je kunt kiezen welke soorten cookies en trackingtechnologieën je wilt toestaan.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Strikt noodzakelijk\",\n                    \"description\": \"Deze cookies zijn essentieel voor het goed functioneren van de website en kunnen niet worden uitgeschakeld.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Functionaliteit\",\n                    \"description\": \"Deze cookies maken verbeterde functionaliteit en personalisatie van de website mogelijk.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"Deze cookies worden gebruikt om relevante advertenties te leveren en hun effectiviteit te volgen.\"\n                },\n                \"measurement\": {\n                    \"title\": \"Analyses\",\n                    \"description\": \"Deze cookies helpen ons te begrijpen hoe bezoekers omgaan met de website en verbeteren de prestaties ervan.\"\n                },\n                \"experience\": {\n                    \"title\": \"Ervaring\",\n                    \"description\": \"Deze cookies helpen ons een betere gebruikerservaring te bieden en nieuwe functies te testen.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"De oorspronkelijke prijs was {price}.\",\n            \"currentPrice\": \"De huidige prijs is {price}.\",\n            \"range\": \"Prijs van {minValue} tot {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Cadeaubonnen\",\n        \"description\": \"Geef het perfecte cadeau dat nooit uit de mode raakt. Laat vrienden en geliefden precies kiezen wat ze willen uit onze hele collectie.\",\n        \"purchaseLabel\": \"Nu winkelen\",\n        \"checkBalanceLabel\": \"Saldo controleren\",\n        \"expiresAtLabel\": \"Geldig tot\",\n        \"CheckBalance\": {\n            \"title\": \"Saldo controleren\",\n            \"description\": \"Je kunt het saldo controleren en de informatie over je cadeaubon opvragen door de code in het vak hieronder in te voeren.\",\n            \"inputLabel\": \"Code\",\n            \"inputPlaceholder\": \"XXX-XXX-XXX-XXX\",\n            \"purchasedDateLabel\": \"Gekocht\",\n            \"senderLabel\": \"Van\",\n            \"Errors\": {\n                \"invalidCode\": \"De cadeauboncode die u heeft ingevoerd is ongeldig. Controleer de code en probeer het opnieuw.\",\n                \"codeRequired\": \"Voer een cadeauboncode in.\",\n                \"somethingWentWrong\": \"Er is iets fout gegaan. Probeer het later opnieuw.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Een cadeaubon kopen\",\n            \"title\": \"Digitale cadeaubon\",\n            \"description\": \"Ontdek onze cadeaubonnen, perfect voor elke gelegenheid. Kies het bedrag en personaliseer je bericht.\",\n            \"successMessage\": \"Cadeaubon is toegevoegd aan <cartLink> je winkelmandje</cartLink>\",\n            \"missingCart\": \"Winkelmandje niet gevonden. Probeer het later opnieuw.\",\n            \"unknownError\": \"Onbekende fout. Probeer het later opnieuw.\",\n            \"Form\": {\n                \"amountLabel\": \"Bedrag\",\n                \"customAmountLabel\": \"Bedrag (tussen {minAmount} en {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Selecteer een bedrag\",\n                \"customAmountPlaceholder\": \"Voer een aangepast bedrag in\",\n                \"senderNameLabel\": \"Uw naam\",\n                \"senderEmailLabel\": \"Uw e-mail\",\n                \"recipientNameLabel\": \"Naam ontvanger\",\n                \"recipientEmailLabel\": \"E-mailadres ontvanger\",\n                \"namePlaceholder\": \"Voer een naam in\",\n                \"emailPlaceholder\": \"Voer het e-mailadres in\",\n                \"messageLabel\": \"Bericht\",\n                \"messagePlaceholder\": \"Voer je bericht in (optioneel)\",\n                \"nonRefundableCheckboxLabel\": \"Ik ga ermee akkoord dat cadeaubonnen niet terugbetaald kunnen worden\",\n                \"expiryCheckboxLabel\": \"Ik erken dat deze cadeaubon vervalt op {expiryDate}\",\n                \"ctaLabel\": \"Toevoegen aan winkelmandje\",\n                \"Errors\": {\n                    \"amountRequired\": \"Selecteer of voer een cadeaubonbedrag in\",\n                    \"amountInvalid\": \"Selecteer een geldig cadeaubonbedrag\",\n                    \"amountOutOfRange\": \"Voer een bedrag in tussen {minAmount} en {maxAmount}\",\n                    \"unexpectedSettingsError\": \"Er is een onverwachte fout opgetreden bij het ophalen van de instellingen voor de cadeaubon. Probeer het later opnieuw.\",\n                    \"senderNameRequired\": \"Uw naam is verplicht\",\n                    \"senderEmailRequired\": \"Uw e-mailadres is vereist\",\n                    \"recipientNameRequired\": \"De naam van de ontvanger is vereist\",\n                    \"recipientEmailRequired\": \"Het e-mailadres van de ontvanger is vereist\",\n                    \"emailInvalid\": \"Voer een geldig e-mailadres in\",\n                    \"checkboxRequired\": \"U moet dit vakje aanvinken om door te gaan\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"optioneel\",\n        \"recaptchaRequired\": \"Voltooi de reCAPTCHA-verificatie.\",\n        \"Errors\": {\n            \"invalidInput\": \"Controleer uw invoer en probeer het opnieuw\",\n            \"invalidFormat\": \"De ingevoerde waarde komt niet overeen met het vereiste formaat\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/no.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Friske funn for enhver anledning\",\n                \"description\": \"Utforsk de siste nyhetene våre, som er kuratert for å gi deg stil, funksjonalitet og inspirasjon. Handle nå og oppdag din neste favoritt.\",\n                \"alt\": \"Fem små potteplanter vist på beige stablede blokker, med et utvalg av grønt løvverk i mørkegrå potter mot en nøytral bakgrunn.\",\n                \"cta\": \"Kjøp nå\"\n            },\n            \"Slide02\": {\n                \"title\": \"Oppdag hva som er nytt\",\n                \"description\": \"Utforsk de nye varene og finn noe friskt og spennende til hjemmet ditt.\",\n                \"alt\": \"Hender som strekker seg ut for å holde en grønn bregne i en flettet kurv med en dekorativ sløyfe, mot en beige bakgrunn med myke skygger.\",\n                \"cta\": \"Kjøp nå\"\n            },\n            \"Slide03\": {\n                \"title\": \"Noe for enhver\",\n                \"description\": \"Ikke gå glipp av eksklusive tilbud på våre bestselgende produkter. Handle i dag og spar mye på varene du elsker.\",\n                \"alt\": \"Nærbilde av et livlig grønt blad med hull, som fremhever den glatte teksturen og de naturlige detaljene.\",\n                \"cta\": \"Kjøp nå\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Utvalgt samling\",\n            \"description\": \"Utforsk toppvalgene våre i denne utvalgte samlingen. Finn den perfekte gaven eller skjem bort deg selv!\",\n            \"cta\": \"Se mer\",\n            \"emptyStateTitle\": \"Ingen produkter funnet\",\n            \"emptyStateSubtitle\": \"Prøv å bla gjennom vår komplette produktkatalog.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Nytt\",\n            \"description\": \"Våre nyeste produkter er her. Sjekk ut hva som er nytt i butikken.\",\n            \"cta\": \"Se alt\",\n            \"emptyStateTitle\": \"Ingen produkter funnet\",\n            \"emptyStateSubtitle\": \"Prøv å bla gjennom vår komplette produktkatalog.\",\n            \"previousProducts\": \"Forrige produkter\",\n            \"nextProducts\": \"Neste produkter\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Endre passord\",\n            \"newPassword\": \"Nytt passord\",\n            \"confirmPassword\": \"Bekreft passord\",\n            \"passwordUpdated\": \"Passordet er oppdatert!\",\n            \"somethingWentWrong\": \"Noe gikk galt. Prøv igjen senere.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Passord er påkrevd\",\n                \"passwordTooSmall\": \"Passordet må være minst {minLength, plural, =1 {1 character} other {# tegn}} langt\",\n                \"passwordLowercaseRequired\": \"Passordet må inneholde minst én liten bokstav\",\n                \"passwordUppercaseRequired\": \"Passordet må inneholde minst én stor bokstav\",\n                \"passwordNumberRequired\": \"Passordet må inneholde minst  {minNumbers, plural, =1 {ett tall} other {# tall}}\",\n                \"passwordSpecialCharacterRequired\": \"Passordet må inneholde minst ett spesialtegn\",\n                \"passwordsMustMatch\": \"Passordene stemmer ikke overens\",\n                \"confirmPasswordRequired\": \"Bekreft passordet ditt\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Logg inn\",\n            \"heading\": \"Logg inn\",\n            \"forgotPassword\": \"Har du glemt passordet?\",\n            \"cta\": \"Logg inn\",\n            \"email\": \"E-post\",\n            \"password\": \"Passord\",\n            \"invalidCredentials\": \"E-postadressen eller passordet er feil. Prøv å logge på igjen, eller tilbakestill passordet ditt.\",\n            \"somethingWentWrong\": \"Noe gikk galt. Prøv igjen senere.\",\n            \"passwordResetRequired\": \"Krever tilbakestilling av passord. Kontroller e-posten din for instruksjoner om hvordan du tilbakestiller passordet.\",\n            \"invalidToken\": \"Innloggingskoblingen din er ugyldig eller har utløpt. Prøv å logge inn igjen.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"E-post er påkrevd\",\n                \"emailInvalid\": \"Skriv inn en gyldig e-postadresse\",\n                \"passwordRequired\": \"Passord er påkrevd\",\n                \"invalidInput\": \"Sjekk inndataene dine og prøv igjen.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"Ny kunde?\",\n                \"accountBenefits\": \"Opprett en konto hos oss, så kan du:\",\n                \"fastCheckout\": \"Betale raskere\",\n                \"multipleAddresses\": \"Lagre flere leveringsadresser\",\n                \"ordersHistory\": \"Få tilgang til bestillingshistorikken din\",\n                \"ordersTracking\": \"Spore nye bestillinger\",\n                \"wishlists\": \"Legge til varer på ønskelisten din\",\n                \"cta\": \"Opprett konto\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Glemt passord\",\n                \"subtitle\": \"Skriv inn e-posten som er knyttet til kontoen din nedenfor. Vi sender deg instruksjoner for å tilbakestille passordet ditt.\",\n                \"confirmResetPassword\": \"Hvis e-postadressen {email} er koblet til en konto i butikken vår, har vi sendt deg en e-post for tilbakestilling av passord. Sjekk innboksen og søppelpostmappen hvis du ikke ser den.\",\n                \"somethingWentWrong\": \"Noe gikk galt. Prøv igjen senere.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"E-post er påkrevd\",\n                    \"emailInvalid\": \"Skriv inn en gyldig e-postadresse\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Registrer deg for en konto\",\n            \"heading\": \"Ny konto\",\n            \"cta\": \"Opprett konto\",\n            \"somethingWentWrong\": \"Noe gikk galt. Prøv igjen senere.\",\n            \"recaptchaRequired\": \"Fullfør reCAPTCHA-verifiseringen.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Fornavn er påkrevd\",\n                \"lastNameRequired\": \"Etternavn er påkrevd\",\n                \"emailRequired\": \"E-post er påkrevd\",\n                \"emailInvalid\": \"Skriv inn en gyldig e-postadresse\",\n                \"passwordRequired\": \"Passord er påkrevd\",\n                \"passwordTooSmall\": \"Passordet må være minst {minLength, plural, =1 {1 character} other {# tegn}} langt\",\n                \"passwordLowercaseRequired\": \"Passordet må inneholde minst én liten bokstav\",\n                \"passwordUppercaseRequired\": \"Passordet må inneholde minst én stor bokstav\",\n                \"passwordNumberRequired\": \"Passordet må inneholde minst  {minNumbers, plural, =1 {ett tall} other {# tall}}\",\n                \"passwordSpecialCharacterRequired\": \"Passordet må inneholde minst ett spesialtegn\",\n                \"passwordsMustMatch\": \"Passordene stemmer ikke overens\",\n                \"addressLine1Required\": \"Adresselinje 1 er påkrevd\",\n                \"cityRequired\": \"By er påkrevd\",\n                \"countryRequired\": \"Land er påkrevd\",\n                \"stateRequired\": \"Delstat/provins er påkrevd\",\n                \"postalCodeRequired\": \"Postnummer er påkrevd\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"Ingen produkter i dette merket\",\n                \"subtitle\": \"Prøv å bruke forskjellige filtre.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Kategorier\",\n            \"Empty\": {\n                \"title\": \"Ingen produkter i denne kategorien\",\n                \"subtitle\": \"Prøv å bruke forskjellige filtre.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Søkeresultater\",\n            \"searchResults\": \"Søkeresultater for\",\n            \"subCategories\": \"Kategorier\",\n            \"Breadcrumbs\": {\n                \"home\": \"Hjem\",\n                \"search\": \"Søk\"\n            },\n            \"Empty\": {\n                \"title\": \"Beklager, ingen resultater for «{term}».\",\n                \"subtitle\": \"Prøv et nytt søk.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtre\",\n            \"resetFilters\": \"Tilbakestill filtre\",\n            \"Range\": {\n                \"apply\": \"Bruk\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Gratis frakt\",\n                \"isFeaturedLabel\": \"Er omtalt\",\n                \"inStockLabel\": \"På lager\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Sorter etter:\",\n            \"featuredItems\": \"Utvalgte varer\",\n            \"bestSellingItems\": \"Bestselgende varer\",\n            \"newestItems\": \"Nyeste varer\",\n            \"aToZ\": \"A til Å\",\n            \"zToA\": \"Å til A\",\n            \"byReview\": \"Etter anmeldelse\",\n            \"priceAscending\": \"Pris: Stigende\",\n            \"priceDescending\": \"Pris: Synkende\",\n            \"relevance\": \"Relevans\"\n        },\n        \"Compare\": {\n            \"compare\": \"Sammenlign\",\n            \"remove\": \"Fjern\",\n            \"maxCompareLimit\": \"Du har nådd det maksimale antallet produkter for sammenligning. Fjern et produkt for å legge til et nytt.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Adresser\",\n            \"logout\": \"Logg av\",\n            \"orders\": \"Bestillinger\",\n            \"settings\": \"Konto\",\n            \"wishlists\": \"Ønskelister\"\n        },\n        \"Orders\": {\n            \"title\": \"Bestillinger\",\n            \"orderNumber\": \"Bestillingsnr.\",\n            \"totalPrice\": \"Totalbeløp\",\n            \"viewDetails\": \"Vis detaljer\",\n            \"EmptyState\": {\n                \"title\": \"De har ingen bestillinger\",\n                \"cta\": \"Kjøp nå\"\n            },\n            \"Details\": {\n                \"title\": \"Bestillingsnummer {orderNumber}\",\n                \"shippingAddress\": \"Leveringsadresse\",\n                \"shippingMethod\": \"Forsendelsesmetode\",\n                \"summaryTotal\": \"Totalbeløp\",\n                \"destination\": \"Mål\",\n                \"destinationWithCount\": \"Destinasjon {number, number}/{total, number}\",\n                \"digitalDelivery\": \"Digital levering til {email}\",\n                \"subtotal\": \"Delsum\",\n                \"shipping\": \"Frakt\",\n                \"tax\": \"Avgift\",\n                \"orderSummary\": \"Bestillingssammendrag\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Betalingsmåte} other {Betalingsmåter}}\",\n                \"paymentEndingInLabel\": \"Slutter på\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Kredittkort\",\n                    \"giftCertificate\": \"Gavekort\",\n                    \"storeCredit\": \"Butikkreditt\",\n                    \"other\": \"Annet\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Adresser\",\n            \"cta\": \"Legg til adresse\",\n            \"edit\": \"Rediger\",\n            \"delete\": \"Slett\",\n            \"cancel\": \"Avbryt\",\n            \"create\": \"Opprett\",\n            \"update\": \"Oppdater\",\n            \"setDefault\": \"Angi som standard\",\n            \"somethingWentWrong\": \"Noe gikk galt. Prøv igjen senere.\",\n            \"EmptyState\": {\n                \"title\": \"Du har ingen adresser\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Fornavn er påkrevd\",\n                \"lastNameRequired\": \"Etternavn er påkrevd\",\n                \"addressLine1Required\": \"Adresselinje 1 er påkrevd\",\n                \"cityRequired\": \"By er påkrevd\",\n                \"countryRequired\": \"Land er påkrevd\",\n                \"stateRequired\": \"Delstat/provins er påkrevd\",\n                \"postalCodeRequired\": \"Postnummer er påkrevd\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"Kontoinnstillinger\",\n            \"changePassword\": \"Endre passord\",\n            \"passwordUpdated\": \"Passordet er oppdatert!\",\n            \"settingsUpdated\": \"Kontoinnstillingene har blitt oppdatert!\",\n            \"somethingWentWrong\": \"Noe gikk galt. Prøv igjen senere.\",\n            \"currentPassword\": \"Nåværende passord\",\n            \"newPassword\": \"Nytt passord\",\n            \"confirmPassword\": \"Bekreft passord\",\n            \"cta\": \"Oppdater\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Markedsføringsinnstillinger\",\n                \"label\": \"Abonner på nyhetsbrevet vårt.\",\n                \"marketingPreferencesUpdated\": \"Markedsføringsinnstillingene er oppdatert!\",\n                \"somethingWentWrong\": \"Noe gikk galt. Prøv igjen senere.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Fornavn er påkrevd\",\n                \"firstNameTooSmall\": \"Fornavnet må være minst 2 tegn langt\",\n                \"lastNameRequired\": \"Etternavn er påkrevd\",\n                \"lastNameTooSmall\": \"Etternavnet må være minst 2 tegn langt\",\n                \"emailRequired\": \"E-post er påkrevd\",\n                \"emailInvalid\": \"Skriv inn en gyldig e-postadresse\",\n                \"currentPasswordRequired\": \"Gjeldende passord kreves\",\n                \"passwordRequired\": \"Passord er påkrevd\",\n                \"passwordTooSmall\": \"Passordet må være minst {minLength, plural, =1 {1 character} other {# tegn}} langt\",\n                \"passwordLowercaseRequired\": \"Passordet må inneholde minst én liten bokstav\",\n                \"passwordUppercaseRequired\": \"Passordet må inneholde minst én stor bokstav\",\n                \"passwordNumberRequired\": \"Passordet må inneholde minst  {minNumbers, plural, =1 {ett tall} other {# tall}}\",\n                \"passwordSpecialCharacterRequired\": \"Passordet må inneholde minst ett spesialtegn\",\n                \"passwordsMustMatch\": \"Passordene stemmer ikke overens\",\n                \"confirmPasswordRequired\": \"Bekreft passordet ditt\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Ønskelistehandlinger\",\n        \"title\": \"Ønskelister\",\n        \"new\": \"Ny ønskeliste\",\n        \"items\": \"{count, plural, =1 {1 vare} other {# varer}}\",\n        \"viewWishlist\": \"Se listen\",\n        \"noWishlists\": \"Du har ingen ønskelister\",\n        \"noWishlistsCallToAction\": \"Opprett en ønskeliste\",\n        \"emptyWishlist\": \"Du har ikke lagt til produkter på denne ønskelisten.\",\n        \"share\": \"Del\",\n        \"shareSuccess\": \"Ønskelisten er delt.\",\n        \"shareCopied\": \"Den offentlige URL-en til ønskelisten er kopiert til utklippstavlen din.\",\n        \"shareDisabled\": \"Ønskelisten din må være offentlig for at du skal kunne dele den.\",\n        \"makePublic\": \"Gjøre offentlig\",\n        \"makePrivate\": \"Gjør privat\",\n        \"rename\": \"Gi nytt navn\",\n        \"delete\": \"Slett\",\n        \"removeButtonTitle\": \"Fjern produktet fra ønskelisten\",\n        \"Visibility\": {\n            \"public\": \"Offentlig\",\n            \"private\": \"Privat\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Avbryt\",\n            \"close\": \"Lukk\",\n            \"copy\": \"Kopier\",\n            \"create\": \"Opprett\",\n            \"save\": \"Lagre\",\n            \"delete\": \"Slett\",\n            \"newTitle\": \"Opprett en ny ønskeliste\",\n            \"shareTitle\": \"Del {name}\",\n            \"renameTitle\": \"Gi nytt navn til {name}\",\n            \"deleteTitle\": \"Slett {name}\",\n            \"changeVisibilityPublicTitle\": \"Vil du gjøre {name} offentlig?\",\n            \"changeVisibilityPrivateTitle\": \"Vil du gjøre {name} privat?\",\n            \"makePublicContent\": \"Er du sikker på at du vil gjøre <bold>{name}</bold> offentlig? Dette gjør at andre kan se ønskelisten din hvis de har lenken.\",\n            \"makePrivateContent\": \"Er du sikker på at du vil gjøre <bold>{name}</bold> privat? Hvis du har delt ønskelisten din med andre, vil de ikke lenger kunne se den.\",\n            \"deleteContent\": \"Er du sikker på at du vil slette <bold>{name}</bold>? Denne handlingen kan ikke angres.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Navn\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"Ønskelistenavn kan ikke være tomt.\",\n            \"updateFailed\": \"Kan ikke oppdatere ønskelisten. Prøv igjen.\",\n            \"deleteFailed\": \"Kan ikke slette ønskelisten din. Prøv igjen.\",\n            \"removeProductFailed\": \"Kan ikke fjerne produktet fra ønskelisten. Prøv igjen.\",\n            \"unauthorized\": \"Du er ikke autorisert til å utføre denne handlingen. Logg på og prøv igjen.\",\n            \"unexpected\": \"En uventet feil oppstod, prøv igjen\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"Ønskelisten er opprettet.\",\n            \"updateSuccess\": \"Ønskelisten er oppdatert.\",\n            \"deleteSuccess\": \"Ønskelisten er slettet.\",\n            \"removeItemSuccess\": \"Varen er fjernet fra ønskelisten din.\"\n        },\n        \"Button\": {\n            \"label\": \"Legg til på ønskeliste\",\n            \"addToNewWishlist\": \"Legg til i ny ønskeliste\",\n            \"defaultWishlistName\": \"Min ønskeliste\",\n            \"addSuccessMessage\": \"Produktet er lagt til i ønskelisten din\",\n            \"removeSuccessMessage\": \"Produktet er fjernet fra ønskelisten din\",\n            \"Errors\": {\n                \"addProductFailed\": \"Kan ikke legge til produktet fra ønskelisten din, prøv igjen.\",\n                \"removeProductFailed\": \"Kan ikke fjerne produktet fra ønskelisten, prøv igjen.\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Offentlig ønskeliste\",\n        \"defaultName\": \"Offentlig ønskeliste\",\n        \"emptyWishlist\": \"Denne ønskelisten har ingen produkter til nå\"\n    },\n    \"Blog\": {\n        \"title\": \"blogg\",\n        \"home\": \"Hjem\",\n        \"Empty\": {\n            \"title\": \"Ingen blogginnlegg ble funnet\",\n            \"subtitle\": \"Kom tilbake senere for mer innhold\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Del\",\n            \"email\": \"E-post\",\n            \"print\": \"Skriv ut\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Handlekurv\",\n        \"heading\": \"Din handlekurv\",\n        \"proceedToCheckout\": \"Fortsett til kassen\",\n        \"increment\": \"Øk antall\",\n        \"decrement\": \"Reduser antall\",\n        \"removeItem\": \"Fjern vare\",\n        \"cartCombined\": \"Vi la merke til at du hadde varer lagret i en tidligere handlekurv, så vi har lagt dem til i din nåværende handlekurv for deg.\",\n        \"cartRestored\": \"Du startet en handlekurv på en annen enhet, og vi har gjenopprettet den her, slik at du kan fortsette der du slapp.\",\n        \"cartUpdateInProgress\": \"Du har en handlekurvoppdatering i gang. Er du sikker på at du vil forlate denne siden? Endringene kan gå tapt.\",\n        \"originalPrice\": \"Opprinnelig pris var {price}.\",\n        \"currentPrice\": \"Nåværende pris er {price}.\",\n        \"quantityReadyToShip\": \"{antall, number} klar til sending\",\n        \"quantityOnBackorder\": \"{antall, nummer} vil være restordre\",\n        \"partiallyAvailable\": \"Kun {antall, number} tilgjengelig\",\n        \"CheckoutSummary\": {\n            \"title\": \"Sammendrag\",\n            \"subTotal\": \"Delsum\",\n            \"discounts\": \"Rabatter\",\n            \"tax\": \"Avgift\",\n            \"total\": \"Totalbeløp\",\n            \"CouponCode\": {\n                \"apply\": \"Bruk\",\n                \"couponCode\": \"Kupongkode\",\n                \"removeCouponCode\": \"Fjern kupongkode\",\n                \"invalidCouponCode\": \"Skriv inn en gyldig kupongkode\",\n                \"cartNotFound\": \"Det oppsto en feil da du hentet handlekurven din\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Frakt\",\n                \"add\": \"Legg til\",\n                \"change\": \"Endre\",\n                \"cancel\": \"Avbryt\",\n                \"country\": \"Land\",\n                \"city\": \"By\",\n                \"state\": \"Delstat/provins\",\n                \"postalCode\": \"Postnummer\",\n                \"updatedShippingOptions\": \"Oppdater forsendelsesalternativer\",\n                \"viewShippingOptions\": \"Vis forsendelsesalternativer\",\n                \"editAddress\": \"Rediger adresse\",\n                \"shippingOptions\": \"Fraktalternativer\",\n                \"updateShipping\": \"Oppdater forsendelse\",\n                \"addShipping\": \"Legg til forsendelse\",\n                \"cartNotFound\": \"Det oppsto en feil da du hentet handlekurven din\",\n                \"noShippingOptions\": \"Det finnes ingen tilgjengelige forsendelsesalternativer for adressen din\",\n                \"countryRequired\": \"Land er påkrevd\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Gavekort\",\n            \"giftCertificateCode\": \"Gavekortkode\",\n            \"removeGiftCertificate\": \"Fjern gavekortet\",\n            \"apply\": \"Bruk\",\n            \"to\": \"Til\",\n            \"message\": \"Melding\",\n            \"invalidGiftCertificate\": \"Skriv inn en gyldig gavekortkode\",\n            \"cartNotFound\": \"Det oppsto en feil da du hentet handlekurven din\"\n        },\n        \"Empty\": {\n            \"title\": \"Handlekurven din er tom.\",\n            \"subtitle\": \"Legg til noen produkter for å komme i gang.\",\n            \"cta\": \"Fortsett å handle\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Det oppsto en feil da du hentet handlekurven din\",\n            \"lineItemNotFound\": \"Linjeelement ikke funnet.\",\n            \"failedToUpdateQuantity\": \"Kan ikke oppdatere antall.\",\n            \"somethingWentWrong\": \"Noe gikk galt. Prøv igjen senere.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Sammenlign produkter\",\n        \"addToCart\": \"Legg i handlekurv\",\n        \"next\": \"Neste produkter\",\n        \"previous\": \"Forrige produkter\",\n        \"noProductsToCompare\": \"Ingen produkter å sammenligne\",\n        \"sku\": \"Sku\",\n        \"weight\": \"Vekt\",\n        \"description\": \"Beskrivelse\",\n        \"noDescription\": \"Det finnes ingen beskrivelse tilgjengelig.\",\n        \"rating\": \"Vurdering\",\n        \"noRatings\": \"Det er ingen anmeldelser.\",\n        \"otherDetails\": \"Andre detaljer\",\n        \"noOtherDetails\": \"Det er ingen andre detaljer.\",\n        \"viewOptions\": \"Vis alternativer\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 vare} other {# varer}} lagt til i <cartLink>handlekurven din</cartLink>\",\n        \"missingCart\": \"Handlekurv ikke funnet. Prøv på nytt senere.\",\n        \"unknownError\": \"Ukjent feil. Prøv på nytt senere.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Mengde\",\n            \"increaseQuantity\": \"Øk antall\",\n            \"decreaseQuantity\": \"Reduser antall\",\n            \"emptySelectPlaceholder\": \"Velg et alternativ\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 vare} other {# varer}} lagt til i <cartLink>handlekurven din</cartLink>\",\n            \"missingCart\": \"Handlekurv ikke funnet. Prøv på nytt senere!\",\n            \"unknownError\": \"Ukjent feil. Prøv på nytt senere.\",\n            \"variantRequiredError\": \"Dette produktet krever at alternativer velges for å kunne legge til i handlekurven.\",\n            \"increaseNumber\": \"Øk nummer\",\n            \"decreaseNumber\": \"Reduser tall\",\n            \"thumbnail\": \"Vis bildets nummer\",\n            \"additionalInformation\": \"Mer informasjon\",\n            \"currentStock\": \"{antall, nummer} på lager\",\n            \"backorderQuantity\": \"{antall, nummer} vil være restordre\",\n            \"loadingMoreImages\": \"Laster inn flere bilder\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 bilde til ble lastet inn} other {# flere bilder ble lastet inn}}\",\n            \"Submit\": {\n                \"addToCart\": \"Legg i handlekurv\",\n                \"outOfStock\": \"Utsolgt\",\n                \"preorder\": \"Forhåndsbestill\",\n                \"unavailable\": \"Utilgjengelig\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Spesifikasjoner\",\n                \"warranty\": \"Garanti\",\n                \"sku\": \"Sku\",\n                \"weight\": \"Vekt\",\n                \"condition\": \"Tilstand\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Relaterte produkter\",\n            \"noRelatedProducts\": \"Ingen relaterte produkter ble funnet\",\n            \"browseCatalog\": \"Prøv å bla gjennom vår komplette produktkatalog.\",\n            \"cta\": \"Kjøp alle\",\n            \"previousProducts\": \"Forrige produkter\",\n            \"nextProducts\": \"Neste produkter\",\n            \"scrollbar\": \"Relaterte produkter-rullefelt\"\n        },\n        \"Reviews\": {\n            \"title\": \"Anmeldelser\",\n            \"empty\": \"Ingen anmeldelser har blitt lagt til for dette produktet.\",\n            \"previous\": \"Tidligere anmeldelser\",\n            \"next\": \"Neste anmeldelser\",\n            \"Form\": {\n                \"button\": \"Skriv en anmeldelse\",\n                \"title\": \"Skriv en anmeldelse\",\n                \"submit\": \"Send inn\",\n                \"cancel\": \"Avbryt\",\n                \"ratingLabel\": \"Vurdering\",\n                \"titleLabel\": \"Tittel\",\n                \"reviewLabel\": \"Anmeldelse\",\n                \"nameLabel\": \"Navn\",\n                \"emailLabel\": \"E-post\",\n                \"successMessage\": \"Din anmeldelse er sendt inn!\",\n                \"somethingWentWrong\": \"Noe gikk galt. Prøv igjen senere.\",\n                \"recaptchaRequired\": \"Fullfør reCAPTCHA-verifiseringen.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Tittel er påkrevd\",\n                    \"authorRequired\": \"Navn er påkrevd\",\n                    \"emailRequired\": \"E-post er påkrevd\",\n                    \"emailInvalid\": \"Skriv inn en gyldig e-postadresse\",\n                    \"textRequired\": \"Gjennomgang er nødvendig\",\n                    \"ratingRequired\": \"Vurdering er påkrevd\",\n                    \"ratingTooSmall\": \"Vurderingen må være minst 1\",\n                    \"ratingTooLarge\": \"Vurderingen må være maksimalt 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Hjem\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Hjem\",\n            \"Form\": {\n                \"success\": \"Takk for at du tok kontakt. Vi kommer tilbake til deg snart.\",\n                \"successCta\": \"Fortsett å handle\",\n                \"fullName\": \"Fullt navn\",\n                \"companyName\": \"Firmanavn\",\n                \"phone\": \"Telefon\",\n                \"orderNo\": \"Bestillingsnummer\",\n                \"rma\": \"RMA-nummer\",\n                \"email\": \"E-post\",\n                \"comments\": \"Kommentarer/spørsmål\",\n                \"cta\": \"Send inn skjema\",\n                \"somethingWentWrong\": \"Noe gikk galt. Prøv igjen senere.\",\n                \"recaptchaRequired\": \"Fullfør reCAPTCHA-verifiseringen.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"Vedlikehold\",\n        \"message\": \"Vi er nede for vedlikehold\",\n        \"contactUs\": \"Du kan kontakte oss på:\"\n    },\n    \"Error\": {\n        \"title\": \"Det oppstod en serverfeil!\",\n        \"subtitle\": \"Prøv på nytt senere.\",\n        \"cta\": \"Prøv igjen\"\n    },\n    \"NotFound\": {\n        \"title\": \"Vi fant ikke den siden!\",\n        \"subtitle\": \"Prøv å søke etter noe annet, eller gå tilbake til hjemmesiden.\",\n        \"featuredProducts\": \"Utvalgt produkter\",\n        \"search\": \"Søk\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Hjem\",\n            \"toggleNavigation\": \"Veksle navigasjon\",\n            \"Icons\": {\n                \"account\": \"Profil\",\n                \"cart\": \"Handlekurv\",\n                \"search\": \"Åpne popup-vinduet for søk\",\n                \"giftCertificates\": \"Gavekort\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Bytt valuta\",\n                \"invalidCurrency\": \"Ugyldig valuta\",\n                \"errorUpdatingCurrency\": \"Det oppsto en feil ved oppdatering av valutaen for handlekurven din. Prøv igjen.\"\n            },\n            \"Search\": {\n                \"products\": \"Produkter\",\n                \"categories\": \"Kategorier\",\n                \"brands\": \"merker\",\n                \"noSearchResultsTitle\": \"Beklager, ingen resultater for «{term}».\",\n                \"noSearchResultsSubtitle\": \"Prøv et nytt søk.\",\n                \"somethingWentWrong\": \"Noe gikk galt. Prøv igjen.\",\n                \"inputPlaceholder\": \"Søk etter produkter, kategorier, merker ...\",\n                \"submitLabel\": \"Søk\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Hjem\",\n            \"contactUs\": \"kontakte oss\",\n            \"socialMediaLinks\": \"Lenker til sosiale medier\",\n            \"categories\": \"Kategorier\",\n            \"brands\": \"merker\",\n            \"navigate\": \"Naviger\",\n            \"giftCertificates\": \"Gavekort\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Meld deg på nyhetsbrevet vårt\",\n            \"placeholder\": \"Skriv inn epostadressen din\",\n            \"description\": \"Hold deg oppdatert med de siste nyhetene og tilbudene fra butikken vår.\",\n            \"subscribedToNewsletter\": \"Du har abonnert på nyhetsbrevet vårt.\",\n            \"Errors\": {\n                \"emailRequired\": \"E-post er påkrevd\",\n                \"invalidEmail\": \"Skriv inn en gyldig e-postadresse\",\n                \"somethingWentWrong\": \"Noe gikk galt. Prøv igjen senere.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Avvis alle\",\n                \"acceptAll\": \"Aksepter alle\",\n                \"customize\": \"Tilpass\",\n                \"save\": \"Lagre innstillinger\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"Vi verdsetter personvernet ditt\",\n                \"description\": \"Dette nettstedet bruker informasjonskapsler for å forbedre nettopplevelsen, analysere trafikken på nettstedet, og vise personlig tilpasset innhold.\",\n                \"privacyPolicy\": \"Personvernerklæring\"\n            },\n            \"Dialog\": {\n                \"title\": \"Personverninnstillinger\",\n                \"description\": \"Tilpass personverninnstillingene dine her. Du kan velge hvilke typer informasjonskapsler og sporingsteknologier du ønsker å tillate.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Strengt nødvendig\",\n                    \"description\": \"Disse informasjonskapslene er avgjørende for at nettsiden skal fungere skikkelig og kan ikke deaktiveres.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Funksjonalitet\",\n                    \"description\": \"Disse informasjonskapslene muliggjør forbedret funksjonalitet og personalisering av nettstedet.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Markedsføring\",\n                    \"description\": \"Disse informasjonskapslene brukes til å levere relevante annonser og spore effektiviteten deres.\"\n                },\n                \"measurement\": {\n                    \"title\": \"Analyse\",\n                    \"description\": \"Disse informasjonskapslene hjelper oss med å forstå hvordan besøkende samhandler med nettstedet og forbedre ytelsen.\"\n                },\n                \"experience\": {\n                    \"title\": \"Løsning\",\n                    \"description\": \"Disse informasjonskapslene hjelper oss med å gi en bedre brukeropplevelse og teste nye funksjoner.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"Opprinnelig pris var {price}.\",\n            \"currentPrice\": \"Nåværende pris er {price}.\",\n            \"range\": \"Pris fra {minValue} til {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Gavekort\",\n        \"description\": \"Gi den perfekte gaven som aldri går av moten. La venner og kjære velge akkurat det de ønsker fra hele kolleksjonen vår.\",\n        \"purchaseLabel\": \"Kjøp nå\",\n        \"checkBalanceLabel\": \"Sjekk saldo\",\n        \"expiresAtLabel\": \"Gyldig til og med\",\n        \"CheckBalance\": {\n            \"title\": \"Sjekk saldo\",\n            \"description\": \"Du kan sjekke saldoen og få informasjon om gavekortet ved å skrive inn koden i boksen nedenfor.\",\n            \"inputLabel\": \"Kode\",\n            \"inputPlaceholder\": \"XXX-XXX-XXX-XXX\",\n            \"purchasedDateLabel\": \"Kjøpt\",\n            \"senderLabel\": \"Fra\",\n            \"Errors\": {\n                \"invalidCode\": \"Gavekortkoden du skrev inn er ugyldig. Sjekk koden og prøv igjen.\",\n                \"codeRequired\": \"Vennligst skriv inn en gavekortkode.\",\n                \"somethingWentWrong\": \"Noe gikk galt. Prøv igjen senere.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Kjøp et gavekort\",\n            \"title\": \"Digitalt gavekort\",\n            \"description\": \"Utforsk gavekortene våre, perfekte for enhver anledning. Velg beløpet og tilpass meldingen.\",\n            \"successMessage\": \"Gavekort ble lagt til <cartLink> i handlekurven</cartLink>\",\n            \"missingCart\": \"Handlekurv ikke funnet. Prøv på nytt senere.\",\n            \"unknownError\": \"Ukjent feil. Prøv på nytt senere.\",\n            \"Form\": {\n                \"amountLabel\": \"Beløp\",\n                \"customAmountLabel\": \"Beløp (mellom {minAmount} og {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Velg et beløp\",\n                \"customAmountPlaceholder\": \"Skriv inn egendefinert beløp\",\n                \"senderNameLabel\": \"Navnet ditt\",\n                \"senderEmailLabel\": \"E-postadressen din\",\n                \"recipientNameLabel\": \"Mottakers navn\",\n                \"recipientEmailLabel\": \"Mottakers e-postadresse\",\n                \"namePlaceholder\": \"Skriv inn et navn\",\n                \"emailPlaceholder\": \"Skriv inn e-post\",\n                \"messageLabel\": \"Melding\",\n                \"messagePlaceholder\": \"Skriv inn melding (valgfritt)\",\n                \"nonRefundableCheckboxLabel\": \"Jeg godtar at gavekort ikke kan refunderes\",\n                \"expiryCheckboxLabel\": \"Jeg erkjenner at dette gavekortet utløper {expiryDate}\",\n                \"ctaLabel\": \"Legg i handlekurv\",\n                \"Errors\": {\n                    \"amountRequired\": \"Velg eller skriv inn et gavekortbeløp\",\n                    \"amountInvalid\": \"Velg et gyldig gavekortbeløp\",\n                    \"amountOutOfRange\": \"Skriv inn et beløp mellom {minAmount} og {maxAmount}\",\n                    \"unexpectedSettingsError\": \"Det oppstod en uventet feil under henting av innstillinger for gavekort. Prøv på nytt senere.\",\n                    \"senderNameRequired\": \"Ditt navn er påkrevd\",\n                    \"senderEmailRequired\": \"Din e-postadresse er påkrevd\",\n                    \"recipientNameRequired\": \"Mottakerens navn er obligatorisk\",\n                    \"recipientEmailRequired\": \"Mottakerens e-postadresse er obligatorisk\",\n                    \"emailInvalid\": \"Skriv inn en gyldig e-postadresse\",\n                    \"checkboxRequired\": \"Du må krysse av i denne boksen for å fortsette\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"valgfri\",\n        \"recaptchaRequired\": \"Fullfør reCAPTCHA-verifiseringen.\",\n        \"Errors\": {\n            \"invalidInput\": \"Sjekk inndataene dine og prøv igjen\",\n            \"invalidFormat\": \"Den angitte verdien samsvarer ikke med det nødvendige formatet\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/pl.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Świeże odkrycia na każdą okazję\",\n                \"description\": \"Odkryj nasze najnowsze propozycje, starannie wyselekcjonowane, by połączyć styl, funkcjonalność i inspirację. Zrób zakupy już teraz i znajdź swój kolejny ulubiony produkt.\",\n                \"alt\": \"Pięć małych roślin doniczkowych ułożonych na beżowych blokach, z różnorodnymi zielonymi liśćmi w ciemnoszarych doniczkach na neutralnym tle.\",\n                \"cta\": \"Kup teraz\"\n            },\n            \"Slide02\": {\n                \"title\": \"Odkryj, co nowego\",\n                \"description\": \"Zapoznaj się z naszą najnowszą ofertą i znajdź coś świeżego i ekscytującego do swojego domu.\",\n                \"alt\": \"Ręce sięgające po zieloną paproć w plecionym koszyku z ozdobną kokardą, na beżowym tle z delikatnymi cieniami.\",\n                \"cta\": \"Kup teraz\"\n            },\n            \"Slide03\": {\n                \"title\": \"Coś dla każdego\",\n                \"description\": \"Nie przegap ekskluzywnych ofert na nasze najlepiej sprzedające się produkty. Zacznij kupować już dziś i zaoszczędź na tym, co uwielbiasz.\",\n                \"alt\": \"Zbliżenie jaskrawozielonego liścia z otworami, ukazujące jego gładką fakturę i naturalne detale.\",\n                \"cta\": \"Kup teraz\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Polecana kolekcja\",\n            \"description\": \"Zapoznaj się z naszymi najlepszymi propozycjami w tej polecanej kolekcji. Znajdź idealny prezent dla bliskiej osoby lub spraw przyjemność sobie!\",\n            \"cta\": \"Zobacz więcej\",\n            \"emptyStateTitle\": \"Nie znaleziono produktów\",\n            \"emptyStateSubtitle\": \"Przejrzyj nasz pełny katalog produktów.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Nowe produkty\",\n            \"description\": \"Tutaj znajdziesz nasze najnowsze produkty. Sprawdź, co nowego w sklepie.\",\n            \"cta\": \"Zobacz wszystko\",\n            \"emptyStateTitle\": \"Nie znaleziono produktów\",\n            \"emptyStateSubtitle\": \"Przejrzyj nasz pełny katalog produktów.\",\n            \"previousProducts\": \"Poprzednie produkty\",\n            \"nextProducts\": \"Następne produkty\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Zmień hasło\",\n            \"newPassword\": \"Nowe hasło\",\n            \"confirmPassword\": \"Potwierdź hasło\",\n            \"passwordUpdated\": \"Hasło zostało pomyślnie zaktualizowane!\",\n            \"somethingWentWrong\": \"Coś poszło nie tak. Proszę spróbować później.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Hasło jest wymagane\",\n                \"passwordTooSmall\": \"Password must be at least {minLength, plural, =1 {1 character} other {# characters}} long\",\n                \"passwordLowercaseRequired\": \"Password must contain at least one lowercase letter\",\n                \"passwordUppercaseRequired\": \"Password must contain at least one uppercase letter\",\n                \"passwordNumberRequired\": \"Password must contain at least {minNumbers, plural, =1 {one number} other {# numbers}}\",\n                \"passwordSpecialCharacterRequired\": \"Password must contain at least one special character\",\n                \"passwordsMustMatch\": \"The passwords do not match\",\n                \"confirmPasswordRequired\": \"Please confirm your password\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Zaloguj się\",\n            \"heading\": \"Zaloguj się\",\n            \"forgotPassword\": \"Nie pamiętasz hasła?\",\n            \"cta\": \"Zaloguj się\",\n            \"email\": \"Email\",\n            \"password\": \"Hasło\",\n            \"invalidCredentials\": \"Twój adres e-mail lub hasło są nieprawidłowe. Spróbuj zalogować się ponownie lub zresetuj hasło\",\n            \"somethingWentWrong\": \"Coś poszło nie tak. Proszę spróbować później.\",\n            \"passwordResetRequired\": \"Password reset required. Please check your email for instructions to reset your password.\",\n            \"invalidToken\": \"Your login link is invalid or has expired. Please try logging in again.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"Email is required\",\n                \"emailInvalid\": \"Please enter a valid email address\",\n                \"passwordRequired\": \"Hasło jest wymagane\",\n                \"invalidInput\": \"Please check your input and try again.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"Nowy klient?\",\n                \"accountBenefits\": \"Załóż u nas konto, a będziesz mógł:\",\n                \"fastCheckout\": \"Do kasy szybciej\",\n                \"multipleAddresses\": \"Zapisz wiele adresów wysyłki\",\n                \"ordersHistory\": \"Dostęp do historii zamówień\",\n                \"ordersTracking\": \"Śledzenie nowych zamówień\",\n                \"wishlists\": \"Zapisz elementy na swojej liście życzeń\",\n                \"cta\": \"Utwórz konto\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Nie pamiętam hasła\",\n                \"subtitle\": \"Podaj poniżej adres e-mail powiązany z Twoim kontem. Wyślemy Ci instrukcje resetowania hasła.\",\n                \"confirmResetPassword\": \"Jeśli adres e-mail {email} jest powiązany z kontem w naszym sklepie, otrzymasz wiadomość e-mail umożliwiającą zresetowanie hasła. Jeśli jej nie widzisz, sprawdź skrzynkę odbiorczą i folder ze spamem.\",\n                \"somethingWentWrong\": \"Coś poszło nie tak. Proszę spróbować później.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"Email is required\",\n                    \"emailInvalid\": \"Please enter a valid email address\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Zarejestruj konto\",\n            \"heading\": \"Nowe konto\",\n            \"cta\": \"Utwórz konto\",\n            \"somethingWentWrong\": \"Coś poszło nie tak. Proszę spróbować później.\",\n            \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Imię jest wymagane\",\n                \"lastNameRequired\": \"Nazwisko jest wymagane\",\n                \"emailRequired\": \"Email is required\",\n                \"emailInvalid\": \"Please enter a valid email address\",\n                \"passwordRequired\": \"Hasło jest wymagane\",\n                \"passwordTooSmall\": \"Password must be at least {minLength, plural, =1 {1 character} other {# characters}} long\",\n                \"passwordLowercaseRequired\": \"Password must contain at least one lowercase letter\",\n                \"passwordUppercaseRequired\": \"Password must contain at least one uppercase letter\",\n                \"passwordNumberRequired\": \"Password must contain at least {minNumbers, plural, =1 {one number} other {# numbers}}\",\n                \"passwordSpecialCharacterRequired\": \"Password must contain at least one special character\",\n                \"passwordsMustMatch\": \"The passwords do not match\",\n                \"addressLine1Required\": \"Address line 1 is required\",\n                \"cityRequired\": \"Miasto jest wymagane\",\n                \"countryRequired\": \"Wymagany jest kraj\",\n                \"stateRequired\": \"Wymagany jest stan/prowincja\",\n                \"postalCodeRequired\": \"Kod pocztowy jest wymagany\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"Brak produktów tej marki\",\n                \"subtitle\": \"Spróbuj użyć różnych filtrów.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Kategorie\",\n            \"Empty\": {\n                \"title\": \"Brak produktów w tej kategorii\",\n                \"subtitle\": \"Spróbuj użyć różnych filtrów.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Wyniki wyszukiwania\",\n            \"searchResults\": \"Wyniki wyszukiwania dla\",\n            \"subCategories\": \"Kategorie\",\n            \"Breadcrumbs\": {\n                \"home\": \"Strona główna\",\n                \"search\": \"Szukaj na stronie\"\n            },\n            \"Empty\": {\n                \"title\": \"Brak wyników dla „{term}”.\",\n                \"subtitle\": \"Spróbuj innego wyszukiwania.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtry\",\n            \"resetFilters\": \"Zresetuj filtry\",\n            \"Range\": {\n                \"apply\": \"Zgłoś się na\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Darmowa wysyłka\",\n                \"isFeaturedLabel\": \"Jest promowany\",\n                \"inStockLabel\": \"Na stanie magazynowym\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Sortuj według:\",\n            \"featuredItems\": \"Polecane produkty\",\n            \"bestSellingItems\": \"Najlepiej sprzedające się przedmioty\",\n            \"newestItems\": \"Najnowsze produkty\",\n            \"aToZ\": \"Od A do Z\",\n            \"zToA\": \"Z do A\",\n            \"byReview\": \"Według oceny\",\n            \"priceAscending\": \"Cena: rosnąco\",\n            \"priceDescending\": \"Cena: malejąco\",\n            \"relevance\": \"Znaczenie\"\n        },\n        \"Compare\": {\n            \"compare\": \"Porównaj\",\n            \"remove\": \"Usuń\",\n            \"maxCompareLimit\": \"Osiągnięto maksymalną liczbę produktów do porównania. Usuń produkt, aby dodać nowy.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Adresy\",\n            \"logout\": \"Wylogowanie\",\n            \"orders\": \"Zamówienia\",\n            \"settings\": \"Konto\",\n            \"wishlists\": \"Listy życzeń\"\n        },\n        \"Orders\": {\n            \"title\": \"Zamówienia\",\n            \"orderNumber\": \"Zamówienie #\",\n            \"totalPrice\": \"Razem\",\n            \"viewDetails\": \"Zobacz szczegóły\",\n            \"EmptyState\": {\n                \"title\": \"Nie masz żadnych zamówień\",\n                \"cta\": \"Kup teraz\"\n            },\n            \"Details\": {\n                \"title\": \"Nr zamówienia {orderNumber}\",\n                \"shippingAddress\": \"Adres dostawy\",\n                \"shippingMethod\": \"Metoda wysyłki\",\n                \"summaryTotal\": \"Razem\",\n                \"destination\": \"Miejsce docelowe\",\n                \"destinationWithCount\": \"Miejsce docelowe {number, number}/{total, number}\",\n                \"digitalDelivery\": \"Digital delivery to {email}\",\n                \"subtotal\": \"Suma cząstkowa\",\n                \"shipping\": \"Wysyłka\",\n                \"tax\": \"Podatek\",\n                \"orderSummary\": \"Podsumowanie zamówienia\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Payment method} other {Payment methods}}\",\n                \"paymentEndingInLabel\": \"ending in\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Karta kredytowa\",\n                    \"giftCertificate\": \"Bon podarunkowy\",\n                    \"storeCredit\": \"Kredyt sklepowy\",\n                    \"other\": \"Inne\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Adresy\",\n            \"cta\": \"Dodaj adres\",\n            \"edit\": \"Edytuj\",\n            \"delete\": \"Usuń\",\n            \"cancel\": \"Anuluj\",\n            \"create\": \"Utwórz\",\n            \"update\": \"Aktualizacja\",\n            \"setDefault\": \"Ustaw jako domyślne\",\n            \"somethingWentWrong\": \"Coś poszło nie tak. Proszę spróbować później.\",\n            \"EmptyState\": {\n                \"title\": \"You don't have any addresses\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Imię jest wymagane\",\n                \"lastNameRequired\": \"Nazwisko jest wymagane\",\n                \"addressLine1Required\": \"Address line 1 is required\",\n                \"cityRequired\": \"Miasto jest wymagane\",\n                \"countryRequired\": \"Wymagany jest kraj\",\n                \"stateRequired\": \"Wymagany jest stan/prowincja\",\n                \"postalCodeRequired\": \"Kod pocztowy jest wymagany\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"Ustawienia konta\",\n            \"changePassword\": \"Zmień hasło\",\n            \"passwordUpdated\": \"Hasło zostało pomyślnie zaktualizowane!\",\n            \"settingsUpdated\": \"Ustawienia konta zostały pomyślnie zaktualizowane!\",\n            \"somethingWentWrong\": \"Coś poszło nie tak. Proszę spróbować później.\",\n            \"currentPassword\": \"Aktualne hasło\",\n            \"newPassword\": \"Nowe hasło\",\n            \"confirmPassword\": \"Potwierdź hasło\",\n            \"cta\": \"Aktualizacja\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Marketing preferences\",\n                \"label\": \"Zapisz się do naszego newslettera.\",\n                \"marketingPreferencesUpdated\": \"Marketing preferences have been updated successfully!\",\n                \"somethingWentWrong\": \"Coś poszło nie tak. Proszę spróbować później.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Imię jest wymagane\",\n                \"firstNameTooSmall\": \"First name must be at least 2 characters long\",\n                \"lastNameRequired\": \"Nazwisko jest wymagane\",\n                \"lastNameTooSmall\": \"Last name must be at least 2 characters long\",\n                \"emailRequired\": \"Email is required\",\n                \"emailInvalid\": \"Please enter a valid email address\",\n                \"currentPasswordRequired\": \"Wymagane jest aktualne hasło\",\n                \"passwordRequired\": \"Hasło jest wymagane\",\n                \"passwordTooSmall\": \"Password must be at least {minLength, plural, =1 {1 character} other {# characters}} long\",\n                \"passwordLowercaseRequired\": \"Password must contain at least one lowercase letter\",\n                \"passwordUppercaseRequired\": \"Password must contain at least one uppercase letter\",\n                \"passwordNumberRequired\": \"Password must contain at least {minNumbers, plural, =1 {one number} other {# numbers}}\",\n                \"passwordSpecialCharacterRequired\": \"Password must contain at least one special character\",\n                \"passwordsMustMatch\": \"The passwords do not match\",\n                \"confirmPasswordRequired\": \"Please confirm your password\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Działania na liście życzeń\",\n        \"title\": \"Wish Lists\",\n        \"new\": \"Nowa lista życzeń\",\n        \"items\": \"{count, plural, =1 {1 pozycja} other {# pozycji(-e)}}\",\n        \"viewWishlist\": \"Wyświetl listę\",\n        \"noWishlists\": \"Nie masz żadnych list życzeń\",\n        \"noWishlistsCallToAction\": \"Utwórz listę życzeń\",\n        \"emptyWishlist\": \"Nie dodano żadnych produktów do tej listy życzeń.\",\n        \"share\": \"Udostępnij\",\n        \"shareSuccess\": \"Lista życzeń została pomyślnie udostępniona.\",\n        \"shareCopied\": \"Skopiowano publiczny adres URL listy życzeń do schowka.\",\n        \"shareDisabled\": \"Lista życzeń musi być ustawiona jako publiczna, aby można było ją udostępniać.\",\n        \"makePublic\": \"Upublicznij\",\n        \"makePrivate\": \"Uczyń prywatnym\",\n        \"rename\": \"Zmień nazwę\",\n        \"delete\": \"Usuń\",\n        \"removeButtonTitle\": \"Remove product from wish list\",\n        \"Visibility\": {\n            \"public\": \"Publiczna\",\n            \"private\": \"Prywatna\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Anuluj\",\n            \"close\": \"Zamknij\",\n            \"copy\": \"Kopiuj\",\n            \"create\": \"Utwórz\",\n            \"save\": \"Zapisz\",\n            \"delete\": \"Usuń\",\n            \"newTitle\": \"Utwórz nową listę życzeń\",\n            \"shareTitle\": \"Udostępnij {name}\",\n            \"renameTitle\": \"Zmień nazwę {name}\",\n            \"deleteTitle\": \"Usuń lokalizację {name}\",\n            \"changeVisibilityPublicTitle\": \"Czy uczynić {name} publicznym?\",\n            \"changeVisibilityPrivateTitle\": \"Ustawić: {name} jako prywatne?\",\n            \"makePublicContent\": \"Czy na pewno chcesz ustawić <bold>{name}</bold> jako publiczną? Pozwoli to innym osobom zobaczyć Twoją listę życzeń, jeśli mają do niej link.\",\n            \"makePrivateContent\": \"Czy na pewno chcesz ustawić <bold>{name}</bold> jako prywatną? Jeśli lista życzeń została udostępniona innym osobom, utracą one możliwość wyświetlenia jej.\",\n            \"deleteContent\": \"Czy na pewno chcesz usunąć: <bold>{name}</bold>? Tego działania nie można cofnąć.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Nazwa\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"Nazwa listy życzeń nie może być pusta.\",\n            \"updateFailed\": \"Nie można zaktualizować listy życzeń. Spróbuj ponownie.\",\n            \"deleteFailed\": \"Nie można usunąć listy życzeń. Spróbuj ponownie.\",\n            \"removeProductFailed\": \"Nie można usunąć produktu z listy życzeń. Spróbuj ponownie.\",\n            \"unauthorized\": \"Nie masz uprawnień do wykonania tej czynności. Zaloguj się i spróbuj ponownie.\",\n            \"unexpected\": \"Wystąpił nieoczekiwany błąd. Spróbuj ponownie.\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"Lista życzeń została pomyślnie utworzona.\",\n            \"updateSuccess\": \"Lista życzeń została pomyślnie zaktualizowana.\",\n            \"deleteSuccess\": \"Lista życzeń została pomyślnie usunięta.\",\n            \"removeItemSuccess\": \"Produkt został usunięty z listy życzeń.\"\n        },\n        \"Button\": {\n            \"label\": \"Add to wish list\",\n            \"addToNewWishlist\": \"Dodaj do nowej listy życzeń\",\n            \"defaultWishlistName\": \"Moja lista życzeń\",\n            \"addSuccessMessage\": \"Produkt został dodany do Twojej listy życzeń.\",\n            \"removeSuccessMessage\": \"Produkt został usunięty z Twojej listy życzeń\",\n            \"Errors\": {\n                \"addProductFailed\": \"Nie można dodać produktu z listy życzeń. Spróbuj ponownie.\",\n                \"removeProductFailed\": \"Nie można usunąć produktu z listy życzeń. Spróbuj ponownie.\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Publiczna lista życzeń\",\n        \"defaultName\": \"Publiczna lista życzeń\",\n        \"emptyWishlist\": \"Na tej liście życzeń nie ma jeszcze żadnych produktów.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Strona główna\",\n        \"Empty\": {\n            \"title\": \"Nie znaleziono wpisów na blogu\",\n            \"subtitle\": \"Zajrzyj tu później, aby uzyskać więcej informacji\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Udostępnij\",\n            \"email\": \"Email\",\n            \"print\": \"Drukuj\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Koszyk\",\n        \"heading\": \"Twój koszyk\",\n        \"proceedToCheckout\": \"Przejdź do kasy\",\n        \"increment\": \"Zwiększ ilość\",\n        \"decrement\": \"Zmniejsz ilość\",\n        \"removeItem\": \"Usuń element\",\n        \"cartCombined\": \"Zauważyliśmy, że w poprzednim koszyku były zapisane produkty, więc dodaliśmy je do bieżącego koszyka.\",\n        \"cartRestored\": \"Dodano produkty do koszyka na innym urządzeniu. Przywróciliśmy koszyk tutaj, aby umożliwić kontynuowanie zakupów.\",\n        \"cartUpdateInProgress\": \"You have a cart update in progress. Are you sure you want to leave this page? Your changes may be lost.\",\n        \"originalPrice\": \"Original price was {price}.\",\n        \"currentPrice\": \"Current price is {price}.\",\n        \"quantityReadyToShip\": \"{quantity, number} ready to ship\",\n        \"quantityOnBackorder\": \"{quantity, number} will be backordered\",\n        \"partiallyAvailable\": \"Only {quantity, number} available\",\n        \"CheckoutSummary\": {\n            \"title\": \"Podsumowanie\",\n            \"subTotal\": \"Suma cząstkowa\",\n            \"discounts\": \"Rabaty\",\n            \"tax\": \"Podatek\",\n            \"total\": \"Razem\",\n            \"CouponCode\": {\n                \"apply\": \"Zgłoś się na\",\n                \"couponCode\": \"Kod kuponu\",\n                \"removeCouponCode\": \"Usuń kod kuponu\",\n                \"invalidCouponCode\": \"Wprowadź prawidłowy kod kuponu\",\n                \"cartNotFound\": \"An error occurred when retrieving your cart\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Wysyłka\",\n                \"add\": \"Dodaj\",\n                \"change\": \"Zmiana\",\n                \"cancel\": \"Anuluj\",\n                \"country\": \"Kraj\",\n                \"city\": \"Miasto\",\n                \"state\": \"Państwo/Prowincja\",\n                \"postalCode\": \"Kod pocztowy\",\n                \"updatedShippingOptions\": \"Zaktualizuj opcje wysyłki\",\n                \"viewShippingOptions\": \"Wyświetl opcje wysyłki\",\n                \"editAddress\": \"Edytuj adres\",\n                \"shippingOptions\": \"Opcje wysyłki\",\n                \"updateShipping\": \"Zaktualizuj wysyłkę\",\n                \"addShipping\": \"Dodaj wysyłkę\",\n                \"cartNotFound\": \"An error occurred when retrieving your cart\",\n                \"noShippingOptions\": \"Brak dostępnych opcji wysyłki dla Państwa adresu\",\n                \"countryRequired\": \"Wymagany jest kraj\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Bon podarunkowy\",\n            \"giftCertificateCode\": \"Kod bonu upominkowego\",\n            \"removeGiftCertificate\": \"Remove gift certificate\",\n            \"apply\": \"Zgłoś się na\",\n            \"to\": \"Do\",\n            \"message\": \"Wiadomość\",\n            \"invalidGiftCertificate\": \"Please enter a valid gift certificate code\",\n            \"cartNotFound\": \"An error occurred when retrieving your cart\"\n        },\n        \"Empty\": {\n            \"title\": \"Twój koszyk jest pusty.\",\n            \"subtitle\": \"Dodaj kilka produktów, aby rozpocząć.\",\n            \"cta\": \"Kontynuuj zakupy\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"An error occurred when retrieving your cart\",\n            \"lineItemNotFound\": \"Nie znaleziono pozycji.\",\n            \"failedToUpdateQuantity\": \"Nie udało się zaktualizować liczby sztuk.\",\n            \"somethingWentWrong\": \"Coś poszło nie tak. Proszę spróbować później.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Porównaj produkty\",\n        \"addToCart\": \"Dodaj do koszyka\",\n        \"next\": \"Następne produkty\",\n        \"previous\": \"Poprzednie produkty\",\n        \"noProductsToCompare\": \"Brak produktów do porównania\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Waga\",\n        \"description\": \"Opis\",\n        \"noDescription\": \"Brak dostępnego opisu.\",\n        \"rating\": \"Ocena\",\n        \"noRatings\": \"Brak opinii.\",\n        \"otherDetails\": \"Inne szczegóły\",\n        \"noOtherDetails\": \"Brak innych szczegółów.\",\n        \"viewOptions\": \"Wyświetl opcje\",\n        \"successMessage\": \"Dodano {cartItems, plural, =1 {1 produkt} other {# produkty(-ów)}} <cartLink>do koszyka</cartLink>\",\n        \"missingCart\": \"Nie znaleziono koszyka. Spróbuj ponownie później.\",\n        \"unknownError\": \"Nieznany błąd. Spróbuj ponownie później.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Ilość\",\n            \"increaseQuantity\": \"Zwiększ ilość\",\n            \"decreaseQuantity\": \"Zmniejsz ilość\",\n            \"emptySelectPlaceholder\": \"Wybierz opcję\",\n            \"successMessage\": \"Dodano {cartItems, plural, =1 {1 produkt} other {# produkty(-ów)}} <cartLink>do koszyka</cartLink>\",\n            \"missingCart\": \"Nie znaleziono koszyka. Spróbuj ponownie później.\",\n            \"unknownError\": \"Nieznany błąd. Spróbuj ponownie później.\",\n            \"variantRequiredError\": \"Ten produkt wymaga wybrania opcji, aby można go było dodać do koszyka.\",\n            \"increaseNumber\": \"Zwiększ liczbę\",\n            \"decreaseNumber\": \"Zmniejsz liczbę\",\n            \"thumbnail\": \"Wyświetl numer obrazu\",\n            \"additionalInformation\": \"Informacje dodatkowe\",\n            \"currentStock\": \"{quantity, number} in stock\",\n            \"backorderQuantity\": \"{quantity, number} will be on backorder\",\n            \"loadingMoreImages\": \"Loading more images\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 more image loaded} other {# more images loaded}}\",\n            \"Submit\": {\n                \"addToCart\": \"Dodaj do koszyka\",\n                \"outOfStock\": \"Brak w magazynie\",\n                \"preorder\": \"Zamówienie wstępne\",\n                \"unavailable\": \"Niedostępne\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Specyfikacje\",\n                \"warranty\": \"Gwarancja\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Waga\",\n                \"condition\": \"Stan\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Powiązane produkty\",\n            \"noRelatedProducts\": \"Nie znaleziono powiązanych produktów\",\n            \"browseCatalog\": \"Przejrzyj nasz pełny katalog produktów.\",\n            \"cta\": \"Przeglądaj wszystko\",\n            \"previousProducts\": \"Poprzednie produkty\",\n            \"nextProducts\": \"Następne produkty\",\n            \"scrollbar\": \"Pasek przewijania powiązanych produktów\"\n        },\n        \"Reviews\": {\n            \"title\": \"Opinie\",\n            \"empty\": \"Nie dodano żadnych opinii dla tego produktu.\",\n            \"previous\": \"Poprzednie recenzje\",\n            \"next\": \"Następne opinie\",\n            \"Form\": {\n                \"button\": \"Write a review\",\n                \"title\": \"Write a review\",\n                \"submit\": \"Wyślij\",\n                \"cancel\": \"Anuluj\",\n                \"ratingLabel\": \"Ocena\",\n                \"titleLabel\": \"Tytuł\",\n                \"reviewLabel\": \"Przegląd\",\n                \"nameLabel\": \"Nazwa\",\n                \"emailLabel\": \"Email\",\n                \"successMessage\": \"Your review has been submitted successfully!\",\n                \"somethingWentWrong\": \"Coś poszło nie tak. Proszę spróbować później.\",\n                \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Title is required\",\n                    \"authorRequired\": \"Nazwa jest wymagana\",\n                    \"emailRequired\": \"Email is required\",\n                    \"emailInvalid\": \"Please enter a valid email address\",\n                    \"textRequired\": \"Review is required\",\n                    \"ratingRequired\": \"Rating is required\",\n                    \"ratingTooSmall\": \"Rating must be at least 1\",\n                    \"ratingTooLarge\": \"Rating must be at most 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Strona główna\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Strona główna\",\n            \"Form\": {\n                \"success\": \"Dziękuję za kontakt. Skontaktujemy się z Tobą wkrótce.\",\n                \"successCta\": \"Kontynuuj zakupy\",\n                \"fullName\": \"Imię i nazwisko\",\n                \"companyName\": \"Nazwa firmy\",\n                \"phone\": \"Telefon\",\n                \"orderNo\": \"Numer zamówienia\",\n                \"rma\": \"Numer RMA\",\n                \"email\": \"Email\",\n                \"comments\": \"Uwagi/pytania\",\n                \"cta\": \"Prześlij formularz\",\n                \"somethingWentWrong\": \"Coś poszło nie tak. Proszę spróbować później.\",\n                \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"Konserwacja\",\n        \"message\": \"Przerwa z powodu konserwacji\",\n        \"contactUs\": \"Możesz skontaktować się z nami:\"\n    },\n    \"Error\": {\n        \"title\": \"Wystąpił błąd serwera!\",\n        \"subtitle\": \"Spróbuj ponownie później.\",\n        \"cta\": \"Spróbuj ponownie\"\n    },\n    \"NotFound\": {\n        \"title\": \"Nie znaleziono tej strony!\",\n        \"subtitle\": \"Spróbuj wyszukać coś innego lub wróć do strony głównej.\",\n        \"featuredProducts\": \"Polecane produkty\",\n        \"search\": \"Szukaj na stronie\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Strona główna\",\n            \"toggleNavigation\": \"Przełącz nawigację\",\n            \"Icons\": {\n                \"account\": \"Profil\",\n                \"cart\": \"Koszyk\",\n                \"search\": \"Otwórz wyskakujące okienko wyszukiwania\",\n                \"giftCertificates\": \"Bony upominkowe\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Switch currency\",\n                \"invalidCurrency\": \"Nieprawidłowa waluta\",\n                \"errorUpdatingCurrency\": \"Błąd aktualizacji waluty w koszyku. Spróbuj ponownie.\"\n            },\n            \"Search\": {\n                \"products\": \"Produkty\",\n                \"categories\": \"Kategorie\",\n                \"brands\": \"Marki\",\n                \"noSearchResultsTitle\": \"Brak wyników dla „{term}”.\",\n                \"noSearchResultsSubtitle\": \"Spróbuj innego wyszukiwania.\",\n                \"somethingWentWrong\": \"Coś poszło nie tak. Proszę spróbować ponownie.\",\n                \"inputPlaceholder\": \"Wyszukaj produkty, kategorie, marki...\",\n                \"submitLabel\": \"Szukaj na stronie\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Strona główna\",\n            \"contactUs\": \"Kontakt z nami\",\n            \"socialMediaLinks\": \"Linki do mediów społecznościowych\",\n            \"categories\": \"Kategorie\",\n            \"brands\": \"Marki\",\n            \"navigate\": \"Nawiguj po stronie\",\n            \"giftCertificates\": \"Bony upominkowe\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Zapisz się do naszego newslettera\",\n            \"placeholder\": \"Wpisz swój adres e-mail\",\n            \"description\": \"Bądź na bieżąco z najnowszymi informacjami i ofertami naszego sklepu.\",\n            \"subscribedToNewsletter\": \"You have been subscribed to our newsletter!\",\n            \"Errors\": {\n                \"emailRequired\": \"Email is required\",\n                \"invalidEmail\": \"Please enter a valid email address\",\n                \"somethingWentWrong\": \"Coś poszło nie tak. Proszę spróbować później.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Reject All\",\n                \"acceptAll\": \"Accept All\",\n                \"customize\": \"Dostosuj\",\n                \"save\": \"Zapisz ustawienia\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"We value your privacy\",\n                \"description\": \"This site uses cookies to improve your browsing experience, analyze site traffic, and show personalized content.\",\n                \"privacyPolicy\": \"Polityka prywatności\"\n            },\n            \"Dialog\": {\n                \"title\": \"Privacy Settings\",\n                \"description\": \"Customize your privacy settings here. You can choose which types of cookies and tracking technologies you would like to allow.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Strictly Necessary\",\n                    \"description\": \"These cookies are essential for the website to function properly and cannot be disabled.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Functionality\",\n                    \"description\": \"These cookies enable enhanced functionality and personalization of the website.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"These cookies are used to deliver relevant advertisements and track their effectiveness.\"\n                },\n                \"measurement\": {\n                    \"title\": \"Analityka\",\n                    \"description\": \"These cookies help us understand how visitors interact with the website and improve its performance.\"\n                },\n                \"experience\": {\n                    \"title\": \"Doświadczenie\",\n                    \"description\": \"These cookies help us provide a better user experience and test new features.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"Original price was {price}.\",\n            \"currentPrice\": \"Current price is {price}.\",\n            \"range\": \"Price from {minValue} to {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Bony upominkowe\",\n        \"description\": \"Give the perfect gift that never goes out of style. Let friends and loved ones choose exactly what they want from our entire collection.\",\n        \"purchaseLabel\": \"Kup teraz\",\n        \"checkBalanceLabel\": \"Check balance\",\n        \"expiresAtLabel\": \"Valid thru\",\n        \"CheckBalance\": {\n            \"title\": \"Check balance\",\n            \"description\": \"You can check the balance and get the information about your gift certificate by typing the code in the box below.\",\n            \"inputLabel\": \"Kod\",\n            \"inputPlaceholder\": \"xxx-xxx-xxx-xxx\",\n            \"purchasedDateLabel\": \"Zakupione\",\n            \"senderLabel\": \"Ze strony\",\n            \"Errors\": {\n                \"invalidCode\": \"The gift certificate code you entered is invalid. Please check the code and try again.\",\n                \"codeRequired\": \"Wpisz kod bonu upominkowego.\",\n                \"somethingWentWrong\": \"Coś poszło nie tak. Proszę spróbować później.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Purchase a gift certificate\",\n            \"title\": \"Digital gift certificate\",\n            \"description\": \"Explore our gift certificates, perfect for any occasion. Choose the amount and personalize your message.\",\n            \"successMessage\": \"Gift certificate has been added to <cartLink> your cart</cartLink>\",\n            \"missingCart\": \"Nie znaleziono koszyka. Spróbuj ponownie później.\",\n            \"unknownError\": \"Nieznany błąd. Spróbuj ponownie później.\",\n            \"Form\": {\n                \"amountLabel\": \"Kwota\",\n                \"customAmountLabel\": \"Amount (between {minAmount} and {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Select an amount\",\n                \"customAmountPlaceholder\": \"Enter custom amount\",\n                \"senderNameLabel\": \"Your name\",\n                \"senderEmailLabel\": \"Your email\",\n                \"recipientNameLabel\": \"Recipient's name\",\n                \"recipientEmailLabel\": \"Recipient's email\",\n                \"namePlaceholder\": \"Enter name\",\n                \"emailPlaceholder\": \"Wprowadź adres e-mail\",\n                \"messageLabel\": \"Wiadomość\",\n                \"messagePlaceholder\": \"Enter your message (optional)\",\n                \"nonRefundableCheckboxLabel\": \"I agree that Gift Certificates are non-refundable\",\n                \"expiryCheckboxLabel\": \"I acknowledge that this Gift Certificate will expire on {expiryDate}\",\n                \"ctaLabel\": \"Dodaj do koszyka\",\n                \"Errors\": {\n                    \"amountRequired\": \"Please select or enter a gift certificate amount\",\n                    \"amountInvalid\": \"Please select a valid gift certificate amount\",\n                    \"amountOutOfRange\": \"Please enter an amount between {minAmount} and {maxAmount}\",\n                    \"unexpectedSettingsError\": \"An unexpected error occurred while retrieving gift certificate settings. Please try again later.\",\n                    \"senderNameRequired\": \"Your name is required\",\n                    \"senderEmailRequired\": \"Your email is required\",\n                    \"recipientNameRequired\": \"Recipient's name is required\",\n                    \"recipientEmailRequired\": \"Recipient's email is required\",\n                    \"emailInvalid\": \"Please enter a valid email address\",\n                    \"checkboxRequired\": \"You must check this box to continue\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"opcjonalne\",\n        \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n        \"Errors\": {\n            \"invalidInput\": \"Please check your input and try again\",\n            \"invalidFormat\": \"The value entered does not match the required format\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/pt-BR.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Novidades para todas as ocasiões\",\n                \"description\": \"Explore nossas novidades, selecionadas para oferecer estilo, funcionalidade e inspiração. Compre agora e conheça o seu próximo favorito.\",\n                \"alt\": \"Cinco plantas pequenas em vasos exibidas em blocos beges empilhados, com uma variedade de folhagens verdes em vasos cinza-escuro em um fundo neutro.\",\n                \"cta\": \"Comprar agora\"\n            },\n            \"Slide02\": {\n                \"title\": \"Descubra o que há de novo\",\n                \"description\": \"Confira nossas últimas novidades e encontre algo novo e empolgante para a sua casa.\",\n                \"alt\": \"Mãos estendidas para pegar uma samambaia verde em uma cesta de vime com um laço decorativo sobre um fundo bege com sombras suaves.\",\n                \"cta\": \"Comprar agora\"\n            },\n            \"Slide03\": {\n                \"title\": \"Algo para todos\",\n                \"description\": \"Não perca ofertas exclusivas de nossos produtos mais vendidos. Compre hoje seus itens preferidos e economize muito.\",\n                \"alt\": \"Close de uma folha verde vibrante com perfurações, destacando sua textura suave e detalhes naturais.\",\n                \"cta\": \"Comprar agora\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Coleção em destaque\",\n            \"description\": \"Explore nossas principais escolhas nesta coleção em destaque. Encontre o presente perfeito para você ou para aquela pessoa especial!\",\n            \"cta\": \"Ver mais\",\n            \"emptyStateTitle\": \"Nenhum produto encontrado\",\n            \"emptyStateSubtitle\": \"Experimente conferir nosso catálogo completo de produtos.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Recém-chegados\",\n            \"description\": \"Nossos produtos mais recentes estão aqui. Confira as novidades na loja.\",\n            \"cta\": \"Ver tudo\",\n            \"emptyStateTitle\": \"Nenhum produto encontrado\",\n            \"emptyStateSubtitle\": \"Experimente conferir nosso catálogo completo de produtos.\",\n            \"previousProducts\": \"Produtos anteriores\",\n            \"nextProducts\": \"Próximos produtos\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Alterar senha\",\n            \"newPassword\": \"Nova senha\",\n            \"confirmPassword\": \"Confirmar senha\",\n            \"passwordUpdated\": \"A senha foi atualizada com sucesso!\",\n            \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"A senha é obrigatória\",\n                \"passwordTooSmall\": \"Password must be at least {minLength, plural, =1 {1 character} other {# characters}} long\",\n                \"passwordLowercaseRequired\": \"Password must contain at least one lowercase letter\",\n                \"passwordUppercaseRequired\": \"Password must contain at least one uppercase letter\",\n                \"passwordNumberRequired\": \"Password must contain at least {minNumbers, plural, =1 {one number} other {# numbers}}\",\n                \"passwordSpecialCharacterRequired\": \"Password must contain at least one special character\",\n                \"passwordsMustMatch\": \"The passwords do not match\",\n                \"confirmPasswordRequired\": \"Please confirm your password\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Acesso\",\n            \"heading\": \"Credencial de acesso\",\n            \"forgotPassword\": \"Esqueceu sua senha?\",\n            \"cta\": \"Credencial de acesso\",\n            \"email\": \"Email\",\n            \"password\": \"Senha\",\n            \"invalidCredentials\": \"O endereço de email ou a senha está incorreto. Tente fazer login novamente ou redefinir sua senha\",\n            \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n            \"passwordResetRequired\": \"Password reset required. Please check your email for instructions to reset your password.\",\n            \"invalidToken\": \"Your login link is invalid or has expired. Please try logging in again.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"Email is required\",\n                \"emailInvalid\": \"Please enter a valid email address\",\n                \"passwordRequired\": \"A senha é obrigatória\",\n                \"invalidInput\": \"Please check your input and try again.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"Cliente novo?\",\n                \"accountBenefits\": \"Crie uma conta conosco para poder:\",\n                \"fastCheckout\": \"Finalizar as compras mais rápido\",\n                \"multipleAddresses\": \"Salvar vários endereços de entrega\",\n                \"ordersHistory\": \"Acessar seu histórico de pedidos\",\n                \"ordersTracking\": \"Rastrear novos pedidos\",\n                \"wishlists\": \"Salvar itens na sua lista de desejos\",\n                \"cta\": \"Criar conta\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Esqueci a senha\",\n                \"subtitle\": \"Insira abaixo o e-mail associado à sua conta. Enviaremos instruções para redefinir sua senha.\",\n                \"confirmResetPassword\": \"Se o endereço de e-mail {email} está vinculado a uma conta na nossa loja, enviamos um e-mail de redefinição de senha. Verifique sua caixa de entrada e, caso não encontre, confira a pasta de spam.\",\n                \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"Email is required\",\n                    \"emailInvalid\": \"Please enter a valid email address\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Registrar conta\",\n            \"heading\": \"Nova conta\",\n            \"cta\": \"Criar conta\",\n            \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n            \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"O primeiro nome é obrigatório\",\n                \"lastNameRequired\": \"O sobrenome é obrigatório\",\n                \"emailRequired\": \"Email is required\",\n                \"emailInvalid\": \"Please enter a valid email address\",\n                \"passwordRequired\": \"A senha é obrigatória\",\n                \"passwordTooSmall\": \"Password must be at least {minLength, plural, =1 {1 character} other {# characters}} long\",\n                \"passwordLowercaseRequired\": \"Password must contain at least one lowercase letter\",\n                \"passwordUppercaseRequired\": \"Password must contain at least one uppercase letter\",\n                \"passwordNumberRequired\": \"Password must contain at least {minNumbers, plural, =1 {one number} other {# numbers}}\",\n                \"passwordSpecialCharacterRequired\": \"Password must contain at least one special character\",\n                \"passwordsMustMatch\": \"The passwords do not match\",\n                \"addressLine1Required\": \"Address line 1 is required\",\n                \"cityRequired\": \"É obrigatório informar a cidade\",\n                \"countryRequired\": \"O país é obrigatório\",\n                \"stateRequired\": \"Estado/Província é obrigatório\",\n                \"postalCodeRequired\": \"O código postal é obrigatório\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"Não existem produtos nesta marca\",\n                \"subtitle\": \"Experimente usar filtros diferentes.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Categorias\",\n            \"Empty\": {\n                \"title\": \"Não há produtos nesta categoria\",\n                \"subtitle\": \"Experimente usar filtros diferentes.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Resultados da pesquisa\",\n            \"searchResults\": \"Pesquisar resultados por\",\n            \"subCategories\": \"Categorias\",\n            \"Breadcrumbs\": {\n                \"home\": \"Página inicial\",\n                \"search\": \"Pesquisar\"\n            },\n            \"Empty\": {\n                \"title\": \"Nenhum resultado para “{term}”.\",\n                \"subtitle\": \"Experimente outra pesquisa.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtros\",\n            \"resetFilters\": \"Redefinir filtros\",\n            \"Range\": {\n                \"apply\": \"Aplicar\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Frete gratuito\",\n                \"isFeaturedLabel\": \"Está em destaque\",\n                \"inStockLabel\": \"Em estoque\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Ordenar por:\",\n            \"featuredItems\": \"Itens em destaque\",\n            \"bestSellingItems\": \"Itens mais vendidos\",\n            \"newestItems\": \"Itens mais recentes\",\n            \"aToZ\": \"A a Z\",\n            \"zToA\": \"Z a A\",\n            \"byReview\": \"Por avaliação\",\n            \"priceAscending\": \"Preço: crescente\",\n            \"priceDescending\": \"Preço: decrescente\",\n            \"relevance\": \"Relevância\"\n        },\n        \"Compare\": {\n            \"compare\": \"Comparar\",\n            \"remove\": \"Remover\",\n            \"maxCompareLimit\": \"Você atingiu o número máximo de produtos para comparação. Para adicionar um novo, remova um produto.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Endereços\",\n            \"logout\": \"Sair\",\n            \"orders\": \"Pedidos\",\n            \"settings\": \"Conta\",\n            \"wishlists\": \"Lista de desejos\"\n        },\n        \"Orders\": {\n            \"title\": \"Pedidos\",\n            \"orderNumber\": \"Pedido n.º\",\n            \"totalPrice\": \"Total\",\n            \"viewDetails\": \"Ver detalhes\",\n            \"EmptyState\": {\n                \"title\": \"Você não tem nenhum pedido\",\n                \"cta\": \"Comprar agora\"\n            },\n            \"Details\": {\n                \"title\": \"Pedido n.º {orderNumber}\",\n                \"shippingAddress\": \"Endereço de entrega\",\n                \"shippingMethod\": \"Método de envio:\",\n                \"summaryTotal\": \"Total\",\n                \"destination\": \"Destino\",\n                \"destinationWithCount\": \"Destino {número, número}/{total, número}\",\n                \"digitalDelivery\": \"Digital delivery to {email}\",\n                \"subtotal\": \"Subtotal\",\n                \"shipping\": \"Envio\",\n                \"tax\": \"Imposto\",\n                \"orderSummary\": \"Resumo do pedido\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Payment method} other {Payment methods}}\",\n                \"paymentEndingInLabel\": \"ending in\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Cartão de crédito\",\n                    \"giftCertificate\": \"Vale-presente\",\n                    \"storeCredit\": \"Crédito na loja\",\n                    \"other\": \"Outro\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Endereços\",\n            \"cta\": \"Adicionar endereço\",\n            \"edit\": \"Editar\",\n            \"delete\": \"Excluir\",\n            \"cancel\": \"Cancelar\",\n            \"create\": \"Criar\",\n            \"update\": \"Atualizar\",\n            \"setDefault\": \"Definir como padrão\",\n            \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n            \"EmptyState\": {\n                \"title\": \"You don't have any addresses\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"O primeiro nome é obrigatório\",\n                \"lastNameRequired\": \"O sobrenome é obrigatório\",\n                \"addressLine1Required\": \"Address line 1 is required\",\n                \"cityRequired\": \"É obrigatório informar a cidade\",\n                \"countryRequired\": \"O país é obrigatório\",\n                \"stateRequired\": \"Estado/Província é obrigatório\",\n                \"postalCodeRequired\": \"O código postal é obrigatório\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"Configurações da conta\",\n            \"changePassword\": \"Alterar senha\",\n            \"passwordUpdated\": \"A senha foi atualizada com sucesso!\",\n            \"settingsUpdated\": \"As configurações da conta foram atualizadas com sucesso!\",\n            \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n            \"currentPassword\": \"Senha atual\",\n            \"newPassword\": \"Nova senha\",\n            \"confirmPassword\": \"Confirmar senha\",\n            \"cta\": \"Atualizar\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Marketing preferences\",\n                \"label\": \"Inscreva-se no nosso boletim informativo.\",\n                \"marketingPreferencesUpdated\": \"Marketing preferences have been updated successfully!\",\n                \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"O primeiro nome é obrigatório\",\n                \"firstNameTooSmall\": \"First name must be at least 2 characters long\",\n                \"lastNameRequired\": \"O sobrenome é obrigatório\",\n                \"lastNameTooSmall\": \"Last name must be at least 2 characters long\",\n                \"emailRequired\": \"Email is required\",\n                \"emailInvalid\": \"Please enter a valid email address\",\n                \"currentPasswordRequired\": \"A senha atual é obrigatória\",\n                \"passwordRequired\": \"A senha é obrigatória\",\n                \"passwordTooSmall\": \"Password must be at least {minLength, plural, =1 {1 character} other {# characters}} long\",\n                \"passwordLowercaseRequired\": \"Password must contain at least one lowercase letter\",\n                \"passwordUppercaseRequired\": \"Password must contain at least one uppercase letter\",\n                \"passwordNumberRequired\": \"Password must contain at least {minNumbers, plural, =1 {one number} other {# numbers}}\",\n                \"passwordSpecialCharacterRequired\": \"Password must contain at least one special character\",\n                \"passwordsMustMatch\": \"The passwords do not match\",\n                \"confirmPasswordRequired\": \"Please confirm your password\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Ações da lista de desejos\",\n        \"title\": \"Lista de desejos\",\n        \"new\": \"Nova lista de desejos\",\n        \"items\": \"{count, plural, =1 {1 item} other {# itens}}\",\n        \"viewWishlist\": \"Ver lista\",\n        \"noWishlists\": \"Você não tem nenhuma lista de desejos\",\n        \"noWishlistsCallToAction\": \"Criar uma lista de desejos\",\n        \"emptyWishlist\": \"Você não adicionou produtos a esta lista de desejos.\",\n        \"share\": \"Compartilhar\",\n        \"shareSuccess\": \"A lista de desejos foi compartilhada.\",\n        \"shareCopied\": \"A URL pública da lista de desejos foi copiada para a sua área de transferência.\",\n        \"shareDisabled\": \"Sua lista de desejos deve ser pública para poder ser compartilhada.\",\n        \"makePublic\": \"Tornar pública\",\n        \"makePrivate\": \"Tornar privada\",\n        \"rename\": \"Renomear\",\n        \"delete\": \"Excluir\",\n        \"removeButtonTitle\": \"Remove product from wish list\",\n        \"Visibility\": {\n            \"public\": \"Pública\",\n            \"private\": \"Privado\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Cancelar\",\n            \"close\": \"Fechar\",\n            \"copy\": \"Copiar\",\n            \"create\": \"Criar\",\n            \"save\": \"Salvar\",\n            \"delete\": \"Excluir\",\n            \"newTitle\": \"Criar uma nova lista de desejos\",\n            \"shareTitle\": \"Compartilhar {name}\",\n            \"renameTitle\": \"Renomear {name}\",\n            \"deleteTitle\": \"Excluir {name}\",\n            \"changeVisibilityPublicTitle\": \"Tornar {name} pública?\",\n            \"changeVisibilityPrivateTitle\": \"Tornar {name} privada?\",\n            \"makePublicContent\": \"Deseja mesmo tornar a lista <bold>{name}</bold> pública? Isso permitirá que outras pessoas vejam a sua lista de desejos se tiverem o link.\",\n            \"makePrivateContent\": \"Deseja mesmo tornar a lista <bold>{name}</bold> privada? Se você compartilhou sua lista de desejos com outras pessoas, elas não poderão mais vê-la.\",\n            \"deleteContent\": \"Deseja mesmo excluir <bold>{name}</bold>? Esta ação não pode ser desfeita.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Nome\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"O nome da lista de desejos não pode estar vazio.\",\n            \"updateFailed\": \"Não foi possível atualizar a sua lista de desejos. Tente novamente.\",\n            \"deleteFailed\": \"Não foi possível excluir a sua lista de desejos. Tente novamente.\",\n            \"removeProductFailed\": \"Não foi possível remover o produto da sua lista de desejos. Tente novamente.\",\n            \"unauthorized\": \"Você não tem autorização para executar esta ação. Faça login e tente novamente.\",\n            \"unexpected\": \"Ocorreu um erro inesperado. Tente novamente.\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"A lista de desejos foi criada.\",\n            \"updateSuccess\": \"A lista de desejos foi atualizada.\",\n            \"deleteSuccess\": \"A lista de desejos foi excluída.\",\n            \"removeItemSuccess\": \"O item foi removido da sua lista de desejos.\"\n        },\n        \"Button\": {\n            \"label\": \"Add to wish list\",\n            \"addToNewWishlist\": \"Adicionar à nova lista de desejos\",\n            \"defaultWishlistName\": \"Minhas listas de desejos\",\n            \"addSuccessMessage\": \"O produto foi adicionado à sua lista de desejos\",\n            \"removeSuccessMessage\": \"O produto foi removido da sua lista de desejos\",\n            \"Errors\": {\n                \"addProductFailed\": \"Não foi possível adicionar o produto da sua lista de desejos. Tente novamente.\",\n                \"removeProductFailed\": \"Não foi possível remover o produto da sua lista de desejos. Tente novamente.\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Lista de desejos pública\",\n        \"defaultName\": \"Lista de desejos pública\",\n        \"emptyWishlist\": \"Esta lista de desejos ainda não tem nenhum produto.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Página inicial\",\n        \"Empty\": {\n            \"title\": \"Nenhum post do blog foi encontrado\",\n            \"subtitle\": \"Volte mais tarde para mais conteúdo\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Compartilhar\",\n            \"email\": \"Email\",\n            \"print\": \"Imprimir\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Carrinho\",\n        \"heading\": \"Seu carrinho\",\n        \"proceedToCheckout\": \"Prosseguir para a finalização da compra\",\n        \"increment\": \"Aumentar quantidade\",\n        \"decrement\": \"Reduzir quantidade\",\n        \"removeItem\": \"Remover item\",\n        \"cartCombined\": \"Percebemos que você tinha itens salvos em um carrinho anterior, então colocamos esses itens no carrinho atual para você.\",\n        \"cartRestored\": \"Você iniciou um carrinho em outro dispositivo, e restauramos esse carrinho aqui para que você possa continuar de onde parou.\",\n        \"cartUpdateInProgress\": \"You have a cart update in progress. Are you sure you want to leave this page? Your changes may be lost.\",\n        \"originalPrice\": \"Original price was {price}.\",\n        \"currentPrice\": \"Current price is {price}.\",\n        \"quantityReadyToShip\": \"{quantity, number} ready to ship\",\n        \"quantityOnBackorder\": \"{quantity, number} will be backordered\",\n        \"partiallyAvailable\": \"Only {quantity, number} available\",\n        \"CheckoutSummary\": {\n            \"title\": \"Resumo\",\n            \"subTotal\": \"Subtotal\",\n            \"discounts\": \"Descontos\",\n            \"tax\": \"Imposto\",\n            \"total\": \"Total\",\n            \"CouponCode\": {\n                \"apply\": \"Aplicar\",\n                \"couponCode\": \"Código de cupom\",\n                \"removeCouponCode\": \"Remover código de cupom\",\n                \"invalidCouponCode\": \"Digite um código de cupom válido\",\n                \"cartNotFound\": \"An error occurred when retrieving your cart\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Envio\",\n                \"add\": \"Adicionar\",\n                \"change\": \"Alterar\",\n                \"cancel\": \"Cancelar\",\n                \"country\": \"País\",\n                \"city\": \"Cidade\",\n                \"state\": \"Estado/Província\",\n                \"postalCode\": \"Código postal\",\n                \"updatedShippingOptions\": \"Atualizar opções de envio\",\n                \"viewShippingOptions\": \"Visualizar opções de envio\",\n                \"editAddress\": \"Editar endereço\",\n                \"shippingOptions\": \"Opções de envio\",\n                \"updateShipping\": \"Atualizar envio\",\n                \"addShipping\": \"Adicionar envio\",\n                \"cartNotFound\": \"An error occurred when retrieving your cart\",\n                \"noShippingOptions\": \"Não há opções de envio disponíveis para o seu endereço\",\n                \"countryRequired\": \"O país é obrigatório\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Vale-presente\",\n            \"giftCertificateCode\": \"Código do vale-presente\",\n            \"removeGiftCertificate\": \"Remove gift certificate\",\n            \"apply\": \"Aplicar\",\n            \"to\": \"para\",\n            \"message\": \"Mensagem\",\n            \"invalidGiftCertificate\": \"Please enter a valid gift certificate code\",\n            \"cartNotFound\": \"An error occurred when retrieving your cart\"\n        },\n        \"Empty\": {\n            \"title\": \"Seu carrinho está vazio.\",\n            \"subtitle\": \"Adicione alguns produtos para começar.\",\n            \"cta\": \"Continuar comprando\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"An error occurred when retrieving your cart\",\n            \"lineItemNotFound\": \"Item de linha não encontrado.\",\n            \"failedToUpdateQuantity\": \"Não foi possível atualizar a quantidade.\",\n            \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Comparar produtos\",\n        \"addToCart\": \"Adicionar ao carrinho\",\n        \"next\": \"Próximos produtos\",\n        \"previous\": \"Produtos anteriores\",\n        \"noProductsToCompare\": \"Não há produtos para comparar\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Peso\",\n        \"description\": \"Descrição\",\n        \"noDescription\": \"Não há nenhuma descrição disponível.\",\n        \"rating\": \"Taxa\",\n        \"noRatings\": \"Não há avaliações.\",\n        \"otherDetails\": \"Outros detalhes\",\n        \"noOtherDetails\": \"Não há outros detalhes.\",\n        \"viewOptions\": \"Ver opções\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 item} other {# itens}} adicionado(s) <cartLink> ao carrinho</cartLink>\",\n        \"missingCart\": \"Carrinho não encontrado. Tente novamente mais tarde.\",\n        \"unknownError\": \"Erro desconhecido. Tente novamente mais tarde.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Quantidade\",\n            \"increaseQuantity\": \"Aumentar quantidade\",\n            \"decreaseQuantity\": \"Reduzir quantidade\",\n            \"emptySelectPlaceholder\": \"Selecione uma opção\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 item} other {# itens}} adicionado(s) <cartLink> ao carrinho</cartLink>\",\n            \"missingCart\": \"Carrinho não encontrado. Tente novamente mais tarde!\",\n            \"unknownError\": \"Erro desconhecido. Tente novamente mais tarde.\",\n            \"variantRequiredError\": \"Para colocar este produto no carrinho, você precisa selecionar algumas opções.\",\n            \"increaseNumber\": \"Aumentar número\",\n            \"decreaseNumber\": \"Diminuir número\",\n            \"thumbnail\": \"Visualizar número da imagem\",\n            \"additionalInformation\": \"Outras informações\",\n            \"currentStock\": \"{quantity, number} in stock\",\n            \"backorderQuantity\": \"{quantity, number} will be on backorder\",\n            \"loadingMoreImages\": \"Loading more images\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 more image loaded} other {# more images loaded}}\",\n            \"Submit\": {\n                \"addToCart\": \"Adicionar ao carrinho\",\n                \"outOfStock\": \"Fora do estoque\",\n                \"preorder\": \"Pré-venda\",\n                \"unavailable\": \"Indisponível\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Especificações\",\n                \"warranty\": \"Garantia\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Peso\",\n                \"condition\": \"Condição\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Produtos relacionados\",\n            \"noRelatedProducts\": \"Nenhum produto relacionado encontrado\",\n            \"browseCatalog\": \"Experimente conferir nosso catálogo completo de produtos.\",\n            \"cta\": \"Comprar todos\",\n            \"previousProducts\": \"Produtos anteriores\",\n            \"nextProducts\": \"Próximos produtos\",\n            \"scrollbar\": \"Barra de rolagem de produtos relacionados\"\n        },\n        \"Reviews\": {\n            \"title\": \"Avaliações\",\n            \"empty\": \"Nenhuma avaliação foi adicionada para este produto.\",\n            \"previous\": \"Avaliações anteriores\",\n            \"next\": \"Próximas avaliações\",\n            \"Form\": {\n                \"button\": \"Write a review\",\n                \"title\": \"Write a review\",\n                \"submit\": \"Enviar\",\n                \"cancel\": \"Cancelar\",\n                \"ratingLabel\": \"Taxa\",\n                \"titleLabel\": \"Título\",\n                \"reviewLabel\": \"Avaliação\",\n                \"nameLabel\": \"Nome\",\n                \"emailLabel\": \"Email\",\n                \"successMessage\": \"Your review has been submitted successfully!\",\n                \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n                \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Title is required\",\n                    \"authorRequired\": \"O nome é obrigatório\",\n                    \"emailRequired\": \"Email is required\",\n                    \"emailInvalid\": \"Please enter a valid email address\",\n                    \"textRequired\": \"Review is required\",\n                    \"ratingRequired\": \"Rating is required\",\n                    \"ratingTooSmall\": \"Rating must be at least 1\",\n                    \"ratingTooLarge\": \"Rating must be at most 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Página inicial\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Página inicial\",\n            \"Form\": {\n                \"success\": \"Agradecemos o contato. Responderemos em breve.\",\n                \"successCta\": \"Continuar comprando\",\n                \"fullName\": \"Nome completo\",\n                \"companyName\": \"Nome da empresa\",\n                \"phone\": \"Telefone\",\n                \"orderNo\": \"Número do pedido\",\n                \"rma\": \"Número do RMA\",\n                \"email\": \"Email\",\n                \"comments\": \"Comentários/perguntas\",\n                \"cta\": \"Enviar formulário\",\n                \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n                \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"Manutenção\",\n        \"message\": \"Estamos em manutenção\",\n        \"contactUs\": \"Entre em contato conosco em:\"\n    },\n    \"Error\": {\n        \"title\": \"Houve um erro no servidor!\",\n        \"subtitle\": \"Tente novamente mais tarde.\",\n        \"cta\": \"Tente novamente\"\n    },\n    \"NotFound\": {\n        \"title\": \"Não encontramos essa página!\",\n        \"subtitle\": \"Tente fazer uma pesquisa diferente ou volte para a página inicial.\",\n        \"featuredProducts\": \"Produtos em destaque\",\n        \"search\": \"Pesquisar\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Página inicial\",\n            \"toggleNavigation\": \"Alternar navegação\",\n            \"Icons\": {\n                \"account\": \"Perfil\",\n                \"cart\": \"Carrinho\",\n                \"search\": \"Abrir pop-up de pesquisa\",\n                \"giftCertificates\": \"Vales-presente\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Switch currency\",\n                \"invalidCurrency\": \"Moeda inválida\",\n                \"errorUpdatingCurrency\": \"Erro ao atualizar a moeda do seu carrinho. Tente novamente.\"\n            },\n            \"Search\": {\n                \"products\": \"Produtos\",\n                \"categories\": \"Categorias\",\n                \"brands\": \"Marcas\",\n                \"noSearchResultsTitle\": \"Nenhum resultado para “{term}”.\",\n                \"noSearchResultsSubtitle\": \"Experimente outra pesquisa.\",\n                \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente.\",\n                \"inputPlaceholder\": \"Pesquise produtos, categorias, marcas...\",\n                \"submitLabel\": \"Pesquisar\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Página inicial\",\n            \"contactUs\": \"Fale conosco\",\n            \"socialMediaLinks\": \"Links de redes sociais\",\n            \"categories\": \"Categorias\",\n            \"brands\": \"Marcas\",\n            \"navigate\": \"Navegar\",\n            \"giftCertificates\": \"Vales-presente\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Assine nosso boletim informativo\",\n            \"placeholder\": \"Digite seu email\",\n            \"description\": \"Fique por dentro das últimas novidades e ofertas da nossa loja.\",\n            \"subscribedToNewsletter\": \"You have been subscribed to our newsletter!\",\n            \"Errors\": {\n                \"emailRequired\": \"Email is required\",\n                \"invalidEmail\": \"Please enter a valid email address\",\n                \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Reject All\",\n                \"acceptAll\": \"Accept All\",\n                \"customize\": \"Personalizar\",\n                \"save\": \"Salvar configurações\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"We value your privacy\",\n                \"description\": \"This site uses cookies to improve your browsing experience, analyze site traffic, and show personalized content.\",\n                \"privacyPolicy\": \"Política de privacidade\"\n            },\n            \"Dialog\": {\n                \"title\": \"Privacy Settings\",\n                \"description\": \"Customize your privacy settings here. You can choose which types of cookies and tracking technologies you would like to allow.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Strictly Necessary\",\n                    \"description\": \"These cookies are essential for the website to function properly and cannot be disabled.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Functionality\",\n                    \"description\": \"These cookies enable enhanced functionality and personalization of the website.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"These cookies are used to deliver relevant advertisements and track their effectiveness.\"\n                },\n                \"measurement\": {\n                    \"title\": \"Análise\",\n                    \"description\": \"These cookies help us understand how visitors interact with the website and improve its performance.\"\n                },\n                \"experience\": {\n                    \"title\": \"Experiência\",\n                    \"description\": \"These cookies help us provide a better user experience and test new features.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"Original price was {price}.\",\n            \"currentPrice\": \"Current price is {price}.\",\n            \"range\": \"Price from {minValue} to {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Vales-presente\",\n        \"description\": \"Give the perfect gift that never goes out of style. Let friends and loved ones choose exactly what they want from our entire collection.\",\n        \"purchaseLabel\": \"Comprar agora\",\n        \"checkBalanceLabel\": \"Check balance\",\n        \"expiresAtLabel\": \"Valid thru\",\n        \"CheckBalance\": {\n            \"title\": \"Check balance\",\n            \"description\": \"You can check the balance and get the information about your gift certificate by typing the code in the box below.\",\n            \"inputLabel\": \"Código\",\n            \"inputPlaceholder\": \"xxx-xxx-xxx-xxx\",\n            \"purchasedDateLabel\": \"Comprados\",\n            \"senderLabel\": \"De\",\n            \"Errors\": {\n                \"invalidCode\": \"The gift certificate code you entered is invalid. Please check the code and try again.\",\n                \"codeRequired\": \"Insira um código de vale-presente.\",\n                \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Purchase a gift certificate\",\n            \"title\": \"Digital gift certificate\",\n            \"description\": \"Explore our gift certificates, perfect for any occasion. Choose the amount and personalize your message.\",\n            \"successMessage\": \"Gift certificate has been added to <cartLink> your cart</cartLink>\",\n            \"missingCart\": \"Carrinho não encontrado. Tente novamente mais tarde.\",\n            \"unknownError\": \"Erro desconhecido. Tente novamente mais tarde.\",\n            \"Form\": {\n                \"amountLabel\": \"Quantidade\",\n                \"customAmountLabel\": \"Amount (between {minAmount} and {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Select an amount\",\n                \"customAmountPlaceholder\": \"Enter custom amount\",\n                \"senderNameLabel\": \"Your name\",\n                \"senderEmailLabel\": \"Your email\",\n                \"recipientNameLabel\": \"Recipient's name\",\n                \"recipientEmailLabel\": \"Recipient's email\",\n                \"namePlaceholder\": \"Enter name\",\n                \"emailPlaceholder\": \"Informe o e-mail\",\n                \"messageLabel\": \"Mensagem\",\n                \"messagePlaceholder\": \"Enter your message (optional)\",\n                \"nonRefundableCheckboxLabel\": \"I agree that Gift Certificates are non-refundable\",\n                \"expiryCheckboxLabel\": \"I acknowledge that this Gift Certificate will expire on {expiryDate}\",\n                \"ctaLabel\": \"Adicionar ao carrinho\",\n                \"Errors\": {\n                    \"amountRequired\": \"Please select or enter a gift certificate amount\",\n                    \"amountInvalid\": \"Please select a valid gift certificate amount\",\n                    \"amountOutOfRange\": \"Please enter an amount between {minAmount} and {maxAmount}\",\n                    \"unexpectedSettingsError\": \"An unexpected error occurred while retrieving gift certificate settings. Please try again later.\",\n                    \"senderNameRequired\": \"Your name is required\",\n                    \"senderEmailRequired\": \"Your email is required\",\n                    \"recipientNameRequired\": \"Recipient's name is required\",\n                    \"recipientEmailRequired\": \"Recipient's email is required\",\n                    \"emailInvalid\": \"Please enter a valid email address\",\n                    \"checkboxRequired\": \"You must check this box to continue\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"Opcional\",\n        \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n        \"Errors\": {\n            \"invalidInput\": \"Please check your input and try again\",\n            \"invalidFormat\": \"The value entered does not match the required format\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/pt.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Novidades para todas as ocasiões\",\n                \"description\": \"Explore nossas novidades, selecionadas para oferecer estilo, funcionalidade e inspiração. Compre agora e conheça o seu próximo favorito.\",\n                \"alt\": \"Cinco plantas pequenas em vasos exibidas em blocos beges empilhados, com uma variedade de folhagens verdes em vasos cinza-escuro em um fundo neutro.\",\n                \"cta\": \"Comprar agora\"\n            },\n            \"Slide02\": {\n                \"title\": \"Descubra o que há de novo\",\n                \"description\": \"Confira nossas últimas novidades e encontre algo novo e empolgante para a sua casa.\",\n                \"alt\": \"Mãos estendidas para pegar uma samambaia verde em uma cesta de vime com um laço decorativo sobre um fundo bege com sombras suaves.\",\n                \"cta\": \"Comprar agora\"\n            },\n            \"Slide03\": {\n                \"title\": \"Algo para todos\",\n                \"description\": \"Não perca ofertas exclusivas de nossos produtos mais vendidos. Compre hoje seus itens preferidos e economize muito.\",\n                \"alt\": \"Close de uma folha verde vibrante com perfurações, destacando sua textura suave e detalhes naturais.\",\n                \"cta\": \"Comprar agora\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Coleção em destaque\",\n            \"description\": \"Explore nossas principais escolhas nesta coleção em destaque. Encontre o presente perfeito para você ou para aquela pessoa especial!\",\n            \"cta\": \"Ver mais\",\n            \"emptyStateTitle\": \"Nenhum produto encontrado\",\n            \"emptyStateSubtitle\": \"Experimente conferir nosso catálogo completo de produtos.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Recém-chegados\",\n            \"description\": \"Nossos produtos mais recentes estão aqui. Confira as novidades na loja.\",\n            \"cta\": \"Ver tudo\",\n            \"emptyStateTitle\": \"Nenhum produto encontrado\",\n            \"emptyStateSubtitle\": \"Experimente conferir nosso catálogo completo de produtos.\",\n            \"previousProducts\": \"Produtos anteriores\",\n            \"nextProducts\": \"Próximos produtos\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Alterar senha\",\n            \"newPassword\": \"Nova senha\",\n            \"confirmPassword\": \"Confirmar senha\",\n            \"passwordUpdated\": \"A senha foi atualizada com sucesso!\",\n            \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"A senha é obrigatória\",\n                \"passwordTooSmall\": \"Password must be at least {minLength, plural, =1 {1 character} other {# characters}} long\",\n                \"passwordLowercaseRequired\": \"Password must contain at least one lowercase letter\",\n                \"passwordUppercaseRequired\": \"Password must contain at least one uppercase letter\",\n                \"passwordNumberRequired\": \"Password must contain at least {minNumbers, plural, =1 {one number} other {# numbers}}\",\n                \"passwordSpecialCharacterRequired\": \"Password must contain at least one special character\",\n                \"passwordsMustMatch\": \"The passwords do not match\",\n                \"confirmPasswordRequired\": \"Please confirm your password\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Acesso\",\n            \"heading\": \"Credencial de acesso\",\n            \"forgotPassword\": \"Esqueceu sua senha?\",\n            \"cta\": \"Credencial de acesso\",\n            \"email\": \"Email\",\n            \"password\": \"Senha\",\n            \"invalidCredentials\": \"O endereço de email ou a senha está incorreto. Tente fazer login novamente ou redefinir sua senha\",\n            \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n            \"passwordResetRequired\": \"Password reset required. Please check your email for instructions to reset your password.\",\n            \"invalidToken\": \"Your login link is invalid or has expired. Please try logging in again.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"Email is required\",\n                \"emailInvalid\": \"Please enter a valid email address\",\n                \"passwordRequired\": \"A senha é obrigatória\",\n                \"invalidInput\": \"Please check your input and try again.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"Cliente novo?\",\n                \"accountBenefits\": \"Crie uma conta conosco para poder:\",\n                \"fastCheckout\": \"Finalizar as compras mais rápido\",\n                \"multipleAddresses\": \"Salvar vários endereços de entrega\",\n                \"ordersHistory\": \"Acessar seu histórico de pedidos\",\n                \"ordersTracking\": \"Rastrear novos pedidos\",\n                \"wishlists\": \"Salvar itens na sua lista de desejos\",\n                \"cta\": \"Criar conta\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Esqueci a senha\",\n                \"subtitle\": \"Insira abaixo o e-mail associado à sua conta. Enviaremos instruções para redefinir sua senha.\",\n                \"confirmResetPassword\": \"Se o endereço de e-mail {email} está vinculado a uma conta na nossa loja, enviamos um e-mail de redefinição de senha. Verifique sua caixa de entrada e, caso não encontre, confira a pasta de spam.\",\n                \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"Email is required\",\n                    \"emailInvalid\": \"Please enter a valid email address\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Registrar conta\",\n            \"heading\": \"Nova conta\",\n            \"cta\": \"Criar conta\",\n            \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n            \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"O primeiro nome é obrigatório\",\n                \"lastNameRequired\": \"O sobrenome é obrigatório\",\n                \"emailRequired\": \"Email is required\",\n                \"emailInvalid\": \"Please enter a valid email address\",\n                \"passwordRequired\": \"A senha é obrigatória\",\n                \"passwordTooSmall\": \"Password must be at least {minLength, plural, =1 {1 character} other {# characters}} long\",\n                \"passwordLowercaseRequired\": \"Password must contain at least one lowercase letter\",\n                \"passwordUppercaseRequired\": \"Password must contain at least one uppercase letter\",\n                \"passwordNumberRequired\": \"Password must contain at least {minNumbers, plural, =1 {one number} other {# numbers}}\",\n                \"passwordSpecialCharacterRequired\": \"Password must contain at least one special character\",\n                \"passwordsMustMatch\": \"The passwords do not match\",\n                \"addressLine1Required\": \"Address line 1 is required\",\n                \"cityRequired\": \"É obrigatório informar a cidade\",\n                \"countryRequired\": \"O país é obrigatório\",\n                \"stateRequired\": \"Estado/Província é obrigatório\",\n                \"postalCodeRequired\": \"O código postal é obrigatório\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"Não existem produtos nesta marca\",\n                \"subtitle\": \"Experimente usar filtros diferentes.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Categorias\",\n            \"Empty\": {\n                \"title\": \"Não há produtos nesta categoria\",\n                \"subtitle\": \"Experimente usar filtros diferentes.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Resultados da pesquisa\",\n            \"searchResults\": \"Pesquisar resultados por\",\n            \"subCategories\": \"Categorias\",\n            \"Breadcrumbs\": {\n                \"home\": \"Página inicial\",\n                \"search\": \"Pesquisar\"\n            },\n            \"Empty\": {\n                \"title\": \"Nenhum resultado para “{term}”.\",\n                \"subtitle\": \"Experimente outra pesquisa.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filtros\",\n            \"resetFilters\": \"Redefinir filtros\",\n            \"Range\": {\n                \"apply\": \"Aplicar\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Frete gratuito\",\n                \"isFeaturedLabel\": \"Está em destaque\",\n                \"inStockLabel\": \"Em estoque\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Ordenar por:\",\n            \"featuredItems\": \"Itens em destaque\",\n            \"bestSellingItems\": \"Itens mais vendidos\",\n            \"newestItems\": \"Itens mais recentes\",\n            \"aToZ\": \"A a Z\",\n            \"zToA\": \"Z a A\",\n            \"byReview\": \"Por avaliação\",\n            \"priceAscending\": \"Preço: crescente\",\n            \"priceDescending\": \"Preço: decrescente\",\n            \"relevance\": \"Relevância\"\n        },\n        \"Compare\": {\n            \"compare\": \"Comparar\",\n            \"remove\": \"Remover\",\n            \"maxCompareLimit\": \"Você atingiu o número máximo de produtos para comparação. Para adicionar um novo, remova um produto.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Endereços\",\n            \"logout\": \"Sair\",\n            \"orders\": \"Pedidos\",\n            \"settings\": \"Conta\",\n            \"wishlists\": \"Lista de desejos\"\n        },\n        \"Orders\": {\n            \"title\": \"Pedidos\",\n            \"orderNumber\": \"Pedido n.º\",\n            \"totalPrice\": \"Total\",\n            \"viewDetails\": \"Ver detalhes\",\n            \"EmptyState\": {\n                \"title\": \"Você não tem nenhum pedido\",\n                \"cta\": \"Comprar agora\"\n            },\n            \"Details\": {\n                \"title\": \"Pedido n.º {orderNumber}\",\n                \"shippingAddress\": \"Endereço de entrega\",\n                \"shippingMethod\": \"Método de envio:\",\n                \"summaryTotal\": \"Total\",\n                \"destination\": \"Destino\",\n                \"destinationWithCount\": \"Destino {número, número}/{total, número}\",\n                \"digitalDelivery\": \"Digital delivery to {email}\",\n                \"subtotal\": \"Subtotal\",\n                \"shipping\": \"Envio\",\n                \"tax\": \"Imposto\",\n                \"orderSummary\": \"Resumo do pedido\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Payment method} other {Payment methods}}\",\n                \"paymentEndingInLabel\": \"ending in\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Cartão de crédito\",\n                    \"giftCertificate\": \"Vale-presente\",\n                    \"storeCredit\": \"Crédito na loja\",\n                    \"other\": \"Outro\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Endereços\",\n            \"cta\": \"Adicionar endereço\",\n            \"edit\": \"Editar\",\n            \"delete\": \"Excluir\",\n            \"cancel\": \"Cancelar\",\n            \"create\": \"Criar\",\n            \"update\": \"Atualizar\",\n            \"setDefault\": \"Definir como padrão\",\n            \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n            \"EmptyState\": {\n                \"title\": \"You don't have any addresses\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"O primeiro nome é obrigatório\",\n                \"lastNameRequired\": \"O sobrenome é obrigatório\",\n                \"addressLine1Required\": \"Address line 1 is required\",\n                \"cityRequired\": \"É obrigatório informar a cidade\",\n                \"countryRequired\": \"O país é obrigatório\",\n                \"stateRequired\": \"Estado/Província é obrigatório\",\n                \"postalCodeRequired\": \"O código postal é obrigatório\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"Configurações da conta\",\n            \"changePassword\": \"Alterar senha\",\n            \"passwordUpdated\": \"A senha foi atualizada com sucesso!\",\n            \"settingsUpdated\": \"As configurações da conta foram atualizadas com sucesso!\",\n            \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n            \"currentPassword\": \"Senha atual\",\n            \"newPassword\": \"Nova senha\",\n            \"confirmPassword\": \"Confirmar senha\",\n            \"cta\": \"Atualizar\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Marketing preferences\",\n                \"label\": \"Inscreva-se no nosso boletim informativo.\",\n                \"marketingPreferencesUpdated\": \"Marketing preferences have been updated successfully!\",\n                \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"O primeiro nome é obrigatório\",\n                \"firstNameTooSmall\": \"First name must be at least 2 characters long\",\n                \"lastNameRequired\": \"O sobrenome é obrigatório\",\n                \"lastNameTooSmall\": \"Last name must be at least 2 characters long\",\n                \"emailRequired\": \"Email is required\",\n                \"emailInvalid\": \"Please enter a valid email address\",\n                \"currentPasswordRequired\": \"A senha atual é obrigatória\",\n                \"passwordRequired\": \"A senha é obrigatória\",\n                \"passwordTooSmall\": \"Password must be at least {minLength, plural, =1 {1 character} other {# characters}} long\",\n                \"passwordLowercaseRequired\": \"Password must contain at least one lowercase letter\",\n                \"passwordUppercaseRequired\": \"Password must contain at least one uppercase letter\",\n                \"passwordNumberRequired\": \"Password must contain at least {minNumbers, plural, =1 {one number} other {# numbers}}\",\n                \"passwordSpecialCharacterRequired\": \"Password must contain at least one special character\",\n                \"passwordsMustMatch\": \"The passwords do not match\",\n                \"confirmPasswordRequired\": \"Please confirm your password\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Ações da lista de desejos\",\n        \"title\": \"Lista de desejos\",\n        \"new\": \"Nova lista de desejos\",\n        \"items\": \"{count, plural, =1 {1 item} other {# itens}}\",\n        \"viewWishlist\": \"Ver lista\",\n        \"noWishlists\": \"Você não tem nenhuma lista de desejos\",\n        \"noWishlistsCallToAction\": \"Criar uma lista de desejos\",\n        \"emptyWishlist\": \"Você não adicionou produtos a esta lista de desejos.\",\n        \"share\": \"Compartilhar\",\n        \"shareSuccess\": \"A lista de desejos foi compartilhada.\",\n        \"shareCopied\": \"A URL pública da lista de desejos foi copiada para a sua área de transferência.\",\n        \"shareDisabled\": \"Sua lista de desejos deve ser pública para poder ser compartilhada.\",\n        \"makePublic\": \"Tornar pública\",\n        \"makePrivate\": \"Tornar privada\",\n        \"rename\": \"Renomear\",\n        \"delete\": \"Excluir\",\n        \"removeButtonTitle\": \"Remove product from wish list\",\n        \"Visibility\": {\n            \"public\": \"Pública\",\n            \"private\": \"Privado\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Cancelar\",\n            \"close\": \"Fechar\",\n            \"copy\": \"Copiar\",\n            \"create\": \"Criar\",\n            \"save\": \"Salvar\",\n            \"delete\": \"Excluir\",\n            \"newTitle\": \"Criar uma nova lista de desejos\",\n            \"shareTitle\": \"Compartilhar {name}\",\n            \"renameTitle\": \"Renomear {name}\",\n            \"deleteTitle\": \"Excluir {name}\",\n            \"changeVisibilityPublicTitle\": \"Tornar {name} pública?\",\n            \"changeVisibilityPrivateTitle\": \"Tornar {name} privada?\",\n            \"makePublicContent\": \"Deseja mesmo tornar a lista <bold>{name}</bold> pública? Isso permitirá que outras pessoas vejam a sua lista de desejos se tiverem o link.\",\n            \"makePrivateContent\": \"Deseja mesmo tornar a lista <bold>{name}</bold> privada? Se você compartilhou sua lista de desejos com outras pessoas, elas não poderão mais vê-la.\",\n            \"deleteContent\": \"Deseja mesmo excluir <bold>{name}</bold>? Esta ação não pode ser desfeita.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Nome\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"O nome da lista de desejos não pode estar vazio.\",\n            \"updateFailed\": \"Não foi possível atualizar a sua lista de desejos. Tente novamente.\",\n            \"deleteFailed\": \"Não foi possível excluir a sua lista de desejos. Tente novamente.\",\n            \"removeProductFailed\": \"Não foi possível remover o produto da sua lista de desejos. Tente novamente.\",\n            \"unauthorized\": \"Você não tem autorização para executar esta ação. Faça login e tente novamente.\",\n            \"unexpected\": \"Ocorreu um erro inesperado. Tente novamente.\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"A lista de desejos foi criada.\",\n            \"updateSuccess\": \"A lista de desejos foi atualizada.\",\n            \"deleteSuccess\": \"A lista de desejos foi excluída.\",\n            \"removeItemSuccess\": \"O item foi removido da sua lista de desejos.\"\n        },\n        \"Button\": {\n            \"label\": \"Add to wish list\",\n            \"addToNewWishlist\": \"Adicionar à nova lista de desejos\",\n            \"defaultWishlistName\": \"Minhas listas de desejos\",\n            \"addSuccessMessage\": \"O produto foi adicionado à sua lista de desejos\",\n            \"removeSuccessMessage\": \"O produto foi removido da sua lista de desejos\",\n            \"Errors\": {\n                \"addProductFailed\": \"Não foi possível adicionar o produto da sua lista de desejos. Tente novamente.\",\n                \"removeProductFailed\": \"Não foi possível remover o produto da sua lista de desejos. Tente novamente.\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Lista de desejos pública\",\n        \"defaultName\": \"Lista de desejos pública\",\n        \"emptyWishlist\": \"Esta lista de desejos ainda não tem nenhum produto.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blog\",\n        \"home\": \"Página inicial\",\n        \"Empty\": {\n            \"title\": \"Nenhum post do blog foi encontrado\",\n            \"subtitle\": \"Volte mais tarde para mais conteúdo\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Compartilhar\",\n            \"email\": \"Email\",\n            \"print\": \"Imprimir\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Carrinho\",\n        \"heading\": \"Seu carrinho\",\n        \"proceedToCheckout\": \"Prosseguir para a finalização da compra\",\n        \"increment\": \"Aumentar quantidade\",\n        \"decrement\": \"Reduzir quantidade\",\n        \"removeItem\": \"Remover item\",\n        \"cartCombined\": \"Percebemos que você tinha itens salvos em um carrinho anterior, então colocamos esses itens no carrinho atual para você.\",\n        \"cartRestored\": \"Você iniciou um carrinho em outro dispositivo, e restauramos esse carrinho aqui para que você possa continuar de onde parou.\",\n        \"cartUpdateInProgress\": \"You have a cart update in progress. Are you sure you want to leave this page? Your changes may be lost.\",\n        \"originalPrice\": \"Original price was {price}.\",\n        \"currentPrice\": \"Current price is {price}.\",\n        \"quantityReadyToShip\": \"{quantity, number} ready to ship\",\n        \"quantityOnBackorder\": \"{quantity, number} will be backordered\",\n        \"partiallyAvailable\": \"Only {quantity, number} available\",\n        \"CheckoutSummary\": {\n            \"title\": \"Resumo\",\n            \"subTotal\": \"Subtotal\",\n            \"discounts\": \"Descontos\",\n            \"tax\": \"Imposto\",\n            \"total\": \"Total\",\n            \"CouponCode\": {\n                \"apply\": \"Aplicar\",\n                \"couponCode\": \"Código de cupom\",\n                \"removeCouponCode\": \"Remover código de cupom\",\n                \"invalidCouponCode\": \"Digite um código de cupom válido\",\n                \"cartNotFound\": \"An error occurred when retrieving your cart\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Envio\",\n                \"add\": \"Adicionar\",\n                \"change\": \"Alterar\",\n                \"cancel\": \"Cancelar\",\n                \"country\": \"País\",\n                \"city\": \"Cidade\",\n                \"state\": \"Estado/Província\",\n                \"postalCode\": \"Código postal\",\n                \"updatedShippingOptions\": \"Atualizar opções de envio\",\n                \"viewShippingOptions\": \"Visualizar opções de envio\",\n                \"editAddress\": \"Editar endereço\",\n                \"shippingOptions\": \"Opções de envio\",\n                \"updateShipping\": \"Atualizar envio\",\n                \"addShipping\": \"Adicionar envio\",\n                \"cartNotFound\": \"An error occurred when retrieving your cart\",\n                \"noShippingOptions\": \"Não há opções de envio disponíveis para o seu endereço\",\n                \"countryRequired\": \"O país é obrigatório\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Vale-presente\",\n            \"giftCertificateCode\": \"Código do vale-presente\",\n            \"removeGiftCertificate\": \"Remove gift certificate\",\n            \"apply\": \"Aplicar\",\n            \"to\": \"para\",\n            \"message\": \"Mensagem\",\n            \"invalidGiftCertificate\": \"Please enter a valid gift certificate code\",\n            \"cartNotFound\": \"An error occurred when retrieving your cart\"\n        },\n        \"Empty\": {\n            \"title\": \"Seu carrinho está vazio.\",\n            \"subtitle\": \"Adicione alguns produtos para começar.\",\n            \"cta\": \"Continuar comprando\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"An error occurred when retrieving your cart\",\n            \"lineItemNotFound\": \"Item de linha não encontrado.\",\n            \"failedToUpdateQuantity\": \"Não foi possível atualizar a quantidade.\",\n            \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Comparar produtos\",\n        \"addToCart\": \"Adicionar ao carrinho\",\n        \"next\": \"Próximos produtos\",\n        \"previous\": \"Produtos anteriores\",\n        \"noProductsToCompare\": \"Não há produtos para comparar\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Peso\",\n        \"description\": \"Descrição\",\n        \"noDescription\": \"Não há nenhuma descrição disponível.\",\n        \"rating\": \"Taxa\",\n        \"noRatings\": \"Não há avaliações.\",\n        \"otherDetails\": \"Outros detalhes\",\n        \"noOtherDetails\": \"Não há outros detalhes.\",\n        \"viewOptions\": \"Ver opções\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 item} other {# itens}} adicionado(s) <cartLink> ao carrinho</cartLink>\",\n        \"missingCart\": \"Carrinho não encontrado. Tente novamente mais tarde.\",\n        \"unknownError\": \"Erro desconhecido. Tente novamente mais tarde.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Quantidade\",\n            \"increaseQuantity\": \"Aumentar quantidade\",\n            \"decreaseQuantity\": \"Reduzir quantidade\",\n            \"emptySelectPlaceholder\": \"Selecione uma opção\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 item} other {# itens}} adicionado(s) <cartLink> ao carrinho</cartLink>\",\n            \"missingCart\": \"Carrinho não encontrado. Tente novamente mais tarde!\",\n            \"unknownError\": \"Erro desconhecido. Tente novamente mais tarde.\",\n            \"variantRequiredError\": \"Para colocar este produto no carrinho, você precisa selecionar algumas opções.\",\n            \"increaseNumber\": \"Aumentar número\",\n            \"decreaseNumber\": \"Diminuir número\",\n            \"thumbnail\": \"Visualizar número da imagem\",\n            \"additionalInformation\": \"Outras informações\",\n            \"currentStock\": \"{quantity, number} in stock\",\n            \"backorderQuantity\": \"{quantity, number} will be on backorder\",\n            \"loadingMoreImages\": \"Loading more images\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 more image loaded} other {# more images loaded}}\",\n            \"Submit\": {\n                \"addToCart\": \"Adicionar ao carrinho\",\n                \"outOfStock\": \"Fora do estoque\",\n                \"preorder\": \"Pré-venda\",\n                \"unavailable\": \"Indisponível\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Especificações\",\n                \"warranty\": \"Garantia\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Peso\",\n                \"condition\": \"Condição\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Produtos relacionados\",\n            \"noRelatedProducts\": \"Nenhum produto relacionado encontrado\",\n            \"browseCatalog\": \"Experimente conferir nosso catálogo completo de produtos.\",\n            \"cta\": \"Comprar todos\",\n            \"previousProducts\": \"Produtos anteriores\",\n            \"nextProducts\": \"Próximos produtos\",\n            \"scrollbar\": \"Barra de rolagem de produtos relacionados\"\n        },\n        \"Reviews\": {\n            \"title\": \"Avaliações\",\n            \"empty\": \"Nenhuma avaliação foi adicionada para este produto.\",\n            \"previous\": \"Avaliações anteriores\",\n            \"next\": \"Próximas avaliações\",\n            \"Form\": {\n                \"button\": \"Write a review\",\n                \"title\": \"Write a review\",\n                \"submit\": \"Enviar\",\n                \"cancel\": \"Cancelar\",\n                \"ratingLabel\": \"Taxa\",\n                \"titleLabel\": \"Título\",\n                \"reviewLabel\": \"Avaliação\",\n                \"nameLabel\": \"Nome\",\n                \"emailLabel\": \"Email\",\n                \"successMessage\": \"Your review has been submitted successfully!\",\n                \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n                \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Title is required\",\n                    \"authorRequired\": \"O nome é obrigatório\",\n                    \"emailRequired\": \"Email is required\",\n                    \"emailInvalid\": \"Please enter a valid email address\",\n                    \"textRequired\": \"Review is required\",\n                    \"ratingRequired\": \"Rating is required\",\n                    \"ratingTooSmall\": \"Rating must be at least 1\",\n                    \"ratingTooLarge\": \"Rating must be at most 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Página inicial\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Página inicial\",\n            \"Form\": {\n                \"success\": \"Agradecemos o contato. Responderemos em breve.\",\n                \"successCta\": \"Continuar comprando\",\n                \"fullName\": \"Nome completo\",\n                \"companyName\": \"Nome da empresa\",\n                \"phone\": \"Telefone\",\n                \"orderNo\": \"Número do pedido\",\n                \"rma\": \"Número do RMA\",\n                \"email\": \"Email\",\n                \"comments\": \"Comentários/perguntas\",\n                \"cta\": \"Enviar formulário\",\n                \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\",\n                \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"Manutenção\",\n        \"message\": \"Estamos em manutenção\",\n        \"contactUs\": \"Entre em contato conosco em:\"\n    },\n    \"Error\": {\n        \"title\": \"Houve um erro no servidor!\",\n        \"subtitle\": \"Tente novamente mais tarde.\",\n        \"cta\": \"Tente novamente\"\n    },\n    \"NotFound\": {\n        \"title\": \"Não encontramos essa página!\",\n        \"subtitle\": \"Tente fazer uma pesquisa diferente ou volte para a página inicial.\",\n        \"featuredProducts\": \"Produtos em destaque\",\n        \"search\": \"Pesquisar\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Página inicial\",\n            \"toggleNavigation\": \"Alternar navegação\",\n            \"Icons\": {\n                \"account\": \"Perfil\",\n                \"cart\": \"Carrinho\",\n                \"search\": \"Abrir pop-up de pesquisa\",\n                \"giftCertificates\": \"Vales-presente\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Switch currency\",\n                \"invalidCurrency\": \"Moeda inválida\",\n                \"errorUpdatingCurrency\": \"Erro ao atualizar a moeda do seu carrinho. Tente novamente.\"\n            },\n            \"Search\": {\n                \"products\": \"Produtos\",\n                \"categories\": \"Categorias\",\n                \"brands\": \"Marcas\",\n                \"noSearchResultsTitle\": \"Nenhum resultado para “{term}”.\",\n                \"noSearchResultsSubtitle\": \"Experimente outra pesquisa.\",\n                \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente.\",\n                \"inputPlaceholder\": \"Pesquise produtos, categorias, marcas...\",\n                \"submitLabel\": \"Pesquisar\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Página inicial\",\n            \"contactUs\": \"Fale conosco\",\n            \"socialMediaLinks\": \"Links de redes sociais\",\n            \"categories\": \"Categorias\",\n            \"brands\": \"Marcas\",\n            \"navigate\": \"Navegar\",\n            \"giftCertificates\": \"Vales-presente\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Assine nosso boletim informativo\",\n            \"placeholder\": \"Digite seu email\",\n            \"description\": \"Fique por dentro das últimas novidades e ofertas da nossa loja.\",\n            \"subscribedToNewsletter\": \"You have been subscribed to our newsletter!\",\n            \"Errors\": {\n                \"emailRequired\": \"Email is required\",\n                \"invalidEmail\": \"Please enter a valid email address\",\n                \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Reject All\",\n                \"acceptAll\": \"Accept All\",\n                \"customize\": \"Personalizar\",\n                \"save\": \"Salvar configurações\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"We value your privacy\",\n                \"description\": \"This site uses cookies to improve your browsing experience, analyze site traffic, and show personalized content.\",\n                \"privacyPolicy\": \"Política de privacidade\"\n            },\n            \"Dialog\": {\n                \"title\": \"Privacy Settings\",\n                \"description\": \"Customize your privacy settings here. You can choose which types of cookies and tracking technologies you would like to allow.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Strictly Necessary\",\n                    \"description\": \"These cookies are essential for the website to function properly and cannot be disabled.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Functionality\",\n                    \"description\": \"These cookies enable enhanced functionality and personalization of the website.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marketing\",\n                    \"description\": \"These cookies are used to deliver relevant advertisements and track their effectiveness.\"\n                },\n                \"measurement\": {\n                    \"title\": \"Análise\",\n                    \"description\": \"These cookies help us understand how visitors interact with the website and improve its performance.\"\n                },\n                \"experience\": {\n                    \"title\": \"Experiência\",\n                    \"description\": \"These cookies help us provide a better user experience and test new features.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"Original price was {price}.\",\n            \"currentPrice\": \"Current price is {price}.\",\n            \"range\": \"Price from {minValue} to {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Vales-presente\",\n        \"description\": \"Give the perfect gift that never goes out of style. Let friends and loved ones choose exactly what they want from our entire collection.\",\n        \"purchaseLabel\": \"Comprar agora\",\n        \"checkBalanceLabel\": \"Check balance\",\n        \"expiresAtLabel\": \"Valid thru\",\n        \"CheckBalance\": {\n            \"title\": \"Check balance\",\n            \"description\": \"You can check the balance and get the information about your gift certificate by typing the code in the box below.\",\n            \"inputLabel\": \"Código\",\n            \"inputPlaceholder\": \"xxx-xxx-xxx-xxx\",\n            \"purchasedDateLabel\": \"Comprados\",\n            \"senderLabel\": \"De\",\n            \"Errors\": {\n                \"invalidCode\": \"The gift certificate code you entered is invalid. Please check the code and try again.\",\n                \"codeRequired\": \"Insira um código de vale-presente.\",\n                \"somethingWentWrong\": \"Ocorreu um erro. Tente novamente mais tarde.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Purchase a gift certificate\",\n            \"title\": \"Digital gift certificate\",\n            \"description\": \"Explore our gift certificates, perfect for any occasion. Choose the amount and personalize your message.\",\n            \"successMessage\": \"Gift certificate has been added to <cartLink> your cart</cartLink>\",\n            \"missingCart\": \"Carrinho não encontrado. Tente novamente mais tarde.\",\n            \"unknownError\": \"Erro desconhecido. Tente novamente mais tarde.\",\n            \"Form\": {\n                \"amountLabel\": \"Quantidade\",\n                \"customAmountLabel\": \"Amount (between {minAmount} and {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Select an amount\",\n                \"customAmountPlaceholder\": \"Enter custom amount\",\n                \"senderNameLabel\": \"Your name\",\n                \"senderEmailLabel\": \"Your email\",\n                \"recipientNameLabel\": \"Recipient's name\",\n                \"recipientEmailLabel\": \"Recipient's email\",\n                \"namePlaceholder\": \"Enter name\",\n                \"emailPlaceholder\": \"Informe o e-mail\",\n                \"messageLabel\": \"Mensagem\",\n                \"messagePlaceholder\": \"Enter your message (optional)\",\n                \"nonRefundableCheckboxLabel\": \"I agree that Gift Certificates are non-refundable\",\n                \"expiryCheckboxLabel\": \"I acknowledge that this Gift Certificate will expire on {expiryDate}\",\n                \"ctaLabel\": \"Adicionar ao carrinho\",\n                \"Errors\": {\n                    \"amountRequired\": \"Please select or enter a gift certificate amount\",\n                    \"amountInvalid\": \"Please select a valid gift certificate amount\",\n                    \"amountOutOfRange\": \"Please enter an amount between {minAmount} and {maxAmount}\",\n                    \"unexpectedSettingsError\": \"An unexpected error occurred while retrieving gift certificate settings. Please try again later.\",\n                    \"senderNameRequired\": \"Your name is required\",\n                    \"senderEmailRequired\": \"Your email is required\",\n                    \"recipientNameRequired\": \"Recipient's name is required\",\n                    \"recipientEmailRequired\": \"Recipient's email is required\",\n                    \"emailInvalid\": \"Please enter a valid email address\",\n                    \"checkboxRequired\": \"You must check this box to continue\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"Opcional\",\n        \"recaptchaRequired\": \"Please complete the reCAPTCHA verification.\",\n        \"Errors\": {\n            \"invalidInput\": \"Please check your input and try again\",\n            \"invalidFormat\": \"The value entered does not match the required format\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/messages/sv.json",
    "content": "{\n    \"Home\": {\n        \"Slideshow\": {\n            \"Slide01\": {\n                \"title\": \"Fräscha fynd för varje tillfälle\",\n                \"description\": \"Utforska våra senaste produkter, noggrant utvalda för sin stil, funktionalitet och inspiration. Köp nu och upptäck din nästa favorit.\",\n                \"alt\": \"Fem små krukväxter på beige staplade block, med en mängd olika gröna växter i mörkgrå krukor mot en neutral bakgrund.\",\n                \"cta\": \"Köp nu\"\n            },\n            \"Slide02\": {\n                \"title\": \"Upptäck vad som är nytt\",\n                \"description\": \"Shoppa våra senaste produkter och hitta något fräscht och spännande till ditt hem.\",\n                \"alt\": \"Händer som sträcks ut för att hålla en grön ormbunke i en flätad korg med en dekorativ rosett, mot en beige bakgrund med mjuka skuggor.\",\n                \"cta\": \"Köp nu\"\n            },\n            \"Slide03\": {\n                \"title\": \"Något för alla\",\n                \"description\": \"Missa inte de exklusiva erbjudandena på våra bästsäljande produkter. Handla idag och spara stort på dina favoritvaror.\",\n                \"alt\": \"Närbild av ett livfullt grönt blad med perforeringar, som framhäver dess släta struktur och naturliga detaljer.\",\n                \"cta\": \"Köp nu\"\n            }\n        },\n        \"FeaturedProducts\": {\n            \"title\": \"Utvald kollektion\",\n            \"description\": \"Utforska våra toppval i denna utvalda kollektion. Hitta den perfekta presenten eller skäm bort dig själv!\",\n            \"cta\": \"Visa mer\",\n            \"emptyStateTitle\": \"Inga produkter hittades\",\n            \"emptyStateSubtitle\": \"Prova att bläddra i vår kompletta produktkatalog.\"\n        },\n        \"NewestProducts\": {\n            \"title\": \"Nyinkomna\",\n            \"description\": \"Våra senaste produkter finns här. Kolla in vad som är nytt i butiken.\",\n            \"cta\": \"Visa alla\",\n            \"emptyStateTitle\": \"Inga produkter hittades\",\n            \"emptyStateSubtitle\": \"Prova att bläddra i vår kompletta produktkatalog.\",\n            \"previousProducts\": \"Tidigare produkter\",\n            \"nextProducts\": \"Nästa produkter\"\n        }\n    },\n    \"Auth\": {\n        \"ChangePassword\": {\n            \"title\": \"Ändra lösenor\",\n            \"newPassword\": \"Nytt lösenord\",\n            \"confirmPassword\": \"Bekräfta lösenord\",\n            \"passwordUpdated\": \"Lösenordet har uppdaterats!\",\n            \"somethingWentWrong\": \"Någonting gick fel. Vänligen försök igen senare.\",\n            \"FieldErrors\": {\n                \"passwordRequired\": \"Lösenord krävs\",\n                \"passwordTooSmall\": \"Lösenordet måste vara minst {minLength, plural, =1 {1 tecken} other {# tecken}} långt\",\n                \"passwordLowercaseRequired\": \"Lösenordet måste innehålla minst en liten bokstav\",\n                \"passwordUppercaseRequired\": \"Lösenordet måste innehålla minst en stor bokstav\",\n                \"passwordNumberRequired\": \"Lösenordet måste innehålla minst {minNumbers, plural, =1 {ett nummer} other {# nummer}}\",\n                \"passwordSpecialCharacterRequired\": \"Lösenordet måste innehålla minst ett specialtecken\",\n                \"passwordsMustMatch\": \"Lösenorden stämmer inte överens\",\n                \"confirmPasswordRequired\": \"Bekräfta ditt lösenord\"\n            }\n        },\n        \"Login\": {\n            \"title\": \"Logga in\",\n            \"heading\": \"Logga in\",\n            \"forgotPassword\": \"Glömt ditt lösenord?\",\n            \"cta\": \"Logga in\",\n            \"email\": \"E-post\",\n            \"password\": \"Lösenord\",\n            \"invalidCredentials\": \"Din e-postadress eller ditt lösenord är felaktigt. Försök att logga in igen eller återställ ditt lösenord\",\n            \"somethingWentWrong\": \"Någonting gick fel. Vänligen försök igen senare.\",\n            \"passwordResetRequired\": \"Återställning av lösenord krävs. Kontrollera din e-post för instruktioner om hur du återställer ditt lösenord.\",\n            \"invalidToken\": \"Din inloggningslänk är ogiltig eller har löpt ut. Försök logga in igen.\",\n            \"FieldErrors\": {\n                \"emailRequired\": \"E-postadress krävs\",\n                \"emailInvalid\": \"Ange en giltig e-postadress\",\n                \"passwordRequired\": \"Lösenord krävs\",\n                \"invalidInput\": \"Kontrollera din inmatning och försök igen.\"\n            },\n            \"CreateAccount\": {\n                \"title\": \"Nya kunder?\",\n                \"accountBenefits\": \"Skapa ett konto hos oss, så kan du:\",\n                \"fastCheckout\": \"Kolla snabbare\",\n                \"multipleAddresses\": \"Spara flera leveransadresser\",\n                \"ordersHistory\": \"Gå till din orderhistorik\",\n                \"ordersTracking\": \"Spåra nya beställningar\",\n                \"wishlists\": \"Spara objekt i din önskelista\",\n                \"cta\": \"Skapa konto\"\n            },\n            \"ForgotPassword\": {\n                \"title\": \"Glömt ditt lösenord\",\n                \"subtitle\": \"Ange e-postadressen som är kopplad till ditt konto nedan. Vi skickar instruktioner för att återställa ditt lösenord.\",\n                \"confirmResetPassword\": \"Om e-postadressen {email} är kopplad till ett konto i vår butik har vi skickat ett e-postmeddelande om återställning av lösenord. Kontrollera din inkorg och skräppostmapp om du inte ser meddelandet.\",\n                \"somethingWentWrong\": \"Någonting gick fel. Vänligen försök igen senare.\",\n                \"FieldErrors\": {\n                    \"emailRequired\": \"E-postadress krävs\",\n                    \"emailInvalid\": \"Ange en giltig e-postadress\"\n                }\n            }\n        },\n        \"Register\": {\n            \"title\": \"Registrera konto\",\n            \"heading\": \"Nytt konto\",\n            \"cta\": \"Skapa konto\",\n            \"somethingWentWrong\": \"Någonting gick fel. Vänligen försök igen senare.\",\n            \"recaptchaRequired\": \"Slutför reCAPTCHA-verifieringen.\",\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Förnamn krävs\",\n                \"lastNameRequired\": \"Efternamn krävs\",\n                \"emailRequired\": \"E-postadress krävs\",\n                \"emailInvalid\": \"Ange en giltig e-postadress\",\n                \"passwordRequired\": \"Lösenord krävs\",\n                \"passwordTooSmall\": \"Lösenordet måste vara minst {minLength, plural, =1 {1 tecken} other {# tecken}} långt\",\n                \"passwordLowercaseRequired\": \"Lösenordet måste innehålla minst en liten bokstav\",\n                \"passwordUppercaseRequired\": \"Lösenordet måste innehålla minst en stor bokstav\",\n                \"passwordNumberRequired\": \"Lösenordet måste innehålla minst {minNumbers, plural, =1 {ett nummer} other {# nummer}}\",\n                \"passwordSpecialCharacterRequired\": \"Lösenordet måste innehålla minst ett specialtecken\",\n                \"passwordsMustMatch\": \"Lösenorden stämmer inte överens\",\n                \"addressLine1Required\": \"Adresslinje 1 krävs\",\n                \"cityRequired\": \"Stad krävs\",\n                \"countryRequired\": \"Land krävs\",\n                \"stateRequired\": \"Stat/provins krävs\",\n                \"postalCodeRequired\": \"Postnummer krävs\"\n            }\n        }\n    },\n    \"Faceted\": {\n        \"Brand\": {\n            \"Empty\": {\n                \"title\": \"Inga produkter i detta varumärke\",\n                \"subtitle\": \"Prova att använda olika filter.\"\n            }\n        },\n        \"Category\": {\n            \"subCategories\": \"Kategorier\",\n            \"Empty\": {\n                \"title\": \"Inga produkter i denna kategori\",\n                \"subtitle\": \"Prova att använda olika filter.\"\n            }\n        },\n        \"Search\": {\n            \"title\": \"Sökresultat\",\n            \"searchResults\": \"Sökresultat för\",\n            \"subCategories\": \"Kategorier\",\n            \"Breadcrumbs\": {\n                \"home\": \"Hem\",\n                \"search\": \"Sök\"\n            },\n            \"Empty\": {\n                \"title\": \"Tyvärr finns inga resultat för ”{term}”.\",\n                \"subtitle\": \"Försök med en ny sökning.\"\n            }\n        },\n        \"FacetedSearch\": {\n            \"filters\": \"Filter\",\n            \"resetFilters\": \"Återställ filtren\",\n            \"Range\": {\n                \"apply\": \"Tillämpa\"\n            },\n            \"Facets\": {\n                \"freeShippingLabel\": \"Gratis frakt\",\n                \"isFeaturedLabel\": \"Är utvald\",\n                \"inStockLabel\": \"I lager\"\n            }\n        },\n        \"SortBy\": {\n            \"sortBy\": \"Sortera efter:\",\n            \"featuredItems\": \"Erbjudna artiklar\",\n            \"bestSellingItems\": \"Bästsäljande artiklar\",\n            \"newestItems\": \"Nyaste artiklarna\",\n            \"aToZ\": \"A till Z\",\n            \"zToA\": \"Z till A\",\n            \"byReview\": \"Genom granskning\",\n            \"priceAscending\": \"Pris: stigande\",\n            \"priceDescending\": \"Pris: fallande\",\n            \"relevance\": \"Relevans\"\n        },\n        \"Compare\": {\n            \"compare\": \"Jämför\",\n            \"remove\": \"Ta bort\",\n            \"maxCompareLimit\": \"Du har nått det maximala antalet produkter att jämföra. Ta bort en produkt för att lägga till en ny.\"\n        }\n    },\n    \"Account\": {\n        \"Layout\": {\n            \"addresses\": \"Adresser\",\n            \"logout\": \"Logga ut\",\n            \"orders\": \"Beställningar\",\n            \"settings\": \"Konto\",\n            \"wishlists\": \"Önskelistor\"\n        },\n        \"Orders\": {\n            \"title\": \"Beställningar\",\n            \"orderNumber\": \"Beställningsnummer\",\n            \"totalPrice\": \"Total\",\n            \"viewDetails\": \"Visa detaljer\",\n            \"EmptyState\": {\n                \"title\": \"Du har inga beställningar\",\n                \"cta\": \"Köp nu\"\n            },\n            \"Details\": {\n                \"title\": \"Beställningsnummer {orderNumber}\",\n                \"shippingAddress\": \"Leveransadress\",\n                \"shippingMethod\": \"Fraktmetod:\",\n                \"summaryTotal\": \"Total\",\n                \"destination\": \"Destination\",\n                \"destinationWithCount\": \"Destination {number, number}/{total, number}\",\n                \"digitalDelivery\": \"Digital leverans till {email}\",\n                \"subtotal\": \"Delsumma\",\n                \"shipping\": \"Frakt\",\n                \"tax\": \"Beskatta\",\n                \"orderSummary\": \"Beställningssammanfattning\",\n                \"paymentMethodsLabel\": \"{count, plural, =1 {Betalningsmetod} other {Betalningsmetoder}}\",\n                \"paymentEndingInLabel\": \"Slutar på\",\n                \"PaymentMethods\": {\n                    \"creditCard\": \"Kreditkort\",\n                    \"giftCertificate\": \"Presentkort\",\n                    \"storeCredit\": \"Butikskredit\",\n                    \"other\": \"Övrigt\"\n                }\n            }\n        },\n        \"Addresses\": {\n            \"title\": \"Adresser\",\n            \"cta\": \"Lägg till adress\",\n            \"edit\": \"Redigera\",\n            \"delete\": \"Radera\",\n            \"cancel\": \"Annullera\",\n            \"create\": \"skapa\",\n            \"update\": \"Uppdatering\",\n            \"setDefault\": \"Ange som standard\",\n            \"somethingWentWrong\": \"Någonting gick fel. Vänligen försök igen senare.\",\n            \"EmptyState\": {\n                \"title\": \"Du har inga adresser\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Förnamn krävs\",\n                \"lastNameRequired\": \"Efternamn krävs\",\n                \"addressLine1Required\": \"Adresslinje 1 krävs\",\n                \"cityRequired\": \"Stad krävs\",\n                \"countryRequired\": \"Land krävs\",\n                \"stateRequired\": \"Stat/provins krävs\",\n                \"postalCodeRequired\": \"Postnummer krävs\"\n            }\n        },\n        \"Settings\": {\n            \"title\": \"Kontoinställningar\",\n            \"changePassword\": \"Ändra lösenor\",\n            \"passwordUpdated\": \"Lösenordet har uppdaterats!\",\n            \"settingsUpdated\": \"Kontoinställningarna har uppdaterats!\",\n            \"somethingWentWrong\": \"Någonting gick fel. Vänligen försök igen senare.\",\n            \"currentPassword\": \"Nuvarande lösenord\",\n            \"newPassword\": \"Nytt lösenord\",\n            \"confirmPassword\": \"Bekräfta lösenord\",\n            \"cta\": \"Uppdatering\",\n            \"NewsletterSubscription\": {\n                \"title\": \"Marknadsföringsinställningar\",\n                \"label\": \"Prenumerera på vårt nyhetsbrev.\",\n                \"marketingPreferencesUpdated\": \"Marknadsföringsinställningarna har uppdaterats framgångsrikt!\",\n                \"somethingWentWrong\": \"Någonting gick fel. Vänligen försök igen senare.\"\n            },\n            \"FieldErrors\": {\n                \"firstNameRequired\": \"Förnamn krävs\",\n                \"firstNameTooSmall\": \"Förnamnet måste vara minst 2 tecken långt\",\n                \"lastNameRequired\": \"Efternamn krävs\",\n                \"lastNameTooSmall\": \"Efternamnet måste vara minst två tecken långt\",\n                \"emailRequired\": \"E-postadress krävs\",\n                \"emailInvalid\": \"Ange en giltig e-postadress\",\n                \"currentPasswordRequired\": \"Nuvarande lösenord krävs\",\n                \"passwordRequired\": \"Lösenord krävs\",\n                \"passwordTooSmall\": \"Lösenordet måste vara minst {minLength, plural, =1 {1 tecken} other {# tecken}} långt\",\n                \"passwordLowercaseRequired\": \"Lösenordet måste innehålla minst en liten bokstav\",\n                \"passwordUppercaseRequired\": \"Lösenordet måste innehålla minst en stor bokstav\",\n                \"passwordNumberRequired\": \"Lösenordet måste innehålla minst {minNumbers, plural, =1 {ett nummer} other {# nummer}}\",\n                \"passwordSpecialCharacterRequired\": \"Lösenordet måste innehålla minst ett specialtecken\",\n                \"passwordsMustMatch\": \"Lösenorden stämmer inte överens\",\n                \"confirmPasswordRequired\": \"Bekräfta ditt lösenord\"\n            }\n        }\n    },\n    \"Wishlist\": {\n        \"actionsTitle\": \"Åtgärder för önskelista\",\n        \"title\": \"Önskelistor\",\n        \"new\": \"Ny önskelista\",\n        \"items\": \"{count, plural, =1 {1 objekt} other {# objekt}}\",\n        \"viewWishlist\": \"Visa lista\",\n        \"noWishlists\": \"Du har inga önskelistor\",\n        \"noWishlistsCallToAction\": \"Skapa en önskelista\",\n        \"emptyWishlist\": \"Du har inte lagt till några produkter på den här önskelistan.\",\n        \"share\": \"Dela med sig\",\n        \"shareSuccess\": \"Önskelistan har delats.\",\n        \"shareCopied\": \"Önskelistans offentliga URL har kopierats till urklipp.\",\n        \"shareDisabled\": \"Din önskelista måste vara offentlig för att kunna delas.\",\n        \"makePublic\": \"Gör offentlig\",\n        \"makePrivate\": \"Gör privat\",\n        \"rename\": \"Döp om\",\n        \"delete\": \"Radera\",\n        \"removeButtonTitle\": \"Radera produkten från önskelistan\",\n        \"Visibility\": {\n            \"public\": \"Offentlig\",\n            \"private\": \"Privat\"\n        },\n        \"Modal\": {\n            \"cancel\": \"Annullera\",\n            \"close\": \"Stäng\",\n            \"copy\": \"Kopiera\",\n            \"create\": \"skapa\",\n            \"save\": \"Spara\",\n            \"delete\": \"Radera\",\n            \"newTitle\": \"Skapa en ny önskelista\",\n            \"shareTitle\": \"Dela {name}\",\n            \"renameTitle\": \"Byt namn på {name}\",\n            \"deleteTitle\": \"Radera {name}\",\n            \"changeVisibilityPublicTitle\": \"Vill du göra {name} offentlig?\",\n            \"changeVisibilityPrivateTitle\": \"Vill du göra {name} privat?\",\n            \"makePublicContent\": \"Är du säker på att du vill göra <bold>{name}</bold> offentlig? Då kommer andra att kunna se din önskelista om de har länken.\",\n            \"makePrivateContent\": \"Är du säker på att du vill göra <bold>{name}</bold> privat? Om du har delat din önskelista med andra kommer de inte längre att kunna se den.\",\n            \"deleteContent\": \"Är du säker på att du vill radera <bold>{name}</bold>? Denna åtgärd kan inte ångras.\"\n        },\n        \"Form\": {\n            \"nameLabel\": \"Namn\"\n        },\n        \"Errors\": {\n            \"nameRequired\": \"Namnet på önskelistan får inte vara tomt.\",\n            \"updateFailed\": \"Det gick inte att uppdatera din önskelista. Försök igen.\",\n            \"deleteFailed\": \"Det gick inte att radera din önskelista. Försök igen.\",\n            \"removeProductFailed\": \"Det gick inte att ta bort produkten från din önskelista. Försök igen.\",\n            \"unauthorized\": \"Du har inte behörighet att utföra den här åtgärden. Logga in och försök igen.\",\n            \"unexpected\": \"Ett oväntat fel inträffade. Försök igen.\"\n        },\n        \"Result\": {\n            \"createSuccess\": \"Önskelistan har skapats.\",\n            \"updateSuccess\": \"Önskelistan har uppdaterats.\",\n            \"deleteSuccess\": \"Önskelistan har tagits bort.\",\n            \"removeItemSuccess\": \"Objektet har tagits bort från din önskelista.\"\n        },\n        \"Button\": {\n            \"label\": \"Lägg till i önskelista\",\n            \"addToNewWishlist\": \"Lägg till i en ny önskelista\",\n            \"defaultWishlistName\": \"Min önskelista\",\n            \"addSuccessMessage\": \"Produkten har lagts till i din önskelista.\",\n            \"removeSuccessMessage\": \"Produkten har tagits bort från din önskelista.\",\n            \"Errors\": {\n                \"addProductFailed\": \"Det gick inte att lägga till produkten från din önskelista. Försök igen.\",\n                \"removeProductFailed\": \"Det gick inte att ta bort produkten från din önskelista. Försök igen.\"\n            }\n        }\n    },\n    \"PublicWishlist\": {\n        \"title\": \"Offentlig önskelista\",\n        \"defaultName\": \"Offentlig önskelista\",\n        \"emptyWishlist\": \"Det finns inga produkter i den här önskelistan ännu.\"\n    },\n    \"Blog\": {\n        \"title\": \"Blogg\",\n        \"home\": \"Hem\",\n        \"Empty\": {\n            \"title\": \"Inga blogginlägg hittades\",\n            \"subtitle\": \"Kom tillbaka senare för mer innehåll\"\n        },\n        \"SharingLinks\": {\n            \"share\": \"Dela med sig\",\n            \"email\": \"E-post\",\n            \"print\": \"Skriva ut\"\n        }\n    },\n    \"Cart\": {\n        \"title\": \"Kundvagn\",\n        \"heading\": \"Din vagn\",\n        \"proceedToCheckout\": \"Fortsätt till utcheckningen\",\n        \"increment\": \"Öka kvantiteten\",\n        \"decrement\": \"Minska kvantitet\",\n        \"removeItem\": \"Ta bort vara\",\n        \"cartCombined\": \"Vi märkte att du hade varor sparade i en tidigare kundvagn, så vi har lagt till dem i din nuvarande kundvagn åt dig.\",\n        \"cartRestored\": \"Du startade en kundvagn på en annan enhet, och vi har återställt den här så att du kan fortsätta där du slutade.\",\n        \"cartUpdateInProgress\": \"Du har en pågående kundvagnsuppdatering. Är du säker på att du vill lämna den här sidan? Dina ändringar kan gå förlorade.\",\n        \"originalPrice\": \"Ursprungligt pris var {price}.\",\n        \"currentPrice\": \"Nuvarande pris är {price}.\",\n        \"quantityReadyToShip\": \"{quantity, number} klara för leverans\",\n        \"quantityOnBackorder\": \"{quantity, number} kommer att vara restnoterade\",\n        \"partiallyAvailable\": \"Endast {quantity, number} tillgängligt\",\n        \"CheckoutSummary\": {\n            \"title\": \"Summering\",\n            \"subTotal\": \"Delsumma\",\n            \"discounts\": \"Rabatter\",\n            \"tax\": \"Beskatta\",\n            \"total\": \"Total\",\n            \"CouponCode\": {\n                \"apply\": \"Tillämpa\",\n                \"couponCode\": \"Kupongkod\",\n                \"removeCouponCode\": \"Ta bort kupongkod\",\n                \"invalidCouponCode\": \"Ange en giltig kupongkod\",\n                \"cartNotFound\": \"Ett fel uppstod när er varukorg hämtades\"\n            },\n            \"Shipping\": {\n                \"shipping\": \"Frakt\",\n                \"add\": \"Lägg till\",\n                \"change\": \"Ändra\",\n                \"cancel\": \"Annullera\",\n                \"country\": \"Land\",\n                \"city\": \"Stad\",\n                \"state\": \"Stat/provins\",\n                \"postalCode\": \"Postnummer\",\n                \"updatedShippingOptions\": \"Uppdatera fraktalternativ\",\n                \"viewShippingOptions\": \"Visa fraktalternativ\",\n                \"editAddress\": \"Redigera adress\",\n                \"shippingOptions\": \"Frakt alternativ\",\n                \"updateShipping\": \"Uppdatera frakt\",\n                \"addShipping\": \"Lägg till frakt\",\n                \"cartNotFound\": \"Ett fel uppstod när er varukorg hämtades\",\n                \"noShippingOptions\": \"Det finns inga tillgängliga fraktalternativ för din adress\",\n                \"countryRequired\": \"Land krävs\"\n            }\n        },\n        \"GiftCertificate\": {\n            \"giftCertificate\": \"Presentkort\",\n            \"giftCertificateCode\": \"Presentkort kod\",\n            \"removeGiftCertificate\": \"Ta bort presentkortet\",\n            \"apply\": \"Tillämpa\",\n            \"to\": \"Till\",\n            \"message\": \"Meddelande\",\n            \"invalidGiftCertificate\": \"Ange en giltig presentkortskod\",\n            \"cartNotFound\": \"Ett fel uppstod när er varukorg hämtades\"\n        },\n        \"Empty\": {\n            \"title\": \"Din kundvagn är tom.\",\n            \"subtitle\": \"Lägg till några produkter för att komma igång.\",\n            \"cta\": \"Fortsätt att handla\"\n        },\n        \"Errors\": {\n            \"cartNotFound\": \"Ett fel uppstod när er varukorg hämtades\",\n            \"lineItemNotFound\": \"Radartikeln hittades inte.\",\n            \"failedToUpdateQuantity\": \"Det gick inte att uppdatera antalet.\",\n            \"somethingWentWrong\": \"Någonting gick fel. Vänligen försök igen senare.\"\n        }\n    },\n    \"Compare\": {\n        \"title\": \"Jämför produkter\",\n        \"addToCart\": \"Lägg till i kundvagn\",\n        \"next\": \"Nästa produkter\",\n        \"previous\": \"Tidigare produkter\",\n        \"noProductsToCompare\": \"Inga produkter att jämföra\",\n        \"sku\": \"SKU\",\n        \"weight\": \"Vikt\",\n        \"description\": \"Beskrivning\",\n        \"noDescription\": \"Det finns ingen beskrivning tillgänglig.\",\n        \"rating\": \"Bedömning\",\n        \"noRatings\": \"Det finns inga recensioner.\",\n        \"otherDetails\": \"Övrig information\",\n        \"noOtherDetails\": \"Det finns inga övriga detaljer.\",\n        \"viewOptions\": \"Visa alternativ\",\n        \"successMessage\": \"{cartItems, plural, =1 {1 Item} other {# Items}} har lagts till <cartLink> i kundvagnen</cartLink>\",\n        \"missingCart\": \"Kundvagnen hittades inte. Försök igen senare.\",\n        \"unknownError\": \"Okänt fel. Försök igen senare.\"\n    },\n    \"Product\": {\n        \"ProductDetails\": {\n            \"quantity\": \"Kvantitet\",\n            \"increaseQuantity\": \"Öka kvantiteten\",\n            \"decreaseQuantity\": \"Minska kvantitet\",\n            \"emptySelectPlaceholder\": \"Välj ett alternativ\",\n            \"successMessage\": \"{cartItems, plural, =1 {1 Item} other {# Items}} har lagts till <cartLink> i kundvagnen</cartLink>\",\n            \"missingCart\": \"Kundvagnen hittades inte. Försök igen senare!\",\n            \"unknownError\": \"Okänt fel. Försök igen senare.\",\n            \"variantRequiredError\": \"Denna produkt kräver att alternativ väljs för att kunna läggas till i kundvagnen.\",\n            \"increaseNumber\": \"Öka antalet\",\n            \"decreaseNumber\": \"Minska antalet\",\n            \"thumbnail\": \"Visa bildnummer\",\n            \"additionalInformation\": \"Ytterligare information\",\n            \"currentStock\": \"{quantity, number} i lager\",\n            \"backorderQuantity\": \"{quantity, number} kommer att vara restnoterade\",\n            \"loadingMoreImages\": \"Laddar fler bilder\",\n            \"imagesLoaded\": \"{count, plural, =1 {1 till bild har laddats} other {# till bilder har laddats}}\",\n            \"Submit\": {\n                \"addToCart\": \"Lägg till i kundvagn\",\n                \"outOfStock\": \"Ej i lager\",\n                \"preorder\": \"Förboka\",\n                \"unavailable\": \"Inte tillgänglig\"\n            },\n            \"Accordions\": {\n                \"specifications\": \"Specifikationer\",\n                \"warranty\": \"Garanti\",\n                \"sku\": \"SKU\",\n                \"weight\": \"Vikt\",\n                \"condition\": \"Skick\"\n            }\n        },\n        \"RelatedProducts\": {\n            \"title\": \"Relaterade produkter\",\n            \"noRelatedProducts\": \"Inga relaterade produkter hittades\",\n            \"browseCatalog\": \"Prova att bläddra i vår kompletta produktkatalog.\",\n            \"cta\": \"Handla alla\",\n            \"previousProducts\": \"Tidigare produkter\",\n            \"nextProducts\": \"Nästa produkter\",\n            \"scrollbar\": \"Rullningslist med relaterade produkter\"\n        },\n        \"Reviews\": {\n            \"title\": \"Recensioner\",\n            \"empty\": \"Inga recensioner har lagts till för denna produkt.\",\n            \"previous\": \"Tidigare recensioner\",\n            \"next\": \"Kommande recensioner\",\n            \"Form\": {\n                \"button\": \"Skriva en recension\",\n                \"title\": \"Skriva en recension\",\n                \"submit\": \"Skicka in\",\n                \"cancel\": \"Annullera\",\n                \"ratingLabel\": \"Bedömning\",\n                \"titleLabel\": \"Rubrik\",\n                \"reviewLabel\": \"Recension\",\n                \"nameLabel\": \"Namn\",\n                \"emailLabel\": \"E-post\",\n                \"successMessage\": \"Din recension har skickats in!\",\n                \"somethingWentWrong\": \"Någonting gick fel. Vänligen försök igen senare.\",\n                \"recaptchaRequired\": \"Slutför reCAPTCHA-verifieringen.\",\n                \"FieldErrors\": {\n                    \"titleRequired\": \"Rubrik krävs\",\n                    \"authorRequired\": \"Namn krävs\",\n                    \"emailRequired\": \"E-postadress krävs\",\n                    \"emailInvalid\": \"Ange en giltig e-postadress\",\n                    \"textRequired\": \"Granskning krävs\",\n                    \"ratingRequired\": \"Betyg krävs\",\n                    \"ratingTooSmall\": \"Betyget måste vara minst 1\",\n                    \"ratingTooLarge\": \"Betyget får vara högst 5\"\n                }\n            }\n        }\n    },\n    \"WebPages\": {\n        \"Normal\": {\n            \"home\": \"Hem\"\n        },\n        \"ContactUs\": {\n            \"home\": \"Hem\",\n            \"Form\": {\n                \"success\": \"Tack för att du hörde av dig. Vi återkommer till dig inom kort.\",\n                \"successCta\": \"Fortsätt att handla\",\n                \"fullName\": \"Fullständigt namn\",\n                \"companyName\": \"Företagsnamn\",\n                \"phone\": \"Telefon\",\n                \"orderNo\": \"Beställningsnummer:\",\n                \"rma\": \"RMA-nummer\",\n                \"email\": \"E-post\",\n                \"comments\": \"Kommentarer/frågor\",\n                \"cta\": \"Skicka formulär\",\n                \"somethingWentWrong\": \"Någonting gick fel. Vänligen försök igen senare.\",\n                \"recaptchaRequired\": \"Slutför reCAPTCHA-verifieringen.\"\n            }\n        }\n    },\n    \"Maintenance\": {\n        \"title\": \"Underhåll\",\n        \"message\": \"Vi är nere för underhåll\",\n        \"contactUs\": \"Kontakta oss på:\"\n    },\n    \"Error\": {\n        \"title\": \"Det uppstod ett serverfel!\",\n        \"subtitle\": \"Försök igen senare.\",\n        \"cta\": \"Försök igen\"\n    },\n    \"NotFound\": {\n        \"title\": \"Vi kunde inte hitta den sidan!\",\n        \"subtitle\": \"Prova att söka efter något annat eller gå tillbaka till startsidan.\",\n        \"featuredProducts\": \"Utvalda produkter\",\n        \"search\": \"Sök\"\n    },\n    \"Components\": {\n        \"Header\": {\n            \"home\": \"Hem\",\n            \"toggleNavigation\": \"Växla navigering\",\n            \"Icons\": {\n                \"account\": \"Profil\",\n                \"cart\": \"Kundvagn\",\n                \"search\": \"Öppna popup-fönstret för sökning\",\n                \"giftCertificates\": \"Presentkort\"\n            },\n            \"SwitchCurrency\": {\n                \"label\": \"Byt valuta\",\n                \"invalidCurrency\": \"Ogiltig valuta\",\n                \"errorUpdatingCurrency\": \"Det gick inte att uppdatera valutan för din kundvagn. Försök igen.\"\n            },\n            \"Search\": {\n                \"products\": \"Produkter\",\n                \"categories\": \"Kategorier\",\n                \"brands\": \"Varumärke\",\n                \"noSearchResultsTitle\": \"Tyvärr finns inga resultat för ”{term}”.\",\n                \"noSearchResultsSubtitle\": \"Försök med en ny sökning.\",\n                \"somethingWentWrong\": \"Något gick fel. Var god försök igen.\",\n                \"inputPlaceholder\": \"Sök efter produkter, kategorier, varumärken...\",\n                \"submitLabel\": \"Sök\"\n            }\n        },\n        \"Footer\": {\n            \"home\": \"Hem\",\n            \"contactUs\": \"Kontakta oss\",\n            \"socialMediaLinks\": \"Sociala Media-länkar\",\n            \"categories\": \"Kategorier\",\n            \"brands\": \"Varumärke\",\n            \"navigate\": \"Navigera\",\n            \"giftCertificates\": \"Presentkort\"\n        },\n        \"Subscribe\": {\n            \"title\": \"Anmäl dig till vårt nyhetsbrev\",\n            \"placeholder\": \"Ange din e-postadress\",\n            \"description\": \"Håll dig uppdaterad med de senaste nyheterna och erbjudandena från vår butik.\",\n            \"subscribedToNewsletter\": \"Du prenumererar nu på vårt nyhetsbrev!\",\n            \"Errors\": {\n                \"emailRequired\": \"E-postadress krävs\",\n                \"invalidEmail\": \"Ange en giltig e-postadress\",\n                \"somethingWentWrong\": \"Någonting gick fel. Vänligen försök igen senare.\"\n            }\n        },\n        \"ConsentManager\": {\n            \"Common\": {\n                \"rejectAll\": \"Avvisa allt\",\n                \"acceptAll\": \"Godkänn alla\",\n                \"customize\": \"Anpassa\",\n                \"save\": \"Spara inställningar\"\n            },\n            \"CookieBanner\": {\n                \"title\": \"Vi värdesätter din integritet\",\n                \"description\": \"Denna webbplats använder cookies för att förbättra din surfupplevelse, analysera webbplatstrafik och visa personligt innehåll.\",\n                \"privacyPolicy\": \"Sekretesspolicy\"\n            },\n            \"Dialog\": {\n                \"title\": \"Sekretessinställningar\",\n                \"description\": \"Anpassa dina sekretessinställningar här. Du kan välja vilka typer av cookies och spårningstekniker du vill tillåta.\"\n            },\n            \"ConsentTypes\": {\n                \"necessary\": {\n                    \"title\": \"Strikt nödvändigt\",\n                    \"description\": \"Dessa cookies är viktiga för att webbplatsen ska fungera korrekt och kan inte inaktiveras.\"\n                },\n                \"functionality\": {\n                    \"title\": \"Funktionalitet\",\n                    \"description\": \"Dessa cookies möjliggör förbättrad funktionalitet och personalisering av webbplatsen.\"\n                },\n                \"marketing\": {\n                    \"title\": \"Marknadsföring\",\n                    \"description\": \"Dessa cookies används för att leverera relevanta annonser och spåra deras effektivitet.\"\n                },\n                \"measurement\": {\n                    \"title\": \"Analytics\",\n                    \"description\": \"Dessa cookies hjälper oss att förstå hur besökare interagerar med webbplatsen och förbättra dess prestanda.\"\n                },\n                \"experience\": {\n                    \"title\": \"Erfarenhet\",\n                    \"description\": \"Dessa cookies hjälper oss att ge en bättre användarupplevelse och testa nya funktioner.\"\n                }\n            }\n        },\n        \"Price\": {\n            \"originalPrice\": \"Ursprungligt pris var {price}.\",\n            \"currentPrice\": \"Nuvarande pris är {price}.\",\n            \"range\": \"Pris från {minValue} till {maxValue}.\"\n        }\n    },\n    \"GiftCertificates\": {\n        \"title\": \"Presentkort\",\n        \"description\": \"Ge den perfekta gåvan som aldrig går ur stil. Låt vänner och nära och kära välja exakt vad de vill ha från hela vår kollektion.\",\n        \"purchaseLabel\": \"Köp nu\",\n        \"checkBalanceLabel\": \"Kontrollera saldot\",\n        \"expiresAtLabel\": \"Giltigt till och med\",\n        \"CheckBalance\": {\n            \"title\": \"Kontrollera saldot\",\n            \"description\": \"Du kan kontrollera saldot och få information om ditt presentkort genom att skriva in koden i rutan nedan.\",\n            \"inputLabel\": \"Kod\",\n            \"inputPlaceholder\": \"XXX-XXX-XXX-XXX\",\n            \"purchasedDateLabel\": \"Köpt\",\n            \"senderLabel\": \"Från\",\n            \"Errors\": {\n                \"invalidCode\": \"Presentkortskoden du angav är ogiltig. Var god kontrollera koden och försök igen.\",\n                \"codeRequired\": \"Ange en presentkortskod.\",\n                \"somethingWentWrong\": \"Någonting gick fel. Vänligen försök igen senare.\"\n            }\n        },\n        \"Purchase\": {\n            \"breadcrumbTitle\": \"Köp presentkort\",\n            \"title\": \"Digitalt presentkort\",\n            \"description\": \"Utforska våra presentkort, perfekta för alla tillfällen. Välj ett belopp och anpassa ditt meddelande.\",\n            \"successMessage\": \"Presentkortet har lagts till i <cartLink> er varukorg</cartLink>\",\n            \"missingCart\": \"Kundvagnen hittades inte. Försök igen senare.\",\n            \"unknownError\": \"Okänt fel. Försök igen senare.\",\n            \"Form\": {\n                \"amountLabel\": \"Belopp\",\n                \"customAmountLabel\": \"Belopp (mellan {minAmount} och {maxAmount})\",\n                \"selectAmountPlaceholder\": \"Välj ett belopp\",\n                \"customAmountPlaceholder\": \"Ange ett anpassat belopp\",\n                \"senderNameLabel\": \"Ditt namn\",\n                \"senderEmailLabel\": \"Din E-post\",\n                \"recipientNameLabel\": \"Mottagarens namn\",\n                \"recipientEmailLabel\": \"Mottagarens e-postadress\",\n                \"namePlaceholder\": \"Ange ett namn\",\n                \"emailPlaceholder\": \"Ange e-postadress\",\n                \"messageLabel\": \"Meddelande\",\n                \"messagePlaceholder\": \"Ange ert meddelande (valfritt)\",\n                \"nonRefundableCheckboxLabel\": \"Jag godkänner att presentkort inte återbetalas\",\n                \"expiryCheckboxLabel\": \"Jag bekräftar att detta presentkort kommer att upphöra att gälla den {expiryDate}\",\n                \"ctaLabel\": \"Lägg till i kundvagn\",\n                \"Errors\": {\n                    \"amountRequired\": \"Var god välj eller ange ett presentkortsbelopp\",\n                    \"amountInvalid\": \"Välj ett giltigt presentkortsbelopp.\",\n                    \"amountOutOfRange\": \"Ange ett belopp mellan {minAmount} och {maxAmount}\",\n                    \"unexpectedSettingsError\": \"Ett oväntat fel inträffade när presentkortets inställningar hämtades. Försök igen senare.\",\n                    \"senderNameRequired\": \"Ditt namn krävs\",\n                    \"senderEmailRequired\": \"Din e-postadress krävs\",\n                    \"recipientNameRequired\": \"Mottagarens namn är obligatoriskt\",\n                    \"recipientEmailRequired\": \"Mottagarens e-postadress krävs\",\n                    \"emailInvalid\": \"Ange en giltig e-postadress\",\n                    \"checkboxRequired\": \"Du måste markera den här rutan för att fortsätta\"\n                }\n            }\n        }\n    },\n    \"Form\": {\n        \"optional\": \"frivillig\",\n        \"recaptchaRequired\": \"Slutför reCAPTCHA-verifieringen.\",\n        \"Errors\": {\n            \"invalidInput\": \"Kontrollera det som angetts och försök igen\",\n            \"invalidFormat\": \"Det inmatade värdet stämmer inte överens med det format som krävs\"\n        }\n    }\n}\n"
  },
  {
    "path": "core/next.config.ts",
    "content": "import bundleAnalyzer from '@next/bundle-analyzer';\nimport type { NextConfig } from 'next';\nimport createNextIntlPlugin from 'next-intl/plugin';\n\nimport { writeBuildConfig } from './build-config/writer';\nimport { client } from './client';\nimport { graphql } from './client/graphql';\nimport { cspHeader } from './lib/content-security-policy';\n\nconst withNextIntl = createNextIntlPlugin({\n  experimental: {\n    createMessagesDeclaration: './messages/en.json',\n  },\n});\n\nconst SettingsQuery = graphql(`\n  query SettingsQuery {\n    site {\n      settings {\n        url {\n          vanityUrl\n          cdnUrl\n          checkoutUrl\n        }\n        locales {\n          code\n          isDefault\n        }\n      }\n    }\n  }\n`);\n\nasync function writeSettingsToBuildConfig() {\n  const { data } = await client.fetch({ document: SettingsQuery });\n\n  const cdnEnvHostnames = process.env.NEXT_PUBLIC_BIGCOMMERCE_CDN_HOSTNAME;\n\n  const cdnUrls = (\n    cdnEnvHostnames\n      ? cdnEnvHostnames.split(',').map((s) => s.trim())\n      : [data.site.settings?.url.cdnUrl]\n  ).filter((url): url is string => !!url);\n\n  if (!cdnUrls.length) {\n    throw new Error(\n      'No CDN URLs found. Please ensure that NEXT_PUBLIC_BIGCOMMERCE_CDN_HOSTNAME is set correctly.',\n    );\n  }\n\n  return await writeBuildConfig({\n    locales: data.site.settings?.locales,\n    urls: {\n      ...data.site.settings?.url,\n      cdnUrls,\n    },\n  });\n}\n\nexport default async (): Promise<NextConfig> => {\n  const settings = await writeSettingsToBuildConfig();\n\n  let nextConfig: NextConfig = {\n    reactStrictMode: true,\n    experimental: {\n      optimizePackageImports: ['@icons-pack/react-simple-icons'],\n    },\n    typescript: {\n      ignoreBuildErrors: !!process.env.CI,\n    },\n    // default URL generation in BigCommerce uses trailing slash\n    trailingSlash: process.env.TRAILING_SLASH !== 'false',\n    // eslint-disable-next-line @typescript-eslint/require-await\n    async headers() {\n      const cdnLinks = settings.urls.cdnUrls.map((url) => ({\n        key: 'Link',\n        value: `<https://${url}>; rel=preconnect`,\n      }));\n\n      return [\n        {\n          source: '/(.*)',\n          headers: [\n            {\n              key: 'Content-Security-Policy',\n              value: cspHeader.replace(/\\n/g, ''),\n            },\n            ...cdnLinks,\n          ],\n        },\n      ];\n    },\n  };\n\n  // Apply withNextIntl to the config\n  nextConfig = withNextIntl(nextConfig);\n\n  if (process.env.ANALYZE === 'true') {\n    const withBundleAnalyzer = bundleAnalyzer();\n\n    nextConfig = withBundleAnalyzer(nextConfig);\n  }\n\n  return nextConfig;\n};\n"
  },
  {
    "path": "core/package.json",
    "content": "{\n  \"name\": \"@bigcommerce/catalyst-core\",\n  \"description\": \"BigCommerce Catalyst is a Next.js starter kit for building headless BigCommerce storefronts.\",\n  \"version\": \"1.6.2\",\n  \"private\": true,\n  \"engines\": {\n    \"node\": \">=24.0.0\"\n  },\n  \"scripts\": {\n    \"dev\": \"npm run generate && next dev\",\n    \"generate\": \"dotenv -e .env.local -- node ./scripts/generate.cjs\",\n    \"build\": \"npm run generate && next build\",\n    \"build:analyze\": \"ANALYZE=true npm run build\",\n    \"start\": \"next start\",\n    \"prelint\": \"next typegen\",\n    \"lint\": \"eslint . --ext .js,.jsx,.ts,.tsx\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@bigcommerce/catalyst-client\": \"workspace:^\",\n    \"@c15t/nextjs\": \"^1.8.2\",\n    \"@conform-to/react\": \"^1.6.1\",\n    \"@conform-to/zod\": \"^1.6.1\",\n    \"@icons-pack/react-simple-icons\": \"^11.2.0\",\n    \"@opentelemetry/api\": \"^1.9.0\",\n    \"@opentelemetry/api-logs\": \"^0.208.0\",\n    \"@opentelemetry/instrumentation\": \"^0.208.0\",\n    \"@opentelemetry/sdk-logs\": \"^0.208.0\",\n    \"@radix-ui/react-accordion\": \"^1.2.11\",\n    \"@radix-ui/react-checkbox\": \"^1.3.2\",\n    \"@radix-ui/react-dialog\": \"^1.1.14\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.15\",\n    \"@radix-ui/react-label\": \"^2.1.7\",\n    \"@radix-ui/react-navigation-menu\": \"^1.2.13\",\n    \"@radix-ui/react-popover\": \"^1.1.14\",\n    \"@radix-ui/react-portal\": \"^1.1.9\",\n    \"@radix-ui/react-radio-group\": \"^1.3.7\",\n    \"@radix-ui/react-select\": \"^2.2.5\",\n    \"@radix-ui/react-switch\": \"^1.2.5\",\n    \"@radix-ui/react-toggle\": \"^1.1.9\",\n    \"@radix-ui/react-toggle-group\": \"^1.1.10\",\n    \"@radix-ui/react-tooltip\": \"^1.2.7\",\n    \"@t3-oss/env-core\": \"^0.13.6\",\n    \"@upstash/redis\": \"^1.35.0\",\n    \"@vercel/analytics\": \"^1.5.0\",\n    \"@vercel/functions\": \"^2.2.12\",\n    \"@vercel/otel\": \"^2.1.0\",\n    \"@vercel/speed-insights\": \"^1.2.0\",\n    \"clsx\": \"^2.1.1\",\n    \"content-security-policy-builder\": \"^2.3.0\",\n    \"deepmerge\": \"^4.3.1\",\n    \"embla-carousel\": \"9.0.0-rc01\",\n    \"embla-carousel-autoplay\": \"9.0.0-rc01\",\n    \"embla-carousel-fade\": \"9.0.0-rc01\",\n    \"embla-carousel-react\": \"9.0.0-rc01\",\n    \"gql.tada\": \"^1.8.10\",\n    \"graphql\": \"^16.11.0\",\n    \"dompurify\": \"^3.3.1\",\n    \"jose\": \"^5.10.0\",\n    \"lodash.debounce\": \"^4.0.8\",\n    \"lru-cache\": \"^11.1.0\",\n    \"lucide-react\": \"^0.474.0\",\n    \"next\": \"~16.1.6\",\n    \"next-auth\": \"5.0.0-beta.30\",\n    \"next-intl\": \"^4.6.1\",\n    \"nuqs\": \"^2.4.3\",\n    \"p-lazy\": \"^5.0.0\",\n    \"react\": \"19.1.5\",\n    \"react-day-picker\": \"^9.7.0\",\n    \"react-dom\": \"19.1.5\",\n    \"react-google-recaptcha\": \"^3.1.0\",\n    \"react-headroom\": \"^3.2.1\",\n    \"schema-dts\": \"^1.1.5\",\n    \"server-only\": \"^0.0.1\",\n    \"sonner\": \"^1.7.4\",\n    \"tailwindcss-radix\": \"^3.0.5\",\n    \"uuid\": \"^11.1.0\",\n    \"zod\": \"^3.25.51\"\n  },\n  \"devDependencies\": {\n    \"@0no-co/graphqlsp\": \"^1.12.16\",\n    \"@bigcommerce/eslint-config\": \"^2.11.0\",\n    \"@bigcommerce/eslint-config-catalyst\": \"workspace:^\",\n    \"@faker-js/faker\": \"^9.8.0\",\n    \"@gql.tada/cli-utils\": \"^1.6.3\",\n    \"@next/bundle-analyzer\": \"^16.1.6\",\n    \"@playwright/test\": \"^1.52.0\",\n    \"@tailwindcss/container-queries\": \"^0.1.1\",\n    \"@tailwindcss/typography\": \"^0.5.16\",\n    \"@types/gtag.js\": \"^0.0.20\",\n    \"@types/lodash.debounce\": \"^4.0.9\",\n    \"@types/node\": \"^22.15.30\",\n    \"@types/react\": \"^19.1.6\",\n    \"@types/react-dom\": \"^19.1.6\",\n    \"@types/react-google-recaptcha\": \"^2.1.9\",\n    \"@types/react-headroom\": \"^3.2.3\",\n    \"@types/uuid\": \"^10.0.0\",\n    \"autoprefixer\": \"^10.4.21\",\n    \"dotenv\": \"^16.5.0\",\n    \"dotenv-cli\": \"^8.0.0\",\n    \"eslint\": \"^8.57.1\",\n    \"eslint-config-next\": \"15.5.10\",\n    \"postcss\": \"^8.5.4\",\n    \"postcss-preset-env\": \"^10.2.1\",\n    \"prettier\": \"^3.6.2\",\n    \"prettier-plugin-tailwindcss\": \"^0.6.12\",\n    \"tailwindcss\": \"^3.4.17\",\n    \"tailwindcss-animate\": \"1.0.7\",\n    \"typescript\": \"^5.8.3\"\n  }\n}\n"
  },
  {
    "path": "core/playwright.config.ts",
    "content": "import { defineConfig, devices } from '@playwright/test';\n\nimport { testEnv } from '~/tests/environment';\n\nexport default defineConfig({\n  testDir: './tests',\n  outputDir: './.tests/test-results',\n  workers: 1, // TODO: Implement parallel workers in the future\n  expect: {\n    toHaveScreenshot: {\n      maxDiffPixelRatio: 0.02,\n    },\n  },\n  reporter: [\n    ['list', { outputFolder: './.tests/reports/list' }],\n    ['html', { outputFolder: './.tests/reports/html' }],\n  ],\n  use: {\n    locale: testEnv.TESTS_LOCALE,\n    baseURL: testEnv.PLAYWRIGHT_TEST_BASE_URL,\n    screenshot: 'only-on-failure',\n    video: 'retain-on-failure',\n    trace: 'retain-on-failure',\n    extraHTTPHeaders: {\n      'x-vercel-protection-bypass': testEnv.VERCEL_PROTECTION_BYPASS,\n      'x-vercel-set-bypass-cookie': testEnv.CI.toString(),\n    },\n  },\n  projects: [\n    {\n      name: 'tests-chromium',\n      testIgnore: /.*\\.mobile\\.spec\\.ts$/,\n      use: {\n        ...devices['Desktop Chrome'],\n        launchOptions: {\n          // When redirected to checkout, BigCommerce blocks preflight requests from a HeadlessChrome user agent.\n          // We need to disable web security to allow the preflight request to go through.\n          args: ['--disable-web-security'],\n        },\n      },\n    },\n    {\n      name: 'tests-webkit-mobile',\n      testMatch: /.*\\.mobile\\.spec\\.ts$/,\n      use: {\n        ...devices['iPhone 11'],\n        // WebKit doesn't support --disable-web-security\n      },\n    },\n  ],\n  retries: 2,\n});\n"
  },
  {
    "path": "core/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n    'postcss-preset-env': {\n      browsers: '>2%, last 2 versions, Firefox ESR',\n      features: {\n        'cascade-layers': true,\n        'color-mix': true,\n        'oklab-function': true,\n        'is-pseudo-class': true,\n        'has-pseudo-class': true,\n        'focus-visible-pseudo-class': true,\n        'gap-properties': true,\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "core/prettier.config.js",
    "content": "// @ts-check\n\n/** @type {import(\"prettier\").Config} */\nconst config = {\n  printWidth: 100,\n  singleQuote: true,\n  trailingComma: 'all',\n  plugins: ['prettier-plugin-tailwindcss'],\n  tailwindFunctions: ['cn'],\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "core/proxies/compose-proxies.ts",
    "content": "import { type NextProxy, NextResponse } from 'next/server';\n\nexport type ProxyFactory = (proxy: NextProxy) => NextProxy;\n\nexport const composeProxies = (\n  firstProxyWrapper: ProxyFactory,\n  ...otherProxyWrappers: ProxyFactory[]\n): NextProxy => {\n  const proxies = otherProxyWrappers.reduce(\n    (accumulatedProxies, nextProxy) => (proxy) => accumulatedProxies(nextProxy(proxy)),\n    firstProxyWrapper,\n  );\n\n  return proxies(() => {\n    return NextResponse.next();\n  });\n};\n"
  },
  {
    "path": "core/proxies/with-analytics-cookies.ts",
    "content": "import { validate as isUuid, v4 as uuidv4 } from 'uuid';\n\nimport {\n  getVisitIdCookie,\n  getVisitorIdCookie,\n  setVisitIdCookie,\n  setVisitorIdCookie,\n} from '~/lib/analytics/bigcommerce';\nimport { sendVisitStartedEvent } from '~/lib/analytics/bigcommerce/data-events';\n\nimport { ProxyFactory } from './compose-proxies';\n\nexport const withAnalyticsCookies: ProxyFactory = (next) => {\n  return async (request, event) => {\n    const existingVisitorId = await getVisitorIdCookie();\n    const existingVisitId = await getVisitIdCookie();\n\n    const isPrefetch = request.headers.get('Next-Router-Prefetch') === '1';\n    const isRSC = request.headers.get('RSC') === '1';\n\n    const visitorId = existingVisitorId && isUuid(existingVisitorId) ? existingVisitorId : uuidv4();\n\n    await setVisitorIdCookie(visitorId);\n\n    const hasValidVisit = existingVisitId != null && isUuid(existingVisitId);\n\n    if (hasValidVisit) {\n      // Sliding window: refresh the TTL on every request\n      await setVisitIdCookie(existingVisitId);\n    } else if (!isPrefetch && !isRSC) {\n      // New visit on a real navigation: create cookie and fire event\n      const visitId = uuidv4();\n\n      await setVisitIdCookie(visitId);\n      event.waitUntil(recordNewVisit(request, visitorId, visitId));\n    }\n    // Prefetch/RSC with no valid visit: skip entirely so the\n    // subsequent real navigation properly detects a new visit.\n\n    return next(request, event);\n  };\n};\n\nasync function recordNewVisit(request: Request, visitorId: string, visitId: string) {\n  await sendVisitStartedEvent({\n    initiator: { visitId, visitorId },\n    request: {\n      url: request.url,\n      refererUrl: request.headers.get('referer') || '',\n      userAgent: request.headers.get('user-agent') || '',\n    },\n  });\n}\n"
  },
  {
    "path": "core/proxies/with-auth.ts",
    "content": "import { NextResponse, URLPattern } from 'next/server';\n\nimport { anonymousSignIn, auth, clearAnonymousSession, getAnonymousSession } from '~/auth';\n\nimport { type ProxyFactory } from './compose-proxies';\n\n// Path matcher for any routes that require authentication\nconst protectedPathPattern = new URLPattern({ pathname: `{/:locale}?/(account)/*` });\n\nfunction redirectToLogin(url: string) {\n  return NextResponse.redirect(new URL('/login', url), { status: 302 });\n}\n\nexport const withAuth: ProxyFactory = (next) => {\n  return async (request, event) => {\n    return auth(async (req) => {\n      const anonymousSession = await getAnonymousSession();\n      const isProtectedRoute = protectedPathPattern.test(req.nextUrl.toString().toLowerCase());\n      const isGetRequest = req.method === 'GET';\n\n      // Create the anonymous session if it doesn't exist\n      if (!req.auth && !anonymousSession) {\n        await anonymousSignIn();\n      }\n\n      // If the user is authenticated and there is an anonymous session, clear the anonymous session\n      if (req.auth && anonymousSession) {\n        await clearAnonymousSession();\n      }\n\n      if (!req.auth) {\n        if (isProtectedRoute && isGetRequest) {\n          return redirectToLogin(req.url);\n        }\n\n        return next(req, event);\n      }\n\n      const { customerAccessToken } = req.auth.user ?? {};\n\n      if (isProtectedRoute && isGetRequest && !customerAccessToken) {\n        return redirectToLogin(req.url);\n      }\n\n      // Continue the proxy chain\n      return next(req, event);\n    })(request, event);\n  };\n};\n"
  },
  {
    "path": "core/proxies/with-channel-id.ts",
    "content": "import { getChannelIdFromLocale } from '~/channels.config';\n\nimport { type ProxyFactory } from './compose-proxies';\n\nexport const withChannelId: ProxyFactory = (next) => {\n  return (request, event) => {\n    const locale = request.headers.get('x-bc-locale') ?? '';\n    const channelId = getChannelIdFromLocale(locale) ?? '';\n\n    request.headers.set('x-bc-channel-id', channelId);\n\n    return next(request, event);\n  };\n};\n"
  },
  {
    "path": "core/proxies/with-intl.ts",
    "content": "import createMiddleware from 'next-intl/middleware';\n\nimport { routing } from '~/i18n/routing';\n\nimport { type ProxyFactory } from './compose-proxies';\n\nconst intlMiddleware = createMiddleware(routing);\n\nexport const withIntl: ProxyFactory = (next) => {\n  return async (request, event) => {\n    const intlResponse = intlMiddleware(request);\n\n    // If intlMiddleware redirects, or returns a non-200 return it immediately\n    if (!intlResponse.ok) {\n      return intlResponse;\n    }\n\n    // Extract locale from intlMiddleware response\n    const locale = intlResponse.headers.get('x-middleware-request-x-next-intl-locale') ?? '';\n\n    request.headers.set('x-bc-locale', locale);\n\n    // Continue the proxy chain\n    const response = await next(request, event);\n\n    // Copy headers from intlResponse to response, excluding 'x-middleware-rewrite'\n    intlResponse.headers.forEach((v, k) => {\n      if (k !== 'x-middleware-rewrite') {\n        response?.headers.set(k, v);\n      }\n    });\n\n    return response;\n  };\n};\n"
  },
  {
    "path": "core/proxies/with-routes.ts",
    "content": "import { NextFetchEvent, NextRequest, NextResponse } from 'next/server';\nimport { z } from 'zod';\n\nimport { client } from '~/client';\nimport { graphql } from '~/client/graphql';\nimport { revalidate } from '~/client/revalidate-target';\nimport { getVisitIdCookie, getVisitorIdCookie } from '~/lib/analytics/bigcommerce';\nimport { sendProductViewedEvent } from '~/lib/analytics/bigcommerce/data-events';\nimport { kvKey, STORE_STATUS_KEY } from '~/lib/kv/keys';\n\nimport { kv } from '../lib/kv';\n\nimport { type ProxyFactory } from './compose-proxies';\n\nconst trailingSlashDisabled = process.env.TRAILING_SLASH === 'false';\n\nconst GetRouteQuery = graphql(`\n  query GetRouteQuery($path: String!) {\n    site {\n      route(path: $path, redirectBehavior: FOLLOW) {\n        redirect {\n          to {\n            __typename\n            ... on BlogPostRedirect {\n              path\n            }\n            ... on BrandRedirect {\n              path\n            }\n            ... on CategoryRedirect {\n              path\n            }\n            ... on PageRedirect {\n              path\n            }\n            ... on ProductRedirect {\n              path\n            }\n            ... on ManualRedirect {\n              url\n            }\n          }\n          fromPath\n          toUrl\n        }\n        node {\n          __typename\n          id\n          ... on Product {\n            entityId\n          }\n          ... on Category {\n            entityId\n          }\n          ... on Brand {\n            entityId\n          }\n          ... on BlogPost {\n            entityId\n          }\n        }\n      }\n    }\n  }\n`);\n\nconst getRoute = async (path: string, channelId?: string) => {\n  const response = await client.fetch({\n    document: GetRouteQuery,\n    variables: { path },\n    fetchOptions: { next: { revalidate } },\n    channelId,\n  });\n\n  return response.data.site.route;\n};\n\nconst getRawWebPageContentQuery = graphql(`\n  query getRawWebPageContent($id: ID!) {\n    node(id: $id) {\n      __typename\n      ... on RawHtmlPage {\n        htmlBody\n      }\n    }\n  }\n`);\n\nconst getRawWebPageContent = async (id: string) => {\n  const response = await client.fetch({\n    document: getRawWebPageContentQuery,\n    variables: { id },\n  });\n\n  const node = response.data.node;\n\n  if (node?.__typename !== 'RawHtmlPage') {\n    throw new Error('Failed to fetch raw web page content');\n  }\n\n  return node;\n};\n\nconst GetStoreStatusQuery = graphql(`\n  query getStoreStatus {\n    site {\n      settings {\n        status\n      }\n    }\n  }\n`);\n\nconst getStoreStatus = async (channelId?: string) => {\n  const { data } = await client.fetch({\n    document: GetStoreStatusQuery,\n    fetchOptions: { next: { revalidate: 300 } },\n    channelId,\n  });\n\n  return data.site.settings?.status;\n};\n\ntype Route = Awaited<ReturnType<typeof getRoute>>;\ntype StorefrontStatusType = ReturnType<typeof graphql.scalar<'StorefrontStatusType'>>;\n\ninterface RouteCache {\n  route: Route;\n  expiryTime: number;\n}\n\ninterface StorefrontStatusCache {\n  status: StorefrontStatusType;\n  expiryTime: number;\n}\n\nconst StorefrontStatusCacheSchema = z.object({\n  status: z.union([\n    z.literal('HIBERNATION'),\n    z.literal('LAUNCHED'),\n    z.literal('MAINTENANCE'),\n    z.literal('PRE_LAUNCH'),\n  ]),\n  expiryTime: z.number(),\n});\n\nconst RedirectSchema = z.object({\n  to: z.union([\n    z.object({ __typename: z.literal('BlogPostRedirect'), path: z.string() }),\n    z.object({ __typename: z.literal('BrandRedirect'), path: z.string() }),\n    z.object({ __typename: z.literal('CategoryRedirect'), path: z.string() }),\n    z.object({ __typename: z.literal('PageRedirect'), path: z.string() }),\n    z.object({ __typename: z.literal('ProductRedirect'), path: z.string() }),\n    z.object({ __typename: z.literal('ManualRedirect'), url: z.string() }),\n  ]),\n  fromPath: z.string(),\n  toUrl: z.string(),\n});\n\nconst NodeSchema = z.union([\n  z.object({ __typename: z.literal('Product'), entityId: z.number() }),\n  z.object({ __typename: z.literal('Category'), entityId: z.number() }),\n  z.object({ __typename: z.literal('Brand'), entityId: z.number() }),\n  z.object({ __typename: z.literal('ContactPage'), id: z.string() }),\n  z.object({ __typename: z.literal('NormalPage'), id: z.string() }),\n  z.object({ __typename: z.literal('RawHtmlPage'), id: z.string() }),\n  z.object({ __typename: z.literal('Blog'), id: z.string() }),\n  z.object({ __typename: z.literal('BlogPost'), entityId: z.number() }),\n]);\n\nconst RouteSchema = z.object({\n  redirect: z.nullable(RedirectSchema),\n  node: z.nullable(NodeSchema),\n});\n\nconst RouteCacheSchema = z.object({\n  route: z.nullable(RouteSchema),\n  expiryTime: z.number(),\n});\n\nconst updateRouteCache = async (\n  pathname: string,\n  channelId: string,\n  event: NextFetchEvent,\n): Promise<RouteCache> => {\n  const routeCache: RouteCache = {\n    route: await getRoute(pathname, channelId),\n    expiryTime: Date.now() + 1000 * 60 * 30, // 30 minutes\n  };\n\n  event.waitUntil(kv.set(kvKey(pathname, channelId), routeCache));\n\n  return routeCache;\n};\n\nconst updateStatusCache = async (\n  channelId: string,\n  event: NextFetchEvent,\n): Promise<StorefrontStatusCache> => {\n  const status = await getStoreStatus(channelId);\n\n  if (status === undefined) {\n    throw new Error('Failed to fetch new storefront status');\n  }\n\n  const statusCache: StorefrontStatusCache = {\n    status,\n    expiryTime: Date.now() + 1000 * 60 * 5, // 5 minutes\n  };\n\n  event.waitUntil(kv.set(kvKey(STORE_STATUS_KEY, channelId), statusCache));\n\n  return statusCache;\n};\n\nconst clearLocaleFromPath = (path: string, locale: string) => {\n  if (path === `/${locale}` || path === `/${locale}/`) {\n    return '/';\n  }\n\n  if (path.startsWith(`/${locale}/`)) {\n    return path.replace(`/${locale}`, '');\n  }\n\n  return path;\n};\n\nfunction normalizeForCompare(url: URL): string {\n  if (trailingSlashDisabled && url.pathname !== '/' && url.pathname.endsWith('/')) {\n    return `${url.pathname.replace(/\\/+$/, '')}${url.search}`;\n  }\n\n  if (!trailingSlashDisabled && !url.pathname.endsWith('/')) {\n    return `${url.pathname}/${url.search}`;\n  }\n\n  return `${url.pathname}${url.search}`;\n}\n\nconst sameInternalUrl = (a: URL, b: URL) =>\n  a.origin === b.origin && normalizeForCompare(a) === normalizeForCompare(b);\n\nconst getRouteInfo = async (request: NextRequest, event: NextFetchEvent) => {\n  const locale = request.headers.get('x-bc-locale') ?? '';\n  const channelId = request.headers.get('x-bc-channel-id') ?? '';\n\n  try {\n    // For route resolution parity, we need to also include query params, otherwise certain redirects will not work.\n    const pathname = clearLocaleFromPath(request.nextUrl.pathname + request.nextUrl.search, locale);\n\n    let [routeCache, statusCache] = await kv.mget<RouteCache | StorefrontStatusCache>(\n      kvKey(pathname, channelId),\n      kvKey(STORE_STATUS_KEY, channelId),\n    );\n\n    // If caches are old, update them in the background and return the old data (SWR-like behavior)\n    // If cache is missing, update it and return the new data, but write to KV in the background\n    if (statusCache && statusCache.expiryTime < Date.now()) {\n      event.waitUntil(updateStatusCache(channelId, event));\n    } else if (!statusCache) {\n      statusCache = await updateStatusCache(channelId, event);\n    }\n\n    if (routeCache && routeCache.expiryTime < Date.now()) {\n      event.waitUntil(updateRouteCache(pathname, channelId, event));\n    } else if (!routeCache) {\n      routeCache = await updateRouteCache(pathname, channelId, event);\n    }\n\n    const parsedRoute = RouteCacheSchema.safeParse(routeCache);\n    const parsedStatus = StorefrontStatusCacheSchema.safeParse(statusCache);\n\n    return {\n      route: parsedRoute.success ? parsedRoute.data.route : undefined,\n      status: parsedStatus.success ? parsedStatus.data.status : undefined,\n    };\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n\n    return {\n      route: undefined,\n      status: undefined,\n    };\n  }\n};\n\nexport const withRoutes: ProxyFactory = () => {\n  // eslint-disable-next-line complexity\n  return async (request, event) => {\n    const locale = request.headers.get('x-bc-locale') ?? '';\n\n    const { route, status } = await getRouteInfo(request, event);\n\n    if (status === 'MAINTENANCE') {\n      // 503 status code not working - https://github.com/vercel/next.js/issues/50155\n      return NextResponse.rewrite(new URL(`/${locale}/maintenance`, request.url), { status: 503 });\n    }\n\n    const redirectConfig = {\n      // Use 301 status code as it is more universally supported by crawlers\n      status: 301,\n      nextConfig: {\n        // Preserve the trailing slash if it was present in the original URL\n        // BigCommerce by default returns the trailing slash.\n        trailingSlash: process.env.TRAILING_SLASH !== 'false',\n      },\n    };\n\n    if (route?.redirect) {\n      // Only carry over query params if the fromPath does not have any, as Bigcommerce 301 redirects support matching by specific query params.\n      const fromPathSearchParams = new URL(route.redirect.fromPath, request.url).search;\n      const searchParams = fromPathSearchParams.length > 0 ? '' : request.nextUrl.search;\n\n      switch (route.redirect.to.__typename) {\n        case 'BlogPostRedirect':\n        case 'BrandRedirect':\n        case 'CategoryRedirect':\n        case 'PageRedirect':\n        case 'ProductRedirect': {\n          // For dynamic redirects, assume an internal redirect and construct the URL from the path\n          const redirectUrl = new URL(route.redirect.to.path + searchParams, request.url);\n\n          if (sameInternalUrl(request.nextUrl, redirectUrl)) {\n            break;\n          }\n\n          return NextResponse.redirect(redirectUrl, redirectConfig);\n        }\n\n        case 'ManualRedirect': {\n          // For manual redirects, to.url will be a relative path if it is an internal redirect and an absolute URL if it is an external redirect.\n          // URL constructor will correctly handle both cases.\n          // If the manual redirect is an external URL, we should not carry query params.\n          const redirectUrl = new URL(route.redirect.to.url, request.url);\n\n          if (redirectUrl.origin === request.nextUrl.origin) {\n            redirectUrl.search = searchParams;\n\n            if (sameInternalUrl(request.nextUrl, redirectUrl)) {\n              break;\n            }\n          }\n\n          return NextResponse.redirect(redirectUrl, redirectConfig);\n        }\n\n        default: {\n          // If for some reason the redirect type is not recognized, use the toUrl as a fallback\n          return NextResponse.redirect(route.redirect.toUrl, redirectConfig);\n        }\n      }\n    }\n\n    const node = route?.node;\n    let url: string;\n\n    switch (node?.__typename) {\n      case 'Brand': {\n        url = `/${locale}/brand/${node.entityId}`;\n        break;\n      }\n\n      case 'Category': {\n        url = `/${locale}/category/${node.entityId}`;\n        break;\n      }\n\n      case 'Product': {\n        url = `/${locale}/product/${node.entityId}`;\n\n        const isPrefetch = request.headers.get('Next-Router-Prefetch') === '1';\n        const isRSC = request.headers.get('RSC') === '1';\n\n        if (!isPrefetch && !isRSC) {\n          event.waitUntil(recordProductVisit(request, node.entityId));\n        }\n\n        break;\n      }\n\n      case 'NormalPage': {\n        url = `/${locale}/webpages/${node.id}/normal/`;\n        break;\n      }\n\n      case 'ContactPage': {\n        url = `/${locale}/webpages/${node.id}/contact/`;\n        break;\n      }\n\n      case 'RawHtmlPage': {\n        const { htmlBody } = await getRawWebPageContent(node.id);\n\n        return new NextResponse(htmlBody, {\n          headers: { 'content-type': 'text/html' },\n        });\n      }\n\n      case 'Blog': {\n        url = `/${locale}/blog`;\n        break;\n      }\n\n      case 'BlogPost': {\n        url = `/${locale}/blog/${node.entityId}`;\n        break;\n      }\n\n      default: {\n        const { pathname } = new URL(request.url);\n\n        const cleanPathName = clearLocaleFromPath(pathname, locale);\n\n        url = `/${locale}${cleanPathName}`;\n      }\n    }\n\n    const rewriteUrl = new URL(url, request.url);\n\n    rewriteUrl.search = request.nextUrl.search;\n\n    return NextResponse.rewrite(rewriteUrl);\n  };\n};\n\nasync function recordProductVisit(request: Request, productId: number) {\n  const visitId = await getVisitIdCookie();\n  const visitorId = await getVisitorIdCookie();\n\n  if (visitId && visitorId) {\n    await sendProductViewedEvent({\n      productId,\n      initiator: { visitId, visitorId },\n      request: {\n        url: request.url,\n        refererUrl: request.headers.get('referer') || '',\n        userAgent: request.headers.get('user-agent') || '',\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "core/proxy.ts",
    "content": "import { composeProxies } from './proxies/compose-proxies';\nimport { withAnalyticsCookies } from './proxies/with-analytics-cookies';\nimport { withAuth } from './proxies/with-auth';\nimport { withChannelId } from './proxies/with-channel-id';\nimport { withIntl } from './proxies/with-intl';\nimport { withRoutes } from './proxies/with-routes';\n\nexport const proxy = composeProxies(\n  withAuth,\n  withAnalyticsCookies,\n  withIntl,\n  withChannelId,\n  withRoutes,\n);\n\nexport const config = {\n  matcher: [\n    /*\n     * Match all request paths except for the ones starting with:\n     * - api (API routes)\n     * - _next/static (static files)\n     * - _next/image (image optimization files)\n     * - _vercel (vercel internals, eg: web vitals)\n     * - favicon.ico (favicon file)\n     * - admin (admin panel)\n     * - sitemap.xml (sitemap route)\n     * - xmlsitemap.php (legacy sitemap route)\n     * - robots.txt (robots route)\n     */\n    '/((?!api|admin|_next/static|_next/image|_vercel|favicon.ico|xmlsitemap.php|sitemap.xml|robots.txt).*)',\n  ],\n};\n"
  },
  {
    "path": "core/scripts/generate.cjs",
    "content": "// @ts-check\nconst { generateSchema, generateOutput } = require('@gql.tada/cli-utils');\nconst { join } = require('path');\n\nconst graphqlApiDomain = process.env.BIGCOMMERCE_GRAPHQL_API_DOMAIN ?? 'mybigcommerce.com';\n\nconst getStoreHash = () => {\n  const storeHash = process.env.BIGCOMMERCE_STORE_HASH;\n\n  if (!storeHash) {\n    throw new Error('Missing store hash');\n  }\n\n  return storeHash;\n};\n\nconst getChannelId = () => {\n  const channelId = process.env.BIGCOMMERCE_CHANNEL_ID;\n\n  return channelId;\n};\n\nconst getToken = () => {\n  const token = process.env.BIGCOMMERCE_STOREFRONT_TOKEN;\n\n  if (!token) {\n    throw new Error('Missing storefront token');\n  }\n\n  return token;\n};\n\nconst getEndpoint = () => {\n  const storeHash = getStoreHash();\n  const channelId = getChannelId();\n\n  // Not all sites have the channel-specific canonical URL backfilled.\n  // Wait till MSF-2643 is resolved before removing and simplifying the endpoint logic.\n  if (!channelId || channelId === '1') {\n    return `https://store-${storeHash}.${graphqlApiDomain}/graphql`;\n  }\n\n  return `https://store-${storeHash}-${channelId}.${graphqlApiDomain}/graphql`;\n};\n\nconst generate = async () => {\n  try {\n    await generateSchema({\n      input: getEndpoint(),\n      headers: { Authorization: `Bearer ${getToken()}` },\n      output: join(__dirname, '../bigcommerce.graphql'),\n      tsconfig: undefined,\n    });\n\n    await generateOutput({\n      disablePreprocessing: false,\n      output: undefined,\n      tsconfig: undefined,\n    });\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error);\n    process.exit(1);\n  }\n};\n\ngenerate();\n"
  },
  {
    "path": "core/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nconst config = {\n  content: [\n    './app/**/*.{ts,tsx}',\n    './components/**/*.{ts,tsx}',\n    './vibes/**/*.{ts,tsx}',\n    '!./node_modules/**', // Exclude everything in node_modules to speed up builds\n  ],\n  theme: {\n    extend: {\n      typography: {\n        DEFAULT: {\n          css: {\n            h1: {\n              color: 'hsl(var(--foreground))',\n              fontFamily: 'var(--font-family-heading)',\n            },\n            h2: {\n              color: 'hsl(var(--foreground))',\n              fontFamily: 'var(--font-family-heading)',\n            },\n            h3: {\n              color: 'hsl(var(--foreground))',\n              fontFamily: 'var(--font-family-heading)',\n            },\n            h4: {\n              color: 'hsl(var(--foreground))',\n              fontFamily: 'var(--font-family-heading)',\n            },\n            h5: {\n              color: 'hsl(var(--foreground))',\n              fontFamily: 'var(--font-family-heading)',\n            },\n            h6: {\n              color: 'hsl(var(--foreground))',\n              fontFamily: 'var(--font-family-heading)',\n            },\n            p: {\n              color: 'hsl(var(--foreground))',\n              fontFamily: 'var(--font-family-body)',\n            },\n            a: {\n              color: 'color-mix(in oklab, hsl(var(--primary)), black 15%)',\n              textDecoration: 'none',\n              '&:hover': {\n                textDecoration: 'underline',\n              },\n            },\n            ul: {\n              color: 'hsl(var(--contrast-500))',\n              fontFamily: 'var(--font-family-body)',\n            },\n            ol: {\n              color: 'hsl(var(--contrast-500))',\n              fontFamily: 'var(--font-family-body)',\n            },\n            strong: {\n              fontWeight: '600',\n            },\n            blockquote: {\n              borderLeftColor: 'hsl(var(--contrast-300))',\n              p: {\n                color: 'hsl(var(--contrast-500))',\n                fontStyle: 'normal',\n                fontWeight: '400',\n              },\n            },\n            code: {\n              color: 'hsl(var(--contrast-500))',\n              fontFamily: 'var(--font-family-mono)',\n            },\n            pre: {\n              color: 'hsl(var(--background))',\n              backgroundColor: 'hsl(var(--foreground))',\n              fontFamily: 'var(--font-family-mono)',\n            },\n          },\n        },\n      },\n      colors: {\n        primary: {\n          DEFAULT: 'hsl(var(--primary))',\n          highlight: 'color-mix(in oklab, hsl(var(--primary)), white 75%)',\n          shadow: 'color-mix(in oklab, hsl(var(--primary)), black 75%)',\n        },\n        accent: {\n          DEFAULT: 'hsl(var(--accent))',\n          highlight: 'color-mix(in oklab, hsl(var(--accent)), white 75%)',\n          shadow: 'color-mix(in oklab, hsl(var(--accent)), black 75%)',\n        },\n        success: {\n          DEFAULT: 'hsl(var(--success))',\n          highlight: 'color-mix(in oklab, hsl(var(--success)), white 75%)',\n          shadow: 'color-mix(in oklab, hsl(var(--success)), black 75%)',\n        },\n        error: {\n          DEFAULT: 'hsl(var(--error))',\n          highlight: 'color-mix(in oklab, hsl(var(--error)), white 75%)',\n          shadow: 'color-mix(in oklab, hsl(var(--error)), black 75%)',\n        },\n        warning: {\n          DEFAULT: 'hsl(var(--warning))',\n          highlight: 'color-mix(in oklab, hsl(var(--warning)), white 75%)',\n          shadow: 'color-mix(in oklab, hsl(var(--warning)), black 75%)',\n        },\n        info: {\n          DEFAULT: 'hsl(var(--info))',\n          highlight: 'color-mix(in oklab, hsl(var(--info)), white 75%)',\n          shadow: 'color-mix(in oklab, hsl(var(--info)), black 75%)',\n        },\n        background: 'hsl(var(--background))',\n        foreground: 'hsl(var(--foreground))',\n        contrast: {\n          100: 'hsl(var(--contrast-100))',\n          200: 'hsl(var(--contrast-200))',\n          300: 'hsl(var(--contrast-300))',\n          400: 'hsl(var(--contrast-400))',\n          500: 'hsl(var(--contrast-500))',\n        },\n      },\n      fontFamily: {\n        heading: [\n          'var(--font-family-heading)',\n          {\n            fontFeatureSettings: 'var(--font-feature-settings-heading)',\n            fontVariationSettings: 'var(--font-variation-settings-heading)',\n          },\n        ],\n        body: [\n          'var(--font-family-body)',\n          {\n            fontFeatureSettings: 'var(--font-feature-settings-body)',\n            fontVariationSettings: 'var(--font-variation-settings-body)',\n          },\n        ],\n        mono: [\n          'var(--font-family-mono)',\n          {\n            fontFeatureSettings: 'var(--font-feature-settings-mono)',\n            fontVariationSettings: 'var(--font-variation-settings-mono)',\n          },\n        ],\n      },\n      fontSize: {\n        xs: 'var(--font-size-xs, 0.75rem)',\n        sm: 'var(--font-size-sm, 0.875rem)',\n        base: 'var(--font-size-base, 1rem)',\n        lg: 'var(--font-size-lg, 1.125rem)',\n        xl: 'var(--font-size-xl, 1.25rem)',\n        '2xl': 'var(--font-size-2xl, 1.5rem)',\n        '3xl': 'var(--font-size-3xl, 1.875rem)',\n        '4xl': 'var(--font-size-4xl, 2.25rem)',\n        '5xl': 'var(--font-size-5xl, 3rem)',\n        '6xl': 'var(--font-size-6xl, 3.75rem)',\n        '7xl': 'var(--font-size-7xl, 4.5rem)',\n        '8xl': 'var(--font-size-8xl, 6rem)',\n        '9xl': 'var(--font-size-9xl, 8rem)',\n      },\n      shadows: {\n        sm: 'var(--shadow-sm)',\n        DEFAULT: 'var(--shadow-base)',\n        md: 'var(--shadow-md)',\n        lg: 'var(--shadow-lg)',\n        xl: 'var(--shadow-xl)',\n      },\n      keyframes: {\n        collapse: {\n          from: { height: 'var(--radix-accordion-content-height)' },\n          to: { height: '0' },\n        },\n        expand: {\n          from: { height: '0' },\n          to: { height: 'var(--radix-accordion-content-height)' },\n        },\n        'marching-ants': {\n          to: {\n            'background-position':\n              '0 0, 0 -1px, calc(100% + 1px) 0, 100% calc(100% + 1px), -1px 100%',\n          },\n        },\n        rotateFade: {\n          from: { opacity: '1', transform: 'rotateZ(0deg) translate3d(-50%,-50%,0)' },\n          '35%': { opacity: '0' },\n          '70%': { opacity: '0' },\n          to: { opacity: '1', transform: 'rotateZ(360deg) translate3d(-50%,-50%,0)' },\n        },\n        rotate: {\n          from: {\n            transform: 'rotateZ(0deg) translate3d(-50%,-50%,0)',\n          },\n          to: {\n            transform: 'rotateZ(360deg) translate3d(-50%,-50%,0)',\n          },\n        },\n        scroll: {\n          to: { backgroundPosition: '5px 0' },\n        },\n        dotScrollSmall: {\n          to: { backgroundPosition: '-6px -6px, -12px -12px' },\n        },\n        dotScrollLarge: {\n          to: { backgroundPosition: '-8px -8px, -16px -16px' },\n        },\n        scrollLeft: {\n          '0%': { transform: 'translateX(0)' },\n          '100%': { transform: 'translateX(-100%)' },\n        },\n        shake: {\n          '10%, 90%': { transform: 'translate3d(-1px, 0, 0)' },\n          '20%, 80%': { transform: 'translate3d(1px, 0, 0)' },\n          '30%, 50%, 70%': { transform: 'translate3d(-2px, 0, 0)' },\n          '40%, 60%': { transform: 'translate3d(2px, 0, 0)' },\n        },\n        slideIn: {\n          '0%': { transform: 'translateX(-100%)' },\n          '100%': { transform: 'translateX(0%)' },\n        },\n      },\n      animation: {\n        collapse: 'collapse 400ms cubic-bezier(1, 0, 0.25, 1)',\n        expand: 'expand 400ms cubic-bezier(1, 0, 0.25, 1)',\n        marching: 'marching-ants 10s linear infinite',\n        rotate: 'rotate 2000ms linear infinite',\n        scroll: 'scroll 200ms infinite linear both',\n        scrollLeft: 'scrollLeft var(--marquee-duration) linear infinite',\n        shake: 'shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both',\n        slideIn: 'slideIn 800ms cubic-bezier(0.25, 1, 0, 1)',\n      },\n    },\n  },\n  plugins: [\n    // @ts-ignore\n    require('tailwindcss-radix')(),\n    require('tailwindcss-animate'),\n    require('@tailwindcss/container-queries'),\n    require('@tailwindcss/typography'),\n  ],\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "core/tests/README.md",
    "content": "# Testing your Catalyst storefront\n\nThis document provides an overview of how to run tests for your Catalyst storefront. This includes the requirements, environment setup, folder structure, and best practices. This README assumes that you already have a Catalyst storefront set up and all of the dependencies installed.\n\n## Table of Contents\n- [Prerequisites](#prerequisites)\n- [Environment setup](#environment-setup)\n- [Running tests](#running-tests)\n\n## Prerequisites\n- Ensure that you have already installed and set up Playwright. (See the [Playwright documentation](https://playwright.dev/docs/intro) for more information.)\n- **Recommended**: Install the Playwright VSCode extension for easier test management and execution.\n\n## Environment setup\n\nTo run tests, you first need to make sure you have the necessary environment setup. You should have already installed the required dependencies and configured your Catalyst environment in `.env.local`.\n\nTo begin, copy `.env.test.example` to `.env.test`. The Catalyst test environment merges `.env.local` with `.env.test.local`, so any test-related environment variables should **only** be in `.env.test.local`. This allows you to keep your test configuration separate from your development configuration.\n\n**NOTE:** Any environment variables defined in `.env.test.local` will override those in `.env.local`.\n\n## Running tests\n\nWhen running tests locally, ensure that you have an active build for your Catalyst storefront by running `pnpm build`. This will ensure that the latest changes are included in the tests. Alternatively, you can use `pnpm dev` to run the development server, but this is not recommended.\n\nWhen running tests against a live deployment, ensure that you have set the `PLAYWRIGHT_TEST_BASE_URL` environment variable to the URL of your live deployment storefront, and ensure that `TESTS_READ_ONLY` is set accordingly for your needs.\n\n**NOTE:** The examples use `pnpm`, but you can also use `npx` if you prefer. Additionally, you can leverage the Playwright VSCode extension to run your tests.\n\n### CLI commands\nTo run all of the tests, use the following command:\n\n```bash\npnpm playwright test\n```\n\nTo run a specific test file, you can specify the path to the test file:\n\n```bash\npnpm playwright test auth/logout.spec.ts\n```\n\nTo run a specific test within a file, you can use the `-g` flag followed by the name of the test:\n\n```bash\npnpm playwright test auth/logout.spec.ts -g \"Logout works as expected\"\n```\n\nFor more information on the available CLI commands, refer to the [Playwright CLI documentation](https://playwright.dev/docs/test-cli).\n"
  },
  {
    "path": "core/tests/environment.ts",
    "content": "import { createEnv } from '@t3-oss/env-core';\nimport { config as dotenvConfig } from 'dotenv';\nimport { z } from 'zod/v4';\n\nimport { defaultLocale, locales } from '~/i18n/locales';\n\ndotenvConfig({ path: ['.env', '.env.local', '.env.test'], override: true });\n\nconst localeSchema = z.string().refine((val: string) => locales.includes(val), {\n  error: `TESTS_LOCALE must be one of: ${locales.join(', ')}`,\n});\n\nexport const testEnv = createEnv({\n  server: {\n    BIGCOMMERCE_ADMIN_API_HOST: z.string().optional().default('api.bigcommerce.com'),\n    BIGCOMMERCE_ACCESS_TOKEN: z.string().optional(),\n    BIGCOMMERCE_ACCESS_TOKEN_CLIENT_ID: z.string().optional(),\n    BIGCOMMERCE_ACCESS_TOKEN_CLIENT_SECRET: z.string().optional(),\n    BIGCOMMERCE_CHANNEL_ID: z.coerce.number().optional(),\n    BIGCOMMERCE_STORE_HASH: z.string().optional(),\n    PLAYWRIGHT_TEST_BASE_URL: z.string().optional().default('http://localhost:3000'),\n    VERCEL_PROTECTION_BYPASS: z.string().optional().default(''),\n    CI: z.stringbool().optional().default(false),\n    TESTS_READ_ONLY: z.stringbool().optional().default(false),\n    TESTS_LOCALE: localeSchema.default(defaultLocale),\n    TESTS_FALLBACK_LOCALE: localeSchema.default(defaultLocale),\n    TEST_CUSTOMER_ID: z.coerce.number().optional(),\n    TEST_CUSTOMER_EMAIL: z.string().optional(),\n    TEST_CUSTOMER_PASSWORD: z.string().optional(),\n    DEFAULT_PRODUCT_ID: z.coerce.number().optional(),\n    DEFAULT_COMPLEX_PRODUCT_ID: z.coerce.number().optional(),\n    TRAILING_SLASH: z.stringbool().optional().default(true),\n  },\n  runtimeEnv: process.env,\n  emptyStringAsUndefined: true,\n});\n"
  },
  {
    "path": "core/tests/fixtures/blog/index.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { Fixture } from '~/tests/fixtures/fixture';\nimport { Blog, BlogPost, CreateBlogPostData } from '~/tests/fixtures/utils/api/blog';\n\nexport class BlogFixture extends Fixture {\n  posts: BlogPost[] = [];\n\n  getBlog(): Promise<Blog> {\n    return this.api.blog.get();\n  }\n\n  getPosts(page?: number, limit?: number): Promise<BlogPost[]> {\n    return this.api.blog.getPosts(page, limit);\n  }\n\n  async createPost(data?: Partial<CreateBlogPostData>): Promise<BlogPost> {\n    this.skipIfReadonly();\n\n    const blogPost = await this.api.blog.createPost({\n      title: `Test Post ${faker.string.alpha(5)}`,\n      body: faker.lorem.paragraphs({ min: 1, max: 5 }),\n      author: faker.person.fullName(),\n      tags: [faker.lorem.word(), faker.lorem.word()],\n      isPublished: true,\n      ...data,\n    });\n\n    this.posts.push(blogPost);\n\n    return blogPost;\n  }\n\n  async cleanup() {\n    await this.api.blog.deletePosts(this.posts.map(({ id }) => id));\n  }\n}\n"
  },
  {
    "path": "core/tests/fixtures/browser.ts",
    "content": "import { Browser, BrowserContext } from '@playwright/test';\n\nimport { extendedPage } from './page';\n\n// We need to ensure that BrowserContext.newPage returns the extended page type,\n// otherwise calling .waitForURL() on the new page will not work when using alternate locales.\nexport function extendedBrowser(browser: Browser): Browser {\n  const originalNewContext = browser.newContext.bind(browser);\n\n  const browserWithOverrides: Browser = Object.assign(browser, {\n    newContext: async (...args: Parameters<typeof browser.newContext>) => {\n      const context = await originalNewContext(...args);\n\n      return extendedBrowserContext(context);\n    },\n  });\n\n  return browserWithOverrides;\n}\n\nfunction extendedBrowserContext(context: BrowserContext) {\n  const originalNewPage = context.newPage.bind(context);\n\n  const contextWithOverrides: BrowserContext = Object.assign(context, {\n    newPage: async (...args: Parameters<typeof context.newPage>) => {\n      const page = await originalNewPage(...args);\n\n      return extendedPage(page);\n    },\n  });\n\n  return contextWithOverrides;\n}\n"
  },
  {
    "path": "core/tests/fixtures/catalog/index.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { testEnv } from '~/tests/environment';\nimport { Fixture } from '~/tests/fixtures/fixture';\nimport {\n  Brand,\n  Category,\n  CreateProductData,\n  CreateVariantData,\n  Product,\n} from '~/tests/fixtures/utils/api/catalog';\n\nexport class CatalogFixture extends Fixture {\n  products: Product[] = [];\n\n  async getDefaultOrCreateSimpleProduct(): Promise<Product> {\n    if (!testEnv.DEFAULT_PRODUCT_ID) {\n      return this.createSimpleProduct();\n    }\n\n    const testProduct = await this.api.catalog.getProductById(testEnv.DEFAULT_PRODUCT_ID);\n\n    if (testProduct.inventoryLevel === 0 && testProduct.inventoryTracking !== 'none') {\n      throw new Error(\n        'Product for DEFAULT_PRODUCT_ID is out of stock and cannot be used in tests.',\n      );\n    }\n\n    return testProduct;\n  }\n\n  async getDefaultOrCreateComplexProduct(): Promise<Product> {\n    if (!testEnv.DEFAULT_COMPLEX_PRODUCT_ID) {\n      return this.createComplexProduct();\n    }\n\n    const testProduct = await this.api.catalog.getProductById(testEnv.DEFAULT_COMPLEX_PRODUCT_ID);\n\n    if (testProduct.inventoryLevel === 0 && testProduct.inventoryTracking !== 'none') {\n      throw new Error(\n        'Product for DEFAULT_COMPLEX_PRODUCT_ID is out of stock and cannot be used in tests.',\n      );\n    }\n\n    return testProduct;\n  }\n\n  getCategories(filters?: { nameLike?: string; ids?: number[] }): Promise<Category[]> {\n    return this.api.catalog.getCategories(filters);\n  }\n\n  getBrands(filters?: { nameLike?: string; ids?: number[] }): Promise<Brand[]> {\n    return this.api.catalog.getBrands(filters);\n  }\n\n  async createSimpleProduct(\n    overrides?: Partial<Omit<CreateProductData, 'variants'>>,\n  ): Promise<Product> {\n    this.skipIfReadonly();\n\n    const product = await this.api.catalog.createProduct(this.fakeCreateProductData(overrides));\n\n    this.products.push(product);\n\n    return product;\n  }\n\n  async createComplexProduct(overrides?: Partial<CreateProductData>): Promise<Product> {\n    this.skipIfReadonly();\n\n    const product = await this.api.catalog.createProduct({\n      ...this.fakeCreateProductData(overrides),\n      variants: [\n        this.fakeCreateVariantData('Small'),\n        this.fakeCreateVariantData('Medium'),\n        this.fakeCreateVariantData('Large'),\n      ],\n    });\n\n    this.products.push(product);\n\n    return product;\n  }\n\n  async cleanup() {\n    await this.api.catalog.deleteProducts(this.products.map(({ id }) => id));\n  }\n\n  private fakeCreateProductData(\n    data?: Partial<Omit<CreateProductData, 'variants'>>,\n  ): CreateProductData {\n    const suffix = faker.string.alpha(5);\n\n    return {\n      name: `Test Product ${suffix}`,\n      weight: faker.number.int({ min: 1, max: 100 }),\n      price: faker.number.int({ min: 10, max: 1000 }),\n      salePrice: faker.number.int({ min: 5, max: 900 }),\n      retailPrice: faker.number.int({ min: 15, max: 1100 }),\n      sku: `TEST-PRODUCT-${suffix.toUpperCase()}`,\n      type: 'physical',\n      description: faker.lorem.paragraph({ min: 3, max: 50 }),\n      isVisible: true,\n      inventoryLevel: faker.number.int({ min: 10, max: 100 }),\n      inventoryWarningLevel: faker.number.int({ min: 1, max: 50 }),\n      inventoryTracking: 'none',\n      ...data,\n    };\n  }\n\n  private fakeCreateVariantData(label?: string): CreateVariantData {\n    const suffix = faker.string.alpha(5);\n\n    return {\n      sku: `TEST-VARIANT-${suffix.toUpperCase()}`,\n      optionValues: [\n        {\n          label: label ?? suffix,\n          optionDisplayName: 'Size',\n        },\n      ],\n      price: faker.number.int({ min: 10, max: 1000 }),\n      salePrice: faker.number.int({ min: 5, max: 900 }),\n      retailPrice: faker.number.int({ min: 15, max: 1100 }),\n    };\n  }\n}\n"
  },
  {
    "path": "core/tests/fixtures/currency/index.ts",
    "content": "import { Fixture } from '~/tests/fixtures/fixture';\nimport { getTranslations } from '~/tests/lib/i18n';\n\nexport class CurrencyFixture extends Fixture {\n  async getDefaultCurrency(): Promise<string> {\n    const currencyAssignments = await this.api.currencies.getCurrencyAssignments();\n\n    return currencyAssignments.defaultCurrency;\n  }\n\n  async getEnabledCurrencies(): Promise<string[]> {\n    const currencyAssignments = await this.api.currencies.getCurrencyAssignments();\n\n    return currencyAssignments.enabledCurrencies;\n  }\n\n  async convertWithExchangeRate(currencyCode: string, value: number): Promise<number> {\n    const currencies = await this.api.currencies.getCurrencies();\n    const currency = currencies.find((c) => c.currencyCode === currencyCode);\n\n    if (!currency) {\n      throw new Error(`Currency with code ${currencyCode} not found`);\n    }\n\n    return value * currency.exchangeRate;\n  }\n\n  async selectCurrency(currency: string): Promise<void> {\n    const t = await getTranslations('Components.Header.SwitchCurrency');\n\n    await this.page.getByRole('button', { name: t('label') }).click();\n    await this.page.getByRole('menuitem', { name: currency }).click();\n    await this.page.waitForLoadState('networkidle');\n  }\n\n  async cleanup() {\n    // no cleanup needed\n  }\n}\n"
  },
  {
    "path": "core/tests/fixtures/customer/index.ts",
    "content": "/* eslint-disable valid-jsdoc */\nimport { faker } from '@faker-js/faker';\n\nimport { generateCustomerLoginApiJwt } from '~/auth/customer-login-api';\nimport { testEnv } from '~/tests/environment';\nimport { expect, Page } from '~/tests/fixtures';\nimport { Fixture } from '~/tests/fixtures/fixture';\nimport {\n  Address,\n  CreateCustomerData,\n  CreateWishlistData,\n  Customer,\n  Wishlist,\n} from '~/tests/fixtures/utils/api/customers';\nimport { getTranslations } from '~/tests/lib/i18n';\n\nimport { customerSessionStore } from './session';\n\nexport class CustomerFixture extends Fixture {\n  customers: Customer[] = [];\n  addresses: Address[] = [];\n  wishlists: Wishlist[] = [];\n\n  constructor(\n    readonly reuseCustomerSession: boolean,\n    ...args: ConstructorParameters<typeof Fixture>\n  ) {\n    super(...args);\n  }\n\n  /**\n   * Checks environment variables for a test customer. If the test customer is not found, it creates a new one. \\\n   * This method should always be preferred over creating a new customer directly, unless the test you are writing specifically requires a new customer.\n   */\n  async getOrCreateTestCustomer(): Promise<Customer> {\n    const testCustomer = await this.getTestCustomer();\n\n    if (testCustomer) {\n      return testCustomer;\n    }\n\n    return this.createNewCustomer();\n  }\n\n  async createNewCustomer(): Promise<Customer> {\n    this.skipIfReadonly();\n\n    // Prefix is added to ensure that the password requirements are met\n    const password = faker.internet.password({\n      pattern: /[a-zA-Z0-9]/,\n      prefix: '1At!',\n      length: 10,\n    });\n\n    const customer = await this.api.customers.create(this.fakeCreateCustomerData(password, true));\n\n    this.customers.push(customer);\n\n    return customer;\n  }\n\n  async getTestCustomer(): Promise<Customer | undefined> {\n    if (\n      !testEnv.TEST_CUSTOMER_ID ||\n      !testEnv.TEST_CUSTOMER_EMAIL ||\n      !testEnv.TEST_CUSTOMER_PASSWORD\n    ) {\n      return undefined;\n    }\n\n    const customer = await this.api.customers.getById(testEnv.TEST_CUSTOMER_ID, true);\n\n    customer.password = testEnv.TEST_CUSTOMER_PASSWORD;\n\n    return customer;\n  }\n\n  /** Gets customer information from the API via ID. Will not include a password, as this cannot be obtained via API. */\n  getById(customerId: number, includeAddresses?: boolean): Promise<Customer> {\n    return this.api.customers.getById(customerId, includeAddresses);\n  }\n\n  getByEmail(email: string, includeAddresses?: boolean): Promise<Customer> {\n    return this.api.customers.getByEmail(email, includeAddresses);\n  }\n\n  async createAddress(customerId: number): Promise<Address> {\n    this.skipIfReadonly();\n\n    const address = await this.api.customers.createAddress(\n      this.fakeCreateAddressData({ customerId }),\n    );\n\n    this.addresses.push(address);\n\n    return address;\n  }\n\n  async createWishlist({\n    customerId,\n    name = `Test wishlist ${faker.string.alpha(10)}`,\n    isPublic = false,\n    items = [],\n  }: Partial<CreateWishlistData> & { customerId: number }): Promise<Wishlist> {\n    this.skipIfReadonly();\n\n    const wishlist = await this.api.customers.createWishlist({\n      name,\n      isPublic,\n      customerId,\n      items,\n    });\n\n    this.wishlists.push(wishlist);\n\n    return wishlist;\n  }\n\n  async generateLoginJwt(customerId: number, redirectTo = '/account/orders'): Promise<string> {\n    try {\n      return await generateCustomerLoginApiJwt(\n        customerId,\n        testEnv.BIGCOMMERCE_CHANNEL_ID ?? 1,\n        redirectTo,\n      );\n    } catch {\n      this.test.skip(true, 'Failed to generate JWT for customer login test');\n\n      // Will never be reached due to the test.skip\n      return '';\n    }\n  }\n\n  /**\n   * Logs in with the designated test customer account, or makes a new customer account to login with if no test account is set.\n   * If possible, will reuse an existing login session to avoid unnecessary logins.\n   */\n  async login(redirectTo?: string): Promise<Customer> {\n    const customer = await this.getOrCreateTestCustomer();\n\n    await this.loginAs(customer, redirectTo);\n\n    return customer;\n  }\n\n  async loginAs(customer: Customer, redirectTo?: string): Promise<void> {\n    if (!customer.password) {\n      throw new Error('Unable to perform login due to customer password not being set.');\n    }\n\n    const t = await getTranslations('Auth.Login');\n    const searchParams = redirectTo ? `?${new URLSearchParams({ redirectTo }).toString()}` : '';\n    const url = `/login${searchParams}`;\n\n    if (!redirectTo && this.reuseCustomerSession) {\n      const usedExistingSession = await customerSessionStore.useExistingSession(this, customer.id);\n\n      if (usedExistingSession) {\n        return;\n      }\n    }\n\n    await this.page.goto(url);\n    await this.page.getByLabel(t('email')).fill(customer.email);\n    await this.page.getByLabel(t('password')).fill(customer.password);\n    await this.page.getByRole('button', { name: t('cta') }).click();\n    await this.page.waitForLoadState('networkidle');\n\n    await expect(this.page.getByText(t('invalidCredentials'))).not.toBeVisible();\n    await expect(this.page).not.toHaveURL(url);\n\n    // If the assertions are passed, we can assume login was successful.\n    await customerSessionStore.updateCustomerSession(this, customer.id);\n  }\n\n  async logout(): Promise<void> {\n    const t = await getTranslations();\n\n    await this.page.getByLabel(t('Components.Header.Icons.account')).click();\n    await this.page.waitForURL('/account/**');\n    await this.page.getByRole('link', { name: t('Account.Layout.logout') }).click();\n    await this.page.waitForLoadState('networkidle');\n  }\n\n  async delete(...customerIds: number[]): Promise<void> {\n    this.skipIfReadonly();\n\n    await this.api.customers.delete(customerIds);\n  }\n\n  async deleteAllAddresses(customerId: number): Promise<void> {\n    this.skipIfReadonly();\n\n    const addresses = await this.api.customers.getAddresses(customerId);\n\n    await this.api.customers.deleteAddresses(addresses.map(({ id }) => id));\n  }\n\n  async deleteAllWishlists(customerId: number): Promise<void> {\n    this.skipIfReadonly();\n\n    const wishlists = await this.api.customers.getWishlists(customerId);\n\n    await this.api.customers.deleteWishlists(wishlists.map(({ id }) => id));\n  }\n\n  /** Clones the fixture with a new page object. Useful if the fixture is needed in a new browser window. */\n  withNewPage(page: Page): CustomerFixture {\n    return new CustomerFixture(this.reuseCustomerSession, page, this.test);\n  }\n\n  async cleanup() {\n    // Cleanup will not remove the test customer set in the environment variables\n    await this.api.customers.delete(\n      this.customers.map(({ id }) => id).filter((id) => id !== testEnv.TEST_CUSTOMER_ID),\n    );\n\n    await this.api.customers.deleteAddresses(this.addresses.map(({ id }) => id));\n    await this.api.customers.deleteWishlists(this.wishlists.map(({ id }) => id));\n  }\n\n  private fakeCreateAddressData({\n    firstName,\n    lastName,\n    customerId,\n  }: {\n    firstName?: string;\n    lastName?: string;\n    customerId?: number;\n  }) {\n    const first = firstName ?? faker.person.firstName();\n    const last = lastName ?? faker.person.lastName();\n    const address1 = faker.location.streetAddress();\n    const city = faker.location.city();\n    const state = faker.location.state();\n    const postalCode = faker.location.zipCode('#####');\n\n    return {\n      ...(customerId ? { customerId } : {}),\n      firstName: first,\n      lastName: last,\n      address1,\n      city,\n      stateOrProvince: state,\n      countryCode: 'US',\n      postalCode,\n    };\n  }\n\n  private fakeCreateCustomerData(\n    password: string,\n    createFakeAddress?: boolean,\n  ): CreateCustomerData {\n    const firstName = faker.person.firstName();\n    const lastName = faker.person.lastName();\n    const email = faker.internet.email({ firstName, lastName, provider: 'example.com' });\n\n    return {\n      firstName,\n      lastName,\n      email,\n      password,\n      ...(createFakeAddress\n        ? { addresses: [this.fakeCreateAddressData({ firstName, lastName })] }\n        : {}),\n      originChannelId: Number(testEnv.BIGCOMMERCE_CHANNEL_ID),\n    };\n  }\n}\n"
  },
  {
    "path": "core/tests/fixtures/customer/session.ts",
    "content": "import { access, readFile, writeFile } from 'node:fs/promises';\nimport { z } from 'zod';\n\nimport { CustomerFixture } from '.';\n\nconst fileSchema = z.record(\n  z.coerce.number(),\n  z.array(\n    z.object({\n      name: z.string(),\n      value: z.string(),\n      domain: z.string(),\n      path: z.string(),\n      expires: z.number(),\n      httpOnly: z.boolean(),\n      secure: z.boolean(),\n      sameSite: z.enum(['Strict', 'Lax', 'None']),\n    }),\n  ),\n);\n\nclass CustomerSessionStore {\n  private storageFilePath = '.tests/customer-session.json';\n\n  async updateCustomerSession(fixture: CustomerFixture, customerId: number): Promise<void> {\n    await this.ensureStorageFileExists();\n\n    const cookieJar = await fixture.page.context().cookies();\n    const cookies = cookieJar.filter((cookie) => cookie.name.includes('authjs'));\n\n    const file = await readFile(this.storageFilePath, 'utf-8');\n    const storage = fileSchema.parse(JSON.parse(file));\n\n    storage[customerId] = cookies;\n\n    await writeFile(this.storageFilePath, JSON.stringify(storage, null, 2));\n  }\n\n  // Uses an existing session for a customer ID if it exists. Returns true if successful and false if not found\n  async useExistingSession(fixture: CustomerFixture, customerId: number): Promise<boolean> {\n    await this.ensureStorageFileExists();\n\n    fixture.page.on('response', async (resp) => {\n      if (resp.url().includes('/logout')) {\n        await this.removeCustomerSession(customerId);\n      }\n    });\n\n    const file = await readFile(this.storageFilePath, 'utf-8');\n\n    const storage = fileSchema.parse(JSON.parse(file));\n    const customerSession = storage[customerId];\n\n    if (!customerSession) {\n      return false;\n    }\n\n    await fixture.page.context().addCookies(customerSession);\n    await fixture.page.goto('/login/', { waitUntil: 'networkidle' });\n\n    if (new URL(fixture.page.url()).pathname.includes('/login/')) {\n      // If the session is no longer valid, the page will stay on the login page.\n      // If this happens, the session should be removed so the test can login as normal.\n      await this.removeCustomerSession(customerId);\n\n      return false;\n    }\n\n    return true;\n  }\n\n  private async removeCustomerSession(customerId: number): Promise<void> {\n    await this.ensureStorageFileExists();\n\n    const file = await readFile(this.storageFilePath, 'utf-8');\n\n    const storage = fileSchema.parse(JSON.parse(file));\n    const updatedStorage = Object.fromEntries(\n      Object.entries(storage).filter(([key]) => key !== String(customerId)),\n    );\n\n    await writeFile(this.storageFilePath, JSON.stringify(updatedStorage, null, 2));\n  }\n\n  private async ensureStorageFileExists(): Promise<void> {\n    try {\n      await access(this.storageFilePath);\n    } catch {\n      await writeFile(this.storageFilePath, JSON.stringify({}));\n    }\n  }\n}\n\nexport const customerSessionStore = new CustomerSessionStore();\n"
  },
  {
    "path": "core/tests/fixtures/fixture.ts",
    "content": "import { type Page, type TestInfo } from '@playwright/test';\n\nimport { testEnv } from '~/tests/environment';\nimport { ApiClient, httpApiClient } from '~/tests/fixtures/utils/api';\n\nexport abstract class Fixture {\n  protected readonly api: ApiClient;\n\n  constructor(\n    readonly page: Page,\n    readonly test: TestInfo,\n  ) {\n    this.api = httpApiClient;\n  }\n\n  protected skipIfReadonly(): void {\n    this.test.skip(testEnv.TESTS_READ_ONLY, 'Tests are running in read-only mode.');\n  }\n\n  abstract cleanup(): Promise<void>;\n}\n"
  },
  {
    "path": "core/tests/fixtures/index.ts",
    "content": "// Disabling the rule as this should be the only place where we import test and expect from Playwright\n// eslint-disable-next-line @typescript-eslint/no-restricted-imports\nimport { expect as baseExpect, test as baseTest } from '@playwright/test';\nimport { validate as isUuid } from 'uuid';\n\nimport { testEnv } from '~/tests/environment';\nimport { TAGS } from '~/tests/tags';\n\nimport { BlogFixture } from './blog';\nimport { extendedBrowser } from './browser';\nimport { CatalogFixture } from './catalog';\nimport { CurrencyFixture } from './currency';\nimport { CustomerFixture } from './customer';\nimport { OrderFixture } from './order';\nimport { extendedPage, toHaveURL } from './page';\nimport { PromotionFixture } from './promotion';\nimport { RedirectsFixture } from './redirects';\nimport { SettingsFixture } from './settings';\nimport { SubscribeFixture } from './subscribe';\nimport { WebPageFixture } from './webpage';\n\ninterface Fixtures {\n  blog: BlogFixture;\n  order: OrderFixture;\n  catalog: CatalogFixture;\n  customer: CustomerFixture;\n  currency: CurrencyFixture;\n  promotion: PromotionFixture;\n  redirects: RedirectsFixture;\n  settings: SettingsFixture;\n  subscribe: SubscribeFixture;\n  webPage: WebPageFixture;\n  /**\n   * 'reuseCustomerSession' sets the the configuration for the customer fixture and determines whether to reuse the customer session.\n   * For example, in login tests we do not want to reuse the session so we call `test.use({ reuseCustomerSession: false });`\n   */\n  reuseCustomerSession: boolean;\n  // eslint-disable-next-line @typescript-eslint/no-invalid-void-type\n  skipWritesOnReadonly: void;\n  page: ReturnType<typeof extendedPage>;\n}\n\nexport const test = baseTest.extend<Fixtures>({\n  page: [\n    async ({ page }, use) => {\n      await use(extendedPage(page));\n    },\n    { scope: 'test' },\n  ],\n  browser: [\n    async ({ browser }, use) => {\n      await use(extendedBrowser(browser));\n    },\n    { scope: 'worker' },\n  ],\n  blog: [\n    async ({ page }, use, currentTest) => {\n      const blogFixture = new BlogFixture(page, currentTest);\n\n      await use(blogFixture);\n\n      await blogFixture.cleanup();\n    },\n    { scope: 'test' },\n  ],\n  order: [\n    async ({ page }, use, currentTest) => {\n      const orderFixture = new OrderFixture(page, currentTest);\n\n      await use(orderFixture);\n\n      await orderFixture.cleanup();\n    },\n    { scope: 'test' },\n  ],\n  catalog: [\n    async ({ page }, use, currentTest) => {\n      const catalogFixture = new CatalogFixture(page, currentTest);\n\n      await use(catalogFixture);\n\n      await catalogFixture.cleanup();\n    },\n    { scope: 'test' },\n  ],\n  customer: [\n    async ({ page, reuseCustomerSession }, use, currentTest) => {\n      const customerFixture = new CustomerFixture(reuseCustomerSession, page, currentTest);\n\n      await use(customerFixture);\n\n      await customerFixture.cleanup();\n    },\n    { scope: 'test' },\n  ],\n  currency: [\n    async ({ page }, use, currentTest) => {\n      const currencyFixture = new CurrencyFixture(page, currentTest);\n\n      await use(currencyFixture);\n\n      await currencyFixture.cleanup();\n    },\n    { scope: 'test' },\n  ],\n  promotion: [\n    async ({ page }, use, currentTest) => {\n      const promotionFixture = new PromotionFixture(page, currentTest);\n\n      await use(promotionFixture);\n\n      await promotionFixture.cleanup();\n    },\n    { scope: 'test' },\n  ],\n  redirects: [\n    async ({ page }, use, currentTest) => {\n      const redirectsFixture = new RedirectsFixture(page, currentTest);\n\n      await use(redirectsFixture);\n\n      await redirectsFixture.cleanup();\n    },\n    { scope: 'test' },\n  ],\n  settings: [\n    async ({ page }, use, currentTest) => {\n      const settingsFixture = new SettingsFixture(page, currentTest);\n\n      await use(settingsFixture);\n\n      await settingsFixture.cleanup();\n    },\n    { scope: 'test' },\n  ],\n  subscribe: [\n    async ({ page }, use, currentTest) => {\n      const subscribeFixture = new SubscribeFixture(page, currentTest);\n\n      await use(subscribeFixture);\n\n      await subscribeFixture.cleanup();\n    },\n    { scope: 'test' },\n  ],\n  webPage: [\n    async ({ page }, use, currentTest) => {\n      const webPageFixture = new WebPageFixture(page, currentTest);\n\n      await use(webPageFixture);\n\n      await webPageFixture.cleanup();\n    },\n    { scope: 'test' },\n  ],\n  reuseCustomerSession: [true, { option: true }],\n  skipWritesOnReadonly: [\n    // eslint-disable-next-line no-empty-pattern\n    async ({}, use, currentTest) => {\n      if (currentTest.tags.includes(TAGS.writesData) && testEnv.TESTS_READ_ONLY) {\n        currentTest.skip(true, 'Test environment is set to read-only mode.');\n      }\n\n      await use();\n    },\n    { auto: true },\n  ],\n});\n\nexport const expect = baseExpect.extend({\n  toBeUuid(received) {\n    const pass = isUuid(received);\n\n    if (pass) {\n      return {\n        message: () => `expected ${received} not to be a uuid`,\n        pass: true,\n      };\n    }\n\n    return {\n      message: () => `expected ${received} to be a uuid`,\n      pass: false,\n    };\n  },\n  toHaveURL,\n});\n\nexport { type Page } from '@playwright/test';\n"
  },
  {
    "path": "core/tests/fixtures/order/index.ts",
    "content": "import { testEnv } from '~/tests/environment';\nimport { Fixture } from '~/tests/fixtures/fixture';\nimport { Order } from '~/tests/fixtures/utils/api/orders';\n\nexport class OrderFixture extends Fixture {\n  orders: Order[] = [];\n\n  async createWithDefaultProduct(customerId?: number): Promise<Order> {\n    if (!testEnv.DEFAULT_PRODUCT_ID) {\n      throw new Error(\n        'A product ID is required to create an order. Provide a product ID or set the TEST_DEFAULT_SIMPLE_PRODUCT_ID environment variable.',\n      );\n    }\n\n    return this.create(testEnv.DEFAULT_PRODUCT_ID, customerId);\n  }\n\n  async create(productId: number, customerId?: number): Promise<Order> {\n    this.skipIfReadonly();\n\n    const order = await this.api.orders.create(productId, customerId);\n\n    this.orders.push(order);\n\n    return order;\n  }\n\n  async deleteAllCustomerOrders(customerId: number): Promise<void> {\n    this.skipIfReadonly();\n\n    const orders = await this.api.orders.get(customerId);\n\n    await this.api.orders.delete(orders.map(({ id }) => id));\n  }\n\n  async cleanup() {\n    await this.api.orders.delete(this.orders.map(({ id }) => id));\n  }\n}\n"
  },
  {
    "path": "core/tests/fixtures/page.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/no-restricted-imports\nimport { expect, ExpectMatcherState, MatcherReturnType, Page } from '@playwright/test';\n\nimport { defaultLocale } from '~/i18n/locales';\nimport { testEnv } from '~/tests/environment';\n\nexport function extendedPage(page: Page) {\n  const originalWaitForURL = page.waitForURL.bind(page);\n  const pageWithOverrides: Page = Object.assign(page, {\n    // Overrides the page.waitForURL method to ensure that locale-specific URLs are also handled.\n    // This ensures that an /account/orders/ assertion will also work for /de/account/orders/ for easier usage in tests.\n    waitForURL: (...[url, options]: Parameters<typeof page.waitForURL>) => {\n      if (\n        typeof url === 'string' &&\n        testEnv.TESTS_LOCALE !== defaultLocale &&\n        url.startsWith('/')\n      ) {\n        return Promise.race([\n          originalWaitForURL(url, options),\n          originalWaitForURL(`/${testEnv.TESTS_LOCALE}${url}`, options),\n        ]);\n      }\n\n      return originalWaitForURL(url, options);\n    },\n  });\n\n  return pageWithOverrides;\n}\n\nfunction normalizeForTrailingSlashEnvVar(url: string): string {\n  const [pathname = '/', searchAndHash = ''] = url.split(/([?#].*)/);\n\n  if (!testEnv.TRAILING_SLASH) {\n    if (pathname !== '/' && pathname.endsWith('/')) {\n      return pathname.slice(0, -1) + searchAndHash;\n    }\n\n    return pathname + searchAndHash;\n  }\n\n  if (pathname !== '/' && !pathname.endsWith('/')) {\n    return `${pathname}/${searchAndHash}`;\n  }\n\n  return pathname + searchAndHash;\n}\n\n// Override expect(page).toHaveURL assertion to ensure we are also checking locale-specific URLs when using relative paths.\n// e.g. expect(page).toHaveURL('/account/orders/') will expect /de/account/orders/ if the locale is set to 'de' and is not the default locale.\nexport async function toHaveURL(\n  this: ExpectMatcherState,\n  page: Page,\n  url: string | RegExp | ((url: URL) => boolean),\n  options?: { timeout?: number; ignoreCase?: boolean },\n): Promise<MatcherReturnType> {\n  const assertionName = 'toHaveURL';\n  let pass: boolean;\n  let matcherResult: MatcherReturnType | undefined;\n\n  const isRelativeLocaleUrl =\n    typeof url === 'string' && testEnv.TESTS_LOCALE !== defaultLocale && url.startsWith('/');\n\n  try {\n    const expectation = this.isNot ? expect(page).not : expect(page);\n    const urlsToCheck = isRelativeLocaleUrl ? [url, `/${testEnv.TESTS_LOCALE}${url}`] : [url];\n\n    // This ensures that if you call expect(page).toHaveURL('/my-url/') when TRAILING_SLASH=false, it asserts `/my-url`, and vice-versa.\n    // Trailing slash assertions are updated to respect the TRAILING_SLASH env var.\n    const updatedUrlsToCheck = urlsToCheck.map((urlToCheck) => {\n      if (typeof urlToCheck === 'string') {\n        return normalizeForTrailingSlashEnvVar(urlToCheck);\n      }\n\n      return urlToCheck;\n    });\n\n    const checks = updatedUrlsToCheck.map((u) => expectation.toHaveURL(u, options));\n\n    if (this.isNot) {\n      // if we are negating the assertion, all checks must be executed\n      await Promise.all(checks);\n    } else {\n      // if promise is not negated, we only need to wait for one of the checks to pass\n      await Promise.race(checks);\n    }\n\n    pass = true;\n  } catch (error: unknown) {\n    if (error instanceof Error && 'matcherResult' in error) {\n      // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n      matcherResult = error.matcherResult as MatcherReturnType;\n    }\n\n    pass = false;\n  }\n\n  if (this.isNot) {\n    pass = !pass;\n  }\n\n  const matcherHint = this.utils.matcherHint(assertionName, undefined, undefined, {\n    isNot: this.isNot,\n  });\n\n  const expectedMessage = (): string => {\n    const notPrefix = this.isNot ? 'not ' : '';\n\n    if (typeof url === 'string') {\n      const urlWithLocaleMaybe =\n        testEnv.TESTS_LOCALE !== defaultLocale && url.startsWith('/')\n          ? `/${testEnv.TESTS_LOCALE}${url}`\n          : url;\n\n      const absoluteUrl = new URL(urlWithLocaleMaybe, page.url());\n\n      return `Expected: ${notPrefix}${this.utils.printExpected(absoluteUrl)}`;\n    } else if (url instanceof RegExp) {\n      return `Expected URL ${notPrefix}to match pattern ${this.utils.printExpected(url)}`;\n    }\n\n    return `Expected URL predicate to ${notPrefix}succeed`;\n  };\n\n  const receivedMessage = matcherResult\n    ? `Received: ${this.utils.printReceived(matcherResult.actual)}`\n    : '';\n\n  const message = () => `${matcherHint}\\n\\n${expectedMessage()}\\n${receivedMessage}`;\n\n  return {\n    message,\n    pass,\n    name: assertionName,\n    expected: url,\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n    actual: matcherResult?.actual,\n  };\n}\n"
  },
  {
    "path": "core/tests/fixtures/promotion/index.ts",
    "content": "import { Fixture } from '~/tests/fixtures/fixture';\nimport { Coupon, PromotionWithCoupon } from '~/tests/fixtures/utils/api/promotions';\n\nexport class PromotionFixture extends Fixture {\n  coupons: PromotionWithCoupon[] = [];\n\n  async createCouponCode(): Promise<Coupon> {\n    this.skipIfReadonly();\n\n    const promoWithCoupon = await this.api.promotions.createCouponCode();\n\n    this.coupons.push(promoWithCoupon);\n\n    return promoWithCoupon.coupon;\n  }\n\n  async cleanup() {\n    await Promise.all(\n      this.coupons.map(({ promotionId, coupon }) =>\n        this.api.promotions.deleteCouponCode(promotionId, coupon.id),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "core/tests/fixtures/redirects/index.ts",
    "content": "import { Fixture } from '~/tests/fixtures/fixture';\nimport { Redirect, UpsertRedirectData } from '~/tests/fixtures/utils/api/redirects';\n\nexport class RedirectsFixture extends Fixture {\n  redirects: Redirect[] = [];\n\n  async upsertRedirect(data: UpsertRedirectData): Promise<Redirect | undefined> {\n    this.skipIfReadonly();\n\n    const redirect = await this.api.redirects.upsert(data);\n\n    if (redirect) {\n      this.redirects.push(redirect);\n    }\n\n    return redirect;\n  }\n\n  async cleanup() {\n    await this.api.redirects.delete(this.redirects.map(({ id }) => id));\n  }\n}\n"
  },
  {
    "path": "core/tests/fixtures/settings/index.ts",
    "content": "import { Fixture } from '~/tests/fixtures/fixture';\nimport { InventorySettings } from '~/tests/fixtures/utils/api/settings';\n\nexport class SettingsFixture extends Fixture {\n  private initialInventorySettings: InventorySettings | null = null;\n\n  async getInventorySettings(): Promise<InventorySettings> {\n    const settings = await this.api.settings.getInventorySettings();\n\n    return settings;\n  }\n\n  async setInventorySettings(settings: InventorySettings): Promise<void> {\n    if (!this.initialInventorySettings) {\n      this.initialInventorySettings = await this.getInventorySettings();\n    }\n\n    await this.api.settings.setInventorySettings(settings);\n  }\n\n  async cleanup() {\n    if (this.initialInventorySettings) {\n      await this.api.settings.setInventorySettings(this.initialInventorySettings);\n    }\n  }\n}\n"
  },
  {
    "path": "core/tests/fixtures/subscribe/index.ts",
    "content": "import { Fixture } from '~/tests/fixtures/fixture';\n\nexport class SubscribeFixture extends Fixture {\n  subscribedEmails: string[] = [];\n\n  trackSubscription(email: string): void {\n    this.subscribedEmails.push(email);\n  }\n\n  async subscribe(email: string, firstName: string, lastName: string): Promise<void> {\n    this.skipIfReadonly();\n\n    await this.api.subscribe.subscribe(email, firstName, lastName);\n\n    this.trackSubscription(email);\n  }\n\n  async unsubscribe(email: string): Promise<void> {\n    this.skipIfReadonly();\n\n    await this.api.subscribe.unsubscribe(email);\n  }\n\n  async cleanup(): Promise<void> {\n    this.skipIfReadonly();\n\n    await Promise.all(this.subscribedEmails.map((email) => this.api.subscribe.unsubscribe(email)));\n\n    this.subscribedEmails = [];\n  }\n}\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/blog/http.ts",
    "content": "import { z } from 'zod';\n\nimport { httpClient } from '../client';\nimport { apiResponseSchema } from '../schema';\n\nimport { Blog, BlogApi, BlogPost, CreateBlogPostData } from '.';\n\nconst BlogSchema = z\n  .object({\n    id: z.number(),\n    name: z.string(),\n    url: z.string(),\n  })\n  .transform(\n    (data): Blog => ({\n      name: data.name,\n      path: data.url,\n    }),\n  );\n\nconst BlogPostSchema = z\n  .object({\n    id: z.number(),\n    author: z.string().nullable(),\n    title: z.string(),\n    body: z.string(),\n    summary: z.string(),\n    tags: z.array(z.string()),\n    url: z.string(),\n  })\n  .transform(\n    ({ url, author, ...data }): BlogPost => ({\n      ...data,\n      author: author ?? undefined,\n      path: url,\n    }),\n  );\n\nconst BlogPostCreateSchema = z.object({\n  title: z.string(),\n  body: z.string(),\n  author: z.string().optional(),\n  is_published: z.boolean().optional(),\n  tags: z.array(z.string()).optional(),\n});\n\nconst transformCreateBlogPostData = (data: CreateBlogPostData) =>\n  BlogPostCreateSchema.parse({\n    title: data.title,\n    body: data.body,\n    author: data.author,\n    is_published: data.isPublished,\n    tags: data.tags,\n  });\n\nexport const blogHttpClient: BlogApi = {\n  get: async () => {\n    const pages = await httpClient\n      .get('/v3/content/pages?limit=250')\n      .parse(\n        apiResponseSchema(z.array(z.object({ id: z.number(), type: z.string() }).passthrough())),\n      );\n\n    const blogPage = pages.data.find((page) => page.type === 'blog');\n\n    if (!blogPage) {\n      throw new Error('Blog not found');\n    }\n\n    return BlogSchema.parse(blogPage);\n  },\n  getPosts: async (page = 1, limit = 9) => {\n    const posts = await httpClient\n      .get(`/v2/blog/posts?page=${page}&limit=${limit}&is_published=true`)\n      .parse(z.array(BlogPostSchema).optional());\n\n    return posts ?? [];\n  },\n  createPost: async (data) => {\n    const post = await httpClient\n      .post('/v2/blog/posts', transformCreateBlogPostData(data))\n      .parse(BlogPostSchema);\n\n    return post;\n  },\n  deletePosts: async (ids: number[]) => {\n    if (ids.length > 0) {\n      await Promise.all(ids.map((id) => httpClient.delete(`/v2/blog/posts/${id}`)));\n    }\n  },\n};\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/blog/index.ts",
    "content": "export interface Blog {\n  readonly name: string;\n  readonly path: string;\n}\n\nexport interface BlogPost {\n  readonly id: number;\n  readonly author?: string;\n  readonly title: string;\n  readonly body: string;\n  readonly summary: string;\n  readonly tags: string[];\n  readonly path: string;\n}\n\nexport interface CreateBlogPostData {\n  title: string;\n  body: string;\n  author?: string;\n  tags?: string[];\n  isPublished?: boolean;\n}\n\nexport interface BlogApi {\n  get: () => Promise<Blog>;\n  getPosts: (page?: number, limit?: number) => Promise<BlogPost[]>;\n  createPost: (data: CreateBlogPostData) => Promise<BlogPost>;\n  deletePosts: (ids: number[]) => Promise<void>;\n}\n\nexport { blogHttpClient } from './http';\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/catalog/http.ts",
    "content": "import { z } from 'zod';\n\nimport { testEnv } from '~/tests/environment';\n\nimport { httpClient } from '../client';\nimport { apiResponseSchema } from '../schema';\n\nimport {\n  Brand,\n  CatalogApi,\n  Category,\n  CreateProductData,\n  CreateVariantData,\n  Product,\n  Variant,\n} from '.';\n\nconst VariantSchema = z\n  .object({\n    id: z.number(),\n    product_id: z.number(),\n    sku: z.string(),\n    price: z.number().nullable(),\n    sale_price: z.number().nullable(),\n    retail_price: z.number().nullable(),\n    option_values: z.array(\n      z.object({\n        id: z.number(),\n        label: z.string(),\n        option_id: z.number(),\n        option_display_name: z.string(),\n      }),\n    ),\n    inventory_level: z.number().optional(),\n    inventory_warning_level: z.number().optional(),\n  })\n  .transform(\n    (data): Variant => ({\n      id: data.id,\n      productId: data.product_id,\n      sku: data.sku,\n      price: data.price ?? undefined,\n      salePrice: data.sale_price ?? undefined,\n      retailPrice: data.retail_price ?? undefined,\n      optionValues: data.option_values.map((option) => ({\n        id: option.id,\n        label: option.label,\n        optionId: option.option_id,\n        optionDisplayName: option.option_display_name,\n      })),\n      inventoryLevel: data.inventory_level,\n      inventoryWarningLevel: data.inventory_warning_level,\n    }),\n  );\n\nconst VariantCreateSchema = z.object({\n  sku: z.string(),\n  option_values: z.array(\n    z.object({\n      label: z.string(),\n      option_display_name: z.string(),\n    }),\n  ),\n  price: z.number().optional(),\n  sale_price: z.number().optional(),\n  retail_price: z.number().optional(),\n  inventory_level: z.number().optional(),\n  inventory_warning_level: z.number().optional(),\n});\n\nconst ProductSchema = z\n  .object({\n    id: z.number(),\n    name: z.string(),\n    description: z.string(),\n    sku: z.string(),\n    price: z.number(),\n    retail_price: z.number(),\n    sale_price: z.number(),\n    custom_url: z.object({\n      url: z.string(),\n    }),\n    variants: z.array(VariantSchema).default([]),\n    categories: z.array(z.number()).default([]),\n    inventory_level: z.number().optional(),\n    inventory_warning_level: z.number().optional(),\n    inventory_tracking: z.enum(['none', 'product', 'variant']).optional(),\n  })\n  .transform(\n    (data): Product => ({\n      id: data.id,\n      name: data.name,\n      description: data.description,\n      sku: data.sku,\n      price: data.price,\n      retailPrice: data.retail_price,\n      salePrice: data.sale_price,\n      customUrl: data.custom_url,\n      path: data.custom_url.url,\n      variants: data.variants,\n      categories: data.categories,\n      inventoryLevel: data.inventory_level,\n      inventoryWarningLevel: data.inventory_warning_level,\n      inventoryTracking: data.inventory_tracking,\n    }),\n  );\n\nconst ProductCreateCustomUrlSchema = z.object({\n  url: z.string(),\n  is_customized: z.boolean().optional(),\n  create_redirect: z.boolean().optional(),\n});\n\nconst ProductCreateSchema = z.object({\n  name: z.string(),\n  weight: z.number(),\n  price: z.number(),\n  sale_price: z.number().optional(),\n  retail_price: z.number().optional(),\n  custom_url: ProductCreateCustomUrlSchema.optional(),\n  sku: z.string().optional(),\n  type: z.enum(['physical', 'digital']).optional(),\n  description: z.string().optional(),\n  is_visible: z.boolean().optional(),\n  variants: z.array(VariantCreateSchema).optional(),\n  categories: z.array(z.number()).optional(),\n  inventory_level: z.number().optional(),\n  inventory_warning_level: z.number().optional(),\n  inventory_tracking: z.enum(['none', 'product', 'variant']).optional(),\n});\n\nconst CategorySchema = z\n  .object({\n    category_id: z.number(),\n    parent_id: z.number(),\n    name: z.string(),\n    description: z.string(),\n    url: z.object({\n      path: z.string(),\n    }),\n  })\n  .transform(\n    (data): Category => ({\n      categoryId: data.category_id,\n      parentId: data.parent_id,\n      name: data.name,\n      description: data.description,\n      path: data.url.path,\n    }),\n  );\n\nconst BrandSchema = z\n  .object({\n    id: z.number(),\n    name: z.string(),\n    custom_url: z.object({\n      url: z.string(),\n    }),\n  })\n  .transform(\n    (data): Brand => ({\n      id: data.id,\n      name: data.name,\n      path: data.custom_url.url,\n    }),\n  );\n\nconst transformCreateVariantData = (data: CreateVariantData) =>\n  VariantCreateSchema.parse({\n    sku: data.sku,\n    option_values: data.optionValues.map((option) => ({\n      label: option.label,\n      option_display_name: option.optionDisplayName,\n    })),\n    price: data.price,\n    sale_price: data.salePrice,\n    retail_price: data.retailPrice,\n    inventory_level: data.inventoryLevel,\n    inventory_warning_level: data.inventoryWarningLevel,\n  });\n\nconst transformCreateProductCustomUrl = (data: CreateProductData['customUrl']) => {\n  if (!data) {\n    return undefined;\n  }\n\n  return ProductCreateCustomUrlSchema.parse({\n    url: data.url,\n    is_customized: data.isCustomized,\n    create_redirect: data.createRedirect,\n  });\n};\n\nconst transformCreateProductData = (data: CreateProductData) =>\n  ProductCreateSchema.parse({\n    name: data.name,\n    weight: data.weight,\n    price: data.price,\n    sku: data.sku,\n    type: data.type,\n    description: data.description,\n    custom_url: transformCreateProductCustomUrl(data.customUrl),\n    is_visible: data.isVisible ?? true,\n    variants: data.variants?.map(transformCreateVariantData),\n    categories: data.categories,\n    inventory_level: data.inventoryLevel,\n    inventory_warning_level: data.inventoryWarningLevel,\n    inventory_tracking: data.inventoryTracking,\n  });\n\nexport const catalogHttpClient: CatalogApi = {\n  getProductById: async (id: number): Promise<Product> => {\n    const resp = await httpClient\n      .get(`/v3/catalog/products/${id}?include=variants`)\n      .parse(apiResponseSchema(ProductSchema));\n\n    return resp.data;\n  },\n  getCategories: async (filters = {}): Promise<Category[]> => {\n    const trees = await httpClient\n      .get(`/v3/catalog/trees?channel_id:in=${testEnv.BIGCOMMERCE_CHANNEL_ID ?? 1}`)\n      .parse(apiResponseSchema(z.array(z.object({ id: z.number() }))));\n\n    const params = new URLSearchParams({\n      is_visible: 'true',\n      'tree_id:in': trees.data.map((tree) => tree.id).join(','),\n      ...(filters.ids ? { 'id:in': filters.ids.join(',') } : {}),\n      ...(filters.nameLike ? { 'name:like': filters.nameLike } : {}),\n    });\n\n    const resp = await httpClient\n      .get(`/v3/catalog/trees/categories?${params}`)\n      .parse(apiResponseSchema(z.array(CategorySchema)));\n\n    return resp.data;\n  },\n  getBrands: async (filters = {}): Promise<Brand[]> => {\n    const params = new URLSearchParams({\n      ...(filters.ids ? { 'id:in': filters.ids.join(',') } : {}),\n      ...(filters.nameLike ? { 'name:like': filters.nameLike } : {}),\n    });\n\n    const resp = await httpClient\n      .get(`/v3/catalog/brands${params.size ? `?${params}` : ''}`)\n      .parse(apiResponseSchema(z.array(BrandSchema)));\n\n    return resp.data;\n  },\n  createProduct: async (data): Promise<Product> => {\n    const resp = await httpClient\n      .post('/v3/catalog/products', transformCreateProductData(data))\n      .parse(apiResponseSchema(ProductSchema));\n\n    const product = resp.data;\n\n    // Assign the product to the channel\n    await httpClient.put('/v3/catalog/products/channel-assignments', [\n      {\n        product_id: product.id,\n        channel_id: testEnv.BIGCOMMERCE_CHANNEL_ID ?? 1,\n      },\n    ]);\n\n    return product;\n  },\n  deleteProducts: async (ids: number[]): Promise<void> => {\n    if (ids.length > 0) {\n      await httpClient.delete(`/v3/catalog/products?id:in=${ids.join(',')}`);\n    }\n  },\n};\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/catalog/index.ts",
    "content": "export interface Variant {\n  readonly id: number;\n  readonly productId: number;\n  readonly sku: string;\n  readonly price?: number;\n  readonly salePrice?: number;\n  readonly retailPrice?: number;\n  readonly optionValues: Array<{\n    readonly id: number;\n    readonly label: string;\n    readonly optionId: number;\n    readonly optionDisplayName: string;\n  }>;\n  readonly inventoryLevel?: number;\n  readonly inventoryWarningLevel?: number;\n}\nexport interface Product {\n  readonly id: number;\n  readonly name: string;\n  readonly description: string;\n  readonly sku: string;\n  readonly price: number;\n  readonly retailPrice: number;\n  readonly salePrice: number;\n  readonly path: string;\n  readonly variants: Variant[];\n  readonly categories: number[];\n  readonly inventoryLevel?: number;\n  readonly inventoryWarningLevel?: number;\n  readonly inventoryTracking?: 'none' | 'product' | 'variant';\n  readonly customUrl: {\n    url: string;\n  };\n}\n\nexport interface CreateVariantData {\n  sku: string;\n  optionValues: Array<{\n    label: string;\n    optionDisplayName: string;\n  }>;\n  price?: number;\n  salePrice?: number;\n  retailPrice?: number;\n  inventoryLevel?: number;\n  inventoryWarningLevel?: number;\n}\n\nexport interface CreateProductData {\n  name: string;\n  weight: number;\n  price: number;\n  salePrice?: number;\n  retailPrice?: number;\n  sku?: string;\n  type?: 'physical' | 'digital';\n  description?: string;\n  isVisible?: boolean;\n  categories?: number[];\n  variants?: CreateVariantData[];\n  inventoryLevel?: number;\n  inventoryWarningLevel?: number;\n  inventoryTracking?: 'none' | 'product' | 'variant';\n  customUrl?: {\n    url: string;\n    isCustomized?: boolean;\n    createRedirect?: boolean;\n  };\n}\n\nexport interface Category {\n  readonly categoryId: number;\n  readonly parentId: number;\n  readonly name: string;\n  readonly description: string;\n  readonly path: string;\n}\n\nexport interface Brand {\n  readonly id: number;\n  readonly name: string;\n  readonly path: string;\n}\n\nexport interface CatalogApi {\n  getProductById: (id: number) => Promise<Product>;\n  getCategories: (filters?: { nameLike?: string; ids?: number[] }) => Promise<Category[]>;\n  getBrands: (filters?: { nameLike?: string; ids?: number[] }) => Promise<Brand[]>;\n  createProduct: (data: CreateProductData) => Promise<Product>;\n  deleteProducts: (ids: number[]) => Promise<void>;\n}\n\nexport { catalogHttpClient } from './http';\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/client.ts",
    "content": "import { z } from 'zod';\n\nimport { testEnv } from '~/tests/environment';\n\nimport { TestApiClientResponseError } from './errors';\n\nclass ApiClientResponse extends Promise<Response> {\n  async parse<Out, In = Out>(schema: z.ZodType<Out, z.ZodTypeDef, In>): Promise<Out> {\n    const resp = await this.then((res) => res.text());\n\n    return schema.parse(resp.length ? JSON.parse(resp) : undefined);\n  }\n}\n\nfunction httpRequest(path: string, config: RequestInit): ApiClientResponse {\n  // eslint-disable-next-line @typescript-eslint/no-misused-promises\n  return new ApiClientResponse(async (resolve, reject) => {\n    if (!testEnv.BIGCOMMERCE_ACCESS_TOKEN) {\n      throw new Error('BIGCOMMERCE_ACCESS_TOKEN is not set');\n    }\n\n    if (!testEnv.BIGCOMMERCE_STORE_HASH) {\n      throw new Error('BIGCOMMERCE_STORE_HASH is not set');\n    }\n\n    const {\n      BIGCOMMERCE_ACCESS_TOKEN: accessToken,\n      BIGCOMMERCE_STORE_HASH: storeHash,\n      BIGCOMMERCE_ADMIN_API_HOST: host,\n    } = testEnv;\n\n    const { method, headers = {}, body } = config;\n    const req = new Request(`https://${host}/stores/${storeHash}${path}`, {\n      method,\n      headers: {\n        Accept: 'application/json',\n        'Content-Type': 'application/json',\n        'X-Auth-Token': accessToken,\n        ...Object.fromEntries(new Headers(headers).entries()),\n      },\n      body,\n    });\n\n    const response = await fetch(req);\n\n    if (!response.ok) {\n      reject(await TestApiClientResponseError.create(req, response));\n    }\n\n    resolve(response);\n  });\n}\n\nexport const httpClient = {\n  get: (path: string) => httpRequest(path, { method: 'GET' }),\n  post: (path: string, body: unknown, config?: RequestInit) =>\n    httpRequest(path, { ...config, method: 'POST', body: JSON.stringify(body) }),\n  put: (path: string, body: unknown, config?: RequestInit) =>\n    httpRequest(path, { ...config, method: 'PUT', body: JSON.stringify(body) }),\n  delete: (path: string) => httpRequest(path, { method: 'DELETE' }),\n};\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/currencies/http.ts",
    "content": "import { z } from 'zod';\n\nimport { testEnv } from '~/tests/environment';\n\nimport { httpClient } from '../client';\nimport { apiResponseSchema } from '../schema';\n\nimport { CurrenciesApi, Currency, CurrencyAssignments } from '.';\n\nconst CurrencyAssignmentSchema = z\n  .object({\n    enabled_currencies: z.array(z.string()),\n    default_currency: z.string(),\n  })\n  .transform(\n    (data): CurrencyAssignments => ({\n      enabledCurrencies: data.enabled_currencies,\n      defaultCurrency: data.default_currency,\n    }),\n  );\n\nconst CurrencySchema = z\n  .object({\n    id: z.number(),\n    is_default: z.boolean(),\n    enabled: z.boolean(),\n    decimal_places: z.number(),\n    currency_exchange_rate: z.coerce.number(),\n    currency_code: z.string(),\n  })\n  .transform(\n    (data): Currency => ({\n      id: data.id,\n      isDefault: data.is_default,\n      isEnabled: data.enabled,\n      decimalPlaces: data.decimal_places,\n      exchangeRate: data.currency_exchange_rate,\n      currencyCode: data.currency_code,\n    }),\n  );\n\nexport const currenciesHttpClient: CurrenciesApi = {\n  getCurrencyAssignments: async (): Promise<CurrencyAssignments> => {\n    const resp = await httpClient\n      .get(`/v3/channels/${testEnv.BIGCOMMERCE_CHANNEL_ID ?? 1}/currency-assignments`)\n      .parse(apiResponseSchema(CurrencyAssignmentSchema));\n\n    return resp.data;\n  },\n  getCurrencies: async (): Promise<Currency[]> => {\n    const resp = await httpClient.get(`/v2/currencies?limit=250`).parse(z.array(CurrencySchema));\n\n    return resp;\n  },\n};\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/currencies/index.ts",
    "content": "export interface CurrencyAssignments {\n  readonly enabledCurrencies: string[];\n  readonly defaultCurrency: string;\n}\n\nexport interface Currency {\n  readonly id: number;\n  readonly isDefault: boolean;\n  readonly isEnabled: boolean;\n  readonly decimalPlaces: number;\n  readonly exchangeRate: number;\n  readonly currencyCode: string;\n}\n\nexport interface CurrenciesApi {\n  getCurrencyAssignments: () => Promise<CurrencyAssignments>;\n  getCurrencies: () => Promise<Currency[]>;\n}\n\nexport { currenciesHttpClient } from './http';\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/customers/http.ts",
    "content": "import { z } from 'zod';\n\nimport { testEnv } from '~/tests/environment';\nimport {\n  Address,\n  CreateAddressData,\n  CreateCustomerData,\n  CreateWishlistData,\n  Customer,\n  CustomersApi,\n  Wishlist,\n} from '~/tests/fixtures/utils/api/customers';\n\nimport { httpClient } from '../client';\nimport { apiResponseSchema } from '../schema';\n\nconst AddressSchema = z\n  .object({\n    id: z.number(),\n    address1: z.string(),\n    address2: z.string().optional(),\n    address_type: z.string(),\n    city: z.string(),\n    company: z.string().optional(),\n    country: z.string(),\n    country_code: z.string(),\n    customer_id: z.number().optional(),\n    first_name: z.string(),\n    last_name: z.string(),\n    phone: z.string().optional(),\n    postal_code: z.string(),\n    state_or_province: z.string().optional(),\n  })\n  .transform(\n    (data): Address => ({\n      id: data.id,\n      address1: data.address1,\n      city: data.city,\n      country: data.country,\n      countryCode: data.country_code,\n      firstName: data.first_name,\n      lastName: data.last_name,\n      postalCode: data.postal_code,\n      customerId: data.customer_id,\n      addressType: data.address_type,\n      address2: data.address2,\n      company: data.company,\n      phone: data.phone,\n      stateOrProvince: data.state_or_province,\n    }),\n  );\n\nconst AddressCreateSchema = z.object({\n  customer_id: z.number().optional(),\n  first_name: z.string(),\n  last_name: z.string(),\n  address1: z.string(),\n  address2: z.string().optional(),\n  city: z.string(),\n  company: z.string().optional(),\n  country_code: z.string(),\n  phone: z.string().optional(),\n  postal_code: z.string(),\n  state_or_province: z.string().optional(),\n});\n\nconst CustomerSchema = z\n  .object({\n    id: z.number(),\n    address_count: z.number().optional(),\n    addresses: z.array(AddressSchema).optional(),\n    company: z.string().optional(),\n    customer_group_id: z.number().optional(),\n    email: z.string(),\n    first_name: z.string(),\n    last_name: z.string(),\n    notes: z.string().optional(),\n    phone: z.string().optional(),\n    origin_channel_id: z.number(),\n    channel_ids: z.nullable(z.array(z.number())),\n  })\n  .transform(\n    (data): Customer => ({\n      id: data.id,\n      email: data.email,\n      firstName: data.first_name,\n      lastName: data.last_name,\n      originChannelId: data.origin_channel_id,\n      channelIds: data.channel_ids,\n      addressCount: data.address_count ?? 0,\n      addresses: data.addresses,\n      company: data.company,\n      customerGroupId: data.customer_group_id ?? 0,\n      notes: data.notes,\n      phone: data.phone,\n    }),\n  );\n\nconst CustomerCreateSchema = z.object({\n  addresses: z.array(AddressCreateSchema).optional(),\n  authentication: z\n    .object({\n      new_password: z.string(),\n    })\n    .optional(),\n  company: z.string().optional(),\n  customer_group_id: z.number().optional(),\n  email: z.string(),\n  first_name: z.string(),\n  last_name: z.string(),\n  notes: z.string().optional(),\n  phone: z.string().optional(),\n  origin_channel_id: z.number(),\n  channel_ids: z.nullable(z.array(z.number())).optional(),\n});\n\nconst WishlistSchema = z\n  .object({\n    id: z.number(),\n    customer_id: z.number(),\n    name: z.string(),\n    is_public: z.boolean(),\n    token: z.string(),\n    items: z.array(\n      z.object({\n        id: z.number(),\n        product_id: z.number(),\n        variant_id: z.number().optional(),\n      }),\n    ),\n  })\n  .transform(\n    (data): Wishlist => ({\n      id: data.id,\n      customerId: data.customer_id,\n      name: data.name,\n      isPublic: data.is_public,\n      token: data.token,\n      items: data.items.map((item) => ({\n        id: item.id,\n        productId: item.product_id,\n        variantId: item.variant_id,\n      })),\n    }),\n  );\n\nconst WishlistCreateSchema = z.object({\n  customer_id: z.number(),\n  name: z.string(),\n  is_public: z.boolean(),\n  items: z.array(\n    z.object({\n      product_id: z.number(),\n      variant_id: z.number().optional(),\n    }),\n  ),\n});\n\nconst transformCreateAddressData = (data: CreateAddressData): z.infer<typeof AddressCreateSchema> =>\n  AddressCreateSchema.parse({\n    customer_id: data.customerId,\n    first_name: data.firstName,\n    last_name: data.lastName,\n    address1: data.address1,\n    address2: data.address2,\n    city: data.city,\n    company: data.company,\n    country_code: data.countryCode,\n    phone: data.phone,\n    postal_code: data.postalCode,\n    state_or_province: data.stateOrProvince,\n  });\n\nconst transformCreateCustomerData = (\n  data: CreateCustomerData,\n): z.infer<typeof CustomerCreateSchema> =>\n  CustomerCreateSchema.parse({\n    authentication: { new_password: data.password },\n    email: data.email,\n    first_name: data.firstName,\n    last_name: data.lastName,\n    origin_channel_id: data.originChannelId,\n    channel_ids: data.channelIds ?? null,\n    company: data.company,\n    customer_group_id: data.customerGroupId ?? 0,\n    notes: data.notes,\n    phone: data.phone,\n    addresses: data.addresses?.map(transformCreateAddressData),\n  });\n\nconst transformCreateWishlistData = (data: CreateWishlistData) =>\n  WishlistCreateSchema.parse({\n    customer_id: data.customerId,\n    name: data.name,\n    is_public: data.isPublic,\n    items: data.items.map((item) => ({\n      product_id: item.productId,\n      variant_id: item.variantId,\n    })),\n  });\n\nexport const customersHttpClient: CustomersApi = {\n  getById: async (customerId: number, includeAddresses = false): Promise<Customer> => {\n    const addressesQuery = includeAddresses ? '&include=addresses' : '';\n    const resp = await httpClient\n      .get(`/v3/customers?id:in=${customerId}${addressesQuery}`)\n      .parse(apiResponseSchema(z.array(CustomerSchema)));\n\n    const customer = resp.data[0];\n\n    if (!customer) {\n      throw new Error(`No customer found with the provided ID: ${customerId}`);\n    }\n\n    if (\n      customer.originChannelId !== (testEnv.BIGCOMMERCE_CHANNEL_ID ?? 1) &&\n      customer.channelIds !== null // if channelIds is null, the customer belongs to all channels\n    ) {\n      throw new Error(\n        `Customer ${customerId} is not from the correct channel. Expected ${\n          testEnv.BIGCOMMERCE_CHANNEL_ID ?? 1\n        }, got ${customer.originChannelId}.`,\n      );\n    }\n\n    return customer;\n  },\n  getByEmail: async (email: string, includeAddresses = false): Promise<Customer> => {\n    const addressesQuery = includeAddresses ? '&include=addresses' : '';\n    const resp = await httpClient\n      .get(`/v3/customers?email:in=${email}${addressesQuery}`)\n      .parse(apiResponseSchema(z.array(CustomerSchema)));\n\n    const customer = resp.data[0];\n\n    if (!customer) {\n      throw new Error(`No customer found with the provided email: ${email}`);\n    }\n\n    if (customer.originChannelId !== (testEnv.BIGCOMMERCE_CHANNEL_ID ?? 1)) {\n      throw new Error(\n        `Customer ${customer.id} is not from the correct channel. Expected ${\n          testEnv.BIGCOMMERCE_CHANNEL_ID ?? 1\n        }, got ${customer.originChannelId}.`,\n      );\n    }\n\n    return customer;\n  },\n  getAddresses: async (customerId: number): Promise<Address[]> => {\n    const resp = await httpClient\n      .get(`/v3/customers/addresses?customer_id:in=${customerId}`)\n      .parse(apiResponseSchema(z.array(AddressSchema)));\n\n    return resp.data;\n  },\n  getWishlists: async (customerId: number): Promise<Wishlist[]> => {\n    const resp = await httpClient\n      .get(`/v3/wishlists?customer_id=${customerId}`)\n      .parse(apiResponseSchema(z.array(WishlistSchema)));\n\n    return resp.data;\n  },\n  create: async (data: CreateCustomerData): Promise<Customer> => {\n    const resp = await httpClient\n      .post('/v3/customers', [transformCreateCustomerData(data)])\n      .parse(apiResponseSchema(z.array(CustomerSchema)));\n\n    const customer = resp.data[0];\n\n    if (!customer) {\n      throw new Error('Customer not found in response');\n    }\n\n    customer.password = data.password;\n\n    return customer;\n  },\n  createAddress: async (data: CreateAddressData): Promise<Address> => {\n    const resp = await httpClient\n      .post('/v3/customers/addresses', [transformCreateAddressData(data)])\n      .parse(apiResponseSchema(z.array(AddressSchema)));\n\n    const createdAddress = resp.data[0];\n\n    if (!createdAddress) {\n      throw new Error('No address found in response');\n    }\n\n    return createdAddress;\n  },\n  createWishlist: async (data: CreateWishlistData): Promise<Wishlist> => {\n    const resp = await httpClient\n      .post('/v3/wishlists', transformCreateWishlistData(data))\n      .parse(apiResponseSchema(WishlistSchema));\n\n    return resp.data;\n  },\n  delete: async (ids: number[]): Promise<void> => {\n    if (ids.length > 0) {\n      await httpClient.delete(`/v3/customers?id:in=${ids.join(',')}`);\n    }\n  },\n  deleteAddresses: async (ids: number[]): Promise<void> => {\n    if (ids.length > 0) {\n      await httpClient.delete(`/v3/customers/addresses?id:in=${ids.join(',')}`);\n    }\n  },\n  deleteWishlists: async (ids: number[]): Promise<void> => {\n    if (ids.length > 0) {\n      await Promise.all(ids.map((id) => httpClient.delete(`/v3/wishlists/${id}`)));\n    }\n  },\n};\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/customers/index.ts",
    "content": "export interface Address {\n  readonly id: number;\n  readonly address1: string;\n  readonly city: string;\n  readonly country: string;\n  readonly countryCode: string;\n  readonly firstName: string;\n  readonly lastName: string;\n  readonly postalCode: string;\n  readonly customerId?: number;\n  readonly addressType?: string;\n  readonly address2?: string;\n  readonly company?: string;\n  readonly phone?: string;\n  readonly stateOrProvince?: string;\n}\n\nexport interface CreateAddressData {\n  address1: string;\n  address2?: string;\n  city: string;\n  company?: string;\n  countryCode: string;\n  customerId?: number;\n  firstName: string;\n  lastName: string;\n  phone?: string;\n  postalCode: string;\n  stateOrProvince?: string;\n}\n\nexport interface Customer {\n  readonly id: number;\n  readonly email: string;\n  readonly firstName: string;\n  readonly lastName: string;\n  readonly originChannelId: number;\n  readonly channelIds: number[] | null;\n  readonly addressCount?: number;\n  readonly addresses?: Address[];\n  readonly company?: string;\n  readonly customerGroupId: number;\n  readonly notes?: string;\n  readonly phone?: string;\n  password?: string;\n}\n\nexport interface CreateCustomerData {\n  addresses?: CreateAddressData[];\n  company?: string;\n  customerGroupId?: number;\n  email: string;\n  firstName: string;\n  lastName: string;\n  notes?: string;\n  password?: string;\n  phone?: string;\n  channelIds?: number[];\n  originChannelId: number;\n}\n\nexport interface Wishlist {\n  readonly id: number;\n  readonly customerId: number;\n  readonly name: string;\n  readonly isPublic: boolean;\n  readonly token: string;\n  readonly items: Array<{ id: number; productId: number; variantId?: number }>;\n}\n\nexport interface CreateWishlistData {\n  customerId: number;\n  name: string;\n  isPublic: boolean;\n  items: Array<{ productId: number; variantId?: number }>;\n}\n\nexport interface CustomersApi {\n  getById: (id: number, includeAddresses?: boolean) => Promise<Customer>;\n  getByEmail: (email: string, includeAddresses?: boolean) => Promise<Customer>;\n  getAddresses: (customerId: number) => Promise<Address[]>;\n  getWishlists: (customerId: number) => Promise<Wishlist[]>;\n  create: (data: CreateCustomerData) => Promise<Customer>;\n  createAddress: (data: CreateAddressData) => Promise<Address>;\n  createWishlist: (data: CreateWishlistData) => Promise<Wishlist>;\n  delete: (ids: number[]) => Promise<void>;\n  deleteAddresses: (ids: number[]) => Promise<void>;\n  deleteWishlists: (ids: number[]) => Promise<void>;\n}\n\nexport { customersHttpClient } from './http';\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/errors/index.ts",
    "content": "export { TestApiClientResponseError } from './response-error';\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/errors/response-error.ts",
    "content": "export class TestApiClientResponseError extends Error {\n  readonly name = 'TestApiClientResponseError';\n\n  constructor(request: Request, response: Response, responseText?: string) {\n    const { method, url } = request;\n    const { pathname } = new URL(url);\n    const { status, statusText } = response;\n\n    const message = `\n    BigCommerce API returned ${status}\n    ${method} '${pathname}' failed with ${status} ${statusText}\n    ${responseText}\n    `;\n\n    super(message);\n  }\n\n  static async create(request: Request, response: Response) {\n    try {\n      let errorResponse: string;\n\n      const contentType = response.headers.get('content-type');\n\n      if (contentType && ['application/json', 'application/json+problem'].includes(contentType)) {\n        const data: unknown = await response.json();\n\n        errorResponse = JSON.stringify(data, null, 2);\n      }\n\n      errorResponse = await response.text();\n\n      return new TestApiClientResponseError(request, response, errorResponse);\n    } catch {\n      return new TestApiClientResponseError(request, response);\n    }\n  }\n}\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/index.ts",
    "content": "import { BlogApi, blogHttpClient } from '~/tests/fixtures/utils/api/blog';\nimport { CatalogApi, catalogHttpClient } from '~/tests/fixtures/utils/api/catalog';\nimport { CurrenciesApi, currenciesHttpClient } from '~/tests/fixtures/utils/api/currencies';\nimport { CustomersApi, customersHttpClient } from '~/tests/fixtures/utils/api/customers';\nimport { OrdersApi, ordersHttpClient } from '~/tests/fixtures/utils/api/orders';\nimport { PromotionsApi, promotionsHttpClient } from '~/tests/fixtures/utils/api/promotions';\nimport { RedirectsApi, redirectsHttpClient } from '~/tests/fixtures/utils/api/redirects';\nimport { SettingsApi, settingsHttpClient } from '~/tests/fixtures/utils/api/settings';\nimport { SubscribeApi, subscribeHttpClient } from '~/tests/fixtures/utils/api/subscribe';\nimport { WebPagesApi, webPagesHttpClient } from '~/tests/fixtures/utils/api/webpages';\n\nexport interface ApiClient {\n  blog: BlogApi;\n  catalog: CatalogApi;\n  customers: CustomersApi;\n  currencies: CurrenciesApi;\n  orders: OrdersApi;\n  promotions: PromotionsApi;\n  settings: SettingsApi;\n  subscribe: SubscribeApi;\n  webPages: WebPagesApi;\n  redirects: RedirectsApi;\n}\n\nexport const httpApiClient: ApiClient = {\n  blog: blogHttpClient,\n  catalog: catalogHttpClient,\n  customers: customersHttpClient,\n  currencies: currenciesHttpClient,\n  orders: ordersHttpClient,\n  promotions: promotionsHttpClient,\n  settings: settingsHttpClient,\n  subscribe: subscribeHttpClient,\n  webPages: webPagesHttpClient,\n  redirects: redirectsHttpClient,\n};\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/orders/http.ts",
    "content": "import { faker } from '@faker-js/faker';\nimport { z } from 'zod';\n\nimport { testEnv } from '~/tests/environment';\n\nimport { httpClient } from '../client';\n\nimport { Order, OrderAddress, OrdersApi } from '.';\n\nconst OrderAddressSchema = z\n  .object({\n    first_name: z.string(),\n    last_name: z.string(),\n    company: z.string().optional(),\n    street_1: z.string(),\n    street_2: z.string().optional(),\n    city: z.string().optional(),\n    state: z.string().optional(),\n    zip: z.string(),\n    country: z.string(),\n    country_iso2: z.string(),\n    phone: z.string().optional(),\n    email: z.string().optional(),\n  })\n  .transform(\n    (data): OrderAddress => ({\n      firstName: data.first_name,\n      lastName: data.last_name,\n      company: data.company,\n      street1: data.street_1,\n      street2: data.street_2,\n      city: data.city,\n      state: data.state,\n      zip: data.zip,\n      country: data.country,\n      countryIso2: data.country_iso2,\n      phone: data.phone,\n      email: data.email,\n    }),\n  );\n\nconst OrderSchema = z\n  .object({\n    id: z.number(),\n    customer_id: z.number(),\n    currency_code: z.string(),\n    currency_id: z.number(),\n    date_created: z.string(),\n    default_currency_code: z.string(),\n    default_currency_id: z.number(),\n    status_id: z.number(),\n    status: z.string(),\n    subtotal_ex_tax: z.string(),\n    subtotal_tax: z.string(),\n    total_inc_tax: z.string(),\n    items_total: z.number(),\n    discount_amount: z.string(),\n    coupon_discount: z.string(),\n    billing_address: OrderAddressSchema,\n  })\n  .transform(\n    (data): Order => ({\n      id: data.id,\n      customerId: data.customer_id,\n      currencyCode: data.currency_code,\n      currencyId: data.currency_id,\n      dateCreated: data.date_created,\n      defaultCurrencyCode: data.default_currency_code,\n      defaultCurrencyId: data.default_currency_id,\n      statusId: data.status_id,\n      status: data.status,\n      subtotalExTax: data.subtotal_ex_tax,\n      subtotalTax: data.subtotal_tax,\n      totalIncTax: data.total_inc_tax,\n      itemsTotal: data.items_total,\n      discountAmount: data.discount_amount,\n      couponDiscount: data.coupon_discount,\n      billingAddress: data.billing_address,\n    }),\n  );\n\nexport const ordersHttpClient: OrdersApi = {\n  get: async (customerId?: number): Promise<Order[]> => {\n    const params = new URLSearchParams({\n      is_deleted: 'false',\n      ...(customerId ? { customer_id: customerId.toString() } : {}),\n    });\n\n    const resp = await httpClient\n      .get(`/v2/orders?${params}`)\n      .parse(z.array(OrderSchema).optional());\n\n    return resp ?? [];\n  },\n  create: async (productId: number, customerId?: number): Promise<Order> => {\n    const first = faker.person.firstName();\n    const last = faker.person.lastName();\n    const street1 = faker.location.streetAddress();\n    const city = faker.location.city();\n    const state = faker.location.state();\n    const zip = faker.location.zipCode('#####');\n    const countryCode = 'US';\n\n    return httpClient\n      .post('/v2/orders', {\n        status_id: 1,\n        channel_id: testEnv.BIGCOMMERCE_CHANNEL_ID ?? 1,\n        customer_id: customerId ?? 0,\n        billing_address: {\n          first_name: first,\n          last_name: last,\n          email: faker.internet.email({\n            firstName: first,\n            lastName: last,\n            provider: 'example.com',\n          }),\n          street_1: street1,\n          city,\n          state,\n          zip,\n          country_iso2: countryCode,\n        },\n        products: [\n          {\n            product_id: productId,\n            quantity: 1,\n          },\n        ],\n      })\n      .parse(OrderSchema);\n  },\n  delete: async (ids: number[]): Promise<void> => {\n    if (ids.length > 0) {\n      await Promise.all(ids.map((id) => httpClient.delete(`/v2/orders/${id}`)));\n    }\n  },\n};\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/orders/index.ts",
    "content": "export interface OrderAddress {\n  readonly firstName: string;\n  readonly lastName: string;\n  readonly street1: string;\n  readonly zip: string;\n  readonly country: string;\n  readonly countryIso2: string;\n  readonly city?: string;\n  readonly email?: string;\n  readonly company?: string;\n  readonly street2?: string;\n  readonly state?: string;\n  readonly phone?: string;\n}\n\nexport interface Order {\n  readonly id: number;\n  readonly customerId: number;\n  readonly dateCreated: string;\n  readonly statusId: number;\n  readonly status: string;\n  readonly subtotalExTax: string;\n  readonly subtotalTax: string;\n  readonly totalIncTax: string;\n  readonly itemsTotal: number;\n  readonly discountAmount: string;\n  readonly couponDiscount: string;\n  readonly billingAddress: OrderAddress;\n  readonly currencyCode: string;\n  readonly currencyId: number;\n  readonly defaultCurrencyCode: string;\n  readonly defaultCurrencyId: number;\n}\n\nexport interface OrdersApi {\n  get: (customerId?: number) => Promise<Order[]>;\n  create: (productId: number, customerId?: number) => Promise<Order>;\n  delete: (ids: number[]) => Promise<void>;\n}\n\nexport { ordersHttpClient } from './http';\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/promotions/http.ts",
    "content": "import { faker } from '@faker-js/faker';\nimport { z } from 'zod';\n\nimport { testEnv } from '~/tests/environment';\n\nimport { httpClient } from '../client';\nimport { apiResponseSchema } from '../schema';\n\nimport { Coupon, PromotionsApi } from '.';\n\nconst CouponSchema = z\n  .object({\n    id: z.number(),\n    code: z.string(),\n  })\n  .transform(\n    (data): Coupon => ({\n      id: data.id,\n      code: data.code,\n    }),\n  );\n\nexport const promotionsHttpClient: PromotionsApi = {\n  createCouponCode: async () => {\n    const promotion = await httpClient\n      .post('/v3/promotions', {\n        name: `Catalyst Test Promotion ${faker.string.alpha(10)}`,\n        channels: [{ id: testEnv.BIGCOMMERCE_CHANNEL_ID ?? 1 }],\n        rules: [\n          {\n            action: {\n              cart_value: {\n                discount: {\n                  percentage_amount: 10,\n                },\n              },\n            },\n          },\n        ],\n        redemption_type: 'COUPON',\n        status: 'ENABLED',\n      })\n      .parse(apiResponseSchema(z.object({ id: z.number() }).passthrough()));\n\n    const coupon = await httpClient\n      .post(`/v3/promotions/${promotion.data.id}/codes`, {\n        code: `CATALYST-TEST-${faker.string.alpha(10).toUpperCase()}`,\n        max_uses: 1,\n      })\n      .parse(apiResponseSchema(CouponSchema));\n\n    return {\n      promotionId: promotion.data.id,\n      coupon: coupon.data,\n    };\n  },\n  deleteCouponCode: async (promotionId: number, couponId: number) => {\n    await httpClient.delete(`/v3/promotions/${promotionId}/codes/${couponId}`);\n    await httpClient.delete(`/v3/promotions/${promotionId}`);\n  },\n};\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/promotions/index.ts",
    "content": "export interface Coupon {\n  readonly id: number;\n  readonly code: string;\n}\n\nexport interface PromotionWithCoupon {\n  promotionId: number;\n  coupon: Coupon;\n}\n\nexport interface PromotionsApi {\n  createCouponCode: () => Promise<PromotionWithCoupon>;\n  deleteCouponCode: (promotionId: number, couponId: number) => Promise<void>;\n}\n\nexport { promotionsHttpClient } from './http';\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/redirects/http.ts",
    "content": "import { z } from 'zod';\n\nimport { testEnv } from '~/tests/environment';\n\nimport { httpClient } from '../client';\nimport { apiResponseSchema } from '../schema';\n\nimport { Redirect, RedirectsApi, UpsertRedirectData } from '.';\n\nconst RedirectToTypeSchema = z.enum(['product', 'brand', 'category', 'page', 'post', 'url']);\n\nconst RedirectToEntitySchema = z.object({\n  type: RedirectToTypeSchema.exclude(['url']),\n  entity_id: z.number(),\n});\n\nconst RedirectToUrlSchema = z.object({\n  type: z.literal('url'),\n  url: z.string(),\n});\n\nconst RedirectToSchema = z.union([RedirectToEntitySchema, RedirectToUrlSchema]);\n\nconst RedirectSchema = z\n  .object({\n    id: z.number(),\n    site_id: z.number(),\n    from_path: z.string(),\n    to: RedirectToSchema,\n  })\n  .transform(\n    (data): Redirect => ({\n      id: data.id,\n      siteId: data.site_id,\n      fromPath: data.from_path,\n      to:\n        data.to.type === 'url'\n          ? { type: 'url', url: data.to.url }\n          : {\n              type: data.to.type,\n              entityId: data.to.entity_id,\n            },\n    }),\n  );\n\nconst RedirectUpsertDataSchema = z.object({\n  site_id: z.number(),\n  from_path: z.string(),\n  to: RedirectToSchema,\n});\n\nconst transformRedirectTo = (data: UpsertRedirectData['to']) => {\n  if (data.type === 'url') {\n    return RedirectToSchema.parse({\n      type: 'url',\n      url: data.url,\n    });\n  }\n\n  return RedirectToSchema.parse({\n    type: data.type,\n    entity_id: data.entityId,\n  });\n};\n\nconst transformRedirectUpsertData = (data: UpsertRedirectData & { siteId: number }) =>\n  RedirectUpsertDataSchema.parse({\n    site_id: data.siteId,\n    from_path: data.fromPath,\n    to: transformRedirectTo(data.to),\n  });\n\nexport const redirectsHttpClient: RedirectsApi = {\n  upsert: async (data) => {\n    // Redirects require a siteID, so it must be fetched via the channel ID\n    const channelId = Number(testEnv.BIGCOMMERCE_CHANNEL_ID);\n    const {\n      data: { id: siteId },\n    } = await httpClient\n      .get(`/v3/channels/${channelId}/site`)\n      .parse(apiResponseSchema(z.object({ id: z.number() })));\n\n    await httpClient\n      .put('/v3/storefront/redirects', [transformRedirectUpsertData({ ...data, siteId })])\n      .parse(apiResponseSchema(z.array(RedirectSchema)));\n\n    // Upsert redirects API will not return created redirects, just a 200 success with an empty array.\n    // Because of this, a GET needs to be done for the newly created redirect in order to get the ID.\n\n    const created = await httpClient\n      .get(`/v3/storefront/redirects?site_id=${siteId}&keyword=${data.fromPath}`)\n      .parse(apiResponseSchema(z.array(RedirectSchema)));\n\n    return created.data[0];\n  },\n  delete: async (ids: number[]) => {\n    if (ids.length > 0) {\n      await httpClient.delete(`/v3/storefront/redirects?id:in=${ids.join(',')}`);\n    }\n  },\n};\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/redirects/index.ts",
    "content": "type RedirectToType = 'product' | 'brand' | 'category' | 'page' | 'post' | 'url';\n\ninterface RedirectToEntity {\n  type: Exclude<RedirectToType, 'url'>;\n  entityId: number;\n}\n\ninterface RedirectToUrl {\n  type: 'url';\n  url: string;\n}\n\nexport interface Redirect {\n  readonly id: number;\n  readonly siteId: number;\n  readonly fromPath: string;\n  readonly to: RedirectToEntity | RedirectToUrl;\n}\n\nexport interface UpsertRedirectData {\n  fromPath: string;\n  to: RedirectToEntity | RedirectToUrl;\n}\n\nexport interface RedirectsApi {\n  upsert: (data: UpsertRedirectData) => Promise<Redirect | undefined>;\n  delete: (ids: number[]) => Promise<void>;\n}\n\nexport { redirectsHttpClient } from './http';\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/schema.ts",
    "content": "import { z } from 'zod';\n\nconst metaSchema = z.object({\n  pagination: z\n    .object({\n      total: z.number(),\n      count: z.number(),\n      per_page: z.number(),\n      current_page: z.number(),\n      total_pages: z.number(),\n    })\n    .optional(),\n});\n\nexport const apiResponseSchema = <T extends z.ZodSchema>(\n  schema: T,\n): z.ZodObject<{ data: T; meta: typeof metaSchema }> =>\n  z.object({\n    data: schema,\n    meta: metaSchema,\n  });\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/settings/http.ts",
    "content": "import { z } from 'zod';\n\nimport { httpClient } from '../client';\nimport { apiResponseSchema } from '../schema';\n\nimport { InventorySettings, SettingsApi } from '.';\n\nconst InventorySettingsSchema = z\n  .object({\n    default_out_of_stock_message: z.string(),\n    show_out_of_stock_message: z.boolean(),\n    stock_level_display: z.enum(['dont_show', 'show', 'show_when_low']).nullable(),\n  })\n  .transform(\n    (data): InventorySettings => ({\n      defaultOutOfStockMessage: data.default_out_of_stock_message,\n      showOutOfStockMessage: data.show_out_of_stock_message,\n      stockLevelDisplay: data.stock_level_display,\n    }),\n  );\n\nconst transformInventorySettingsData = (data: InventorySettings) => ({\n  default_out_of_stock_message: data.defaultOutOfStockMessage,\n  show_out_of_stock_message: data.showOutOfStockMessage,\n  stock_level_display: data.stockLevelDisplay,\n});\n\nexport const settingsHttpClient: SettingsApi = {\n  getInventorySettings: async (): Promise<InventorySettings> => {\n    const resp = await httpClient\n      .get(`/v3/settings/inventory`)\n      .parse(apiResponseSchema(InventorySettingsSchema));\n\n    return resp.data;\n  },\n  setInventorySettings: async (settings: InventorySettings): Promise<void> => {\n    await httpClient.put(`/v3/settings/inventory`, transformInventorySettingsData(settings));\n  },\n};\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/settings/index.ts",
    "content": "export interface InventorySettings {\n  readonly defaultOutOfStockMessage?: string;\n  readonly showOutOfStockMessage?: boolean;\n  readonly stockLevelDisplay?: 'dont_show' | 'show' | 'show_when_low' | null;\n}\n\nexport interface SettingsApi {\n  getInventorySettings: () => Promise<InventorySettings>;\n  setInventorySettings: (settings: InventorySettings) => Promise<void>;\n}\n\nexport { settingsHttpClient } from './http';\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/subscribe/http.ts",
    "content": "import { testEnv } from '~/tests/environment';\n\nimport { httpClient } from '../client';\n\nimport { SubscribeApi } from '.';\n\nexport const subscribeHttpClient: SubscribeApi = {\n  subscribe: async (email: string, firstName: string, lastName: string) => {\n    await httpClient.post('/v3/customers/subscribers', {\n      channel_id: testEnv.BIGCOMMERCE_CHANNEL_ID ?? 1,\n      email,\n      first_name: firstName,\n      last_name: lastName,\n    });\n  },\n  unsubscribe: async (email: string) => {\n    await httpClient.delete(`/v3/customers/subscribers?email=${encodeURIComponent(email)}`);\n  },\n};\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/subscribe/index.ts",
    "content": "export interface SubscribeApi {\n  subscribe(email: string, firstName: string, lastName: string): Promise<void>;\n  unsubscribe(email: string): Promise<void>;\n}\n\nexport { subscribeHttpClient } from './http';\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/webpages/http.ts",
    "content": "import { z } from 'zod';\n\nimport { testEnv } from '~/tests/environment';\n\nimport { httpClient } from '../client';\nimport { apiResponseSchema } from '../schema';\n\nimport { CreateWebPageData, WebPage, WebPagesApi } from '.';\n\nconst WebPageTypeSchema = z.enum(['page', 'link', 'contact_form', 'raw']);\nconst ContactFieldTypeSchema = z.array(\n  z.enum(['fullname', 'companyname', 'phone', 'orderno', 'rma']),\n);\n\nconst WebPageSchema = z\n  .object({\n    id: z.number(),\n    parent_id: z.number(),\n    name: z.string(),\n    is_visible: z.boolean(),\n    is_customers_only: z.boolean(),\n    type: WebPageTypeSchema,\n    url: z.string().nullable().optional(),\n    email: z.string().nullable().optional(),\n    link: z.string().nullable().optional(),\n    body: z.string().nullable().optional(),\n    contact_fields: z.string().nullable().optional(),\n  })\n  .transform(\n    (data): WebPage => ({\n      id: data.id,\n      parentId: data.parent_id,\n      name: data.name,\n      isVisibleInNavigation: data.is_visible,\n      isCustomersOnly: data.is_customers_only,\n      type: data.type,\n      path: data.url ?? undefined,\n      email: data.email ?? undefined,\n      link: data.link ?? undefined,\n      body: data.body ?? undefined,\n      contactFields: ContactFieldTypeSchema.parse(data.contact_fields?.split(',') ?? []),\n    }),\n  );\n\nconst WebPageCreateSchema = z.object({\n  name: z.string(),\n  type: WebPageTypeSchema,\n  channel_id: z.number(),\n  parent_id: z.number().optional(),\n  is_visible: z.boolean().optional(),\n  is_customers_only: z.boolean().optional(),\n  url: z.string().optional(),\n  email: z.string().optional(),\n  link: z.string().optional(),\n  body: z.string().optional(),\n  contact_fields: z.string().optional(),\n});\n\nconst transformCreateWebPageData = (data: CreateWebPageData) =>\n  WebPageCreateSchema.parse({\n    name: data.name,\n    type: data.type,\n    channel_id: testEnv.BIGCOMMERCE_CHANNEL_ID ?? 1,\n    parent_id: data.parentId,\n    is_visible: data.isVisibleInNavigation,\n    is_customers_only: data.isCustomersOnly,\n    url: data.path,\n    email: data.email,\n    link: data.link,\n    body: data.body,\n    contact_fields: data.contactFields?.join(','),\n  });\n\nexport const webPagesHttpClient: WebPagesApi = {\n  getById: async (id) => {\n    const resp = await httpClient\n      .get(`/v3/content/pages/${id}?include=body`)\n      .parse(apiResponseSchema(WebPageSchema));\n\n    return resp.data;\n  },\n  create: async (data) => {\n    const resp = await httpClient\n      .post('/v3/content/pages?include=body', transformCreateWebPageData(data))\n      .parse(apiResponseSchema(WebPageSchema));\n\n    return resp.data;\n  },\n  delete: async (ids) => {\n    if (ids.length > 0) {\n      // TODO: Switch to the bulk delete v3 endpoint when the DELETE /v3/content/pages API endpoint is fixed.\n      // Currently, it does not reliably delete pages.\n      await Promise.all(ids.map((id) => httpClient.delete(`/v2/pages/${id}`)));\n    }\n  },\n};\n"
  },
  {
    "path": "core/tests/fixtures/utils/api/webpages/index.ts",
    "content": "type WebPageType = 'page' | 'link' | 'contact_form' | 'raw';\ntype ContactFieldType = 'fullname' | 'companyname' | 'phone' | 'orderno' | 'rma';\n\nexport interface WebPage {\n  readonly id: number;\n  readonly parentId: number;\n  readonly name: string;\n  readonly isVisibleInNavigation: boolean;\n  readonly isCustomersOnly: boolean;\n  readonly type: WebPageType;\n  readonly path?: string;\n  readonly email?: string;\n  readonly link?: string;\n  readonly body?: string;\n  readonly contactFields?: ContactFieldType[];\n}\n\nexport interface CreateWebPageData {\n  name: string;\n  type: WebPageType;\n  parentId?: number;\n  isVisibleInNavigation?: boolean;\n  isCustomersOnly?: boolean;\n  path?: string;\n  email?: string;\n  link?: string;\n  body?: string;\n  contactFields?: ContactFieldType[];\n}\n\nexport interface WebPagesApi {\n  getById: (id: number) => Promise<WebPage>;\n  create: (data: CreateWebPageData) => Promise<WebPage>;\n  delete: (ids: number[]) => Promise<void>;\n}\n\nexport { webPagesHttpClient } from './http';\n"
  },
  {
    "path": "core/tests/fixtures/webpage/index.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { Fixture } from '~/tests/fixtures/fixture';\nimport { CreateWebPageData, WebPage } from '~/tests/fixtures/utils/api/webpages';\n\nexport class WebPageFixture extends Fixture {\n  webPages: WebPage[] = [];\n\n  getById(id: number): Promise<WebPage> {\n    return this.api.webPages.getById(id);\n  }\n\n  async create(data?: Partial<CreateWebPageData>): Promise<WebPage> {\n    this.skipIfReadonly();\n\n    const webPage = await this.api.webPages.create(this.fakeCreatePageData(data));\n\n    this.webPages.push(webPage);\n\n    return webPage;\n  }\n\n  async cleanup() {\n    await this.api.webPages.delete(this.webPages.map(({ id }) => id));\n  }\n\n  private fakeCreatePageData(data?: Partial<CreateWebPageData>): CreateWebPageData {\n    return {\n      parentId: 0,\n      name: `Catalyst Test Page ${faker.string.alpha(5)}`,\n      type: 'page',\n      body: faker.lorem.paragraphs({ min: 1, max: 5 }),\n      isVisibleInNavigation: true,\n      isCustomersOnly: false,\n      ...data,\n    };\n  }\n}\n"
  },
  {
    "path": "core/tests/lib/formatter/index.ts",
    "content": "import { createFormatter } from 'next-intl';\n\nimport { testEnv } from '~/tests/environment';\n\n// Simple wrapper to allow using the NextJS formatter inside of tests\nexport function getFormatter() {\n  return createFormatter({ locale: testEnv.TESTS_LOCALE });\n}\n"
  },
  {
    "path": "core/tests/lib/i18n/index.ts",
    "content": "import deepmerge from 'deepmerge';\nimport { createTranslator, Messages, NamespaceKeys, NestedKeyOf } from 'next-intl';\n\nimport { testEnv } from '~/tests/environment';\n\nconst { TESTS_LOCALE: locale, TESTS_FALLBACK_LOCALE: fallbackLocale } = testEnv;\n\nasync function loadMessages(): Promise<Messages> {\n  const importJson = (path: string) => import(path, { with: { type: 'json' } });\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-member-access\n  const messages = (await importJson(`~/messages/${locale}.json`)).default as Messages;\n\n  if (locale === fallbackLocale) {\n    return messages;\n  }\n\n  return deepmerge(\n    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-member-access\n    (await importJson(`~/messages/${fallbackLocale}.json`)).default as Messages,\n    messages,\n  );\n}\n\nexport async function getTranslations<\n  NestedKey extends NamespaceKeys<Messages, NestedKeyOf<Messages>> = never,\n>(namespace?: NestedKey): Promise<ReturnType<typeof createTranslator<Messages, NestedKey>>> {\n  return createTranslator<Messages, NestedKey>({\n    namespace,\n    messages: await loadMessages(),\n    locale,\n  });\n}\n"
  },
  {
    "path": "core/tests/routes.ts",
    "content": "export default {\n  SHOP_ALL: '/shop-all',\n  SAMPLE_ABLE_BREWING_SYSTEM: '/sample-able-brewing-system',\n  ORBIT_TERRARIUM_LARGE: '/orbit-terrarium-large',\n  BLOG: '/blog',\n  BATH_LUXURY: '/bath/towels/luxury',\n  CONTACT_US: '/contact-us',\n  LOGIN: '/login',\n  FOG_LINEN_CHAMBRAY: '/fog-linen-chambray-towel-beige-stripe/',\n  PARFAIT_JAR: '/1-l-le-parfait-jar',\n};\n"
  },
  {
    "path": "core/tests/tags.ts",
    "content": "export const TAGS = {\n  // @writes-data is used to mark tests that modify data on the storefront without directly using the API.\n  writesData: '@writes-data',\n  // @alternate-locale is used to mark tests that should be run with an alternate locale setting.\n  alternateLocale: '@alternate-locale',\n  // @no-trailing-slash is used to mark tests that should be run with TRAILING_SLASH disabled.\n  noTrailingSlash: '@no-trailing-slash',\n};\n"
  },
  {
    "path": "core/tests/ui/components/accordion.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\ntest('Verify accordion behavior on header', async ({ page }) => {\n  await page.goto('/');\n  await expect(\n    page.getByRole('navigation', { name: 'Main' }).getByRole('link', { name: 'Kitchen' }),\n  ).toBeVisible();\n\n  await page\n    .getByRole('navigation', { name: 'Main' })\n    .getByRole('link', { name: 'Kitchen' })\n    .hover();\n  await expect(page.getByRole('link', { name: 'Knives' })).toBeVisible();\n  await expect(page.getByRole('link', { name: 'Plates' })).toBeVisible();\n  await expect(\n    page.getByRole('navigation', { name: 'Main' }).getByRole('link', { name: 'Garden' }),\n  ).toBeVisible();\n\n  await page\n    .getByRole('navigation', { name: 'Main' })\n    .getByRole('link', { name: 'Garden' })\n    .hover();\n  await expect(page.getByRole('link', { name: 'Knives' })).toBeHidden();\n  await expect(page.getByRole('link', { name: 'Plates' })).toBeHidden();\n});\n\ntest('Verify accordion behavior on desktop filter', async ({ page }) => {\n  await page.goto('/kitchen/');\n\n  await expect(page.getByRole('button', { name: 'Color' })).toBeVisible();\n  await expect(page.getByText('Black1 products')).toBeVisible();\n  await expect(page.getByText('Blue1 products')).toBeVisible();\n\n  await page.getByRole('button', { name: 'Color' }).click();\n  await expect(page.getByText('Black1 products')).toBeHidden();\n  await expect(page.getByText('Blue1 products')).toBeHidden();\n});\n"
  },
  {
    "path": "core/tests/ui/components/breadcrumbs.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\ntest('Verify breadcrumbs on product selection', async ({ page }) => {\n  await page.goto('/kitchen/knives/most-popular/');\n\n  const breadcrumbs = page.getByRole('navigation', { name: 'Breadcrumb' });\n\n  await expect(breadcrumbs.getByRole('link', { name: 'Kitchen' })).toBeVisible();\n  await expect(breadcrumbs.getByRole('link', { name: 'Knives' })).toBeVisible();\n  await expect(breadcrumbs.getByRole('link', { name: 'Most popular' })).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/components/carousel.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto('/');\n\n  await page\n    .getByRole('region')\n    .filter({ has: page.getByRole('heading', { name: 'Featured products' }) })\n    .getByRole('tablist', { name: 'Slides' })\n    .scrollIntoViewIfNeeded();\n\n  await page\n    .getByRole('link', { name: '[Sample] Smith Journal 13' })\n    .first()\n    .scrollIntoViewIfNeeded();\n\n  await expect(page.getByRole('link', { name: '[Sample] Smith Journal 13' }).first()).toBeVisible();\n});\n\ntest('Navigate to next set of products', async ({ page }) => {\n  await page\n    .getByRole('region')\n    .filter({ has: page.getByRole('heading', { name: 'Featured products' }) })\n    .getByRole('button', { name: 'Next products' })\n    .click();\n\n  await expect(\n    page.getByRole('link', { name: '[Sample] Tiered Wire Basket' }).first(),\n  ).toBeVisible();\n});\n\ntest('Navigate to previous set of products', async ({ page }) => {\n  await page\n    .getByRole('region')\n    .filter({ has: page.getByRole('heading', { name: 'Featured products' }) })\n    .getByRole('button', { name: 'Next products' })\n    .click();\n\n  await expect(\n    page.getByRole('link', { name: '[Sample] Tiered Wire Basket' }).first(),\n  ).toBeVisible();\n\n  await page\n    .getByRole('region')\n    .filter({ has: page.getByRole('heading', { name: 'Featured products' }) })\n    .getByRole('button', { name: 'Previous products' })\n    .click();\n\n  await expect(page.getByRole('link', { name: '[Sample] Smith Journal 13' }).first()).toBeVisible();\n});\n\ntest('Navigation on set of products is cyclic', async ({ page }) => {\n  await page\n    .getByRole('region')\n    .filter({ has: page.getByRole('heading', { name: 'Featured products' }) })\n    .getByRole('button', { name: 'Next products' })\n    .click();\n\n  await expect(\n    page.getByRole('link', { name: '[Sample] Tiered Wire Basket' }).first(),\n  ).toBeVisible();\n\n  await page\n    .getByRole('region')\n    .filter({ has: page.getByRole('heading', { name: 'Featured products' }) })\n    .getByRole('button', { name: 'Next products' })\n    .click();\n\n  await expect(\n    page.getByRole('link', { name: '[Sample] Able Brewing System' }).first(),\n  ).toBeVisible();\n\n  await page\n    .getByRole('region')\n    .filter({ has: page.getByRole('heading', { name: 'Featured products' }) })\n    .getByRole('button', { name: 'Next products' })\n    .click();\n\n  await expect(page.getByRole('link', { name: '[Sample] Smith Journal 13' }).first()).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/components/checkbox.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\ntest('Filter products by selecting checkbox', async ({ page }) => {\n  await page.goto('/');\n  await page\n    .getByRole('navigation', { name: 'Main' })\n    .getByRole('link', { name: 'Shop All' })\n    .click();\n\n  await expect(page.getByText('13 items')).toBeVisible();\n  await page.getByLabel('OFS5 products').click();\n  await expect(page.getByText('5 items')).toBeVisible();\n\n  await page.getByLabel('OFS5 products').click();\n  await expect(page.getByText('13 items')).toBeVisible();\n\n  await page.getByLabel('Common Good1 products').click();\n  await expect(page.getByText('1 item')).toBeVisible();\n  await expect(page.getByRole('link', { name: '[Sample] Laundry Detergent' })).toBeVisible();\n\n  await page.getByRole('button', { name: 'Clear all' }).click();\n  await expect(page.getByText('13 items')).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/components/counter.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\ntest('Increase count and verify the results', async ({ page }) => {\n  await page.goto('/orbit-terrarium-large/');\n\n  await expect(page.getByRole('spinbutton')).toHaveValue('1');\n\n  await page.getByLabel('Increase count').click();\n  await page.getByLabel('Increase count').click();\n\n  await expect(page.getByRole('spinbutton')).toHaveValue('3');\n});\n\ntest('Decrease count and verify the results', async ({ page }) => {\n  await page.goto('/orbit-terrarium-large/');\n\n  await expect(page.getByRole('spinbutton')).toHaveValue('1');\n\n  await page.getByLabel('Increase count').click();\n  await expect(page.getByRole('spinbutton')).toHaveValue('2');\n\n  await page.getByLabel('Decrease count').click();\n\n  await expect(page.getByRole('spinbutton')).toHaveValue('1');\n});\n"
  },
  {
    "path": "core/tests/ui/components/radio-group.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Changing selection on radio button options should update query parameters', async ({\n  page,\n}) => {\n  await page.goto(routes.FOG_LINEN_CHAMBRAY);\n\n  await expect(\n    page.getByRole('heading', {\n      level: 1,\n      name: '[Sample] Fog Linen Chambray Towel - Beige Stripe',\n    }),\n  ).toBeVisible();\n\n  await page.getByLabel('Radio').getByText('1').click();\n\n  await expect(page).toHaveURL('fog-linen-chambray-towel-beige-stripe/?134=139');\n\n  await expect(\n    page.getByRole('heading', {\n      level: 1,\n      name: '[Sample] Fog Linen Chambray Towel - Beige Stripe',\n    }),\n  ).toBeVisible();\n\n  await page.getByLabel('Radio').getByText('2').click();\n\n  await expect(page).toHaveURL('fog-linen-chambray-towel-beige-stripe/?134=140');\n\n  await page.getByLabel('Radio').getByText('3').click();\n\n  await expect(page).toHaveURL('fog-linen-chambray-towel-beige-stripe/?134=141');\n});\n"
  },
  {
    "path": "core/tests/ui/components/search.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\nconst productName = '[Sample] Able Brewing System';\n\ntest('Search for specific product and verify results', async ({ page }) => {\n  await page.goto('/');\n\n  await page.getByLabel('Open search popup').click();\n\n  const searchBox = page.getByPlaceholder('Search...');\n\n  await expect(searchBox).toBeVisible();\n\n  await searchBox.fill(productName);\n  await searchBox.press('Enter');\n\n  await expect(page.getByRole('link', { name: productName })).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/components/select.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto('/');\n  await page\n    .getByRole('navigation', { name: 'Main' })\n    .getByRole('link', { name: 'Shop All' })\n    .click();\n});\n\ntest('Sort products on display by review', async ({ page }) => {\n  await page.getByLabel('Sort by:').click();\n  await page.getByText('By review').click();\n\n  await expect(page).toHaveURL('shop-all/?sort=best_reviewed');\n});\n\ntest('Sort products on display by ascending price', async ({ page }) => {\n  await page.getByLabel('Sort by:').click();\n  await page.getByText('Price: ascending').click();\n\n  await expect(page).toHaveURL('shop-all/?sort=lowest_price');\n});\n\ntest('Sort products on display by descending price', async ({ page }) => {\n  await page.getByLabel('Sort by:').click();\n  await page.getByText('Price: descending').click();\n\n  await expect(page).toHaveURL('shop-all/?sort=highest_price');\n});\n\ntest('Sort products on display by relevance', async ({ page }) => {\n  await page.getByLabel('Sort by:').click();\n  await page.getByText('Relevance').click();\n\n  await expect(page).toHaveURL('shop-all/?sort=relevance');\n});\n"
  },
  {
    "path": "core/tests/ui/components/slideshow.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\ntest.beforeEach(async ({ page }) => {\n  await page.goto('/');\n  await expect(page.getByLabel('Next slide')).toBeVisible();\n});\n\ntest('Navigate to next slide', async ({ page }) => {\n  await page.getByLabel('Next slide').click();\n\n  await expect(page.getByRole('heading', { name: 'Great Deals' })).toBeVisible();\n});\n\ntest('Navigate to previous slide', async ({ page }) => {\n  await page.getByLabel('Previous slide').click();\n\n  await expect(page.getByRole('heading', { name: 'Low Prices' })).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/components/swatch.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Selecting various options on color panel should update query parameters', async ({\n  page,\n}) => {\n  await page.goto(routes.FOG_LINEN_CHAMBRAY);\n\n  await expect(\n    page.getByRole('heading', {\n      level: 1,\n      name: '[Sample] Fog Linen Chambray Towel - Beige Stripe',\n    }),\n  ).toBeVisible();\n\n  await page.getByRole('radio', { name: 'Silver' }).click();\n\n  await expect(page).toHaveURL('fog-linen-chambray-towel-beige-stripe/?109=103');\n\n  await expect(\n    page.getByRole('heading', {\n      level: 1,\n      name: '[Sample] Fog Linen Chambray Towel - Beige Stripe',\n    }),\n  ).toBeVisible();\n\n  await page.getByRole('radio', { name: 'Purple' }).click();\n\n  await expect(page).toHaveURL('fog-linen-chambray-towel-beige-stripe/?109=105');\n\n  await page.getByRole('radio', { name: 'Orange' }).click();\n\n  await expect(page).toHaveURL('fog-linen-chambray-towel-beige-stripe/?109=109');\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/account/account-settings.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\nimport { TAGS } from '~/tests/tags';\n\ntest('Updating account information works as expected', async ({ page, customer }) => {\n  const t = await getTranslations('Account.Settings');\n  const testCustomer = await customer.createNewCustomer();\n\n  await customer.loginAs(testCustomer);\n\n  const updatedFirstName = `${testCustomer.firstName}-modified`;\n  const updatedLastName = `${testCustomer.lastName}-modified`;\n\n  await page.goto('/account/settings');\n  await expect(page.getByRole('heading', { name: t('title') })).toBeVisible();\n\n  // TODO: Account settings form fields need to be translated\n  await expect(page.getByLabel('First Name')).toHaveValue(testCustomer.firstName);\n  await expect(page.getByLabel('Last Name')).toHaveValue(testCustomer.lastName);\n  await expect(page.getByLabel('Email').first()).toHaveValue(testCustomer.email);\n\n  await page.getByLabel('First Name').fill(updatedFirstName);\n  await page.getByLabel('Last Name').fill(updatedLastName);\n  await page\n    .getByRole('button', { name: t('cta') })\n    .first()\n    .click();\n\n  await expect(page.getByText(t('settingsUpdated'))).toBeVisible();\n\n  // Ensure that the customer data is updated on the back-end\n  const updatedData = await customer.getById(testCustomer.id);\n\n  expect(updatedData.firstName).toBe(updatedFirstName);\n  expect(updatedData.lastName).toBe(updatedLastName);\n  expect(updatedData.email).toBe(testCustomer.email);\n});\n\ntest('Changing password works as expected', async ({ page, customer }) => {\n  const t = await getTranslations('Account.Settings');\n  const testCustomer = await customer.createNewCustomer();\n\n  if (!testCustomer.password) {\n    throw new Error('Error when creating test customer. Password is undefined.');\n  }\n\n  await customer.loginAs(testCustomer);\n\n  const newPassword = faker.internet.password({\n    pattern: /[a-zA-Z0-9]/,\n    prefix: '1At!',\n    length: 10,\n  });\n\n  await page.goto('/account/settings');\n  await expect(page.getByRole('heading', { name: t('title') })).toBeVisible();\n\n  await page.getByLabel(t('currentPassword')).fill(testCustomer.password);\n  await page.getByLabel(t('newPassword')).fill(newPassword);\n  await page.getByLabel(t('confirmPassword')).fill(newPassword);\n  await page\n    .getByRole('button', { name: t('cta') })\n    .nth(1) // The second button is the update password button\n    .click();\n\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByText(t('passwordUpdated'))).toBeVisible();\n\n  // Now that the password is updated, attempt logging in with the new password\n  await page.goto('/logout');\n\n  testCustomer.password = newPassword;\n\n  await customer.loginAs(testCustomer);\n\n  await expect(page).toHaveURL('/account/orders/');\n});\n\ntest(\n  'Subscribing to newsletter works as expected',\n  { tag: [TAGS.writesData] },\n  async ({ page, customer, subscribe }) => {\n    const t = await getTranslations('Account.Settings');\n    const tNewsletter = await getTranslations('Account.Settings.NewsletterSubscription');\n    const testCustomer = await customer.createNewCustomer();\n\n    // Ensure customer is unsubscribed initially\n    await subscribe.unsubscribe(testCustomer.email);\n\n    await customer.loginAs(testCustomer);\n\n    await page.goto('/account/settings');\n    await expect(page.getByRole('heading', { name: t('title') })).toBeVisible();\n\n    // Find the newsletter subscription switch\n    const newsletterSwitch = page.getByLabel(t('NewsletterSubscription.label'));\n\n    await expect(newsletterSwitch).toBeVisible();\n\n    // Verify switch is unchecked (customer is not subscribed)\n    const switchElement = page.getByRole('switch', { name: t('NewsletterSubscription.label') });\n\n    await expect(switchElement).not.toBeChecked();\n\n    // Click the switch to subscribe\n    await switchElement.click();\n\n    // Click the submit button (should be the last button on the page)\n    await page\n      .getByRole('button', { name: t('cta') })\n      .last()\n      .click();\n\n    await page.waitForLoadState('networkidle');\n\n    // Verify success message appears\n    await expect(page.getByText(tNewsletter('marketingPreferencesUpdated'))).toBeVisible();\n\n    // Track subscription for cleanup\n    subscribe.trackSubscription(testCustomer.email);\n  },\n);\n\ntest(\n  'Unsubscribing from newsletter works as expected',\n  { tag: [TAGS.writesData] },\n  async ({ page, customer, subscribe }) => {\n    const t = await getTranslations('Account.Settings');\n    const tNewsletter = await getTranslations('Account.Settings.NewsletterSubscription');\n    const testCustomer = await customer.createNewCustomer();\n\n    // Ensure customer is subscribed initially\n    await subscribe.subscribe(testCustomer.email, testCustomer.firstName, testCustomer.lastName);\n\n    await customer.loginAs(testCustomer);\n\n    await page.goto('/account/settings');\n    await expect(page.getByRole('heading', { name: t('title') })).toBeVisible();\n\n    // Find the newsletter subscription switch\n    const newsletterSwitch = page.getByLabel(t('NewsletterSubscription.label'));\n\n    await expect(newsletterSwitch).toBeVisible();\n\n    // Verify switch is checked (customer is subscribed)\n    const switchElement = page.getByRole('switch', { name: t('NewsletterSubscription.label') });\n\n    await expect(switchElement).toBeChecked();\n\n    // Click the switch to unsubscribe\n    await switchElement.click();\n\n    // Click the submit button (should be the last button on the page)\n    await page\n      .getByRole('button', { name: t('cta') })\n      .last()\n      .click();\n\n    await page.waitForLoadState('networkidle');\n\n    // Verify success message appears\n    await expect(page.getByText(tNewsletter('marketingPreferencesUpdated'))).toBeVisible();\n\n    // Track subscription for cleanup\n    subscribe.trackSubscription(testCustomer.email);\n  },\n);\n"
  },
  {
    "path": "core/tests/ui/e2e/account/account.spec.ts",
    "content": "import { defaultLocale } from '~/i18n/locales';\nimport { testEnv } from '~/tests/environment';\nimport { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\nimport { TAGS } from '~/tests/tags';\n\nconst accountUrls = [\n  '/account/orders',\n  '/account/settings',\n  '/account/addresses',\n  '/account/wishlists',\n];\n\naccountUrls.forEach((url) => {\n  test(`${url} page is restricted for guest users`, async ({ page }) => {\n    await page.goto(url);\n    await expect(page).toHaveURL('/login/');\n  });\n});\n\naccountUrls.forEach((url) => {\n  test(\n    `${url} is restricted for guest users when explicitly browsing to the locale URL`,\n    { tag: TAGS.alternateLocale },\n    async ({ page }) => {\n      test.skip(testEnv.TESTS_LOCALE === defaultLocale);\n\n      await page.goto(`/${testEnv.TESTS_LOCALE}/${url}`);\n      await expect(page).toHaveURL('/login/', { timeout: 1000 });\n    },\n  );\n});\n\ntest('Account page displays the menu items for each section', async ({ page, customer }) => {\n  const t = await getTranslations('Account.Layout');\n\n  await customer.login();\n\n  await expect(page.getByRole('link', { name: t('orders') })).toBeVisible();\n  await expect(page.getByRole('link', { name: t('addresses') })).toBeVisible();\n  await expect(page.getByRole('link', { name: t('settings') })).toBeVisible();\n  await expect(page.getByRole('link', { name: t('wishlists') })).toBeVisible();\n  await expect(page.getByRole('link', { name: t('logout') })).toBeVisible();\n});\n\ntest('Account icon is visible in the header menu and navigates to the login page for guest users', async ({\n  page,\n}) => {\n  const t = await getTranslations('Components.Header.Icons');\n\n  await page.goto('/');\n  await page.getByLabel(t('account')).click();\n  await expect(page).toHaveURL('/login/');\n});\n\ntest('Account icon is visible in the header menu and navigates to the account page for logged in users', async ({\n  page,\n  customer,\n}) => {\n  const t = await getTranslations('Components.Header.Icons');\n\n  await customer.login('/');\n\n  await page.getByLabel(t('account')).click();\n  await expect(page).toHaveURL('/account/orders/');\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/account/addresses.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { expect, Page, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\nimport { TAGS } from '~/tests/tags';\n\nasync function fillAddressForm(page: Page) {\n  const firstName = faker.person.firstName();\n  const lastName = faker.person.lastName();\n  const companyName = faker.company.name();\n  const phoneNumber = faker.phone.number();\n  const addressLine1 = faker.location.streetAddress();\n  const addressLine2 = faker.location.secondaryAddress();\n  const city = faker.location.city();\n  const state = faker.location.state();\n  const zipCode = faker.location.zipCode('#####');\n  const country = 'United States';\n\n  // TODO: Add translations for address form field labels\n  await page.getByLabel('First name').fill(firstName);\n  await page.getByLabel('Last name').fill(lastName);\n  await page.getByLabel('Company name').fill(companyName);\n  await page.getByLabel('Phone number').fill(phoneNumber);\n  await page.getByLabel('Address line 1').fill(addressLine1);\n  await page.getByLabel('Address line 2').fill(addressLine2);\n  await page.getByLabel('Suburb/city').fill(city);\n  await page.getByLabel('State/province').fill(state);\n  await page.getByLabel('Zip/postcode').fill(zipCode);\n  await page.getByLabel('Country').click();\n  await page.getByRole('option', { name: country }).first().click();\n\n  return {\n    firstName,\n    lastName,\n    companyName,\n    phoneNumber,\n    addressLine1,\n    addressLine2,\n    city,\n    state,\n    zipCode,\n    country,\n  };\n}\n\nasync function assertAddressSectionHasAddress(\n  page: Page,\n  address: Awaited<ReturnType<typeof fillAddressForm>>,\n) {\n  const t = await getTranslations('Account.Addresses');\n  const addressesSection = page\n    .locator('section')\n    .filter({ has: page.getByRole('heading', { name: t('title') }) });\n\n  await expect(\n    addressesSection.getByText(`${address.firstName} ${address.lastName}`, { exact: true }),\n  ).toBeVisible();\n  await expect(addressesSection.getByText(address.companyName, { exact: true })).toBeVisible();\n  await expect(addressesSection.getByText(address.addressLine1, { exact: true })).toBeVisible();\n  await expect(addressesSection.getByText(address.addressLine2, { exact: true })).toBeVisible();\n  await expect(\n    addressesSection.getByText(`${address.city}, ${address.state} ${address.zipCode}`, {\n      exact: true,\n    }),\n  ).toBeVisible();\n  await expect(addressesSection.getByText('US', { exact: true })).toBeVisible();\n  await expect(addressesSection.getByText(address.phoneNumber, { exact: true })).toBeVisible();\n}\n\ntest(\n  'Adding a new address works as expected',\n  { tag: [TAGS.writesData] },\n  async ({ page, customer }) => {\n    const t = await getTranslations('Account.Addresses');\n    const { id } = await customer.login();\n\n    // Ensure addresses are in a reliable state before the test\n    await customer.deleteAllAddresses(id);\n\n    await page.goto('/account/addresses/');\n    await page.getByRole('button', { name: t('cta') }).click();\n\n    const address = await fillAddressForm(page);\n\n    await page.getByRole('button', { name: t('create'), exact: true }).click();\n    await page.waitForLoadState('networkidle');\n\n    await assertAddressSectionHasAddress(page, address);\n  },\n);\n\ntest(\n  'Editing an address works as expected',\n  { tag: [TAGS.writesData] },\n  async ({ page, customer }) => {\n    const t = await getTranslations('Account.Addresses');\n    const { id } = await customer.login();\n\n    await customer.deleteAllAddresses(id);\n\n    const address = await customer.createAddress(id);\n\n    await page.goto('/account/addresses/');\n    await page.getByRole('button', { name: t('edit') }).click();\n\n    await expect(page.getByLabel('First name')).toHaveValue(address.firstName);\n    await expect(page.getByLabel('Last name')).toHaveValue(address.lastName);\n    await expect(page.getByLabel('Company name')).toHaveValue(address.company ?? '');\n    await expect(page.getByLabel('Phone number')).toHaveValue(address.phone ?? '');\n    await expect(page.getByLabel('Address line 1')).toHaveValue(address.address1);\n    await expect(page.getByLabel('Address line 2')).toHaveValue(address.address2 ?? '');\n    await expect(page.getByLabel('Suburb/city')).toHaveValue(address.city);\n    await expect(page.getByLabel('State/province')).toHaveValue(address.stateOrProvince ?? '');\n    await expect(page.getByLabel('Zip/postcode')).toHaveValue(address.postalCode);\n    await expect(page.getByRole('combobox', { name: 'Country' })).toHaveText(address.country);\n\n    const newAddress = await fillAddressForm(page);\n\n    await page.getByRole('button', { name: t('update'), exact: true }).click();\n    await page.waitForLoadState('networkidle');\n\n    await assertAddressSectionHasAddress(page, newAddress);\n  },\n);\n\ntest('Deleting an address works as expected', async ({ page, customer }) => {\n  const t = await getTranslations('Account.Addresses');\n  const { id } = await customer.login();\n\n  await customer.deleteAllAddresses(id);\n\n  const address = await customer.createAddress(id);\n\n  await page.goto('/account/addresses/');\n  await page.getByRole('button', { name: t('delete') }).click();\n  await page.waitForLoadState('networkidle');\n\n  const addressesSection = page\n    .locator('section')\n    .filter({ has: page.getByRole('heading', { name: t('title') }) });\n\n  await expect(\n    addressesSection.getByText(`${address.firstName} ${address.lastName}`, { exact: true }),\n  ).not.toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/account/order-details.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport { getFormatter } from '~/tests/lib/formatter';\nimport { getTranslations } from '~/tests/lib/i18n';\n\ntest('Order details page is displayed and uses correct formatting', async ({\n  page,\n  catalog,\n  customer,\n  order,\n}) => {\n  const t = await getTranslations('Account.Orders.Details');\n  const format = getFormatter();\n  const { id: customerId } = await customer.login();\n  const orderDetails = await order.createWithDefaultProduct(customerId);\n  const { name: productName } = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto(`/account/orders/${orderDetails.id}/`);\n\n  const formattedTotal = format.number(Number(orderDetails.totalIncTax), {\n    style: 'currency',\n    currency: orderDetails.currencyCode,\n  });\n\n  await expect(\n    page.getByText(t('title', { orderNumber: String(orderDetails.id) })).first(),\n  ).toBeVisible();\n\n  await expect(page.getByText(t('summaryTotal')).first()).toBeVisible();\n  await expect(page.getByText(formattedTotal).first()).toBeVisible();\n\n  await expect(page.getByText(orderDetails.status).first()).toBeVisible();\n  await expect(page.getByRole('link', { name: productName }).first()).toBeVisible();\n\n  await expect(page.getByText(t('destination')).first()).toBeVisible();\n  await expect(page.getByText(t('shippingAddress')).first()).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/account/orders.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport { getFormatter } from '~/tests/lib/formatter';\nimport { getTranslations } from '~/tests/lib/i18n';\n\ntest('Orders page has an empty state when no orders exist', async ({ page, customer, order }) => {\n  const { id } = await customer.login();\n\n  // Delete all orders for the customer to ensure test reliability\n  await order.deleteAllCustomerOrders(id);\n\n  const t = await getTranslations();\n\n  await page.goto('/account/orders/');\n\n  await expect(\n    page.getByRole('heading', { name: t('Account.Orders.title'), exact: true }),\n  ).toBeVisible();\n\n  await expect(page.getByText(t('Account.Orders.EmptyState.title'))).toBeVisible();\n  await expect(page.getByRole('link', { name: t('Account.Orders.EmptyState.cta') })).toBeVisible();\n});\n\ntest('Order details are displayed and use correct formatting', async ({\n  page,\n  catalog,\n  customer,\n  order,\n}) => {\n  const t = await getTranslations();\n  const format = getFormatter();\n  const { id: customerId } = await customer.login();\n  const orderDetails = await order.createWithDefaultProduct(customerId);\n  const { name: productName } = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto('/account/orders/');\n\n  const formattedTotal = format.number(Number(orderDetails.totalIncTax), {\n    style: 'currency',\n    currency: orderDetails.currencyCode,\n  });\n\n  await expect(page.getByText(t('Account.Orders.orderNumber')).first()).toBeVisible();\n  await expect(page.getByText(String(orderDetails.id))).toBeVisible();\n\n  await expect(page.getByText(t('Account.Orders.totalPrice')).first()).toBeVisible();\n  await expect(page.getByText(formattedTotal).first()).toBeVisible();\n\n  await expect(page.getByText(orderDetails.status).first()).toBeVisible();\n  await expect(page.getByRole('link', { name: productName }).first()).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/account/wishlist-details.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\nimport { TAGS } from '~/tests/tags';\n\ntest('Wishlist details displays an empty state when no items are in the wishlist', async ({\n  page,\n  customer,\n}) => {\n  const t = await getTranslations('Wishlist');\n  const { id: customerId } = await customer.login();\n  const { id, name } = await customer.createWishlist({ customerId });\n\n  await page.goto(`/account/wishlists/${id}`);\n  await page.waitForLoadState('networkidle');\n  await expect(page.getByRole('heading', { name, exact: true })).toBeVisible();\n  await expect(page.getByText(t('items', { count: 0 }))).toBeVisible();\n  await expect(page.getByText(t('emptyWishlist'))).toBeVisible();\n});\n\ntest('Wishlist details displays the wishlist actions bar and actions menu items correctly', async ({\n  page,\n  customer,\n}) => {\n  const t = await getTranslations('Wishlist');\n  const { id: customerId } = await customer.login();\n  const { id, name } = await customer.createWishlist({ customerId, isPublic: false });\n\n  await page.goto(`/account/wishlists/${id}`);\n  await page.waitForLoadState('networkidle');\n  await expect(page.getByRole('heading', { name, exact: true })).toBeVisible();\n  await expect(\n    page.getByRole('switch', { name: `${t('Visibility.public')} ${t('Visibility.private')}` }),\n  ).toBeVisible();\n  await expect(page.getByLabel(t('Visibility.private'))).toBeVisible();\n  await expect(page.getByRole('button', { name: t('share') })).toBeDisabled();\n\n  await page.getByRole('button', { name: t('actionsTitle') }).click();\n  await expect(page.getByRole('menuitem', { name: t('rename') })).toBeVisible();\n  await expect(page.getByRole('menuitem', { name: t('delete') })).toBeVisible();\n});\n\ntest('Wishlist details displays a product correctly with \"Add to Cart\" button', async ({\n  page,\n  catalog,\n  customer,\n}) => {\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n  const t = await getTranslations();\n  const { id: customerId } = await customer.login();\n  const { id: wishlistId, name } = await customer.createWishlist({\n    customerId,\n    items: [\n      {\n        productId: product.id,\n      },\n    ],\n  });\n\n  await page.goto(`/account/wishlists/${wishlistId}`);\n  await page.waitForLoadState('networkidle');\n  await expect(page.getByRole('heading', { name, exact: true })).toBeVisible();\n\n  await expect(page.getByRole('link', { name: product.name })).toBeVisible();\n  await expect(\n    page.getByRole('button', { name: t('Product.ProductDetails.Submit.addToCart') }),\n  ).toBeVisible();\n});\n\ntest('Wishlist product is able to be added to the cart', async ({ page, catalog, customer }) => {\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n  const t = await getTranslations();\n  const { id: customerId } = await customer.login();\n  const { id: wishlistId, name } = await customer.createWishlist({\n    customerId,\n    items: [\n      {\n        productId: product.id,\n      },\n    ],\n  });\n\n  await page.goto(`/account/wishlists/${wishlistId}`);\n  await page.waitForLoadState('networkidle');\n  await expect(page.getByRole('heading', { name, exact: true })).toBeVisible();\n\n  await expect(page.getByRole('link', { name: product.name })).toBeVisible();\n  await page.getByRole('button', { name: t('Product.ProductDetails.Submit.addToCart') }).click();\n  await page.waitForLoadState('networkidle');\n\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  const addToCartSuccessMessage = t.rich('Product.ProductDetails.successMessage', {\n    cartItems: 1,\n    cartLink: (chunks: React.ReactNode) => chunks,\n  }) as string;\n\n  await expect(page.getByText(addToCartSuccessMessage)).toBeVisible();\n});\n\ntest('Wishlist product is able to be removed from the wishlist', async ({\n  page,\n  catalog,\n  customer,\n}) => {\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n  const t = await getTranslations();\n  const { id: customerId } = await customer.login();\n  const { id: wishlistId, name } = await customer.createWishlist({\n    customerId,\n    items: [\n      {\n        productId: product.id,\n      },\n    ],\n  });\n\n  await page.goto(`/account/wishlists/${wishlistId}`);\n  await page.waitForLoadState('networkidle');\n  await expect(page.getByRole('heading', { name, exact: true })).toBeVisible();\n\n  await expect(page.getByRole('link', { name: product.name })).toBeVisible();\n  await page.getByRole('button', { name: t('Wishlist.removeButtonTitle') }).click();\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByText(t('Wishlist.Result.removeItemSuccess'))).toBeVisible();\n  await expect(page.getByText(t('Wishlist.emptyWishlist'), { exact: true })).toBeVisible();\n});\n\ntest('Wishlist details displays an out of stock product correctly', async ({\n  page,\n  catalog,\n  customer,\n  settings,\n}) => {\n  await settings.setInventorySettings({ showOutOfStockMessage: true });\n\n  const product = await catalog.createSimpleProduct({\n    inventoryTracking: 'product',\n    inventoryLevel: 0,\n  });\n\n  const t = await getTranslations();\n  const { id: customerId } = await customer.login();\n  const { id: wishlistId, name } = await customer.createWishlist({\n    customerId,\n    items: [\n      {\n        productId: product.id,\n      },\n    ],\n  });\n\n  await page.goto(`/account/wishlists/${wishlistId}`);\n  await page.waitForLoadState('networkidle');\n  await expect(page.getByRole('heading', { name, exact: true })).toBeVisible();\n\n  await expect(page.getByRole('link', { name: product.name })).toBeVisible();\n  await expect(\n    page.getByRole('button', { name: t('Product.ProductDetails.Submit.outOfStock') }),\n  ).toBeDisabled();\n});\n\ntest('Toggling wishlist visibility works as expected', async ({ page, customer }) => {\n  const t = await getTranslations('Wishlist');\n  const { id: customerId } = await customer.login();\n  const { id: wishlistId, name } = await customer.createWishlist({\n    customerId,\n    isPublic: false,\n  });\n\n  await page.goto(`/account/wishlists/${wishlistId}`);\n  await page.waitForLoadState('networkidle');\n  await expect(page.getByRole('heading', { name, exact: true })).toBeVisible();\n\n  await expect(page.getByLabel(t('Visibility.private'))).toBeVisible();\n  await expect(page.getByRole('button', { name: t('share') })).toBeDisabled();\n\n  await page.getByLabel(t('Visibility.private')).click();\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByLabel(t('Visibility.public'))).toBeVisible();\n  await expect(page.getByRole('button', { name: t('share') })).toBeEnabled();\n});\n\ntest.describe('Wishlist details actions', () => {\n  test(\n    'Rename action renames the wishlist',\n    { tag: [TAGS.writesData] },\n    async ({ page, customer }) => {\n      const t = await getTranslations('Wishlist');\n      const { id: customerId } = await customer.login();\n      const { id: wishlistId, name } = await customer.createWishlist({ customerId });\n\n      await page.goto(`/account/wishlists/${wishlistId}`);\n      await page.waitForLoadState('networkidle');\n      await expect(page.getByRole('heading', { name, exact: true })).toBeVisible();\n\n      await page.getByRole('button', { name: t('actionsTitle') }).click();\n      await page.getByRole('menuitem', { name: t('rename') }).click();\n\n      await expect(\n        page.getByRole('heading', { name: t('Modal.renameTitle', { name }) }),\n      ).toBeVisible();\n\n      const newName = `${name} (renamed)`;\n\n      await page.getByRole('textbox', { name: t('Form.nameLabel') }).fill(newName);\n      await page.getByRole('button', { name: t('Modal.save') }).click();\n\n      await expect(page.getByText(t('Result.updateSuccess'))).toBeVisible();\n      await expect(page.getByRole('heading', { name })).toBeVisible();\n    },\n  );\n\n  test(\n    'Delete action deletes the wishlist',\n    { tag: [TAGS.writesData] },\n    async ({ page, customer }) => {\n      const t = await getTranslations('Wishlist');\n      const { id: customerId } = await customer.login();\n      const { id: wishlistId, name } = await customer.createWishlist({ customerId });\n\n      await page.goto(`/account/wishlists/${wishlistId}`);\n      await page.waitForLoadState('networkidle');\n      await expect(page.getByRole('heading', { name, exact: true })).toBeVisible();\n\n      await page.getByRole('button', { name: t('actionsTitle') }).click();\n      await page.getByRole('menuitem', { name: t('delete') }).click();\n      await page.getByRole('button', { name: t('Modal.delete') }).click();\n\n      await expect(page.getByText(t('Result.deleteSuccess'))).toBeVisible();\n      await expect(page).toHaveURL('/account/wishlists/');\n    },\n  );\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/account/wishlists.mobile.spec.ts",
    "content": "import { devices } from '@playwright/test';\n\nimport { testEnv } from '~/tests/environment';\nimport { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\n\ntest.use({ ...devices['iPhone 11'], permissions: ['clipboard-read'] });\n\ntest('Share button calls navigator.share with the correct URL', async ({ page, customer }) => {\n  const navigatorShareData: { hasBeenCalled: boolean; shareUrl?: string } = {\n    hasBeenCalled: false,\n    shareUrl: undefined,\n  };\n\n  const setNavigatorShareCalled = (data?: ShareData) => {\n    navigatorShareData.hasBeenCalled = true;\n    navigatorShareData.shareUrl = data?.url;\n  };\n\n  await page.exposeFunction('setNavigatorShareCalled', setNavigatorShareCalled);\n  await page.addInitScript(() => {\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    if (!window.navigator.share) {\n      window.navigator.share = async () => Promise.resolve();\n    }\n\n    const originalNavigatorShare = window.navigator.share.bind(window.navigator);\n\n    window.navigator.share = async (data) => {\n      setNavigatorShareCalled(data);\n\n      return originalNavigatorShare(data);\n    };\n  });\n\n  const t = await getTranslations();\n  const { id: customerId } = await customer.login();\n  const { name, token } = await customer.createWishlist({\n    customerId,\n    isPublic: true,\n  });\n\n  await page.goto('/account/wishlists/');\n  await page.waitForLoadState('networkidle');\n\n  const locator = page.getByRole('region', { name });\n\n  await locator.getByRole('button', { name: t('Wishlist.actionsTitle') }).click();\n  await page.getByRole('menuitem', { name: t('Wishlist.share') }).click();\n\n  await expect(page.getByText(t('Wishlist.shareSuccess'))).toBeVisible();\n\n  const expectedUrl = `${testEnv.PLAYWRIGHT_TEST_BASE_URL}/wishlist/${token}`;\n\n  expect(navigatorShareData.hasBeenCalled).toBe(true);\n  expect(navigatorShareData.shareUrl).toBe(expectedUrl);\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/account/wishlists.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\nimport { Browser } from '@playwright/test';\n\nimport { testEnv } from '~/tests/environment';\nimport { expect, test } from '~/tests/fixtures';\nimport { CustomerFixture } from '~/tests/fixtures/customer';\nimport { getTranslations } from '~/tests/lib/i18n';\nimport { TAGS } from '~/tests/tags';\n\ntest.use({ permissions: ['clipboard-read', 'clipboard-write'] });\n\nconst bold = (chunks: React.ReactNode) => chunks;\n\nasync function logoutInSeparateBrowser(fixture: CustomerFixture, browser: Browser) {\n  return test.step('Sign out in a different browser window', async () => {\n    const newBrowser = await browser.newContext();\n    const newPage = await newBrowser.newPage();\n    const customer = fixture.withNewPage(newPage);\n\n    await customer.login();\n    await newPage.goto('/account/wishlists/');\n    await newPage.waitForURL('/account/wishlists/');\n\n    await newPage.goto('/logout', { waitUntil: 'networkidle' });\n    await newPage.close();\n  });\n}\n\ntest(\n  'Creating a new wishlist works as expected',\n  { tag: [TAGS.writesData] },\n  async ({ page, customer }) => {\n    const t = await getTranslations('Wishlist');\n    const { id } = await customer.login();\n\n    await page.goto('/account/wishlists/');\n    await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n\n    await page.getByRole('button', { name: t('new'), exact: true }).click();\n\n    const wishlistName = `Wishlist ${faker.string.alpha(10)}`;\n\n    await page.getByLabel(t('Form.nameLabel')).fill(wishlistName);\n    await page.getByRole('button', { name: t('Modal.create') }).click();\n    await expect(page.getByText(t('Result.createSuccess'))).toBeVisible();\n\n    const locator = page.getByRole('region', { name: wishlistName });\n\n    await expect(locator.getByText(wishlistName)).toBeVisible();\n\n    await customer.deleteAllWishlists(id);\n  },\n);\n\ntest('Creating a new wishlist disallows empty names', async ({ page, customer }) => {\n  const t = await getTranslations('Wishlist');\n\n  await customer.login();\n\n  await page.goto('/account/wishlists/');\n  await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n\n  await page.getByRole('button', { name: t('new'), exact: true }).click();\n  await page.getByLabel(t('Form.nameLabel')).fill('');\n  await page.getByRole('button', { name: t('Modal.create') }).click();\n  await expect(page.getByText(t('Errors.nameRequired'))).toBeVisible();\n});\n\ntest('Wishlists page displays empty state when there are no wishlists', async ({\n  page,\n  customer,\n}) => {\n  const t = await getTranslations('Wishlist');\n  const { id } = await customer.login();\n\n  await customer.deleteAllWishlists(id);\n\n  await page.goto('/account/wishlists/');\n  await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n  await expect(page.getByRole('heading', { name: t('noWishlists'), exact: true })).toBeVisible();\n  await expect(page.getByRole('button', { name: t('new'), exact: true })).toBeVisible();\n  await expect(\n    page.getByRole('button', { name: t('noWishlistsCallToAction'), exact: true }),\n  ).toBeVisible();\n});\n\ntest('Wishlists page displays empty wishlists correctly', async ({ page, customer }) => {\n  const t = await getTranslations('Wishlist');\n  const { id: customerId } = await customer.login();\n  const wishlist1 = await customer.createWishlist({\n    customerId,\n    isPublic: true,\n  });\n\n  const wishlist2 = await customer.createWishlist({ customerId });\n\n  await page.goto('/account/wishlists/');\n  await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n\n  const wishlist1Locator = page.getByRole('region', { name: wishlist1.name });\n  const wishlist2Locator = page.getByRole('region', { name: wishlist2.name });\n\n  await expect(wishlist1Locator.getByText(wishlist1.name)).toBeVisible();\n  await expect(wishlist1Locator.getByText(t('Visibility.public'))).toBeVisible();\n  await expect(wishlist1Locator.getByRole('link', { name: t('viewWishlist') })).toBeVisible();\n  await expect(wishlist1Locator.getByRole('button', { name: t('actionsTitle') })).toBeVisible();\n  await expect(wishlist1Locator.getByText(t('emptyWishlist'))).toBeVisible();\n\n  await expect(wishlist2Locator.getByText(wishlist2.name)).toBeVisible();\n  await expect(wishlist2Locator.getByText(t('Visibility.private'))).toBeVisible();\n  await expect(wishlist2Locator.getByRole('link', { name: t('viewWishlist') })).toBeVisible();\n  await expect(wishlist2Locator.getByRole('button', { name: t('actionsTitle') })).toBeVisible();\n  await expect(wishlist2Locator.getByText(t('emptyWishlist'))).toBeVisible();\n});\n\ntest('Wishlists page displays a wishlist with simple and complex products correctly', async ({\n  page,\n  catalog,\n  customer,\n}) => {\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n  const productWithVariants = await catalog.getDefaultOrCreateComplexProduct();\n  const t = await getTranslations('Wishlist');\n  const { id: customerId } = await customer.login();\n  const wishlist1 = await customer.createWishlist({\n    customerId,\n    isPublic: true,\n    items: [\n      {\n        productId: product.id,\n      },\n      {\n        productId: productWithVariants.id,\n        variantId: productWithVariants.variants[1]?.id,\n      },\n    ],\n  });\n\n  await page.goto('/account/wishlists/');\n  await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n\n  const wishlistLocator = page.getByRole('region', { name: wishlist1.name });\n\n  await expect(wishlistLocator.getByText(wishlist1.name)).toBeVisible();\n  await expect(wishlistLocator.getByText(t('Visibility.public'))).toBeVisible();\n  await expect(wishlistLocator.getByRole('link', { name: t('viewWishlist') })).toBeVisible();\n  await expect(wishlistLocator.getByRole('button', { name: t('actionsTitle') })).toBeVisible();\n  await expect(\n    wishlistLocator.getByRole('link', { name: product.name, exact: true }),\n  ).toBeVisible();\n  await expect(\n    wishlistLocator.getByRole('link', { name: productWithVariants.name, exact: true }),\n  ).toBeVisible();\n});\n\ntest.describe('Wishlist actions menu', () => {\n  test('Wishlist actions menu displays all actions', async ({ page, customer }) => {\n    const t = await getTranslations('Wishlist');\n    const { id: customerId } = await customer.login();\n    const { name } = await customer.createWishlist({ customerId });\n\n    await page.goto('/account/wishlists/');\n    await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n\n    const locator = page.getByRole('region', { name });\n\n    await locator.getByRole('button', { name: t('actionsTitle') }).click();\n    await expect(page.getByRole('menuitem', { name: t('share') })).toBeVisible();\n    await expect(page.getByRole('menuitem', { name: t('rename') })).toBeVisible();\n    await expect(page.getByRole('menuitem', { name: t('makePublic') })).toBeVisible();\n    await expect(page.getByRole('menuitem', { name: t('delete') })).toBeVisible();\n  });\n\n  test('Share wishlist action is enabled, displays the correct URL for a public wishlist, and copies the URL to clipboard', async ({\n    page,\n    customer,\n  }) => {\n    const t = await getTranslations('Wishlist');\n    const { id: customerId } = await customer.login();\n    const { name, token } = await customer.createWishlist({\n      customerId,\n      isPublic: true,\n    });\n\n    await page.goto('/account/wishlists/');\n    await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n\n    const locator = page.getByRole('region', { name });\n\n    await locator.getByRole('button', { name: t('actionsTitle') }).click();\n    await page.getByRole('menuitem', { name: t('share') }).click();\n\n    const expectedUrl = `${testEnv.PLAYWRIGHT_TEST_BASE_URL}/wishlist/${token}`;\n\n    await expect(page.getByText(t('Modal.shareTitle', { name }))).toBeVisible();\n    await expect(page.getByRole('textbox')).toHaveValue(expectedUrl);\n\n    await page.getByRole('button', { name: t('Modal.copy') }).click();\n    await expect(page.getByText(t('shareCopied'))).toBeVisible();\n\n    const clipboardText = await page.evaluate(() => navigator.clipboard.readText());\n\n    expect(clipboardText).toBe(expectedUrl);\n  });\n\n  test('Share wishlist action is disabled for a private wishlist', async ({ page, customer }) => {\n    const t = await getTranslations('Wishlist');\n    const { id: customerId } = await customer.login();\n    const { name } = await customer.createWishlist({ customerId });\n\n    await page.goto('/account/wishlists/');\n    await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n\n    const locator = page.getByRole('region', { name });\n\n    await locator.getByRole('button', { name: t('actionsTitle') }).click();\n    await expect(page.getByRole('menuitem', { name: t('share') })).toBeDisabled();\n  });\n\n  test('Rename wishlist action renames a wishlist successfully', async ({ page, customer }) => {\n    const t = await getTranslations('Wishlist');\n    const { id: customerId } = await customer.login();\n    const { name } = await customer.createWishlist({ customerId });\n\n    await page.goto('/account/wishlists/');\n    await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n\n    const locator = page.getByRole('region', { name });\n\n    await locator.getByRole('button', { name: t('actionsTitle') }).click();\n    await page.getByRole('menuitem', { name: t('rename') }).click();\n\n    await expect(\n      page.getByRole('heading', { name: t('Modal.renameTitle', { name }) }),\n    ).toBeVisible();\n\n    await expect(page.getByLabel(t('Form.nameLabel'), { exact: true })).toHaveValue(name);\n\n    const newName = `${name} (renamed)`;\n\n    await page.getByLabel(t('Form.nameLabel'), { exact: true }).fill(newName);\n    await page.getByRole('button', { name: t('Modal.save') }).click();\n\n    await expect(page.getByText(t('Result.updateSuccess'))).toBeVisible();\n    await expect(page.getByRole('region').filter({ hasText: newName })).toBeVisible();\n  });\n\n  test('Rename wishlist action disallows empty names', async ({ page, customer }) => {\n    const t = await getTranslations('Wishlist');\n    const { id: customerId } = await customer.login();\n    const { name } = await customer.createWishlist({ customerId });\n\n    await page.goto('/account/wishlists/');\n    await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n\n    const locator = page.getByRole('region', { name });\n\n    await locator.getByRole('button', { name: t('actionsTitle') }).click();\n    await page.getByRole('menuitem', { name: t('rename') }).click();\n\n    await page.getByLabel(t('Form.nameLabel'), { exact: true }).fill('');\n    await page.getByRole('button', { name: t('Modal.save') }).click();\n\n    await expect(page.getByText(t('Errors.nameRequired'))).toBeVisible();\n  });\n\n  test('Rename wishlist action fails if the user is no longer logged in', async ({\n    page,\n    browser,\n    customer,\n  }) => {\n    const t = await getTranslations('Wishlist');\n    const { id: customerId } = await customer.login();\n    const { name } = await customer.createWishlist({ customerId });\n\n    await page.goto('/account/wishlists/');\n    await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n\n    await logoutInSeparateBrowser(customer, browser);\n\n    const locator = page.getByRole('region', { name });\n\n    await locator.getByRole('button', { name: t('actionsTitle') }).click();\n    await page.getByRole('menuitem', { name: t('rename') }).click();\n    await page.getByLabel(t('Form.nameLabel'), { exact: true }).fill(`${name} (renamed)`);\n    await page.getByRole('button', { name: t('Modal.save') }).click();\n\n    await expect(page.getByText(t('Errors.unauthorized'))).toBeVisible();\n  });\n\n  test('Public/private wishlist action toggles visibility', async ({ page, customer }) => {\n    const t = await getTranslations('Wishlist');\n    const { id: customerId } = await customer.login();\n    const { name } = await customer.createWishlist({\n      customerId,\n      isPublic: false,\n    });\n\n    await page.goto('/account/wishlists/');\n    await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n\n    const locator = page.getByRole('region', { name });\n\n    await expect(locator.getByText(t('Visibility.private'))).toBeVisible();\n    await locator.getByRole('button', { name: t('actionsTitle') }).click();\n\n    await page.getByRole('menuitem', { name: t('makePublic') }).click();\n\n    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n    const makePublicContent = t.rich('Modal.makePublicContent', {\n      name,\n      bold,\n    }) as string;\n\n    await expect(page.getByText(makePublicContent)).toBeVisible();\n    await page.getByRole('button', { name: t('makePublic') }).click();\n    await expect(page.getByText(t('Result.updateSuccess'))).toBeVisible();\n    await expect(locator.getByText(t('Visibility.public'))).toBeVisible();\n\n    await locator.getByRole('button', { name: t('actionsTitle') }).click();\n    await page.getByRole('menuitem', { name: t('makePrivate') }).click();\n\n    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n    const makePrivateContent = t.rich('Modal.makePrivateContent', {\n      name,\n      bold,\n    }) as string;\n\n    await expect(page.getByText(makePrivateContent)).toBeVisible();\n    await page.getByRole('button', { name: t('makePrivate') }).click();\n    await expect(page.getByText(t('Result.updateSuccess'))).toBeVisible();\n    await expect(locator.getByText(t('Visibility.private'))).toBeVisible();\n  });\n\n  test('Public/private wishlist action fails if the user is no longer logged in', async ({\n    page,\n    browser,\n    customer,\n  }) => {\n    const t = await getTranslations('Wishlist');\n    const { id: customerId } = await customer.login();\n    const { name } = await customer.createWishlist({ customerId });\n\n    await page.goto('/account/wishlists/');\n    await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n\n    await logoutInSeparateBrowser(customer, browser);\n\n    const locator = page.getByRole('region', { name });\n\n    await locator.getByRole('button', { name: t('actionsTitle') }).click();\n    await page.getByRole('menuitem', { name: t('makePublic') }).click();\n    await page.getByRole('button', { name: t('makePublic') }).click();\n\n    await expect(page.getByText(t('Errors.unauthorized'))).toBeVisible();\n  });\n\n  test('Delete wishlist action deletes a wishlist successfully', async ({ page, customer }) => {\n    const t = await getTranslations('Wishlist');\n    const { id: customerId } = await customer.login();\n    const { name } = await customer.createWishlist({ customerId });\n\n    await page.goto('/account/wishlists/');\n    await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n\n    const locator = page.getByRole('region', { name });\n\n    await locator.getByRole('button', { name: t('actionsTitle') }).click();\n    await page.getByRole('menuitem', { name: t('delete') }).click();\n\n    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n    const deleteContent = t.rich('Modal.deleteContent', {\n      name,\n      bold,\n    }) as string;\n\n    await expect(page.getByText(deleteContent)).toBeVisible();\n\n    await page.getByRole('button', { name: t('Modal.delete') }).click();\n    await expect(page.getByText(t('Result.deleteSuccess'))).toBeVisible();\n    await expect(locator).not.toBeVisible();\n  });\n\n  test('Delete wishlist action fails if the user is no longer logged in', async ({\n    page,\n    browser,\n    customer,\n  }) => {\n    const t = await getTranslations('Wishlist');\n    const { id: customerId } = await customer.login();\n    const { name } = await customer.createWishlist({ customerId });\n\n    await page.goto('/account/wishlists/');\n    await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n\n    await logoutInSeparateBrowser(customer, browser);\n\n    const locator = page.getByRole('region', { name });\n\n    await locator.getByRole('button', { name: t('actionsTitle') }).click();\n    await page.getByRole('menuitem', { name: t('delete') }).click();\n    await page.getByRole('button', { name: t('Modal.delete') }).click();\n\n    await expect(page.getByText(t('Errors.unauthorized'))).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/analytics-session.spec.ts",
    "content": "import { z } from 'zod';\n\nimport { expect, test } from '~/tests/fixtures';\n\nconst CookieSchema = z.object({\n  name: z.string(),\n  value: z.string(),\n  expires: z.number().optional(),\n});\n\ntest.describe('Analytics cookies proxy', () => {\n  test('sets visitorId and visitId cookies on first visit', async ({ page, context }) => {\n    await page.goto('/');\n\n    const cookies = await context.cookies();\n    const visitorId = cookies.find((c) => c.name === 'catalyst.visitorId');\n    const visitId = cookies.find((c) => c.name === 'catalyst.visitId');\n\n    expect(visitorId).toBeDefined();\n    expect(visitorId?.value).toBeUuid();\n    expect(visitId).toBeDefined();\n    expect(visitId?.value).toBeUuid();\n  });\n\n  test('visitId cookie has correct expiry', async ({ page, context }) => {\n    await page.goto('/');\n\n    const cookies = await context.cookies();\n    const visitId = cookies.find((c) => c.name === 'visitId');\n    const parsed = CookieSchema.safeParse(visitId);\n\n    if (parsed.success && parsed.data.expires) {\n      const visitIdExpiry = new Date(parsed.data.expires * 1000);\n      const now = Date.now();\n\n      expect(visitIdExpiry.getTime()).toBeGreaterThan(now);\n      expect(visitIdExpiry.getTime()).toBeLessThan(now + 31 * 60 * 1000); // +1 minute buffer\n    }\n  });\n\n  test('visitorId cookie has correct expiry', async ({ page, context }) => {\n    await page.goto('/');\n\n    const cookies = await context.cookies();\n    const visitorId = cookies.find((c) => c.name === 'visitorId');\n    const parsed = CookieSchema.safeParse(visitorId);\n\n    if (parsed.success && parsed.data.expires) {\n      const visitorIdExpiry = new Date(parsed.data.expires * 1000);\n      const now = Date.now();\n\n      expect(visitorIdExpiry.getTime()).toBeGreaterThan(now);\n      expect(visitorIdExpiry.getTime()).toBeLessThan(now + 401 * 24 * 60 * 60 * 1000); // +1 day buffer\n    }\n  });\n\n  test('creates a new visitId after expiry', async ({ page, context }) => {\n    await page.goto('/');\n\n    let cookies = await context.cookies();\n    const oldVisitId = cookies.find((c) => c.name === 'catalyst.visitId')?.value;\n\n    // Simulate expiry by clearing the visitId cookie\n    await context.clearCookies();\n    await page.reload();\n\n    cookies = await context.cookies();\n\n    const newVisitId = cookies.find((c) => c.name === 'catalyst.visitId')?.value;\n\n    expect(newVisitId).toBeDefined();\n    expect(newVisitId).not.toBe(oldVisitId);\n  });\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/auth/anonymous-session.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\ntest.describe('Anonymous session proxy', () => {\n  test('handles invalid anonymous JWT cookie gracefully', async ({ page, context }) => {\n    // Set an invalid anonymous JWT cookie to simulate the issue\n    await context.addCookies([\n      {\n        name: 'authjs.anonymous-session-token',\n        value: 'invalid.jwt.token.from.previous.instance',\n        domain: 'localhost',\n        path: '/',\n        httpOnly: true,\n        sameSite: 'Lax',\n      },\n    ]);\n\n    // Listen for console errors to verify logging\n    const consoleErrors: string[] = [];\n\n    page.on('console', (msg) => {\n      if (msg.type() === 'error') {\n        consoleErrors.push(msg.text());\n      }\n    });\n\n    // Navigate to the homepage - this should trigger the proxy\n    await page.goto('/');\n\n    // The page should load successfully despite the invalid cookie\n    await expect(page).toHaveTitle(/.*Catalyst/);\n\n    // Check that the invalid cookie was cleared and a new valid one was set\n    const cookies = await context.cookies();\n    const anonymousCookie = cookies.find((c) => c.name === 'authjs.anonymous-session-token');\n\n    // Should have a new valid cookie (not the invalid one we set)\n    expect(anonymousCookie).toBeDefined();\n    expect(anonymousCookie?.value).not.toBe('invalid.jwt.token.from.previous.instance');\n\n    // Verify that the application continues to work normally\n    // For example, check that navigation works\n    const navigation = page.getByRole('navigation', { name: 'Main' });\n\n    await expect(navigation).toBeVisible();\n  });\n\n  test('creates anonymous session when no cookie exists', async ({ page, context }) => {\n    // Clear all cookies first\n    await context.clearCookies();\n\n    await page.goto('/');\n\n    // Should create a new anonymous session cookie\n    const cookies = await context.cookies();\n    const anonymousCookie = cookies.find((c) => c.name === 'authjs.anonymous-session-token');\n\n    expect(anonymousCookie).toBeDefined();\n    expect(anonymousCookie?.value).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/auth/forgot-password.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\n\ntest('Forgot password form works as expected', async ({ page }) => {\n  const t = await getTranslations('Auth.Login.ForgotPassword');\n  const email = faker.internet.email({ provider: 'example.com' });\n\n  await page.goto('/login/forgot-password');\n  await page.getByRole('heading', { name: t('title') }).waitFor();\n\n  // TODO: Forgot password form fields and CTA need to be translated\n  await page.getByLabel('Email').fill(email);\n  await page.getByRole('button', { name: 'Reset password' }).click();\n\n  await expect(page.getByText(t('confirmResetPassword', { email }))).toBeVisible();\n});\n\ntest('Forgot password form displays error if email is not valid', async ({ page }) => {\n  const t = await getTranslations('Auth.Login.ForgotPassword');\n\n  await page.goto('/login/forgot-password');\n  await page.getByRole('heading', { name: t('title') }).waitFor();\n\n  await page.getByLabel('Email').fill('not-an-email');\n  await page.getByRole('button', { name: 'Reset password' }).click();\n\n  await expect(page.getByText(t('FieldErrors.emailInvalid'))).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/auth/login.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\n\ntest.use({ reuseCustomerSession: false });\n\ntest('Login and logout flows work as expected', async ({ page, customer }) => {\n  const t = await getTranslations();\n\n  await customer.login();\n  await page.waitForURL('/account/orders/');\n  await expect(\n    page.getByRole('heading', { name: t('Account.Orders.title') }).first(),\n  ).toBeVisible();\n\n  await customer.logout();\n  await expect(page.getByRole('heading', { name: t('Auth.Login.heading') })).toBeVisible();\n});\n\ntest('Login with redirectTo query parameter works with a simple path', async ({\n  page,\n  customer,\n}) => {\n  const redirectTo = '/test-path/';\n\n  await customer.login(redirectTo);\n  await page.waitForURL(redirectTo);\n});\n\ntest('Login with redirectTo query parameter works with a path containing query parameters', async ({\n  page,\n  customer,\n}) => {\n  const redirectTo = '/test-path/?param1=value1&param2=value2';\n\n  await customer.login(redirectTo);\n  await page.waitForURL(redirectTo);\n});\n\ntest('Login with invalid credentials returns an error', async ({ page }) => {\n  const t = await getTranslations('Auth.Login');\n\n  await page.goto(`/login`);\n  await page.getByLabel(t('email')).fill('invalid-email-testing@testing.com');\n  await page.getByLabel(t('password')).fill('invalid-password');\n  await page.getByRole('button', { name: t('cta') }).click();\n\n  await expect(page.getByText(t('invalidCredentials'))).toBeVisible();\n});\n\ntest('Browsing to /login/ when already logged in redirects to /account/orders/', async ({\n  page,\n  customer,\n}) => {\n  const t = await getTranslations();\n  const ordersHeader = t('Account.Orders.title');\n\n  await customer.login();\n  await page.waitForURL('/account/orders/');\n  await expect(page.getByRole('heading', { name: ordersHeader, exact: true })).toBeVisible();\n\n  await page.goto('/login/');\n  await page.waitForURL('/account/orders/');\n  await expect(page.getByRole('heading', { name: ordersHeader, exact: true })).toBeVisible();\n});\n\ntest('JWT login works as expected and redirects to /account/orders/ by default', async ({\n  page,\n  customer,\n}) => {\n  const t = await getTranslations('Account.Orders');\n  const { id } = await customer.getOrCreateTestCustomer();\n  const jwt = await customer.generateLoginJwt(id);\n\n  await page.goto(`/login/token/${jwt}`);\n  await page.waitForURL('/account/orders/');\n  await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n});\n\ntest('JWT login redirects to the specified redirect_to value in the token payload', async ({\n  page,\n  customer,\n}) => {\n  const t = await getTranslations('Account.Addresses');\n  const { id } = await customer.getOrCreateTestCustomer();\n  const jwt = await customer.generateLoginJwt(id, '/account/addresses/');\n\n  await page.goto(`/login/token/${jwt}`);\n  await page.waitForURL('/account/addresses/');\n  await expect(page.getByRole('heading', { name: t('title'), exact: true })).toBeVisible();\n});\n\ntest('JWT login with an invalid/expired token shows an error message', async ({ page }) => {\n  const t = await getTranslations('Auth.Login');\n\n  const invalidJwt =\n    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';\n\n  await page.goto(`/login/token/${invalidJwt}`);\n  await page.waitForURL('/login/?error=InvalidToken');\n  await expect(page.getByText(t('invalidToken'))).toBeVisible();\n});\n\ntest('After invalid JWT login, manually logging in with invalid credentials will replace the error message displayed', async ({\n  page,\n}) => {\n  const t = await getTranslations('Auth.Login');\n\n  const invalidJwt =\n    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';\n\n  await page.goto(`/login/token/${invalidJwt}`);\n  await page.waitForURL('/login/?error=InvalidToken');\n  await expect(page.getByText(t('invalidToken'))).toBeVisible();\n\n  await page.getByLabel(t('email')).fill('invalid-email-testing@testing.com');\n  await page.getByLabel(t('password')).fill('invalid-password');\n  await page.getByRole('button', { name: t('cta') }).click();\n\n  await page.waitForURL('/login/');\n  await expect(page.getByText(t('invalidToken'))).not.toBeVisible();\n  await expect(page.getByText(t('invalidCredentials'))).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/auth/logout.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\ntest('Logout works as expected', async ({ page, customer }) => {\n  await customer.login();\n\n  await page.goto(`/logout/`);\n  await expect(page).toHaveURL('/login/');\n\n  await page.goto('/account/orders');\n  await expect(page).toHaveURL('/login/');\n});\n\ntest('Logout with redirectTo query parameter logs out customer and redirects to the path', async ({\n  page,\n  customer,\n}) => {\n  await customer.login();\n\n  const redirectTo = '/test-path/';\n\n  await page.goto(`/logout?redirectTo=${redirectTo}`);\n  await expect(page).toHaveURL(redirectTo);\n\n  // Ensure the customer is logged out\n  await page.goto('/account/orders');\n  await expect(page).toHaveURL('/login/');\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/auth/register.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\nimport { TAGS } from '~/tests/tags';\n\ntest('Registration works as expected', { tag: [TAGS.writesData] }, async ({ page, customer }) => {\n  const t = await getTranslations();\n\n  const email = faker.internet.email({ provider: 'example.com' });\n  // Prefix is added to ensure that the password requirements are met\n  const password = faker.internet.password({\n    pattern: /[a-zA-Z0-9]/,\n    prefix: '1At!',\n    length: 10,\n  });\n  const phone = faker.phone.number({ style: 'national' });\n  const streetAddress = faker.location.streetAddress();\n  const city = faker.location.city();\n  const state = faker.location.state();\n  const postalCode = faker.location.zipCode();\n\n  await page.goto('/register');\n  await page.getByRole('heading', { name: t('Auth.Register.heading') }).waitFor();\n\n  // TODO: Form fields when creating a new account need to be translated\n  await page.getByLabel('First Name').fill(faker.person.firstName());\n  await page.getByLabel('Last Name').fill(faker.person.lastName());\n  await page.getByLabel('Email Address').fill(email);\n  await page.getByLabel('Password', { exact: true }).fill(password);\n  await page.getByLabel('Confirm Password').fill(password);\n  await page.getByLabel('Phone').fill(phone);\n  await page.getByLabel('Address Line 1').fill(streetAddress);\n  await page.getByLabel('Suburb/City').fill(city);\n  await page.getByLabel('State/Province').fill(state);\n  await page.getByLabel('Zip/Postcode').fill(postalCode);\n  await page.getByRole('combobox', { name: 'Country' }).click();\n  await page.keyboard.type('United States');\n  await page.keyboard.press('Enter');\n\n  // Click reCAPTCHA if enabled (uses test key — no challenge, always passes)\n  const recaptchaFrame = page.frameLocator('iframe[title=\"reCAPTCHA\"]');\n  const recaptchaCheckbox = recaptchaFrame.locator('.recaptcha-checkbox-border');\n\n  if (await recaptchaCheckbox.isVisible()) {\n    await recaptchaCheckbox.click();\n    await recaptchaFrame.locator('.recaptcha-checkbox-checked').waitFor();\n  }\n\n  await page.getByRole('button', { name: t('Auth.Register.cta') }).click();\n\n  await expect(page).toHaveURL('/account/orders/');\n  await expect(\n    page.getByRole('heading', { name: t('Account.Orders.title'), exact: true }),\n  ).toBeVisible();\n\n  const { id } = await customer.getByEmail(email);\n\n  // Ensure registered customer is cleaned up after the test\n  await customer.delete(id);\n});\n\ntest('Registration fails if email is already in use', async ({ page, customer }) => {\n  const { email } = await customer.createNewCustomer();\n  const t = await getTranslations('Auth.Register');\n\n  // Prefix is added to ensure that the password requirements are met\n  const password = faker.internet.password({\n    pattern: /[a-zA-Z0-9]/,\n    prefix: '1At!',\n    length: 10,\n  });\n  const phone = faker.phone.number({ style: 'national' });\n  const streetAddress = faker.location.streetAddress();\n  const city = faker.location.city();\n  const state = faker.location.state();\n  const postalCode = faker.location.zipCode();\n\n  await page.goto('/register');\n  await page.getByRole('heading', { name: t('heading') }).waitFor();\n\n  // TODO: Form fields when creating a new account need to be translated\n  await page.getByLabel('First Name').fill(faker.person.firstName());\n  await page.getByLabel('Last Name').fill(faker.person.lastName());\n  await page.getByLabel('Email Address').fill(email);\n  await page.getByLabel('Password', { exact: true }).fill(password);\n  await page.getByLabel('Confirm Password').fill(password);\n  await page.getByLabel('Phone').fill(phone);\n  await page.getByLabel('Address Line 1').fill(streetAddress);\n  await page.getByLabel('Suburb/City').fill(city);\n  await page.getByLabel('State/Province').fill(state);\n  await page.getByLabel('Zip/Postcode').fill(postalCode);\n  await page.getByRole('combobox', { name: 'Country' }).click();\n  await page.keyboard.type('United States');\n  await page.keyboard.press('Enter');\n\n  // Click reCAPTCHA if enabled (uses test key — no challenge, always passes)\n  const recaptchaFrame = page.frameLocator('iframe[title=\"reCAPTCHA\"]');\n  const recaptchaCheckbox = recaptchaFrame.locator('.recaptcha-checkbox-border');\n\n  if (await recaptchaCheckbox.isVisible()) {\n    await recaptchaCheckbox.click();\n    await recaptchaFrame.locator('.recaptcha-checkbox-checked').waitFor();\n  }\n\n  await page.getByRole('button', { name: t('cta') }).click();\n\n  await expect(page).not.toHaveURL('/account/orders/');\n\n  // TODO: Error message needs to be translated\n  await expect(page.getByText('The email address is already in use.')).toBeVisible();\n});\n\ntest('Registration fails if reCAPTCHA is not completed', async ({ page }) => {\n  const t = await getTranslations('Auth.Register');\n\n  await page.goto('/register');\n  await page.getByRole('heading', { name: t('heading') }).waitFor();\n\n  const recaptchaFrame = page.frameLocator('iframe[title=\"reCAPTCHA\"]');\n  const recaptchaCheckbox = recaptchaFrame.locator('.recaptcha-checkbox-border');\n\n  try {\n    await recaptchaCheckbox.waitFor({ state: 'visible', timeout: 5000 });\n  } catch {\n    test.skip();\n  }\n\n  // Fill form but intentionally skip clicking reCAPTCHA\n  await page.getByLabel('First Name').fill(faker.person.firstName());\n  await page.getByLabel('Last Name').fill(faker.person.lastName());\n  await page.getByLabel('Email Address').fill(faker.internet.email({ provider: 'example.com' }));\n\n  const password = faker.internet.password({ pattern: /[a-zA-Z0-9]/, prefix: '1At!', length: 10 });\n\n  await page.getByLabel('Password', { exact: true }).fill(password);\n  await page.getByLabel('Confirm Password').fill(password);\n  await page.getByLabel('Phone').fill(faker.phone.number({ style: 'national' }));\n  await page.getByLabel('Address Line 1').fill(faker.location.streetAddress());\n  await page.getByLabel('Suburb/City').fill(faker.location.city());\n  await page.getByLabel('State/Province').fill(faker.location.state());\n  await page.getByLabel('Zip/Postcode').fill(faker.location.zipCode());\n  await page.getByRole('combobox', { name: 'Country' }).click();\n  await page.keyboard.type('United States');\n  await page.keyboard.press('Enter');\n\n  await page.getByRole('button', { name: t('cta') }).click();\n\n  await expect(page).not.toHaveURL('/account/orders/');\n  await expect(page.getByText(t('recaptchaRequired'))).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/blog.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\n\ntest('Blog is accessible and displays posts', async ({ page, blog }) => {\n  const t = await getTranslations('Blog');\n  const { name, path } = await blog.getBlog();\n  const posts = await blog.getPosts();\n\n  await page.goto(path);\n  await expect(page.getByRole('heading', { name })).toBeVisible();\n\n  const breadcrumbs = page.getByLabel('breadcrumb');\n\n  await expect(breadcrumbs.getByText(t('home')).first()).toBeVisible();\n  await expect(breadcrumbs.getByText(name).first()).toBeVisible();\n\n  // eslint-disable-next-line @typescript-eslint/no-misused-promises\n  posts.forEach(async (post) => {\n    await expect(\n      page.locator(`a[href*=\"${post.path}\"]`).filter({ hasText: post.title }).first(),\n    ).toBeVisible();\n  });\n});\n\ntest('Blog can be filtered by tags', async ({ page, blog }) => {\n  const t = await getTranslations('Blog');\n  const { name, path } = await blog.getBlog();\n  const tag = faker.string.alpha(10);\n  const post = await blog.createPost({\n    tags: [tag],\n  });\n\n  await page.goto(`${path}?tag=${tag}`);\n  await expect(page.getByRole('heading', { name })).toBeVisible();\n\n  const breadcrumbs = page.getByLabel('breadcrumb');\n\n  await expect(breadcrumbs.getByText(t('home'))).toBeVisible();\n  await expect(breadcrumbs.getByText(name)).toBeVisible();\n  await expect(breadcrumbs.getByText(tag)).toBeVisible();\n  await expect(page.getByRole('link', { name: post.title })).toBeVisible();\n});\n\ntest('Blog post page displays content, breadcrumbs, tags, and author info', async ({\n  page,\n  blog,\n}) => {\n  const t = await getTranslations('Blog');\n  const { name } = await blog.getBlog();\n  const post = await blog.createPost({\n    author: faker.person.fullName(),\n    body: `\n    <h1>Test header element</h2>\n    <p>Test paragraph element</p>\n    <div>Test div element</div>\n    <a href=\"https://example.com\">Test link element</a>\n    `,\n    tags: ['Tag 1', 'Tag 2'],\n  });\n\n  await page.goto(post.path);\n  await expect(page.getByRole('heading', { name: post.title })).toBeVisible();\n\n  const breadcrumbs = page.getByLabel('breadcrumb');\n\n  await expect(breadcrumbs.getByText(t('home'))).toBeVisible();\n  await expect(breadcrumbs.getByText(name)).toBeVisible();\n  await expect(breadcrumbs.getByText(post.title)).toBeVisible();\n\n  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n  await expect(page.getByText(post.author!)).toBeVisible();\n  await expect(page.getByRole('link', { name: 'Tag 1' })).toBeVisible();\n  await expect(page.getByRole('link', { name: 'Tag 2' })).toBeVisible();\n  await expect(page.getByText('Test header element')).toBeVisible();\n  await expect(page.getByText('Test paragraph element')).toBeVisible();\n  await expect(page.getByText('Test div element')).toBeVisible();\n  await expect(page.getByRole('link', { name: 'Test link element' })).toHaveAttribute(\n    'href',\n    'https://example.com',\n  );\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/cart.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport { getFormatter } from '~/tests/lib/formatter';\nimport { getTranslations } from '~/tests/lib/i18n';\n\ntest('Cart page displays empty state when no items are in the cart', async ({ page }) => {\n  const t = await getTranslations('Cart');\n\n  await page.goto('/cart');\n\n  await expect(page.getByRole('heading', { name: t('title') })).toBeVisible();\n  await expect(page.getByRole('heading', { name: t('Empty.title'), exact: true })).toBeVisible();\n  await expect(page.getByText(t('Empty.subtitle')).first()).toBeVisible();\n  await expect(page.getByRole('link', { name: t('Empty.cta') })).toBeVisible();\n});\n\ntest('Cart page displays line item', async ({ page, catalog, currency }) => {\n  const format = getFormatter();\n  const t = await getTranslations();\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto(product.path);\n  await page.getByRole('button', { name: t('Product.ProductDetails.Submit.addToCart') }).click();\n\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  const addToCartSuccessMessage = t.rich('Product.ProductDetails.successMessage', {\n    cartItems: 1,\n    cartLink: (chunks: React.ReactNode) => chunks,\n  }) as string;\n\n  await expect(page.getByText(addToCartSuccessMessage)).toBeVisible();\n\n  await page.goto('/cart');\n\n  await expect(page.getByRole('heading', { name: t('Cart.title') })).toBeVisible();\n  await expect(page.getByRole('heading', { name: t('Cart.CheckoutSummary.title') })).toBeVisible();\n\n  const lineItem = page.getByRole('listitem').filter({ hasText: product.name });\n  const formattedPrice = format.number(product.price, {\n    style: 'currency',\n    currency: await currency.getDefaultCurrency(),\n  });\n\n  await expect(lineItem.getByText(formattedPrice)).toBeVisible();\n});\n\ntest('Cart page allows updating item quantity', async ({ page, catalog }) => {\n  const t = await getTranslations();\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  if (product.inventoryLevel === 0 && product.inventoryTracking !== 'none') {\n    test.skip(\n      true,\n      'Product is out of stock, skipping test. This means that the DEFAULT_PRODUCT_ID points to an out of stock product.',\n    );\n  }\n\n  await page.goto(product.path);\n  await page.getByRole('button', { name: t('Product.ProductDetails.Submit.addToCart') }).click();\n\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  const addToCartSuccessMessage = t.rich('Product.ProductDetails.successMessage', {\n    cartItems: 1,\n    cartLink: (chunks: React.ReactNode) => chunks,\n  }) as string;\n\n  await expect(page.getByText(addToCartSuccessMessage)).toBeVisible();\n\n  await page.goto('/cart');\n  await expect(page.getByRole('heading', { name: `${t('Cart.title')}1` })).toBeVisible();\n\n  await page.getByLabel(t('Cart.increment')).click();\n  await expect(page.getByRole('heading', { name: `${t('Cart.title')}2` })).toBeVisible();\n});\n\ntest('Cart page allows removing a line item', async ({ page, catalog }) => {\n  const t = await getTranslations();\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto(product.path);\n  await page.getByRole('button', { name: t('Product.ProductDetails.Submit.addToCart') }).click();\n\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  const addToCartSuccessMessage = t.rich('Product.ProductDetails.successMessage', {\n    cartItems: 1,\n    cartLink: (chunks: React.ReactNode) => chunks,\n  }) as string;\n\n  await expect(page.getByText(addToCartSuccessMessage)).toBeVisible();\n\n  await page.goto('/cart');\n  await expect(page.getByRole('heading', { name: t('Cart.title') })).toBeVisible();\n\n  await page.getByRole('button', { name: t('Cart.removeItem') }).click();\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByRole('heading', { name: t('Cart.Empty.title') })).toBeVisible();\n});\n\ntest('Cart page can proceed to checkout', async ({ page, catalog }) => {\n  const t = await getTranslations();\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto(product.path);\n  await page.getByRole('button', { name: t('Product.ProductDetails.Submit.addToCart') }).click();\n\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  const addToCartSuccessMessage = t.rich('Product.ProductDetails.successMessage', {\n    cartItems: 1,\n    cartLink: (chunks: React.ReactNode) => chunks,\n  }) as string;\n\n  await expect(page.getByText(addToCartSuccessMessage)).toBeVisible();\n\n  await page.goto('/cart');\n  await expect(page.getByRole('heading', { name: t('Cart.title') })).toBeVisible();\n\n  await page.getByRole('button', { name: t('Cart.proceedToCheckout') }).click();\n  await page.waitForURL('**/checkout', {\n    waitUntil: 'networkidle',\n  });\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/checkout.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { expect, Page, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\n\nasync function selectCountry(page: Page) {\n  const countryInput = page.locator('#countryCodeInput');\n  const countryInputValues = await Promise.all(\n    (await countryInput.getByRole('option').all()).map((o) => o.getAttribute('value')),\n  );\n  const countryCodes = countryInputValues.filter(\n    (value): value is string => value !== null && value !== '',\n  );\n\n  await countryInput.selectOption(faker.helpers.arrayElement(countryCodes));\n}\n\nasync function selectState(page: Page) {\n  const stateSelector = page.locator('#provinceCodeInput');\n\n  if (await stateSelector.isVisible()) {\n    const stateInputValues = await Promise.all(\n      (await stateSelector.getByRole('option').all()).map((o) => o.getAttribute('value')),\n    );\n    const stateCodes = stateInputValues.filter(\n      (value): value is string => value !== null && value !== '',\n    );\n\n    await stateSelector.selectOption(faker.helpers.arrayElement(stateCodes));\n  } else {\n    const stateInput = page.locator('#provinceInput');\n\n    await stateInput.fill(faker.location.state());\n  }\n}\n\nasync function selectShippingMethod(page: Page) {\n  await page.locator('label[for*=\"shippingOptionRadio\"]').first().waitFor({ state: 'visible' });\n\n  const shippingSelector = await page.locator('label[for*=\"shippingOptionRadio\"]').all();\n\n  await faker.helpers.arrayElement(shippingSelector).click();\n  await page.waitForLoadState('networkidle');\n}\n\nasync function enterAddressDetails(page: Page) {\n  await page.locator('#firstNameInput').fill(faker.person.firstName());\n  await page.locator('#lastNameInput').fill(faker.person.lastName());\n  await page.locator('#addressLine1Input').fill(faker.location.streetAddress());\n  await page.locator('#addressLine2Input').fill(faker.location.secondaryAddress());\n  await page.locator('#cityInput').fill(faker.location.city());\n  await selectCountry(page);\n  await selectState(page);\n  await page.locator('#postCodeInput').fill(faker.location.zipCode('#####'));\n  await selectShippingMethod(page);\n  await page.locator('button[type=\"submit\"]').click();\n  await page.waitForLoadState('networkidle');\n}\n\nasync function enterPaymentDetails(page: Page) {\n  await page.locator('label[for=\"radio-bigpaypay\"]').click();\n\n  const iframeCcNumber = page.frameLocator('#bigpaypay-ccNumber iframe').locator('#card-number');\n  const ccNumber = page.locator('#ccNumber');\n  const iframeCcExpiry = page.frameLocator('#bigpaypay-ccExpiry iframe').locator('#card-expiry');\n  const ccExpiry = page.locator('#ccExpiry');\n  const iframeCcName = page.frameLocator('#bigpaypay-ccName iframe').locator('#card-name');\n  const ccName = page.locator('#ccName');\n  const iframeCcCvc = page.frameLocator('#bigpaypay-ccCvv iframe').locator('#card-code');\n  const ccCvc = page.locator('#ccCvv');\n\n  await iframeCcNumber.or(ccNumber).fill('4111 1111 1111 1111');\n  await iframeCcExpiry.or(ccExpiry).fill('12/30');\n  await iframeCcName.or(ccName).fill('success'); // BC test payment gateway requires 'success' as name\n  await iframeCcCvc.or(ccCvc).fill('123');\n}\n\ntest('Checkout works as a guest shopper', async ({ page, catalog }) => {\n  const t = await getTranslations();\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto(product.path);\n  await page.getByRole('button', { name: t('Product.ProductDetails.Submit.addToCart') }).click();\n\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  const addToCartSuccessMessage = t.rich('Product.ProductDetails.successMessage', {\n    cartItems: 1,\n    cartLink: (chunks: React.ReactNode) => chunks,\n  }) as string;\n\n  await expect(page.getByText(addToCartSuccessMessage)).toBeVisible();\n\n  await page.goto('/cart');\n  await page.getByRole('button', { name: t('Cart.proceedToCheckout') }).click();\n  await page.waitForURL('**/checkout');\n\n  await page\n    .locator('input[name=\"email\"]')\n    .fill(faker.internet.email({ provider: 'test.catalyst' }));\n  await page.locator('button[type=\"submit\"]').click();\n  await page.waitForLoadState('networkidle');\n\n  await enterAddressDetails(page);\n  await enterPaymentDetails(page);\n\n  // Complete order\n  await page.locator('button[type=\"submit\"]').click();\n  await page.waitForLoadState('networkidle');\n  await page.waitForURL('**/checkout/order-confirmation', { timeout: 5000 });\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/compare.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport { getFormatter } from '~/tests/lib/formatter';\nimport { getTranslations } from '~/tests/lib/i18n';\n\ntest('Validate compare page', async ({ page, catalog, currency }) => {\n  const format = getFormatter();\n  const t = await getTranslations('Compare');\n  const defaultCurrency = await currency.getDefaultCurrency();\n\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n  const productWithVariants = await catalog.getDefaultOrCreateComplexProduct();\n\n  await page.goto(`/compare/?ids=${product.id},${productWithVariants.id}`);\n  await expect(page.getByRole('heading', { name: `${t('title')} 2` })).toBeVisible();\n\n  // Products names\n  await expect(page.getByText(product.name, { exact: true }).first()).toBeVisible();\n  await expect(page.getByText(productWithVariants.name, { exact: true }).first()).toBeVisible();\n\n  // Products CTAs\n  await expect(page.getByRole('button', { name: t('addToCart') })).toBeVisible();\n  await expect(page.getByRole('link', { name: t('viewOptions') })).toBeVisible();\n\n  // Product prices\n  const productPrice = format.number(product.price, {\n    style: 'currency',\n    currency: defaultCurrency,\n  });\n\n  const productWithVariantsPrice = format.number(productWithVariants.price, {\n    style: 'currency',\n    currency: defaultCurrency,\n  });\n\n  await expect(page.getByText(productPrice, { exact: true }).first()).toBeVisible();\n  await expect(page.getByText(productWithVariantsPrice, { exact: true }).first()).toBeVisible();\n});\n\ntest('Validate compare page with alternate currency', async ({ page, catalog, currency }) => {\n  const format = getFormatter();\n  const defaultCurrency = await currency.getDefaultCurrency();\n  const alternateCurrency = (await currency.getEnabledCurrencies()).find(\n    (c) => c !== defaultCurrency,\n  );\n\n  if (!alternateCurrency) {\n    test.skip(true, 'No alternative currencies found.');\n\n    return;\n  }\n\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n  const productWithVariants = await catalog.getDefaultOrCreateComplexProduct();\n\n  await page.goto(`/compare/?ids=${product.id},${productWithVariants.id}`);\n  await page.waitForLoadState('networkidle');\n  await expect(\n    page\n      .getByText(\n        format.number(product.price, {\n          style: 'currency',\n          currency: defaultCurrency,\n        }),\n      )\n      .first(),\n  ).toBeVisible();\n\n  await currency.selectCurrency(alternateCurrency);\n\n  const productPriceConverted = await currency.convertWithExchangeRate(\n    alternateCurrency,\n    product.price,\n  );\n\n  const formattedProductPrice = format.number(productPriceConverted, {\n    style: 'currency',\n    currency: alternateCurrency,\n  });\n\n  await expect(page.getByText(formattedProductPrice)).toBeVisible();\n\n  const productWithVariantsPriceConverted = await currency.convertWithExchangeRate(\n    alternateCurrency,\n    productWithVariants.price,\n  );\n\n  const formattedProductWithVariantsPrice = format.number(productWithVariantsPriceConverted, {\n    style: 'currency',\n    currency: alternateCurrency,\n  });\n\n  await expect(page.getByText(formattedProductWithVariantsPrice).first()).toBeVisible();\n});\n\ntest('Can add simple product to cart', async ({ page, catalog }) => {\n  const t = await getTranslations('Compare');\n\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto(`/compare/?ids=${product.id}`);\n\n  await page.getByRole('button', { name: t('addToCart') }).click();\n\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  const addToCartSuccessMessage = t.rich('successMessage', {\n    cartItems: 1,\n    cartLink: (chunks: React.ReactNode) => chunks,\n  }) as string;\n\n  await expect(page.getByText(addToCartSuccessMessage)).toBeVisible();\n});\n\ntest(\"Product with variants 'View options' redirect to product page\", async ({ page, catalog }) => {\n  const product = await catalog.getDefaultOrCreateComplexProduct();\n\n  await page.goto(`/compare/?ids=${product.id}`);\n  await page.getByRole('link', { name: 'View options' }).click();\n\n  await expect(page.getByRole('heading', { name: product.name })).toBeVisible();\n});\n\ntest('Disabled add to cart for out of stock products', async ({ page, catalog }) => {\n  const t = await getTranslations('Compare');\n\n  const product = await catalog.createSimpleProduct({\n    inventoryTracking: 'product',\n    inventoryLevel: 0,\n  });\n\n  const productWithVariants = await catalog.createComplexProduct({\n    inventoryTracking: 'product',\n    inventoryLevel: 0,\n  });\n\n  await page.goto(`/compare/?ids=${product.id},${productWithVariants.id}`);\n\n  // Simple products should have the add to cart button disabled\n  await expect(page.getByRole('button', { name: t('addToCart') })).toBeDisabled();\n  // Product with variants should have the view options link even when OOS\n  await expect(page.getByRole('link', { name: t('viewOptions') })).toBeVisible();\n});\n\ntest('Show empty state when no products are selected', async ({ page }) => {\n  const t = await getTranslations('Compare');\n\n  await page.goto('/compare');\n\n  await expect(page.getByText(t('noProductsToCompare')).first()).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/coupon.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\n\ntest('Valid coupon code can be applied to the cart', async ({ page, catalog, promotion }) => {\n  const t = await getTranslations();\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n  const coupon = await promotion.createCouponCode();\n\n  await page.goto(product.path);\n  await page.getByRole('button', { name: t('Product.ProductDetails.Submit.addToCart') }).click();\n\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  const addToCartSuccessMessage = t.rich('Product.ProductDetails.successMessage', {\n    cartItems: 1,\n    cartLink: (chunks: React.ReactNode) => chunks,\n  }) as string;\n\n  await expect(page.getByText(addToCartSuccessMessage)).toBeVisible();\n  await page.goto('/cart');\n\n  await expect(page.getByRole('heading', { name: t('Cart.title') })).toBeVisible();\n\n  await page.getByLabel(t('Cart.CheckoutSummary.CouponCode.couponCode')).fill(coupon.code);\n  await page.getByRole('button', { name: t('Cart.CheckoutSummary.CouponCode.apply') }).click();\n  await page.waitForLoadState('networkidle');\n\n  try {\n    await expect(page.getByText(coupon.code)).toBeVisible();\n    await expect(\n      page.getByRole('button', { name: t('Cart.CheckoutSummary.CouponCode.removeCouponCode') }),\n    ).toBeVisible();\n  } catch {\n    // TODO: Remove try/catch when root cause of next state issue is found/resolved [CATALYST-1685]\n    // NextJS seems to have some issues when running local builds.\n    // In this test, the coupon button will get stuck spinning forever and cause the assertions to fail.\n    // This doesn't happen on deployed production builds, just local next builds.\n    // To combat this, if the previous assertions fail, we hard refresh the page and then try again.\n    await page.reload();\n    await expect(page.getByText(coupon.code)).toBeVisible();\n    await expect(\n      page.getByRole('button', { name: t('Cart.CheckoutSummary.CouponCode.removeCouponCode') }),\n    ).toBeVisible();\n\n    // eslint-disable-next-line no-console\n    console.warn('Coupon applied but page got stuck in loading state.');\n  }\n});\n\ntest('Invalid coupon code cannot be applied', async ({ page, catalog }) => {\n  const t = await getTranslations();\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto(product.path);\n  await page.getByRole('button', { name: t('Product.ProductDetails.Submit.addToCart') }).click();\n\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  const addToCartSuccessMessage = t.rich('Product.ProductDetails.successMessage', {\n    cartItems: 1,\n    cartLink: (chunks: React.ReactNode) => chunks,\n  }) as string;\n\n  await expect(page.getByText(addToCartSuccessMessage)).toBeVisible();\n  await page.goto('/cart');\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByRole('heading', { name: t('Cart.title') })).toBeVisible();\n\n  await page\n    .getByLabel(t('Cart.CheckoutSummary.CouponCode.couponCode'))\n    .fill('some-invalid-coupon-code');\n\n  await page.getByRole('button', { name: t('Cart.CheckoutSummary.CouponCode.apply') }).click();\n  await page.waitForLoadState('networkidle');\n\n  try {\n    await expect(\n      page.getByText(t('Cart.CheckoutSummary.CouponCode.invalidCouponCode')),\n    ).toBeVisible();\n  } catch {\n    // TODO: Remove try/catch when root cause of next state issue is found/resolved [CATALYST-1685]\n    await page.reload();\n    await expect(\n      page.getByText(t('Cart.CheckoutSummary.CouponCode.invalidCouponCode')),\n    ).toBeVisible();\n  }\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/facets.spec.ts",
    "content": "import { expect, Page, test } from '~/tests/fixtures';\n\nconst SHOP_ALL_URL = '/shop-all/';\n\nconst PRODUCT_LE_PARFAIT_JAR = '[Sample] 1 L Le Parfait Jar';\nconst PRODUCT_DUSTPAN_BRUSH = '[Sample] Dustpan & Brush';\nconst PRODUCT_UTILITY_CADDY = '[Sample] Utility Caddy';\n\nasync function expandFilterIfNeeded(page: Page, filterLabel: string) {\n  const filterButton = page\n    .getByRole('heading', { name: filterLabel, level: 3 })\n    .getByRole('button', { name: filterLabel });\n\n  const isExpanded = await filterButton.getAttribute('aria-expanded');\n\n  if (isExpanded === 'false') {\n    await filterButton.click();\n  }\n}\n\nasync function clickSpecificFilterOption(page: Page, filterName: string, optionName: string) {\n  const filterButton = page\n    .getByRole('region', { name: filterName })\n    .getByRole('button', { name: optionName, disabled: false });\n\n  await filterButton.click();\n}\n\ntest('Blue color filter shows expected product on shop-all page', async ({ page }) => {\n  await page.goto(SHOP_ALL_URL);\n  await expect(page.getByRole('heading', { name: 'Shop All 13' })).toBeVisible();\n\n  await expandFilterIfNeeded(page, 'Color');\n  await clickSpecificFilterOption(page, 'Color', 'Blue');\n\n  await expect(page).toHaveURL((url) => url.searchParams.get('attr_Color') === 'Blue');\n  await expect(page.getByRole('link', { name: PRODUCT_LE_PARFAIT_JAR })).toBeVisible();\n});\n\ntest('Brand filter shows correct products', async ({ page }) => {\n  await page.goto(SHOP_ALL_URL);\n  await expect(page.getByRole('heading', { name: 'Shop All 13' })).toBeVisible();\n\n  await expandFilterIfNeeded(page, 'Brand');\n  await clickSpecificFilterOption(page, 'Brand', 'OFS');\n\n  await expect(page).toHaveURL((url) => url.searchParams.has('brand'));\n  await expect(page.getByRole('heading', { name: 'Shop All 5' })).toBeVisible();\n  await expect(page.getByRole('link', { name: PRODUCT_DUSTPAN_BRUSH })).toBeVisible();\n  await expect(page.getByRole('link', { name: PRODUCT_UTILITY_CADDY })).toBeVisible();\n  await expect(page.getByRole('link', { name: PRODUCT_LE_PARFAIT_JAR })).toBeVisible();\n});\n\ntest('Multiple filters work together (Color + Brand)', async ({ page }) => {\n  await page.goto(SHOP_ALL_URL);\n  await expect(page.getByRole('heading', { name: 'Shop All 13' })).toBeVisible();\n\n  await expandFilterIfNeeded(page, 'Color');\n  await clickSpecificFilterOption(page, 'Color', 'Blue');\n\n  await expect(page).toHaveURL((url) => url.searchParams.get('attr_Color') === 'Blue');\n\n  await expandFilterIfNeeded(page, 'Brand');\n  await clickSpecificFilterOption(page, 'Brand', 'OFS');\n\n  await expect(page).toHaveURL((url) => {\n    const params = url.searchParams;\n\n    return params.get('attr_Color') === 'Blue' && params.has('brand');\n  });\n  await expect(page.getByRole('link', { name: PRODUCT_LE_PARFAIT_JAR })).toBeVisible();\n});\n\ntest('Removing filter restores product list', async ({ page }) => {\n  await page.goto(SHOP_ALL_URL);\n  await expect(page.getByRole('heading', { name: 'Shop All 13' })).toBeVisible();\n\n  await expandFilterIfNeeded(page, 'Brand');\n  await clickSpecificFilterOption(page, 'Brand', 'OFS');\n\n  await expect(page).toHaveURL((url) => url.searchParams.has('brand'));\n  await expect(page.getByRole('heading', { name: 'Shop All 5' })).toBeVisible();\n\n  await page.getByRole('button', { name: 'Reset filters' }).click();\n\n  await expect(page).toHaveURL((url) => !url.searchParams.has('brand'));\n  await expect(page.getByRole('heading', { name: 'Shop All 13' })).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/home.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\n\ntest('Homepage displays featured products section with title, description, and CTA', async ({\n  page,\n}) => {\n  const t = await getTranslations('Home');\n\n  await page.goto('/');\n\n  await expect(page.getByRole('heading', { name: t('FeaturedProducts.title') })).toBeVisible();\n  await expect(page.getByText(t('FeaturedProducts.description'))).toBeVisible();\n  await expect(page.getByRole('link', { name: t('FeaturedProducts.cta') })).toBeVisible();\n});\n\ntest('Homepage displays newest products carousel with title, description, and CTA', async ({\n  page,\n}) => {\n  const t = await getTranslations('Home');\n\n  await page.goto('/');\n\n  await expect(page.getByRole('heading', { name: t('NewestProducts.title') })).toBeVisible();\n  await expect(page.getByText(t('NewestProducts.description'))).toBeVisible();\n  await expect(page.getByRole('link', { name: t('NewestProducts.cta') })).toBeVisible();\n});\n\ntest('Featured products CTA link navigates to shop-all page', async ({ page }) => {\n  const t = await getTranslations('Home');\n\n  await page.goto('/');\n\n  const ctaLink = page.getByRole('link', { name: t('FeaturedProducts.cta') });\n\n  await expect(ctaLink).toBeVisible();\n  await expect(ctaLink).toHaveAttribute('href', '/shop-all/');\n});\n\ntest('Newest products CTA link navigates to shop-all page with sort parameter', async ({\n  page,\n}) => {\n  const t = await getTranslations('Home');\n\n  await page.goto('/');\n\n  const ctaLink = page.getByRole('link', { name: t('NewestProducts.cta') });\n\n  await expect(ctaLink).toBeVisible();\n  await expect(ctaLink).toHaveAttribute('href', '/shop-all/?sort=newest');\n});\n\ntest('Homepage displays products when available', async ({ page }) => {\n  await page.goto('/');\n  await page.waitForLoadState('networkidle');\n\n  // Check if any product links are visible (from either featured or newest sections)\n  const productLinks = page.locator('a[href*=\"/product/\"]');\n  const count = await productLinks.count();\n\n  if (count > 0) {\n    await expect(productLinks.first()).toBeVisible();\n  }\n});\n\ntest('Homepage displays newsletter settings when enabled', async ({ page }) => {\n  const t = await getTranslations('Components.Subscribe');\n\n  await page.goto('/');\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByRole('heading', { name: t('title') })).toBeVisible();\n  await expect(page.getByText(t('description'))).toBeVisible();\n  await expect(page.getByPlaceholder(t('placeholder'))).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/not-found.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\n\ntest('Displays title, subtitle, and search button', async ({ page }) => {\n  const t = await getTranslations();\n\n  await page.goto('/unknown-url');\n\n  await expect(page.getByRole('heading', { name: t('NotFound.title') })).toBeVisible();\n  await expect(page.getByText(t('NotFound.subtitle'))).toBeVisible();\n  await expect(\n    page.getByRole('button', { name: t('Components.Header.Icons.search') }),\n  ).toBeVisible();\n});\n\ntest('Clicking the search button opens the search bar', async ({ page }) => {\n  const t = await getTranslations('Components.Header');\n\n  await page.goto('/unknown-url');\n\n  await page.getByRole('button', { name: t('Icons.search') }).click();\n  await expect(page.getByRole('textbox', { name: t('Search.inputPlaceholder') })).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/product.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { expect, test } from '~/tests/fixtures';\nimport { getFormatter } from '~/tests/lib/formatter';\nimport { getTranslations } from '~/tests/lib/i18n';\nimport { TAGS } from '~/tests/tags';\n\ntest('Displays a simple product and can add it to the cart', async ({\n  page,\n  catalog,\n  currency,\n}) => {\n  const t = await getTranslations('Product.ProductDetails');\n  const format = getFormatter();\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto(product.path);\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByRole('heading', { name: product.name })).toBeVisible();\n  await expect(\n    page.getByText(\n      format.number(product.price, {\n        style: 'currency',\n        currency: await currency.getDefaultCurrency(),\n      }),\n    ),\n  ).toBeVisible();\n\n  await expect(page.getByRole('button', { name: t('Submit.addToCart') })).toBeVisible();\n\n  await page.getByRole('button', { name: t('Submit.addToCart') }).click();\n});\n\ntest('Displays out of stock product correctly', async ({ page, catalog }) => {\n  const t = await getTranslations('Product.ProductDetails');\n\n  const product = await catalog.createSimpleProduct({\n    inventoryTracking: 'product',\n    inventoryLevel: 0,\n  });\n\n  await page.goto(product.path);\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByRole('heading', { name: product.name })).toBeVisible();\n  await expect(page.getByRole('button', { name: t('Submit.outOfStock') })).toBeVisible();\n});\n\ntest('Displays out of stock product correctly when out of stock message is enabled', async ({\n  page,\n  catalog,\n  settings,\n}) => {\n  // Test is flaky due to cache. Set an increased timeout for the entire test.\n  test.setTimeout(90000);\n\n  const t = await getTranslations('Product.ProductDetails');\n\n  await settings.setInventorySettings({\n    showOutOfStockMessage: true,\n    defaultOutOfStockMessage: 'Currently out of stock',\n  });\n\n  const product = await catalog.createSimpleProduct({\n    inventoryTracking: 'product',\n    inventoryLevel: 0,\n  });\n\n  await page.goto(product.path);\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByRole('heading', { name: product.name })).toBeVisible();\n  await expect(page.getByRole('button', { name: t('Submit.outOfStock') })).toBeVisible();\n\n  // Test is flakey due to settings cache. Retry assertions several times until it passes.\n  await expect(async () => {\n    try {\n      await expect(page.getByText('Currently out of stock')).toBeVisible();\n    } catch {\n      await page.reload();\n      await expect(page.getByText('Currently out of stock')).toBeVisible();\n    }\n  }).toPass({ timeout: 90000, intervals: [2000] });\n});\n\ntest('Displays current stock message when stock level message is enabled', async ({\n  page,\n  catalog,\n  settings,\n}) => {\n  // Test is flaky due to cache. Set an increased timeout for the entire test.\n  test.setTimeout(90000);\n\n  const t = await getTranslations('Product.ProductDetails');\n\n  await settings.setInventorySettings({\n    stockLevelDisplay: 'show',\n  });\n\n  const product = await catalog.createSimpleProduct({\n    inventoryTracking: 'product',\n    inventoryLevel: 10,\n  });\n\n  await page.goto(product.path);\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByRole('heading', { name: product.name })).toBeVisible();\n\n  // Test is flakey due to settings cache. Retry assertions several times until it passes.\n  await expect(async () => {\n    try {\n      await expect(page.getByText(t('currentStock', { quantity: 10 }))).toBeVisible();\n    } catch {\n      await page.reload();\n      await expect(page.getByText(t('currentStock', { quantity: 10 }))).toBeVisible();\n    }\n  }).toPass({ timeout: 90000, intervals: [2000] });\n});\n\ntest('Displays product price correctly for an alternate currency', async ({\n  page,\n  catalog,\n  currency,\n}) => {\n  const format = getFormatter();\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n  const defaultCurrency = await currency.getDefaultCurrency();\n  const alternateCurrency = (await currency.getEnabledCurrencies()).find(\n    (c) => c !== defaultCurrency,\n  );\n\n  if (!alternateCurrency) {\n    test.skip(true, 'No alternative currencies found.');\n\n    return;\n  }\n\n  await page.goto(product.path);\n  await page.waitForLoadState('networkidle');\n  await expect(\n    page.getByText(\n      format.number(product.price, {\n        style: 'currency',\n        currency: defaultCurrency,\n      }),\n    ),\n  ).toBeVisible();\n\n  await currency.selectCurrency(alternateCurrency);\n\n  const productPriceConverted = await currency.convertWithExchangeRate(\n    alternateCurrency,\n    product.price,\n  );\n\n  const formatted = format.number(productPriceConverted, {\n    style: 'currency',\n    currency: alternateCurrency,\n  });\n\n  await expect(page.getByText(formatted)).toBeVisible();\n});\n\ntest('Quantity buttons work and adds the correct amount to the cart', async ({ page, catalog }) => {\n  const t = await getTranslations('Product.ProductDetails');\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto(product.path);\n  await page.waitForLoadState('networkidle');\n\n  await page.getByLabel(t('increaseQuantity')).click();\n  await page.getByLabel(t('increaseQuantity')).click();\n  await page.getByLabel(t('increaseQuantity')).click();\n  await expect(page.getByRole('spinbutton', { name: t('quantity') })).toHaveValue('4');\n  await page.getByLabel(t('decreaseQuantity')).click();\n  await expect(page.getByRole('spinbutton', { name: t('quantity') })).toHaveValue('3');\n\n  await page.getByRole('button', { name: t('Submit.addToCart') }).click();\n\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  const addToCartSuccessMessage = t.rich('successMessage', {\n    cartItems: 3,\n    cartLink: (chunks: React.ReactNode) => chunks,\n  }) as string;\n\n  await expect(page.getByText(addToCartSuccessMessage)).toBeVisible();\n});\n\ntest('Quantity input works and adds the correct amount to the cart', async ({ page, catalog }) => {\n  const t = await getTranslations('Product.ProductDetails');\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto(product.path);\n  await page.waitForLoadState('networkidle');\n  await page.getByRole('spinbutton', { name: t('quantity') }).fill('5');\n\n  await page.getByRole('button', { name: t('Submit.addToCart') }).click();\n\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  const addToCartSuccessMessage = t.rich('successMessage', {\n    cartItems: 5,\n    cartLink: (chunks: React.ReactNode) => chunks,\n  }) as string;\n\n  await expect(page.getByText(addToCartSuccessMessage)).toBeVisible();\n});\n\ntest('Displays the wishlist button with default menu items', async ({ page, catalog }) => {\n  const t = await getTranslations();\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto(product.path);\n  await page.waitForLoadState('networkidle');\n\n  await page.getByRole('button', { name: t('Wishlist.Button.label') }).click();\n\n  await expect(\n    page.getByRole('menuitem', { name: t('Wishlist.Button.addToNewWishlist') }),\n  ).toBeVisible();\n\n  await expect(\n    page.getByRole('menuitem', { name: t('Wishlist.Button.defaultWishlistName') }),\n  ).toBeVisible();\n});\n\ntest(\n  'Adding to a new wishlist as a guest user redirects to login, and redirects back to PDP to finish adding to the new wishlist',\n  { tag: [TAGS.writesData] },\n  async ({ page, catalog, customer }) => {\n    const t = await getTranslations();\n    const { id: customerId, email, password } = await customer.getOrCreateTestCustomer();\n\n    if (!password) {\n      test.skip(true, 'No password set for the customer, skipping test.');\n\n      return;\n    }\n\n    const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n    await page.goto(product.path);\n    await page.waitForLoadState('networkidle');\n\n    await page.getByRole('button', { name: t('Wishlist.Button.label') }).click();\n    await page.getByRole('menuitem', { name: t('Wishlist.Button.addToNewWishlist') }).click();\n    await page.waitForLoadState('networkidle');\n\n    await page.getByLabel(t('Auth.Login.email')).fill(email);\n    await page.getByLabel(t('Auth.Login.password')).fill(password);\n    await page.getByRole('button', { name: t('Auth.Login.cta') }).click();\n    await page.waitForURL(`${product.path}?action=addToNewWishlist`);\n\n    const wishlistName = `Wishlist ${faker.string.alpha(10)}`;\n\n    await page.getByLabel(t('Wishlist.Form.nameLabel')).fill(wishlistName);\n    await page.getByRole('button', { name: t('Wishlist.Modal.create') }).click();\n    await page.waitForLoadState('networkidle');\n\n    await expect(page.getByText(t('Wishlist.Button.addSuccessMessage'))).toBeVisible();\n\n    await customer.deleteAllWishlists(customerId);\n  },\n);\n\ntest(\n  'Wishlist button adds to the default wishlist if a customer does not have any wishlists',\n  { tag: [TAGS.writesData] },\n  async ({ page, customer, catalog }) => {\n    const t = await getTranslations();\n    const { id: customerId } = await customer.login();\n\n    // Ensure the customer has no wishlists before starting the test\n    await customer.deleteAllWishlists(customerId);\n\n    const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n    await page.goto(product.path);\n    await page.waitForLoadState('networkidle');\n\n    await page.getByRole('button', { name: t('Wishlist.Button.label') }).click();\n    await page.getByRole('menuitem', { name: t('Wishlist.Button.defaultWishlistName') }).click();\n\n    await page.waitForLoadState('networkidle');\n\n    await expect(page.getByText(t('Wishlist.Button.addSuccessMessage'))).toBeVisible();\n  },\n);\n\ntest(\n  'Wishlist button adds the product to an existing wishlist',\n  { tag: [TAGS.writesData] },\n  async ({ page, customer, catalog }) => {\n    const t = await getTranslations();\n    const { id: customerId } = await customer.login();\n    const { name } = await customer.createWishlist({ customerId });\n\n    const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n    await page.goto(product.path);\n    await page.waitForLoadState('networkidle');\n\n    await page.getByRole('button', { name: t('Wishlist.Button.label') }).click();\n    await page.getByRole('menuitem', { name }).click();\n    await page.waitForLoadState('networkidle');\n\n    await expect(page.getByText(t('Wishlist.Button.addSuccessMessage'))).toBeVisible();\n  },\n);\n"
  },
  {
    "path": "core/tests/ui/e2e/proxy/redirects.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { testEnv } from '~/tests/environment';\nimport { expect, test } from '~/tests/fixtures';\nimport { TAGS } from '~/tests/tags';\n\ntest('Proxy follows a dynamic 301 redirect correctly', async ({ page, catalog, redirects }) => {\n  const redirectFrom = `/test-dynamic-redirect-${faker.string.alpha(6)}`;\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await redirects.upsertRedirect({\n    fromPath: redirectFrom,\n    to: {\n      type: 'product',\n      entityId: product.id,\n    },\n  });\n\n  await page.goto(redirectFrom);\n  await expect(page).toHaveURL(product.customUrl.url);\n});\n\ntest('Proxy follows a manual redirect correctly', async ({ page, redirects }) => {\n  const fromPath = `/test-manual-redirect-${faker.string.alpha(6)}`;\n  const toPath = `/test-manual-destination-${faker.string.alpha(6)}`;\n\n  await redirects.upsertRedirect({\n    fromPath,\n    to: {\n      type: 'url',\n      url: toPath,\n    },\n  });\n\n  // First, navigate to the original fromPath and expect to be redirected to toPath\n  await page.goto(fromPath);\n  await expect(page).toHaveURL(toPath);\n});\n\ntest('Proxy follows redirects in respect to capitalization', async ({ page, redirects }) => {\n  const fromPath = '/path-test';\n  const toPath = '/path-TEST';\n\n  await redirects.upsertRedirect({\n    fromPath,\n    to: {\n      type: 'url',\n      url: toPath,\n    },\n  });\n\n  await page.goto(fromPath);\n  await expect(page).toHaveURL(toPath);\n});\n\ntest.describe(\n  'Trailing slash redirect loop regression tests',\n  { tag: TAGS.noTrailingSlash },\n  () => {\n    test.skip(() => testEnv.TRAILING_SLASH);\n\n    test('Dynamic redirect to a product with a trailing slash in the url does not cause a redirect loop when TRAILING_SLASH=false', async ({\n      page,\n      catalog,\n      redirects,\n    }) => {\n      const productUrlWithoutTrailingSlash = `/dynamic-redirect-product-${faker.string.alpha(6)}`;\n      const product = await catalog.createSimpleProduct({\n        customUrl: {\n          url: `${productUrlWithoutTrailingSlash}/`,\n        },\n      });\n\n      await redirects.upsertRedirect({\n        fromPath: productUrlWithoutTrailingSlash,\n        to: {\n          type: 'product',\n          entityId: product.id,\n        },\n      });\n\n      await page.goto(productUrlWithoutTrailingSlash);\n      await expect(page).toHaveURL(productUrlWithoutTrailingSlash);\n    });\n\n    test('Manual redirect from a non-trailing-slash path to a trailing-slash path does not cause a redirect loop when TRAILING_SLASH=false', async ({\n      page,\n      redirects,\n    }) => {\n      const pathNoSlash = `/test-manual-redirect-${faker.string.alpha(6)}`;\n\n      await redirects.upsertRedirect({\n        fromPath: pathNoSlash,\n        to: {\n          type: 'url',\n          url: `${pathNoSlash}/`,\n        },\n      });\n\n      await page.goto(pathNoSlash);\n      await expect(page).toHaveURL(pathNoSlash);\n    });\n  },\n);\n"
  },
  {
    "path": "core/tests/ui/e2e/reviews.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\nimport { TAGS } from '~/tests/tags';\n\ntest(\n  'Submit a review as a non-logged in customer',\n  { tag: [TAGS.writesData] },\n  async ({ page, catalog }) => {\n    const t = await getTranslations('Product.Reviews.Form');\n    const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n    await page.goto(product.path);\n    await page.waitForLoadState('networkidle');\n\n    await page.getByRole('button', { name: t('button') }).click();\n\n    const modal = page.getByRole('dialog');\n\n    await expect(modal.getByRole('heading', { name: t('title') })).toBeVisible();\n\n    const rating = faker.number.int({ min: 1, max: 5 });\n    const ratingLabel = rating === 1 ? '1 star' : `${rating} stars`;\n\n    await modal.getByLabel(ratingLabel).click();\n\n    const reviewTitle = faker.lorem.sentence();\n\n    await modal.getByLabel(t('titleLabel')).fill(reviewTitle);\n\n    const reviewText = faker.lorem.paragraph();\n\n    await modal.getByLabel(t('reviewLabel')).fill(reviewText);\n\n    const customerName = faker.person.fullName();\n\n    await modal.getByLabel(t('nameLabel')).fill(customerName);\n\n    const customerEmail = faker.internet.email();\n\n    await modal.getByLabel(t('emailLabel')).fill(customerEmail);\n\n    await expect(modal.getByLabel(t('nameLabel'))).toBeEnabled();\n    await expect(modal.getByLabel(t('emailLabel'))).toBeEnabled();\n\n    // Click reCAPTCHA if enabled (uses test key — no challenge, always passes)\n    const recaptchaFrame = page.frameLocator('iframe[title=\"reCAPTCHA\"]');\n    const recaptchaCheckbox = recaptchaFrame.locator('.recaptcha-checkbox-border');\n\n    if (await recaptchaCheckbox.isVisible()) {\n      await recaptchaCheckbox.click();\n      await recaptchaFrame.locator('.recaptcha-checkbox-checked').waitFor();\n    }\n\n    await modal.getByRole('button', { name: t('submit') }).click();\n    await page.waitForLoadState('networkidle');\n\n    await expect(page.getByText(t('successMessage'))).toBeVisible();\n    await expect(modal).toBeHidden();\n    await expect(page.getByRole('button', { name: t('button') })).toBeVisible();\n  },\n);\n\ntest('Shows validation errors when submitting review form with empty inputs', async ({\n  page,\n  catalog,\n}) => {\n  const t = await getTranslations('Product.Reviews.Form');\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto(product.path);\n  await page.waitForLoadState('networkidle');\n\n  await page.getByRole('button', { name: t('button') }).click();\n\n  const modal = page.getByRole('dialog');\n\n  await expect(modal.getByRole('heading', { name: t('title') })).toBeVisible();\n\n  await modal.getByRole('button', { name: t('submit') }).click();\n  await page.waitForLoadState('networkidle');\n\n  await expect(modal).toBeVisible();\n\n  const ratingField = modal.getByLabel(t('ratingLabel'));\n  const titleField = modal.getByLabel(t('titleLabel'));\n  const reviewField = modal.getByLabel(t('reviewLabel'));\n  const nameField = modal.getByLabel(t('nameLabel'));\n  const emailField = modal.getByLabel(t('emailLabel'));\n\n  await expect(ratingField).toBeVisible();\n  await expect(titleField).toBeVisible();\n  await expect(reviewField).toBeVisible();\n  await expect(nameField).toBeVisible();\n  await expect(emailField).toBeVisible();\n\n  const errorMessages = modal.locator('text=/required|invalid/i');\n\n  await expect(errorMessages.first()).toBeVisible();\n  await expect(errorMessages.nth(1)).toBeVisible();\n  await expect(errorMessages.nth(2)).toBeVisible();\n  await expect(errorMessages.nth(3)).toBeVisible();\n  await expect(errorMessages.nth(4)).toBeVisible();\n});\n\ntest('Review submission fails if reCAPTCHA is not completed', async ({ page, catalog }) => {\n  const t = await getTranslations('Product.Reviews.Form');\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto(product.path);\n  await page.waitForLoadState('networkidle');\n\n  await page.getByRole('button', { name: t('button') }).click();\n\n  const modal = page.getByRole('dialog');\n\n  await expect(modal.getByRole('heading', { name: t('title') })).toBeVisible();\n\n  const recaptchaFrame = page.frameLocator('iframe[title=\"reCAPTCHA\"]');\n  const recaptchaCheckbox = recaptchaFrame.locator('.recaptcha-checkbox-border');\n\n  try {\n    await recaptchaCheckbox.waitFor({ state: 'visible', timeout: 5000 });\n  } catch {\n    test.skip();\n  }\n\n  // Fill all required fields but intentionally skip clicking reCAPTCHA\n  const rating = faker.number.int({ min: 1, max: 5 });\n  const ratingLabel = rating === 1 ? '1 star' : `${rating} stars`;\n\n  await modal.getByLabel(ratingLabel).click();\n  await modal.getByLabel(t('titleLabel')).fill(faker.lorem.sentence());\n  await modal.getByLabel(t('reviewLabel')).fill(faker.lorem.paragraph());\n  await modal.getByLabel(t('nameLabel')).fill(faker.person.fullName());\n  await modal.getByLabel(t('emailLabel')).fill(faker.internet.email());\n\n  await modal.getByRole('button', { name: t('submit') }).click();\n  await page.waitForLoadState('networkidle');\n\n  await expect(modal).toBeVisible();\n  await expect(page.getByText(t('recaptchaRequired'))).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/search.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\n\ntest('Typing in the search bar displays quick search results', async ({ page, catalog }) => {\n  const t = await getTranslations('Components.Header');\n  const { name } = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto('/');\n  await page.getByRole('button', { name: t('Icons.search') }).click();\n  await page.getByPlaceholder(t('Search.inputPlaceholder')).fill(name);\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByRole('heading', { name: t('Search.categories') })).toBeVisible();\n  await expect(page.getByRole('heading', { name: t('Search.brands') })).toBeVisible();\n  await expect(page.getByRole('heading', { name: t('Search.products') }).first()).toBeVisible();\n\n  const searchResultLocator = page.getByRole('region', { name: t('Search.products') });\n\n  await expect(searchResultLocator.getByRole('link', { name })).toBeVisible();\n});\n\ntest('Typing in the search bar and pressing Enter goes to the Search Results page', async ({\n  page,\n  catalog,\n}) => {\n  const t = await getTranslations();\n  const { name } = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto('/');\n  await page.getByRole('button', { name: t('Components.Header.Icons.search') }).click();\n\n  const searchInput = page.getByPlaceholder(t('Components.Header.Search.inputPlaceholder'));\n\n  await searchInput.fill(name);\n  await searchInput.press('Enter');\n  await page.waitForLoadState('networkidle');\n\n  await expect(\n    page.getByRole('heading', { name: t('Faceted.Search.searchResults') }),\n  ).toBeVisible();\n  await expect(page.getByRole('link', { name })).toBeVisible();\n});\n\ntest('Searching by SKU returns the product in the search results', async ({ page, catalog }) => {\n  const t = await getTranslations('Components.Header');\n  const { name, sku } = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto('/');\n  await page.getByRole('button', { name: t('Icons.search') }).click();\n  await page.getByPlaceholder(t('Search.inputPlaceholder')).fill(sku);\n  await page.waitForLoadState('networkidle');\n\n  const searchResultLocator = page.getByRole('region', { name: t('Search.products') });\n\n  await expect(searchResultLocator.getByRole('link', { name })).toBeVisible();\n});\n\ntest('Searching for non-existent product displays no results', async ({ page }) => {\n  const t = await getTranslations();\n  const randomSearchTerm = faker.string.alphanumeric(10);\n\n  await page.goto('/');\n  await page.getByRole('button', { name: t('Components.Header.Icons.search') }).click();\n  await page\n    .getByPlaceholder(t('Components.Header.Search.inputPlaceholder'))\n    .fill(randomSearchTerm);\n\n  await page.waitForLoadState('networkidle');\n\n  await expect(\n    page.getByText(t('Components.Header.Search.noSearchResultsTitle', { term: randomSearchTerm })),\n  ).toBeVisible();\n\n  await expect(page.getByText(t('Components.Header.Search.noSearchResultsSubtitle'))).toBeVisible();\n});\n\ntest('Searching for non-existent product displays no results on the Search Results page', async ({\n  page,\n}) => {\n  const t = await getTranslations();\n\n  await page.goto('/');\n  await page.getByRole('button', { name: t('Components.Header.Icons.search') }).click();\n\n  const searchInput = page.getByPlaceholder(t('Components.Header.Search.inputPlaceholder'));\n  const randomSearchTerm = faker.string.alphanumeric(10);\n\n  await searchInput.fill(randomSearchTerm);\n  await searchInput.press('Enter');\n  await page.waitForLoadState('networkidle');\n\n  await expect(\n    page.getByText(t('Faceted.Search.Empty.title', { term: randomSearchTerm })),\n  ).toBeVisible();\n\n  await expect(page.getByText(t('Faceted.Search.Empty.subtitle'))).toBeVisible();\n});\n\ntest('Searching for a category displays in the search results', async ({ page, catalog }) => {\n  const t = await getTranslations('Components.Header');\n  const categories = await catalog.getCategories();\n  const category = categories[0];\n\n  if (!category) {\n    test.skip(true, 'No categories found in the catalog');\n\n    return;\n  }\n\n  const { name } = category;\n\n  await page.goto('/');\n  await page.getByRole('button', { name: t('Icons.search') }).click();\n\n  await page.getByPlaceholder(t('Search.inputPlaceholder')).fill(name);\n  await page.waitForLoadState('networkidle');\n\n  const searchResultLocator = page.getByRole('region', { name: t('Search.categories') });\n\n  await expect(searchResultLocator.getByRole('link', { name })).toBeVisible();\n});\n\ntest('Searching for a brand displays in the search results', async ({ page, catalog }) => {\n  const t = await getTranslations('Components.Header');\n  const brands = await catalog.getBrands();\n  const brand = brands[0];\n\n  if (!brand) {\n    test.skip(true, 'No brands found in the catalog');\n\n    return;\n  }\n\n  const { name } = brand;\n\n  await page.goto('/');\n  await page.getByRole('button', { name: t('Icons.search') }).click();\n\n  await page.getByPlaceholder(t('Search.inputPlaceholder')).fill(name);\n  await page.waitForLoadState('networkidle');\n\n  const searchResultLocator = page.getByRole('region', { name: t('Search.brands') });\n\n  await expect(searchResultLocator.getByRole('link', { name })).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/shipping.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { expect, Page, test } from '~/tests/fixtures';\nimport { CatalogFixture } from '~/tests/fixtures/catalog';\nimport { getTranslations } from '~/tests/lib/i18n';\n\nasync function addProductAndGoToCart(page: Page, catalog: CatalogFixture) {\n  const t = await getTranslations();\n  const product = await catalog.getDefaultOrCreateSimpleProduct();\n\n  await page.goto(product.path);\n  await page.getByRole('button', { name: t('Product.ProductDetails.Submit.addToCart') }).click();\n\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  const addToCartSuccessMessage = t.rich('Product.ProductDetails.successMessage', {\n    cartItems: 1,\n    cartLink: (chunks: React.ReactNode) => chunks,\n  }) as string;\n\n  await expect(page.getByText(addToCartSuccessMessage)).toBeVisible();\n\n  await page.goto('/cart');\n  await expect(page.getByRole('heading', { name: t('Cart.title') })).toBeVisible();\n}\n\nasync function fillOutShippingForm(page: Page) {\n  const t = await getTranslations('Cart.CheckoutSummary.Shipping');\n\n  await page.getByLabel(t('country')).click();\n  await page.getByRole('option').first().click();\n  await page.getByLabel(t('city')).fill(faker.location.city());\n  await page.getByLabel(t('state')).click();\n\n  const states = await page.getByRole('option').allTextContents();\n\n  if (states.length >= 1) {\n    // Click a random state\n    const randomIndex = Math.floor(Math.random() * states.length);\n\n    await page.getByRole('option').nth(randomIndex).click();\n  } else {\n    await page.keyboard.press('Escape');\n  }\n\n  await page.getByLabel(t('postalCode')).click();\n  await page.getByLabel(t('postalCode')).fill(faker.location.zipCode('#####'));\n}\n\nasync function selectRandomShippingOption(page: Page): Promise<string> {\n  const shippingOptions = await page.getByRole('radio').all();\n\n  let selectedOption = '';\n\n  if (shippingOptions.length >= 1) {\n    // Click a random state\n    const randomIndex = Math.floor(Math.random() * shippingOptions.length);\n    const shippingOption = shippingOptions[randomIndex];\n\n    selectedOption =\n      (await shippingOption?.locator('~ label > span[id*=\"label\"]').innerText()) ?? '';\n\n    await shippingOption?.click();\n  }\n\n  return selectedOption;\n}\n\ntest('Add shipping estimates', async ({ page, catalog }) => {\n  const t = await getTranslations('Cart.CheckoutSummary.Shipping');\n\n  await addProductAndGoToCart(page, catalog);\n\n  await page.waitForLoadState('networkidle');\n\n  await page.getByRole('button', { name: t('add'), exact: true }).click();\n\n  await fillOutShippingForm(page);\n\n  await page.getByRole('button', { name: t('viewShippingOptions') }).click();\n\n  try {\n    await expect(page.getByLabel(t('shippingOptions'))).toBeVisible();\n  } catch {\n    // TODO: Remove try/catch when root cause of next state issue is found/resolved [CATALYST-1685]\n    await page.reload();\n    await expect(page.getByLabel(t('shippingOptions'))).toBeVisible();\n  }\n\n  const selectedOption = await selectRandomShippingOption(page);\n\n  await page.getByRole('button', { name: t('addShipping') }).click();\n  await page.waitForLoadState('networkidle');\n\n  try {\n    await expect(page.getByText(`${selectedOption}${t('change')}`)).toBeVisible();\n  } catch {\n    await page.reload();\n    // TODO: Remove try/catch when root cause of next state issue is found/resolved [CATALYST-1685]\n    await expect(page.getByText(`${selectedOption}${t('change')}`)).toBeVisible();\n  }\n});\n\ntest('Update shipping estimates', async ({ page, catalog }) => {\n  const t = await getTranslations('Cart.CheckoutSummary.Shipping');\n\n  await addProductAndGoToCart(page, catalog);\n\n  await page.waitForLoadState('networkidle');\n\n  await page.getByRole('button', { name: t('add'), exact: true }).click();\n\n  await fillOutShippingForm(page);\n\n  await page.getByRole('button', { name: t('viewShippingOptions') }).click();\n\n  try {\n    await expect(page.getByLabel(t('shippingOptions'))).toBeVisible();\n  } catch {\n    // TODO: Remove try/catch when root cause of next state issue is found/resolved [CATALYST-1685]\n    await page.reload();\n    await page.waitForLoadState('networkidle');\n    await expect(page.getByLabel(t('shippingOptions'))).toBeVisible();\n  }\n\n  let selectedOption = await selectRandomShippingOption(page);\n\n  await page.getByRole('button', { name: t('addShipping') }).click();\n  await page.waitForLoadState('networkidle');\n\n  try {\n    await expect(page.getByText(t('change'))).toBeVisible();\n  } catch {\n    // TODO: Remove try/catch when root cause of next state issue is found/resolved [CATALYST-1685]\n    await page.reload();\n    await page.waitForLoadState('networkidle');\n    await expect(page.getByText(t('change'))).toBeVisible();\n  }\n\n  await page.getByText(t('change')).click();\n  await page.getByRole('button', { name: t('editAddress') }).click();\n\n  await fillOutShippingForm(page);\n\n  await page.getByRole('button', { name: t('updatedShippingOptions') }).click();\n\n  try {\n    await expect(page.getByRole('button', { name: t('updatedShippingOptions') })).toBeHidden();\n  } catch {\n    // TODO: Remove try/catch when root cause of next state issue is found/resolved [CATALYST-1685]\n    await page.reload();\n    await page.waitForLoadState('networkidle');\n    await expect(page.getByRole('button', { name: t('updatedShippingOptions') })).toBeHidden();\n  }\n\n  if (await page.getByLabel(t('shippingOptions')).isVisible()) {\n    selectedOption = await selectRandomShippingOption(page);\n    await page.getByRole('button', { name: t('updateShipping') }).click();\n    await page.waitForLoadState('networkidle');\n  }\n\n  await expect(page.getByText(`${selectedOption}${t('change')}`)).toBeVisible();\n});\n\ntest('Updating cart quantity with a shipping estimate opens the shipping options for a new quote', async ({\n  page,\n  catalog,\n}) => {\n  const t = await getTranslations('Cart');\n\n  await addProductAndGoToCart(page, catalog);\n\n  await page.waitForLoadState('networkidle');\n\n  await page.getByRole('button', { name: t('CheckoutSummary.Shipping.add'), exact: true }).click();\n\n  await fillOutShippingForm(page);\n\n  await page\n    .getByRole('button', { name: t('CheckoutSummary.Shipping.viewShippingOptions') })\n    .click();\n\n  try {\n    await expect(page.getByLabel(t('CheckoutSummary.Shipping.shippingOptions'))).toBeVisible();\n  } catch {\n    // TODO: Remove try/catch when root cause of next state issue is found/resolved [CATALYST-1685]\n    await page.reload();\n    await page.waitForLoadState('networkidle');\n    await expect(page.getByLabel(t('CheckoutSummary.Shipping.shippingOptions'))).toBeVisible();\n  }\n\n  let selectedOption = await selectRandomShippingOption(page);\n\n  await page.getByRole('button', { name: t('CheckoutSummary.Shipping.addShipping') }).click();\n  await page.waitForLoadState('networkidle');\n\n  try {\n    await expect(\n      page.getByText(`${selectedOption}${t('CheckoutSummary.Shipping.change')}`),\n    ).toBeVisible();\n  } catch {\n    // TODO: Remove try/catch when root cause of next state issue is found/resolved [CATALYST-1685]\n    await page.reload();\n    await expect(\n      page.getByText(`${selectedOption}${t('CheckoutSummary.Shipping.change')}`),\n    ).toBeVisible();\n  }\n\n  await page.getByLabel(t('increment')).click();\n  await page.waitForLoadState('networkidle');\n\n  try {\n    await expect(page.getByLabel(t('CheckoutSummary.Shipping.shippingOptions'))).toBeVisible();\n  } catch {\n    // TODO: Remove try/catch when root cause of next state issue is found/resolved [CATALYST-1685]\n    await page.reload();\n    await page.waitForLoadState('networkidle');\n    await expect(page.getByLabel(t('CheckoutSummary.Shipping.shippingOptions'))).toBeVisible();\n  }\n\n  selectedOption = await selectRandomShippingOption(page);\n\n  await page.getByRole('button', { name: t('CheckoutSummary.Shipping.addShipping') }).click();\n\n  try {\n    await expect(\n      page.getByText(`${selectedOption}${t('CheckoutSummary.Shipping.change')}`),\n    ).toBeVisible();\n  } catch {\n    // TODO: Remove try/catch when root cause of next state issue is found/resolved [CATALYST-1685]\n    await page.reload();\n  }\n\n  await page.waitForLoadState('networkidle');\n  await expect(\n    page.getByText(`${selectedOption}${t('CheckoutSummary.Shipping.change')}`),\n  ).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/subscribe.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\nimport { TAGS } from '~/tests/tags';\n\ntest(\n  'Successfully subscribes a user to the newsletter',\n  { tag: [TAGS.writesData] },\n  async ({ page, subscribe }) => {\n    const t = await getTranslations('Components.Subscribe');\n\n    await page.goto('/');\n    await page.waitForLoadState('networkidle');\n\n    const email = faker.internet.email();\n\n    const emailInput = page.getByPlaceholder(t('placeholder'));\n\n    await emailInput.fill(email);\n\n    const submitButton = page.locator('input[type=\"email\"]').locator('..').getByRole('button');\n\n    await submitButton.click();\n    await page.waitForLoadState('networkidle');\n\n    await expect(page.getByText(t('subscribedToNewsletter'))).toBeVisible();\n\n    // Track that we attempted to subscribe this email\n    subscribe.trackSubscription(email);\n  },\n);\n\ntest('Shows success message when user tries to subscribe again with the same email', async ({\n  page,\n  subscribe,\n}) => {\n  const t = await getTranslations('Components.Subscribe');\n\n  await page.goto('/');\n  await page.waitForLoadState('networkidle');\n\n  const email = faker.internet.email();\n\n  const emailInput = page.getByPlaceholder(t('placeholder'));\n\n  const submitButton = page.locator('input[type=\"email\"]').locator('..').getByRole('button');\n\n  // Subscribe with the email\n  await emailInput.fill(email);\n  await submitButton.click();\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByText(t('subscribedToNewsletter'))).toBeVisible();\n\n  // Try to subscribe again with the same email\n  await emailInput.fill(email);\n  await submitButton.click();\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByText(t('subscribedToNewsletter'))).toBeVisible();\n\n  // Track that we attempted to subscribe this email\n  subscribe.trackSubscription(email);\n});\n"
  },
  {
    "path": "core/tests/ui/e2e/webpages.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nimport { expect, test } from '~/tests/fixtures';\nimport { getTranslations } from '~/tests/lib/i18n';\n\ntest('Normal web page works and displays the HTML content', async ({ page, webPage }) => {\n  const t = await getTranslations('WebPages.Normal');\n  const { name, path } = await webPage.create({\n    body: `\n      <h1>I am some testing page content</h1>\n      <p>This is a paragraph of text to test the web page rendering.</p>\n      <p>It should be visible when the page is loaded.</p>\n      <p>\n        <a href=\"https://example.com\">Do links work too?</a>\n      </p>\n      <div>Testing div element</div>\n    `,\n  });\n\n  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n  await page.goto(path!);\n  await page.waitForLoadState('networkidle');\n\n  const breadcrumbs = page.getByLabel('breadcrumb');\n\n  await expect(breadcrumbs.getByText(t('home'))).toBeVisible();\n  await expect(breadcrumbs.getByText(name)).toBeVisible();\n\n  await expect(page.getByRole('heading', { name })).toBeVisible();\n  await expect(page.getByText('I am some testing page content')).toBeVisible();\n  await expect(\n    page.getByText('This is a paragraph of text to test the web page rendering.'),\n  ).toBeVisible();\n  await expect(page.getByText('It should be visible when the page is loaded.')).toBeVisible();\n  await expect(page.getByRole('link', { name: 'Do links work too?' })).toHaveAttribute(\n    'href',\n    'https://example.com',\n  );\n  await expect(page.getByText('Testing div element')).toBeVisible();\n});\n\ntest('Nested web pages display the children in the side menu, navigate correctly, and truncate breadcrumbs', async ({\n  page,\n  webPage,\n}) => {\n  const t = await getTranslations('WebPages.Normal');\n  const parent = await webPage.create();\n  const child1 = await webPage.create({ parentId: parent.id });\n  const child2 = await webPage.create({ parentId: parent.id });\n  const nestedChild = await webPage.create({ parentId: child1.id });\n  const nestedChild2 = await webPage.create({ parentId: nestedChild.id });\n\n  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n  await page.goto(parent.path!);\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByRole('heading', { name: parent.name })).toBeVisible();\n  await expect(page.getByRole('link', { name: child1.name })).toBeVisible();\n  await expect(page.getByRole('link', { name: child2.name })).toBeVisible();\n\n  await page.getByRole('link', { name: child1.name }).click();\n  await page.getByRole('link', { name: nestedChild.name }).click();\n  await page.getByRole('link', { name: nestedChild2.name }).click();\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByRole('heading', { name: nestedChild2.name })).toBeVisible();\n\n  const breadcrumbs = page.getByLabel('breadcrumb');\n\n  await expect(breadcrumbs.getByText(t('home'))).toBeVisible();\n  await expect(breadcrumbs.getByText(parent.name)).toBeVisible();\n  await expect(breadcrumbs.getByText('...')).toBeVisible();\n  await expect(breadcrumbs.getByText(nestedChild.name)).toBeVisible();\n  await expect(breadcrumbs.getByText(nestedChild2.name)).toBeVisible();\n});\n\ntest('Contact page works with all fields and submits successfully', async ({ page, webPage }) => {\n  const t = await getTranslations('WebPages.ContactUs');\n  const contactPage = await webPage.create({\n    type: 'contact_form',\n    body: '<p>Reach out to us with any questions!</p>',\n    email: faker.internet.email({ provider: 'catalyst-example.catalyst' }),\n    contactFields: ['fullname', 'phone', 'companyname', 'orderno', 'rma'],\n  });\n\n  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n  await page.goto(contactPage.path!);\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByRole('heading', { name: contactPage.name })).toBeVisible();\n\n  await page.getByLabel(t('Form.email')).fill(faker.internet.email());\n  await page.getByLabel(t('Form.fullName')).fill(faker.person.fullName());\n  await page.getByLabel(t('Form.phone')).fill(faker.phone.number());\n  await page.getByLabel(t('Form.companyName')).fill(faker.company.name());\n  await page.getByLabel(t('Form.orderNo')).fill(faker.string.numeric(10));\n  await page.getByLabel(t('Form.rma')).fill(faker.string.numeric(10));\n  await page.getByLabel(t('Form.comments')).fill(faker.lorem.paragraph());\n\n  // Click reCAPTCHA if enabled (uses test key — no challenge, always passes)\n  const recaptchaFrame = page.frameLocator('iframe[title=\"reCAPTCHA\"]');\n  const recaptchaCheckbox = recaptchaFrame.locator('.recaptcha-checkbox-border');\n\n  if (await recaptchaCheckbox.isVisible()) {\n    await recaptchaCheckbox.click();\n    await recaptchaFrame.locator('.recaptcha-checkbox-checked').waitFor();\n  }\n\n  await page.getByRole('button', { name: t('Form.cta') }).click();\n  await page.waitForLoadState('networkidle');\n  await expect(page.getByText(t('Form.success'))).toBeVisible();\n  await expect(page.getByRole('link', { name: t('Form.successCta') })).toBeVisible();\n});\n\ntest('Contact page fails if reCAPTCHA is not completed', async ({ page, webPage }) => {\n  const t = await getTranslations('WebPages.ContactUs');\n  const contactPage = await webPage.create({\n    type: 'contact_form',\n    body: '<p>Reach out to us with any questions!</p>',\n    email: faker.internet.email({ provider: 'catalyst-example.catalyst' }),\n  });\n\n  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n  await page.goto(contactPage.path!);\n  await page.waitForLoadState('networkidle');\n\n  const recaptchaFrame = page.frameLocator('iframe[title=\"reCAPTCHA\"]');\n  const recaptchaCheckbox = recaptchaFrame.locator('.recaptcha-checkbox-border');\n\n  try {\n    await recaptchaCheckbox.waitFor({ state: 'visible', timeout: 5000 });\n  } catch {\n    test.skip();\n  }\n\n  // Fill required fields but intentionally skip clicking reCAPTCHA\n  await page.getByLabel(t('Form.email')).fill(faker.internet.email());\n  await page.getByLabel(t('Form.comments')).fill(faker.lorem.paragraph());\n\n  await page.getByRole('button', { name: t('Form.cta') }).click();\n  await page.waitForLoadState('networkidle');\n\n  await expect(page.getByText(t('Form.recaptchaRequired'))).toBeVisible();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/accordion.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('accordion expanded', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.SHOP_ALL);\n\n  // Act\n  const accordion = page\n    .locator('div[data-state=\"open\"]')\n    .filter({ has: page.getByRole('button', { name: 'Brand', expanded: true }) });\n\n  // Assert\n  await expect(accordion).toHaveScreenshot();\n});\n\ntest('accordion closed', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.SHOP_ALL);\n\n  // Act\n  await page.getByRole('button', { name: 'Brand', expanded: true }).click();\n\n  const accordion = page\n    .locator('div[data-state=\"closed\"]')\n    .filter({ has: page.getByRole('button', { name: 'Brand', expanded: false }) });\n\n  // Assert\n  await expect(accordion).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/badge.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('badge with icon', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.SAMPLE_ABLE_BREWING_SYSTEM);\n  await page.getByRole('heading', { level: 1, name: '[Sample] Able Brewing System' }).waitFor();\n  await page.getByRole('button', { name: 'Add to Cart' }).click();\n\n  const addToCartNotification = page\n    .getByRole('status')\n    .filter({ hasText: 'Item added to your cart' });\n\n  // Wait for the add to cart notification to appear and disappear\n  await addToCartNotification.waitFor();\n  await addToCartNotification.waitFor({ state: 'detached' });\n\n  // Act\n  const badge = page.getByRole('link', { name: 'Cart Items 1' });\n\n  // Assert\n  await expect(badge).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/blog-post-card.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('blog post card', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.BLOG);\n  await page.getByRole('heading', { name: 'Blog', exact: true }).waitFor();\n\n  // Act\n  const blogPostCard = page.getByRole('listitem').filter({ hasText: 'Your first blog post!' });\n\n  // Assert\n  await expect(blogPostCard).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/breadcrumbs.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('breadcrumbs', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.BATH_LUXURY);\n\n  // Act\n  const breadcrumb = page.getByLabel('Breadcrumb');\n\n  await breadcrumb.waitFor();\n\n  // Assert\n  await expect(breadcrumb).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/button.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Primary button', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.ORBIT_TERRARIUM_LARGE);\n\n  // Act\n  const button = page.getByRole('button', { name: 'Add to cart' });\n\n  await button.waitFor();\n\n  // Assert\n  await expect(button).toHaveScreenshot();\n});\n\ntest('Secondary button', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.SHOP_ALL);\n\n  const button = page.getByRole('button', { name: 'Update price' });\n\n  await button.waitFor();\n\n  // Assert\n  await expect(button).toHaveScreenshot();\n});\n\ntest('As a child', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.SAMPLE_ABLE_BREWING_SYSTEM);\n  await page.getByRole('heading', { level: 1, name: '[Sample] Able Brewing System' }).waitFor();\n\n  // Act\n  await page.getByRole('button', { name: 'Add to Cart' }).first().click();\n  await page.getByRole('button', { name: 'Add to Cart' }).first().isEnabled();\n  await page.getByRole('link', { name: 'Cart Items 1' }).click();\n  await page.getByText('Shipping cost').waitFor();\n\n  // Assert\n  await expect(page.getByRole('button', { name: 'Add' }).first()).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/carousel.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\ntest('Carousel', async ({ page }) => {\n  // Arrange\n  await page.goto('/');\n  await page.waitForLoadState('networkidle');\n\n  // Act\n  const slides = page.getByRole('region', { name: 'Featured products' });\n\n  // Assert\n  await expect(slides).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/checkbox.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Checked checkbox with label', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.SHOP_ALL);\n\n  // Act\n  const checkbox = page.getByLabel('Common Good1 products');\n\n  await checkbox.click();\n\n  // Assert\n  await expect(checkbox).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/counter.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Counter default', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.FOG_LINEN_CHAMBRAY);\n\n  // Act\n  const spinButton = page.getByRole('spinbutton', { name: 'Number' });\n\n  await spinButton.waitFor();\n\n  // Assert\n  await expect(spinButton).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/datepicker.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Date picker', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.FOG_LINEN_CHAMBRAY);\n\n  // Act\n  const datePicker = page.getByPlaceholder('MM/DD/YYYY');\n\n  await datePicker.waitFor();\n\n  // Assert\n  await expect(datePicker).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/footer.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\ntest('Footer', async ({ page }) => {\n  // Arrange\n  await page.goto('/');\n\n  // Act\n  const footer = page.locator('section').filter({ hasText: 'CategoriesShop' });\n\n  await footer.waitFor();\n\n  // Assert\n  await expect(footer).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/form.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Form', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.CONTACT_US);\n\n  // Act\n  const form = page.getByRole('heading', { name: 'Contact Us' });\n\n  await form.waitFor();\n\n  // Assert\n  await expect(form.locator('..').locator('..')).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/gallery.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Gallery image', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.SAMPLE_ABLE_BREWING_SYSTEM);\n\n  // Act\n  const gallery = page.getByRole('figure').locator('img');\n\n  await gallery.waitFor();\n\n  // Assert\n  await expect(gallery).toHaveScreenshot();\n});\n\ntest('Gallery thumbnail image', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.SAMPLE_ABLE_BREWING_SYSTEM);\n\n  // Act\n  const thumbnail = page.getByLabel('Thumbnail navigation');\n\n  await thumbnail.waitFor();\n\n  // Assert\n  await expect(page.getByLabel('Thumbnail navigation')).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/header.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\ntest('Header', async ({ page }) => {\n  // Arrange\n  await page.goto('/');\n\n  // Act\n  const navigation = page.getByRole('navigation', { name: 'Main' });\n\n  await page.waitForLoadState('networkidle');\n\n  // Assert\n  await expect(navigation).toBeInViewport();\n  await expect(navigation).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/input.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Input with placeholder', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.SAMPLE_ABLE_BREWING_SYSTEM);\n  await page.getByRole('heading', { level: 1, name: '[Sample] Able Brewing System' }).waitFor();\n\n  // Act\n  await page.getByRole('button', { name: 'Add to Cart' }).first().click();\n  await page.getByRole('button', { name: 'Add to Cart' }).first().isEnabled();\n  await page.getByRole('link', { name: 'Cart Items 1' }).click();\n  await page.getByText('Shipping cost').waitFor();\n  await page.getByRole('button', { name: 'Add' }).first().click();\n\n  const input = page.getByLabel('Suburb/city');\n\n  await input.waitFor();\n\n  // Assert\n  await expect(input).toHaveScreenshot();\n});\n\ntest('Input error state', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.CONTACT_US);\n  await page.getByRole('button', { name: 'Submit form' }).waitFor();\n\n  // Act\n  await page.getByRole('button', { name: 'Submit form' }).click();\n\n  // Assert\n  await expect(page.getByLabel('EmailRequired')).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/label.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Label with input', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.LOGIN);\n\n  // Act\n  const label = page.getByText('Password', { exact: true });\n\n  await label.waitFor();\n\n  // Assert\n  await expect(label.locator('..')).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/picklist.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Pick list', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.FOG_LINEN_CHAMBRAY);\n\n  // Act\n  const pickList = page.getByLabel('Pick List');\n\n  await pickList.waitFor();\n\n  // Assert\n  await expect(pickList.locator('..')).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/radio-group.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Default radio group', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.FOG_LINEN_CHAMBRAY);\n\n  // Act\n  const radioGroup = page.getByRole('radiogroup', { name: 'Radio' });\n\n  await radioGroup.waitFor();\n\n  // Assert\n  await expect(radioGroup).toHaveScreenshot();\n});\n\ntest('Default radio group selected', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.FOG_LINEN_CHAMBRAY);\n\n  // Act\n  const radioGroup = page.getByRole('radiogroup', { name: 'Radio' });\n\n  await radioGroup.waitFor();\n  await page.getByLabel('1', { exact: true }).click();\n\n  // Assert\n  await expect(radioGroup).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/rating.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Zero star rating', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.FOG_LINEN_CHAMBRAY);\n\n  // Act\n  const rating = page.getByRole('paragraph').getByRole('img').first();\n\n  await rating.waitFor();\n\n  // Assert\n  await expect(page.getByRole('paragraph').getByRole('img').first()).toHaveScreenshot();\n});\n\ntest('Five start rating', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.PARFAIT_JAR);\n\n  // Act\n  const rating = page.getByRole('paragraph').getByRole('img').first();\n\n  await rating.waitFor();\n\n  // Assert\n  await expect(rating).toHaveScreenshot();\n});\n\ntest('Floating rating', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.ORBIT_TERRARIUM_LARGE);\n\n  // Act\n  const rating = page.getByRole('paragraph').getByRole('img').first();\n\n  await rating.waitFor();\n\n  // Assert\n  await expect(rating).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/rectangle-list.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Rectangle list', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.PARFAIT_JAR);\n\n  // Act\n  const rectangleList = page.getByRole('radiogroup', { name: 'Size' });\n\n  await rectangleList.waitFor();\n\n  // Assert\n  await expect(rectangleList).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/select.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Select default', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.FOG_LINEN_CHAMBRAY);\n\n  // Act\n  const select = page.getByRole('combobox');\n\n  await select.waitFor();\n\n  // Assert\n  await expect(select).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/slideshow.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\ntest('Slideshow multiple slides', async ({ page }) => {\n  // Arrange\n  await page.goto('/');\n\n  // Act\n  const slideshow = page.getByLabel('Interactive slide show');\n\n  await slideshow.waitFor();\n\n  // Assert\n  await expect(slideshow).toHaveScreenshot();\n});\n\ntest('Slideshow paused', async ({ page }) => {\n  // Arrange\n  await page.goto('/');\n\n  // Act\n  const slideshow = page.getByLabel('Interactive slide show');\n\n  await slideshow.waitFor();\n  await page.getByLabel('Pause slideshow').click();\n\n  // Assert\n  await expect(slideshow).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/swatch.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Swatch basic', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.FOG_LINEN_CHAMBRAY);\n\n  // Act\n  const swatch = page.getByRole('radiogroup', { name: 'Color' });\n\n  await swatch.waitFor();\n\n  await expect(swatch).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/tags.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\n\ntest('Tags', async ({ page }) => {\n  await page.goto('/shop-all/?brand=37');\n\n  const tag = page.getByLabel('Filters').getByRole('listitem').filter({ hasText: 'Common Good' });\n\n  await tag.waitFor();\n\n  await expect(tag).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tests/visual-regression/components/textarea.spec.ts",
    "content": "import { expect, test } from '~/tests/fixtures';\nimport routes from '~/tests/routes';\n\ntest('Textarea basic', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.CONTACT_US);\n\n  // Act\n  const textarea = page.getByRole('textbox', { name: 'Comments/questions Required' });\n\n  await textarea.waitFor();\n\n  // Assert\n  await expect(textarea).toHaveScreenshot();\n});\n\ntest('Textarea error', async ({ page }) => {\n  // Arrange\n  await page.goto(routes.CONTACT_US);\n\n  // Act\n  await page.getByRole('button', { name: 'Submit form' }).waitFor();\n  await page.getByRole('button', { name: 'Submit form' }).click();\n\n  // Assert\n  await expect(\n    page.getByRole('textbox', { name: 'Comments/questions Required' }),\n  ).toHaveScreenshot();\n});\n"
  },
  {
    "path": "core/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowArbitraryExtensions\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      },\n      {\n        \"name\": \"@0no-co/graphqlsp\",\n        \"trackFieldUsage\": false,\n        \"shouldCheckForColocatedFragments\": false,\n        \"schemas\": [\n          {\n            \"name\": \"bigcommerce\",\n            \"schema\": \"./bigcommerce.graphql\",\n            \"tadaOutputLocation\": \"./bigcommerce-graphql.d.ts\"\n          }\n        ]\n      }\n    ],\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/vibes/*\": [\"./vibes/*\"],\n      \"~/*\": [\"./*\"]\n    },\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"playwright.config.ts\",\n    \"**/*.cjs\",\n    \"**/*.mjs\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\",\n    \"tests/**/*\"\n  ],\n  \"exclude\": [\"node_modules\", \".next\"]\n}\n"
  },
  {
    "path": "core/user-agent.ts",
    "content": "import packageInfo from './package.json';\n\nconst commitSha = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA;\n\nconst { name, version } = packageInfo;\n\n// Add package name and version to the user agent\n// Used as part of API client instantiation\nexport const backendUserAgent = `${name}/${version}${commitSha ? ` (${commitSha})` : ''}`;\n"
  },
  {
    "path": "core/vibes/soul/form/button-radio-group/index.tsx",
    "content": "import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';\nimport { clsx } from 'clsx';\nimport * as React from 'react';\n\nimport { FieldError } from '@/vibes/soul/form/field-error';\nimport { Label } from '@/vibes/soul/form/label';\n\ninterface Option {\n  value: string;\n  label: string;\n  disabled?: boolean;\n}\n\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *   --button-radio-group-focus: hsl(var(--primary));\n *   --button-radio-group-light-unchecked-border: hsl(var(--contrast-100));\n *   --button-radio-group-light-unchecked-background: hsl(var(--background));\n *   --button-radio-group-light-unchecked-text: hsl(var(--foreground));\n *   --button-radio-group-light-unchecked-border-hover: hsl(var(--contrast-200));\n *   --button-radio-group-light-unchecked-background-hover: hsl(var(--contrast-100));\n *   --button-radio-group-light-checked-background: hsl(var(--foreground));\n *   --button-radio-group-light-checked-text: hsl(var(--background));\n *   --button-radio-group-light-border-error: hsl(var(--error));\n *   --button-radio-group-dark-unchecked-border: hsl(var(--contrast-500));\n *   --button-radio-group-dark-unchecked-background: hsl(var(--background));\n *   --button-radio-group-dark-unchecked-text: hsl(var(--background));\n *   --button-radio-group-dark-unchecked-border-hover: hsl(var(--contrast-400));\n *   --button-radio-group-dark-unchecked-background-hover: hsl(var(--contrast-500));\n *   --button-radio-group-dark-checked-background: hsl(var(--background));\n *   --button-radio-group-dark-checked-text: hsl(var(--foreground));\n *   --button-radio-group-dark-border-error: hsl(var(--error));\n *  }\n * ```\n */\nexport const ButtonRadioGroup = React.forwardRef<\n  React.ComponentRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> & {\n    label?: string;\n    options: Option[];\n    errors?: string[];\n    onOptionMouseEnter?: (value: string) => void;\n    colorScheme?: 'light' | 'dark';\n  }\n>(\n  (\n    {\n      label,\n      options,\n      errors,\n      className,\n      onOptionMouseEnter,\n      colorScheme = 'light',\n      required,\n      ...rest\n    },\n    ref,\n  ) => {\n    const id = React.useId();\n\n    return (\n      <div className={clsx('button-radio-group space-y-2', className)}>\n        {label !== undefined && label !== '' && (\n          <Label colorScheme={colorScheme} id={id} required={required}>\n            {label}\n          </Label>\n        )}\n        <RadioGroupPrimitive.Root\n          {...rest}\n          aria-labelledby={id}\n          className=\"flex flex-wrap gap-2\"\n          ref={ref}\n          required={required}\n        >\n          {options.map((option) => (\n            <RadioGroupPrimitive.Item\n              aria-label={option.label}\n              className={clsx(\n                'h-12 whitespace-nowrap rounded-full border px-4 font-body text-sm font-normal leading-normal transition-colors focus-visible:outline-0 focus-visible:ring-2 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n                {\n                  light:\n                    'border-[var(--button-radio-group-light-unchecked-border,hsl(var(--contrast-100)))] focus-visible:ring-[var(--button-radio-group-light-focus,hsl(var(--primary)))] data-[state=checked]:bg-[var(--button-radio-group-light-checked-background,hsl(var(--foreground)))] data-[state=unchecked]:bg-[var(--button-radio-group-light-unchecked-background,hsl(var(--background)))] data-[state=checked]:text-[var(--button-radio-group-light-checked-text,hsl(var(--background)))] data-[state=unchecked]:text-[var(--button-radio-group-light-unchecked-text,hsl(var(--foreground)))] data-[state=unchecked]:hover:border-[var(--button-radio-group-light-unchecked-border-hover,hsl(var(--contrast-200)))] data-[state=unchecked]:hover:bg-[var(--button-radio-group-light-unchecked-background-hover,hsl(var(--contrast-100)))]',\n                  dark: 'border-[var(--button-radio-group-dark-unchecked-border,hsl(var(--contrast-500)))] focus-visible:ring-[var(--button-radio-group-dark-focus,hsl(var(--primary)))] data-[state=checked]:bg-[var(--button-radio-group-dark-checked-background,hsl(var(--background)))] data-[state=unchecked]:bg-[var(--button-radio-group-dark-unchecked-background,hsl(var(--foreground)))] data-[state=checked]:text-[var(--button-radio-group-dark-checked-text,hsl(var(--foreground)))] data-[state=unchecked]:text-[var(--button-radio-group-dark-checked-text,hsl(var(--background)))] data-[state=unchecked]:hover:border-[var(--button-radio-group-dark-unchecked-border-hover,hsl(var(--contrast-400)))] data-[state=unchecked]:hover:bg-[var(--button-radio-group-dark-unchecked-background-hover,hsl(var(--contrast-500)))]',\n                }[colorScheme],\n                {\n                  light:\n                    errors && errors.length > 0\n                      ? 'data-[state=unchecked]:border-[var(--button-radio-group-light-border-error,hsl(var(--error)))]'\n                      : 'data-[state=checked]:border-[var(--button-radio-group-light-checked-background,hsl(var(--foreground)))]',\n                  dark:\n                    errors && errors.length > 0\n                      ? 'data-[state=unchecked]:border-[var(--button-radio-group-dark-border-error,hsl(var(--error)))]'\n                      : 'data-[state=checked]:border-[var(--button-radio-group-dark-checked-background,hsl(var(--foreground)))]',\n                }[colorScheme],\n              )}\n              disabled={option.disabled}\n              id={option.value}\n              key={option.value}\n              onMouseEnter={() => {\n                onOptionMouseEnter?.(option.value);\n              }}\n              value={option.value}\n            >\n              {option.label}\n            </RadioGroupPrimitive.Item>\n          ))}\n        </RadioGroupPrimitive.Root>\n        {errors?.map((error) => (\n          <FieldError key={error}>{error}</FieldError>\n        ))}\n      </div>\n    );\n  },\n);\n\nButtonRadioGroup.displayName = 'ButtonRadioGroup';\n"
  },
  {
    "path": "core/vibes/soul/form/card-radio-group/index.tsx",
    "content": "import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';\nimport { clsx } from 'clsx';\nimport * as React from 'react';\n\nimport { FieldError } from '@/vibes/soul/form/field-error';\nimport { Label } from '@/vibes/soul/form/label';\nimport { Image } from '~/components/image';\n\ninterface Option {\n  value: string;\n  label: string;\n  image?: { src: string; alt: string };\n  disabled?: boolean;\n}\n\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *   --card-radio-group-focus: hsl(var(--primary));\n *   --card-radio-group-light-unchecked-border: hsl(var(--contrast-100));\n *   --card-radio-group-light-unchecked-border-hover: hsl(var(--contrast-200));\n *   --card-radio-group-light-unchecked-background: hsl(var(--background));\n *   --card-radio-group-light-unchecked-text: hsl(var(--foreground));\n *   --card-radio-group-light-unchecked-background-hover: hsl(var(--contrast-100));\n *   --card-radio-group-light-checked-background: hsl(var(--foreground));\n *   --card-radio-group-light-checked-text: hsl(var(--background));\n *   --card-radio-group-light-border-error: hsl(var(--error));\n *   --card-radio-group-dark-unchecked-border: hsl(var(--contrast-500));\n *   --card-radio-group-dark-unchecked-border-hover: hsl(var(--contrast-400));\n *   --card-radio-group-dark-unchecked-background: hsl(var(--foreground));\n *   --card-radio-group-dark-unchecked-background-hover: hsl(var(--contrast-500));\n *   --card-radio-group-dark-unchecked-text: hsl(var(--background));\n *   --card-radio-group-dark-checked-background: hsl(var(--background));\n *   --card-radio-group-dark-checked-text: hsl(var(--foreground));\n *   --card-radio-group-dark-border-error: hsl(var(--error));\n *  }\n * ```\n */\nexport const CardRadioGroup = React.forwardRef<\n  React.ComponentRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> & {\n    label?: string;\n    options: Option[];\n    errors?: string[];\n    onOptionMouseEnter?: (value: string) => void;\n    colorScheme?: 'light' | 'dark';\n  }\n>(\n  (\n    {\n      label,\n      options,\n      errors,\n      className,\n      onOptionMouseEnter,\n      colorScheme = 'light',\n      required,\n      ...rest\n    },\n    ref,\n  ) => {\n    const id = React.useId();\n\n    return (\n      <div className={clsx('space-y-2', className)}>\n        {label !== undefined && label !== '' && (\n          <Label colorScheme={colorScheme} id={id} required={required}>\n            {label}\n          </Label>\n        )}\n        <RadioGroupPrimitive.Root\n          {...rest}\n          aria-labelledby={id}\n          className=\"space-y-2\"\n          ref={ref}\n          required={required}\n        >\n          {options.map((option) => (\n            <RadioGroupPrimitive.Item\n              aria-label={option.label}\n              className={clsx(\n                'relative flex h-12 w-full items-center overflow-hidden rounded-lg border font-body text-sm font-normal leading-normal transition-colors focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-[var(--card-radio-group-focus,hsl(var(--primary)))] data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n                {\n                  light:\n                    'border-[var(--card-radio-group-light-unchecked-border,hsl(var(--contrast-100)))] text-[var(--card-radio-group-light-unchecked-text,hsl(var(--foreground)))] data-[state=checked]:bg-[var(--card-radio-group-light-checked-background,hsl(var(--foreground)))] data-[state=unchecked]:bg-[var(--card-radio-group-light-unchecked-background,hsl(var(--background)))] data-[state=checked]:text-[var(--card-radio-group-light-checked-text,hsl(var(--background)))] data-[state=unchecked]:hover:border-[var(--card-radio-group-light-unchecked-border-hover,hsl(var(--contrast-200)))] data-[state=unchecked]:hover:bg-[var(--card-radio-group-light-unchecked-background-hover,hsl(var(--contrast-100)))]',\n                  dark: 'border-[var(--card-radio-group-dark-unchecked-border,hsl(var(--contrast-500)))] text-[var(--card-radio-group-dark-unchecked-text,hsl(var(--background)))] data-[state=checked]:bg-[var(--card-radio-group-dark-checked-background,hsl(var(--background)))] data-[state=unchecked]:bg-[var(--card-radio-group-dark-unchecked-background,hsl(var(--foreground)))] data-[state=checked]:text-[var(--card-radio-group-dark-checked-text,hsl(var(--foreground)))] data-[state=unchecked]:hover:border-[var(--card-radio-group-dark-unchecked-border-hover,hsl(var(--contrast-400)))] data-[state=unchecked]:hover:bg-[var(--card-radio-group-dark-unchecked-background-hover,hsl(var(--contrast-500)))]',\n                }[colorScheme],\n                {\n                  light:\n                    errors && errors.length > 0\n                      ? 'data-[state=unchecked]:border-[var(--card-radio-group-light-border-error,hsl(var(--error)))]'\n                      : 'data-[state=checked]:border-[var(--card-radio-group-light-checked-background,hsl(var(--foreground)))]',\n                  dark:\n                    errors && errors.length > 0\n                      ? 'data-[state=unchecked]:border-[var(--card-radio-group-dark-border-error,hsl(var(--error)))]'\n                      : 'data-[state=checked]:border-[var(--card-radio-group-dark-checked-background,hsl(var(--foreground)))]',\n                }[colorScheme],\n              )}\n              disabled={option.disabled}\n              id={option.value}\n              key={option.value}\n              onMouseEnter={() => {\n                onOptionMouseEnter?.(option.value);\n              }}\n              value={option.value}\n            >\n              {option.image && (\n                <div className=\"relative aspect-square h-full\">\n                  <Image\n                    alt={option.image.alt}\n                    className=\"bg-background object-fill\"\n                    fill\n                    src={option.image.src}\n                  />\n                </div>\n              )}\n\n              <span className=\"flex-1 truncate text-ellipsis px-4 text-left\">{option.label}</span>\n            </RadioGroupPrimitive.Item>\n          ))}\n        </RadioGroupPrimitive.Root>\n        {errors?.map((error) => (\n          <FieldError key={error}>{error}</FieldError>\n        ))}\n      </div>\n    );\n  },\n);\n\nCardRadioGroup.displayName = 'CardRadioGroup';\n"
  },
  {
    "path": "core/vibes/soul/form/checkbox/index.tsx",
    "content": "'use client';\n\nimport * as CheckboxPrimitive from '@radix-ui/react-checkbox';\nimport * as LabelPrimitive from '@radix-ui/react-label';\nimport { clsx } from 'clsx';\nimport { Check } from 'lucide-react';\nimport { ComponentPropsWithoutRef, ReactNode, useId } from 'react';\n\nimport { FieldError } from '@/vibes/soul/form/field-error';\n\nexport interface CheckboxProps extends ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> {\n  label?: ReactNode;\n  errors?: string[];\n  colorScheme?: 'light' | 'dark';\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *    --checkbox-focus: hsl(var(--primary));\n *    --checkbox-light-label: hsl(var(--foreground));\n *    --checkbox-light-error: hsl(var(--error));\n *    --checkbox-light-unchecked-border: hsl(var(--contrast-200));\n *    --checkbox-light-unchecked-border-hover: hsl(var(--contrast-300));\n *    --checkbox-light-unchecked-background: hsl(var(--background));\n *    --checkbox-light-unchecked-icon: hsl(var(--foreground));\n *    --checkbox-light-checked-border: hsl(var(--foreground));\n *    --checkbox-light-checked-border-hover: hsl(var(--foreground));\n *    --checkbox-light-checked-background: hsl(var(--foreground));\n *    --checkbox-light-checked-icon: hsl(var(--background));\n *    --checkbox-light-disabled-border: hsl(var(--contrast-200));\n *    --checkbox-light-disabled-background: hsl(var(--contrast-100));\n *    --checkbox-light-disabled-icon: hsl(var(--contrast-300));\n *    --checkbox-dark-label: hsl(var(--background));\n *    --checkbox-dark-error: hsl(var(--error));\n *    --checkbox-dark-unchecked-border: hsl(var(--contrast-400));\n *    --checkbox-dark-unchecked-border-hover: hsl(var(--contrast-300));\n *    --checkbox-dark-unchecked-background: hsl(var(--foreground));\n *    --checkbox-dark-unchecked-icon: hsl(var(--background));\n *    --checkbox-dark-checked-border: hsl(var(--background));\n *    --checkbox-dark-checked-border-hover: hsl(var(--background));\n *    --checkbox-dark-checked-background: hsl(var(--foreground));\n *    --checkbox-dark-checked-icon: hsl(var(--foreground));\n *    --checkbox-dark-disabled-border: hsl(var(--contrast-200));\n *    --checkbox-dark-disabled-background: hsl(var(--contrast-100));\n *    --checkbox-dark-disabled-icon: hsl(var(--contrast-300));\n *    --checkbox-font-family: var(--font-family-body);\n *  }\n * ```\n */\nexport function Checkbox({\n  id,\n  label,\n  errors,\n  className,\n  colorScheme = 'light',\n  ...props\n}: CheckboxProps) {\n  const generatedId = useId();\n\n  return (\n    <div className=\"space-y-2\">\n      <div\n        className={clsx(\n          'flex items-center gap-2 font-[family-name:var(--checkbox-font-family,var(--font-family-body))]',\n          className,\n        )}\n      >\n        <CheckboxPrimitive.Root\n          {...props}\n          aria-labelledby={id !== undefined ? `${id}-label` : `${generatedId}-label`}\n          className={clsx(\n            'peer flex h-5 w-5 items-center justify-center rounded-md border transition-colors duration-150 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-[var(--checkbox-focus,hsl(var(--primary)))] disabled:cursor-not-allowed',\n            {\n              light:\n                errors && errors.length > 0\n                  ? 'border-[var(--checkbox-light-error,hsl(var(--error)))]'\n                  : clsx(\n                      // Disabled states\n                      'disabled:border-[var(--checkbox-light-disabled-border,hsl(var(--contrast-200)))] disabled:bg-[var(--checkbox-light-disabled-background,hsl(var(--contrast-100)))] disabled:text-[var(--checkbox-light-disabled-icon,hsl(var(--contrast-300)))]',\n                      // Normal states\n                      'enabled:data-[state=checked]:border-[var(--checkbox-light-checked-border,hsl(var(--foreground)))] enabled:data-[state=unchecked]:border-[var(--checkbox-light-unchecked-border,hsl(var(--contrast-200)))] enabled:data-[state=checked]:bg-[var(--checkbox-light-checked-background,hsl(var(--foreground)))] enabled:data-[state=unchecked]:bg-[var(--checkbox-light-unchecked-background,hsl(var(--background)))] enabled:data-[state=checked]:text-[var(--checkbox-light-checked-text,hsl(var(--background)))] enabled:data-[state=unchecked]:text-[var(--checkbox-light-unchecked-text,hsl(var(--foreground)))]',\n                      // Hover states (only apply when checkbox is enabled)\n                      'enabled:data-[state=checked]:hover:border-[var(--checkbox-light-checked-border-hover,hsl(var(--foreground)))] enabled:data-[state=unchecked]:hover:border-[var(--checkbox-light-unchecked-border-hover,hsl(var(--contrast-300)))]',\n                    ),\n              dark:\n                errors && errors.length > 0\n                  ? 'border-[var(--checkbox-dark-error,hsl(var(--error)))]'\n                  : clsx(\n                      // Disabled states\n                      'disabled:border-[var(--checkbox-dark-disabled-border,hsl(var(--contrast-200)))] disabled:bg-[var(--checkbox-dark-disabled-background,hsl(var(--contrast-100)))] disabled:text-[var(--checkbox-dark-disabled-icon,hsl(var(--contrast-300)))]',\n                      // Normal states\n                      'enabled:data-[state=checked]:border-[var(--checkbox-dark-checked-border,hsl(var(--background)))] enabled:data-[state=unchecked]:border-[var(--checkbox-dark-unchecked-border,hsl(var(--contrast-400)))] enabled:data-[state=checked]:bg-[var(--checkbox-dark-checked-background,hsl(var(--foreground)))] enabled:data-[state=unchecked]:bg-[var(--checkbox-dark-unchecked-background,hsl(var(--foreground)))] enabled:data-[state=checked]:text-[var(--checkbox-dark-checked-text,hsl(var(--background)))] enabled:data-[state=unchecked]:text-[var(--checkbox-dark-unchecked-text,hsl(var(--background)))]',\n                      // Hover states (only apply when checkbox is enabled)\n                      'enabled:data-[state=checked]:hover:border-[var(--checkbox-dark-checked-border-hover,hsl(var(--background)))] enabled:data-[state=unchecked]:hover:border-[var(--checkbox-dark-unchecked-border-hover,hsl(var(--contrast-300)))]',\n                    ),\n            }[colorScheme],\n          )}\n          id={id ?? generatedId}\n        >\n          <CheckboxPrimitive.Indicator>\n            <Check className=\"h-4 w-4\" color=\"currentColor\" />\n          </CheckboxPrimitive.Indicator>\n        </CheckboxPrimitive.Root>\n\n        {label != null && label !== '' && (\n          <LabelPrimitive.Root\n            className={clsx(\n              'cursor-pointer text-sm peer-disabled:cursor-not-allowed peer-disabled:opacity-50',\n              {\n                light: 'text-[var(--checkbox-light-label,hsl(var(--foreground)))]',\n                dark: 'text-[var(--checkbox-dark-label,hsl(var(--background)))]',\n              }[colorScheme],\n            )}\n            htmlFor={id ?? generatedId}\n            id={id !== undefined ? `${id}-label` : `${generatedId}-label`}\n          >\n            {label}\n          </LabelPrimitive.Root>\n        )}\n      </div>\n      {errors?.map((error) => (\n        <FieldError key={error}>{error}</FieldError>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/form/checkbox-group/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport * as React from 'react';\n\nimport { Checkbox } from '@/vibes/soul/form/checkbox';\nimport { FieldError } from '@/vibes/soul/form/field-error';\nimport { Label } from '@/vibes/soul/form/label';\n\ninterface Option {\n  value: string;\n  label: string;\n  disabled?: boolean;\n}\n\ninterface Props {\n  id?: string;\n  className?: string;\n  label?: string;\n  options: Option[];\n  errors?: string[];\n  name?: string;\n  value: string[];\n  onValueChange: (value: string[]) => void;\n  colorScheme?: 'light' | 'dark';\n  required?: boolean;\n}\n\nexport function CheckboxGroup({\n  className,\n  label,\n  options,\n  errors,\n  name,\n  value,\n  onValueChange,\n  colorScheme,\n  required,\n}: Props) {\n  const id = React.useId();\n\n  return (\n    <div className={clsx('space-y-2', className)}>\n      {label !== undefined && label !== '' && (\n        <Label colorScheme={colorScheme} id={id} required={required}>\n          {label}\n        </Label>\n      )}\n      <div aria-labelledby={id} className=\"space-y-2\">\n        {options.map((option) => (\n          <Checkbox\n            checked={value.includes(option.value)}\n            colorScheme={colorScheme}\n            key={option.value}\n            label={option.label}\n            name={name}\n            onCheckedChange={(checked) =>\n              onValueChange(\n                checked === true\n                  ? [...value, option.value]\n                  : value.filter((v) => v !== option.value),\n              )\n            }\n            value={option.value}\n          />\n        ))}\n      </div>\n      {errors?.map((error) => (\n        <FieldError key={error}>{error}</FieldError>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/form/date-picker/index.tsx",
    "content": "import * as PopoverPrimitive from '@radix-ui/react-popover';\nimport { CalendarIcon } from 'lucide-react';\nimport { ComponentPropsWithoutRef, ComponentRef, forwardRef, useState } from 'react';\n\nimport { Input } from '@/vibes/soul/form/input';\nimport { Calendar } from '@/vibes/soul/primitives/calendar';\n\ntype CalendarProps = ComponentPropsWithoutRef<typeof Calendar>;\n\ntype Props = {\n  defaultValue?: string | Date;\n  disabledDays?: CalendarProps['disabled'];\n  errors?: string[];\n  onSelect?: (date: Date | undefined) => void;\n  selected?: Date | undefined;\n  colorScheme?: 'light' | 'dark';\n} & Omit<ComponentPropsWithoutRef<typeof Input>, 'defaultValue' | 'onSelect' | 'value' | 'type'>;\n\nconst DatePicker = forwardRef<ComponentRef<typeof Input>, Props>(\n  (\n    {\n      defaultValue,\n      disabledDays,\n      errors,\n      onSelect,\n      placeholder = 'MM/DD/YYYY',\n      required = false,\n      selected,\n      colorScheme = 'light',\n      ...props\n    },\n    ref,\n  ) => {\n    // State to manage the selected date\n    const [date, setDate] = useState<Date | undefined>(\n      defaultValue != null ? new Date(defaultValue) : undefined,\n    );\n\n    // Format the selected date for display\n    const formattedSelected = selected ? Intl.DateTimeFormat().format(selected) : undefined;\n\n    // Format the default date for display\n    const formattedDate = date ? Intl.DateTimeFormat().format(date) : undefined;\n\n    return (\n      <PopoverPrimitive.Root>\n        <PopoverPrimitive.Trigger asChild>\n          <Input\n            {...props}\n            colorScheme={colorScheme}\n            errors={errors}\n            placeholder={placeholder}\n            prepend={<CalendarIcon className=\"h-5 w-5\" strokeWidth={1} />}\n            readOnly={true}\n            ref={ref}\n            required={required}\n            type=\"text\"\n            // We control the value of the input based on the selected date or the default date\n            value={formattedSelected ?? formattedDate ?? ''}\n          />\n        </PopoverPrimitive.Trigger>\n        <PopoverPrimitive.Portal>\n          <PopoverPrimitive.Content\n            align=\"start\"\n            className=\"z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\"\n            sideOffset={8}\n          >\n            <Calendar\n              colorScheme={colorScheme}\n              disabled={disabledDays}\n              mode=\"single\"\n              onSelect={onSelect ?? setDate}\n              selected={selected ?? date}\n            />\n          </PopoverPrimitive.Content>\n        </PopoverPrimitive.Portal>\n      </PopoverPrimitive.Root>\n    );\n  },\n);\n\nDatePicker.displayName = 'DatePicker';\n\nexport { DatePicker };\n"
  },
  {
    "path": "core/vibes/soul/form/dynamic-form/index.tsx",
    "content": "/* eslint-disable complexity */\n'use client';\n\nimport {\n  FieldMetadata,\n  FormProvider,\n  getFormProps,\n  getInputProps,\n  SubmissionResult,\n  useForm,\n  useInputControl,\n} from '@conform-to/react';\nimport { getZodConstraint, parseWithZod } from '@conform-to/zod';\nimport { useTranslations } from 'next-intl';\nimport {\n  FormEvent,\n  MouseEvent,\n  ReactNode,\n  startTransition,\n  useActionState,\n  useEffect,\n} from 'react';\nimport { useFormStatus } from 'react-dom';\nimport RecaptchaWidget from 'react-google-recaptcha';\nimport { z } from 'zod';\n\nimport { ButtonRadioGroup } from '@/vibes/soul/form/button-radio-group';\nimport { CardRadioGroup } from '@/vibes/soul/form/card-radio-group';\nimport { Checkbox } from '@/vibes/soul/form/checkbox';\nimport { CheckboxGroup } from '@/vibes/soul/form/checkbox-group';\nimport { DatePicker } from '@/vibes/soul/form/date-picker';\nimport { FormStatus } from '@/vibes/soul/form/form-status';\nimport { Input } from '@/vibes/soul/form/input';\nimport { NumberInput } from '@/vibes/soul/form/number-input';\nimport { RadioGroup } from '@/vibes/soul/form/radio-group';\nimport { Select } from '@/vibes/soul/form/select';\nimport { SwatchRadioGroup } from '@/vibes/soul/form/swatch-radio-group';\nimport { Textarea } from '@/vibes/soul/form/textarea';\nimport { Button, ButtonProps } from '@/vibes/soul/primitives/button';\n\nimport {\n  Field,\n  FieldGroup,\n  FormErrorTranslationMap,\n  PasswordComplexitySettings,\n  schema,\n} from './schema';\nimport { removeOptionsFromFields } from './utils';\n\nexport interface DynamicFormActionArgs<F extends Field> {\n  fields: Array<F | FieldGroup<F>>;\n  passwordComplexity?: PasswordComplexitySettings | null;\n}\n\ntype Action<F extends Field, S, P> = (\n  args: DynamicFormActionArgs<F>,\n  state: Awaited<S>,\n  payload: P,\n) => S | Promise<S>;\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  successMessage?: ReactNode;\n}\n\nexport type DynamicFormAction<F extends Field> = Action<F, State, FormData>;\n\nexport interface DynamicFormProps<F extends Field> {\n  fields: Array<F | FieldGroup<F>>;\n  action: DynamicFormAction<F>;\n  buttonSize?: ButtonProps['size'];\n  cancelLabel?: string;\n  submitLabel?: string;\n  submitName?: string;\n  submitValue?: string;\n  onCancel?: (e: MouseEvent<HTMLButtonElement>) => void;\n  onChange?: (e: FormEvent<HTMLFormElement>) => void;\n  onSuccess?: (lastResult: SubmissionResult, successMessage: ReactNode) => void;\n  passwordComplexity?: PasswordComplexitySettings | null;\n  errorTranslations?: FormErrorTranslationMap;\n  recaptchaSiteKey?: string;\n}\n\nexport function DynamicForm<F extends Field>({\n  action,\n  fields,\n  buttonSize = 'medium',\n  cancelLabel = 'Cancel',\n  submitLabel = 'Submit',\n  submitName,\n  submitValue,\n  onCancel,\n  onChange,\n  onSuccess,\n  passwordComplexity,\n  errorTranslations,\n  recaptchaSiteKey,\n}: DynamicFormProps<F>) {\n  const t = useTranslations('Form');\n  // Remove options from fields before passing to action to reduce payload size\n  // Options are only needed for rendering, not for processing form submissions\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  const fieldsWithoutOptions = removeOptionsFromFields(fields) as Array<F | FieldGroup<F>>;\n  const actionWithFields = action.bind(null, { fields: fieldsWithoutOptions, passwordComplexity });\n\n  const [{ lastResult, successMessage }, formAction] = useActionState(actionWithFields, {\n    lastResult: null,\n  });\n\n  const dynamicSchema = schema(fields, passwordComplexity, errorTranslations);\n  const defaultValue = fields\n    .flatMap((f) => (Array.isArray(f) ? f : [f]))\n    .reduce<z.infer<typeof dynamicSchema>>(\n      (acc, field) => ({\n        ...acc,\n        [field.name]: 'defaultValue' in field ? field.defaultValue : '',\n      }),\n      {},\n    );\n\n  const [form, formFields] = useForm({\n    lastResult,\n    constraint: getZodConstraint(dynamicSchema),\n    onValidate({ formData }) {\n      return parseWithZod(formData, {\n        schema: dynamicSchema,\n        errorMap: (issue) => {\n          if (\n            !errorTranslations &&\n            issue.code === z.ZodIssueCode.invalid_string &&\n            issue.validation === 'regex'\n          ) {\n            return { message: t('Errors.invalidFormat') };\n          }\n\n          if (!errorTranslations) {\n            return { message: issue.message ?? t('Errors.invalidInput') };\n          }\n\n          const field = issue.path[0];\n          const fieldKey = typeof field === 'string' ? field : '';\n          const errorMessage = errorTranslations[fieldKey]?.[issue.code];\n\n          return { message: errorMessage ?? issue.message ?? t('Errors.invalidInput') };\n        },\n      });\n    },\n    defaultValue,\n    shouldValidate: 'onSubmit',\n    shouldRevalidate: 'onInput',\n    onSubmit(event, { formData }) {\n      event.preventDefault();\n\n      startTransition(() => {\n        formAction(formData);\n      });\n    },\n  });\n\n  useEffect(() => {\n    if (lastResult && lastResult.status === 'success' && successMessage) {\n      onSuccess?.(lastResult, successMessage);\n    }\n  }, [lastResult, successMessage, onSuccess]);\n\n  return (\n    <FormProvider context={form.context}>\n      <form {...getFormProps(form)} action={formAction} onChange={onChange}>\n        <div className=\"space-y-6\">\n          {fields.map((field, index) => {\n            if (Array.isArray(field)) {\n              return (\n                <div className=\"flex flex-col gap-4 @sm:flex-row\" key={index}>\n                  {field.map((f) => {\n                    const groupFormField = formFields[f.name];\n\n                    if (!groupFormField) return null;\n\n                    return (\n                      <DynamicFormField\n                        field={f}\n                        formField={groupFormField}\n                        key={groupFormField.id}\n                      />\n                    );\n                  })}\n                </div>\n              );\n            }\n\n            const formField = formFields[field.name];\n\n            if (formField == null) return null;\n\n            return <DynamicFormField field={field} formField={formField} key={formField.id} />;\n          })}\n          {recaptchaSiteKey ? <RecaptchaWidget sitekey={recaptchaSiteKey} /> : null}\n          <div className=\"flex gap-1 pt-3\">\n            {onCancel && (\n              <Button\n                aria-label={`${cancelLabel} ${submitLabel}`}\n                onClick={onCancel}\n                size={buttonSize}\n                variant=\"tertiary\"\n              >\n                {cancelLabel}\n              </Button>\n            )}\n            <SubmitButton name={submitName} size={buttonSize} value={submitValue}>\n              {submitLabel}\n            </SubmitButton>\n          </div>\n          {form.errors?.map((error, index) => (\n            <FormStatus key={index} type=\"error\">\n              {error}\n            </FormStatus>\n          ))}\n        </div>\n      </form>\n    </FormProvider>\n  );\n}\n\nfunction SubmitButton({\n  children,\n  name,\n  value,\n  size,\n}: {\n  children: ReactNode;\n  name?: string;\n  value?: string;\n  size: ButtonProps['size'];\n}) {\n  const { pending } = useFormStatus();\n\n  return (\n    <Button loading={pending} name={name} size={size} type=\"submit\" value={value}>\n      {children}\n    </Button>\n  );\n}\n\nfunction DynamicFormField({\n  field,\n  formField,\n}: {\n  field: Field;\n  formField: FieldMetadata<string | string[] | number | boolean | Date | undefined>;\n}) {\n  const controls = useInputControl(formField);\n\n  switch (field.type) {\n    case 'number':\n      return (\n        <NumberInput\n          {...getInputProps(formField, { type: 'number' })}\n          decrementLabel={field.decrementLabel}\n          defaultValue={field.defaultValue}\n          errors={formField.errors}\n          incrementLabel={field.incrementLabel}\n          key={field.name}\n          label={field.label}\n          placeholder={field.placeholder}\n        />\n      );\n\n    case 'text':\n      return (\n        <Input\n          {...getInputProps(formField, { type: 'text', pattern: field.pattern })}\n          errors={formField.errors}\n          key={field.name}\n          label={field.label}\n          placeholder={field.placeholder}\n        />\n      );\n\n    case 'textarea':\n      return (\n        <Textarea\n          {...getInputProps(formField, { type: 'text' })}\n          errors={formField.errors}\n          key={field.name}\n          label={field.label}\n          placeholder={field.placeholder}\n        />\n      );\n\n    case 'password':\n    case 'confirm-password':\n      return (\n        <Input\n          {...getInputProps(formField, { type: 'password' })}\n          errors={formField.errors}\n          key={field.name}\n          label={field.label}\n          placeholder={field.placeholder}\n        />\n      );\n\n    case 'email':\n      return (\n        <Input\n          {...getInputProps(formField, { type: 'email' })}\n          errors={formField.errors}\n          key={field.name}\n          label={field.label}\n          placeholder={field.placeholder}\n        />\n      );\n\n    case 'checkbox':\n      return (\n        <Checkbox\n          defaultValue={field.defaultValue}\n          errors={formField.errors}\n          key={field.name}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onCheckedChange={(value) => controls.change(String(value))}\n          onFocus={controls.focus}\n          required={field.required}\n          value={controls.value}\n        />\n      );\n\n    case 'checkbox-group':\n      return (\n        <CheckboxGroup\n          errors={formField.errors}\n          key={field.name}\n          label={field.label}\n          name={formField.name}\n          onValueChange={controls.change}\n          options={field.options}\n          required={field.required}\n          value={Array.isArray(controls.value) ? controls.value : []}\n        />\n      );\n\n    case 'select':\n      return (\n        <Select\n          errors={formField.errors}\n          key={field.name}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onFocus={controls.focus}\n          onValueChange={controls.change}\n          options={field.options}\n          placeholder={field.placeholder}\n          required={formField.required}\n          value={typeof controls.value === 'string' ? controls.value : ''}\n        />\n      );\n\n    case 'radio-group':\n      return (\n        <RadioGroup\n          errors={formField.errors}\n          key={field.name}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onFocus={controls.focus}\n          onValueChange={controls.change}\n          options={field.options}\n          required={formField.required}\n          value={typeof controls.value === 'string' ? controls.value : ''}\n        />\n      );\n\n    case 'swatch-radio-group':\n      return (\n        <SwatchRadioGroup\n          errors={formField.errors}\n          id={formField.id}\n          key={field.name}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onFocus={controls.focus}\n          onValueChange={controls.change}\n          options={field.options}\n          required={formField.required}\n          value={typeof controls.value === 'string' ? controls.value : ''}\n        />\n      );\n\n    case 'card-radio-group':\n      return (\n        <CardRadioGroup\n          errors={formField.errors}\n          id={formField.id}\n          key={field.name}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onFocus={controls.focus}\n          onValueChange={controls.change}\n          options={field.options}\n          required={formField.required}\n          value={typeof controls.value === 'string' ? controls.value : ''}\n        />\n      );\n\n    case 'button-radio-group':\n      return (\n        <ButtonRadioGroup\n          errors={formField.errors}\n          id={formField.id}\n          key={field.name}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onFocus={controls.focus}\n          onValueChange={controls.change}\n          options={field.options}\n          required={formField.required}\n          value={typeof controls.value === 'string' ? controls.value : ''}\n        />\n      );\n\n    case 'date':\n      return (\n        <DatePicker\n          defaultValue={field.defaultValue}\n          disabledDays={\n            field.minDate != null && field.maxDate != null\n              ? {\n                  before: new Date(field.minDate),\n                  after: new Date(field.maxDate),\n                }\n              : undefined\n          }\n          errors={formField.errors}\n          key={field.name}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onFocus={controls.focus}\n          onSelect={(date) =>\n            controls.change(date ? Intl.DateTimeFormat().format(date) : undefined)\n          }\n          required={formField.required}\n          selected={typeof controls.value === 'string' ? new Date(controls.value) : undefined}\n        />\n      );\n\n    case 'hidden':\n      return <input {...getInputProps(formField, { type: 'hidden' })} key={field.name} />;\n  }\n}\n"
  },
  {
    "path": "core/vibes/soul/form/dynamic-form/schema.ts",
    "content": "import { z } from 'zod';\n\nexport interface PasswordComplexitySettings {\n  minimumNumbers?: number | null;\n  minimumPasswordLength?: number | null;\n  minimumSpecialCharacters?: number | null;\n  requireLowerCase?: boolean | null;\n  requireNumbers?: boolean | null;\n  requireSpecialCharacters?: boolean | null;\n  requireUpperCase?: boolean | null;\n}\n\nexport type FormErrorTranslationMap = Record<\n  string,\n  Partial<\n    Record<\n      | z.ZodIssueCode\n      | 'lowercase_required'\n      | 'uppercase_required'\n      | 'number_required'\n      | 'special_character_required'\n      | 'passwords_must_match',\n      string\n    >\n  >\n>;\n\ninterface FormField {\n  name: string;\n  label?: string;\n  errors?: string[];\n  required?: boolean;\n  id?: string;\n  placeholder?: string;\n}\n\ntype RadioField = {\n  type: 'radio-group';\n  options: Array<{ label: string; value: string }>;\n  defaultValue?: string;\n} & FormField;\n\ntype SelectField = {\n  type: 'select';\n  options: Array<{ label: string; value: string }>;\n  defaultValue?: string;\n} & FormField;\n\ntype CheckboxField = {\n  type: 'checkbox';\n  defaultValue?: string;\n} & FormField;\n\ntype CheckboxGroupField = {\n  type: 'checkbox-group';\n  options: Array<{ label: string; value: string }>;\n  defaultValue?: string[];\n} & FormField;\n\ntype NumberInputField = {\n  type: 'number';\n  defaultValue?: string;\n  min?: number;\n  max?: number;\n  step?: number;\n  incrementLabel?: string;\n  decrementLabel?: string;\n} & FormField;\n\ntype TextInputField = {\n  type: 'text';\n  defaultValue?: string;\n  pattern?: string;\n} & FormField;\n\ntype EmailInputField = {\n  type: 'email';\n  defaultValue?: string;\n} & FormField;\n\ntype TextAreaField = {\n  type: 'textarea';\n  defaultValue?: string;\n} & FormField;\n\ntype DateField = {\n  type: 'date';\n  defaultValue?: string;\n  minDate?: string;\n  maxDate?: string;\n} & FormField;\n\ntype SwatchRadioFieldOption =\n  | {\n      type: 'color';\n      value: string;\n      label: string;\n      color: string;\n      disabled?: boolean;\n    }\n  | {\n      type: 'image';\n      value: string;\n      label: string;\n      image: { src: string; alt: string };\n      disabled?: boolean;\n    };\n\ntype SwatchRadioField = {\n  type: 'swatch-radio-group';\n  defaultValue?: string;\n  options: SwatchRadioFieldOption[];\n} & FormField;\n\ntype CardRadioField = {\n  type: 'card-radio-group';\n  defaultValue?: string;\n  options: Array<{\n    value: string;\n    label: string;\n    image: { src: string; alt: string };\n    disabled?: boolean;\n  }>;\n} & FormField;\n\ntype ButtonRadioField = {\n  type: 'button-radio-group';\n  defaultValue?: string;\n  pattern?: string;\n  options: Array<{\n    value: string;\n    label: string;\n    disabled?: boolean;\n  }>;\n} & FormField;\n\ntype PasswordField = {\n  type: 'password';\n} & FormField;\n\ntype ConfirmPasswordField = {\n  type: 'confirm-password';\n} & FormField;\n\ntype HiddenInputField = {\n  type: 'hidden';\n  defaultValue?: string;\n} & FormField;\n\nexport type Field =\n  | RadioField\n  | CheckboxField\n  | CheckboxGroupField\n  | NumberInputField\n  | TextInputField\n  | TextAreaField\n  | DateField\n  | SwatchRadioField\n  | CardRadioField\n  | ButtonRadioField\n  | SelectField\n  | PasswordField\n  | ConfirmPasswordField\n  | EmailInputField\n  | HiddenInputField;\n\nexport type FieldGroup<F> = F[];\n\nexport type SchemaRawShape = Record<\n  string,\n  | z.ZodString\n  | z.ZodOptional<z.ZodString>\n  | z.ZodNumber\n  | z.ZodOptional<z.ZodNumber>\n  | z.ZodArray<z.ZodString>\n  | z.ZodOptional<z.ZodArray<z.ZodString>>\n  | z.ZodLiteral<'true'>\n  | z.ZodEnum<['true', 'false']>\n  | z.ZodOptional<z.ZodEnum<['true', 'false']>>\n>;\n\n// eslint-disable-next-line complexity\nexport function getPasswordSchema(\n  passwordComplexity?: PasswordComplexitySettings | null,\n  errorTranslations?: FormErrorTranslationMap,\n) {\n  const minLength = passwordComplexity?.minimumPasswordLength ?? 8;\n  const minNumbers = passwordComplexity?.minimumNumbers ?? 0;\n  const minSpecialChars = passwordComplexity?.minimumSpecialCharacters ?? 0;\n  const requireLowerCase = passwordComplexity?.requireLowerCase ?? false;\n  const requireUpperCase = passwordComplexity?.requireUpperCase ?? false;\n  const requireNumbers = passwordComplexity?.requireNumbers ?? true;\n  const requireSpecialChars = passwordComplexity?.requireSpecialCharacters ?? true;\n\n  let fieldSchema = z.string().trim();\n\n  fieldSchema = fieldSchema.min(minLength);\n\n  if (requireLowerCase) {\n    fieldSchema = fieldSchema.regex(/[a-z]/, {\n      message:\n        errorTranslations?.password?.lowercase_required ?? 'Contain at least one lowercase letter',\n    });\n  }\n\n  if (requireUpperCase) {\n    fieldSchema = fieldSchema.regex(/[A-Z]/, {\n      message:\n        errorTranslations?.password?.uppercase_required ?? 'Contain at least one uppercase letter',\n    });\n  }\n\n  if (requireNumbers && minNumbers > 0) {\n    const numberRegex = new RegExp(`(.*[0-9]){${minNumbers},}`);\n\n    fieldSchema = fieldSchema.regex(numberRegex, {\n      message:\n        errorTranslations?.password?.number_required ??\n        (minNumbers === 1\n          ? 'Contain at least one number'\n          : `Contain at least ${minNumbers} numbers`),\n    });\n  } else if (requireNumbers) {\n    fieldSchema = fieldSchema.regex(/[0-9]/, {\n      message: errorTranslations?.password?.number_required ?? 'Contain at least one number',\n    });\n  }\n\n  if (requireSpecialChars && minSpecialChars > 0) {\n    const specialCharRegex = new RegExp(`(.*[^a-zA-Z0-9]){${minSpecialChars},}`);\n\n    fieldSchema = fieldSchema.regex(specialCharRegex, {\n      message:\n        errorTranslations?.password?.special_character_required ??\n        (minSpecialChars === 1\n          ? 'Contain at least one special character'\n          : `Contain at least ${minSpecialChars} special characters`),\n    });\n  } else if (requireSpecialChars) {\n    fieldSchema = fieldSchema.regex(/[^a-zA-Z0-9]/, {\n      message:\n        errorTranslations?.password?.special_character_required ??\n        'Contain at least one special character',\n    });\n  }\n\n  return fieldSchema;\n}\n\nfunction getFieldSchema(\n  field: Field,\n  passwordComplexity?: PasswordComplexitySettings | null,\n  errorTranslations?: FormErrorTranslationMap,\n) {\n  let fieldSchema:\n    | z.ZodString\n    | z.ZodNumber\n    | z.ZodLiteral<'true'>\n    | z.ZodOptional<z.ZodString>\n    | z.ZodOptional<z.ZodNumber>\n    | z.ZodOptional<z.ZodLiteral<'true'>>\n    | z.ZodArray<z.ZodString, 'atleastone' | 'many'>\n    | z.ZodOptional<z.ZodArray<z.ZodString, 'atleastone' | 'many'>>\n    | z.ZodOptional<z.ZodEnum<['true', 'false']>>;\n\n  switch (field.type) {\n    case 'number':\n      fieldSchema = z.number();\n\n      if (field.min != null) fieldSchema = fieldSchema.min(field.min);\n      if (field.max != null) fieldSchema = fieldSchema.max(field.max);\n      if (field.required !== true) fieldSchema = fieldSchema.optional();\n\n      break;\n\n    case 'text':\n      fieldSchema = z.string();\n\n      if (field.pattern != null) {\n        fieldSchema = fieldSchema.regex(new RegExp(field.pattern));\n      }\n\n      if (field.required !== true) fieldSchema = fieldSchema.optional();\n\n      break;\n\n    case 'password': {\n      fieldSchema = getPasswordSchema(passwordComplexity, errorTranslations);\n\n      if (field.required !== true) fieldSchema = fieldSchema.optional();\n\n      break;\n    }\n\n    case 'email':\n      fieldSchema = z.string().email().trim();\n\n      if (field.required !== true) fieldSchema = fieldSchema.optional();\n\n      break;\n\n    case 'checkbox-group':\n      fieldSchema = z.string().array();\n\n      if (field.required === true) fieldSchema = fieldSchema.nonempty();\n\n      break;\n\n    case 'checkbox':\n      if (field.required === true) {\n        fieldSchema = z.literal('true');\n      } else {\n        fieldSchema = z.enum(['true', 'false']).optional();\n      }\n\n      break;\n\n    default:\n      fieldSchema = z.string();\n\n      if (field.required !== true) fieldSchema = fieldSchema.optional();\n  }\n\n  return fieldSchema;\n}\n\nexport function schema(\n  fields: Array<Field | FieldGroup<Field>>,\n  passwordComplexity?: PasswordComplexitySettings | null,\n  errorTranslations?: FormErrorTranslationMap,\n) {\n  const shape: SchemaRawShape = {};\n  let passwordFieldName: string | undefined;\n  let confirmPasswordFieldName: string | undefined;\n\n  fields.forEach((field) => {\n    if (Array.isArray(field)) {\n      field.forEach((f) => {\n        shape[f.name] = getFieldSchema(f, passwordComplexity, errorTranslations);\n\n        if (f.type === 'password') passwordFieldName = f.name;\n        if (f.type === 'confirm-password') confirmPasswordFieldName = f.name;\n      });\n    } else {\n      shape[field.name] = getFieldSchema(field, passwordComplexity, errorTranslations);\n\n      if (field.type === 'password') passwordFieldName = field.name;\n      if (field.type === 'confirm-password') confirmPasswordFieldName = field.name;\n    }\n  });\n\n  return z.object(shape).superRefine((data, ctx) => {\n    if (\n      passwordFieldName != null &&\n      confirmPasswordFieldName != null &&\n      data[passwordFieldName] !== data[confirmPasswordFieldName]\n    ) {\n      ctx.addIssue({\n        code: 'custom',\n        message: errorTranslations?.password?.passwords_must_match ?? 'The passwords do not match',\n        path: [confirmPasswordFieldName],\n      });\n    }\n  });\n}\n"
  },
  {
    "path": "core/vibes/soul/form/dynamic-form/utils.ts",
    "content": "import { Field, FieldGroup } from './schema';\n\nfunction removeOptionsFromField<F extends Field>(field: F) {\n  // Only remove the options property if it exists on the field\n  if ('options' in field) {\n    const { options, ...fieldWithoutOptions } = field;\n\n    return fieldWithoutOptions;\n  }\n\n  return field;\n}\n\nexport function removeOptionsFromFields<F extends Field>(fields: Array<F | FieldGroup<F>>) {\n  return fields.map((field) => {\n    if (Array.isArray(field)) {\n      return field.map(removeOptionsFromField);\n    }\n\n    return removeOptionsFromField(field);\n  });\n}\n"
  },
  {
    "path": "core/vibes/soul/form/field-error/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { CircleAlert } from 'lucide-react';\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *    --field-error: hsl(var(--error));\n *  }\n * ```\n */\nexport function FieldError({\n  className,\n  children,\n  ...rest\n}: React.ComponentPropsWithoutRef<'div'>) {\n  return (\n    <div\n      {...rest}\n      className={clsx(\n        'flex items-center gap-1 text-xs text-[var(--field-error,hsl(var(--error)))]',\n        className,\n      )}\n    >\n      <CircleAlert size={20} strokeWidth={1.5} />\n\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/form/form-status/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { CheckCircle, CircleAlert } from 'lucide-react';\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *    --form-status-light-background-error: color-mix(in oklab, hsl(var(--error)), white 75%);\n *    --form-status-light-text-error: color-mix(in oklab, hsl(var(--error)), black 75%);\n *    --form-status-light-background-success: color-mix(in oklab, hsl(var(--success)), white 75%);\n *    --form-status-light-text-success: color-mix(in oklab, hsl(var(--success)), black 75%);\n *    --form-status-dark-background-error: color-mix(in oklab, hsl(var(--error)), white 75%);\n *    --form-status-dark-text-error: color-mix(in oklab, hsl(var(--error)), black 75%);\n *    --form-status-dark-background-success: color-mix(in oklab, hsl(var(--success)), white 75%);\n *    --form-status-dark-text-success: color-mix(in oklab, hsl(var(--success)), black 75%);\n *  }\n * ```\n */\nexport function FormStatus({\n  className,\n  children,\n  type = 'success',\n  colorScheme = 'light',\n  ...rest\n}: React.ComponentPropsWithoutRef<'div'> & {\n  type?: 'error' | 'success';\n  colorScheme?: 'light' | 'dark';\n}) {\n  return (\n    <div\n      {...rest}\n      className={clsx(\n        'flex items-center gap-3 rounded-xl px-4 py-3 text-sm',\n        {\n          light: {\n            error:\n              'bg-[var(--form-status-light-background-error,color-mix(in_oklab,hsl(var(--error)),white_75%))] text-[var(--form-status-light-text-error,color-mix(in_oklab,hsl(var(--error)),black_75%))]',\n            success:\n              'bg-[var(--form-status-light-background-success,color-mix(in_oklab,hsl(var(--success)),white_75%))] text-[var(--form-status-light-text-success,color-mix(in_oklab,hsl(var(--success)),black_75%))]',\n          }[type],\n          dark: {\n            error:\n              'bg-[var(--form-status-dark-background-error,color-mix(in_oklab,hsl(var(--error)),white_60%))] text-[var(--form-status-dark-text-error,color-mix(in_oklab,hsl(var(--error)),black_60%))]',\n            success:\n              'bg-[var(--form-status-dark-background-success,color-mix(in_oklab,hsl(var(--success)),white_60%))] text-[var(--form-status-dark-text-success,color-mix(in_oklab,hsl(var(--success)),black_60%))]',\n          }[type],\n        }[colorScheme],\n        className,\n      )}\n    >\n      {type === 'error' && <CircleAlert className=\"shrink-0\" size={20} strokeWidth={1.5} />}\n      {type === 'success' && <CheckCircle className=\"shrink-0\" size={20} strokeWidth={1.5} />}\n\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/form/input/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport * as React from 'react';\n\nimport { FieldError } from '@/vibes/soul/form/field-error';\nimport { Label } from '@/vibes/soul/form/label';\n\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *   --input-light-background: hsl(var(--background));\n *   --input-light-text: hsl(var(--foreground));\n *   --input-light-border: hsl(var(--contrast-100));\n *   --input-light-border-error: hsl(var(--error));\n *   --input-light-focus: hsl(var(--foreground));\n *   --input-light-placeholder: hsl(var(--contrast-500));\n *   --input-dark-background: hsl(var(--foreground));\n *   --input-dark-text: hsl(var(--background));\n *   --input-dark-border: hsl(var(--contrast-500));\n *   --input-dark-focus: hsl(var(--background));\n *   --input-dark-placeholder: hsl(var(--contrast-100));\n *   --input-dark-border-error: hsl(var(--error));\n *  }\n * ```\n */\nexport const Input = React.forwardRef<\n  React.ComponentRef<'input'>,\n  React.ComponentPropsWithoutRef<'input'> & {\n    prepend?: React.ReactNode;\n    label?: string;\n    errors?: string[];\n    colorScheme?: 'light' | 'dark';\n  }\n>(({ prepend, label, className, required, errors, colorScheme = 'light', id, ...rest }, ref) => {\n  const generatedId = React.useId();\n\n  return (\n    <div className={clsx('w-full space-y-2', className)}>\n      {label != null && label !== '' && (\n        <Label colorScheme={colorScheme} htmlFor={id ?? generatedId} required={required}>\n          {label}\n        </Label>\n      )}\n      <div\n        className={clsx(\n          'relative overflow-hidden rounded-lg border transition-colors duration-200 focus:outline-none',\n          {\n            light:\n              'bg-[var(--input-light-background,hsl(var(--background)))] focus-within:border-[var(--input-light-focus,hsl(var(--foreground)))]',\n            dark: 'bg-[var(--input-dark-background,hsl(var(--foreground)))] focus-within:border-[var(--input-dark-focus,hsl(var(--background)))]',\n          }[colorScheme],\n          {\n            light:\n              errors && errors.length > 0\n                ? 'border-[var(--input-light-border-error,hsl(var(--error)))]'\n                : 'border-[var(--input-light-border,hsl(var(--contrast-100)))]',\n            dark:\n              errors && errors.length > 0\n                ? 'border-[var(--input-dark-border-error,hsl(var(--error)))]'\n                : 'border-[var(--input-dark-border,hsl(var(--contrast-500)))]',\n          }[colorScheme],\n        )}\n      >\n        {prepend != null && prepend !== '' && (\n          <span className=\"pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2\">\n            {prepend}\n          </span>\n        )}\n        <input\n          {...rest}\n          className={clsx(\n            'w-full px-6 py-3 text-sm [appearance:textfield] placeholder:font-normal focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',\n            {\n              light:\n                'bg-[var(--input-light-background,hsl(var(--background)))] text-[var(--input-light-text,hsl(var(--foreground)))] placeholder:text-[var(--input-light-placeholder,hsl(var(--contrast-500)))]',\n              dark: 'bg-[var(--input-dark-background,hsl(var(--foreground)))] text-[var(--input-dark-text,hsl(var(--background)))] placeholder:text-[var(--input-dark-placeholder,hsl(var(--contrast-100)))]',\n            }[colorScheme],\n            { 'py-2.5 pe-4 ps-12': prepend },\n          )}\n          id={id ?? generatedId}\n          ref={ref}\n          required={required}\n        />\n      </div>\n      {errors?.map((error) => (\n        <FieldError key={error}>{error}</FieldError>\n      ))}\n    </div>\n  );\n});\n\nInput.displayName = 'Input';\n"
  },
  {
    "path": "core/vibes/soul/form/label/index.tsx",
    "content": "'use client';\n\nimport * as LabelPrimitive from '@radix-ui/react-label';\nimport { clsx } from 'clsx';\nimport { useTranslations } from 'next-intl';\nimport { ComponentPropsWithoutRef } from 'react';\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *    --label-light-text: hsl(var(--contrast-500));\n *    --label-dark-text: hsl(var(--contrast-100));\n *  }\n * ```\n */\nexport function Label({\n  className,\n  colorScheme = 'light',\n  required,\n  children,\n  ...rest\n}: ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & {\n  colorScheme?: 'light' | 'dark';\n  required?: boolean;\n}) {\n  const t = useTranslations('Form');\n\n  return (\n    <LabelPrimitive.Root\n      {...rest}\n      className={clsx(\n        'block font-mono text-xs uppercase',\n        {\n          light: 'text-[var(--label-light-text,hsl(var(--contrast-500)))]',\n          dark: 'text-[var(--label-dark-text,hsl(var(--contrast-100)))]',\n        }[colorScheme],\n        className,\n      )}\n    >\n      {children}\n      {!required && <span className=\"ml-1 normal-case\">({t('optional')})</span>}\n    </LabelPrimitive.Root>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/form/number-input/index.tsx",
    "content": "'use client';\n\nimport { clsx } from 'clsx';\nimport { Minus, Plus } from 'lucide-react';\nimport * as React from 'react';\n\nimport { FieldError } from '@/vibes/soul/form/field-error';\nimport { Label } from '@/vibes/soul/form/label';\n\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *   --number-input-focus: hsl(var(--primary));\n *   --number-input-light-background: hsl(var(--background));\n *   --number-input-light-text: hsl(var(--foreground));\n *   --number-input-light-icon: hsl(var(--contrast-300));\n *   --number-input-light-icon-hover: hsl(var(--foreground));\n *   --number-input-light-button-background: hsl(var(--background));\n *   --number-input-light-button-background-hover: hsl(var(--contrast-100) / 50%);\n *   --number-input-light-border: hsl(var(--contrast-100));\n *   --number-input-light-border-error: hsl(var(--error));\n *   --number-input-dark-background: hsl(var(--background));\n *   --number-input-dark-text: hsl(var(--background));\n *   --number-input-dark-icon: hsl(var(--contrast-300));\n *   --number-input-dark-icon-hover: hsl(var(--background));\n *   --number-input-dark-button-background: hsl(var(--foreground));\n *   --number-input-dark-button-background-hover: hsl(var(--contrast-500) / 50%);\n *   --number-input-dark-border: hsl(var(--contrast-500));\n *   --number-input-dark-border-error: hsl(var(--error));\n *  }\n * ```\n */\nexport const NumberInput = React.forwardRef<\n  React.ComponentRef<'input'>,\n  Omit<React.ComponentPropsWithoutRef<'input'>, 'id'> & {\n    label?: string;\n    errors?: string[];\n    decrementLabel?: string;\n    incrementLabel?: string;\n    colorScheme?: 'light' | 'dark';\n  }\n>(\n  (\n    {\n      label,\n      className,\n      required,\n      errors,\n      decrementLabel,\n      incrementLabel,\n      disabled = false,\n      colorScheme = 'light',\n      ...rest\n    },\n    ref,\n  ) => {\n    const id = React.useId();\n\n    return (\n      <div className={clsx('space-y-2', className)}>\n        {label != null && label !== '' && (\n          <Label colorScheme={colorScheme} htmlFor={id} required={required}>\n            {label}\n          </Label>\n        )}\n        <div\n          className={clsx(\n            'inline-flex items-center rounded-lg border',\n            {\n              light: 'bg-[var(--number-input-light-background,hsl(var(--background)))]',\n              dark: 'bg-[var(--number-input-dark-background,hsl(var(--foreground)))]',\n            }[colorScheme],\n            {\n              light:\n                errors && errors.length > 0\n                  ? 'border-[var(--number-input-light-border-error,hsl(var(--error)))]'\n                  : 'border-[var(--number-input-light-border,hsl(var(--contrast-100)))]',\n              dark:\n                errors && errors.length > 0\n                  ? 'border-[var(--number-input-dark-border-error,hsl(var(--error)))]'\n                  : 'border-[var(--number-input-dark-border,hsl(var(--contrast-500)))]',\n            }[colorScheme],\n          )}\n        >\n          <button\n            aria-label={decrementLabel}\n            className={clsx(\n              'group rounded-l-lg p-3.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--number-input-focus,hsl(var(--primary)))] disabled:cursor-not-allowed disabled:opacity-30',\n              {\n                light:\n                  'bg-[var(--number-input-light-button-background,hsl(var(--background)))] hover:bg-[var(--number-input-light-button-background-hover,hsl(var(--contrast-100)/50%))]',\n                dark: 'bg-[var(--number-input-dark-button-background,hsl(var(--foreground)))] hover:bg-[var(--number-input-dark-button-background-hover,hsl(var(--contrast-500)/50%))]',\n              }[colorScheme],\n            )}\n            disabled={disabled}\n            onClick={(e) => {\n              e.preventDefault();\n\n              const input = e.currentTarget.parentElement?.querySelector('input');\n\n              input?.stepDown();\n              input?.dispatchEvent(new InputEvent('change', { bubbles: true, cancelable: true }));\n            }}\n          >\n            <Minus\n              className={clsx(\n                'transition-colors duration-300',\n                {\n                  light:\n                    'text-[var(--number-input-light-icon,hsl(var(--contrast-300)))] group-hover:text-[var(--number-input-light-icon-hover,hsl(var(--foreground)))]',\n                  dark: 'text-[var(--number-input-dark-icon,hsl(var(--contrast-300)))] group-hover:text-[var(--number-input-dark-icon-hover,hsl(var(--background)))]',\n                }[colorScheme],\n              )}\n              size={18}\n              strokeWidth={1.5}\n            />\n          </button>\n          <input\n            {...rest}\n            className={clsx(\n              'w-8 flex-1 select-none justify-center bg-transparent text-center [appearance:textfield] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-30 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',\n              {\n                light: 'text-[var(--number-input-light-text,hsl(var(--foreground)))]',\n                dark: 'text-[var(--number-input-dark-text,hsl(var(--background)))]',\n              }[colorScheme],\n            )}\n            disabled={disabled}\n            id={id}\n            ref={ref}\n            required={required}\n            type=\"number\"\n          />\n          <button\n            aria-label={incrementLabel}\n            className={clsx(\n              'group rounded-r-lg p-3.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--number-input-focus,hsl(var(--primary)))] disabled:cursor-not-allowed disabled:opacity-30',\n              {\n                light:\n                  'bg-[var(--number-input-light-button-background,hsl(var(--background)))] hover:bg-[var(--number-input-light-button-background-hover,hsl(var(--contrast-100)/50%))]',\n                dark: 'bg-[var(--number-input-dark-button-background,hsl(var(--foreground)))] hover:bg-[var(--number-input-dark-button-background-hover,hsl(var(--contrast-500)/50%))]',\n              }[colorScheme],\n            )}\n            disabled={disabled}\n            onClick={(e) => {\n              e.preventDefault();\n\n              const input = e.currentTarget.parentElement?.querySelector('input');\n\n              input?.stepUp();\n              input?.dispatchEvent(new InputEvent('change', { bubbles: true, cancelable: true }));\n            }}\n          >\n            <Plus\n              className={clsx(\n                'transition-colors duration-300',\n                {\n                  light:\n                    'text-[var(--number-input-light-icon,hsl(var(--contrast-300)))] group-hover:text-[var(--number-input-light-icon-hover,hsl(var(--foreground)))]',\n                  dark: 'text-[var(--number-input-dark-icon,hsl(var(--contrast-300)))] group-hover:text-[var(--number-input-dark-icon-hover,hsl(var(--background)))]',\n                }[colorScheme],\n              )}\n              size={18}\n              strokeWidth={1.5}\n            />\n          </button>\n        </div>\n        {errors?.map((error) => (\n          <FieldError key={error}>{error}</FieldError>\n        ))}\n      </div>\n    );\n  },\n);\n\nNumberInput.displayName = 'NumberInput';\n"
  },
  {
    "path": "core/vibes/soul/form/radio-group/index.tsx",
    "content": "import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';\nimport { clsx } from 'clsx';\nimport * as React from 'react';\n\nimport { FieldError } from '@/vibes/soul/form/field-error';\nimport { Label } from '@/vibes/soul/form/label';\n\ninterface Option {\n  value: string;\n  label: string;\n  description?: string;\n  disabled?: boolean;\n}\n\nexport const RadioGroup = React.forwardRef<\n  React.ComponentRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> & {\n    label?: string;\n    options: Option[];\n    errors?: string[];\n    colorScheme?: 'light' | 'dark';\n    onOptionMouseEnter?: (value: string) => void;\n  }\n>(\n  (\n    {\n      label,\n      options,\n      errors,\n      className,\n      onOptionMouseEnter,\n      colorScheme = 'light',\n      required,\n      ...rest\n    },\n    ref,\n  ) => {\n    const id = React.useId();\n\n    return (\n      <div className={clsx('space-y-2', className)}>\n        {label !== undefined && label !== '' && (\n          <Label colorScheme={colorScheme} id={id} required={required}>\n            {label}\n          </Label>\n        )}\n        <RadioGroupPrimitive.Root\n          {...rest}\n          aria-labelledby={id}\n          className=\"space-y-2\"\n          ref={ref}\n          required={required}\n        >\n          {options.map((option, index) => (\n            <RadioGroupItem\n              colorScheme={colorScheme}\n              error={errors != null && errors.length > 0}\n              key={index}\n              onOptionMouseEnter={onOptionMouseEnter}\n              option={option}\n            />\n          ))}\n        </RadioGroupPrimitive.Root>\n        {errors?.map((error) => (\n          <FieldError key={error}>{error}</FieldError>\n        ))}\n      </div>\n    );\n  },\n);\n\nRadioGroup.displayName = 'RadioGroup';\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *   --radio-group-light-background: hsl(var(--background));\n *   --radio-group-light-border: hsl(var(--contrast-200));\n *   --radio-group-light-border-error: hsl(var(--error));\n *   --radio-group-light-disabled-border-error: hsl(var(--error) / 50%);\n *   --radio-group-light-border-hover: hsl(var(--contrast-300));\n *   --radio-group-light-border-focus: hsl(var(--contrast-300));\n *   --radio-group-light-indicator-background: hsl(var(--foreground));\n *   --radio-group-light-label: hsl(var(--foreground));\n *   --radio-group-dark-background: hsl(var(--foreground));\n *   --radio-group-dark-border: hsl(var(--contrast-400));\n *   --radio-group-dark-border-error: hsl(var(--error));\n *   --radio-group-dark-disabled-border-error: hsl(var(--error) / 50%);\n *   --radio-group-dark-border-hover: hsl(var(--contrast-300));\n *   --radio-group-dark-border-focus: hsl(var(--contrast-300));\n *   --radio-group-dark-indicator-background: hsl(var(--background));\n *   --radio-group-dark-label: hsl(var(--background));\n *  }\n * ```\n */\nfunction RadioGroupItem({\n  option,\n  error = false,\n  colorScheme = 'light',\n  onOptionMouseEnter,\n}: {\n  option: Option;\n  error?: boolean;\n  colorScheme?: 'light' | 'dark';\n  onOptionMouseEnter?: (value: string) => void;\n}) {\n  const id = React.useId();\n\n  return (\n    <div className=\"flex items-center\" key={option.value}>\n      <RadioGroupPrimitive.Item\n        aria-labelledby={\n          option.description !== undefined ? `${id}-label ${id}-description` : `${id}-label`\n        }\n        className={clsx(\n          'size-5 cursor-default rounded-full border outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&:disabled+label]:pointer-events-none [&:disabled+label]:opacity-50',\n          {\n            light: 'bg-[var(--radio-group-light-background,hsl(var(--background)))]',\n            dark: 'bg-[var(--radio-group-dark-background,hsl(var(--foreground)))]',\n          }[colorScheme],\n          {\n            light: error\n              ? 'border-[var(--radio-group-light-border-error,hsl(var(--error)))] disabled:border-[var(--radio-group-light-disabled-border-error,hsl(var(--error)/50%))]'\n              : 'border-[var(--radio-group-light-border,hsl(var(--contrast-200)))] hover:border-[var(--radio-group-light-border-hover,hsl(var(--contrast-300)))] focus:border-[var(--radio-group-light-border-focus,hsl(var(--contrast-300)))]',\n            dark: error\n              ? 'border-[var(--radio-group-dark-border-error,hsl(var(--error)))] disabled:border-[var(--radio-group-dark-disabled-border-error,hsl(var(--error)/50%))]'\n              : 'border-[var(--radio-group-dark-border,hsl(var(--contrast-400)))] hover:border-[var(--radio-group-dark-border-hover,hsl(var(--contrast-300)))] focus:border-[var(--radio-group-light-border-focus,hsl(var(--contrast-300)))]',\n          }[colorScheme],\n        )}\n        disabled={option.disabled}\n        id={id}\n        onMouseEnter={() => {\n          onOptionMouseEnter?.(option.value);\n        }}\n        value={option.value}\n      >\n        <RadioGroupPrimitive.Indicator\n          className={clsx(\n            'relative flex size-full items-center justify-center after:block after:size-3 after:rounded-full',\n            {\n              light:\n                'after:bg-[var(--radio-group-light-indicator-background,hsl(var(--foreground)))]',\n              dark: 'after:bg-[var(--radio-group-dark-indicator-background,hsl(var(--background)))]',\n            }[colorScheme],\n          )}\n        />\n      </RadioGroupPrimitive.Item>\n      <label\n        className={clsx(\n          'flex grow justify-between pl-3 text-sm leading-none',\n          {\n            light: 'text-[var(--radio-group-light-label,hsl(var(--foreground)))]',\n            dark: 'text-[var(--radio-group-dark-label,hsl(var(--background)))]',\n          }[colorScheme],\n        )}\n        htmlFor={id}\n      >\n        <span id={`${id}-label`}>{option.label}</span>\n        {option.description !== undefined && (\n          <span id={`${id}-description`}>{option.description}</span>\n        )}\n      </label>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/form/range-input/index.tsx",
    "content": "'use client';\n\nimport { ArrowRight } from 'lucide-react';\nimport { useEffect, useState } from 'react';\n\nimport { Input } from '@/vibes/soul/form/input';\nimport { Button } from '@/vibes/soul/primitives/button';\n\ninterface Props {\n  applyLabel?: string;\n  colorScheme?: 'light' | 'dark';\n  disabled?: boolean;\n  max?: number;\n  maxLabel?: string;\n  maxName?: string;\n  maxPlaceholder?: string;\n  maxPrepend?: React.ReactNode;\n  maxStep?: number;\n  min?: number;\n  minLabel?: string;\n  minName?: string;\n  minPlaceholder?: string;\n  minPrepend?: React.ReactNode;\n  minStep?: number;\n  onChange?: (value: { min: number | null; max: number | null }) => void;\n  value?: { min: number | null; max: number | null };\n}\n\nconst clamp = (value: number, min: number | null, max?: number | null) =>\n  Math.min(Math.max(value, min ?? -Infinity), max ?? Infinity);\n\nexport function RangeInput({\n  applyLabel = 'Apply',\n  colorScheme = 'light',\n  disabled = false,\n  max,\n  maxLabel,\n  maxName = 'max',\n  maxPlaceholder = 'Max',\n  maxPrepend,\n  maxStep,\n  min,\n  minLabel,\n  minName = 'min',\n  minPlaceholder = 'Min',\n  minPrepend,\n  minStep,\n  onChange,\n  value,\n}: Props) {\n  const [state, setState] = useState({\n    min: value?.min?.toString() ?? '',\n    max: value?.max?.toString() ?? '',\n  });\n\n  useEffect(() => {\n    setState({ min: value?.min?.toString() ?? '', max: value?.max?.toString() ?? '' });\n  }, [value]);\n\n  const parsedMinState = parseInt(state.min, 10);\n  const parsedMaxState = parseInt(state.max, 10);\n  const minStateAsNumber = Number.isNaN(parsedMinState) ? null : parsedMinState;\n  const maxStateAsNumber = Number.isNaN(parsedMaxState) ? null : parsedMaxState;\n\n  return (\n    <div className=\"flex w-full items-center gap-2\">\n      <Input\n        className=\"flex-1\"\n        colorScheme={colorScheme}\n        disabled={disabled}\n        label={minLabel}\n        max={maxStateAsNumber ?? max}\n        min={min}\n        name={minName}\n        onBlur={(e) => {\n          const clamped = clamp(\n            e.currentTarget.valueAsNumber,\n            min ?? null,\n            e.currentTarget.max === '' ? null : parseInt(e.currentTarget.max, 10),\n          );\n          const nextValue = Number.isNaN(clamped) ? null : clamped;\n\n          setState((prev) => ({ ...prev, min: nextValue?.toString() ?? '' }));\n        }}\n        onChange={(e) => {\n          const nextValue = e.currentTarget.value;\n\n          setState((prev) => ({ ...prev, min: nextValue }));\n        }}\n        placeholder={minPlaceholder}\n        prepend={minPrepend}\n        step={minStep}\n        type=\"number\"\n        value={state.min}\n      />\n      <Input\n        className=\"flex-1\"\n        colorScheme={colorScheme}\n        disabled={disabled}\n        label={maxLabel}\n        max={max}\n        min={minStateAsNumber ?? min}\n        name={maxName}\n        onBlur={(e) => {\n          const clamped = clamp(\n            e.currentTarget.valueAsNumber,\n            e.currentTarget.min === '' ? null : parseInt(e.currentTarget.min, 10),\n            max,\n          );\n          const nextValue = Number.isNaN(clamped) ? null : clamped;\n\n          setState((prev) => ({ ...prev, max: nextValue?.toString() ?? '' }));\n        }}\n        onChange={(e) => {\n          const nextValue = e.currentTarget.value;\n\n          setState((prev) => ({ ...prev, max: nextValue }));\n        }}\n        placeholder={maxPlaceholder}\n        prepend={maxPrepend}\n        step={maxStep}\n        type=\"number\"\n        value={state.max}\n      />\n      <Button\n        className=\"shrink-0\"\n        disabled={disabled || (state.min === state.max && state.min !== '' && state.max !== '')}\n        onClick={() =>\n          onChange?.({\n            min: state.min === '' ? null : Number(state.min),\n            max: state.max === '' ? null : Number(state.max),\n          })\n        }\n        shape=\"circle\"\n        size=\"small\"\n        variant=\"secondary\"\n      >\n        <span className=\"sr-only\">{applyLabel}</span>\n        <ArrowRight size={20} strokeWidth={1} />\n      </Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/form/rating-radio-group/index.tsx",
    "content": "import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';\nimport { clsx } from 'clsx';\nimport * as React from 'react';\n\nimport { FieldError } from '@/vibes/soul/form/field-error';\nimport { Label } from '@/vibes/soul/form/label';\nimport { Star } from '@/vibes/soul/primitives/rating';\n\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *   --rating-radio-group-focus: hsl(var(--primary));\n *   --rating-radio-group-light-star-empty: hsl(var(--contrast-200));\n *   --rating-radio-group-light-star-filled: hsl(var(--foreground));\n *   --rating-radio-group-light-star-hover: hsl(var(--contrast-300));\n *   --rating-radio-group-dark-star-empty: hsl(var(--contrast-400));\n *   --rating-radio-group-dark-star-filled: hsl(var(--background));\n *   --rating-radio-group-dark-star-hover: hsl(var(--contrast-300));\n *  }\n * ```\n */\nexport const RatingRadioGroup = React.forwardRef<\n  React.ComponentRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> & {\n    label: string;\n    max?: number;\n    errors?: string[];\n    onOptionMouseEnter?: (value: string) => void;\n    onOptionMouseLeave?: () => void;\n    colorScheme?: 'light' | 'dark';\n  }\n>(\n  (\n    {\n      label,\n      max = 5,\n      errors,\n      className,\n      onOptionMouseEnter,\n      onOptionMouseLeave,\n      colorScheme = 'light',\n      required,\n      ...rest\n    },\n    ref,\n  ) => {\n    const groupId = React.useId();\n    const [previewValue, setPreviewValue] = React.useState<string | null>(null);\n    const isMouseDownRef = React.useRef(false);\n\n    const currentValue = rest.value?.toString() ?? '0';\n    const displayRating = parseInt(previewValue ?? currentValue, 10) || 0;\n\n    const handleMouseLeave = () => {\n      setPreviewValue(null);\n      onOptionMouseLeave?.();\n    };\n\n    const handleMouseDown = () => {\n      isMouseDownRef.current = true;\n    };\n\n    const handleMouseUp = () => {\n      isMouseDownRef.current = false;\n    };\n\n    const handleBlur = () => {\n      if (!isMouseDownRef.current) {\n        setPreviewValue(null);\n      }\n    };\n\n    return (\n      <div className={clsx('rating-radio-group space-y-2', className)}>\n        <Label colorScheme={colorScheme} id={groupId} required={required}>\n          {label}\n        </Label>\n\n        <RadioGroupPrimitive.Root\n          {...rest}\n          aria-labelledby={groupId}\n          className=\"flex items-center gap-1\"\n          onMouseDown={handleMouseDown}\n          onMouseLeave={handleMouseLeave}\n          onMouseUp={handleMouseUp}\n          ref={ref}\n          required={required}\n        >\n          <div className=\"flex items-center gap-1\">\n            {Array.from({ length: max }, (_, i) => {\n              const ratingValue = i + 1;\n              const filled = displayRating >= ratingValue;\n              const valueStr = ratingValue.toString();\n              const itemId = `${groupId}-${ratingValue}`;\n\n              return (\n                <div className=\"relative\" key={ratingValue}>\n                  <RadioGroupPrimitive.Item\n                    className={clsx(\n                      'peer sr-only',\n                      'transition-colors focus-visible:outline-0 focus-visible:ring-2 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n                      {\n                        light:\n                          'focus-visible:ring-[var(--rating-radio-group-focus,hsl(var(--primary)))]',\n                        dark: 'focus-visible:ring-[var(--rating-radio-group-focus,hsl(var(--primary)))]',\n                      }[colorScheme],\n                    )}\n                    id={itemId}\n                    onBlur={handleBlur}\n                    value={valueStr}\n                  />\n                  <label\n                    aria-label={`${ratingValue} ${ratingValue === 1 ? 'star' : 'stars'}`}\n                    className=\"flex shrink-0 cursor-pointer rounded-full transition-colors focus-visible:outline-0 focus-visible:ring-2 peer-focus-visible:ring-2 peer-focus-visible:ring-[var(--rating-radio-group-focus,hsl(var(--primary)))]\"\n                    htmlFor={itemId}\n                    onMouseEnter={() => {\n                      setPreviewValue(valueStr);\n                      onOptionMouseEnter?.(valueStr);\n                    }}\n                  >\n                    <Star type={filled ? 'full' : 'empty'} />\n                  </label>\n                </div>\n              );\n            })}\n          </div>\n        </RadioGroupPrimitive.Root>\n        {errors?.map((error) => (\n          <FieldError key={error}>{error}</FieldError>\n        ))}\n      </div>\n    );\n  },\n);\n\nRatingRadioGroup.displayName = 'RatingRadioGroup';\n"
  },
  {
    "path": "core/vibes/soul/form/select/index.tsx",
    "content": "'use client';\n\nimport * as SelectPrimitive from '@radix-ui/react-select';\nimport { clsx } from 'clsx';\nimport { ChevronDown, ChevronUp } from 'lucide-react';\nimport * as React from 'react';\n\nimport { FieldError } from '@/vibes/soul/form/field-error';\nimport { Label } from '@/vibes/soul/form/label';\n\nexport type Props = {\n  colorScheme?: 'light' | 'dark';\n  id?: string;\n  name: string;\n  pending?: boolean;\n  placeholder?: string;\n  label?: string;\n  hideLabel?: boolean;\n  variant?: 'round' | 'rectangle';\n  options: Array<{ label: string; value: string }>;\n  className?: string;\n  errors?: string[];\n  onFocus?: (e: React.FocusEvent<HTMLButtonElement>) => void;\n  onBlur?: (e: React.FocusEvent<HTMLButtonElement>) => void;\n  onOptionMouseEnter?: (value: string) => void;\n} & React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root>;\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *    --select-light-trigger-background: var(--background);\n *    --select-light-trigger-border: var(--contrast-100);\n *    --select-light-trigger-border-hover: var(--contrast-300);\n *    --select-light-trigger-border-error: var(--error);\n *    --select-light-trigger-text: var(--foreground);\n *    --select-light-trigger-focus: var(--primary);\n *    --select-light-icon: var(--foreground);\n *    --select-light-content-background: var(--background);\n *    --select-light-content-border: color-mix(in oklab, var(--foreground) 10%, transparent);\n *    --select-light-item-background-hover: var(--contrast-100);\n *    --select-light-item-background-focus: var(--contrast-100);\n *    --select-light-item-text: var(--contrast-400);\n *    --select-light-item-text-hover: var(--foreground);\n *    --select-light-item-text-focus: var(--foreground);\n *    --select-light-item-checked-text-focus: var(--foreground);\n *    --select-dark-trigger-background: var(--foreground);\n *    --select-dark-trigger-border: var(--contrast-500);\n *    --select-dark-trigger-border-hover: var(--contrast-300);\n *    --select-dark-trigger-border-error: var(--error);\n *    --select-dark-trigger-text: var(--background);\n *    --select-dark-trigger-focus: var(--primary);\n *    --select-dark-icon: var(--background);\n *    --select-dark-content-background: var(--foreground);\n *    --select-dark-content-border: color-mix(in oklab, var(--background) 10%, transparent);\n *    --select-dark-item-background-hover: var(--contrast-500);\n *    --select-dark-item-background-focus: var(--contrast-500);\n *    --select-dark-item-text: var(--contrast-200);\n *    --select-dark-item-text-hover: var(--background);\n *    --select-dark-item-text-focus: var(--background);\n *    --select-dark-item-checked-text-focus: var(--background);\n *  }\n * ```\n */\nexport function Select({\n  colorScheme = 'light',\n  label,\n  hideLabel = false,\n  name,\n  pending = false,\n  placeholder = 'Select an item',\n  variant = 'rectangle',\n  options,\n  className,\n  errors,\n  onFocus,\n  onBlur,\n  onOptionMouseEnter,\n  value,\n  required,\n  ...rest\n}: Props) {\n  const id = React.useId();\n\n  return (\n    <div className={clsx('w-full', className)}>\n      {label !== undefined && label !== '' && (\n        <Label\n          className={clsx(hideLabel && 'sr-only', 'mb-2')}\n          colorScheme={colorScheme}\n          htmlFor={id}\n          required={required}\n        >\n          {label}\n        </Label>\n      )}\n      {/* Workaround for https://github.com/radix-ui/primitives/issues/3198, remove when fixed */}\n      <input name={name} type=\"hidden\" value={value} />\n      <SelectPrimitive.Root {...rest} name={`${name}_display`} required={required} value={value}>\n        <SelectPrimitive.Trigger\n          aria-label={label}\n          className={clsx(\n            'flex h-fit w-full select-none items-center justify-between gap-3 border p-2 px-5 py-3 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2',\n            variant === 'rectangle' ? 'rounded-lg' : 'rounded-full',\n            {\n              light:\n                'bg-[var(--select-light-trigger-background,hsl(var(--white)))] text-[var(--select-light-trigger-text,hsl(var(--foreground)))] hover:border-[var(--select-light-trigger-border-hover,hsl(var(--contrast-300)))] hover:bg-[var(--select-light-trigger-background-hover,hsl(var(--contrast-100)))] focus-visible:ring-[var(--select-light-trigger-focus,hsl(var(--primary)))]',\n              dark: 'bg-[var(--select-dark-trigger-background,hsl(var(--black)))] text-[var(--select-dark-trigger-text,hsl(var(--background)))] hover:border-[var(--select-dark-trigger-border-hover,hsl(var(--contrast-300)))] hover:bg-[var(--select-dark-trigger-background-hover,hsl(var(--contrast-500)))] focus-visible:ring-[var(--select-dark-trigger-focus,hsl(var(--primary)))]',\n            }[colorScheme],\n            {\n              light:\n                errors && errors.length > 0\n                  ? 'border-[var(--select-light-trigger-border-error,hsl(var(--error)))]'\n                  : 'border-[var(--select-light-trigger-border,hsl(var(--contrast-100)))]',\n              dark:\n                errors && errors.length > 0\n                  ? 'border-[var(--select-dark-trigger-border-error,hsl(var(--error)))]'\n                  : 'border-[var(--select-dark-trigger-border,hsl(var(--contrast-500)))]',\n            }[colorScheme],\n          )}\n          data-pending={pending ? true : null}\n          id={id}\n          onBlur={onBlur}\n          onFocus={onFocus}\n        >\n          <SelectPrimitive.Value placeholder={placeholder} />\n          <SelectPrimitive.Icon asChild>\n            <ChevronDown\n              className={clsx(\n                'w-5 transition-transform',\n                {\n                  light: 'text-(--select-light-icon,var(--foreground))',\n                  dark: 'text-(--select-dark-icon,var(--background))',\n                }[colorScheme],\n              )}\n              strokeWidth={1.5}\n            />\n          </SelectPrimitive.Icon>\n        </SelectPrimitive.Trigger>\n        <SelectPrimitive.Portal>\n          <SelectPrimitive.Content\n            className={clsx(\n              'z-50 max-h-80 w-full overflow-y-auto rounded-xl p-2 shadow-xl ring-1 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 @4xl:rounded-3xl @4xl:p-4',\n              {\n                light:\n                  'bg-[var(--select-light-content-background,hsl(var(--background)))] ring-[var(--select-light-content-border,hsl(var(--contrast-100)))]',\n                dark: 'bg-[var(--select-dark-content-background,hsl(var(--foreground)))] ring-[var(--select-dark-content-border,hsl(var(--contrast-500)))]',\n              }[colorScheme],\n            )}\n          >\n            <SelectPrimitive.ScrollUpButton className=\"flex w-full cursor-default items-center justify-center py-3\">\n              <ChevronUp\n                className={clsx(\n                  'w-5',\n                  {\n                    light: 'text-(--select-light-icon,var(--foreground))',\n                    dark: 'text-(--select-dark-icon,var(--background))',\n                  }[colorScheme],\n                )}\n                strokeWidth={1.5}\n              />\n            </SelectPrimitive.ScrollUpButton>\n            <SelectPrimitive.Viewport>\n              {options.map((option) => (\n                <SelectPrimitive.Item\n                  className={clsx(\n                    'w-full cursor-default select-none rounded-xl px-3 py-2 text-sm font-medium outline-none transition-colors @4xl:text-base',\n                    {\n                      light:\n                        'text-[var(--select-light-item-text,hsl(var(--contrast-400)))] hover:bg-[var(--select-light-item-background-hover,hsl(var(--contrast-100)))] hover:text-[var(--select-light-item-text-hover,hsl(var(--foreground)))] focus-visible:bg-[var(--select-light-item-background-focus,hsl(var(--contrast-100)))] focus-visible:text-[var(--select-light-item-text-focus,hsl(var(--foreground)))] data-[state=checked]:text-[var(--select-light-item-checked-text-focus,hsl(var(--foreground)))]',\n                      dark: 'text-[var(--select-dark-item-text,hsl(var(--contrast-200)))] hover:bg-[var(--select-dark-item-background-hover,hsl(var(--contrast-500)))] hover:text-[var(--select-dark-item-text-hover,hsl(var(--background)))] focus-visible:bg-[var(--select-dark-item-background-focus,hsl(var(--contrast-500)))] focus-visible:text-[var(--select-dark-item-text-focus,hsl(var(--background)))] data-[state=checked]:text-[var(--select-dark-item-checked-text-focus,hsl(var(--background)))]',\n                    }[colorScheme],\n                  )}\n                  key={option.value}\n                  onMouseEnter={() => {\n                    onOptionMouseEnter?.(option.value);\n                  }}\n                  value={option.value}\n                >\n                  <SelectPrimitive.ItemText>{option.label}</SelectPrimitive.ItemText>\n                </SelectPrimitive.Item>\n              ))}\n            </SelectPrimitive.Viewport>\n            <SelectPrimitive.ScrollDownButton className=\"flex w-full cursor-default items-center justify-center py-3\">\n              <ChevronDown\n                className={clsx(\n                  'w-5',\n                  {\n                    light: 'text-(--select-icon,var(--foreground))',\n                    dark: 'text-(--select-icon,var(--background))',\n                  }[colorScheme],\n                )}\n                strokeWidth={1.5}\n              />\n            </SelectPrimitive.ScrollDownButton>\n          </SelectPrimitive.Content>\n        </SelectPrimitive.Portal>\n      </SelectPrimitive.Root>\n      {errors?.map((error) => (\n        <FieldError className=\"mt-2\" key={error}>\n          {error}\n        </FieldError>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/form/swatch-radio-group/index.tsx",
    "content": "import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';\nimport { clsx } from 'clsx';\nimport { X } from 'lucide-react';\nimport * as React from 'react';\n\nimport { FieldError } from '@/vibes/soul/form/field-error';\nimport { Label } from '@/vibes/soul/form/label';\nimport { Image } from '~/components/image';\n\ntype SwatchOption =\n  | {\n      type: 'color';\n      value: string;\n      label: string;\n      color: string;\n      disabled?: boolean;\n    }\n  | {\n      type: 'image';\n      value: string;\n      label: string;\n      image: { src: string; alt: string };\n      disabled?: boolean;\n    };\n\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *    --swatch-radio-group-focus: hsl(var(--primary));\n *    --swatch-radio-group-light-icon: hsl(var(--foreground));\n *    --swatch-radio-group-light-unchecked-border: transparent;\n *    --swatch-radio-group-light-unchecked-border-hover: hsl(var(--contrast-200));\n *    --swatch-radio-group-light-disabled-border: transparent;\n *    --swatch-radio-group-light-border-error: hsl(var(--error));\n *    --swatch-radio-group-light-checked-border: hsl(var(--foreground));\n *    --swatch-radio-group-light-option-border: hsl(var(--foreground) / 10%);\n *    --swatch-radio-group-dark-icon: hsl(var(--background));\n *    --swatch-radio-group-dark-unchecked-border: transparent;\n *    --swatch-radio-group-dark-unchecked-border-hover: hsl(var(--contrast-400));\n *    --swatch-radio-group-dark-disabled-border: transparent;\n *    --swatch-radio-group-dark-border-error: hsl(var(--error));\n *    --swatch-radio-group-dark-checked-border: hsl(var(--background));\n *    --swatch-radio-group-dark-option-border: hsl(var(--background) / 10%);\n *  }\n * ```\n */\nexport const SwatchRadioGroup = React.forwardRef<\n  React.ComponentRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> & {\n    label?: string;\n    options: SwatchOption[];\n    errors?: string[];\n    colorScheme?: 'light' | 'dark';\n    onOptionMouseEnter?: (value: string) => void;\n  }\n>(\n  (\n    {\n      label,\n      options,\n      errors,\n      className,\n      colorScheme = 'light',\n      onOptionMouseEnter,\n      required,\n      ...rest\n    },\n    ref,\n  ) => {\n    const id = React.useId();\n\n    return (\n      <div className={clsx('space-y-2', className)}>\n        {label !== undefined && label !== '' && (\n          <Label colorScheme={colorScheme} id={id} required={required}>\n            {label}\n          </Label>\n        )}\n        <RadioGroupPrimitive.Root\n          {...rest}\n          aria-labelledby={id}\n          className=\"flex flex-wrap gap-1\"\n          ref={ref}\n          required={required}\n        >\n          {options.map((option) => (\n            <RadioGroupPrimitive.Item\n              aria-label={option.label}\n              className={clsx(\n                'group relative box-content h-8 w-8 rounded-full border p-0.5 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--swatch-radio-group-focus,hsl(var(--primary)))] data-[disabled]:pointer-events-none [&:disabled>.disabled-icon]:grid',\n                {\n                  light:\n                    'hover:border-[var(--swatch-radio-group-light-unchecked-border-hover,hsl(var(--contrast-200)))] data-[state=checked]:border-[var(--swatch-radio-group-light-checked-border,hsl(var(--foreground)))]',\n                  dark: 'hover:border-[var(--swatch-radio-group-dark-unchecked-border-hover,hsl(var(--contrast-400)))] data-[state=checked]:border-[var(--swatch-radio-group-dark-checked-border,hsl(var(--background)))]',\n                }[colorScheme],\n                {\n                  light:\n                    errors && errors.length > 0\n                      ? 'border-[var(--swatch-radio-group-light-border-error,hsl(var(--error)))] disabled:border-[var(--swatch-radio-group-light-disabled-border,transparent)]'\n                      : 'border-[var(--swatch-radio-group-light-unchecked-border,transparent)]',\n                  dark:\n                    errors && errors.length > 0\n                      ? 'border-[var(--swatch-radio-group-dark-border-error,hsl(var(--error)))] disabled:border-[var(--swatch-radio-group-dark-disabled-border,transparent)]'\n                      : 'border-[var(--swatch-radio-group-dark-unchecked-border,transparent)]',\n                }[colorScheme],\n              )}\n              disabled={option.disabled}\n              key={option.value}\n              onMouseEnter={() => {\n                onOptionMouseEnter?.(option.value);\n              }}\n              value={option.value}\n            >\n              {option.type === 'color' ? (\n                <span\n                  className={clsx(\n                    'block size-full rounded-full border group-disabled:opacity-20',\n                    {\n                      light:\n                        'border-[var(--swatch-radio-group-light-option-border,hsl(var(--foreground)/10%))]',\n                      dark: 'border-[var(--swatch-radio-group-dark-option-border,hsl(var(--background)/10%))]',\n                    }[colorScheme],\n                  )}\n                  style={{ backgroundColor: option.color }}\n                />\n              ) : (\n                <span\n                  className={clsx(\n                    'relative block size-full overflow-hidden rounded-full border',\n                    {\n                      light:\n                        'border-[var(--swatch-radio-group-light-option-border,hsl(var(--foreground)/10%))]',\n                      dark: 'border-[var(--swatch-radio-group-dark-option-border,hsl(var(--background)/10%))]',\n                    }[colorScheme],\n                  )}\n                >\n                  <Image alt={option.image.alt} height={40} src={option.image.src} width={40} />\n                </span>\n              )}\n              <div\n                className={clsx(\n                  'disabled-icon absolute inset-0 hidden place-content-center',\n                  {\n                    light: 'text-[var(--swatch-radio-group-light-icon,hsl(var(--foreground)))]',\n                    dark: 'text-[var(--swatch-radio-group-dark-icon,hsl(var(--background)))]',\n                  }[colorScheme],\n                )}\n              >\n                <X size={16} strokeWidth={1.5} />\n              </div>\n            </RadioGroupPrimitive.Item>\n          ))}\n        </RadioGroupPrimitive.Root>\n        {errors?.map((error) => (\n          <FieldError key={error}>{error}</FieldError>\n        ))}\n      </div>\n    );\n  },\n);\n\nSwatchRadioGroup.displayName = 'SwatchRadioGroup';\n"
  },
  {
    "path": "core/vibes/soul/form/switch/index.tsx",
    "content": "import * as SwitchPrimitive from '@radix-ui/react-switch';\nimport { clsx } from 'clsx';\nimport { Loader2 } from 'lucide-react';\nimport { useId } from 'react';\n\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\n\ninterface Props {\n  name?: string;\n  required?: boolean;\n  variant?: 'primary' | 'secondary' | 'tertiary';\n  size?: 'large' | 'medium' | 'small';\n  labelPosition?: 'left' | 'right' | 'both';\n  label?: string | { on: string; off: string };\n  checked?: boolean;\n  onCheckedChange?: (checked: boolean) => void | Promise<void>;\n  disabled?: boolean;\n  loading?: boolean;\n}\n\nexport const Switch = ({\n  name,\n  variant = 'primary',\n  size = 'medium',\n  labelPosition = 'right',\n  label,\n  disabled,\n  loading,\n  checked,\n  onCheckedChange,\n}: Props) => {\n  const id = useId();\n  const hasLabel = label != null && label !== '';\n\n  return (\n    <div\n      className=\"group/switch flex items-center gap-2\"\n      data-state={checked ? 'checked' : 'unchecked'}\n    >\n      {(labelPosition === 'left' || labelPosition === 'both') && hasLabel && (\n        <SwitchLabel\n          id={id}\n          label={label}\n          size={size}\n          state={labelPosition === 'both' ? 'off' : undefined}\n        />\n      )}\n      <SwitchPrimitive.Root\n        aria-busy={loading}\n        checked={checked}\n        className={clsx(\n          'w-12 rounded-full border border-contrast-200 p-[3px] transition-colors duration-100 focus-visible:outline-none focus-visible:ring-2 data-[disabled]:cursor-not-allowed [&:not([data-loading])]:data-[disabled]:bg-contrast-100',\n        )}\n        data-loading={loading ? '' : undefined}\n        disabled={disabled || loading}\n        id={id}\n        name={name}\n        onCheckedChange={onCheckedChange}\n      >\n        <SwitchPrimitive.Thumb\n          className={clsx(\n            'relative block h-5 w-5 overflow-hidden rounded-full transition-transform duration-100 data-[state=checked]:translate-x-full data-[disabled]:bg-contrast-200 data-[state=unchecked]:bg-contrast-200',\n            {\n              primary: 'bg-[var(--toggle-primary-background,hsl(var(--primary)))]',\n              secondary: 'bg-[var(--toggle-secondary-background,hsl(var(--foreground)))]',\n              tertiary:\n                'border border-[var(--toggle-tertiary-border,hsl(var(--contrast-200)))] bg-[var(--toggle-tertiary-background,hsl(var(--background)))]',\n            }[variant],\n          )}\n        >\n          <span\n            className={clsx(\n              'absolute inset-0 grid place-content-center transition-all duration-300 ease-in-out',\n              loading ? 'translate-y-0 opacity-100' : 'translate-y-full opacity-0',\n            )}\n          >\n            <Loader2\n              className={clsx('animate-spin', variant === 'tertiary' && 'text-foreground')}\n              size={16}\n            />\n          </span>\n        </SwitchPrimitive.Thumb>\n      </SwitchPrimitive.Root>\n      {(labelPosition === 'right' || labelPosition === 'both') && hasLabel && (\n        <SwitchLabel\n          id={id}\n          label={label}\n          loading={loading}\n          size={size}\n          state={labelPosition === 'both' ? 'on' : undefined}\n        />\n      )}\n    </div>\n  );\n};\n\ninterface LabelProps {\n  id: string;\n  label: string | { on: string; off: string };\n  size: 'large' | 'medium' | 'small';\n  state?: 'off' | 'on';\n  loading?: boolean;\n}\n\nfunction SwitchLabel({ id, label, size = 'medium', state, loading }: LabelProps) {\n  const baseClass =\n    'group-data-[disabled]/switch:[&:not([data-loading])]:text-contrast-400 font-semibold select-none';\n  const sizeClass = {\n    small: 'text-sm',\n    medium: 'text-base',\n    large: 'text-lg',\n  }[size];\n\n  if (typeof label === 'string') {\n    return (\n      <label\n        className={clsx(baseClass, sizeClass)}\n        data-loading={loading ? '' : undefined}\n        htmlFor={id}\n      >\n        {label}\n      </label>\n    );\n  }\n\n  if (state) {\n    return (\n      <label\n        className={clsx(baseClass, sizeClass)}\n        data-loading={loading ? '' : undefined}\n        htmlFor={id}\n      >\n        {label[state]}\n      </label>\n    );\n  }\n\n  return (\n    <div className=\"leading-[0]\">\n      <label\n        className={clsx(\n          'mb-[-2px] leading-[0] group-data-[state=unchecked]/switch:invisible group-data-[state=checked]/switch:block',\n          baseClass,\n          sizeClass,\n        )}\n        data-loading={loading ? '' : undefined}\n        htmlFor={id}\n      >\n        {label.on}\n      </label>\n      <label\n        className={clsx(\n          'mt-[-1px] leading-[0] group-data-[state=checked]/switch:invisible group-data-[state=unchecked]/switch:block',\n          baseClass,\n          sizeClass,\n        )}\n        data-loading={loading ? '' : undefined}\n        htmlFor={id}\n      >\n        {label.off}\n      </label>\n    </div>\n  );\n}\n\nexport function SwitchSkeleton({\n  size = 'medium',\n  labelPosition = 'right',\n  characterCount = 6,\n}: Pick<Props, 'size' | 'labelPosition'> & { characterCount?: number }) {\n  return (\n    <div className=\"flex items-center gap-2\">\n      {(labelPosition === 'left' || labelPosition === 'both') && (\n        <Skeleton.Text\n          characterCount={characterCount}\n          className={clsx(\n            'rounded',\n            {\n              small: 'text-sm',\n              medium: 'text-base',\n              large: 'text-lg',\n            }[size],\n          )}\n        />\n      )}\n      <Skeleton.Box className=\"h-6 w-12 rounded-full p-[3px]\" />\n      {(labelPosition === 'right' || labelPosition === 'both') && (\n        <Skeleton.Text\n          characterCount={characterCount}\n          className={clsx(\n            'rounded',\n            {\n              small: 'text-sm',\n              medium: 'text-base',\n              large: 'text-lg',\n            }[size],\n          )}\n        />\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/form/textarea/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport * as React from 'react';\n\nimport { FieldError } from '@/vibes/soul/form/field-error';\nimport { Label } from '@/vibes/soul/form/label';\n\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *    --textarea-light-background: hsl(var(--background));\n *    --textarea-light-text: hsl(var(--foreground));\n *    --textarea-light-placeholder: hsl(var(--contrast-500));\n *    --textarea-light-border: hsl(var(--contrast-100));\n *    --textarea-light-border-focus: hsl(var(--foreground));\n *    --textarea-light-border-error: hsl(var(--error));\n *    --textarea-dark-background: hsl(var(--foreground));\n *    --textarea-dark-text: hsl(var(--background));\n *    --textarea-dark-placeholder: hsl(var(--contrast-100));\n *    --textarea-dark-border: hsl(var(--contrast-500));\n *    --textarea-dark-border-focus: hsl(var(--background));\n *    --textarea-dark-border-error: hsl(var(--error));\n *  }\n * ```\n */\nexport const Textarea = React.forwardRef<\n  React.ComponentRef<'textarea'>,\n  React.ComponentPropsWithoutRef<'textarea'> & {\n    prepend?: React.ReactNode;\n    label?: string;\n    errors?: string[];\n    colorScheme?: 'light' | 'dark';\n  }\n>(({ label, className, required, errors, colorScheme = 'light', ...rest }, ref) => {\n  const id = React.useId();\n\n  return (\n    <div className={clsx('space-y-2', className)}>\n      {label != null && label !== '' && (\n        <Label colorScheme={colorScheme} htmlFor={id} required={required}>\n          {label}\n        </Label>\n      )}\n      <textarea\n        {...rest}\n        className={clsx(\n          'w-full rounded-lg border p-3 transition-colors duration-200 placeholder:font-normal focus:outline-none disabled:cursor-not-allowed disabled:opacity-50',\n          {\n            light:\n              'bg-[var(--textarea-light-background,hsl(var(--background)))] text-[var(--textarea-light-text,hsl(var(--foreground)))] placeholder-[var(--textarea-light-placeholder,hsl(var(--contrast-500)))] focus:border-[var(--textarea-light-border-focus,hsl(var(--foreground)))]',\n            dark: 'bg-[var(--textarea-dark-background,hsl(var(--foreground)))] text-[var(--textarea-dark-text,hsl(var(--background)))] placeholder-[var(--textarea-dark-placeholder,hsl(var(--contrast-100)))] focus:border-[var(--textarea-dark-border-focus,hsl(var(--background)))]',\n          }[colorScheme],\n          {\n            light:\n              errors && errors.length > 0\n                ? 'border-[var(--textarea-light-border-error,hsl(var(--error)))]'\n                : 'border-[var(--textarea-light-border,hsl(var(--contrast-100)))]',\n            dark:\n              errors && errors.length > 0\n                ? 'border-[var(--textarea-dark-border-error,hsl(var(--error)))]'\n                : 'border-[var(--textarea-dark-border,hsl(var(--contrast-500)))]',\n          }[colorScheme],\n        )}\n        id={id}\n        ref={ref}\n        required={required}\n      />\n      {errors?.map((error) => (\n        <FieldError key={error}>{error}</FieldError>\n      ))}\n    </div>\n  );\n});\n\nTextarea.displayName = 'Textarea';\n"
  },
  {
    "path": "core/vibes/soul/form/toggle-group/index.tsx",
    "content": "'use client';\n\nimport * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';\nimport { clsx } from 'clsx';\nimport * as React from 'react';\n\nimport { FieldError } from '@/vibes/soul/form/field-error';\nimport { Label } from '@/vibes/soul/form/label';\n\ninterface Option {\n  value: string;\n  label: string;\n  disabled?: boolean;\n}\n\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *   --toggle-group-light-focus: hsl(var(--primary));\n *   --toggle-group-light-border: hsl(var(--contrast-100));\n *   --toggle-group-light-on-border: hsl(var(--foreground));\n *   --toggle-group-light-on-background: hsl(var(--foreground));\n *   --toggle-group-light-off-background: hsl(var(--background));\n *   --toggle-group-light-off-text: hsl(var(--foreground));\n *   --toggle-group-light-on-text: hsl(var(--background));\n *   --toggle-group-light-off-border-hover: hsl(var(--contrast-200));\n *   --toggle-group-light-off-background-hover: hsl(var(--contrast-100));\n *   --toggle-group-dark-focus: hsl(var(--primary));\n *   --toggle-group-dark-border: hsl(var(--contrast-500));\n *   --toggle-group-dark-on-border: hsl(var(--background));\n *   --toggle-group-dark-on-background: hsl(var(--background));\n *   --toggle-group-dark-off-background: hsl(var(--foreground));\n *   --toggle-group-dark-off-text: hsl(var(--background));\n *   --toggle-group-dark-on-text: hsl(var(--foreground));\n *   --toggle-group-dark-off-border-hover: hsl(var(--contrast-400));\n *   --toggle-group-dark-off-background-hover: hsl(var(--contrast-500));\n * ```\n */\nexport const ToggleGroup = React.forwardRef<\n  React.ComponentRef<typeof ToggleGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & {\n    label?: string;\n    options: Option[];\n    errors?: string[];\n    colorScheme?: 'light' | 'dark';\n  }\n>(({ label, options, errors, className, colorScheme = 'light', ...rest }, ref) => {\n  const id = React.useId();\n\n  return (\n    <div className={clsx('space-y-2', className)}>\n      {label !== undefined && label !== '' && (\n        <Label colorScheme={colorScheme} id={id}>\n          {label}\n        </Label>\n      )}\n      <ToggleGroupPrimitive.Root\n        {...rest}\n        aria-labelledby={id}\n        className=\"flex flex-wrap gap-2\"\n        ref={ref}\n      >\n        {options.map((option) => (\n          <ToggleGroupPrimitive.Item\n            aria-label={option.label}\n            className={clsx(\n              'h-12 whitespace-nowrap rounded-full border px-4 font-body text-sm font-normal leading-normal transition-colors focus-visible:outline-0 focus-visible:ring-2 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n              {\n                light:\n                  'border-[var(--toggle-group-light-border,hsl(var(--contrast-100)))] ring-[var(--toggle-group-light-focus,hsl(var(--primary)))] data-[state=on]:border-[var(--toggle-group-light-on-border,hsl(var(--foreground)))] data-[state=off]:bg-[var(--toggle-group-light-off-background,hsl(var(--background)))] data-[state=on]:bg-[var(--toggle-group-light-on-background,hsl(var(--foreground)))] data-[state=off]:text-[var(--toggle-group-light-off-text,hsl(var(--foreground)))] data-[state=on]:text-[var(--toggle-group-light-on-text,hsl(var(--background)))] data-[state=off]:hover:border-[var(--toggle-group-light-off-border-hover,hsl(var(--contrast-200)))] data-[state=off]:hover:bg-[var(--toggle-group-light-off-background-hover,hsl(var(--contrast-100)))]',\n                dark: 'border-[var(--toggle-group-dark-border,hsl(var(--contrast-500)))] ring-[var(--toggle-group-dark-focus,hsl(var(--primary)))] data-[state=on]:border-[var(--toggle-group-dark-on-border,hsl(var(--background)))] data-[state=off]:bg-[var(--toggle-group-dark-off-background,hsl(var(--foreground)))] data-[state=on]:bg-[var(--toggle-group-dark-on-background,hsl(var(--background)))] data-[state=off]:text-[var(--toggle-group-dark-off-text,hsl(var(--background)))] data-[state=on]:text-[var(--toggle-group-dark-on-text,hsl(var(--foreground)))] data-[state=off]:hover:border-[var(--toggle-group-dark-off-border-hover,hsl(var(--contrast-400)))] data-[state=off]:hover:bg-[var(--toggle-group-dark-off-background-hover,hsl(var(--contrast-500)))]',\n              }[colorScheme],\n            )}\n            disabled={option.disabled}\n            key={option.value}\n            value={option.value}\n          >\n            {option.label}\n          </ToggleGroupPrimitive.Item>\n        ))}\n      </ToggleGroupPrimitive.Root>\n      {errors?.map((error) => (\n        <FieldError key={error}>{error}</FieldError>\n      ))}\n    </div>\n  );\n});\n\nToggleGroup.displayName = 'ToggleGroup';\n"
  },
  {
    "path": "core/vibes/soul/lib/streamable.tsx",
    "content": "import PLazy from 'p-lazy';\nimport { Suspense, use } from 'react';\nimport { v4 as uuid } from 'uuid';\n\nexport type Streamable<T> = T | Promise<T>;\n\n// eslint-disable-next-line func-names\nconst stableKeys = (function () {\n  const cache = new WeakMap<object, string>();\n\n  function getObjectKey(obj: object): string {\n    const key = cache.get(obj);\n\n    if (key !== undefined) {\n      return key;\n    }\n\n    const keyValue = uuid();\n\n    cache.set(obj, keyValue);\n\n    return keyValue;\n  }\n\n  return {\n    get: (streamable: unknown): string =>\n      streamable != null && typeof streamable === 'object'\n        ? getObjectKey(streamable)\n        : JSON.stringify(streamable),\n  };\n})();\n\nfunction getCompositeKey(streamables: readonly unknown[]): string {\n  return streamables.map(stableKeys.get).join('.');\n}\n\nfunction weakRefCache<K, T extends object>() {\n  const cache = new Map<K, WeakRef<T>>();\n\n  const registry = new FinalizationRegistry((key: K) => {\n    const valueRef = cache.get(key);\n\n    if (valueRef && !valueRef.deref()) cache.delete(key);\n  });\n\n  return {\n    get: (key: K) => cache.get(key)?.deref(),\n    set: (key: K, value: T) => {\n      cache.set(key, new WeakRef(value));\n      registry.register(value, key);\n    },\n  };\n}\n\nconst promiseCache = weakRefCache<string, Promise<unknown>>();\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * A suspense-friendly upgrade to `Promise.all`, guarantees stability of\n * the returned promise instance if passed an identical set of inputs.\n */\nfunction all<T extends readonly unknown[] | []>(\n  streamables: T,\n): Streamable<{ -readonly [P in keyof T]: Awaited<T[P]> }> {\n  const cacheKey = getCompositeKey(streamables);\n\n  const cached = promiseCache.get(cacheKey);\n\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  if (cached != null) return cached as { -readonly [P in keyof T]: Awaited<T[P]> };\n\n  const result = Promise.all(streamables);\n\n  promiseCache.set(cacheKey, result);\n\n  return result;\n}\n\nfunction from<T>(thunk: () => Promise<T>): Streamable<T> {\n  return PLazy.from(thunk);\n}\n\nexport const Streamable = {\n  all,\n  from,\n};\n\nexport function useStreamable<T>(streamable: Streamable<T>): T {\n  return streamable instanceof Promise ? use(streamable) : streamable;\n}\n\nfunction UseStreamable<T>({\n  value,\n  children,\n}: {\n  value: Streamable<T>;\n  children: (value: T) => React.ReactNode;\n}) {\n  return children(useStreamable(value));\n}\n\nexport function Stream<T>({\n  value,\n  fallback,\n  children,\n}: {\n  value: Streamable<T>;\n  fallback?: React.ReactNode;\n  children: (value: T) => React.ReactNode;\n}) {\n  return (\n    <Suspense fallback={fallback}>\n      <UseStreamable value={value}>{children}</UseStreamable>\n    </Suspense>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/accordion/index.tsx",
    "content": "'use client';\n\nimport * as AccordionPrimitive from '@radix-ui/react-accordion';\nimport { clsx } from 'clsx';\nimport { ComponentPropsWithoutRef, useEffect, useState } from 'react';\n\nexport interface AccordionProps extends ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> {\n  colorScheme?: 'light' | 'dark';\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --accordion-focus: hsl(var(--primary));\n *   --acordion-light-offset: hsl(var(--background));\n *   --accordion-light-title-text: hsl(var(--contrast-400));\n *   --accordion-light-title-text-hover: hsl(var(--foreground));\n *   --accordion-light-title-icon: hsl(var(--contrast-500));\n *   --accordion-light-title-icon-hover: hsl(var(--foreground));\n *   --accordion-light-content-text: hsl(var(--foreground));\n *   --acordion-dark-offset: hsl(var(--foreground));\n *   --accordion-dark-title-text: hsl(var(--contrast-200));\n *   --accordion-dark-title-text-hover: hsl(var(--background));\n *   --accordion-dark-title-icon: hsl(var(--contrast-200));\n *   --accordion-dark-title-icon-hover: hsl(var(--background));\n *   --accordion-dark-content-text: hsl(var(--background));\n *   --accordion-title-font-family: var(--font-family-mono);\n *   --accordion-content-font-family: var(--font-family-body);\n * }\n * ```\n */\nfunction AccordionItem({\n  title,\n  children,\n  colorScheme = 'light',\n  className,\n  ...props\n}: AccordionProps) {\n  const [isMounted, setIsMounted] = useState(false);\n\n  useEffect(() => {\n    setIsMounted(true);\n  }, []);\n\n  return (\n    <AccordionPrimitive.Item\n      {...props}\n      className={clsx(\n        'has-focus-visible:ring-2 has-focus-visible:ring-(--accordion-focus,hsl(var(--primary))) has-focus-visible:ring-offset-4 focus:outline-2',\n        {\n          light: 'ring-offset-[var(--acordion-light-offset,hsl(var(--background)))]',\n          dark: 'ring-offset-[var(--acordion-dark-offset,hsl(var(--foreground)))]',\n        }[colorScheme],\n        className,\n      )}\n    >\n      <AccordionPrimitive.Header>\n        <AccordionPrimitive.Trigger className=\"group flex w-full cursor-pointer items-start gap-8 border-none py-3 text-start focus:outline-none @md:py-4\">\n          <div\n            className={clsx(\n              'flex-1 select-none font-[family-name:var(--accordion-title-font-family,var(--font-family-mono))] text-sm font-normal uppercase transition-colors duration-300 ease-out',\n              {\n                light:\n                  'text-[var(--accordion-light-title-text,hsl(var(--contrast-400)))] group-hover:text-[var(--accordion-light-title-text-hover,hsl(var(--foreground)))]',\n                dark: 'text-[var(--accordion-dark-title-text,hsl(var(--contrast-200)))] group-hover:text-[var(--accordion-dark-title-text-hover,hsl(var(--background)))]',\n              }[colorScheme],\n            )}\n          >\n            {title}\n          </div>\n          <AnimatedChevron\n            className={clsx(\n              {\n                light:\n                  'stroke-[var(--accordion-light-title-icon,hsl(var(--contrast-500)))] group-hover:stroke-[var(--accordion-light-title-icon-hover,hsl(var(--foreground)))]',\n                dark: 'stroke-[var(--accordion-dark-title-icon,hsl(var(--contrast-200)))] group-hover:stroke-[var(--accordion-dark-title-icon-hover,hsl(var(--background)))]',\n              }[colorScheme],\n            )}\n          />\n        </AccordionPrimitive.Trigger>\n      </AccordionPrimitive.Header>\n      <AccordionPrimitive.Content\n        className={clsx(\n          'overflow-hidden',\n          // We need to delay the animation until the component is mounted to avoid the animation\n          // from being triggered when the component is first rendered.\n          isMounted && 'data-[state=closed]:animate-collapse data-[state=open]:animate-expand',\n        )}\n      >\n        <div\n          className={clsx(\n            'py-3 font-[family-name:var(--accordion-content-font-family,var(--font-family-body))] text-base font-light leading-normal',\n            {\n              light: 'text-[var(--accordion-light-content-text,hsl(var(--foreground)))]',\n              dark: 'text-[var(--accordion-dark-content-text,hsl(var(--background)))]',\n            }[colorScheme],\n          )}\n        >\n          {children}\n        </div>\n      </AccordionPrimitive.Content>\n    </AccordionPrimitive.Item>\n  );\n}\n\nfunction AnimatedChevron({\n  className,\n  ...props\n}: React.JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      {...props}\n      className={clsx(\n        'mt-1 shrink-0 [&>line]:origin-center [&>line]:transition [&>line]:duration-300 [&>line]:ease-out',\n        className,\n      )}\n      viewBox=\"0 0 10 10\"\n      width={16}\n    >\n      {/* Left Line of Chevron */}\n      <line\n        className=\"group-data-[state=open]:-translate-y-[3px] group-data-[state=open]:-rotate-90\"\n        strokeLinecap=\"round\"\n        x1={2}\n        x2={5}\n        y1={2}\n        y2={5}\n      />\n      {/* Right Line of Chevron */}\n      <line\n        className=\"group-data-[state=open]:-translate-y-[3px] group-data-[state=open]:rotate-90\"\n        strokeLinecap=\"round\"\n        x1={8}\n        x2={5}\n        y1={2}\n        y2={5}\n      />\n    </svg>\n  );\n}\n\nconst Accordion = AccordionPrimitive.Root;\n\nexport { Accordion, AccordionItem };\n"
  },
  {
    "path": "core/vibes/soul/primitives/alert/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { X } from 'lucide-react';\nimport { ReactNode } from 'react';\n\nimport { Button } from '@/vibes/soul/primitives/button';\n\ninterface Props {\n  variant: 'success' | 'warning' | 'error' | 'info';\n  message: ReactNode;\n  description?: string;\n  dismissLabel?: string;\n  action?: {\n    label: string;\n    onClick: () => void;\n  };\n  onDismiss?: () => void;\n}\n\nexport function Alert({\n  variant,\n  message,\n  description,\n  action,\n  dismissLabel = 'Dismiss',\n  onDismiss,\n}: Props) {\n  return (\n    <div\n      className={clsx(\n        'flex min-w-[284px] max-w-[356px] items-center justify-between gap-2 rounded-xl border border-foreground/10 py-3 pe-3 ps-4 shadow-sm ring-foreground group-focus-visible:outline-none group-focus-visible:ring-2',\n        {\n          success: 'bg-success-highlight',\n          warning: 'bg-warning-highlight',\n          error: 'bg-error-highlight',\n          info: 'bg-background',\n        }[variant],\n      )}\n      role=\"alert\"\n    >\n      <div className=\"flex flex-col\">\n        <span className=\"text-sm font-normal text-foreground\">{message}</span>\n        {Boolean(description) && (\n          <span className=\"text-xs font-medium text-contrast-400\">{description}</span>\n        )}\n      </div>\n\n      <div className=\"flex items-center gap-1\">\n        {action && (\n          <Button onClick={action.onClick} size=\"x-small\" variant=\"ghost\">\n            {action.label}\n          </Button>\n        )}\n\n        <Button\n          aria-label={dismissLabel}\n          onClick={onDismiss}\n          shape=\"circle\"\n          size=\"x-small\"\n          variant=\"ghost\"\n        >\n          <X size={20} strokeWidth={1} />\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/animated-underline/index.tsx",
    "content": "import { clsx } from 'clsx';\n\nexport interface AnimatedUnderlineProps {\n  children: string;\n  className?: string;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --animated-underline-hover: hsl(var(--primary));\n *   --animated-underline-text: hsl(var(--foreground));\n *   --animated-underline-font-family: var(--font-family-body);\n * }\n * ```\n */\nexport function AnimatedUnderline({ className, children }: AnimatedUnderlineProps) {\n  return (\n    <span\n      className={clsx(\n        'origin-left font-[family-name:var(--animated-underline-font-family,var(--font-family-body))] font-semibold leading-normal text-[var(--animated-underline-text,hsl(var(--foreground)))] transition-[background-size] duration-300 [background:linear-gradient(0deg,var(--animated-underline-hover,hsl(var(--primary))),var(--animated-underline-hover,hsl(var(--primary))))_no-repeat_left_bottom_/_0_2px] hover:bg-[size:100%_2px] group-focus/underline:bg-[size:100%_2px]',\n        className,\n      )}\n    >\n      {children}\n    </span>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/badge/index.tsx",
    "content": "import { clsx } from 'clsx';\n\nexport interface BadgeProps {\n  children: string;\n  shape?: 'pill' | 'rounded';\n  variant?: 'primary' | 'warning' | 'error' | 'success' | 'info';\n  className?: string;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --badge-primary-background: color-mix(in oklab, hsl(var(--primary)), white 75%);\n *   --badge-accent-background: hsl(var(--accent));\n *   --badge-success-background: color-mix(in oklab, hsl(var(--success)), white 75%);\n *   --badge-warning-background: color-mix(in oklab, hsl(var(--warning)), white 75%);\n *   --badge-error-background: color-mix(in oklab, hsl(var(--error)), white 75%);\n *   --badge-info-background: color-mix(in oklab, hsl(var(--background)), black 5%);\n *   --badge-text: hsl(var(--foreground));\n *   --badge-font-family: var(--font-family-mono);\n * }\n * ```\n */\nexport function Badge({ children, shape = 'rounded', className, variant = 'primary' }: BadgeProps) {\n  return (\n    <span\n      className={clsx(\n        'px-2 py-0.5 font-[family-name:var(--badge-font-family,var(--font-family-mono))] text-xs uppercase tracking-tighter text-[var(--badge-text,hsl(var(--foreground)))]',\n        {\n          pill: 'rounded-full',\n          rounded: 'rounded',\n        }[shape],\n        {\n          primary:\n            'bg-[var(--badge-primary-background,color-mix(in_oklab,_hsl(var(--primary)),_white_75%))]',\n          warning:\n            'bg-[var(--badge-warning-background,color-mix(in_oklab,_hsl(var(--warning)),_white_75%))]',\n          error:\n            'bg-[var(--badge-error-background,color-mix(in_oklab,_hsl(var(--error)),_white_75%))]',\n          success:\n            'bg-[var(--badge-success-background,color-mix(in_oklab,_hsl(var(--success)),_white_75%))]',\n          info: 'bg-[var(--badge-info-background,color-mix(in_oklab,_hsl(var(--background)),_black_5%))]',\n        }[variant],\n        className,\n      )}\n    >\n      {children}\n    </span>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/banner/index.tsx",
    "content": "'use client';\n\nimport { clsx } from 'clsx';\nimport { X } from 'lucide-react';\nimport { ForwardedRef, forwardRef, ReactNode, useCallback, useEffect, useState } from 'react';\n\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --banner-focus: hsl(var(--primary));\n *   --banner-background: hsl(var(--primary));\n *   --banner-text: hdl(var(--foreground));\n *   --banner-close-icon: hdl(var(--foreground));\n *   --banner-close-icon-hover: hdl(var(--foreground));\n *   --banner-close-background: hdl(var(--foreground));\n *   --banner-close-background-hover: hdl(var(--foreground));\n * }\n * ```\n */\nexport const Banner = forwardRef(\n  (\n    {\n      id,\n      children,\n      hideDismiss = false,\n      className,\n      onDismiss,\n    }: {\n      id: string;\n      children: ReactNode;\n      hideDismiss?: boolean;\n      className?: string;\n      onDismiss?: () => void;\n    },\n    ref: ForwardedRef<HTMLDivElement>,\n  ) => {\n    const [banner, setBanner] = useState({ dismissed: false, initialized: false });\n\n    useEffect(() => {\n      const hidden = localStorage.getItem(`${id}-hidden-banner`) === 'true';\n\n      setBanner({ dismissed: hidden, initialized: true });\n    }, [id]);\n\n    const hideBanner = useCallback(() => {\n      setBanner((prev) => ({ ...prev, dismissed: true }));\n      localStorage.setItem(`${id}-hidden-banner`, 'true');\n      onDismiss?.();\n    }, [id, onDismiss]);\n\n    if (!banner.initialized) return null;\n\n    return (\n      <div\n        className={clsx(\n          'relative w-full overflow-hidden bg-[var(--banner-background,hsl(var(--primary)))] transition-all duration-300 ease-in @container',\n          banner.dismissed ? 'pointer-events-none max-h-0' : 'max-h-32',\n          className,\n        )}\n        id=\"announcement-bar\"\n        ref={ref}\n      >\n        <div className=\"p-3 pr-12 text-sm text-[var(--banner-text,hsl(var(--foreground)))] @xl:px-12 @xl:text-center @xl:text-base\">\n          {children}\n        </div>\n\n        {!hideDismiss && (\n          <button\n            aria-label=\"Dismiss banner\"\n            className=\"absolute right-3 top-3 grid h-8 w-8 place-content-center rounded-full bg-[var(--banner-close-background,transparent)] text-[var(--banner-close-icon,hsl(var(--foreground)/50%))] transition-colors duration-300 hover:bg-[var(--banner-close-background-hover,hsl(var(--background)/40%))] hover:text-[var(--banner-close-icon-hover,hsl(var(--foreground)))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--banner-focus,hsl(var(--foreground)))] @xl:top-1/2 @xl:-translate-y-1/2\"\n            onClick={(e) => {\n              e.preventDefault();\n              hideBanner();\n            }}\n          >\n            <X absoluteStrokeWidth size={20} strokeWidth={1.5} />\n          </button>\n        )}\n      </div>\n    );\n  },\n);\n\nBanner.displayName = 'Banner';\n"
  },
  {
    "path": "core/vibes/soul/primitives/blog-post-card/index.tsx",
    "content": "import { clsx } from 'clsx';\n\nimport { Image } from '~/components/image';\nimport { Link } from '~/components/link';\n\nexport interface BlogPostCardBlogPost {\n  id: string;\n  author?: string | null;\n  content: string;\n  date: string;\n  image?: {\n    src: string;\n    alt: string;\n  };\n  href: string;\n  title: string;\n}\n\ninterface Props {\n  blogPost: BlogPostCardBlogPost;\n  className?: string;\n}\n\nexport function BlogPostCard({ blogPost, className }: Props) {\n  const { author, content, date, href, image, title } = blogPost;\n\n  return (\n    <Link\n      className={clsx(\n        'group max-w-full rounded-b-lg rounded-t-2xl text-foreground ring-primary ring-offset-4 @container focus:outline-0 focus-visible:ring-2',\n        className,\n      )}\n      href={href}\n    >\n      <div className=\"relative mb-4 aspect-[4/3] w-full overflow-hidden rounded-2xl bg-contrast-100\">\n        {image?.src != null && image.src !== '' ? (\n          <Image\n            alt={image.alt}\n            className=\"object-cover transition-transform duration-500 ease-out group-hover:scale-110\"\n            fill\n            sizes=\"(min-width: 80rem) 25vw, (min-width: 56rem) 33vw, (min-width: 28rem) 50vw, 100vw\"\n            src={image.src}\n          />\n        ) : (\n          <div className=\"p-4 text-5xl font-bold leading-none tracking-tighter text-foreground/15\">\n            {title}\n          </div>\n        )}\n      </div>\n\n      <div className=\"text-lg font-medium leading-snug\">{title}</div>\n      <p className=\"mb-3 mt-1.5 line-clamp-3 text-sm font-normal text-contrast-400\">{content}</p>\n      <div className=\"text-sm\">\n        <time dateTime={date}>{date}</time>\n        {date !== '' && author != null && author !== '' && (\n          <span className=\"after:mx-2 after:content-['•']\" />\n        )}\n        {author != null && author !== '' && <span>{author}</span>}\n      </div>\n    </Link>\n  );\n}\n\nexport function BlogPostCardSkeleton({ className }: { className?: string }) {\n  return (\n    <div className={clsx('flex max-w-md animate-pulse flex-col gap-2 rounded-xl', className)}>\n      {/* Image */}\n      <div className=\"aspect-[4/3] overflow-hidden rounded-xl bg-contrast-100\" />\n\n      {/* Title */}\n      <div className=\"h-4 w-24 rounded-lg bg-contrast-100\" />\n\n      {/* Content */}\n      <div className=\"h-3 w-full rounded-lg bg-contrast-100\" />\n      <div className=\"h-3 w-full rounded-lg bg-contrast-100\" />\n      <div className=\"h-3 w-1/2 rounded-lg bg-contrast-100\" />\n\n      <div className=\"flex flex-wrap items-center\">\n        {/* Date */}\n        <div className=\"h-4 w-16 rounded-lg bg-contrast-100\" />\n        <span className=\"after:mx-2 after:text-contrast-100 after:content-['•']\" />\n        {/* Author */}\n        <div className=\"h-4 w-20 rounded-lg bg-contrast-100\" />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/button/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { Loader2 } from 'lucide-react';\nimport { ComponentPropsWithoutRef } from 'react';\n\nexport interface ButtonProps extends ComponentPropsWithoutRef<'button'> {\n  variant?: 'primary' | 'secondary' | 'tertiary' | 'ghost' | 'danger';\n  size?: 'large' | 'medium' | 'small' | 'x-small';\n  shape?: 'pill' | 'rounded' | 'square' | 'circle';\n  loading?: boolean;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --button-focus: hsl(var(--primary));\n *   --button-font-family: var(--font-family-body);\n *   --button-primary-background: hsl(var(--primary));\n *   --button-primary-background-hover: color-mix(in oklab, hsl(var(--primary)), white 75%);\n *   --button-primary-text: hsl(var(--foreground));\n *   --button-primary-border: hsl(var(--primary));\n *   --button-secondary-background: hsl(var(--foreground));\n *   --button-secondary-background-hover: hsl(var(--background));\n *   --button-secondary-text: hsl(var(--background));\n *   --button-secondary-border: hsl(var(--foreground));\n *   --button-tertiary-background: hsl(var(--background));\n *   --button-tertiary-background-hover: hsl(var(--contrast-100));\n *   --button-tertiary-text: hsl(var(--foreground));\n *   --button-tertiary-border: hsl(var(--contrast-200));\n *   --button-ghost-background: transparent;\n *   --button-ghost-background-hover: hsl(var(--foreground) / 5%);\n *   --button-ghost-text: hsl(var(--foreground));\n *   --button-ghost-border: transparent;\n *   --button-loader-icon: hsl(var(--foreground));\n *   --button-danger-background: color-mix(in oklab, hsl(var(--error)), white 30%);\n *   --button-danger-background-hover: color-mix(in oklab, hsl(var(--error)), white 75%);\n *   --button-danger-foreground: hsl(var(--foreground));\n *   --button-danger-border: color-mix(in oklab, hsl(var(--error)), white 30%);\n * }\n * ```\n */\nexport function Button({\n  variant = 'primary',\n  size = 'large',\n  shape = 'pill',\n  loading = false,\n  type = 'button',\n  disabled = false,\n  className,\n  children,\n  ...props\n}: ButtonProps) {\n  return (\n    <button\n      {...props}\n      aria-busy={loading}\n      className={clsx(\n        'relative z-0 inline-flex h-fit cursor-pointer select-none items-center justify-center overflow-hidden border text-center font-[family-name:var(--button-font-family,var(--font-family-body))] font-semibold leading-normal after:absolute after:inset-0 after:-z-10 after:-translate-x-[105%] after:duration-300 after:[animation-timing-function:cubic-bezier(0,0.25,0,1)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--button-focus,hsl(var(--primary)))] focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-30',\n        {\n          primary:\n            'border-[var(--button-primary-border,hsl(var(--primary)))] bg-[var(--button-primary-background,hsl(var(--primary)))] text-[var(--button-primary-text,hsl(var(--foreground)))] after:bg-[var(--button-primary-background-hover,color-mix(in_oklab,hsl(var(--primary)),white_75%))]',\n          secondary:\n            'border-[var(--button-secondary-border,hsl(var(--foreground)))] bg-[var(--button-secondary-background,hsl(var(--foreground)))] text-[var(--button-secondary-text,hsl(var(--background)))] after:bg-[var(--button-secondary-background-hover,hsl(var(--background)))]',\n          tertiary:\n            'border-[var(--button-tertiary-border,hsl(var(--contrast-200)))] bg-[var(--button-tertiary-background,hsl(var(--background)))] text-[var(--button-tertiary-text,hsl(var(--foreground)))] after:bg-[var(--button-tertiary-background-hover,hsl(var(--contrast-100)))]',\n          ghost:\n            'border-[var(--button-ghost-border,transparent)] bg-[var(--button-ghost-background,transparent)] text-[var(--button-ghost-text,hsl(var(--foreground)))] after:bg-[var(--button-ghost-background-hover,hsl(var(--foreground)/5%))]',\n          danger:\n            'border-[var(--button-danger-border,color-mix(in_oklab,hsl(var(--error)),white_30%))] bg-[var(--button-danger-background,color-mix(in_oklab,hsl(var(--error)),white_30%))] text-[var(--button-danger-foreground)] after:bg-[var(--button-danger-background-hover,color-mix(in_oklab,hsl(var(--error)),white_75%))]',\n        }[variant],\n        {\n          pill: 'rounded-full after:rounded-full',\n          rounded: 'rounded-lg after:rounded-lg',\n          square: 'rounded-none after:rounded-none',\n          circle: 'rounded-full after:rounded-full',\n        }[shape],\n        !loading && !disabled && 'hover:after:translate-x-0',\n        loading && 'pointer-events-none',\n        className,\n      )}\n      disabled={disabled}\n      type={type}\n    >\n      <span\n        className={clsx(\n          'inline-flex items-center justify-center transition-all duration-300 ease-in-out',\n          loading ? '-translate-y-10 opacity-0' : 'translate-y-0 opacity-100',\n          {\n            'x-small': 'min-h-8 text-xs',\n            small: 'min-h-10 text-sm',\n            medium: 'min-h-12 text-base',\n            large: 'min-h-14 text-base',\n          }[size],\n          shape === 'circle' &&\n            {\n              'x-small': 'min-w-8',\n              small: 'min-w-10',\n              medium: 'min-w-12',\n              large: 'min-w-14',\n            }[size],\n          shape !== 'circle' &&\n            {\n              'x-small': 'gap-x-2 px-3 py-1.5',\n              small: 'gap-x-2 px-4 py-2.5',\n              medium: 'gap-x-2.5 px-5 py-3',\n              large: 'gap-x-3 px-6 py-4',\n            }[size],\n          variant === 'secondary' && 'mix-blend-difference',\n        )}\n      >\n        {children}\n      </span>\n      <span\n        className={clsx(\n          'absolute inset-0 grid place-content-center transition-all duration-300 ease-in-out',\n          loading ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0',\n        )}\n      >\n        <Loader2\n          className={clsx(\n            'animate-spin',\n            variant === 'tertiary' && 'text-[var(--button-loader-icon,hsl(var(--foreground)))]',\n          )}\n        />\n      </span>\n    </button>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/button-link/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { ComponentPropsWithoutRef } from 'react';\n\nimport { Link } from '~/components/link';\n\nexport interface ButtonLinkProps extends ComponentPropsWithoutRef<typeof Link> {\n  variant?: 'primary' | 'secondary' | 'tertiary' | 'ghost';\n  size?: 'large' | 'medium' | 'small' | 'x-small';\n  shape?: 'pill' | 'rounded' | 'square' | 'circle';\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --button-focus: hsl(var(--primary));\n *   --button-font-family: var(--font-family-body);\n *   --button-primary-background: hsl(var(--primary));\n *   --button-primary-background-hover: color-mix(in oklab, hsl(var(--primary)), white 75%);\n *   --button-primary-text: hsl(var(--foreground));\n *   --button-primary-border: hsl(var(--primary));\n *   --button-secondary-background: hsl(var(--foreground));\n *   --button-secondary-background-hover: hsl(var(--background));\n *   --button-secondary-text: hsl(var(--background));\n *   --button-secondary-border: hsl(var(--foreground));\n *   --button-tertiary-background: hsl(var(--background));\n *   --button-tertiary-background-hover: hsl(var(--contrast-100));\n *   --button-tertiary-text: hsl(var(--foreground));\n *   --button-tertiary-border: hsl(var(--contrast-200));\n *   --button-ghost-background: transparent;\n *   --button-ghost-background-hover: hsl(var(--foreground) / 5%);\n *   --button-ghost-text: hsl(var(--foreground));\n *   --button-ghost-border: transparent;\n * }\n * ```\n */\nexport function ButtonLink({\n  variant = 'primary',\n  size = 'large',\n  shape = 'pill',\n  className,\n  children,\n  ...props\n}: ButtonLinkProps) {\n  return (\n    <Link\n      {...props}\n      className={clsx(\n        'relative z-0 inline-flex h-fit select-none items-center justify-center overflow-hidden border text-center font-[family-name:var(--button-font-family)] font-semibold leading-normal after:absolute after:inset-0 after:-z-10 after:-translate-x-[105%] after:transition-[opacity,transform] after:duration-300 after:[animation-timing-function:cubic-bezier(0,0.25,0,1)] hover:after:translate-x-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--button-focus,hsl(var(--primary)))] focus-visible:ring-offset-2',\n        {\n          primary:\n            'border-[var(--button-primary-border,hsl(var(--primary)))] bg-[var(--button-primary-background,hsl(var(--primary)))] text-[var(--button-primary-text)] after:bg-[var(--button-primary-background-hover,color-mix(in_oklab,hsl(var(--primary)),white_75%))]',\n          secondary:\n            'border-[var(--button-secondary-border,hsl(var(--foreground)))] bg-[var(--button-secondary-background,hsl(var(--foreground)))] text-[var(--button-secondary-text,hsl(var(--background)))] after:bg-[var(--button-secondary-background-hover,hsl(var(--background)))]',\n          tertiary:\n            'border-[var(--button-tertiary-border,hsl(var(--contrast-200)))] bg-[var(--button-tertiary-background,hsl(var(--background)))] text-[var(--button-tertiary-text,hsl(var(--foreground)))] after:bg-[var(--button-tertiary-background-hover,hsl(var(--contrast-100)))]',\n          ghost:\n            'border-[var(--button-ghost-border,transparent)] bg-[var(--button-ghost-background,transparent)] text-[var(--button-ghost-text,hsl(var(--foreground)))] after:bg-[var(--button-ghost-background-hover,hsl(var(--foreground)/5%))]',\n        }[variant],\n        {\n          'x-small': 'min-h-8 text-xs',\n          small: 'min-h-10 text-sm',\n          medium: 'min-h-12 text-base',\n          large: 'min-h-14 text-base',\n        }[size],\n        shape !== 'circle' &&\n          {\n            'x-small': 'gap-x-2 px-3 py-1.5',\n            small: 'gap-x-2 px-4 py-2.5',\n            medium: 'gap-x-2.5 px-5 py-3',\n            large: 'gap-x-3 px-6 py-4',\n          }[size],\n        {\n          pill: 'rounded-full after:rounded-full',\n          rounded: 'rounded-lg after:rounded-lg',\n          square: 'rounded-none after:rounded-none',\n          circle: 'aspect-square rounded-full after:rounded-full',\n        }[shape],\n        className,\n      )}\n    >\n      <span className={clsx(variant === 'secondary' && 'mix-blend-difference')}>{children}</span>\n    </Link>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/calendar/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { ChevronLeftIcon } from 'lucide-react';\nimport { ComponentPropsWithoutRef } from 'react';\nimport { DayPicker } from 'react-day-picker';\n\nconst components = {\n  Chevron: () => <ChevronLeftIcon className=\"h-5 w-5\" strokeWidth={1} />,\n};\n\nexport type CalendarProps = ComponentPropsWithoutRef<typeof DayPicker> & {\n  colorScheme?: 'light' | 'dark';\n};\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n *  :root {\n *   --calendar-font-family: var(--font-family-body);\n *   --calendar-light-focus: hsl(var(--foreground));\n *   --calendar-light-border: hsl(var(--contrast-100));\n *   --calendar-light-text: hsl(var(--foreground));\n *   --calendar-light-background: hsl(var(--background));\n *   --calendar-light-button-border-hover: hsl(var(--contrast-200));\n *   --calendar-light-selected-button-background: hsl(var(--primary));\n *   --calendar-light-selected-button-text: hsl(var(--foreground));\n *   --calendar-light-selected-middle-button-background: transparent;\n *   --calendar-light-text-disabled: hsl(var(--contrast-300));\n *   --calendar-light-range-background: color-mix(in oklab, hsl(var(--primary)), white 75%);\n *   --calendar-dark-focus: hsl(var(--background));\n *   --calendar-dark-border: hsl(var(--contrast-500));\n *   --calendar-dark-text: hsl(var(--background));\n *   --calendar-dark-background: hsl(var(--foreground));\n *   --calendar-dark-button-border-hover: hsl(var(--contrast-400));\n *   --calendar-dark-selected-button-background: hsl(var(--primary));\n *   --calendar-dark-selected-button-text: hsl(var(--foreground));\n *   --calendar-dark-selected-middle-button-background: transparent;\n *   --calendar-dark-text-disabled: hsl(var(--contrast-300));\n *   --calendar-dark-range-background: color-mix(in oklab, hsl(var(--primary)), white 60%);\n *  }\n * ```\n */\nexport function Calendar({\n  colorScheme = 'light',\n  className,\n  classNames,\n  ...props\n}: CalendarProps) {\n  return (\n    <DayPicker\n      {...props}\n      className={clsx(\n        'box-content w-[280px] rounded-lg border p-3 font-[family-name:var(--calendar-font-family,var(--font-family-body))]',\n        {\n          light:\n            'border-[var(--calendar-light-border,hsl(var(--contrast-100)))] bg-[var(--calendar-light-background,hsl(var(--background)))] text-[var(--calendar-light-text,hsl(var(--foreground)))]',\n          dark: 'border-[var(--calendar-dark-border,hsl(var(--contrast-500)))] bg-[var(--calendar-dark-background,hsl(var(--foreground)))] text-[var(--calendar-dark-text,hsl(var(--background)))]',\n        }[colorScheme],\n        className,\n      )}\n      classNames={{\n        months: 'relative',\n        month_caption: 'flex justify-center w-full font-medium pb-0.5',\n        nav: 'absolute flex justify-between w-full',\n        button_next: clsx(\n          'rotate-180 rounded-full focus-visible:outline-none focus-visible:ring-1',\n          {\n            light: 'focus-visible:ring-[var(--calendar-light-focus,hsl(var(--foreground)))]',\n            dark: 'focus-visible:ring-[var(--calendar-dark-focus,hsl(var(--background)))]',\n          }[colorScheme],\n        ),\n        button_previous: clsx(\n          'rounded-full focus-visible:outline-none focus-visible:ring-1',\n          {\n            light: 'focus-visible:ring-[var(--calendar-light-focus,hsl(var(--foreground)))]',\n            dark: 'focus-visible:ring-[var(--calendar-dark-focus,hsl(var(--background)))]',\n          }[colorScheme],\n        ),\n        month_grid: 'flex flex-col gap-0.5',\n        weeks: 'flex flex-col gap-0.5',\n        weekdays: 'flex',\n        weekday: 'flex h-10 w-10 items-center justify-center text-xs font-medium',\n        week: 'flex',\n        day: 'h-10 w-10 flex text-xs font-medium group p-0',\n        day_button: clsx(\n          'h-full w-full flex items-center justify-center rounded-full focus-visible:outline-none focus-visible:ring-1 disabled:hover:border-none',\n          {\n            light:\n              'group-data-[selected=true]:text-[var(--calendar-light-selected-button-text,hsl(var(--foreground)))] group-data-[selected=true]:bg-[var(--calendar-light-selected-button-background,hsl(var(--primary)))] group-data-[selected=true]/middle:bg-[var(--calendar-light-selected-middle-button-background,transparent)] hover:border hover:border-[var(--calendar-light-button-border-hover,hsl(var(--contrast-200)))] focus-visible:ring-[var(--calendar-light-focus,hsl(var(--foreground)))]',\n            dark: 'group-data-[selected=true]:text-[var(--calendar-dark-selected-button-text,hsl(var(--foreground)))] group-data-[selected=true]:bg-[var(--calendar-dark-selected-button-background,hsl(var(--primary)))] group-data-[selected=true]/middle:bg-[var(--calendar-dark-selected-middle-button-background,transparent)] hover:border hover:border-[var(--calendar-dark-button-border-hover,hsl(var(--contrast-400)))] focus-visible:ring-[var(--calendar-dark-focus,hsl(var(--background)))]',\n          }[colorScheme],\n        ),\n        disabled: clsx(\n          {\n            light: 'text-[var(--calendar-light-text-disabled,hsl(var(--contrast-300)))]',\n            dark: 'text-[var(--calendar-dark-text-disabled,hsl(var(--contrast-300)))]',\n          }[colorScheme],\n        ),\n        outside: clsx(\n          {\n            light: 'text-[var(--calendar-light-text-disabled,hsl(var(--contrast-300)))]',\n            dark: 'text-[var(--calendar-dark-text-disabled,hsl(var(--contrast-300)))]',\n          }[colorScheme],\n        ),\n        range_start: clsx(\n          'bg-gradient-to-l',\n          {\n            light:\n              'from-[var(--calendar-light-range-background,color-mix(in_oklab,hsl(var(--primary)),white_75%))]',\n            dark: 'from-[var(--calendar-dark-range-background,color-mix(in_oklab,hsl(var(--primary)),black_50%))]',\n          }[colorScheme],\n        ),\n        range_middle: clsx(\n          'group/middle',\n          {\n            light:\n              'bg-[var(--calendar-light-range-background,color-mix(in_oklab,hsl(var(--primary)),white_75%))]',\n            dark: 'bg-[var(--calendar-dark-range-background,color-mix(in_oklab,hsl(var(--primary)),black_50%))]',\n          }[colorScheme],\n        ),\n        range_end: clsx(\n          'bg-gradient-to-r',\n          {\n            light:\n              'from-[var(--calendar-light-range-background,color-mix(in_oklab,hsl(var(--primary)),white_75%))]',\n            dark: 'from-[var(--calendar-dark-range-background,color-mix(in_oklab,hsl(var(--primary)),black_50%))]',\n          }[colorScheme],\n        ),\n        ...classNames,\n      }}\n      components={components}\n    />\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/carousel/index.tsx",
    "content": "/* eslint-disable valid-jsdoc */\n'use client';\n\nimport { clsx } from 'clsx';\nimport useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react';\nimport { ArrowLeft, ArrowRight } from 'lucide-react';\nimport { useCallback, useEffect, useState } from 'react';\nimport * as React from 'react';\n\ntype CarouselApi = UseEmblaCarouselType[1];\ntype UseCarouselParameters = Parameters<typeof useEmblaCarousel>;\ntype CarouselOptions = UseCarouselParameters[0];\ntype CarouselPlugin = UseCarouselParameters[1];\n\ninterface CarouselProps extends React.ComponentPropsWithoutRef<'div'> {\n  opts?: CarouselOptions;\n  plugins?: CarouselPlugin;\n  setApi?: (api: CarouselApi) => void;\n  carouselScrollbarLabel?: string;\n  hideOverflow?: boolean;\n}\n\ntype CarouselContextProps = {\n  carouselRef: ReturnType<typeof useEmblaCarousel>[0];\n  api: ReturnType<typeof useEmblaCarousel>[1];\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n} & CarouselProps;\n\nconst CarouselContext = React.createContext<CarouselContextProps | null>(null);\n\nfunction useCarousel() {\n  const context = React.useContext(CarouselContext);\n\n  if (!context) {\n    throw new Error('useCarousel must be used within a <Carousel />');\n  }\n\n  return context;\n}\n\nfunction Carousel({\n  opts,\n  setApi,\n  plugins,\n  className,\n  children,\n  hideOverflow = true,\n  ...rest\n}: CarouselProps) {\n  const [carouselRef, api] = useEmblaCarousel(opts, plugins);\n  const [canScrollPrev, setCanScrollPrev] = useState(false);\n  const [canScrollNext, setCanScrollNext] = useState(false);\n\n  // eslint-disable-next-line @typescript-eslint/no-shadow\n  const onSelect = React.useCallback((api: CarouselApi) => {\n    if (!api) return;\n\n    setCanScrollPrev(api.canGoToPrev());\n    setCanScrollNext(api.canGoToNext());\n  }, []);\n\n  const scrollPrev = useCallback(() => api?.goToPrev(), [api]);\n\n  const scrollNext = useCallback(() => api?.goToNext(), [api]);\n\n  const handleKeyDown = useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (event.key === 'ArrowLeft') {\n        event.preventDefault();\n        scrollPrev();\n      } else if (event.key === 'ArrowRight') {\n        event.preventDefault();\n        scrollNext();\n      }\n    },\n    [scrollPrev, scrollNext],\n  );\n\n  useEffect(() => {\n    if (!api || !setApi) return;\n\n    setApi(api);\n  }, [api, setApi]);\n\n  useEffect(() => {\n    if (!api) return;\n\n    onSelect(api);\n    api.on('reinit', onSelect);\n    api.on('select', onSelect);\n\n    return () => {\n      api.off('select', onSelect);\n    };\n  }, [api, onSelect]);\n\n  return (\n    <CarouselContext.Provider\n      value={{\n        carouselRef,\n        api,\n        opts,\n        scrollPrev,\n        scrollNext,\n        canScrollPrev,\n        canScrollNext,\n      }}\n    >\n      <div\n        {...rest}\n        aria-roledescription=\"carousel\"\n        className={clsx('relative @container', hideOverflow && 'overflow-hidden', className)}\n        onKeyDownCapture={handleKeyDown}\n        role=\"region\"\n      >\n        {children}\n      </div>\n    </CarouselContext.Provider>\n  );\n}\n\nfunction CarouselContent({ className, ...rest }: React.HTMLAttributes<HTMLDivElement>) {\n  const { carouselRef } = useCarousel();\n\n  return (\n    <div className=\"w-full\" ref={carouselRef}>\n      <div {...rest} className={clsx('-ml-4 flex @2xl:-ml-5', className)} />\n    </div>\n  );\n}\n\nfunction CarouselItem({ className, ...rest }: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      {...rest}\n      aria-roledescription=\"slide\"\n      className={clsx('min-w-0 shrink-0 grow-0 pl-4 @2xl:pl-5', className)}\n      role=\"group\"\n    />\n  );\n}\n\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n    --carousel-focus: hsl(var(--primary));\n    --carousel-light-button: hsl(var(--foreground));\n    --carousel-dark-button: hsl(var(--background));\n * }\n * ```\n */\nfunction CarouselButtons({\n  className,\n  colorScheme = 'light',\n  previousLabel = 'Previous',\n  nextLabel = 'Next',\n  ...rest\n}: React.HTMLAttributes<HTMLDivElement> & {\n  colorScheme?: 'light' | 'dark';\n  previousLabel?: string;\n  nextLabel?: string;\n}) {\n  const { scrollPrev, scrollNext, canScrollPrev, canScrollNext } = useCarousel();\n\n  return (\n    <div\n      {...rest}\n      className={clsx(\n        'flex gap-2',\n        {\n          light: 'text-[var(--carousel-light-button,hsl(var(--foreground)))]',\n          dark: 'text-[var(--carousel-dark-button,hsl(var(--background)))]',\n        }[colorScheme],\n        className,\n      )}\n    >\n      <button\n        className=\"rounded-lg ring-[var(--carousel-focus,hsl(var(--primary)))] transition-colors duration-300 focus-visible:outline-0 focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-25\"\n        disabled={!canScrollPrev}\n        onClick={scrollPrev}\n        title={previousLabel}\n      >\n        <ArrowLeft strokeWidth={1.5} />\n      </button>\n      <button\n        className=\"rounded-lg ring-[var(--carousel-focus,hsl(var(--primary)))] transition-colors duration-300 focus-visible:outline-0 focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-25\"\n        disabled={!canScrollNext}\n        onClick={scrollNext}\n        title={nextLabel}\n      >\n        <ArrowRight strokeWidth={1.5} />\n      </button>\n    </div>\n  );\n}\n\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n    --carousel-light-scrollbar: hsl(var(--foreground));\n    --carousel-dark-scrollbar: hsl(var(--background));\n * }\n * ```\n */\nfunction CarouselScrollbar({\n  className,\n  colorScheme = 'light',\n  label = 'Carousel scrollbar',\n}: React.HTMLAttributes<HTMLDivElement> & { label?: string; colorScheme?: 'light' | 'dark' }) {\n  const { api, canScrollPrev, canScrollNext } = useCarousel();\n  const [progress, setProgress] = useState(0);\n  const [scrollbarPosition, setScrollbarPosition] = useState({ width: 0, left: 0 });\n\n  const findClosestSnap = useCallback(\n    (nextProgress: number) => {\n      if (!api) return 0;\n\n      const point = nextProgress / 100;\n      const snapList = api.snapList();\n\n      if (snapList.length === 0) return -1;\n\n      const closestSnap = snapList.reduce((prev, curr) =>\n        Math.abs(curr - point) < Math.abs(prev - point) ? curr : prev,\n      );\n\n      return snapList.findIndex((snap) => snap === closestSnap);\n    },\n    [api],\n  );\n\n  useEffect(() => {\n    if (!api) return;\n\n    const snapList = api.snapList();\n    const closestSnapIndex = findClosestSnap(progress);\n    const scrollbarWidth = 100 / snapList.length;\n    const scrollbarLeft = (closestSnapIndex / snapList.length) * 100;\n\n    setScrollbarPosition({ width: scrollbarWidth, left: scrollbarLeft });\n\n    api.goTo(closestSnapIndex);\n  }, [progress, api, findClosestSnap]);\n\n  useEffect(() => {\n    if (!api) return;\n\n    function onScroll() {\n      if (!api) return;\n\n      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n      setProgress(api.snapList()[api.selectedSnap()]! * 100);\n    }\n\n    api.on('select', onScroll);\n    api.on('scroll', onScroll);\n    api.on('reinit', onScroll);\n\n    return () => {\n      api.off('select', onScroll);\n      api.off('scroll', onScroll);\n      api.off('reinit', onScroll);\n    };\n  }, [api]);\n\n  return (\n    <div\n      className={clsx(\n        'relative flex h-6 w-full max-w-56 items-center overflow-hidden',\n        !canScrollPrev && !canScrollNext && 'pointer-events-none invisible',\n        className,\n      )}\n    >\n      <input\n        aria-label={label}\n        aria-orientation=\"horizontal\"\n        aria-valuenow={progress}\n        aria-valuetext={`${Math.round(progress)}%`}\n        className=\"absolute h-full w-full cursor-pointer appearance-none bg-transparent opacity-0\"\n        max={100}\n        min={0}\n        onChange={(e) => setProgress(e.currentTarget.valueAsNumber)}\n        type=\"range\"\n        value={progress}\n      />\n      {/* Track */}\n      <div\n        className={clsx(\n          'pointer-events-none absolute h-1 w-full rounded-full opacity-10',\n          {\n            light: 'bg-[var(--carousel-light-scrollbar,hsl(var(--foreground)))]',\n            dark: 'bg-[var(--carousel-dark-scrollbar,hsl(var(--background)))]',\n          }[colorScheme],\n        )}\n      />\n\n      {/* Bar */}\n      <div\n        className={clsx(\n          'pointer-events-none absolute h-1 rounded-full transition-all ease-out',\n          {\n            light: 'bg-[var(--carousel-light-scrollbar,hsl(var(--foreground)))]',\n            dark: 'bg-[var(--carousel-dark-scrollbar,hsl(var(--background)))]',\n          }[colorScheme],\n        )}\n        style={{\n          width: `${scrollbarPosition.width}%`,\n          left: `${scrollbarPosition.left}%`,\n        }}\n      />\n    </div>\n  );\n}\n\nexport {\n  type CarouselApi,\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  CarouselButtons,\n  CarouselScrollbar,\n};\n"
  },
  {
    "path": "core/vibes/soul/primitives/chip/index.tsx",
    "content": "import { X } from 'lucide-react';\n\ninterface Props {\n  name?: string;\n  value?: string;\n  children?: React.ReactNode;\n  removeLabel?: string;\n  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;\n}\n\nexport const Chip = function Chip({\n  name,\n  value,\n  children,\n  removeLabel = 'Remove',\n  onClick,\n}: Props) {\n  return (\n    <span className=\"flex h-9 items-center gap-1.5 rounded-lg bg-contrast-100 py-2 pe-2 ps-3 text-sm font-semibold leading-5 text-foreground\">\n      {children}\n      <button\n        className=\"flex h-5 w-5 items-center justify-center rounded-full hover:bg-contrast-200 focus:outline-none focus:ring-1 focus:ring-foreground\"\n        name={name}\n        onClick={onClick}\n        title={removeLabel}\n        value={value}\n      >\n        <X size={12} />\n      </button>\n    </span>\n  );\n};\n"
  },
  {
    "path": "core/vibes/soul/primitives/compare-card/add-to-cart-form.tsx",
    "content": "'use client';\n\nimport { getFormProps, SubmissionResult, useForm } from '@conform-to/react';\nimport { ReactNode, startTransition, useActionState, useEffect } from 'react';\nimport { requestFormReset } from 'react-dom';\n\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { toast } from '@/vibes/soul/primitives/toaster';\nimport { useEvents } from '~/components/analytics/events';\nimport { useRouter } from '~/i18n/routing';\n\ntype Action<S, P> = (state: Awaited<S>, payload: P) => S | Promise<S>;\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  successMessage?: ReactNode;\n}\n\nexport type CompareAddToCartAction = Action<State, FormData>;\n\ninterface Props {\n  disabled?: boolean;\n  productId: string;\n  addToCartLabel: string;\n  preorderLabel: string;\n  isPreorder?: boolean;\n  addToCartAction: CompareAddToCartAction;\n}\n\nexport function AddToCartForm({\n  productId,\n  addToCartLabel,\n  addToCartAction,\n  isPreorder = false,\n  preorderLabel,\n  disabled = false,\n}: Props) {\n  const router = useRouter();\n  const events = useEvents();\n\n  const [{ lastResult, successMessage }, formAction, pending] = useActionState(addToCartAction, {\n    lastResult: null,\n    successMessage: undefined,\n  });\n\n  const [form] = useForm({\n    lastResult,\n    onSubmit(event, { formData }) {\n      event.preventDefault();\n\n      startTransition(() => {\n        requestFormReset(event.currentTarget);\n        formAction(formData);\n\n        events.onAddToCart?.(formData);\n      });\n    },\n  });\n\n  useEffect(() => {\n    if (lastResult?.status === 'success') {\n      toast.success(successMessage);\n\n      // This is needed to refresh the Data Cache after the product has been added to the cart.\n      // The cart id is not picked up after the first time the cart is created/updated.\n      router.refresh();\n    }\n  }, [lastResult, successMessage, router]);\n\n  useEffect(() => {\n    if (form.errors) {\n      form.errors.forEach((error) => {\n        toast.error(error);\n      });\n    }\n  }, [form.errors]);\n\n  return (\n    <form {...getFormProps(form)} action={formAction}>\n      <input name=\"id\" type=\"hidden\" value={productId} />\n      <Button className=\"w-full\" disabled={disabled} loading={pending} size=\"medium\" type=\"submit\">\n        {isPreorder ? preorderLabel : addToCartLabel}\n      </Button>\n    </form>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/compare-card/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { Fragment } from 'react';\n\nimport { ButtonLink } from '@/vibes/soul/primitives/button-link';\nimport {\n  type Product,\n  ProductCard,\n  ProductCardSkeleton,\n} from '@/vibes/soul/primitives/product-card';\nimport { Rating } from '@/vibes/soul/primitives/rating';\nimport { Reveal } from '@/vibes/soul/primitives/reveal';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\n\nimport { AddToCartForm, CompareAddToCartAction } from './add-to-cart-form';\n\nexport interface CompareProduct extends Product {\n  description?: string | React.ReactNode;\n  customFields?: Array<{ name: string; value: string }>;\n  hasVariants?: boolean;\n  disabled?: boolean;\n  isPreorder?: boolean;\n}\n\nexport interface CompareCardProps {\n  className?: string;\n  product: CompareProduct;\n  addToCartLabel?: string;\n  descriptionLabel?: string;\n  noDescriptionLabel?: string;\n  ratingLabel?: string;\n  noRatingsLabel?: string;\n  otherDetailsLabel?: string;\n  noOtherDetailsLabel?: string;\n  viewOptionsLabel?: string;\n  preorderLabel?: string;\n  addToCartAction?: CompareAddToCartAction;\n  imageSizes?: string;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --compare-card-divider: hsl(var(--contrast-100));\n *   --compare-card-label: hsl(var(--foreground));\n *   --compare-card-description: hsl(var(--contrast-400));\n *   --compare-card-field: hsl(var(--foreground));\n *   --compare-card-font-family-primary: var(--font-family-body);\n *   --compare-card-font-family-secondary: var(--font-family-mono);\n * }\n * ```\n */\nexport function CompareCard({\n  className,\n  product,\n  addToCartAction,\n  addToCartLabel = 'Add to cart',\n  descriptionLabel = 'Description',\n  noDescriptionLabel = 'There is no description available.',\n  ratingLabel = 'Rating',\n  noRatingsLabel = 'There are no reviews.',\n  otherDetailsLabel = 'Other details',\n  noOtherDetailsLabel = 'There are no other details.',\n  viewOptionsLabel = 'View options',\n  preorderLabel = 'Preorder',\n  imageSizes,\n}: CompareCardProps) {\n  return (\n    <div\n      className={clsx(\n        'w-full max-w-72 divide-y divide-[var(--compare-card-divider,hsl(var(--contrast-100)))] font-[family-name:var(--compare-card-font-family-primary,var(--font-family-body))] font-normal @container',\n        className,\n      )}\n    >\n      <div className=\"mb-2 space-y-4 pb-4\">\n        <ProductCard imageSizes={imageSizes} product={product} />\n        {addToCartAction &&\n          (product.hasVariants !== undefined && !product.hasVariants ? (\n            <AddToCartForm\n              addToCartAction={addToCartAction}\n              addToCartLabel={addToCartLabel}\n              disabled={product.disabled}\n              isPreorder={product.isPreorder}\n              preorderLabel={preorderLabel}\n              productId={product.id}\n            />\n          ) : (\n            <ButtonLink className=\"w-full\" href={product.href} size=\"medium\">\n              {viewOptionsLabel}\n            </ButtonLink>\n          ))}\n      </div>\n      <div className=\"space-y-4 py-4\">\n        <div className=\"font-[family-name:var(--compare-card-font-family-secondary,var(--font-family-mono))] text-xs font-normal uppercase text-[var(--compare-card-label,hsl(var(--foreground)))]\">\n          {ratingLabel}\n        </div>\n        {product.rating != null ? (\n          <Rating rating={product.rating} />\n        ) : (\n          <p className=\"text-sm text-[var(--compare-card-description,hsl(var(--contrast-400)))]\">\n            {noRatingsLabel}\n          </p>\n        )}\n      </div>\n      <div className=\"space-y-4 py-4\">\n        <div className=\"font-[family-name:var(--compare-card-font-family-secondary,var(--font-family-mono))] text-xs font-normal uppercase text-[var(--compare-card-label,hsl(var(--foreground)))]\">\n          {descriptionLabel}\n        </div>\n        {product.description != null && product.description !== '' ? (\n          <Reveal>\n            <div className=\"prose prose-sm [&>div>*:first-child]:mt-0\">{product.description}</div>\n          </Reveal>\n        ) : (\n          <p className=\"text-sm text-[var(--compare-card-description,hsl(var(--contrast-400)))]\">\n            {noDescriptionLabel}\n          </p>\n        )}\n      </div>\n      {product.customFields != null ? (\n        <div className=\"space-y-4 py-4\">\n          <div className=\"font-[family-name:var(--compare-card-font-family-secondary,var(--font-family-mono))] text-xs font-normal uppercase text-[var(--compare-card-label,hsl(var(--foreground)))]\">\n            {otherDetailsLabel}\n          </div>\n          <Reveal>\n            <dl className=\"grid grid-cols-2 gap-1 text-xs font-normal text-[var(--compare-card-field,hsl(var(--foreground)))]\">\n              {product.customFields.map((field, index) => (\n                <Fragment key={index}>\n                  <dt className=\"font-semibold\">{field.name}: </dt>\n                  <dd>{field.value}</dd>\n                </Fragment>\n              ))}\n            </dl>\n          </Reveal>\n        </div>\n      ) : (\n        <div className=\"space-y-4 py-4\">\n          <div className=\"font-[family-name:var(--compare-card-font-family-secondary,var(--font-family-mono))] text-xs font-normal uppercase text-[var(--compare-card-label,hsl(var(--foreground)))]\">\n            {otherDetailsLabel}\n          </div>\n          <p className=\"text-sm text-[var(--compare-card-description,hsl(var(--contrast-400)))]\">\n            {noOtherDetailsLabel}\n          </p>\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport function CompareCardSkeleton({ className }: { className?: string }) {\n  return (\n    <div\n      className={clsx(\n        'w-full max-w-md divide-y divide-[var(--skeleton,hsl(var(--contrast-300)/15%))] @container',\n        className,\n      )}\n    >\n      <div className=\"mb-2 space-y-4 pb-4\">\n        <ProductCardSkeleton />\n        <Skeleton.Box className=\"h-12 rounded-full\" />\n      </div>\n      <div className=\"space-y-4 py-4 text-xs\">\n        <Skeleton.Text characterCount={10} className=\"rounded\" />\n        <Skeleton.Box className=\"h-6 w-32 rounded\" />\n      </div>\n      <div className=\"space-y-4 py-4 text-xs\">\n        <Skeleton.Text characterCount={12} className=\"rounded\" />\n        <div className=\"text-sm\">\n          <Skeleton.Text characterCount=\"full\" className=\"rounded\" />\n          <Skeleton.Text characterCount={45} className=\"rounded\" />\n          <Skeleton.Text characterCount={40} className=\"rounded\" />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/compare-card/schema.ts",
    "content": "import { z } from 'zod';\n\nexport const compareAddToCartFormDataSchema = z.object({\n  id: z.string(),\n});\n"
  },
  {
    "path": "core/vibes/soul/primitives/compare-drawer/index.tsx",
    "content": "'use client';\n\nimport * as Portal from '@radix-ui/react-portal';\nimport { ArrowRight, X } from 'lucide-react';\nimport { useQueryState } from 'nuqs';\nimport {\n  createContext,\n  ReactNode,\n  startTransition,\n  useContext,\n  useEffect,\n  useOptimistic,\n} from 'react';\n\nimport { ButtonLink } from '@/vibes/soul/primitives/button-link';\nimport { toast } from '@/vibes/soul/primitives/toaster';\nimport { Image } from '~/components/image';\nimport { Link } from '~/components/link';\n\nimport { compareParser } from './loader';\n\ninterface OptimisticAction {\n  type: 'add' | 'remove';\n  item: CompareDrawerItem;\n}\n\ninterface CompareDrawerContext {\n  optimisticItems: CompareDrawerItem[];\n  setOptimisticItems: (action: OptimisticAction) => void;\n  maxItems?: number;\n}\n\nexport const CompareDrawerContext = createContext<CompareDrawerContext>({\n  optimisticItems: [],\n  // eslint-disable-next-line @typescript-eslint/no-empty-function\n  setOptimisticItems: () => {},\n  maxItems: 0,\n});\n\nexport function CompareDrawerProvider({\n  children,\n  items,\n  maxItems,\n  maxCompareLimitMessage = \"You've reached the maximum number of products for comparison. Remove a product to add a new one.\",\n}: {\n  children: ReactNode;\n  items: CompareDrawerItem[];\n  maxItems?: number;\n  maxCompareLimitMessage?: string;\n}) {\n  useEffect(() => {\n    if (maxItems !== undefined && items.length >= maxItems) {\n      toast.warning(maxCompareLimitMessage);\n    }\n  }, [items.length, maxItems, maxCompareLimitMessage]);\n\n  const [optimisticItems, setOptimisticItems] = useOptimistic(\n    items,\n    (state: CompareDrawerItem[], { type, item }: OptimisticAction) => {\n      switch (type) {\n        case 'add':\n          return [...state, item].sort((a, b) => {\n            const numA = Number(a.id);\n            const numB = Number(b.id);\n\n            if (!Number.isNaN(numA) && !Number.isNaN(numB)) {\n              return numA - numB;\n            }\n\n            if (!Number.isNaN(numA)) return -1;\n            if (!Number.isNaN(numB)) return 1;\n\n            return a.id < b.id ? -1 : 1;\n          });\n\n        case 'remove':\n          return state.filter((i) => i.id !== item.id);\n\n        default:\n          return state;\n      }\n    },\n  );\n\n  return (\n    <CompareDrawerContext value={{ optimisticItems, setOptimisticItems, maxItems }}>\n      {children}\n    </CompareDrawerContext>\n  );\n}\n\nexport function useCompareDrawer() {\n  return useContext(CompareDrawerContext);\n}\n\nfunction getInitials(name: string): string {\n  return name\n    .split(' ')\n    .map((word) => word[0])\n    .join('')\n    .toUpperCase()\n    .slice(0, 2);\n}\n\ninterface CompareDrawerItem {\n  id: string;\n  image?: { src: string; alt: string };\n  href: string;\n  title: string;\n}\n\nexport interface CompareDrawerProps {\n  href?: string;\n  paramName?: string;\n  submitLabel?: string;\n  removeLabel?: string;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --compare-drawer-background: hsl(var(--background));\n *   --compare-drawer-font-family: var(--font-family-body);\n *   --compare-drawer-card-focus: hsl(var(--primary));\n *   --compare-drawer-card-border: hsl(var(--contrast-100));\n *   --compare-drawer-card-background: hsl(var(--background));\n *   --compare-drawer-card-background-hover: hsl(var(--contrast-100));\n *   --compare-drawer-card-image-background: hsl(var(--contrast-100));\n *   --compare-drawer-empty-image-text: hsl(var(--primary-shadow));\n *   --compare-drawer-card-text: hsl(var(--foreground));\n *   --compare-drawer-dismiss-border: hsl(var(--contast-100));\n *   --compare-drawer-dismiss-border-hover: hsl(var(--contast-200));\n *   --compare-drawer-dismiss-background: hsl(var(--background));\n *   --compare-drawer-dismiss-background-hover: hsl(var(--contrast-100));\n *   --compare-drawer-dismiss-icon: hsl(var(--contrast-400));\n *   --compare-drawer-dismiss-icon-hover: hsl(var(--foreground));\n * }\n * ```\n */\nexport function CompareDrawer({\n  href = '/compare',\n  paramName = 'compare',\n  submitLabel = 'Compare',\n  removeLabel = 'Remove',\n}: CompareDrawerProps) {\n  const [params, setParam] = useQueryState(paramName, compareParser);\n\n  const { optimisticItems, setOptimisticItems } = useCompareDrawer();\n\n  return (\n    optimisticItems.length > 0 && (\n      <Portal.Root asChild>\n        <div className=\"sticky bottom-0 z-10 w-full border-t border-[var(--compare-drawer-card-border,hsl(var(--contrast-100)))] bg-[var(--compare-drawer-background,hsl(var(--background)))] px-3 py-4 @container @md:py-5 @xl:px-6 @5xl:px-10\">\n          <div className=\"mx-auto flex w-full max-w-7xl flex-col items-start justify-end gap-x-3 gap-y-4 @md:flex-row\">\n            <div className=\"flex flex-1 flex-wrap justify-end gap-4\">\n              {optimisticItems.map((item) => (\n                <div className=\"relative\" key={item.id}>\n                  <Link\n                    className=\"group relative flex max-w-56 items-center overflow-hidden whitespace-nowrap rounded-xl border border-[var(--compare-drawer-link-border,hsl(var(--contrast-100)))] bg-[var(--compare-drawer-card-background,hsl(var(--background)))] font-semibold ring-[var(--compare-drawer-card-focus,hsl(var(--primary)))] transition-all duration-150 hover:bg-[var(--compare-drawer-card-background-hover,hsl(var(--contrast-100)))] focus:outline-none focus:ring-2\"\n                    href={item.href}\n                  >\n                    <div className=\"relative aspect-square w-12 shrink-0 bg-[var(--compare-drawer-card-image-background,hsl(var(--contrast-100)))]\">\n                      {item.image?.src != null ? (\n                        <Image\n                          alt={item.image.alt}\n                          className=\"rounded-lg object-cover @4xl:rounded-r-none\"\n                          fill\n                          sizes=\"3rem\"\n                          src={item.image.src}\n                        />\n                      ) : (\n                        <span className=\"max-w-full break-all p-1 text-xs text-[var(--compare-drawer-empty-image-text,color-mix(in_oklab,hsl(var(--primary)),black_75%))] opacity-20\">\n                          {getInitials(item.title)}\n                        </span>\n                      )}\n                    </div>\n                    <span className=\"hidden truncate pl-3 pr-5 text-[var(--compare-drawer-card-text,hsl(var(--foreground)))] @4xl:block\">\n                      {item.title}\n                    </span>\n                  </Link>\n                  <button\n                    aria-label={`${removeLabel} ${item.title}`}\n                    className=\"hover:text-[var(--compare-drawer-dismiss-icon-hover,hsl(var(--foreground))] absolute -right-2.5 -top-2.5 flex h-7 w-7 items-center justify-center rounded-full border border-[var(--compare-drawer-dismiss-border,hsl(var(--contrast-100)))] bg-[var(--compare-drawer-dismiss-background,hsl(var(--background)))] text-[var(--compare-drawer-dismiss-icon,hsl(var(--contrast-400)))] transition-colors duration-150 hover:border-[var(--compare-drawer-dismiss-border-hover,hsl(var(--contrast-200)))] hover:bg-[var(--compare-drawer-dismiss-background-hover,hsl(var(--contrast-100)))]\"\n                    onClick={() => {\n                      startTransition(async () => {\n                        setOptimisticItems({ type: 'remove', item });\n\n                        await setParam((prev) => {\n                          const next = prev?.filter((v) => v !== item.id) ?? [];\n\n                          return next.length > 0 ? next : null;\n                        });\n                      });\n                    }}\n                    type=\"button\"\n                  >\n                    <X absoluteStrokeWidth size={16} strokeWidth={1.5} />\n                  </button>\n                </div>\n              ))}\n            </div>\n            <ButtonLink\n              className=\"hidden @md:block\"\n              href={`${href}?ids=${params?.toString()}`}\n              size=\"medium\"\n              variant=\"primary\"\n            >\n              <span className=\"inline-flex items-center gap-1\">\n                {submitLabel} <ArrowRight absoluteStrokeWidth size={20} strokeWidth={1} />\n              </span>\n            </ButtonLink>\n            <ButtonLink className=\"w-full @md:hidden\" href={href} size=\"small\" variant=\"primary\">\n              <span className=\"inline-flex items-center gap-1\">\n                {submitLabel} <ArrowRight absoluteStrokeWidth size={16} strokeWidth={1} />\n              </span>\n            </ButtonLink>\n          </div>\n        </div>\n      </Portal.Root>\n    )\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/compare-drawer/loader.tsx",
    "content": "import { createLoader, parseAsArrayOf, parseAsString } from 'nuqs/server';\n\nexport const compareParser = parseAsArrayOf(parseAsString).withOptions({\n  shallow: false,\n  scroll: false,\n});\n\nexport const createCompareLoader = (paramName = 'compare') =>\n  createLoader({ [paramName]: compareParser });\n"
  },
  {
    "path": "core/vibes/soul/primitives/cursor-pagination/index.tsx",
    "content": "'use client';\n\nimport { clsx } from 'clsx';\nimport { ArrowLeft, ArrowRight } from 'lucide-react';\nimport { useSearchParams } from 'next/navigation';\nimport { createSerializer, parseAsString } from 'nuqs';\nimport { Suspense } from 'react';\n\nimport { Streamable, useStreamable } from '@/vibes/soul/lib/streamable';\nimport { Link } from '~/components/link';\n\nexport interface CursorPaginationInfo {\n  startCursorParamName?: string;\n  startCursor?: string | null;\n  endCursorParamName?: string;\n  endCursor?: string | null;\n}\n\ninterface Props {\n  label?: Streamable<string | null>;\n  info: Streamable<CursorPaginationInfo>;\n  previousLabel?: Streamable<string | null>;\n  nextLabel?: Streamable<string | null>;\n  scroll?: boolean;\n}\n\nexport function CursorPagination(props: Props) {\n  return (\n    <Suspense fallback={<CursorPaginationSkeleton />}>\n      <CursorPaginationResolved {...props} />\n    </Suspense>\n  );\n}\n\nfunction CursorPaginationResolved({\n  label: streamableLabel,\n  info,\n  previousLabel: streamablePreviousLabel,\n  nextLabel: streamableNextLabel,\n  scroll,\n}: Props) {\n  const label = useStreamable(streamableLabel) ?? 'pagination';\n  const {\n    startCursorParamName = 'before',\n    endCursorParamName = 'after',\n    startCursor,\n    endCursor,\n  } = useStreamable(info);\n  const searchParams = useSearchParams();\n  const serialize = createSerializer({\n    [startCursorParamName]: parseAsString,\n    [endCursorParamName]: parseAsString,\n  });\n  const previousLabel = useStreamable(streamablePreviousLabel) ?? 'Go to previous page';\n  const nextLabel = useStreamable(streamableNextLabel) ?? 'Go to next page';\n\n  return (\n    <nav aria-label={label} className=\"py-10\" role=\"navigation\">\n      <ul className=\"flex items-center justify-center gap-3\">\n        <li>\n          {startCursor != null ? (\n            <PaginationLink\n              aria-label={previousLabel}\n              href={serialize(searchParams, {\n                [startCursorParamName]: startCursor,\n                [endCursorParamName]: null,\n              })}\n              scroll={scroll}\n            >\n              <ArrowLeft size={24} strokeWidth={1} />\n            </PaginationLink>\n          ) : (\n            <SkeletonLink>\n              <ArrowLeft size={24} strokeWidth={1} />\n            </SkeletonLink>\n          )}\n        </li>\n        <li>\n          {endCursor != null ? (\n            <PaginationLink\n              aria-label={nextLabel}\n              href={serialize(searchParams, {\n                [endCursorParamName]: endCursor,\n                [startCursorParamName]: null,\n              })}\n              scroll={scroll}\n            >\n              <ArrowRight size={24} strokeWidth={1} />\n            </PaginationLink>\n          ) : (\n            <SkeletonLink>\n              <ArrowRight size={24} strokeWidth={1} />\n            </SkeletonLink>\n          )}\n        </li>\n      </ul>\n    </nav>\n  );\n}\n\nfunction PaginationLink({\n  href,\n  children,\n  scroll,\n  'aria-label': ariaLabel,\n}: {\n  href: string;\n  children: React.ReactNode;\n  scroll?: boolean;\n  ['aria-label']?: string;\n}) {\n  return (\n    <Link\n      aria-label={ariaLabel}\n      className={clsx(\n        'flex h-12 w-12 items-center justify-center rounded-full border border-contrast-100 text-foreground ring-primary transition-colors duration-300 hover:border-contrast-200 hover:bg-contrast-100 focus-visible:outline-0 focus-visible:ring-2',\n      )}\n      href={href}\n      scroll={scroll}\n    >\n      {children}\n    </Link>\n  );\n}\n\nfunction SkeletonLink({ children }: { children: React.ReactNode }) {\n  return (\n    <div className=\"flex h-12 w-12 cursor-not-allowed items-center justify-center rounded-full border border-contrast-100 text-foreground opacity-50 duration-300\">\n      {children}\n    </div>\n  );\n}\n\nexport function CursorPaginationSkeleton() {\n  return (\n    <div className=\"flex w-full justify-center bg-background py-10 text-xs\">\n      <div className=\"flex gap-2\">\n        <SkeletonLink>\n          <ArrowLeft />\n        </SkeletonLink>\n        <SkeletonLink>\n          <ArrowRight />\n        </SkeletonLink>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/dropdown-menu/index.tsx",
    "content": "'use client';\n\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\nimport { clsx } from 'clsx';\nimport React, { PropsWithChildren } from 'react';\n\nimport { Link } from '~/components/link';\n\nexport interface DropdownMenuItem {\n  className?: string;\n  disabled?: boolean;\n  label: React.ReactNode;\n  component?: React.ReactNode;\n  variant?: 'default' | 'danger';\n  action?: string | ((event: React.MouseEvent<HTMLDivElement>) => void);\n  asChild?: boolean;\n}\n\ninterface Props extends PropsWithChildren {\n  className?: string;\n  items: Array<DropdownMenuItem | 'separator'>;\n  align?: 'center' | 'end' | 'start' | undefined;\n  slideOffset?: number;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --dropdown-menu-background: hsl(var(--background));\n *   --dropdown-menu-item-focus: hsl(var(--primary));\n *   --dropdown-menu-item-text: hsl(var(--contrast-500));\n *   --dropdown-menu-item-text-hover: hsl(var(--foreground));\n *   --dropdown-menu-item-danger-text: hsl(var(--error));\n *   --dropdown-menu-item-danger-text-hover: color-mix(in oklab, hsl(var(--error)), black 75%);\n *   --dropdown-menu-item-background: transparent;\n *   --dropdown-menu-item-background-hover: hsl(var(--contrast-100));\n *   --dropdown-menu-item-danger-background: hsl(var(--error));\n *   --dropdown-menu-item-danger-background-hover: color-mix(in oklab, hsl(var(--error)), white 75%);\n *   --dropdown-menu-item-font-family: var(--font-family-body);\n * }\n * ```\n */\nexport const DropdownMenu = ({\n  className = '',\n  items,\n  open,\n  onOpenChange,\n  align = 'end',\n  slideOffset = 6,\n  children,\n}: Props) => {\n  return (\n    <DropdownMenuPrimitive.Root onOpenChange={onOpenChange} open={open}>\n      <DropdownMenuPrimitive.Trigger asChild>{children}</DropdownMenuPrimitive.Trigger>\n      <DropdownMenuPrimitive.Portal>\n        <DropdownMenuPrimitive.Content\n          align={align}\n          className={clsx(\n            'z-50 max-h-80 max-w-lg overflow-y-auto rounded-2xl bg-[var(--dropdown-menu-background,hsl(var(--background)))] p-2 shadow-xl ring-1 ring-contrast-100 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 @4xl:w-32 @4xl:rounded-2xl @4xl:p-2',\n            className,\n          )}\n          sideOffset={slideOffset}\n        >\n          {items.map((item, index) => {\n            if (item === 'separator') {\n              return (\n                <DropdownMenuPrimitive.Separator\n                  className=\"my-1.5 h-[1px] bg-contrast-100\"\n                  key={`dropdown-separator-${index}`}\n                />\n              );\n            }\n\n            const {\n              className: itemClassName = '',\n              action,\n              label,\n              variant = 'default',\n              disabled,\n              asChild,\n            } = item;\n\n            const itemLabel =\n              typeof action === 'string' && !disabled ? (\n                <Link className=\"block\" href={action}>\n                  {label}\n                </Link>\n              ) : (\n                label\n              );\n\n            const labelIsComponent = Boolean(\n              itemLabel && typeof itemLabel === 'object' && 'props' in itemLabel,\n            );\n\n            return (\n              <DropdownMenuPrimitive.Item\n                asChild={asChild ?? labelIsComponent}\n                className={clsx(\n                  'cursor-default rounded-lg bg-[var(--dropdown-menu-item-background,transparent)] px-3 py-2 font-[family-name:var(--dropdown-menu-item-font-family,var(--font-family-body))] text-sm font-medium outline-none transition-colors data-[disabled]:cursor-not-allowed data-[disabled]:bg-contrast-100/50 data-[disabled]:text-contrast-300/95',\n                  {\n                    default:\n                      'text-[var(--dropdown-menu-item-text,hsl(var(--contrast-500)))] ring-[var(--dropdown-menu-item-focus,hsl(var(--primary)))] [&:not([data-disabled])]:hover:bg-[var(--dropdown-menu-item-background-hover,hsl(var(--contrast-100)))] [&:not([data-disabled])]:hover:text-[var(--dropdown-menu-item-text-hover,hsl(var(--foreground)))] [&:not([data-disabled])]:data-[highlighted]:bg-[var(--dropdown-menu-item-background-hover,hsl(var(--contrast-100)))] [&:not([data-disabled])]:data-[highlighted]:text-[var(--dropdown-menu-item-text-hover,hsl(var(--foreground)))]',\n                    danger:\n                      'text-[var(--dropdown-menu-item-danger-text,hsl(var(--error)))] ring-[var(--dropdown-menu-item-focus,hsl(var(--primary)))] [&:not([data-disabled])]:hover:bg-[var(--dropdown-menu-item-danger-background-hover,color-mix(in_oklab,_hsl(var(--error)),_white_75%))] [&:not([data-disabled])]:hover:text-[var(--dropdown-menu-item-danger-text-hover,color-mix(in_oklab,_hsl(var(--error)),_black_75%))] [&:not([data-disabled])]:data-[highlighted]:bg-[var(--dropdown-menu-item-danger-background-hover,color-mix(in_oklab,_hsl(var(--error)),_white_75%))] [&:not([data-disabled])]:data-[highlighted]:text-[var(--dropdown-menu-item-danger-text-hover,color-mix(in_oklab,_hsl(var(--error)),_black_75%))]',\n                  }[variant],\n                  itemClassName,\n                )}\n                disabled={disabled}\n                key={`dropdown-item-${index}`}\n                onClick={!disabled && action && typeof action === 'function' ? action : undefined}\n              >\n                {itemLabel}\n              </DropdownMenuPrimitive.Item>\n            );\n          })}\n        </DropdownMenuPrimitive.Content>\n      </DropdownMenuPrimitive.Portal>\n    </DropdownMenuPrimitive.Root>\n  );\n};\n"
  },
  {
    "path": "core/vibes/soul/primitives/favorite/heart.tsx",
    "content": "import { clsx } from 'clsx';\n\nimport './styles.css';\n\nexport function Heart({ filled = false, title = 'Heart' }: { filled?: boolean; title?: string }) {\n  return (\n    <svg\n      className=\"group-active:heart-pulse transform-gpu transition-transform duration-300 ease-out group-active:scale-75 sm:group-hover:scale-110\"\n      fill=\"none\"\n      height=\"21\"\n      viewBox=\"0 0 20 21\"\n      width=\"20\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <title>{title}</title>\n      {/* Line Heart */}\n      <path\n        className={clsx({\n          '-translate-x-px -translate-y-px scale-110 opacity-0 transition-[opacity,transform] delay-100':\n            filled,\n        })}\n        d=\"M17.3666 4.34166C16.941 3.91583 16.4356 3.57803 15.8794 3.34757C15.3232 3.1171 14.727 2.99847 14.1249 2.99847C13.5229 2.99847 12.9267 3.1171 12.3705 3.34757C11.8143 3.57803 11.3089 3.91583 10.8833 4.34166L9.99994 5.225L9.1166 4.34166C8.25686 3.48192 7.0908 2.99892 5.87494 2.99892C4.65908 2.99892 3.49301 3.48192 2.63327 4.34166C1.77353 5.20141 1.29053 6.36747 1.29053 7.58333C1.29053 8.79919 1.77353 9.96525 2.63327 10.825L3.5166 11.7083L9.99994 18.1917L16.4833 11.7083L17.3666 10.825C17.7924 10.3994 18.1302 9.89401 18.3607 9.33779C18.5912 8.78158 18.7098 8.1854 18.7098 7.58333C18.7098 6.98126 18.5912 6.38508 18.3607 5.82887C18.1302 5.27265 17.7924 4.76729 17.3666 4.34166Z\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      {/* Inner Filler Heart */}\n      <path\n        className={clsx(\n          'origin-center transition-transform duration-300 ease-out',\n          filled ? 'scale-100 fill-current' : 'scale-0',\n        )}\n        d=\"M17.3666 4.34166C16.941 3.91583 16.4356 3.57803 15.8794 3.34757C15.3232 3.1171 14.727 2.99847 14.1249 2.99847C13.5229 2.99847 12.9267 3.1171 12.3705 3.34757C11.8143 3.57803 11.3089 3.91583 10.8833 4.34166L9.99994 5.225L9.1166 4.34166C8.25686 3.48192 7.0908 2.99892 5.87494 2.99892C4.65908 2.99892 3.49301 3.48192 2.63327 4.34166C1.77353 5.20141 1.29053 6.36747 1.29053 7.58333C1.29053 8.79919 1.77353 9.96525 2.63327 10.825L3.5166 11.7083L9.99994 18.1917L16.4833 11.7083L17.3666 10.825C17.7924 10.3994 18.1302 9.89401 18.3607 9.33779C18.5912 8.78158 18.7098 8.1854 18.7098 7.58333C18.7098 6.98126 18.5912 6.38508 18.3607 5.82887C18.1302 5.27265 17.7924 4.76729 17.3666 4.34166Z\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/favorite/index.tsx",
    "content": "import * as Toggle from '@radix-ui/react-toggle';\n\nimport { Heart } from '@/vibes/soul/primitives/favorite/heart';\n\nexport interface FavoriteProps {\n  label?: string;\n  checked?: boolean;\n  setChecked?: (liked: boolean) => void;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --favorite-focus: hsl(var(--primary));\n *   --favorite-border: hsl(var(--contrast-100));\n *   --favorite-icon: hsl(var(--foreground));\n *   --favorite-on-background: hsl(var(--contrast-100));\n *   --favorite-off-border: hsl(var(--contrast-200));\n * }\n * ```\n */\nexport const Favorite = ({ checked = false, setChecked, label = 'Favorite' }: FavoriteProps) => {\n  return (\n    <Toggle.Root\n      className=\"group relative flex h-[50px] w-[50px] shrink-0 cursor-pointer items-center justify-center rounded-full border border-[var(--favorite-border,hsl(var(--contrast-100)))] text-[var(--favorite-icon,hsl(var(--foreground)))] ring-[var(--favorite-focus,hsl(var(--primary)))] transition-[colors,transform] duration-300 focus-within:outline-none focus-within:ring-2 data-[state=on]:bg-[var(--favorite-on-background,hsl(var(--contrast-100)))] data-[state=off]:hover:border-[var(--favorite-off-border,hsl(var(--contrast-200)))]\"\n      onPressedChange={setChecked}\n      pressed={checked}\n    >\n      <Heart filled={checked} title={label} />\n    </Toggle.Root>\n  );\n};\n"
  },
  {
    "path": "core/vibes/soul/primitives/favorite/styles.css",
    "content": ".heart-pulse {\n  animation: heart-pulse 0.75s forwards;\n}\n\n@keyframes heart-pulse {\n  0% {\n    transform: scale(1);\n  }\n  50% {\n    transform: scale(1.3);\n  }\n  100% {\n    transform: scale(1);\n  }\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/gift-certificate-card/gift-certificate-card-logo.tsx",
    "content": "import { clsx } from 'clsx';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Image } from '~/components/image';\n\ninterface Props {\n  className?: string;\n  logo?: Streamable<string | { src: string; alt: string } | null>;\n  label?: string;\n}\n\nexport function GiftCertificateCardLogo({ className, logo: streamableLogo, label }: Props) {\n  return (\n    <Stream\n      fallback={<div className=\"h-6 w-16 animate-pulse rounded-md bg-contrast-100\" />}\n      value={streamableLogo}\n    >\n      {(logo) => (\n        <div\n          aria-label={label}\n          className={clsx(\n            'relative font-[family-name:var(--logo-font-family,var(--font-family-heading))] font-semibold leading-none',\n            className,\n          )}\n        >\n          {typeof logo === 'object' && logo !== null && logo.src !== '' ? (\n            <Image\n              alt={logo.alt}\n              className=\"h-auto w-full object-left\"\n              fill\n              src={logo.src}\n              style={{ objectFit: 'contain' }}\n            />\n          ) : (\n            typeof logo === 'string' && <span>{logo}</span>\n          )}\n        </div>\n      )}\n    </Stream>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/gift-certificate-card/index.tsx",
    "content": "import { clsx } from 'clsx';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Badge } from '@/vibes/soul/primitives/badge';\nimport { GiftCertificateCardLogo } from '@/vibes/soul/primitives/gift-certificate-card/gift-certificate-card-logo';\nimport { GiftCertificateStatus } from '@/vibes/soul/sections/gift-certificate-balance-section';\n\ninterface Props {\n  balance?: Streamable<string>;\n  expiresAt?: Streamable<string | null>;\n  expiresAtLabel?: string;\n  status?: Streamable<GiftCertificateStatus>;\n  logo?: Streamable<string | { src: string; alt: string } | null>;\n  logoLabel?: string;\n  className?: string;\n  loading?: boolean;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --gift-certificate-background-gradient-top: #212B1B;\n *   --gift-certificate-background-gradient-bottom: #3C4E31;\n * }\n * ```\n */\nexport function GiftCertificateCard({\n  balance: streamableBalance,\n  expiresAtLabel = 'Valid thru',\n  expiresAt: streamableExpiresAt,\n  status: streamableStatus,\n  loading,\n  logo,\n  logoLabel,\n  className,\n}: Props) {\n  return (\n    <div\n      className={clsx(\n        'flex aspect-[16/9] w-full flex-col justify-between overflow-hidden rounded-2xl bg-gradient-to-b from-[var(--gift-certificate-background-gradient-top,#212B1B)] to-[var(--gift-certificate-background-gradient-bottom,#3C4E31)] px-6 py-4 text-primary-highlight @container',\n        className,\n      )}\n    >\n      <div className=\"flex min-h-8 items-center justify-start self-stretch\">\n        <GiftCertificateCardLogo\n          className=\"h-[clamp(1.25rem,8cqw,2.25rem)] flex-1 text-[clamp(1.25rem,8cqw,2.25rem)]\"\n          label={logoLabel}\n          logo={logo}\n        />\n        <Stream fallback={null} value={streamableStatus}>\n          {(status) =>\n            !loading &&\n            status != null &&\n            status !== 'ACTIVE' && (\n              <Badge variant={status === 'PENDING' ? 'info' : 'error'}>{status}</Badge>\n            )\n          }\n        </Stream>\n      </div>\n\n      <div className=\"flex justify-between font-heading text-[clamp(1.5rem,14cqw,4rem)] leading-none text-white\">\n        <div className=\"flex flex-col justify-end font-body text-[clamp(0.7rem,4cqw,2rem)]\">\n          <Stream fallback={null} value={streamableExpiresAt}>\n            {(expiresAt) =>\n              !loading &&\n              expiresAt != null && (\n                <div className=\"flex flex-col space-y-1 @xs:space-y-2\">\n                  <span className=\"text-primary\">{expiresAtLabel}</span>\n                  <time dateTime={expiresAt}>{expiresAt}</time>\n                </div>\n              )\n            }\n          </Stream>\n        </div>\n        <Stream fallback={<span className=\"animate-pulse\">....</span>} value={streamableBalance}>\n          {(balance) => (\n            <span className={loading ? 'animate-pulse' : ''}>\n              {loading ? '....' : (balance ?? '....')}\n            </span>\n          )}\n        </Stream>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/inline-email-form/index.tsx",
    "content": "'use client';\n\nimport { getFormProps, getInputProps, SubmissionResult, useForm } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { clsx } from 'clsx';\nimport { ArrowRight } from 'lucide-react';\nimport { useTranslations } from 'next-intl';\nimport { useActionState } from 'react';\n\nimport { FieldError } from '@/vibes/soul/form/field-error';\nimport { FormStatus } from '@/vibes/soul/form/form-status';\nimport { Button } from '@/vibes/soul/primitives/button';\n\nimport { schema } from './schema';\n\ntype Action<State, Payload> = (\n  prevState: Awaited<State>,\n  formData: Payload,\n) => State | Promise<State>;\n\nexport function InlineEmailForm({\n  className,\n  action,\n  submitLabel = 'Submit',\n  placeholder = 'Enter your email',\n}: {\n  className?: string;\n  placeholder?: string;\n  submitLabel?: string;\n  action: Action<{ lastResult: SubmissionResult | null; successMessage?: string }, FormData>;\n}) {\n  const t = useTranslations('Components.Subscribe');\n  const subscribeSchema = schema({\n    requiredMessage: t('Errors.emailRequired'),\n    invalidMessage: t('Errors.invalidEmail'),\n  });\n\n  const [{ lastResult, successMessage }, formAction, isPending] = useActionState(action, {\n    lastResult: null,\n  });\n\n  const [form, fields] = useForm({\n    lastResult,\n    onValidate({ formData }) {\n      return parseWithZod(formData, { schema: subscribeSchema });\n    },\n    shouldValidate: 'onSubmit',\n    shouldRevalidate: 'onInput',\n  });\n\n  return (\n    <form {...getFormProps(form)} action={formAction} className={clsx('space-y-2', className)}>\n      <div\n        className={clsx(\n          'relative rounded-xl border bg-background text-base transition-colors duration-200 focus-within:border-primary focus:outline-none',\n          form.errors?.length || fields.email.errors?.length\n            ? 'border-error focus-within:border-error'\n            : 'border-black focus-within:border-primary',\n        )}\n      >\n        <input\n          {...getInputProps(fields.email, { type: 'email' })}\n          className=\"placeholder-contrast-gray-500 h-14 w-full bg-transparent pl-5 pr-16 text-foreground placeholder:font-normal focus:outline-none\"\n          data-1p-ignore\n          key={fields.email.id}\n          placeholder={placeholder}\n        />\n        <div className=\"absolute right-0 top-1/2 -translate-y-1/2 pr-2\">\n          <Button\n            aria-label={submitLabel}\n            loading={isPending}\n            shape=\"circle\"\n            size=\"small\"\n            type=\"submit\"\n            variant=\"secondary\"\n          >\n            <ArrowRight size={20} strokeWidth={1.5} />\n          </Button>\n        </div>\n      </div>\n      {fields.email.errors?.map((error) => (\n        <FieldError key={error}>{error}</FieldError>\n      ))}\n      {form.errors?.map((error, index) => (\n        <FormStatus key={index} type=\"error\">\n          {error}\n        </FormStatus>\n      ))}\n      {form.status === 'success' && successMessage != null && (\n        <FormStatus>{successMessage}</FormStatus>\n      )}\n    </form>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/inline-email-form/schema.ts",
    "content": "import { z } from 'zod';\n\nexport const schema = ({\n  requiredMessage = 'Email is required',\n  invalidMessage = 'Please enter a valid email address',\n}: {\n  requiredMessage: string;\n  invalidMessage: string;\n}) =>\n  z.object({\n    email: z.string({ required_error: requiredMessage }).email({ message: invalidMessage }),\n  });\n"
  },
  {
    "path": "core/vibes/soul/primitives/logo/index.tsx",
    "content": "import { clsx } from 'clsx';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Image } from '~/components/image';\nimport { Link } from '~/components/link';\n\ninterface Props {\n  className?: string;\n  logo?: Streamable<string | { src: string; alt: string } | null>;\n  label?: string;\n  href: string;\n  width: number;\n  height: number;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --logo-focus: hsl(var(--primary));\n *   --logo-font-family: var(--font-family-heading);\n *   --logo-text: hsl(var(--foreground));\n * }\n * ```\n */\nexport function Logo({ className, logo: streamableLogo, href, width, height, label }: Props) {\n  return (\n    <Stream\n      fallback={<div className=\"h-6 w-16 animate-pulse rounded-md bg-contrast-100\" />}\n      value={streamableLogo}\n    >\n      {(logo) => (\n        <Link\n          aria-label={label}\n          className={clsx(\n            'relative inline-block outline-0 ring-[var(--logo-focus,hsl(var(--primary)))] ring-offset-4 focus-visible:ring-2',\n            className,\n          )}\n          href={href}\n          style={typeof logo === 'string' ? {} : { maxWidth: `${width}px`, height: `${height}px` }}\n        >\n          {typeof logo === 'object' && logo !== null && logo.src !== '' ? (\n            <Image\n              alt={logo.alt}\n              className=\"h-auto w-full object-contain object-left\"\n              height={height}\n              src={logo.src}\n              width={width}\n            />\n          ) : (\n            typeof logo === 'string' && (\n              <span className=\"font-[family-name:var(--logo-font-family,var(--font-family-heading))] text-lg font-semibold leading-none text-[var(--logo-text,hsl(var(--foreground)))] @xl:text-2xl\">\n                {logo}\n              </span>\n            )\n          )}\n        </Link>\n      )}\n    </Stream>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/modal/index.tsx",
    "content": "import * as Dialog from '@radix-ui/react-dialog';\nimport { clsx } from 'clsx';\nimport { XIcon } from 'lucide-react';\n\nimport { Button } from '@/vibes/soul/primitives/button';\n\nexport interface ModalProps extends React.PropsWithChildren {\n  className?: string;\n  isOpen?: boolean;\n  setOpen?: (open: boolean) => void;\n  /** Title should always be given for screen reader support. */\n  title: string;\n  /** Element to trigger the modal. Not required if the modal is being controlled manually. */\n  trigger?: React.ReactNode;\n  /** If `true`, a user will be required to make a choice by clicking on one of the provided actions. Defaults to `false`. */\n  required?: boolean;\n  /** Hides the header / top of the modal. */\n  hideHeader?: boolean;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --modal-background: hsl(var(--background));\n *   --modal-overlay-background: hsl(var(--foreground)/50%);\n * }\n * ```\n */\nexport const Modal = ({\n  className = '',\n  isOpen,\n  setOpen,\n  title,\n  trigger,\n  children,\n  required = false,\n  hideHeader = false,\n}: ModalProps) => {\n  return (\n    <Dialog.Root onOpenChange={setOpen} open={isOpen}>\n      {trigger != null && <Dialog.Trigger asChild>{trigger}</Dialog.Trigger>}\n      <Dialog.Portal>\n        <Dialog.Overlay className=\"fixed inset-0 z-30 flex items-center justify-center bg-[var(--modal-overlay-background,hsl(var(--foreground)/50%))] @container\">\n          <Dialog.Content\n            className={clsx(\n              'mx-3 my-10 max-h-[90%] max-w-3xl overflow-y-auto rounded-2xl bg-[var(--modal-background,hsl(var(--background)))]',\n              'transition ease-out',\n              'data-[state=closed]:duration-200 data-[state=open]:duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out',\n              'focus:outline-none data-[state=closed]:slide-out-to-bottom-16 data-[state=open]:slide-in-from-bottom-16',\n              className,\n            )}\n            onEscapeKeyDown={required ? (event) => event.preventDefault() : undefined}\n            onInteractOutside={required ? (event) => event.preventDefault() : undefined}\n            onPointerDownOutside={required ? (event) => event.preventDefault() : undefined}\n          >\n            <div className=\"flex flex-col\">\n              <div\n                className={clsx(\n                  'flex min-h-10 flex-row items-center py-3 pl-5',\n                  hideHeader ? 'sr-only' : '',\n                )}\n              >\n                <Dialog.Title asChild>\n                  <h1 className=\"flex-1 pr-4 text-base font-semibold leading-none\">{title}</h1>\n                </Dialog.Title>\n                {!(required || hideHeader) && (\n                  <div className=\"flex items-center justify-center pr-3\">\n                    <Dialog.Close asChild>\n                      <Button shape=\"circle\" size=\"x-small\" variant=\"ghost\">\n                        <XIcon size={20} />\n                      </Button>\n                    </Dialog.Close>\n                  </div>\n                )}\n              </div>\n              <div className={clsx('mb-5 flex-1 px-5', hideHeader ? 'mt-5' : '')}>{children}</div>\n            </div>\n          </Dialog.Content>\n        </Dialog.Overlay>\n      </Dialog.Portal>\n    </Dialog.Root>\n  );\n};\n"
  },
  {
    "path": "core/vibes/soul/primitives/navigation/index.tsx",
    "content": "'use client';\n\nimport { SubmissionResult, useForm } from '@conform-to/react';\nimport * as DropdownMenu from '@radix-ui/react-dropdown-menu';\nimport * as NavigationMenu from '@radix-ui/react-navigation-menu';\nimport * as Popover from '@radix-ui/react-popover';\nimport { clsx } from 'clsx';\nimport debounce from 'lodash.debounce';\nimport {\n  ArrowRight,\n  ChevronDown,\n  GiftIcon,\n  Search,\n  SearchIcon,\n  ShoppingBag,\n  User,\n} from 'lucide-react';\nimport { useParams, useSearchParams } from 'next/navigation';\nimport React, {\n  forwardRef,\n  Ref,\n  useActionState,\n  useCallback,\n  useEffect,\n  useMemo,\n  useState,\n  useTransition,\n} from 'react';\nimport { useFormStatus } from 'react-dom';\n\nimport { FormStatus } from '@/vibes/soul/form/form-status';\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { Logo } from '@/vibes/soul/primitives/logo';\nimport { Price } from '@/vibes/soul/primitives/price-label';\nimport { ProductCard } from '@/vibes/soul/primitives/product-card';\nimport { Link } from '~/components/link';\nimport { usePathname, useRouter } from '~/i18n/routing';\nimport { useSearch } from '~/lib/search';\n\ninterface Link {\n  label: string;\n  href: string;\n  groups?: Array<{\n    label?: string;\n    href?: string;\n    links: Array<{\n      label: string;\n      href: string;\n    }>;\n  }>;\n}\n\ninterface Locale {\n  id: string;\n  label: string;\n}\n\ninterface Currency {\n  id: string;\n  label: string;\n}\n\ntype Action<State, Payload> = (\n  state: Awaited<State>,\n  payload: Awaited<Payload>,\n) => State | Promise<State>;\n\nexport type SearchResult =\n  | {\n      type: 'products';\n      title: string;\n      products: Array<{\n        id: string;\n        title: string;\n        href: string;\n        price?: Price;\n        image?: { src: string; alt: string };\n      }>;\n    }\n  | {\n      type: 'links';\n      title: string;\n      links: Array<{ label: string; href: string }>;\n    };\n\ntype CurrencyAction = Action<SubmissionResult | null, FormData>;\ntype SearchAction<S extends SearchResult> = Action<\n  {\n    searchResults: S[] | null;\n    lastResult: SubmissionResult | null;\n    emptyStateTitle?: string;\n    emptyStateSubtitle?: string;\n  },\n  FormData\n>;\n\ninterface Props<S extends SearchResult> {\n  className?: string;\n  isFloating?: boolean;\n  accountHref: string;\n  cartCount?: Streamable<number | null>;\n  cartHref: string;\n  links: Streamable<Link[]>;\n  linksPosition?: 'center' | 'left' | 'right';\n  locales?: Locale[];\n  activeLocaleId?: string;\n  currencies?: Currency[];\n  activeCurrencyId?: Streamable<string | undefined>;\n  currencyAction?: CurrencyAction;\n  logo?: Streamable<string | { src: string; alt: string } | null>;\n  logoWidth?: number;\n  logoHeight?: number;\n  logoHref?: string;\n  logoLabel?: string;\n  mobileLogo?: Streamable<string | { src: string; alt: string } | null>;\n  mobileLogoWidth?: number;\n  mobileLogoHeight?: number;\n  searchHref: string;\n  searchParamName?: string;\n  searchAction?: SearchAction<S>;\n  searchInputPlaceholder?: string;\n  searchSubmitLabel?: string;\n  cartLabel?: string;\n  accountLabel?: string;\n  openSearchPopupLabel?: string;\n  searchLabel?: string;\n  mobileMenuTriggerLabel?: string;\n  switchCurrencyLabel?: string;\n  giftCertificatesLabel?: string;\n  giftCertificatesHref: string;\n  giftCertificatesEnabled?: Streamable<boolean>;\n}\n\nconst MobileMenuButton = forwardRef<\n  React.ComponentRef<'button'>,\n  { open: boolean } & React.ComponentPropsWithoutRef<'button'>\n>(({ open, className, ...rest }, ref) => {\n  return (\n    <button\n      {...rest}\n      className={clsx(\n        'group relative rounded-lg p-2 outline-0 ring-[var(--nav-focus,hsl(var(--primary)))] transition-colors focus-visible:ring-2',\n        className,\n      )}\n      ref={ref}\n    >\n      <div className=\"flex h-4 w-4 origin-center transform flex-col justify-between overflow-hidden transition-all duration-300\">\n        <div\n          className={clsx(\n            'h-px origin-left transform bg-[var(--nav-mobile-button-icon,hsl(var(--foreground)))] transition-all duration-300',\n            open ? 'translate-x-10' : 'w-7',\n          )}\n        />\n        <div\n          className={clsx(\n            'h-px transform rounded bg-[var(--nav-mobile-button-icon,hsl(var(--foreground)))] transition-all delay-75 duration-300',\n            open ? 'translate-x-10' : 'w-7',\n          )}\n        />\n        <div\n          className={clsx(\n            'h-px origin-left transform bg-[var(--nav-mobile-button-icon,hsl(var(--foreground)))] transition-all delay-150 duration-300',\n            open ? 'translate-x-10' : 'w-7',\n          )}\n        />\n\n        <div\n          className={clsx(\n            'absolute top-2 flex transform items-center justify-between bg-[var(--nav-mobile-button-icon,hsl(var(--foreground)))] transition-all duration-500',\n            open ? 'w-12 translate-x-0' : 'w-0 -translate-x-10',\n          )}\n        >\n          <div\n            className={clsx(\n              'absolute h-px w-4 transform bg-[var(--nav-mobile-button-icon,hsl(var(--foreground)))] transition-all delay-300 duration-500',\n              open ? 'rotate-45' : 'rotate-0',\n            )}\n          />\n          <div\n            className={clsx(\n              'absolute h-px w-4 transform bg-[var(--nav-mobile-button-icon,hsl(var(--foreground)))] transition-all delay-300 duration-500',\n              open ? '-rotate-45' : 'rotate-0',\n            )}\n          />\n        </div>\n      </div>\n    </button>\n  );\n});\n\nMobileMenuButton.displayName = 'MobileMenuButton';\n\nconst navGroupClassName =\n  'block rounded-lg bg-[var(--nav-group-background,transparent)] px-3 py-2 font-[family-name:var(--nav-group-font-family,var(--font-family-body))] font-medium text-[var(--nav-group-text,hsl(var(--foreground)))] ring-[var(--nav-focus,hsl(var(--primary)))] transition-colors hover:bg-[var(--nav-group-background-hover,hsl(var(--contrast-100)))] hover:text-[var(--nav-group-text-hover,hsl(var(--foreground)))] focus-visible:outline-0 focus-visible:ring-2';\nconst navButtonClassName =\n  'relative rounded-lg bg-[var(--nav-button-background,transparent)] p-1.5 text-[var(--nav-button-icon,hsl(var(--foreground)))] ring-[var(--nav-focus,hsl(var(--primary)))] transition-colors focus-visible:outline-0 focus-visible:ring-2 @4xl:hover:bg-[var(--nav-button-background-hover,hsl(var(--contrast-100)))] @4xl:hover:text-[var(--nav-button-icon-hover,hsl(var(--foreground)))]';\n\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --nav-focus: hsl(var(--primary));\n *   --nav-background: hsl(var(--background));\n *   --nav-floating-border: hsl(var(--foreground) / 10%);\n *   --nav-link-text: hsl(var(--foreground));\n *   --nav-link-text-hover: hsl(var(--foreground));\n *   --nav-link-background: transparent;\n *   --nav-link-background-hover: hsl(var(--contrast-100));\n *   --nav-link-font-family: var(--font-family-body);\n *   --nav-group-text: hsl(var(--foreground));\n *   --nav-group-text-hover: hsl(var(--foreground));\n *   --nav-group-background: transparent;\n *   --nav-group-background-hover: hsl(var(--contrast-100));\n *   --nav-group-font-family: var(--font-family-body);\n *   --nav-sub-link-text: hsl(var(--contrast-500));\n *   --nav-sub-link-text-hover: hsl(var(--foreground));\n *   --nav-sub-link-background: transparent;\n *   --nav-sub-link-background-hover: hsl(var(--contrast-100));\n *   --nav-sub-link-font-family: var(--font-family-body);\n *   --nav-button-icon: hsl(var(--foreground));\n *   --nav-button-icon-hover: hsl(var(--foreground));\n *   --nav-button-background: hsl(var(--background));\n *   --nav-button-background-hover: hsl(var(--contrast-100));\n *   --nav-menu-background: hsl(var(--background));\n *   --nav-menu-border: hsl(var(--foreground) / 5%);\n *   --nav-mobile-background: hsl(var(--background));\n *   --nav-mobile-divider: hsl(var(--contrast-100));\n *   --nav-mobile-button-icon: hsl(var(--foreground));\n *   --nav-mobile-link-text: hsl(var(--foreground));\n *   --nav-mobile-link-text-hover: hsl(var(--foreground));\n *   --nav-mobile-link-background: transparent;\n *   --nav-mobile-link-background-hover: hsl(var(--contrast-100));\n *   --nav-mobile-link-font-family: var(--font-family-body);\n *   --nav-mobile-sub-link-text: hsl(var(--contrast-500));\n *   --nav-mobile-sub-link-text-hover: hsl(var(--foreground));\n *   --nav-mobile-sub-link-background: transparent;\n *   --nav-mobile-sub-link-background-hover: hsl(var(--contrast-100));\n *   --nav-mobile-sub-link-font-family: var(--font-family-body);\n *   --nav-search-background: hsl(var(--background));\n *   --nav-search-border: hsl(var(--foreground) / 5%);\n *   --nav-search-divider: hsl(var(--foreground) / 5%);\n *   --nav-search-icon: hsl(var(--contrast-500));\n *   --nav-search-empty-title: hsl(var(--foreground));\n *   --nav-search-empty-subtitle: hsl(var(--contrast-500));\n *   --nav-search-result-title: hsl(var(--foreground));\n *   --nav-search-result-title-font-family: var(--font-family-mono);\n *   --nav-search-result-link-text: hsl(var(--foreground));\n *   --nav-search-result-link-text-hover: hsl(var(--foreground));\n *   --nav-search-result-link-background: hsl(var(--background));\n *   --nav-search-result-link-background-hover: hsl(var(--contrast-100));\n *   --nav-search-result-link-font-family: var(--font-family-body);\n *   --nav-cart-count-text: hsl(var(--background));\n *   --nav-cart-count-background: hsl(var(--foreground));\n *   --nav-locale-background: hsl(var(--background));\n *   --nav-locale-link-text: hsl(var(--contrast-400));\n *   --nav-locale-link-text-hover: hsl(var(--foreground));\n *   --nav-locale-link-text-selected: hsl(var(--foreground));\n *   --nav-locale-link-background: transparent;\n *   --nav-locale-link-background-hover: hsl(var(--contrast-100));\n *   --nav-locale-link-font-family: var(--font-family-body);\n * }\n * ```\n */\nexport const Navigation = forwardRef(function Navigation<S extends SearchResult>(\n  {\n    className,\n    isFloating = false,\n    cartHref,\n    cartCount: streamableCartCount,\n    accountHref,\n    links: streamableLinks,\n    logo: streamableLogo,\n    logoHref = '/',\n    logoLabel = 'Home',\n    logoWidth = 200,\n    logoHeight = 40,\n    mobileLogo: streamableMobileLogo,\n    mobileLogoWidth = 100,\n    mobileLogoHeight = 40,\n    linksPosition = 'center',\n    activeLocaleId,\n    locales,\n    currencies: streamableCurrencies,\n    activeCurrencyId: streamableActiveCurrencyId,\n    currencyAction,\n    searchHref,\n    searchParamName = 'query',\n    searchAction,\n    searchInputPlaceholder,\n    searchSubmitLabel,\n    cartLabel = 'Cart',\n    accountLabel = 'Profile',\n    openSearchPopupLabel = 'Open search popup',\n    searchLabel = 'Search',\n    mobileMenuTriggerLabel = 'Toggle navigation',\n    switchCurrencyLabel,\n    giftCertificatesLabel = 'Gift Certificates',\n    giftCertificatesHref,\n    giftCertificatesEnabled: streamableGiftCertificatesEnabled,\n  }: Props<S>,\n  ref: Ref<HTMLDivElement>,\n) {\n  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n  const { isSearchOpen, setIsSearchOpen } = useSearch();\n\n  const pathname = usePathname();\n\n  useEffect(() => {\n    setIsMobileMenuOpen(false);\n    setIsSearchOpen(false);\n  }, [pathname, setIsSearchOpen]);\n\n  useEffect(() => {\n    function handleScroll() {\n      setIsSearchOpen(false);\n      setIsMobileMenuOpen(false);\n    }\n\n    window.addEventListener('scroll', handleScroll);\n\n    return () => window.removeEventListener('scroll', handleScroll);\n  }, [setIsSearchOpen]);\n\n  return (\n    <NavigationMenu.Root\n      className={clsx('relative mx-auto w-full max-w-screen-2xl @container', className)}\n      delayDuration={0}\n      onValueChange={() => setIsSearchOpen(false)}\n      ref={ref}\n    >\n      <div\n        className={clsx(\n          'flex items-center justify-between gap-1 bg-[var(--nav-background,hsl(var(--background)))] py-2 pl-3 pr-2 transition-shadow @4xl:rounded-2xl @4xl:px-2 @4xl:pl-6 @4xl:pr-2.5',\n          isFloating\n            ? 'shadow-xl ring-1 ring-[var(--nav-floating-border,hsl(var(--foreground)/10%))]'\n            : 'shadow-none ring-0',\n        )}\n      >\n        {/* Mobile Menu */}\n        <Popover.Root onOpenChange={setIsMobileMenuOpen} open={isMobileMenuOpen}>\n          <Popover.Anchor className=\"absolute left-0 right-0 top-full\" />\n          <Popover.Trigger asChild>\n            <MobileMenuButton\n              aria-label={mobileMenuTriggerLabel}\n              className=\"mr-1 @4xl:hidden\"\n              onClick={() => setIsMobileMenuOpen((prev) => !prev)}\n              open={isMobileMenuOpen}\n            />\n          </Popover.Trigger>\n          <Popover.Portal>\n            <Popover.Content className=\"max-h-[calc(var(--radix-popover-content-available-height)-8px)] w-[var(--radix-popper-anchor-width)] @container data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\">\n              <div className=\"max-h-[inherit] divide-y divide-[var(--nav-mobile-divider,hsl(var(--contrast-100)))] overflow-y-auto bg-[var(--nav-mobile-background,hsl(var(--background)))]\">\n                <Stream\n                  fallback={\n                    <ul className=\"flex animate-pulse flex-col gap-4 p-5 @4xl:gap-2 @4xl:p-5\">\n                      <li>\n                        <span className=\"block h-4 w-10 rounded-md bg-contrast-100\" />\n                      </li>\n                      <li>\n                        <span className=\"block h-4 w-14 rounded-md bg-contrast-100\" />\n                      </li>\n                      <li>\n                        <span className=\"block h-4 w-24 rounded-md bg-contrast-100\" />\n                      </li>\n                      <li>\n                        <span className=\"block h-4 w-16 rounded-md bg-contrast-100\" />\n                      </li>\n                    </ul>\n                  }\n                  value={streamableLinks}\n                >\n                  {(links) =>\n                    links.map((item, i) => (\n                      <ul className=\"flex flex-col p-2 @4xl:gap-2 @4xl:p-5\" key={i}>\n                        {item.label !== '' && (\n                          <li>\n                            <Link\n                              className=\"block rounded-lg bg-[var(--nav-mobile-link-background,transparent)] px-3 py-2 font-[family-name:var(--nav-mobile-link-font-family,var(--font-family-body))] font-semibold text-[var(--nav-mobile-link-text,hsl(var(--foreground)))] ring-[var(--nav-focus,hsl(var(--primary)))] transition-colors hover:bg-[var(--nav-mobile-link-background-hover,hsl(var(--contrast-100)))] hover:text-[var(--nav-mobile-link-text-hover,hsl(var(--foreground)))] focus-visible:outline-0 focus-visible:ring-2 @4xl:py-4\"\n                              href={item.href}\n                            >\n                              {item.label}\n                            </Link>\n                          </li>\n                        )}\n                        {item.groups\n                          ?.flatMap((group) => group.links)\n                          .map((link, j) => (\n                            <li key={j}>\n                              <Link\n                                className=\"block rounded-lg bg-[var(--nav-mobile-sub-link-background,transparent)] px-3 py-2 font-[family-name:var(--nav-mobile-sub-link-font-family,var(--font-family-body))] text-sm font-medium text-[var(--nav-mobile-sub-link-text,hsl(var(--contrast-500)))] ring-[var(--nav-focus,hsl(var(--primary)))] transition-colors hover:bg-[var(--nav-mobile-sub-link-background-hover,hsl(var(--contrast-100)))] hover:text-[var(--nav-mobile-sub-link-text-hover,hsl(var(--foreground)))] focus-visible:outline-0 focus-visible:ring-2 @4xl:py-4\"\n                                href={link.href}\n                              >\n                                {link.label}\n                              </Link>\n                            </li>\n                          ))}\n                      </ul>\n                    ))\n                  }\n                </Stream>\n                {/* Mobile Locale / Currency Dropdown */}\n                {locales && locales.length > 1 && streamableCurrencies && (\n                  <div className=\"p-2 @4xl:p-5\">\n                    <div className=\"flex items-center px-3 py-1 @4xl:py-2\">\n                      {/* Locale / Language Dropdown */}\n                      {locales.length > 1 ? (\n                        <LocaleSwitcher\n                          activeLocaleId={activeLocaleId}\n                          // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n                          locales={locales as [Locale, Locale, ...Locale[]]}\n                        />\n                      ) : null}\n\n                      {/* Currency Dropdown */}\n                      <Stream\n                        fallback={null}\n                        value={Streamable.all([streamableCurrencies, streamableActiveCurrencyId])}\n                      >\n                        {([currencies, activeCurrencyId]) =>\n                          currencies.length > 1 && currencyAction ? (\n                            <CurrencyForm\n                              action={currencyAction}\n                              activeCurrencyId={activeCurrencyId}\n                              // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n                              currencies={currencies as [Currency, ...Currency[]]}\n                            />\n                          ) : null\n                        }\n                      </Stream>\n                    </div>\n                  </div>\n                )}\n              </div>\n            </Popover.Content>\n          </Popover.Portal>\n        </Popover.Root>\n\n        {/* Logo */}\n        <div\n          className={clsx(\n            'flex items-center justify-start self-stretch',\n            linksPosition === 'center' ? 'flex-1' : 'flex-1 @4xl:flex-none',\n          )}\n        >\n          <Logo\n            className={clsx(streamableMobileLogo != null ? 'hidden @4xl:flex' : 'flex')}\n            height={logoHeight}\n            href={logoHref}\n            label={logoLabel}\n            logo={streamableLogo}\n            width={logoWidth}\n          />\n          {streamableMobileLogo != null && (\n            <Logo\n              className=\"flex @4xl:hidden\"\n              height={mobileLogoHeight}\n              href={logoHref}\n              label={logoLabel}\n              logo={streamableMobileLogo}\n              width={mobileLogoWidth}\n            />\n          )}\n        </div>\n\n        {/* Top Level Nav Links */}\n        <ul\n          className={clsx(\n            'hidden gap-1 @4xl:flex @4xl:flex-1',\n            {\n              left: '@4xl:justify-start',\n              center: '@4xl:justify-center',\n              right: '@4xl:justify-end',\n            }[linksPosition],\n          )}\n        >\n          <Stream\n            fallback={\n              <ul className=\"flex min-h-[41px] animate-pulse flex-row items-center @4xl:gap-6 @4xl:p-2.5\">\n                <li>\n                  <span className=\"block h-4 w-10 rounded-md bg-contrast-100\" />\n                </li>\n                <li>\n                  <span className=\"block h-4 w-14 rounded-md bg-contrast-100\" />\n                </li>\n                <li>\n                  <span className=\"block h-4 w-24 rounded-md bg-contrast-100\" />\n                </li>\n                <li>\n                  <span className=\"block h-4 w-16 rounded-md bg-contrast-100\" />\n                </li>\n              </ul>\n            }\n            value={streamableLinks}\n          >\n            {(links) =>\n              links.map((item, i) => (\n                <NavigationMenu.Item key={i} value={i.toString()}>\n                  <NavigationMenu.Trigger asChild>\n                    <Link\n                      className=\"hidden items-center whitespace-nowrap rounded-xl bg-[var(--nav-link-background,transparent)] p-2.5 font-[family-name:var(--nav-link-font-family,var(--font-family-body))] text-sm font-medium text-[var(--nav-link-text,hsl(var(--foreground)))] ring-[var(--nav-focus,hsl(var(--primary)))] transition-colors duration-200 hover:bg-[var(--nav-link-background-hover,hsl(var(--contrast-100)))] hover:text-[var(--nav-link-text-hover,hsl(var(--foreground)))] focus-visible:outline-0 focus-visible:ring-2 @4xl:inline-flex\"\n                      href={item.href}\n                    >\n                      {item.label}\n                    </Link>\n                  </NavigationMenu.Trigger>\n                  {item.groups != null && item.groups.length > 0 && (\n                    <NavigationMenu.Content className=\"rounded-2xl bg-[var(--nav-menu-background,hsl(var(--background)))] shadow-xl ring-1 ring-[var(--nav-menu-border,hsl(var(--foreground)/5%))]\">\n                      <div className=\"m-auto grid w-full max-w-screen-lg grid-cols-5 justify-center gap-5 px-5 pb-8 pt-5\">\n                        {item.groups.map((group, columnIndex) => (\n                          <ul className=\"flex flex-col\" key={columnIndex}>\n                            {/* Second Level Links */}\n                            {group.label != null && group.label !== '' && (\n                              <li>\n                                {group.href != null && group.href !== '' ? (\n                                  <Link className={navGroupClassName} href={group.href}>\n                                    {group.label}\n                                  </Link>\n                                ) : (\n                                  <span className={navGroupClassName}>{group.label}</span>\n                                )}\n                              </li>\n                            )}\n\n                            {group.links.map((link, idx) => (\n                              // Third Level Links\n                              <li key={idx}>\n                                <Link\n                                  className=\"block rounded-lg bg-[var(--nav-sub-link-background,transparent)] px-3 py-1.5 font-[family-name:var(--nav-sub-link-font-family,var(--font-family-body))] text-sm font-medium text-[var(--nav-sub-link-text,hsl(var(--contrast-500)))] ring-[var(--nav-focus,hsl(var(--primary)))] transition-colors hover:bg-[var(--nav-sub-link-background-hover,hsl(var(--contrast-100)))] hover:text-[var(--nav-sub-link-text-hover,hsl(var(--foreground)))] focus-visible:outline-0 focus-visible:ring-2\"\n                                  href={link.href}\n                                >\n                                  {link.label}\n                                </Link>\n                              </li>\n                            ))}\n                          </ul>\n                        ))}\n                      </div>\n                    </NavigationMenu.Content>\n                  )}\n                </NavigationMenu.Item>\n              ))\n            }\n          </Stream>\n        </ul>\n\n        {/* Icon Buttons */}\n        <div\n          className={clsx(\n            'flex items-center justify-end gap-0.5 transition-colors duration-300',\n            linksPosition === 'center' ? 'flex-1' : 'flex-1 @4xl:flex-none',\n          )}\n        >\n          {searchAction ? (\n            <Popover.Root onOpenChange={setIsSearchOpen} open={isSearchOpen}>\n              <Popover.Anchor className=\"absolute left-0 right-0 top-full\" />\n              <Popover.Trigger asChild>\n                <button\n                  aria-label={openSearchPopupLabel}\n                  className={navButtonClassName}\n                  onPointerEnter={(e) => e.preventDefault()}\n                  onPointerLeave={(e) => e.preventDefault()}\n                  onPointerMove={(e) => e.preventDefault()}\n                >\n                  <Search size={20} strokeWidth={1} />\n                </button>\n              </Popover.Trigger>\n              <Popover.Portal>\n                <Popover.Content className=\"max-h-[calc(var(--radix-popover-content-available-height)-16px)] w-[var(--radix-popper-anchor-width)] py-2 @container data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\">\n                  <div className=\"flex max-h-[inherit] flex-col rounded-2xl bg-[var(--nav-search-background,hsl(var(--background)))] shadow-xl ring-1 ring-[var(--nav-search-border,hsl(var(--foreground)/5%))] transition-all duration-200 ease-in-out @4xl:inset-x-0\">\n                    <SearchForm\n                      searchAction={searchAction}\n                      searchHref={searchHref}\n                      searchInputPlaceholder={searchInputPlaceholder}\n                      searchParamName={searchParamName}\n                      searchSubmitLabel={searchSubmitLabel}\n                    />\n                  </div>\n                </Popover.Content>\n              </Popover.Portal>\n            </Popover.Root>\n          ) : (\n            <Link aria-label={searchLabel} className={navButtonClassName} href={searchHref}>\n              <Search size={20} strokeWidth={1} />\n            </Link>\n          )}\n\n          <Link aria-label={accountLabel} className={navButtonClassName} href={accountHref}>\n            <User size={20} strokeWidth={1} />\n          </Link>\n          <Link aria-label={cartLabel} className={navButtonClassName} href={cartHref}>\n            <ShoppingBag size={20} strokeWidth={1} />\n            <Stream\n              fallback={\n                <span className=\"absolute -right-0.5 -top-0.5 flex h-4 w-4 animate-pulse items-center justify-center rounded-full bg-contrast-100 text-xs text-background\" />\n              }\n              value={streamableCartCount}\n            >\n              {(cartCount) =>\n                cartCount != null &&\n                cartCount > 0 && (\n                  <span className=\"absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-[var(--nav-cart-count-background,hsl(var(--foreground)))] font-[family-name:var(--nav-cart-count-font-family,var(--font-family-body))] text-xs text-[var(--nav-cart-count-text,hsl(var(--background)))]\">\n                    {cartCount}\n                  </span>\n                )\n              }\n            </Stream>\n          </Link>\n\n          <Stream fallback={null} value={streamableGiftCertificatesEnabled}>\n            {(giftCertificatesEnabled) =>\n              giftCertificatesEnabled && (\n                <Link\n                  aria-label={giftCertificatesLabel}\n                  className={navButtonClassName}\n                  href={giftCertificatesHref}\n                >\n                  <GiftIcon size={20} strokeWidth={1} />\n                </Link>\n              )\n            }\n          </Stream>\n\n          {/* Locale / Language Dropdown */}\n          {locales && locales.length > 1 ? (\n            <LocaleSwitcher\n              activeLocaleId={activeLocaleId}\n              className=\"hidden @4xl:block\"\n              // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n              locales={locales as [Locale, Locale, ...Locale[]]}\n            />\n          ) : null}\n\n          {/* Currency Dropdown */}\n          <Stream\n            fallback={null}\n            value={Streamable.all([streamableCurrencies, streamableActiveCurrencyId])}\n          >\n            {([currencies, activeCurrencyId]) =>\n              currencies && currencies.length > 1 && currencyAction ? (\n                <CurrencyForm\n                  action={currencyAction}\n                  activeCurrencyId={activeCurrencyId}\n                  className=\"hidden @4xl:block\"\n                  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n                  currencies={currencies as [Currency, ...Currency[]]}\n                  switchCurrencyLabel={switchCurrencyLabel}\n                />\n              ) : null\n            }\n          </Stream>\n        </div>\n      </div>\n\n      <div className=\"perspective-[2000px] absolute left-0 right-0 top-full z-50 flex w-full justify-center\">\n        <NavigationMenu.Viewport className=\"relative mt-2 w-full data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\" />\n      </div>\n    </NavigationMenu.Root>\n  );\n});\n\nNavigation.displayName = 'Navigation';\n\nfunction SearchForm<S extends SearchResult>({\n  searchAction,\n  searchParamName = 'query',\n  searchHref = '/search',\n  searchInputPlaceholder = 'Search Products',\n  searchSubmitLabel = 'Submit',\n}: {\n  searchAction: SearchAction<S>;\n  searchParamName?: string;\n  searchHref?: string;\n  searchInputPlaceholder?: string;\n  searchSubmitLabel?: string;\n}) {\n  const [query, setQuery] = useState('');\n  const [isSearching, startSearching] = useTransition();\n  const [{ searchResults, lastResult, emptyStateTitle, emptyStateSubtitle }, formAction] =\n    useActionState(searchAction, {\n      searchResults: null,\n      lastResult: null,\n    });\n  const [isDebouncing, setIsDebouncing] = useState(false);\n  const [isSubmitting, setIsSubmitting] = useState(false);\n  const isPending = isSearching || isDebouncing || isSubmitting;\n  const debouncedOnChange = useMemo(() => {\n    const debounced = debounce((q: string) => {\n      setIsDebouncing(false);\n\n      const formData = new FormData();\n\n      formData.append(searchParamName, q);\n\n      startSearching(() => {\n        formAction(formData);\n      });\n    }, 300);\n\n    return (q: string) => {\n      setIsDebouncing(true);\n\n      debounced(q);\n    };\n  }, [formAction, searchParamName]);\n\n  const [form] = useForm({ lastResult });\n\n  const handleSubmit = useCallback(() => {\n    setIsSubmitting(true);\n  }, []);\n\n  return (\n    <>\n      <form\n        action={searchHref}\n        className=\"flex items-center gap-3 px-3 py-3 @4xl:px-5 @4xl:py-4\"\n        onSubmit={handleSubmit}\n      >\n        <SearchIcon\n          className=\"hidden shrink-0 text-[var(--nav-search-icon,hsl(var(--contrast-500)))] @xl:block\"\n          size={20}\n          strokeWidth={1}\n        />\n        <input\n          className=\"grow bg-transparent pl-2 text-lg font-medium outline-0 focus-visible:outline-none @xl:pl-0\"\n          name={searchParamName}\n          onChange={(e) => {\n            setQuery(e.currentTarget.value);\n            debouncedOnChange(e.currentTarget.value);\n          }}\n          placeholder={searchInputPlaceholder}\n          type=\"text\"\n          value={query}\n        />\n        <SubmitButton loading={isPending} submitLabel={searchSubmitLabel} />\n      </form>\n\n      <SearchResults\n        emptySearchSubtitle={emptyStateSubtitle}\n        emptySearchTitle={emptyStateTitle}\n        errors={form.errors}\n        query={query}\n        searchParamName={searchParamName}\n        searchResults={searchResults}\n        stale={isPending}\n      />\n    </>\n  );\n}\n\nfunction SubmitButton({ loading, submitLabel }: { loading: boolean; submitLabel: string }) {\n  const { pending } = useFormStatus();\n\n  return (\n    <Button\n      loading={pending || loading}\n      shape=\"circle\"\n      size=\"small\"\n      type=\"submit\"\n      variant=\"secondary\"\n    >\n      <ArrowRight aria-label={submitLabel} size={20} strokeWidth={1.5} />\n    </Button>\n  );\n}\n\nfunction SearchResults({\n  query,\n  searchResults,\n  stale,\n  emptySearchTitle = `No results were found for '${query}'`,\n  emptySearchSubtitle = 'Please try another search.',\n  errors,\n}: {\n  query: string;\n  searchParamName: string;\n  emptySearchTitle?: string;\n  emptySearchSubtitle?: string;\n  searchResults: SearchResult[] | null;\n  stale: boolean;\n  errors?: string[];\n}) {\n  if (query === '') return null;\n\n  if (errors != null && errors.length > 0) {\n    if (stale) return null;\n\n    return (\n      <div className=\"flex flex-col border-t border-[var(--nav-search-divider,hsl(var(--contrast-100)))] p-6\">\n        {errors.map((error) => (\n          <FormStatus key={error} type=\"error\">\n            {error}\n          </FormStatus>\n        ))}\n      </div>\n    );\n  }\n\n  if (searchResults == null || searchResults.length === 0) {\n    if (stale) return null;\n\n    return (\n      <div className=\"flex flex-col border-t border-[var(--nav-search-divider,hsl(var(--contrast-100)))] p-6\">\n        <p className=\"text-2xl font-medium text-[var(--nav-search-empty-title,hsl(var(--foreground)))]\">\n          {emptySearchTitle}\n        </p>\n        <p className=\"text-[var(--nav-search-empty-subtitle,hsl(var(--contrast-500)))]\">\n          {emptySearchSubtitle}\n        </p>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className={clsx(\n        'flex flex-1 flex-col overflow-y-auto border-t border-[var(--nav-search-divider,hsl(var(--contrast-100)))] @2xl:flex-row',\n        stale && 'opacity-50',\n      )}\n    >\n      {searchResults.map((result, index) => {\n        switch (result.type) {\n          case 'links': {\n            return (\n              <section\n                aria-label={result.title}\n                className=\"flex w-full flex-col gap-1 border-b border-[var(--nav-search-divider,hsl(var(--contrast-100)))] p-5 @2xl:max-w-80 @2xl:border-b-0 @2xl:border-r\"\n                key={`result-${index}`}\n              >\n                <h3 className=\"mb-4 font-[family-name:var(--nav-search-result-title-font-family,var(--font-family-mono))] text-sm uppercase text-[var(--nav-search-result-title,hsl(var(--foreground)))]\">\n                  {result.title}\n                </h3>\n                <ul role=\"listbox\">\n                  {result.links.map((link, i) => (\n                    <li key={i}>\n                      <Link\n                        className=\"block rounded-lg bg-[var(--nav-search-result-link-background,transparent)] px-3 py-4 font-[family-name:var(--nav-search-result-link-font-family,var(--font-family-body))] font-semibold text-[var(--nav-search-result-link-text,hsl(var(--contrast-500)))] ring-[var(--nav-focus,hsl(var(--primary)))] transition-colors hover:bg-[var(--nav-search-result-link-background-hover,hsl(var(--contrast-100)))] hover:text-[var(--nav-search-result-link-text-hover,hsl(var(--foreground)))] focus-visible:outline-0 focus-visible:ring-2\"\n                        href={link.href}\n                      >\n                        {link.label}\n                      </Link>\n                    </li>\n                  ))}\n                </ul>\n              </section>\n            );\n          }\n\n          case 'products': {\n            return (\n              <section\n                aria-label={result.title}\n                className=\"flex w-full flex-col gap-5 p-5\"\n                key={`result-${index}`}\n              >\n                <h3 className=\"font-[family-name:var(--nav-search-result-title-font-family,var(--font-family-mono))] text-sm uppercase text-[var(--nav-search-result-title,hsl(var(--foreground)))]\">\n                  {result.title}\n                </h3>\n                <ul\n                  className=\"grid w-full grid-cols-2 gap-5 @xl:grid-cols-4 @2xl:grid-cols-2 @4xl:grid-cols-4\"\n                  role=\"listbox\"\n                >\n                  {result.products.map((product) => (\n                    <li key={product.id}>\n                      <ProductCard\n                        imageSizes=\"(min-width: 42rem) 25vw, 50vw\"\n                        product={{\n                          id: product.id,\n                          title: product.title,\n                          href: product.href,\n                          price: product.price,\n                          image: product.image,\n                        }}\n                      />\n                    </li>\n                  ))}\n                </ul>\n              </section>\n            );\n          }\n\n          default:\n            return null;\n        }\n      })}\n    </div>\n  );\n}\n\nconst useSwitchLocale = () => {\n  const pathname = usePathname();\n  const router = useRouter();\n  const params = useParams();\n  const searchParams = useSearchParams();\n\n  return useCallback(\n    (locale: string) =>\n      router.push(\n        // @ts-expect-error -- TypeScript will validate that only known `params`\n        // are used in combination with a given `pathname`. Since the two will\n        // always match for the current route, we can skip runtime checks.\n        { pathname, params, query: Object.fromEntries(searchParams.entries()) },\n        { locale },\n      ),\n    [pathname, params, router, searchParams],\n  );\n};\n\nfunction LocaleSwitcher({\n  locales,\n  activeLocaleId,\n  className,\n}: {\n  activeLocaleId?: string;\n  locales: [Locale, ...Locale[]];\n  className?: string;\n}) {\n  const activeLocale = locales.find((locale) => locale.id === activeLocaleId);\n  const [isPending, startTransition] = useTransition();\n  const switchLocale = useSwitchLocale();\n\n  return (\n    <div className={className}>\n      <DropdownMenu.Root>\n        <DropdownMenu.Trigger\n          className={clsx(\n            'flex items-center gap-1 text-xs uppercase transition-opacity disabled:opacity-30',\n            navButtonClassName,\n          )}\n          disabled={isPending}\n        >\n          {activeLocale?.id ?? locales[0].id}\n          <ChevronDown size={16} strokeWidth={1.5} />\n        </DropdownMenu.Trigger>\n        <DropdownMenu.Portal>\n          <DropdownMenu.Content\n            align=\"end\"\n            className=\"z-50 max-h-80 overflow-y-scroll rounded-xl bg-[var(--nav-locale-background,hsl(var(--background)))] p-2 shadow-xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 @4xl:w-32 @4xl:rounded-2xl @4xl:p-2\"\n            sideOffset={16}\n          >\n            {locales.map(({ id, label }) => (\n              <DropdownMenu.Item\n                className={clsx(\n                  'cursor-default rounded-lg bg-[var(--nav-locale-link-background,transparent)] px-2.5 py-2 font-[family-name:var(--nav-locale-link-font-family,var(--font-family-body))] text-sm font-medium text-[var(--nav-locale-link-text,hsl(var(--contrast-400)))] outline-none ring-[var(--nav-focus,hsl(var(--primary)))] transition-colors hover:bg-[var(--nav-locale-link-background-hover,hsl(var(--contrast-100)))] hover:text-[var(--nav-locale-link-text-hover,hsl(var(--foreground)))]',\n                  {\n                    'text-[var(--nav-locale-link-text-selected,hsl(var(--foreground)))]':\n                      id === activeLocaleId,\n                  },\n                )}\n                key={id}\n                onSelect={() => startTransition(() => switchLocale(id))}\n              >\n                {label}\n              </DropdownMenu.Item>\n            ))}\n          </DropdownMenu.Content>\n        </DropdownMenu.Portal>\n      </DropdownMenu.Root>\n    </div>\n  );\n}\n\nfunction CurrencyForm({\n  action,\n  currencies,\n  activeCurrencyId,\n  switchCurrencyLabel = 'Switch currency',\n  className,\n}: {\n  activeCurrencyId?: string;\n  action: CurrencyAction;\n  currencies: [Currency, ...Currency[]];\n  switchCurrencyLabel?: string;\n  className?: string;\n}) {\n  const router = useRouter();\n  const [isPending, startTransition] = useTransition();\n  const [lastResult, formAction] = useActionState(action, null);\n  const activeCurrency = currencies.find((currency) => currency.id === activeCurrencyId);\n\n  useEffect(() => {\n    // eslint-disable-next-line no-console\n    if (lastResult?.error) console.log(lastResult.error);\n  }, [lastResult?.error]);\n\n  return (\n    <div className={className}>\n      <DropdownMenu.Root>\n        <DropdownMenu.Trigger\n          className={clsx(\n            'flex items-center gap-1 text-xs uppercase transition-opacity disabled:opacity-30',\n            navButtonClassName,\n          )}\n          disabled={isPending}\n        >\n          {activeCurrency?.label ?? currencies[0].label}\n          <ChevronDown size={16} strokeWidth={1.5}>\n            <title>{switchCurrencyLabel}</title>\n          </ChevronDown>\n        </DropdownMenu.Trigger>\n        <DropdownMenu.Portal>\n          <DropdownMenu.Content\n            align=\"end\"\n            className=\"z-50 max-h-80 overflow-y-scroll rounded-xl bg-[var(--nav-locale-background,hsl(var(--background)))] p-2 shadow-xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 @4xl:w-32 @4xl:rounded-2xl @4xl:p-2\"\n            sideOffset={16}\n          >\n            {currencies.map((currency) => (\n              <DropdownMenu.Item\n                className={clsx(\n                  'cursor-default rounded-lg bg-[var(--nav-locale-link-background,transparent)] px-2.5 py-2 font-[family-name:var(--nav-locale-link-font-family,var(--font-family-body))] text-sm font-medium text-[var(--nav-locale-link-text,hsl(var(--contrast-400)))] outline-none ring-[var(--nav-focus,hsl(var(--primary)))] transition-colors hover:bg-[var(--nav-locale-link-background-hover,hsl(var(--contrast-100)))] hover:text-[var(--nav-locale-link-text-hover,hsl(var(--foreground)))]',\n                  {\n                    'text-[var(--nav-locale-link-text-selected,hsl(var(--foreground)))]':\n                      currency.id === activeCurrencyId,\n                  },\n                )}\n                key={currency.id}\n                onSelect={() => {\n                  // eslint-disable-next-line @typescript-eslint/require-await\n                  startTransition(async () => {\n                    const formData = new FormData();\n\n                    formData.append('id', currency.id);\n                    formAction(formData);\n\n                    // This is needed to refresh the Data Cache after the product has been added to the cart.\n                    // The cart id is not picked up after the first time the cart is created/updated.\n                    router.refresh();\n                  });\n                }}\n              >\n                {currency.label}\n              </DropdownMenu.Item>\n            ))}\n          </DropdownMenu.Content>\n        </DropdownMenu.Portal>\n      </DropdownMenu.Root>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/price-label/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { useTranslations } from 'next-intl';\n\nexport interface PriceRange {\n  type: 'range';\n  minValue: string;\n  maxValue: string;\n}\n\nexport interface PriceSale {\n  type: 'sale';\n  previousValue: string;\n  currentValue: string;\n}\n\nexport type Price = string | PriceRange | PriceSale;\n\ninterface Props {\n  className?: string;\n  colorScheme?: 'light' | 'dark';\n  price: Price;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --price-light-text: hsl(var(--foreground));\n *   --price-light-sale-text: hsl(var(--foreground));\n *   --price-dark-text: hsl(var(--background));\n *   --price-dark-sale-text: hsl(var(--background));\n * }\n * ```\n */\nexport function PriceLabel({ className, colorScheme = 'light', price }: Props) {\n  const t = useTranslations('Components.Price');\n\n  if (typeof price === 'string') {\n    return (\n      <span\n        className={clsx(\n          'block font-semibold',\n          {\n            light: 'text-[var(--price-light-text,hsl(var(--foreground)))]',\n            dark: 'text-[var(--price-dark-text,hsl(var(--background)))]',\n          }[colorScheme],\n          className,\n        )}\n      >\n        {price}\n      </span>\n    );\n  }\n\n  switch (price.type) {\n    case 'range':\n      return (\n        <span\n          className={clsx(\n            'block font-semibold',\n            {\n              light: 'text-[var(--price-light-text,hsl(var(--foreground)))]',\n              dark: 'text-[var(--price-dark-text,hsl(var(--background)))]',\n            }[colorScheme],\n            className,\n          )}\n        >\n          <span className=\"sr-only\">\n            {t('range', { minValue: price.minValue, maxValue: price.maxValue })}\n          </span>\n          <span aria-hidden=\"true\">\n            {price.minValue} - {price.maxValue}\n          </span>\n        </span>\n      );\n\n    case 'sale':\n      return (\n        <span className={clsx('block font-semibold', className)}>\n          <span className=\"sr-only\">{t('originalPrice', { price: price.previousValue })}</span>\n          <span\n            aria-hidden=\"true\"\n            className={clsx(\n              'font-normal line-through opacity-50',\n              {\n                light: 'text-[var(--price-light-text,hsl(var(--foreground)))]',\n                dark: 'text-[var(--price-dark-text,hsl(var(--background)))]',\n              }[colorScheme],\n            )}\n          >\n            {price.previousValue}\n          </span>{' '}\n          <span className=\"sr-only\">{t('currentPrice', { price: price.currentValue })}</span>\n          <span\n            aria-hidden=\"true\"\n            className={clsx(\n              {\n                light: 'text-[var(--price-light-sale-text,hsl(var(--foreground)))]',\n                dark: 'text-[var(--price-dark-sale-text,hsl(var(--background)))]',\n              }[colorScheme],\n            )}\n          >\n            {price.currentValue}\n          </span>\n        </span>\n      );\n\n    default:\n      return null;\n  }\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/product-card/compare.tsx",
    "content": "'use client';\n\nimport { useQueryState } from 'nuqs';\nimport { startTransition } from 'react';\n\nimport { Checkbox } from '@/vibes/soul/form/checkbox';\nimport { useCompareDrawer } from '@/vibes/soul/primitives/compare-drawer';\nimport { compareParser } from '@/vibes/soul/primitives/compare-drawer/loader';\n\ninterface CompareDrawerItem {\n  id: string;\n  image?: { src: string; alt: string };\n  href: string;\n  title: string;\n}\n\ninterface Props {\n  colorScheme?: 'light' | 'dark';\n  paramName?: string;\n  label?: string;\n  product: CompareDrawerItem;\n}\n\nexport const Compare = function Compare({\n  colorScheme = 'light',\n  paramName = 'compare',\n  label = 'Compare',\n  product,\n}: Props) {\n  const [, setParam] = useQueryState(paramName, compareParser);\n\n  const { optimisticItems, setOptimisticItems, maxItems } = useCompareDrawer();\n\n  return (\n    <Checkbox\n      checked={!!optimisticItems.find((item) => item.id === product.id)}\n      colorScheme={colorScheme}\n      disabled={\n        !optimisticItems.find((item) => item.id === product.id) &&\n        maxItems !== undefined &&\n        optimisticItems.length >= maxItems\n      }\n      label={label}\n      onCheckedChange={(value) => {\n        startTransition(async () => {\n          setOptimisticItems({\n            type: value === true ? 'add' : 'remove',\n            item: product,\n          });\n\n          await setParam((prev) => {\n            const next =\n              value === true\n                ? [...(prev ?? []), product.id]\n                : (prev ?? []).filter((v) => v !== product.id);\n\n            return next.length > 0 ? next : null;\n          });\n        });\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "core/vibes/soul/primitives/product-card/index.tsx",
    "content": "import { clsx } from 'clsx';\n\nimport { Badge } from '@/vibes/soul/primitives/badge';\nimport { Price, PriceLabel } from '@/vibes/soul/primitives/price-label';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\nimport { Image } from '~/components/image';\nimport { Link } from '~/components/link';\n\nimport { Rating } from '../rating';\n\nimport { Compare } from './compare';\n\nexport interface Product {\n  id: string;\n  title: string;\n  href: string;\n  image?: { src: string; alt: string };\n  price?: Price;\n  subtitle?: string;\n  badge?: string;\n  rating?: number;\n  inventoryMessage?: string;\n  numberOfReviews?: number;\n}\n\nexport interface ProductCardProps {\n  className?: string;\n  colorScheme?: 'light' | 'dark';\n  aspectRatio?: '5:6' | '3:4' | '1:1';\n  showCompare?: boolean;\n  imagePriority?: boolean;\n  imageSizes?: string;\n  compareLabel?: string;\n  compareParamName?: string;\n  product: Product;\n  showRating?: boolean;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --product-card-focus: hsl(var(--primary));\n *   --product-card-light-offset: hsl(var(--background));\n *   --product-card-light-background: hsl(var(--contrast-100));\n *   --product-card-light-title: hsl(var(--foreground));\n *   --product-card-light-subtitle: hsl(var(--foreground) / 75%);\n *   --product-card-light-message: hsl(var(--foreground) / 75%);\n *   --product-card-dark-offset: hsl(var(--foreground));\n *   --product-card-dark-background: hsl(var(--contrast-500));\n *   --product-card-dark-title: hsl(var(--background));\n *   --product-card-dark-subtitle: hsl(var(--background) / 75%);\n *   --product-card-dark-message: hsl(var(--background) / 75%);\n *   --product-card-font-family: var(--font-family-body);\n * }\n * ```\n */\nexport function ProductCard({\n  product: {\n    id,\n    title,\n    subtitle,\n    badge,\n    price,\n    image,\n    href,\n    inventoryMessage,\n    rating,\n    numberOfReviews,\n  },\n  showRating = false,\n  colorScheme = 'light',\n  className,\n  showCompare = false,\n  aspectRatio = '5:6',\n  compareLabel,\n  compareParamName,\n  imagePriority = false,\n  imageSizes = '(min-width: 80rem) 20vw, (min-width: 64rem) 25vw, (min-width: 42rem) 33vw, (min-width: 24rem) 50vw, 100vw',\n}: ProductCardProps) {\n  return (\n    <article\n      className={clsx(\n        'group flex min-w-0 max-w-md flex-col gap-3 font-[family-name:var(--card-font-family,var(--font-family-body))] @container',\n        className,\n      )}\n    >\n      <div className=\"relative\">\n        <div\n          className={clsx(\n            'relative overflow-hidden rounded-xl @md:rounded-2xl',\n            {\n              '5:6': 'aspect-[5/6]',\n              '3:4': 'aspect-[3/4]',\n              '1:1': 'aspect-square',\n            }[aspectRatio],\n            {\n              light: 'bg-[var(--product-card-light-background,hsl(var(--contrast-100)))]',\n              dark: 'bg-[var(--product-card-dark-background,hsl(var(--contrast-500)))]',\n            }[colorScheme],\n          )}\n        >\n          {image != null ? (\n            <Image\n              alt={image.alt}\n              className={clsx(\n                'w-full scale-100 select-none object-cover transition-transform duration-500 ease-out group-hover:scale-110',\n                {\n                  light: 'bg-[var(--product-card-light-background,hsl(var(--contrast-100))]',\n                  dark: 'bg-[var(--product-card-dark-background,hsl(var(--contrast-500))]',\n                }[colorScheme],\n              )}\n              fill\n              preload={imagePriority}\n              sizes={imageSizes}\n              src={image.src}\n            />\n          ) : (\n            <div\n              className={clsx(\n                'break-words pl-5 pt-5 text-4xl font-bold leading-[0.8] tracking-tighter opacity-25 transition-transform duration-500 ease-out group-hover:scale-105 @xs:text-7xl',\n                {\n                  light: 'text-[var(--product-card-light-title,hsl(var(--foreground)))]',\n                  dark: 'text-[var(--product-card-dark-title,hsl(var(--background)))]',\n                }[colorScheme],\n              )}\n            >\n              {title}\n            </div>\n          )}\n          {badge != null && badge !== '' && (\n            <Badge className=\"absolute left-3 top-3\" shape=\"rounded\">\n              {badge}\n            </Badge>\n          )}\n        </div>\n\n        <div className=\"mt-2 flex flex-col items-start gap-x-4 gap-y-3 px-1 @xs:mt-3 @2xl:flex-row\">\n          <div className=\"flex-1 text-sm @[16rem]:text-base\">\n            <span\n              className={clsx(\n                'line-clamp-2 font-semibold',\n                {\n                  light: 'text-[var(--product-card-light-title,hsl(var(--foreground)))]',\n                  dark: 'text-[var(--product-card-dark-title,hsl(var(--background)))]',\n                }[colorScheme],\n              )}\n            >\n              {title}\n            </span>\n            {subtitle != null && subtitle !== '' && (\n              <span\n                className={clsx(\n                  'mb-1.5 block text-sm font-normal',\n                  {\n                    light: 'text-[var(--product-card-light-subtitle,hsl(var(--foreground)/75%))]',\n                    dark: 'text-[var(--product-card-dark-subtitle,hsl(var(--background)/75%))]',\n                  }[colorScheme],\n                )}\n              >\n                {subtitle}\n              </span>\n            )}\n            {price != null && <PriceLabel colorScheme={colorScheme} price={price} />}\n            {showRating && typeof rating === 'number' && rating > 0 && (\n              <Rating className=\"mb-2 mt-1\" numberOfReviews={numberOfReviews} rating={rating} />\n            )}\n            <span\n              className={clsx(\n                'block text-sm font-normal',\n                {\n                  light: 'text-[var(--product-card-light-message,hsl(var(--foreground)/75%))]',\n                  dark: 'text-[var(--product-card-dark-message,hsl(var(--background)/75%))]',\n                }[colorScheme],\n              )}\n            >\n              {inventoryMessage}\n            </span>\n          </div>\n        </div>\n        {href !== '#' && (\n          <Link\n            aria-label={title}\n            className={clsx(\n              'absolute inset-0 rounded-b-lg rounded-t-2xl focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--product-card-focus,hsl(var(--primary)))] focus-visible:ring-offset-4',\n              {\n                light: 'ring-offset-[var(--product-card-light-offset,hsl(var(--background)))]',\n                dark: 'ring-offset-[var(--product-card-dark-offset,hsl(var(--foreground)))]',\n              }[colorScheme],\n            )}\n            href={href}\n            id={id}\n          >\n            <span className=\"sr-only\">View product</span>\n          </Link>\n        )}\n      </div>\n      {showCompare && (\n        <div className=\"ml-1 mt-auto shrink-0\">\n          <Compare\n            colorScheme={colorScheme}\n            label={compareLabel}\n            paramName={compareParamName}\n            product={{ id, title, href, image }}\n          />\n        </div>\n      )}\n    </article>\n  );\n}\n\nexport function ProductCardSkeleton({\n  className,\n  aspectRatio = '5:6',\n}: {\n  aspectRatio?: '5:6' | '3:4' | '1:1';\n  className?: string;\n}) {\n  return (\n    <div className={clsx('@container', className)}>\n      <Skeleton.Box\n        className={clsx(\n          'rounded-xl @md:rounded-2xl',\n          {\n            '5:6': 'aspect-[5/6]',\n            '3:4': 'aspect-[3/4]',\n            '1:1': 'aspect-square',\n          }[aspectRatio],\n        )}\n      />\n      <div className=\"mt-2 flex flex-col items-start gap-x-4 gap-y-3 px-1 @xs:mt-3 @2xl:flex-row\">\n        <div className=\"w-full text-sm @[16rem]:text-base\">\n          <Skeleton.Text characterCount={10} className=\"rounded\" />\n          <Skeleton.Text characterCount={8} className=\"rounded\" />\n          <Skeleton.Text characterCount={6} className=\"rounded\" />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/rating/index.tsx",
    "content": "import { clsx } from 'clsx';\n\nexport interface Props {\n  showRating?: boolean;\n  rating: number;\n  numberOfReviews?: number;\n  showNumberOfReviews?: boolean;\n  className?: string;\n}\n\ninterface StarType {\n  type: 'empty' | 'half' | 'full';\n}\n\nexport const Star = ({ type }: StarType) => {\n  const paths = {\n    empty: (\n      <path\n        d=\"M9.99984 1.66669L12.5748 6.88335L18.3332 7.72502L14.1665 11.7834L15.1498 17.5167L9.99984 14.8084L4.84984 17.5167L5.83317 11.7834L1.6665 7.72502L7.42484 6.88335L9.99984 1.66669Z\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeOpacity=\"0.4\"\n      />\n    ),\n    half: (\n      <>\n        <path\n          d=\"M9.99984 1.66669L12.5748 6.88335L18.3332 7.72502L14.1665 11.7834L15.1498 17.5167L9.99984 14.8084L4.84984 17.5167L5.83317 11.7834L1.6665 7.72502L7.42484 6.88335L9.99984 1.66669Z\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M10.0003 1.6665V14.8082L4.85033 17.5165L5.83366 11.7832L1.66699 7.72484L7.42533 6.88317L10.0003 1.6665Z\"\n          fill=\"currentColor\"\n        />\n      </>\n    ),\n    full: (\n      <path\n        d=\"M9.99984 1.66669L12.5748 6.88335L18.3332 7.72502L14.1665 11.7834L15.1498 17.5167L9.99984 14.8084L4.84984 17.5167L5.83317 11.7834L1.6665 7.72502L7.42484 6.88335L9.99984 1.66669Z\"\n        fill=\"currentColor\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    ),\n  };\n\n  return (\n    <svg\n      className=\"inline-block text-foreground\"\n      fill=\"none\"\n      height={20}\n      viewBox=\"0 0 20 20\"\n      width={20}\n    >\n      {paths[type]}\n    </svg>\n  );\n};\n\nexport const Rating = function Rating({\n  showRating = true,\n  rating,\n  numberOfReviews,\n  showNumberOfReviews = true,\n  className,\n}: Readonly<Props>) {\n  const adjustedRating = Math.min(rating, 5);\n\n  const stars: Array<StarType['type']> = Array.from({ length: 5 }, (_, index) => {\n    if (index < Math.floor(adjustedRating)) return 'full';\n    if (index < Math.ceil(adjustedRating)) return 'half';\n\n    return 'empty';\n  });\n\n  return (\n    <div className={clsx('flex items-center', className)}>\n      {stars.map((type, index) => (\n        <Star key={index} type={type} />\n      ))}\n\n      {showRating && (\n        <div className=\"flex items-center gap-1\">\n          <span className=\"ml-2 flex h-6 shrink-0 items-center justify-center text-xs font-semibold text-foreground\">\n            {adjustedRating % 1 !== 0 ? adjustedRating.toFixed(1) : adjustedRating}\n          </span>\n          {showNumberOfReviews && numberOfReviews != null && (\n            <div className=\"flex items-center gap-1\">\n              <span className=\"mx-1 h-4 w-px bg-contrast-200\" />\n              <span className=\"text-xs text-contrast-500\">\n                {numberOfReviews} {numberOfReviews === 1 ? 'review' : 'reviews'}\n              </span>\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "core/vibes/soul/primitives/reveal/index.tsx",
    "content": "'use client';\n\nimport { clsx } from 'clsx';\nimport { ReactNode, useEffect, useRef, useState } from 'react';\n\nimport { AnimatedUnderline } from '@/vibes/soul/primitives/animated-underline';\nimport { Button } from '@/vibes/soul/primitives/button';\n\nexport interface RevealProps {\n  variant?: 'underline' | 'button';\n  showLabel?: string;\n  hideLabel?: string;\n  defaultOpen?: boolean;\n  children: ReactNode;\n  maxHeight?: string;\n}\n\nexport function Reveal({\n  variant = 'underline',\n  showLabel = 'Show more',\n  hideLabel = 'Show less',\n  defaultOpen = false,\n  maxHeight = '10rem',\n  children,\n}: RevealProps) {\n  const [isOpen, setIsOpen] = useState(defaultOpen);\n  const [hasOverflow, setHasOverflow] = useState(true);\n  const contentRef = useRef<HTMLDivElement>(null);\n\n  function convertToPixels(value: string): number {\n    const num = parseFloat(value);\n\n    if (value.endsWith('rem')) {\n      return num * 16; // Convert rem to pixels (1rem = 16px)\n    }\n\n    if (value.endsWith('px')) {\n      return num;\n    }\n\n    return num;\n  }\n\n  useEffect(() => {\n    function checkHeight() {\n      if (contentRef.current) {\n        const contentHeight = contentRef.current.scrollHeight;\n        const maxHeightPx = convertToPixels(maxHeight);\n\n        setHasOverflow(contentHeight > maxHeightPx);\n      }\n    }\n\n    checkHeight();\n\n    const resizeObserver = new ResizeObserver(checkHeight);\n\n    if (contentRef.current) {\n      resizeObserver.observe(contentRef.current);\n    }\n\n    return () => {\n      resizeObserver.disconnect();\n    };\n  }, [maxHeight]);\n\n  return (\n    <div className=\"relative\">\n      <div\n        className={clsx(\n          hasOverflow &&\n            !isOpen &&\n            '[mask-image:linear-gradient(to_top,transparent,black_50px,black_calc(100%-50px))]',\n          'overflow-hidden',\n        )}\n        ref={contentRef}\n        style={{ maxHeight: isOpen ? 'none' : maxHeight }}\n      >\n        {children}\n      </div>\n      {hasOverflow && (\n        <div className={clsx('flex w-full items-end pt-4')}>\n          {variant === 'underline' && (\n            <button\n              className=\"group/underline text-sm focus:outline-none\"\n              onClick={() => setIsOpen(!isOpen)}\n              type=\"button\"\n            >\n              <AnimatedUnderline>{isOpen ? hideLabel : showLabel}</AnimatedUnderline>\n            </button>\n          )}\n          {variant === 'button' && (\n            <Button\n              onClick={() => setIsOpen(!isOpen)}\n              size=\"x-small\"\n              type=\"button\"\n              variant=\"tertiary\"\n            >\n              {isOpen ? hideLabel : showLabel}\n            </Button>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/side-panel/index.tsx",
    "content": "'use client';\n\nimport * as Dialog from '@radix-ui/react-dialog';\nimport { clsx } from 'clsx';\nimport { X } from 'lucide-react';\nimport React from 'react';\n\nimport { Button } from '../button';\n\ninterface Props {\n  title: React.ReactNode;\n  children: React.ReactNode;\n}\n\nfunction Content({ title, children }: Props) {\n  return (\n    <Dialog.Portal>\n      <Dialog.Overlay className=\"fixed inset-0 z-30 bg-foreground/50 @container\">\n        <Dialog.Content\n          className={clsx(\n            'fixed inset-y-0 right-0 flex w-96 max-w-full flex-col bg-background transition duration-500 [animation-timing-function:cubic-bezier(0.25,1,0,1)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',\n          )}\n          forceMount\n        >\n          <div className=\"flex items-center justify-between gap-2 bg-background px-6 pb-4 pt-4 @md:px-8 @md:pt-7\">\n            <Dialog.Title asChild>\n              <div className=\"text-2xl font-medium @lg:text-3xl\">{title}</div>\n            </Dialog.Title>\n            <Dialog.Close asChild>\n              <Button className=\"translate-x-3\" shape=\"circle\" size=\"small\" variant=\"tertiary\">\n                <X size={20} strokeWidth={1} />\n              </Button>\n            </Dialog.Close>\n          </div>\n\n          <div className=\"flex-1 overflow-y-auto px-6 pb-6 @md:px-8 @md:pb-8\">{children}</div>\n        </Dialog.Content>\n      </Dialog.Overlay>\n    </Dialog.Portal>\n  );\n}\n\nconst Root = Dialog.Root;\nconst Trigger = Dialog.Trigger;\n\nexport { Root, Trigger, Content };\n"
  },
  {
    "path": "core/vibes/soul/primitives/skeleton/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { ReactNode } from 'react';\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --skeleton: color-mix(in oklab, hsl(var(--contast-300)), white 75%);\n * }\n * ```\n */\nfunction SkeletonRoot({\n  className,\n  children,\n  pending = false,\n  hideOverflow = false,\n}: {\n  className?: string;\n  children?: React.ReactNode;\n  pending?: boolean;\n  hideOverflow?: boolean;\n}) {\n  return (\n    <div\n      className={clsx('@container', hideOverflow && 'overflow-hidden', className)}\n      data-pending={pending ? '' : undefined}\n      role={pending ? 'status' : undefined}\n    >\n      {children}\n      {pending && <span className=\"sr-only\">Loading...</span>}\n    </div>\n  );\n}\n\nfunction SkeletonBox({ className }: { className?: string }) {\n  return <div className={clsx('bg-[var(--skeleton,hsl(var(--contrast-300)/15%))]', className)} />;\n}\n\nfunction SkeletonText({\n  characterCount = 10,\n  className,\n}: {\n  characterCount?: number | 'full';\n  className?: string;\n}) {\n  return (\n    <div className={clsx('flex h-[1lh] items-center', className)}>\n      <div\n        className={clsx(\n          `h-[1ex] max-w-full rounded-[inherit] bg-[var(--skeleton,hsl(var(--contrast-300)/15%))]`,\n        )}\n        style={{ width: characterCount === 'full' ? '100%' : `${characterCount}ch` }}\n      />\n    </div>\n  );\n}\n\nfunction SkeletonIcon({ className, icon }: { className?: string; icon: ReactNode }) {\n  return (\n    <div className={clsx('text-[var(--skeleton,hsl(var(--contrast-300)))] opacity-25', className)}>\n      {icon}\n    </div>\n  );\n}\n\nexport { SkeletonIcon as Icon, SkeletonRoot as Root, SkeletonBox as Box, SkeletonText as Text };\n"
  },
  {
    "path": "core/vibes/soul/primitives/spinner/index.tsx",
    "content": "import { clsx } from 'clsx';\n\ninterface Props {\n  size?: 'xs' | 'sm' | 'md' | 'lg';\n  loadingAriaLabel?: string;\n}\n\nexport const Spinner = function Spinner({ size = 'sm', loadingAriaLabel }: Props) {\n  return (\n    <span\n      aria-label={loadingAriaLabel ?? 'Loading...'}\n      className={clsx(\n        'box-border inline-block animate-spin rounded-full border-contrast-100 border-b-primary-shadow',\n        {\n          xs: 'h-5 w-5 border-2',\n          sm: 'h-6 w-6 border-2',\n          md: 'h-10 w-10 border-[3px]',\n          lg: 'h-14 w-14 border-4',\n        }[size],\n      )}\n      role=\"status\"\n    />\n  );\n};\n"
  },
  {
    "path": "core/vibes/soul/primitives/toaster/index.tsx",
    "content": "'use client';\n\nimport { ReactNode } from 'react';\nimport { Toaster as Sonner, toast as SonnerToast } from 'sonner';\n\nimport { Alert } from '@/vibes/soul/primitives/alert';\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>;\n\ninterface ToastOptions {\n  action?: {\n    label: string;\n    onClick: () => void;\n  };\n  description?: string;\n  position?: ToasterProps['position'];\n  dismissLabel?: string;\n}\n\nexport const Toaster = ({ ...props }: ToasterProps) => {\n  return (\n    <Sonner\n      toastOptions={{\n        unstyled: true,\n        classNames: {\n          toast: 'group focus-visible:ring-0 right-0',\n        },\n      }}\n      {...props}\n    />\n  );\n};\n\nexport const toast = {\n  success: (message: ReactNode, options?: ToastOptions) => {\n    const position = options?.position;\n\n    const toastId = SonnerToast(\n      <Alert\n        message={message}\n        onDismiss={() => SonnerToast.dismiss(toastId)}\n        variant=\"success\"\n        {...options}\n      />,\n      { position },\n    );\n  },\n  error: (message: ReactNode, options?: ToastOptions) => {\n    const position = options?.position;\n\n    const toastId = SonnerToast(\n      <Alert\n        message={message}\n        onDismiss={() => SonnerToast.dismiss(toastId)}\n        variant=\"error\"\n        {...options}\n      />,\n      { position },\n    );\n  },\n  warning: (message: ReactNode, options?: ToastOptions) => {\n    const position = options?.position;\n\n    const toastId = SonnerToast(\n      <Alert\n        message={message}\n        onDismiss={() => SonnerToast.dismiss(toastId)}\n        variant=\"warning\"\n        {...options}\n      />,\n      { position },\n    );\n  },\n  info: (message: ReactNode, options?: ToastOptions) => {\n    const position = options?.position;\n\n    const toastId = SonnerToast(\n      <Alert\n        message={message}\n        onDismiss={() => SonnerToast.dismiss(toastId)}\n        variant=\"info\"\n        {...options}\n      />,\n      { position },\n    );\n  },\n};\n"
  },
  {
    "path": "core/vibes/soul/primitives/tooltip/index.tsx",
    "content": "import * as TooltipPrimitive from '@radix-ui/react-tooltip';\nimport { clsx } from 'clsx';\n\ninterface Props extends React.PropsWithChildren {\n  align?: 'center' | 'end' | 'start';\n  className?: string;\n  delayDuration?: number;\n  skipDelayDuration?: number;\n  trigger?: React.ReactNode;\n  open?: boolean;\n  setOpen?: (open: boolean) => void;\n  side?: 'top' | 'right' | 'bottom' | 'left';\n  sideOffset?: number;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --tooltip-background: hsl(var(--background));\n * ```\n */\nexport const Tooltip = ({\n  align = 'center',\n  className = '',\n  delayDuration,\n  skipDelayDuration,\n  trigger,\n  open,\n  setOpen,\n  side = 'top',\n  sideOffset = 6,\n  children,\n}: Props) => {\n  return (\n    <TooltipPrimitive.Provider delayDuration={delayDuration} skipDelayDuration={skipDelayDuration}>\n      <TooltipPrimitive.Root onOpenChange={setOpen} open={open}>\n        {trigger != null && (\n          <TooltipPrimitive.Trigger asChild={typeof trigger !== 'string'}>\n            {trigger}\n          </TooltipPrimitive.Trigger>\n        )}\n        <TooltipPrimitive.Portal>\n          <TooltipPrimitive.Content\n            align={align}\n            className={clsx(\n              'z-50 max-h-80 rounded-2xl border border-contrast-100 bg-[var(--tooltip-background,hsl(var(--background)))] p-3 shadow-xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',\n              className,\n            )}\n            side={side}\n            sideOffset={sideOffset}\n          >\n            {children}\n          </TooltipPrimitive.Content>\n        </TooltipPrimitive.Portal>\n      </TooltipPrimitive.Root>\n    </TooltipPrimitive.Provider>\n  );\n};\n"
  },
  {
    "path": "core/vibes/soul/primitives/wishlist-item-card/index.tsx",
    "content": "import { clsx } from 'clsx';\n\nimport {\n  Product,\n  ProductCard,\n  ProductCardProps,\n  ProductCardSkeleton,\n} from '@/vibes/soul/primitives/product-card';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\n\nimport { RemoveWishlistItemAction, RemoveWishlistItemButton } from './remove-wishlist-item';\nimport { AddWishlistItemToCartAction, WishlistItemAddToCart } from './wishlist-item-add-to-cart';\n\nexport interface WishlistItem {\n  itemId: string;\n  productId: string;\n  variantId?: string;\n  callToAction?: {\n    label: string;\n    disabled?: boolean;\n  };\n  product: Product;\n}\n\ninterface WishlistItemCardProps extends Omit<ProductCardProps, 'product' | 'showCompare'> {\n  wishlistId: string;\n  item: WishlistItem;\n  action: AddWishlistItemToCartAction;\n  removeAction?: RemoveWishlistItemAction;\n  removeButtonTitle?: string;\n}\n\nexport const WishlistItemCard = ({\n  wishlistId,\n  item: { itemId, productId, variantId, callToAction, product },\n  action,\n  removeAction,\n  removeButtonTitle,\n  ...props\n}: WishlistItemCardProps) => {\n  return (\n    <div\n      className=\"relative flex max-w-md basis-[calc(100%-1rem)] flex-col justify-between gap-3 @md:basis-[calc(50%-0.75rem)] @lg:basis-[calc(33%-0.5rem)] @2xl:basis-[calc(25%-0.25rem)]\"\n      key={product.id}\n    >\n      <ProductCard aspectRatio=\"3:4\" product={product} showCompare={false} {...props} />\n      {callToAction && (\n        <WishlistItemAddToCart\n          action={action}\n          callToAction={callToAction}\n          productId={productId}\n          variantId={variantId}\n        />\n      )}\n      {removeAction && (\n        <div className=\"absolute -right-3 -top-3 rounded-full transition-shadow duration-100 hover:shadow-md\">\n          <RemoveWishlistItemButton\n            action={removeAction}\n            itemId={itemId}\n            removeButtonTitle={removeButtonTitle}\n            wishlistId={wishlistId}\n          />\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport function WishlistItemSkeleton({ className = '' }: { className?: string }) {\n  return (\n    <div\n      className={clsx(\n        'flex basis-[calc(100%-1rem)] flex-col justify-between gap-3 @md:basis-[calc(50%-0.75rem)] @lg:basis-[calc(33%-0.5rem)] @2xl:basis-[calc(25%-0.25rem)]',\n        className,\n      )}\n    >\n      <ProductCardSkeleton aspectRatio=\"3:4\" />\n      <Skeleton.Box className=\"min-h-10 rounded-full\" />\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/primitives/wishlist-item-card/remove-wishlist-item.tsx",
    "content": "'use client';\n\nimport { SubmissionResult } from '@conform-to/react';\nimport { XIcon } from 'lucide-react';\nimport { useActionState, useEffect, useTransition } from 'react';\n\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { toast } from '@/vibes/soul/primitives/toaster';\n\ntype Action<State, Payload> = (state: Awaited<State>, payload: Payload) => State | Promise<State>;\n\ninterface RemoveWishlistItemState {\n  lastResult: SubmissionResult | null;\n  errorMessage?: string;\n}\n\nexport type RemoveWishlistItemAction = Action<RemoveWishlistItemState, FormData>;\n\ninterface Props {\n  wishlistId: string;\n  itemId: string;\n  action: RemoveWishlistItemAction;\n  removeButtonTitle?: string;\n}\n\nexport const RemoveWishlistItemButton = ({\n  wishlistId,\n  itemId,\n  action,\n  removeButtonTitle = 'Remove product from wishlist',\n}: Props) => {\n  const [isPending, startTransition] = useTransition();\n  const [state, formAction] = useActionState(action, {\n    lastResult: null,\n  });\n\n  useEffect(() => {\n    if (state.lastResult?.status === 'error' && Boolean(state.errorMessage)) {\n      toast.error(state.errorMessage);\n    }\n  }, [state]);\n\n  return (\n    <form action={(formData) => startTransition(() => formAction(formData))}>\n      <input name=\"wishlistId\" type=\"hidden\" value={wishlistId} />\n      <input name=\"wishlistItemId\" type=\"hidden\" value={itemId} />\n      <Button loading={isPending} shape=\"circle\" size=\"x-small\" type=\"submit\" variant=\"tertiary\">\n        <XIcon size={20}>\n          <title>{removeButtonTitle}</title>\n        </XIcon>\n      </Button>\n    </form>\n  );\n};\n"
  },
  {
    "path": "core/vibes/soul/primitives/wishlist-item-card/wishlist-item-add-to-cart.tsx",
    "content": "'use client';\n\nimport { getFormProps, SubmissionResult, useForm } from '@conform-to/react';\nimport { startTransition, useActionState, useEffect } from 'react';\nimport { requestFormReset, useFormStatus } from 'react-dom';\n\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { toast } from '@/vibes/soul/primitives/toaster';\nimport { useEvents } from '~/components/analytics/events';\nimport { useRouter } from '~/i18n/routing';\n\nimport { WishlistItem } from '.';\n\ntype Action<S, P> = (state: Awaited<S>, payload: P) => S | Promise<S>;\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  successMessage?: React.ReactNode;\n  errorMessage?: string;\n}\n\nexport type AddWishlistItemToCartAction = Action<State, FormData>;\n\ninterface Props extends Omit<WishlistItem, 'itemId' | 'product'> {\n  action: AddWishlistItemToCartAction;\n}\n\nexport const WishlistItemAddToCart = ({\n  callToAction = { label: 'Add to cart' },\n  productId,\n  variantId,\n  action,\n}: Props) => {\n  const events = useEvents();\n  const router = useRouter();\n  const [{ lastResult, successMessage, errorMessage }, formAction] = useActionState(action, {\n    lastResult: null,\n  });\n\n  const [form] = useForm({\n    lastResult,\n    onSubmit(event, { formData }) {\n      event.preventDefault();\n\n      startTransition(() => {\n        requestFormReset(event.currentTarget);\n        formAction(formData);\n\n        events.onAddToCart?.(formData);\n      });\n    },\n  });\n\n  useEffect(() => {\n    if (lastResult?.status === 'success' && successMessage) {\n      toast.success(successMessage);\n\n      // This is needed to refresh the Data Cache after the product has been added to the cart.\n      // The cart id is not picked up after the first time the cart is created/updated.\n      router.refresh();\n    }\n\n    if (lastResult?.status === 'error' && errorMessage) {\n      toast.error(errorMessage);\n    }\n  }, [lastResult, successMessage, errorMessage, router]);\n\n  return (\n    <form {...getFormProps(form)} action={formAction} className=\"flex\">\n      <input name=\"productId\" type=\"hidden\" value={productId} />\n      <input name=\"variantId\" type=\"hidden\" value={variantId} />\n      <SubmitButton ctaLabel={callToAction.label} disabled={callToAction.disabled} />\n    </form>\n  );\n};\n\nfunction SubmitButton({ ctaLabel, disabled }: { ctaLabel: string; disabled?: boolean }) {\n  const { pending } = useFormStatus();\n\n  return (\n    <Button className=\"flex-1\" disabled={disabled} loading={pending} size=\"small\" type=\"submit\">\n      {ctaLabel}\n    </Button>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/account-settings/change-password-form.tsx",
    "content": "'use client';\n\nimport { getFormProps, getInputProps, SubmissionResult, useForm } from '@conform-to/react';\nimport { getZodConstraint } from '@conform-to/zod';\nimport { useTranslations } from 'next-intl';\nimport { ReactNode, useActionState, useEffect } from 'react';\nimport { useFormStatus } from 'react-dom';\n\nimport { PasswordComplexitySettings } from '@/vibes/soul/form/dynamic-form/schema';\nimport { Input } from '@/vibes/soul/form/input';\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { toast } from '@/vibes/soul/primitives/toaster';\nimport { parseWithZodTranslatedErrors } from '~/i18n/utils';\n\nimport { changePasswordErrorTranslations, changePasswordSchema } from './schema';\n\ntype Action<S, P> = (state: Awaited<S>, payload: P) => S | Promise<S>;\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  successMessage?: string;\n}\n\nexport type ChangePasswordAction = Action<State, FormData>;\n\nexport interface ChangePasswordFormProps {\n  action: ChangePasswordAction;\n  currentPasswordLabel?: string;\n  newPasswordLabel?: string;\n  confirmPasswordLabel?: string;\n  submitLabel?: string;\n  passwordComplexitySettings?: PasswordComplexitySettings | null;\n}\n\nexport function ChangePasswordForm({\n  action,\n  currentPasswordLabel = 'Current password',\n  newPasswordLabel = 'New password',\n  confirmPasswordLabel = 'Confirm password',\n  submitLabel = 'Update',\n  passwordComplexitySettings,\n}: ChangePasswordFormProps) {\n  const t = useTranslations('Account.Settings');\n  const errorTranslations = changePasswordErrorTranslations(t, passwordComplexitySettings);\n  const schema = changePasswordSchema(passwordComplexitySettings, errorTranslations);\n  const [state, formAction] = useActionState(action, { lastResult: null });\n  const [form, fields] = useForm({\n    constraint: getZodConstraint(schema),\n    shouldValidate: 'onBlur',\n    shouldRevalidate: 'onInput',\n    onValidate({ formData }) {\n      return parseWithZodTranslatedErrors(formData, { schema, errorTranslations });\n    },\n  });\n\n  useEffect(() => {\n    if (state.lastResult?.status === 'success' && state.successMessage != null) {\n      toast.success(state.successMessage);\n    }\n\n    if (state.lastResult?.error) {\n      // eslint-disable-next-line no-console\n      console.log(state.lastResult.error);\n    }\n  }, [state]);\n\n  return (\n    <form {...getFormProps(form)} action={formAction} className=\"space-y-5\">\n      <Input\n        {...getInputProps(fields.currentPassword, { type: 'password' })}\n        errors={fields.currentPassword.errors}\n        key={fields.currentPassword.id}\n        label={currentPasswordLabel}\n      />\n      <Input\n        {...getInputProps(fields.password, { type: 'password' })}\n        errors={fields.password.errors}\n        key={fields.password.id}\n        label={newPasswordLabel}\n      />\n      <Input\n        {...getInputProps(fields.confirmPassword, { type: 'password' })}\n        className=\"mb-6\"\n        errors={fields.confirmPassword.errors}\n        key={fields.confirmPassword.id}\n        label={confirmPasswordLabel}\n      />\n      <SubmitButton>{submitLabel}</SubmitButton>\n    </form>\n  );\n}\n\nfunction SubmitButton({ children }: { children: ReactNode }) {\n  const { pending } = useFormStatus();\n\n  return (\n    <Button loading={pending} size=\"small\" type=\"submit\" variant=\"secondary\">\n      {children}\n    </Button>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/account-settings/index.tsx",
    "content": "import { PasswordComplexitySettings } from '@/vibes/soul/form/dynamic-form/schema';\n\nimport { ChangePasswordAction, ChangePasswordForm } from './change-password-form';\nimport {\n  NewsletterSubscriptionForm,\n  UpdateNewsletterSubscriptionAction,\n} from './newsletter-subscription-form';\nimport { Account, UpdateAccountAction, UpdateAccountForm } from './update-account-form';\n\nexport interface AccountSettingsSectionProps {\n  title?: string;\n  account: Account;\n  updateAccountAction: UpdateAccountAction;\n  updateAccountSubmitLabel?: string;\n  changePasswordTitle?: string;\n  changePasswordAction: ChangePasswordAction;\n  changePasswordSubmitLabel?: string;\n  confirmPasswordLabel?: string;\n  currentPasswordLabel?: string;\n  newPasswordLabel?: string;\n  newsletterSubscriptionEnabled?: boolean;\n  isAccountSubscribed?: boolean;\n  newsletterSubscriptionTitle?: string;\n  newsletterSubscriptionLabel?: string;\n  newsletterSubscriptionCtaLabel?: string;\n  updateNewsletterSubscriptionAction?: UpdateNewsletterSubscriptionAction;\n  passwordComplexitySettings?: PasswordComplexitySettings | null;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --account-settings-section-title-font-family: var(--font-family-heading);\n *   --account-settings-section-font-family: var(--font-family-heading);\n *   --account-settings-section-text: hsl(var(--foreground));\n *   --account-settings-section-border: hsl(var(--contrast-100));\n * }\n * ```\n */\nexport function AccountSettingsSection({\n  title = 'Account Settings',\n  account,\n  updateAccountAction,\n  updateAccountSubmitLabel,\n  changePasswordTitle = 'Change Password',\n  changePasswordAction,\n  changePasswordSubmitLabel,\n  confirmPasswordLabel,\n  currentPasswordLabel,\n  newPasswordLabel,\n  newsletterSubscriptionEnabled = false,\n  isAccountSubscribed = false,\n  newsletterSubscriptionTitle = 'Marketing preferences',\n  newsletterSubscriptionLabel = 'Opt-in to receive emails about new products and promotions.',\n  newsletterSubscriptionCtaLabel = 'Save preferences',\n  updateNewsletterSubscriptionAction,\n  passwordComplexitySettings,\n}: AccountSettingsSectionProps) {\n  return (\n    <section className=\"w-full @container\">\n      <header className=\"mb-4 border-[var(--account-settings-section-border,hsl(var(--contrast-100)))] @2xl:min-h-[72px] @2xl:border-b\">\n        <h1 className=\"hidden font-[family-name:var(--account-settings-section-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none tracking-tight text-[var(--account-settings-section-title,hsl(var(--foreground)))] @2xl:block\">\n          {title}\n        </h1>\n      </header>\n      <div className=\"flex flex-col gap-y-24 @xl:flex-row\">\n        <div className=\"my-4 flex w-full flex-col @xl:max-w-lg\">\n          <div className=\"pb-12\">\n            <UpdateAccountForm\n              account={account}\n              action={updateAccountAction}\n              submitLabel={updateAccountSubmitLabel}\n            />\n          </div>\n          <div className=\"border-t border-[var(--account-settings-section-border,hsl(var(--contrast-100)))] py-12\">\n            <h1 className=\"mb-10 font-[family-name:var(--account-settings-section-font-family,var(--font-family-heading))] text-2xl font-medium leading-none text-[var(--account-settings-section-text,var(--foreground))] @xl:text-2xl\">\n              {changePasswordTitle}\n            </h1>\n            <ChangePasswordForm\n              action={changePasswordAction}\n              confirmPasswordLabel={confirmPasswordLabel}\n              currentPasswordLabel={currentPasswordLabel}\n              newPasswordLabel={newPasswordLabel}\n              passwordComplexitySettings={passwordComplexitySettings}\n              submitLabel={changePasswordSubmitLabel}\n            />\n          </div>\n          {newsletterSubscriptionEnabled && updateNewsletterSubscriptionAction && (\n            <div className=\"border-t border-[var(--account-settings-section-border,hsl(var(--contrast-100)))] pt-12\">\n              <h1 className=\"mb-10 font-[family-name:var(--account-settings-section-font-family,var(--font-family-heading))] text-2xl font-medium leading-none text-[var(--account-settings-section-text,var(--foreground))] @xl:text-2xl\">\n                {newsletterSubscriptionTitle}\n              </h1>\n              <NewsletterSubscriptionForm\n                action={updateNewsletterSubscriptionAction}\n                ctaLabel={newsletterSubscriptionCtaLabel}\n                isAccountSubscribed={isAccountSubscribed}\n                label={newsletterSubscriptionLabel}\n              />\n            </div>\n          )}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/account-settings/newsletter-subscription-form.tsx",
    "content": "'use client';\n\nimport { SubmissionResult } from '@conform-to/react';\nimport { useTranslations } from 'next-intl';\nimport { useActionState, useEffect, useState } from 'react';\n\nimport { Switch } from '@/vibes/soul/form/switch';\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { toast } from '@/vibes/soul/primitives/toaster';\n\ntype Action<S, P> = (state: Awaited<S>, payload: P) => S | Promise<S>;\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  successMessage?: string;\n}\n\nexport type UpdateNewsletterSubscriptionAction = Action<State, FormData>;\n\nexport interface NewsletterSubscriptionFormProps {\n  action: UpdateNewsletterSubscriptionAction;\n  isAccountSubscribed: boolean;\n  label?: string;\n  ctaLabel?: string;\n}\n\nexport function NewsletterSubscriptionForm({\n  action,\n  isAccountSubscribed,\n  label = 'Opt-in to receive emails about new products and promotions.',\n  ctaLabel = 'Update',\n}: NewsletterSubscriptionFormProps) {\n  const t = useTranslations('Account.Settings.NewsletterSubscription');\n\n  const [checked, setChecked] = useState(isAccountSubscribed);\n  const [state, formAction, isPending] = useActionState(action, {\n    lastResult: null,\n  });\n\n  const onCheckedChange = (value: boolean) => {\n    setChecked(value);\n  };\n\n  useEffect(() => {\n    if (state.lastResult?.status === 'success' && state.successMessage != null) {\n      toast.success(state.successMessage);\n    }\n\n    if (state.lastResult?.error) {\n      // eslint-disable-next-line no-console\n      console.log(state.lastResult.error);\n      toast.error(t('somethingWentWrong'));\n    }\n  }, [state, t]);\n\n  return (\n    <form action={formAction} className=\"space-y-5\">\n      <input name=\"intent\" type=\"hidden\" value={checked ? 'subscribe' : 'unsubscribe'} />\n      <Switch checked={checked} label={label} onCheckedChange={onCheckedChange} />\n      <Button\n        disabled={isAccountSubscribed === checked}\n        loading={isPending}\n        size=\"small\"\n        type=\"submit\"\n        variant=\"secondary\"\n      >\n        {ctaLabel}\n      </Button>\n    </form>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/account-settings/schema.ts",
    "content": "import { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport {\n  FormErrorTranslationMap,\n  getPasswordSchema,\n  PasswordComplexitySettings,\n} from '@/vibes/soul/form/dynamic-form/schema';\nimport { ExistingResultType } from '~/client/util';\n\nexport const updateAccountSchema = z.object({\n  firstName: z.string().min(2).trim(),\n  lastName: z.string().min(2).trim(),\n  email: z.string().email().trim(),\n  company: z.string().trim().optional(),\n});\n\nexport const updateAccountErrorTranslations = (\n  t: ExistingResultType<typeof getTranslations<'Account.Settings'>>,\n): FormErrorTranslationMap => ({\n  firstName: {\n    invalid_type: t('FieldErrors.firstNameRequired'),\n    too_small: t('FieldErrors.firstNameTooSmall'),\n  },\n  lastName: {\n    invalid_type: t('FieldErrors.lastNameRequired'),\n    too_small: t('FieldErrors.lastNameTooSmall'),\n  },\n  email: {\n    invalid_type: t('FieldErrors.emailRequired'),\n    invalid_string: t('FieldErrors.emailInvalid'),\n  },\n});\n\nexport const changePasswordErrorTranslations = (\n  t: ExistingResultType<typeof getTranslations<'Account.Settings'>>,\n  passwordComplexity?: PasswordComplexitySettings | null,\n): FormErrorTranslationMap => ({\n  currentPassword: {\n    invalid_type: t('FieldErrors.currentPasswordRequired'),\n  },\n  password: {\n    invalid_type: t('FieldErrors.passwordRequired'),\n    too_small: t('FieldErrors.passwordTooSmall', {\n      minLength: passwordComplexity?.minimumPasswordLength ?? 0,\n    }),\n    lowercase_required: t('FieldErrors.passwordLowercaseRequired'),\n    uppercase_required: t('FieldErrors.passwordUppercaseRequired'),\n    number_required: t('FieldErrors.passwordNumberRequired', {\n      minNumbers: passwordComplexity?.minimumNumbers ?? 1,\n    }),\n    special_character_required: t('FieldErrors.passwordSpecialCharacterRequired'),\n    passwords_must_match: t('FieldErrors.passwordsMustMatch'),\n  },\n  confirmPassword: {\n    invalid_type: t('FieldErrors.confirmPasswordRequired'),\n  },\n});\n\nexport const changePasswordSchema = (\n  passwordComplexity?: PasswordComplexitySettings | null,\n  errorTranslations?: FormErrorTranslationMap,\n) => {\n  const passwordSchema = getPasswordSchema(passwordComplexity, errorTranslations);\n\n  return z\n    .object({\n      currentPassword: z.string().trim(),\n      password: passwordSchema,\n      confirmPassword: z.string(),\n    })\n    .superRefine(({ confirmPassword, password }, ctx) => {\n      if (confirmPassword !== password) {\n        ctx.addIssue({\n          code: 'custom',\n          message:\n            errorTranslations?.password?.passwords_must_match ?? 'The passwords do not match',\n          path: ['confirmPassword'],\n        });\n      }\n    });\n};\n"
  },
  {
    "path": "core/vibes/soul/sections/account-settings/update-account-form.tsx",
    "content": "'use client';\n\nimport { getFormProps, getInputProps, SubmissionResult, useForm } from '@conform-to/react';\nimport { getZodConstraint } from '@conform-to/zod';\nimport { useTranslations } from 'next-intl';\nimport { useActionState, useEffect, useOptimistic, useTransition } from 'react';\nimport { z } from 'zod';\n\nimport { Input } from '@/vibes/soul/form/input';\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { toast } from '@/vibes/soul/primitives/toaster';\nimport { parseWithZodTranslatedErrors } from '~/i18n/utils';\n\nimport { updateAccountErrorTranslations, updateAccountSchema } from './schema';\n\ntype Action<S, P> = (state: Awaited<S>, payload: P) => S | Promise<S>;\n\nexport type UpdateAccountAction = Action<State, FormData>;\n\nexport type Account = z.infer<typeof updateAccountSchema>;\n\ninterface State {\n  account: Account;\n  successMessage?: string;\n  lastResult: SubmissionResult | null;\n}\n\nexport interface UpdateAccountFormProps {\n  action: UpdateAccountAction;\n  account: Account;\n  firstNameLabel?: string;\n  lastNameLabel?: string;\n  emailLabel?: string;\n  companyLabel?: string;\n  submitLabel?: string;\n}\n\nexport function UpdateAccountForm({\n  action,\n  account,\n  firstNameLabel = 'First name',\n  lastNameLabel = 'Last name',\n  emailLabel = 'Email',\n  companyLabel = 'Company',\n  submitLabel = 'Update',\n}: UpdateAccountFormProps) {\n  const t = useTranslations('Account.Settings');\n  const errorTranslations = updateAccountErrorTranslations(t);\n  const [state, formAction] = useActionState(action, { account, lastResult: null });\n  const [pending, startTransition] = useTransition();\n\n  const [optimisticState, setOptimisticState] = useOptimistic<State, FormData>(\n    state,\n    (prevState, formData) => {\n      const intent = formData.get('intent');\n      const submission = parseWithZodTranslatedErrors(formData, {\n        schema: updateAccountSchema,\n        errorTranslations,\n      });\n\n      if (submission.status !== 'success') return prevState;\n\n      switch (intent) {\n        case 'update': {\n          return {\n            ...prevState,\n            account: submission.value,\n          };\n        }\n\n        default:\n          return prevState;\n      }\n    },\n  );\n\n  const [form, fields] = useForm({\n    lastResult: state.lastResult,\n    defaultValue: optimisticState.account,\n    constraint: getZodConstraint(updateAccountSchema),\n    shouldValidate: 'onBlur',\n    shouldRevalidate: 'onInput',\n    onValidate({ formData }) {\n      return parseWithZodTranslatedErrors(formData, {\n        schema: updateAccountSchema,\n        errorTranslations,\n      });\n    },\n  });\n\n  useEffect(() => {\n    if (state.lastResult?.status === 'success' && typeof state.successMessage === 'string') {\n      toast.success(state.successMessage);\n    }\n  }, [state]);\n\n  return (\n    <form\n      {...getFormProps(form)}\n      action={(formData) => {\n        startTransition(() => {\n          formAction(formData);\n          setOptimisticState(formData);\n        });\n      }}\n      className=\"space-y-5\"\n    >\n      <div className=\"flex gap-5\">\n        <Input\n          {...getInputProps(fields.firstName, { type: 'text' })}\n          errors={fields.firstName.errors}\n          key={fields.firstName.id}\n          label={firstNameLabel}\n        />\n        <Input\n          {...getInputProps(fields.lastName, { type: 'text' })}\n          errors={fields.lastName.errors}\n          key={fields.lastName.id}\n          label={lastNameLabel}\n        />\n      </div>\n      <Input\n        {...getInputProps(fields.email, { type: 'text' })}\n        errors={fields.email.errors}\n        key={fields.email.id}\n        label={emailLabel}\n      />\n      <Input\n        {...getInputProps(fields.company, { type: 'text' })}\n        errors={fields.company.errors}\n        key={fields.company.id}\n        label={companyLabel}\n      />\n      <Button\n        loading={pending}\n        name=\"intent\"\n        size=\"small\"\n        type=\"submit\"\n        value=\"update\"\n        variant=\"secondary\"\n      >\n        {submitLabel}\n      </Button>\n    </form>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/address-list-section/index.tsx",
    "content": "'use client';\n\nimport { getFormProps, getInputProps, SubmissionResult, useForm } from '@conform-to/react';\nimport { getZodConstraint, parseWithZod } from '@conform-to/zod';\nimport { useTranslations } from 'next-intl';\nimport {\n  ComponentProps,\n  ReactNode,\n  startTransition,\n  useActionState,\n  useEffect,\n  useOptimistic,\n  useState,\n} from 'react';\nimport { useFormStatus } from 'react-dom';\nimport { z } from 'zod';\n\nimport { DynamicForm } from '@/vibes/soul/form/dynamic-form';\nimport { Field, FieldGroup } from '@/vibes/soul/form/dynamic-form/schema';\nimport { Badge } from '@/vibes/soul/primitives/badge';\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { Spinner } from '@/vibes/soul/primitives/spinner';\nimport { toast } from '@/vibes/soul/primitives/toaster';\n\nimport { addressFormErrorTranslations, schema } from './schema';\n\nexport type Address = z.infer<typeof schema>;\n\nexport interface DefaultAddressConfiguration {\n  id: string | null;\n}\n\ntype Action<F extends Field, S, P> = (\n  fields: Array<F | FieldGroup<F>>,\n  state: Awaited<S>,\n  payload: P,\n) => S | Promise<S>;\n\ninterface State<A extends Address> {\n  addresses: A[];\n  defaultAddress?: DefaultAddressConfiguration;\n  lastResult: SubmissionResult | null;\n}\n\ntype DynamicAddressListFormAction<F extends Field, A extends Address> = Action<\n  F,\n  State<A>,\n  FormData\n>;\n\nexport interface AddressListSectionProps<A extends Address, F extends Field> {\n  title?: string;\n  addresses: A[];\n  fields: Array<F | FieldGroup<F>>;\n  minimumAddressCount?: number;\n  defaultAddress?: DefaultAddressConfiguration;\n  addressAction: DynamicAddressListFormAction<F, A>;\n  editLabel?: string;\n  deleteLabel?: string;\n  updateLabel?: string;\n  createLabel?: string;\n  showAddFormLabel?: string;\n  setDefaultLabel?: string;\n  cancelLabel?: string;\n  emptyStateTitle?: string;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --address-list-section-border: hsl(var(--contrast-100));\n *   --address-list-section-title-font-family: var(--font-family-heading);\n *   --address-list-section-content-font-family: var(--font-family-body);\n *   --address-list-section-title: hsl(var(--foreground));\n *   --address-list-section-name: hsl(var(--foreground));\n *   --address-list-section-info: hsl(var(--contrast-500));\n * }\n * ```\n */\nexport function AddressListSection<A extends Address, F extends Field>({\n  title = 'Addresses',\n  addresses,\n  fields,\n  minimumAddressCount = 1,\n  defaultAddress,\n  addressAction,\n  editLabel = 'Edit',\n  deleteLabel = 'Delete',\n  updateLabel = 'Update',\n  createLabel = 'Create',\n  cancelLabel = 'Cancel',\n  showAddFormLabel = 'Add address',\n  setDefaultLabel = 'Set as default',\n  emptyStateTitle = \"You don't have any addresses\",\n}: AddressListSectionProps<A, F>) {\n  const t = useTranslations('Account.Addresses');\n  const errorTranslations = addressFormErrorTranslations(t);\n  const actionWithFields = addressAction.bind(null, fields);\n\n  const [state, formAction] = useActionState(actionWithFields, {\n    addresses,\n    defaultAddress,\n    lastResult: null,\n  });\n\n  const [optimisticState, setOptimisticState] = useOptimistic<State<Address>, FormData>(\n    state,\n    (prevState, formData) => {\n      const intent = formData.get('intent');\n      const submission = parseWithZod(formData, { schema });\n\n      if (submission.status !== 'success') return prevState;\n\n      switch (intent) {\n        case 'create': {\n          const nextAddress = submission.value;\n\n          return {\n            ...prevState,\n            addresses: [...prevState.addresses, nextAddress],\n          };\n        }\n\n        case 'update': {\n          return {\n            ...prevState,\n            addresses: prevState.addresses.map((a) =>\n              a.id === submission.value.id ? submission.value : a,\n            ),\n          };\n        }\n\n        case 'delete': {\n          return {\n            ...prevState,\n            addresses: prevState.addresses.filter((a) => a.id !== submission.value.id),\n          };\n        }\n\n        case 'setDefault': {\n          return { ...prevState, defaultAddress: { id: submission.value.id } };\n        }\n\n        default:\n          return prevState;\n      }\n    },\n  );\n  const [activeAddressIds, setActiveAddressIds] = useState<string[]>([]);\n  const [showNewAddressForm, setShowNewAddressForm] = useState(false);\n  const [form] = useForm({\n    lastResult: state.lastResult,\n  });\n\n  useEffect(() => {\n    if (form.errors) {\n      form.errors.forEach((error) => {\n        toast.error(error);\n      });\n    }\n  }, [form.errors]);\n\n  const isEmpty = optimisticState.addresses.length === 0;\n\n  return (\n    <section className=\"w-full\">\n      <header className=\"mb-4 border-[var(--address-list-section-border,hsl(var(--contrast-100)))] @2xl:min-h-[72px] @2xl:border-b\">\n        <div className=\"mb-4 flex items-center justify-between\">\n          <Title>{title}</Title>\n          {!showNewAddressForm && !isEmpty && (\n            <Button onClick={() => setShowNewAddressForm(true)} size=\"small\" variant=\"tertiary\">\n              {showAddFormLabel}\n            </Button>\n          )}\n        </div>\n      </header>\n      <div>\n        {showNewAddressForm && (\n          <div className=\"border-b border-[var(--address-list-section-border,hsl(var(--contrast-100)))] pb-6 pt-5\">\n            <div className=\"w-[480px] space-y-4\">\n              <DynamicForm\n                action={(_args, _prevState, formData) => {\n                  setShowNewAddressForm(false);\n\n                  startTransition(() => {\n                    formAction(formData);\n                    setOptimisticState(formData);\n                  });\n\n                  return {\n                    lastResult: optimisticState.lastResult,\n                  };\n                }}\n                buttonSize=\"small\"\n                cancelLabel={cancelLabel}\n                errorTranslations={errorTranslations}\n                fields={fields.map((field) => {\n                  if ('name' in field && field.name === 'id') {\n                    return {\n                      ...field,\n                      name: 'id',\n                      defaultValue: 'new',\n                    };\n                  }\n\n                  return field;\n                })}\n                onCancel={() => setShowNewAddressForm(false)}\n                submitLabel={createLabel}\n                submitName=\"intent\"\n                submitValue=\"create\"\n              />\n            </div>\n          </div>\n        )}\n        {!isEmpty ? (\n          optimisticState.addresses.map((address) => {\n            const addressFields = fields.map((field) => {\n              if (Array.isArray(field)) {\n                return field.map((f) => {\n                  return {\n                    ...f,\n                    defaultValue: address[f.name] ?? '',\n                  };\n                });\n              }\n\n              return {\n                ...field,\n                defaultValue: address[field.name] ?? '',\n              };\n            });\n\n            return (\n              <div\n                className=\"border-b border-[var(--address-list-section-border,hsl(var(--contrast-100)))] pb-6 pt-5\"\n                key={address.id}\n              >\n                {activeAddressIds.includes(address.id) ? (\n                  <div className=\"w-[480px] space-y-4\">\n                    <DynamicForm\n                      action={(_args, _prevState, formData) => {\n                        setActiveAddressIds((prev) => prev.filter((id) => id !== address.id));\n\n                        startTransition(() => {\n                          formAction(formData);\n                          setOptimisticState(formData);\n                        });\n\n                        return {\n                          lastResult: optimisticState.lastResult,\n                        };\n                      }}\n                      buttonSize=\"small\"\n                      cancelLabel={cancelLabel}\n                      errorTranslations={errorTranslations}\n                      fields={addressFields}\n                      onCancel={() =>\n                        setActiveAddressIds((prev) => prev.filter((id) => id !== address.id))\n                      }\n                      submitLabel={updateLabel}\n                      submitName=\"intent\"\n                      submitValue=\"update\"\n                    />\n                  </div>\n                ) : (\n                  <div className=\"space-y-4\">\n                    <AddressPreview\n                      address={address}\n                      isDefault={\n                        optimisticState.defaultAddress\n                          ? optimisticState.defaultAddress.id === address.id\n                          : undefined\n                      }\n                    />\n                    <div className=\"flex gap-1\">\n                      <Button\n                        aria-label={`${editLabel}: ${address.firstName} ${address.lastName}`}\n                        onClick={() => setActiveAddressIds((prev) => [...prev, address.id])}\n                        size=\"small\"\n                        variant=\"tertiary\"\n                      >\n                        {editLabel}\n                      </Button>\n                      {optimisticState.addresses.length > minimumAddressCount && (\n                        <AddressActionButton\n                          action={formAction}\n                          address={address}\n                          aria-label={`${deleteLabel}: ${address.firstName} ${address.lastName}`}\n                          intent=\"delete\"\n                          onSubmit={(formData) => {\n                            startTransition(() => {\n                              formAction(formData);\n                              setOptimisticState(formData);\n                            });\n                          }}\n                        >\n                          {deleteLabel}\n                        </AddressActionButton>\n                      )}\n\n                      {optimisticState.defaultAddress &&\n                        optimisticState.defaultAddress.id !== address.id && (\n                          <AddressActionButton\n                            action={formAction}\n                            address={address}\n                            aria-label={`${setDefaultLabel}: ${address.firstName} ${address.lastName}`}\n                            intent=\"setDefault\"\n                            onSubmit={(formData) => {\n                              startTransition(() => {\n                                formAction(formData);\n                                setOptimisticState(formData);\n                              });\n                            }}\n                          >\n                            {setDefaultLabel}\n                          </AddressActionButton>\n                        )}\n                    </div>\n                  </div>\n                )}\n              </div>\n            );\n          })\n        ) : (\n          <div className=\"@container\">\n            <div className=\"py-20\">\n              <header className=\"mx-auto flex max-w-2xl flex-col items-center gap-5\">\n                <h2 className=\"text-center text-lg font-semibold text-[var(--order-list-empty-state-title,hsl(var(--foreground)))]\">\n                  {emptyStateTitle}\n                </h2>\n                <Button className=\"w-fit\" onClick={() => setShowNewAddressForm(true)}>\n                  {showAddFormLabel}\n                </Button>\n              </header>\n            </div>\n          </div>\n        )}\n      </div>\n    </section>\n  );\n}\n\nfunction Title({ children }: { children: ReactNode }) {\n  const { pending } = useFormStatus();\n\n  return (\n    <h1 className=\"hidden font-[family-name:var(--address-list-section-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none tracking-tight text-[var(--address-list-section-title,hsl(var(--foreground)))] @2xl:block\">\n      {children}\n      {pending && (\n        <span className=\"ml-2\">\n          <Spinner />\n        </span>\n      )}\n    </h1>\n  );\n}\n\nfunction AddressPreview({ address, isDefault = false }: { address: Address; isDefault?: boolean }) {\n  return (\n    <div className=\"flex gap-10 font-[family-name:var(--address-list-section-content-font-family,var(--font-family-body))]\">\n      <div className=\"text-sm text-[var(--address-list-section-info,hsl(var(--contrast-500)))]\">\n        <p className=\"font-bold text-[var(--address-list-section-name,hsl(var(--foreground)))]\">\n          {address.firstName} {address.lastName}\n        </p>\n        <p>{address.company}</p>\n        <p>{address.address1}</p>\n        <p>{address.address2}</p>\n        <p>\n          {address.city}, {address.stateOrProvince} {address.postalCode}\n        </p>\n        <p className=\"mb-3\">{address.countryCode}</p>\n        <p>{address.phone}</p>\n      </div>\n      <div>{isDefault && <Badge>Default</Badge>}</div>\n    </div>\n  );\n}\n\nfunction AddressActionButton({\n  address,\n  intent,\n  action,\n  onSubmit,\n  ...rest\n}: {\n  address: Address;\n  intent: string;\n  action: (formData: FormData) => void;\n  onSubmit: (formData: FormData) => void;\n} & Omit<ComponentProps<'button'>, 'onSubmit'>) {\n  const [form, fields] = useForm({\n    // @ts-expect-error The form requires index signature values to be of\n    // type 'string', 'null', or 'undefined' but the zod .passthrough() method\n    // returns the value 'unknown' for any index signature values.\n    defaultValue: address,\n    constraint: getZodConstraint(schema),\n    onValidate({ formData }) {\n      return parseWithZod(formData, { schema });\n    },\n    onSubmit(event, { submission, formData }) {\n      event.preventDefault();\n\n      if (submission?.status !== 'success') return;\n\n      onSubmit(formData);\n    },\n  });\n\n  return (\n    <form {...getFormProps(form)} action={action}>\n      <input {...getInputProps(fields.id, { type: 'hidden' })} key={fields.id.id} />\n      <input {...getInputProps(fields.firstName, { type: 'hidden' })} key={fields.firstName.id} />\n      <input {...getInputProps(fields.lastName, { type: 'hidden' })} key={fields.lastName.id} />\n      <input {...getInputProps(fields.company, { type: 'hidden' })} key={fields.company.id} />\n      <input {...getInputProps(fields.phone, { type: 'hidden' })} key={fields.phone.id} />\n      <input {...getInputProps(fields.address1, { type: 'hidden' })} key={fields.address1.id} />\n      <input {...getInputProps(fields.address2, { type: 'hidden' })} key={fields.address2.id} />\n      <input {...getInputProps(fields.city, { type: 'hidden' })} key={fields.city.id} />\n      <input\n        {...getInputProps(fields.stateOrProvince, { type: 'hidden' })}\n        key={fields.stateOrProvince.id}\n      />\n      <input {...getInputProps(fields.postalCode, { type: 'hidden' })} key={fields.postalCode.id} />\n      <input\n        {...getInputProps(fields.countryCode, { type: 'hidden' })}\n        key={fields.countryCode.id}\n      />\n      <Button\n        {...rest}\n        name=\"intent\"\n        size=\"small\"\n        type=\"submit\"\n        value={intent}\n        variant=\"tertiary\"\n      />\n    </form>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/address-list-section/schema.ts",
    "content": "import { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { FormErrorTranslationMap } from '@/vibes/soul/form/dynamic-form/schema';\nimport { ExistingResultType } from '~/client/util';\n\nexport const addressFormErrorTranslations = (\n  t: ExistingResultType<typeof getTranslations<'Account.Addresses'>>,\n): FormErrorTranslationMap => ({\n  firstName: {\n    invalid_type: t('FieldErrors.firstNameRequired'),\n  },\n  lastName: {\n    invalid_type: t('FieldErrors.lastNameRequired'),\n  },\n  address1: {\n    invalid_type: t('FieldErrors.addressLine1Required'),\n  },\n  city: {\n    invalid_type: t('FieldErrors.cityRequired'),\n  },\n  countryCode: {\n    invalid_type: t('FieldErrors.countryRequired'),\n  },\n  stateOrProvince: {\n    invalid_type: t('FieldErrors.stateRequired'),\n  },\n  postalCode: {\n    invalid_type: t('FieldErrors.postalCodeRequired'),\n  },\n});\n\nexport const schema = z\n  .object({\n    id: z.string(),\n    firstName: z.string(),\n    lastName: z.string(),\n    company: z.string().optional(),\n    address1: z.string(),\n    address2: z.string().optional(),\n    city: z.string(),\n    stateOrProvince: z.string().optional(),\n    postalCode: z.string().optional(),\n    phone: z.string().optional(),\n    countryCode: z.string(),\n  })\n  .passthrough();\n"
  },
  {
    "path": "core/vibes/soul/sections/blog-post-content/index.tsx",
    "content": "import { clsx } from 'clsx';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { ButtonLink } from '@/vibes/soul/primitives/button-link';\nimport { Breadcrumb, Breadcrumbs, BreadcrumbsSkeleton } from '@/vibes/soul/sections/breadcrumbs';\nimport { Image } from '~/components/image';\n\ninterface Tag {\n  label: string;\n  link: {\n    href: string;\n    target?: string;\n  };\n}\n\ninterface Image {\n  src: string;\n  alt: string;\n}\n\nexport interface BlogPostContentBlogPost {\n  title: string;\n  author?: string;\n  date: string;\n  tags?: Tag[];\n  content: string;\n  image?: Image;\n}\n\ninterface Props {\n  blogPost: Streamable<BlogPostContentBlogPost>;\n  breadcrumbs?: Streamable<Breadcrumb[]>;\n  className?: string;\n}\n\nexport function BlogPostContent({\n  blogPost: streamableBlogPost,\n  className = '',\n  breadcrumbs,\n}: Props) {\n  return (\n    <section className={clsx('@container', className)}>\n      <div className=\"mx-auto max-w-screen-2xl px-4 py-10 @xl:px-6 @xl:py-14 @4xl:px-8 @4xl:py-20\">\n        <Stream fallback={<BlogPostContentSkeleton />} value={streamableBlogPost}>\n          {(blogPost) => {\n            const { title, author, date, tags, content, image } = blogPost;\n\n            return (\n              <>\n                <header className=\"mx-auto w-full max-w-4xl pb-8 @2xl:pb-12 @4xl:pb-16\">\n                  {breadcrumbs && <Breadcrumbs breadcrumbs={breadcrumbs} />}\n\n                  <h1 className=\"mb-4 mt-8 font-heading text-4xl font-medium leading-none @xl:text-5xl @4xl:text-6xl\">\n                    {title}\n                  </h1>\n                  <p>\n                    {date}{' '}\n                    {Boolean(author) && (\n                      <>\n                        <span className=\"px-1\">•</span> {author}\n                      </>\n                    )}\n                  </p>\n\n                  {(tags?.length ?? 0) > 0 && (\n                    <div className=\"-ml-1 mt-4 flex flex-wrap gap-1.5 @xl:mt-6\">\n                      {tags?.map((tag, index) => (\n                        <ButtonLink\n                          href={tag.link.href}\n                          key={index}\n                          size=\"small\"\n                          variant=\"tertiary\"\n                        >\n                          {tag.label}\n                        </ButtonLink>\n                      ))}\n                    </div>\n                  )}\n                </header>\n\n                {image?.src != null && image.src !== '' && (\n                  <Image\n                    alt={image.alt}\n                    className=\"mb-8 aspect-video w-full rounded-2xl bg-contrast-100 object-cover @2xl:mb-12 @4xl:mb-16\"\n                    height={780}\n                    src={image.src}\n                    width={1280}\n                  />\n                )}\n\n                <article\n                  className=\"@-xl:[&_h2]:text-4xl prose mx-auto w-full max-w-4xl space-y-4 [&_h2]:font-heading [&_h2]:text-3xl [&_h2]:font-normal [&_h2]:leading-none [&_img]:mx-auto [&_img]:max-h-[600px] [&_img]:w-fit [&_img]:rounded-2xl [&_img]:object-cover\"\n                  dangerouslySetInnerHTML={{ __html: content }}\n                />\n              </>\n            );\n          }}\n        </Stream>\n      </div>\n    </section>\n  );\n}\n\nfunction BlogPostTitleSkeleton() {\n  return (\n    <div className=\"mb-4 mt-8 animate-pulse\">\n      <div className=\"h-9 w-5/6 rounded-lg bg-contrast-100 @xl:h-12 @4xl:h-[3.75rem]\" />\n    </div>\n  );\n}\n\nfunction BlogPostAuthorSkeleton() {\n  return (\n    <div className=\"animate-pulse\">\n      <div className=\"h-6 w-1/4 rounded-lg bg-contrast-100\" />\n    </div>\n  );\n}\n\nfunction BlogPostTagsSkeleton() {\n  return (\n    <div className=\"mt-4 flex w-2/6 min-w-[250px] animate-pulse flex-wrap gap-3 @xl:mt-6\">\n      <div className=\"-ml-1 h-10 w-[64px] flex-[0.75] rounded-full bg-contrast-100\" />\n      <div className=\"-ml-1 h-10 w-[64px] flex-1 rounded-full bg-contrast-100\" />\n      <div className=\"-ml-1 h-10 w-[64px] flex-1 rounded-full bg-contrast-100\" />\n    </div>\n  );\n}\n\nfunction BlogPostImageSkeleton() {\n  return (\n    <div className=\"mb-8 aspect-video w-full animate-pulse rounded-2xl bg-contrast-100 object-cover @2xl:mb-12 @4xl:mb-16\">\n      <div className=\"h-full w-full\" />\n    </div>\n  );\n}\n\nfunction BlogPostBodySkeleton() {\n  return (\n    <div className=\"mx-auto w-full max-w-4xl animate-pulse pb-8 @2xl:pb-12 @4xl:pb-16\">\n      <div className=\"mb-8 h-[1lh] w-3/5 rounded-lg bg-contrast-100\" />\n      <div className=\"mb-4 h-[0.5lh] w-full rounded-lg bg-contrast-100\" />\n      <div className=\"mb-4 h-[0.5lh] w-full rounded-lg bg-contrast-100\" />\n      <div className=\"mb-4 h-[0.5lh] w-full rounded-lg bg-contrast-100\" />\n      <div className=\"mb-4 h-[0.5lh] w-full rounded-lg bg-contrast-100\" />\n      <div className=\"mb-4 h-[0.5lh] w-full rounded-lg bg-contrast-100\" />\n      <div className=\"mb-4 h-[0.5lh] w-3/4 rounded-lg bg-contrast-100\" />\n    </div>\n  );\n}\n\nexport function BlogPostContentSkeleton() {\n  return (\n    <div>\n      <div className=\"mx-auto w-full max-w-4xl pb-8 @2xl:pb-12 @4xl:pb-16\">\n        <BreadcrumbsSkeleton />\n        <BlogPostTitleSkeleton />\n        <BlogPostAuthorSkeleton />\n        <BlogPostTagsSkeleton />\n      </div>\n      <BlogPostImageSkeleton />\n      <BlogPostBodySkeleton />\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/blog-post-list/index.tsx",
    "content": "import { clsx } from 'clsx';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport {\n  BlogPostCard,\n  BlogPostCardBlogPost,\n  BlogPostCardSkeleton,\n} from '@/vibes/soul/primitives/blog-post-card';\n\ninterface Props {\n  posts: Streamable<BlogPostCardBlogPost[]>;\n  className?: string;\n  emptyStateSubtitle?: Streamable<string | null>;\n  emptyStateTitle?: Streamable<string | null>;\n  placeholderCount?: number;\n}\n\nexport function BlogPostList({\n  posts: streamablePosts,\n  className = '',\n  emptyStateTitle,\n  emptyStateSubtitle,\n  placeholderCount = 6,\n}: Props) {\n  return (\n    <Stream\n      fallback={<BlogPostListSkeleton className={className} placeholderCount={placeholderCount} />}\n      value={streamablePosts}\n    >\n      {(posts) => {\n        if (posts.length === 0) {\n          return (\n            <BlogPostListEmptyState\n              className={className}\n              emptyStateSubtitle={emptyStateSubtitle}\n              emptyStateTitle={emptyStateTitle}\n              placeholderCount={placeholderCount}\n            />\n          );\n        }\n\n        return (\n          <div className={clsx('@container', className)}>\n            <div className=\"mx-auto grid grid-cols-1 gap-x-5 gap-y-8 @md:grid-cols-2 @xl:gap-y-10 @3xl:grid-cols-3 @6xl:grid-cols-4\">\n              {posts.map((post) => (\n                <BlogPostCard blogPost={post} key={post.id} />\n              ))}\n            </div>\n          </div>\n        );\n      }}\n    </Stream>\n  );\n}\n\nexport function BlogPostListSkeleton({\n  className,\n  placeholderCount = 6,\n}: Pick<Props, 'className' | 'placeholderCount'>) {\n  return (\n    <div className={clsx('@container', className)}>\n      <div className=\"mx-auto grid grid-cols-1 gap-x-5 gap-y-8 @md:grid-cols-2 @xl:gap-y-10 @3xl:grid-cols-3 @6xl:grid-cols-4\">\n        {Array.from({ length: placeholderCount }).map((_, index) => (\n          <BlogPostCardSkeleton key={index} />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport function BlogPostListEmptyState({\n  className,\n  placeholderCount = 6,\n  emptyStateTitle,\n  emptyStateSubtitle,\n}: Omit<Props, 'posts'>) {\n  return (\n    <div className={clsx('relative w-full @container', className)}>\n      <div\n        className={clsx(\n          'mx-auto grid grid-cols-1 gap-x-4 gap-y-6 [mask-image:linear-gradient(to_bottom,_black_0%,_transparent_90%)] @sm:grid-cols-2 @2xl:grid-cols-3 @2xl:gap-x-5 @2xl:gap-y-8 @5xl:grid-cols-4 @7xl:grid-cols-5',\n        )}\n      >\n        {Array.from({ length: placeholderCount }).map((_, index) => (\n          <BlogPostCardSkeleton key={index} />\n        ))}\n      </div>\n      <div className=\"absolute inset-0 mx-auto px-3 py-16 pb-3 @4xl:px-10 @4xl:pb-10 @4xl:pt-28\">\n        <div className=\"mx-auto max-w-xl space-y-2 text-center @4xl:space-y-3\">\n          <h3 className=\"@4x:leading-none font-heading text-2xl leading-tight text-foreground @4xl:text-4xl\">\n            {emptyStateTitle}\n          </h3>\n          <p className=\"text-sm text-contrast-500 @4xl:text-lg\">{emptyStateSubtitle}</p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/breadcrumbs/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { ChevronRight } from 'lucide-react';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { AnimatedUnderline } from '@/vibes/soul/primitives/animated-underline';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\nimport { Link } from '~/components/link';\n\nexport interface Breadcrumb {\n  label: string;\n  href: string;\n}\n\nexport interface BreadcrumbsProps {\n  breadcrumbs: Streamable<Breadcrumb[]>;\n  className?: string;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --breadcrumbs-font-family: var(--font-family-body);\n *   --breadcrumbs-primary-text: hsl(var(--foreground));\n *   --breadcrumbs-secondary-text: hsl(var(--contrast-500));\n *   --breadcrumbs-icon: hsl(var(--contrast-500));\n *   --breadcrumbs-hover: hsl(var(--primary));\n * }\n * ```\n */\nexport function Breadcrumbs({ breadcrumbs: streamableBreadcrumbs, className }: BreadcrumbsProps) {\n  return (\n    <Stream fallback={<BreadcrumbsSkeleton className={className} />} value={streamableBreadcrumbs}>\n      {(breadcrumbs) => {\n        if (breadcrumbs.length === 0) {\n          return <BreadCrumbEmptyState className={className} />;\n        }\n\n        return (\n          <nav aria-label=\"breadcrumb\" className={clsx(className)}>\n            <ol className=\"flex flex-wrap items-center gap-x-1.5 text-sm @xl:text-base\">\n              {breadcrumbs.map(({ label, href }, index) => {\n                if (index < breadcrumbs.length - 1) {\n                  return (\n                    <li className=\"inline-flex items-center gap-x-1.5\" key={index}>\n                      <Link className=\"group/underline focus:outline-none\" href={href}>\n                        <AnimatedUnderline className=\"font-[family-name:var(--breadcrumbs-font-family,var(--font-family-body))] text-[var(--breadcrumbs-primary-text,hsl(var(--foreground)))] [background:linear-gradient(0deg,var(--breadcrumbs-hover,hsl(var(--primary))),var(--breadcrumbs-hover,hsl(var(--primary))))_no-repeat_left_bottom_/_0_2px]\">\n                          {label}\n                        </AnimatedUnderline>\n                      </Link>\n                      <ChevronRight\n                        aria-hidden=\"true\"\n                        className=\"text-[var(--breadcrumbs-icon,hsl(var(--contrast-500)))]\"\n                        size={20}\n                        strokeWidth={1}\n                      />\n                    </li>\n                  );\n                }\n\n                return (\n                  <li\n                    className=\"inline-flex items-center font-[family-name:var(--breadcrumbs-font-family,var(--font-family-body))] text-[var(--breadcrumbs-secondary-text,hsl(var(--contrast-500)))]\"\n                    key={index}\n                  >\n                    <span aria-current=\"page\" aria-disabled=\"true\" role=\"link\">\n                      {label}\n                    </span>\n                  </li>\n                );\n              })}\n            </ol>\n          </nav>\n        );\n      }}\n    </Stream>\n  );\n}\n\nexport function BreadcrumbsSkeleton({ className }: Pick<BreadcrumbsProps, 'className'>) {\n  return (\n    <Skeleton.Root\n      className={clsx('group-has-[[data-pending]]/breadcrumbs:animate-pulse', className)}\n      pending\n    >\n      <div className=\"flex flex-wrap items-center gap-x-1.5 text-sm @xl:text-base\">\n        <Skeleton.Text characterCount={4} className=\"rounded text-lg\" />\n        <Skeleton.Icon icon={<ChevronRight aria-hidden className=\"h-5 w-5\" strokeWidth={1} />} />\n        <Skeleton.Text characterCount={6} className=\"rounded text-lg\" />\n        <Skeleton.Icon icon={<ChevronRight aria-hidden className=\"h-5 w-5\" strokeWidth={1} />} />\n        <Skeleton.Text characterCount={6} className=\"rounded text-lg\" />\n      </div>\n    </Skeleton.Root>\n  );\n}\n\nexport function BreadCrumbEmptyState({ className }: Pick<BreadcrumbsProps, 'className'>) {\n  return (\n    <Skeleton.Root className={className}>\n      <div className={clsx('min-h-[1lh]', className)} />\n    </Skeleton.Root>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/cart/client.tsx",
    "content": "'use client';\n\nimport { getFormProps, getInputProps, SubmissionResult, useForm } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { clsx } from 'clsx';\nimport { ArrowRight, GiftIcon, Minus, Plus, Trash2 } from 'lucide-react';\nimport { useTranslations } from 'next-intl';\nimport {\n  ComponentPropsWithoutRef,\n  startTransition,\n  useActionState,\n  useEffect,\n  useMemo,\n  useOptimistic,\n} from 'react';\nimport { useFormStatus } from 'react-dom';\n\nimport { Button } from '@/vibes/soul/primitives/button';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\nimport { toast } from '@/vibes/soul/primitives/toaster';\nimport {\n  GiftCertificateCodeForm,\n  GiftCertificateCodeFormState,\n} from '@/vibes/soul/sections/cart/gift-certificate-code-form';\nimport { StickySidebarLayout } from '@/vibes/soul/sections/sticky-sidebar-layout';\nimport { useEvents } from '~/components/analytics/events';\nimport { Image } from '~/components/image';\n\nimport { CouponCodeForm, CouponCodeFormState } from './coupon-code-form';\nimport { cartLineItemActionFormDataSchema } from './schema';\nimport { ShippingForm, ShippingFormState } from './shipping-form';\n\nimport { CartEmptyState } from '.';\n\ntype Action<State, Payload> = (state: Awaited<State>, payload: Payload) => State | Promise<State>;\n\ninterface CartLineIteminventoryMessages {\n  outOfStockMessage?: string;\n  quantityReadyToShipMessage?: string;\n  quantityBackorderedMessage?: string;\n  quantityOutOfStockMessage?: string;\n  backorderMessage?: string;\n}\n\nexport interface CartLineItem {\n  typename: string;\n  id: string;\n  title: string;\n  image?: { alt: string; src: string };\n  subtitle: string;\n  quantity: number;\n  price: string;\n  salePrice?: string;\n  href?: string;\n  inventoryMessages?: CartLineIteminventoryMessages;\n}\n\nexport interface CartGiftCertificateLineItem extends CartLineItem {\n  sender: {\n    name: string;\n    email: string;\n  };\n  recipient: {\n    name: string;\n    email: string;\n  };\n  message?: string;\n}\n\nexport interface CartSummaryItem {\n  label: string;\n  value: string;\n}\n\nexport interface CartState<LineItem extends CartLineItem> {\n  lineItems: LineItem[];\n  lastResult: SubmissionResult | null;\n}\n\nexport interface Cart<LineItem extends CartLineItem> {\n  lineItems: LineItem[];\n  summaryItems: CartSummaryItem[];\n  total: string;\n  totalLabel?: string;\n}\n\ninterface CouponCode {\n  action: Action<CouponCodeFormState, FormData>;\n  couponCodes?: string[];\n  ctaLabel?: string;\n  disabled?: boolean;\n  label?: string;\n  placeholder?: string;\n  removeLabel?: string;\n}\n\ninterface GiftCertificate {\n  action: Action<GiftCertificateCodeFormState, FormData>;\n  giftCertificateCodes?: string[];\n  ctaLabel?: string;\n  disabled?: boolean;\n  label?: string;\n  placeholder?: string;\n  removeLabel?: string;\n}\n\ninterface ShippingOption {\n  label: string;\n  value: string;\n  price: string;\n}\n\ninterface Country {\n  label: string;\n  value: string;\n}\n\ninterface States {\n  country: string;\n  states: Array<{\n    label: string;\n    value: string;\n  }>;\n}\n\ninterface Address {\n  country: string;\n  city?: string;\n  state?: string;\n  postalCode?: string;\n}\n\ninterface Shipping {\n  action: Action<ShippingFormState, FormData>;\n  countries?: Country[];\n  states?: States[];\n  address?: Address;\n  shippingOptions?: ShippingOption[];\n  shippingOption?: ShippingOption;\n  shippingLabel?: string;\n  addLabel?: string;\n  changeLabel?: string;\n  countryLabel?: string;\n  cityLabel?: string;\n  stateLabel?: string;\n  postalCodeLabel?: string;\n  updateShippingOptionsLabel?: string;\n  viewShippingOptionsLabel?: string;\n  cancelLabel?: string;\n  editAddressLabel?: string;\n  shippingOptionsLabel?: string;\n  updateShippingLabel?: string;\n  addShippingLabel?: string;\n  showShippingForm?: boolean;\n  noShippingOptionsLabel?: string;\n}\n\nexport interface CartProps<LineItem extends CartLineItem> {\n  title?: string;\n  summaryTitle?: string;\n  emptyState?: CartEmptyState;\n  lineItemAction: Action<CartState<LineItem>, FormData>;\n  checkoutAction: Action<SubmissionResult | null, FormData> | string;\n  checkoutLabel?: string;\n  deleteLineItemLabel?: string;\n  decrementLineItemLabel?: string;\n  incrementLineItemLabel?: string;\n  cart: Cart<LineItem>;\n  couponCode?: CouponCode;\n  giftCertificate?: GiftCertificate;\n  shipping?: Shipping;\n  lineItemActionPendingLabel?: string;\n}\n\nconst defaultEmptyState = {\n  title: 'Your cart is empty',\n  subtitle: 'Add some products to get started.',\n  cta: { label: 'Continue shopping', href: '#' },\n};\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --cart-focus: hsl(var(--primary));\n *   --cart-font-family: var(--font-family-body);\n *   --cart-title-font-family: var(--font-family-heading);\n *   --cart-text: hsl(var(--foreground));\n *   --cart-subtitle-text: hsl(var(--contrast-500));\n *   --cart-subtext-text: hsl(var(--contrast-300));\n *   --cart-icon: hsl(var(--contrast-300));\n *   --cart-icon-hover: hsl(var(--foreground));\n *   --cart-border: hsl(var(--contrast-100));\n *   --cart-image-background: hsl(var(--contrast-100));\n *   --cart-button-background: hsl(var(--contrast-100));\n *   --cart-counter-icon: hsl(var(--contrast-300));\n *   --cart-counter-icon-hover: hsl(var(--foreground));\n *   --cart-counter-background: hsl(var(--background));\n *   --cart-counter-background-hover: hsl(var(--contast-100) / 50%);\n * }\n * ```\n */\nexport function CartClient<LineItem extends CartLineItem>({\n  title,\n  cart,\n  couponCode,\n  giftCertificate,\n  decrementLineItemLabel,\n  incrementLineItemLabel,\n  deleteLineItemLabel,\n  lineItemAction,\n  lineItemActionPendingLabel = 'You have a cart update in progress. Are you sure you want to leave this page? Your changes may be lost.',\n  checkoutAction,\n  checkoutLabel = 'Checkout',\n  emptyState = defaultEmptyState,\n  summaryTitle,\n  shipping,\n}: CartProps<LineItem>) {\n  const events = useEvents();\n  const [state, formAction, isLineItemActionPending] = useActionState(lineItemAction, {\n    lineItems: cart.lineItems,\n    lastResult: null,\n  });\n\n  const [form] = useForm({ lastResult: state.lastResult });\n\n  useEffect(() => {\n    if (form.errors) {\n      form.errors.forEach((error) => {\n        toast.error(error);\n      });\n    }\n  }, [form.errors]);\n\n  // Prevent page unload when line item action is pending\n  useEffect(() => {\n    const handleBeforeUnload = (event: BeforeUnloadEvent) => {\n      if (isLineItemActionPending) {\n        event.preventDefault();\n        // eslint-disable-next-line @typescript-eslint/no-deprecated\n        event.returnValue = ''; // Chrome requires returnValue to be set\n\n        return ''; // For older browsers\n      }\n    };\n\n    if (isLineItemActionPending) {\n      window.addEventListener('beforeunload', handleBeforeUnload);\n    }\n\n    return () => {\n      window.removeEventListener('beforeunload', handleBeforeUnload);\n    };\n  }, [isLineItemActionPending]);\n\n  // Prevent client-side navigation when line item action is pending\n  useEffect(() => {\n    const handleClick = (event: MouseEvent) => {\n      if (isLineItemActionPending && event.target instanceof HTMLElement) {\n        const link = event.target.closest('a[href]');\n\n        if (\n          link instanceof HTMLAnchorElement &&\n          link.href &&\n          !link.href.startsWith('mailto:') &&\n          !link.href.startsWith('tel:')\n        ) {\n          // eslint-disable-next-line no-alert\n          const shouldNavigate = window.confirm(lineItemActionPendingLabel);\n\n          if (!shouldNavigate) {\n            event.preventDefault();\n            event.stopPropagation();\n          }\n        }\n      }\n    };\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (\n        isLineItemActionPending &&\n        (event.key === 'Enter' || event.key === ' ') &&\n        event.target instanceof HTMLElement\n      ) {\n        const link = event.target.closest('a[href]');\n\n        if (\n          link instanceof HTMLAnchorElement &&\n          link.href &&\n          !link.href.startsWith('mailto:') &&\n          !link.href.startsWith('tel:')\n        ) {\n          // eslint-disable-next-line no-alert\n          const shouldNavigate = window.confirm(lineItemActionPendingLabel);\n\n          if (!shouldNavigate) {\n            event.preventDefault();\n            event.stopPropagation();\n          }\n        }\n      }\n    };\n\n    if (isLineItemActionPending) {\n      document.addEventListener('click', handleClick, true);\n      document.addEventListener('keydown', handleKeyDown, true);\n    }\n\n    return () => {\n      document.removeEventListener('click', handleClick, true);\n      document.removeEventListener('keydown', handleKeyDown, true);\n    };\n  }, [isLineItemActionPending, lineItemActionPendingLabel]);\n\n  const [optimisticLineItems, setOptimisticLineItems] = useOptimistic<CartLineItem[], FormData>(\n    state.lineItems,\n    (prevState, formData) => {\n      const submission = parseWithZod(formData, { schema: cartLineItemActionFormDataSchema });\n\n      if (submission.status !== 'success') return prevState;\n\n      switch (submission.value.intent) {\n        case 'increment': {\n          const { id } = submission.value;\n\n          return prevState.map((item) =>\n            item.id === id ? { ...item, quantity: item.quantity + 1 } : item,\n          );\n        }\n\n        case 'decrement': {\n          const { id } = submission.value;\n\n          return prevState.map((item) =>\n            item.id === id ? { ...item, quantity: item.quantity - 1 } : item,\n          );\n        }\n\n        case 'delete': {\n          const { id } = submission.value;\n\n          return prevState.filter((item) => item.id !== id);\n        }\n\n        default:\n          return prevState;\n      }\n    },\n  );\n\n  const optimisticQuantity = useMemo(\n    () => optimisticLineItems.reduce((total, item) => total + item.quantity, 0),\n    [optimisticLineItems],\n  );\n\n  if (optimisticQuantity === 0) {\n    return <CartEmptyState {...emptyState} />;\n  }\n\n  return (\n    <StickySidebarLayout\n      className=\"font-[family-name:var(--cart-font-family,var(--font-family-body))] text-[var(--cart-text,hsl(var(--foreground)))]\"\n      sidebar={\n        <div>\n          <h2 className=\"mb-10 font-[family-name:var(--cart-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none @xl:text-5xl\">\n            {summaryTitle}\n          </h2>\n          <dl aria-label=\"Receipt Summary\" className=\"w-full\">\n            <div className=\"divide-y divide-[var(--cart-border,hsl(var(--contrast-100)))]\">\n              {cart.summaryItems.map((summaryItem, index) => (\n                <div className=\"flex justify-between py-4\" key={index}>\n                  <dt>{summaryItem.label}</dt>\n                  {isLineItemActionPending ? (\n                    <Skeleton.Text characterCount={8} className=\"animate-pulse rounded-md\" />\n                  ) : (\n                    <dd>{summaryItem.value}</dd>\n                  )}\n                </div>\n              ))}\n\n              {shipping && <ShippingForm {...shipping} />}\n            </div>\n            {couponCode && (\n              <CouponCodeForm\n                action={couponCode.action}\n                couponCodes={couponCode.couponCodes}\n                ctaLabel={couponCode.ctaLabel}\n                disabled={couponCode.disabled}\n                label={couponCode.label}\n                placeholder={couponCode.placeholder}\n                removeLabel={couponCode.removeLabel}\n              />\n            )}\n            {giftCertificate && (\n              <GiftCertificateCodeForm\n                action={giftCertificate.action}\n                ctaLabel={giftCertificate.ctaLabel}\n                disabled={giftCertificate.disabled}\n                giftCertificateCodes={giftCertificate.giftCertificateCodes}\n                label={giftCertificate.label}\n                placeholder={giftCertificate.placeholder}\n                removeLabel={giftCertificate.removeLabel}\n              />\n            )}\n            <div className=\"flex justify-between border-t border-[var(--cart-border,hsl(var(--contrast-100)))] py-6 text-xl font-bold\">\n              <dt>{cart.totalLabel ?? 'Total'}</dt>\n              {isLineItemActionPending ? (\n                <Skeleton.Text characterCount={8} className=\"animate-pulse rounded-md\" />\n              ) : (\n                <dd>{cart.total}</dd>\n              )}\n            </div>\n          </dl>\n          <CheckoutButton\n            action={checkoutAction}\n            className=\"mt-4 w-full\"\n            isCartUpdatePending={isLineItemActionPending}\n          >\n            {checkoutLabel}\n            <ArrowRight size={20} strokeWidth={1} />\n          </CheckoutButton>\n        </div>\n      }\n      sidebarPosition=\"after\"\n      sidebarSize=\"1/3\"\n    >\n      <div className=\"w-full\">\n        <h1 className=\"mb-10 font-[family-name:var(--cart-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none @xl:text-5xl\">\n          {title}\n          <span className=\"ml-4 text-[var(--cart-subtext-text,hsl(var(--contrast-300)))] contrast-more:text-[var(--cart-subtitle-text,hsl(var(--contrast-500)))]\">\n            {optimisticQuantity}\n          </span>\n        </h1>\n        {/* Cart Items */}\n        <ul className=\"flex flex-col gap-5\">\n          {optimisticLineItems.map((lineItem) => (\n            <li\n              className=\"flex flex-col items-start gap-x-5 gap-y-4 @container @sm:flex-row\"\n              key={lineItem.id}\n            >\n              <div className=\"relative aspect-square w-full max-w-24 overflow-hidden rounded-xl bg-[var(--cart-image-background,hsl(var(--contrast-100)))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] focus-visible:ring-offset-4\">\n                {lineItem.typename === 'CartGiftCertificate' ? (\n                  <div className=\"flex h-full w-full flex-col items-center justify-center p-4 text-center\">\n                    <GiftIcon className=\"h-full w-full text-[var(--cart-icon,hsl(var(--contrast-300)))]\" />\n                  </div>\n                ) : (\n                  lineItem.image != null && (\n                    <Image\n                      alt={lineItem.image.alt}\n                      className=\"object-cover\"\n                      fill\n                      sizes=\"(min-width: 28rem) 9rem, (min-width: 24rem) 6rem, 100vw\"\n                      src={lineItem.image.src}\n                    />\n                  )\n                )}\n              </div>\n              <div className=\"flex grow flex-col flex-wrap justify-between gap-y-2 @xl:flex-row\">\n                <div className=\"flex w-full flex-1 flex-col @xl:w-1/2 @xl:pr-4\">\n                  <span className=\"font-medium\">{lineItem.title}</span>\n                  <span className=\"text-[var(--cart-subtext-text,hsl(var(--contrast-400)))] contrast-more:text-[var(--cart-subtitle-text,hsl(var(--contrast-500)))]\">\n                    {lineItem.subtitle}\n                  </span>\n                </div>\n                <CounterForm\n                  action={formAction}\n                  decrementLabel={decrementLineItemLabel}\n                  deleteLabel={deleteLineItemLabel}\n                  incrementLabel={incrementLineItemLabel}\n                  lineItem={lineItem}\n                  onSubmit={(formData) => {\n                    startTransition(() => {\n                      formAction(formData);\n                      setOptimisticLineItems(formData);\n\n                      const intent = formData.get('intent');\n\n                      if (intent === 'increment') {\n                        formData.set('quantity', '1');\n\n                        events.onAddToCart?.(formData);\n                      }\n\n                      if (intent === 'decrement') {\n                        formData.set('quantity', '1');\n\n                        events.onRemoveFromCart?.(formData);\n                      }\n\n                      if (intent === 'delete') {\n                        formData.set('quantity', lineItem.quantity.toString());\n\n                        events.onRemoveFromCart?.(formData);\n                      }\n                    });\n                  }}\n                />\n              </div>\n            </li>\n          ))}\n        </ul>\n      </div>\n    </StickySidebarLayout>\n  );\n}\n\nfunction CounterForm({\n  lineItem,\n  action,\n  onSubmit,\n  incrementLabel = 'Increase count',\n  decrementLabel = 'Decrease count',\n  deleteLabel = 'Remove item',\n}: {\n  lineItem: CartLineItem;\n  incrementLabel?: string;\n  decrementLabel?: string;\n  deleteLabel?: string;\n  action: (payload: FormData) => void;\n  onSubmit: (formData: FormData) => void;\n}) {\n  const t = useTranslations('Cart');\n\n  const [form, fields] = useForm({\n    defaultValue: { id: lineItem.id },\n    shouldValidate: 'onBlur',\n    shouldRevalidate: 'onInput',\n    onValidate({ formData }) {\n      return parseWithZod(formData, { schema: cartLineItemActionFormDataSchema });\n    },\n    onSubmit(event, { formData }) {\n      event.preventDefault();\n\n      onSubmit(formData);\n    },\n  });\n\n  if (lineItem.typename === 'CartGiftCertificate') {\n    return (\n      <form {...getFormProps(form)} action={action}>\n        <input {...getInputProps(fields.id, { type: 'hidden' })} key={fields.id.id} />\n        <div className=\"flex w-full flex-wrap items-center gap-x-5 gap-y-2\">\n          <span className=\"font-medium @xl:ml-auto\">{lineItem.price}</span>\n\n          <span className=\"flex flex-1 select-none justify-center px-14 py-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))]\">\n            {lineItem.quantity}\n          </span>\n\n          <button\n            aria-label={deleteLabel}\n            className=\"group -ml-1 flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-colors duration-300 hover:bg-[var(--cart-button-background,hsl(var(--contrast-100)))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] focus-visible:ring-offset-4\"\n            name=\"intent\"\n            type=\"submit\"\n            value=\"delete\"\n          >\n            <Trash2\n              className=\"text-[var(--cart-icon,hsl(var(--contrast-300)))] group-hover:text-[var(--cart-icon-hover,hsl(var(--foreground)))]\"\n              size={20}\n              strokeWidth={1}\n            />\n          </button>\n        </div>\n      </form>\n    );\n  }\n\n  return (\n    <form {...getFormProps(form)} action={action}>\n      <input {...getInputProps(fields.id, { type: 'hidden' })} key={fields.id.id} />\n      <div className=\"flex w-full flex-wrap items-center gap-x-5 gap-y-2\">\n        {lineItem.salePrice && lineItem.salePrice !== lineItem.price ? (\n          <span className=\"mt-3 self-start font-medium @xl:ml-auto\">\n            <span className=\"sr-only\">{t('originalPrice', { price: lineItem.price })}</span>\n            <span aria-hidden=\"true\" className=\"line-through\">\n              {lineItem.price}\n            </span>{' '}\n            <span className=\"sr-only\">{t('currentPrice', { price: lineItem.salePrice })}</span>\n            <span aria-hidden=\"true\">{lineItem.salePrice}</span>\n          </span>\n        ) : (\n          <span className=\"mt-3 self-start font-medium @xl:ml-auto\">{lineItem.price}</span>\n        )}\n        <div className=\"flex size-min flex-col gap-y-0\">\n          <div className=\"mb-1 mt-1 flex items-center gap-x-5\">\n            {/* Counter */}\n            <div\n              className={clsx(\n                'flex items-center rounded-lg border border-[var(--cart-counter-border,hsl(var(--contrast-100)))]',\n                (lineItem.inventoryMessages?.outOfStockMessage != null ||\n                  lineItem.inventoryMessages?.quantityOutOfStockMessage != null) &&\n                  'border-red-500',\n              )}\n            >\n              <button\n                aria-label={decrementLabel}\n                className={clsx(\n                  'group rounded-l-lg bg-[var(--cart-counter-background,hsl(var(--background)))] p-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] disabled:cursor-not-allowed',\n                  lineItem.quantity === 1\n                    ? 'opacity-50'\n                    : 'hover:bg-[var(--cart-counter-background-hover,hsl(var(--contrast-100)/50%))]',\n                )}\n                disabled={lineItem.quantity === 1}\n                name=\"intent\"\n                type=\"submit\"\n                value=\"decrement\"\n              >\n                <Minus\n                  className={clsx(\n                    'text-[var(--cart-counter-icon,hsl(var(--contrast-300)))] transition-colors duration-300',\n                    lineItem.quantity !== 1 &&\n                      'group-hover:text-[var(--cart-counter-icon-hover,hsl(var(--foreground)))]',\n                  )}\n                  size={18}\n                  strokeWidth={1.5}\n                />\n              </button>\n              <span className=\"flex w-8 select-none justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))]\">\n                {lineItem.quantity}\n              </span>\n              <button\n                aria-label={incrementLabel}\n                className={clsx(\n                  'group rounded-r-lg bg-[var(--cart-counter-background,hsl(var(--background)))] p-3 transition-colors duration-300 hover:bg-[var(--cart-counter-background-hover,hsl(var(--contrast-100)/50%))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] disabled:cursor-not-allowed',\n                )}\n                name=\"intent\"\n                type=\"submit\"\n                value=\"increment\"\n              >\n                <Plus\n                  className=\"text-[var(--cart-counter-icon,hsl(var(--contrast-300)))] transition-colors duration-300 group-hover:text-[var(--cart-counter-icon-hover,hsl(var(--foreground)))]\"\n                  size={18}\n                  strokeWidth={1.5}\n                />\n              </button>\n            </div>\n            <button\n              aria-label={deleteLabel}\n              className=\"group -ml-1 mt-1.5 flex h-8 w-8 shrink-0 items-center justify-center self-start rounded-full transition-colors duration-300 hover:bg-[var(--cart-button-background,hsl(var(--contrast-100)))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] focus-visible:ring-offset-4\"\n              name=\"intent\"\n              type=\"submit\"\n              value=\"delete\"\n            >\n              <Trash2\n                className=\"text-[var(--cart-icon,hsl(var(--contrast-300)))] group-hover:text-[var(--cart-icon-hover,hsl(var(--foreground)))]\"\n                size={20}\n                strokeWidth={1}\n              />\n            </button>\n          </div>\n          {lineItem.inventoryMessages?.outOfStockMessage != null && (\n            <span className=\"text-xs/5 font-light text-red-500\">\n              {lineItem.inventoryMessages.outOfStockMessage}\n            </span>\n          )}\n          {lineItem.inventoryMessages?.quantityOutOfStockMessage != null && (\n            <span className=\"mb-3 text-xs/5 font-light text-red-500\">\n              {lineItem.inventoryMessages.quantityOutOfStockMessage}\n            </span>\n          )}\n          {lineItem.inventoryMessages?.quantityReadyToShipMessage != null && (\n            <span className=\"text-xs/5 font-light\">\n              {lineItem.inventoryMessages.quantityReadyToShipMessage}\n            </span>\n          )}\n          {lineItem.inventoryMessages?.quantityBackorderedMessage != null && (\n            <span className=\"text-xs/5 font-light\">\n              {lineItem.inventoryMessages.quantityBackorderedMessage}\n            </span>\n          )}\n          {lineItem.inventoryMessages?.backorderMessage != null && (\n            <span className=\"text-xs/5 font-light\">\n              {lineItem.inventoryMessages.backorderMessage}\n            </span>\n          )}\n        </div>\n      </div>\n    </form>\n  );\n}\n\nfunction CheckoutButton({\n  action,\n  isCartUpdatePending,\n  ...props\n}: {\n  action: Action<SubmissionResult | null, FormData> | string;\n  isCartUpdatePending: boolean;\n} & ComponentPropsWithoutRef<typeof Button>) {\n  const [lastResult, formAction] = useActionState(\n    async (state: SubmissionResult | null, formData: FormData) => {\n      if (typeof action === 'string') {\n        await new Promise<void>(() => {\n          window.location.assign(action);\n        });\n\n        return null;\n      }\n\n      return action(state, formData);\n    },\n    null,\n  );\n\n  const [form] = useForm({ lastResult });\n\n  useEffect(() => {\n    if (form.errors) {\n      form.errors.forEach((error) => {\n        toast.error(error);\n      });\n    }\n  }, [form.errors]);\n\n  return (\n    <form action={formAction}>\n      <SubmitButton {...props} isCartUpdatePending={isCartUpdatePending} />\n    </form>\n  );\n}\n\nfunction SubmitButton({\n  isCartUpdatePending,\n  ...props\n}: { isCartUpdatePending: boolean } & ComponentPropsWithoutRef<typeof Button>) {\n  const { pending } = useFormStatus();\n\n  return (\n    <Button\n      {...props}\n      disabled={pending || isCartUpdatePending}\n      loading={pending || isCartUpdatePending}\n      type=\"submit\"\n    />\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/cart/coupon-code-form/coupon-chip.tsx",
    "content": "import { getFormProps, getInputProps, useForm } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\n\nimport { Chip } from '@/vibes/soul/primitives/chip';\n\nimport { couponCodeActionFormDataSchema } from '../schema';\n\nexport interface CouponChipProps {\n  action: (payload: FormData) => void;\n  onSubmit: (formData: FormData) => void;\n  couponCode: string;\n  removeLabel?: string;\n}\n\nexport function CouponChip({\n  couponCode,\n  removeLabel = 'Remove promo code',\n  onSubmit,\n  action,\n}: CouponChipProps) {\n  const [form, fields] = useForm({\n    onValidate({ formData }) {\n      return parseWithZod(formData, {\n        schema: couponCodeActionFormDataSchema({}),\n      });\n    },\n    onSubmit(event, { formData }) {\n      event.preventDefault();\n\n      onSubmit(formData);\n    },\n  });\n\n  return (\n    <form {...getFormProps(form)} action={action}>\n      <input\n        {...getInputProps(fields.couponCode, {\n          type: 'hidden',\n        })}\n        value={couponCode}\n      />\n      <Chip name=\"intent\" removeLabel={removeLabel} value=\"delete\">\n        {couponCode.toUpperCase()}\n      </Chip>\n    </form>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/cart/coupon-code-form/index.tsx",
    "content": "'use client';\n\nimport { getFormProps, getInputProps, SubmissionResult, useForm } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { useTranslations } from 'next-intl';\nimport { startTransition, useActionState, useOptimistic } from 'react';\nimport { useFormStatus } from 'react-dom';\n\nimport { FieldError } from '@/vibes/soul/form/field-error';\nimport { Input } from '@/vibes/soul/form/input';\nimport { Button } from '@/vibes/soul/primitives/button';\n\nimport { couponCodeActionFormDataSchema } from '../schema';\n\nimport { CouponChip } from './coupon-chip';\n\ntype Action<State, Payload> = (state: Awaited<State>, payload: Payload) => State | Promise<State>;\n\nexport interface CouponCodeFormState {\n  couponCodes: string[];\n  lastResult: SubmissionResult | null;\n}\n\nexport interface CouponCodeFormProps {\n  action: Action<CouponCodeFormState, FormData>;\n  couponCodes?: string[];\n  ctaLabel?: string;\n  disabled?: boolean;\n  label?: string;\n  placeholder?: string;\n  removeLabel?: string;\n}\n\nexport function CouponCodeForm({\n  action,\n  couponCodes,\n  ctaLabel = 'Apply',\n  disabled = false,\n  label = 'Promo code',\n  placeholder,\n  removeLabel,\n}: CouponCodeFormProps) {\n  const t = useTranslations('Cart.CheckoutSummary.CouponCode');\n  const schema = couponCodeActionFormDataSchema({ required_error: t('invalidCouponCode') });\n  const [state, formAction] = useActionState(action, {\n    couponCodes: couponCodes ?? [],\n    lastResult: null,\n  });\n\n  const [optimisticCouponCodes, setOptimisticCouponCodes] = useOptimistic<string[], FormData>(\n    state.couponCodes,\n    (prevState, formData) => {\n      const submission = parseWithZod(formData, { schema });\n\n      if (submission.status !== 'success') return prevState;\n\n      switch (submission.value.intent) {\n        case 'delete': {\n          const couponCode = submission.value.couponCode;\n\n          return prevState.filter((code) => code !== couponCode);\n        }\n\n        default:\n          return prevState;\n      }\n    },\n  );\n\n  const [form, fields] = useForm({\n    lastResult: state.lastResult,\n    shouldValidate: 'onBlur',\n    shouldRevalidate: 'onInput',\n    onValidate({ formData }) {\n      return parseWithZod(formData, { schema });\n    },\n    onSubmit(event, { formData }) {\n      event.preventDefault();\n\n      startTransition(() => {\n        formAction(formData);\n        setOptimisticCouponCodes(formData);\n      });\n    },\n  });\n\n  return (\n    <div className=\"space-y-2 border-t border-[var(--cart-border,hsl(var(--contrast-100)))] pb-5 pt-4\">\n      <form {...getFormProps(form)} action={formAction} className=\"space-y-2\">\n        <label htmlFor={fields.couponCode.id}>{label}</label>\n        <div className=\"mt-2 flex gap-1.5\">\n          <Input\n            {...getInputProps(fields.couponCode, {\n              required: true,\n              type: 'text',\n            })}\n            disabled={disabled}\n            errors={fields.couponCode.errors}\n            id={fields.couponCode.id}\n            key={fields.couponCode.id}\n            placeholder={placeholder}\n          />\n          <SubmitButton disabled={disabled}>{ctaLabel}</SubmitButton>\n        </div>\n      </form>\n      {optimisticCouponCodes.length > 0 && (\n        <div className=\"flex flex-wrap gap-1.5\">\n          {optimisticCouponCodes.map((couponCode) => (\n            <CouponChip\n              action={formAction}\n              couponCode={couponCode}\n              key={couponCode}\n              onSubmit={(formData) => {\n                startTransition(() => {\n                  formAction(formData);\n                  setOptimisticCouponCodes(formData);\n                });\n              }}\n              removeLabel={removeLabel}\n            />\n          ))}\n        </div>\n      )}\n      {form.errors?.map((error, index) => (\n        <FieldError key={index}>{error}</FieldError>\n      ))}\n    </div>\n  );\n}\n\nfunction SubmitButton({ disabled, ...props }: React.ComponentPropsWithoutRef<typeof Button>) {\n  const { pending } = useFormStatus();\n\n  return (\n    <Button\n      {...props}\n      className=\"shrink-0\"\n      disabled={disabled ?? pending}\n      loading={pending}\n      name=\"intent\"\n      size=\"small\"\n      type=\"submit\"\n      value=\"apply\"\n      variant=\"secondary\"\n    />\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/cart/gift-certificate-code-form/gift-certificate-chip.tsx",
    "content": "import { getFormProps, getInputProps, useForm } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\n\nimport { Chip } from '@/vibes/soul/primitives/chip';\n\nimport { giftCertificateCodeActionFormDataSchema } from '../schema';\n\nexport interface GiftCertificateChipProps {\n  action: (payload: FormData) => void;\n  onSubmit: (formData: FormData) => void;\n  giftCertificateCode: string;\n  removeLabel?: string;\n}\n\nexport function GiftCertificateChip({\n  giftCertificateCode,\n  removeLabel = 'Remove gift certificate code',\n  onSubmit,\n  action,\n}: GiftCertificateChipProps) {\n  const [form, fields] = useForm({\n    onValidate({ formData }) {\n      return parseWithZod(formData, {\n        schema: giftCertificateCodeActionFormDataSchema({}),\n      });\n    },\n    onSubmit(event, { formData }) {\n      event.preventDefault();\n\n      onSubmit(formData);\n    },\n  });\n\n  return (\n    <form {...getFormProps(form)} action={action}>\n      <input\n        {...getInputProps(fields.giftCertificateCode, {\n          type: 'hidden',\n        })}\n        value={giftCertificateCode}\n      />\n      <Chip name=\"intent\" removeLabel={removeLabel} value=\"delete\">\n        {giftCertificateCode.toUpperCase()}\n      </Chip>\n    </form>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/cart/gift-certificate-code-form/index.tsx",
    "content": "'use client';\n\nimport { getFormProps, getInputProps, SubmissionResult, useForm } from '@conform-to/react';\nimport { parseWithZod } from '@conform-to/zod';\nimport { useTranslations } from 'next-intl';\nimport { startTransition, useActionState, useOptimistic } from 'react';\nimport { useFormStatus } from 'react-dom';\n\nimport { FieldError } from '@/vibes/soul/form/field-error';\nimport { Input } from '@/vibes/soul/form/input';\nimport { Button } from '@/vibes/soul/primitives/button';\n\nimport { giftCertificateCodeActionFormDataSchema } from '../schema';\n\nimport { GiftCertificateChip } from './gift-certificate-chip';\n\ntype Action<State, Payload> = (state: Awaited<State>, payload: Payload) => State | Promise<State>;\n\nexport interface GiftCertificateCodeFormState {\n  giftCertificateCodes: string[];\n  lastResult: SubmissionResult | null;\n}\n\nexport interface GiftCertificateCodeFormProps {\n  action: Action<GiftCertificateCodeFormState, FormData>;\n  giftCertificateCodes?: string[];\n  ctaLabel?: string;\n  disabled?: boolean;\n  label?: string;\n  placeholder?: string;\n  removeLabel?: string;\n}\n\nexport function GiftCertificateCodeForm({\n  action,\n  giftCertificateCodes,\n  ctaLabel = 'Apply',\n  disabled = false,\n  label = 'Gift certificate code',\n  placeholder,\n  removeLabel,\n}: GiftCertificateCodeFormProps) {\n  const t = useTranslations('Cart.GiftCertificate');\n  const [state, formAction] = useActionState(action, {\n    giftCertificateCodes: giftCertificateCodes ?? [],\n    lastResult: null,\n  });\n\n  const schema = giftCertificateCodeActionFormDataSchema({\n    required_error: t('invalidGiftCertificate'),\n  });\n\n  const [optimisticGiftCertificateCodes, setOptimisticGiftCertificateCodes] = useOptimistic<\n    string[],\n    FormData\n  >(state.giftCertificateCodes, (prevState, formData) => {\n    const submission = parseWithZod(formData, { schema });\n\n    if (submission.status !== 'success') return prevState;\n\n    switch (submission.value.intent) {\n      case 'delete': {\n        const giftCertificateCode = submission.value.giftCertificateCode;\n\n        return prevState.filter((code) => code !== giftCertificateCode);\n      }\n\n      default:\n        return prevState;\n    }\n  });\n\n  const [form, fields] = useForm({\n    lastResult: state.lastResult,\n    shouldValidate: 'onBlur',\n    shouldRevalidate: 'onInput',\n    onValidate({ formData }) {\n      return parseWithZod(formData, { schema });\n    },\n    onSubmit(event, { formData }) {\n      event.preventDefault();\n\n      startTransition(() => {\n        formAction(formData);\n        setOptimisticGiftCertificateCodes(formData);\n      });\n    },\n  });\n\n  return (\n    <div className=\"space-y-2 border-t border-[var(--cart-border,hsl(var(--contrast-100)))] pb-5 pt-4\">\n      <form {...getFormProps(form)} action={formAction} className=\"space-y-2\">\n        <label htmlFor={fields.giftCertificateCode.id}>{label}</label>\n        <div className=\"mt-2 flex gap-1.5\">\n          <Input\n            {...getInputProps(fields.giftCertificateCode, {\n              required: true,\n              type: 'text',\n            })}\n            disabled={disabled}\n            errors={fields.giftCertificateCode.errors}\n            id={fields.giftCertificateCode.id}\n            key={fields.giftCertificateCode.id}\n            placeholder={placeholder}\n          />\n          <SubmitButton disabled={disabled}>{ctaLabel}</SubmitButton>\n        </div>\n      </form>\n      {optimisticGiftCertificateCodes.length > 0 && (\n        <div className=\"flex flex-wrap gap-1.5\">\n          {optimisticGiftCertificateCodes.map((giftCertificateCode) => (\n            <GiftCertificateChip\n              action={formAction}\n              giftCertificateCode={giftCertificateCode}\n              key={giftCertificateCode}\n              onSubmit={(formData) => {\n                startTransition(() => {\n                  formAction(formData);\n                  setOptimisticGiftCertificateCodes(formData);\n                });\n              }}\n              removeLabel={removeLabel}\n            />\n          ))}\n        </div>\n      )}\n      {form.errors?.map((error, index) => (\n        <FieldError key={index}>{error}</FieldError>\n      ))}\n    </div>\n  );\n}\n\nfunction SubmitButton({ disabled, ...props }: React.ComponentPropsWithoutRef<typeof Button>) {\n  const { pending } = useFormStatus();\n\n  return (\n    <Button\n      {...props}\n      className=\"shrink-0\"\n      disabled={disabled ?? pending}\n      loading={pending}\n      name=\"intent\"\n      size=\"small\"\n      type=\"submit\"\n      value=\"apply\"\n      variant=\"secondary\"\n    />\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/cart/index.tsx",
    "content": "import { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { ButtonLink } from '@/vibes/soul/primitives/button-link';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\nimport { SectionLayout } from '@/vibes/soul/sections/section-layout';\nimport { StickySidebarLayout } from '@/vibes/soul/sections/sticky-sidebar-layout';\n\nimport { CartClient, Cart as CartData, CartLineItem, CartProps } from './client';\n\nexport { type CartLineItem } from './client';\n\nexport function Cart<LineItem extends CartLineItem>({\n  cart: streamableCart,\n  decrementLineItemLabel: streamableDecrementLineItemLabel,\n  title = 'Cart',\n  summaryTitle = 'Summary',\n  ...props\n}: Omit<CartProps<LineItem>, 'cart'> & {\n  cart: Streamable<CartData<LineItem>>;\n}) {\n  return (\n    <Stream\n      fallback={<CartSkeleton summaryTitle={summaryTitle} title={title} />}\n      value={streamableCart}\n    >\n      {(cart) => <CartClient {...props} cart={cart} summaryTitle={summaryTitle} title={title} />}\n    </Stream>\n  );\n}\n\nexport interface CartSkeletonProps {\n  className?: string;\n  placeholderCount?: number;\n  summaryPlaceholderCount?: number;\n  title?: string;\n  summaryTitle?: string;\n}\n\nexport function CartSkeleton({\n  title = 'Cart',\n  summaryTitle = 'Summary',\n  placeholderCount = 2,\n  summaryPlaceholderCount = 3,\n}: CartSkeletonProps) {\n  return (\n    <StickySidebarLayout\n      className=\"group/cart text-[var(--cart-text,hsl(var(--foreground)))]\"\n      sidebar={\n        <div>\n          <h2 className=\"mb-10 font-[family-name:var(--cart-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none @xl:text-5xl\">\n            {summaryTitle}\n          </h2>\n          <div className=\"group-has-[[data-pending]]/cart:animate-pulse\">\n            <div className=\"w-full\" data-pending>\n              <div className=\"divide-y divide-[var(--skeleton,hsl(var(--contrast-300)/15%))]\">\n                {Array.from({ length: summaryPlaceholderCount }).map((_, index) => (\n                  <div className=\"py-4\" key={index}>\n                    <div className=\"flex items-center justify-between\">\n                      <Skeleton.Text characterCount={10} className=\"rounded-md\" />\n                      <Skeleton.Text characterCount={8} className=\"rounded-md\" />\n                    </div>\n                  </div>\n                ))}\n              </div>\n              <div className=\"flex justify-between border-t border-[var(--skeleton,hsl(var(--contrast-300)/15%))] py-6 text-xl font-bold\">\n                <div className=\"flex items-center justify-between\">\n                  <Skeleton.Text characterCount={8} className=\"rounded-md\" />\n                </div>\n                <div className=\"flex items-center justify-between\">\n                  <Skeleton.Text characterCount={8} className=\"rounded-md\" />\n                </div>\n              </div>\n            </div>\n          </div>\n          <Skeleton.Box className=\"mt-4 h-[58px] w-full rounded-full\" />\n        </div>\n      }\n      sidebarPosition=\"after\"\n      sidebarSize=\"1/3\"\n    >\n      <div>\n        <h1 className=\"mb-10 font-[family-name:var(--cart-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none @xl:text-5xl\">\n          {title}\n        </h1>\n        {/* Cart Line Items */}\n        <div className=\"group-has-[[data-pending]]/cart:animate-pulse\">\n          <ul className=\"flex flex-col gap-5\" data-pending>\n            {Array.from({ length: placeholderCount }).map((_, index) => (\n              <li\n                className=\"flex flex-col items-start gap-x-5 gap-y-4 @container @sm:flex-row\"\n                key={index}\n              >\n                {/* Image */}\n                <Skeleton.Box className=\"aspect-square w-full max-w-24 rounded-xl\" />\n                <div className=\"flex grow flex-col flex-wrap justify-between gap-y-2 @xl:flex-row\">\n                  <div className=\"flex w-full flex-1 flex-col @xl:w-1/2 @xl:pr-4\">\n                    {/* Line Item Title */}\n                    <Skeleton.Text characterCount={15} className=\"rounded-md\" />\n                    {/* Subtitle */}\n                    <Skeleton.Text characterCount={10} className=\"rounded-md\" />\n                  </div>\n                  {/* Counter */}\n                  <div>\n                    <div className=\"flex w-full flex-wrap items-center gap-x-5 gap-y-2\">\n                      {/* Price */}\n                      <Skeleton.Text characterCount={5} className=\"rounded-md\" />\n                      {/* Counter */}\n                      <Skeleton.Box className=\"h-[44px] w-[118px] rounded-lg\" />\n                      {/* DeleteLineItemButton */}\n                      <Skeleton.Box className=\"-ml-1 h-8 w-8 rounded-full\" />\n                    </div>\n                  </div>\n                </div>\n              </li>\n            ))}\n          </ul>\n        </div>\n      </div>\n    </StickySidebarLayout>\n  );\n}\n\nexport interface CartEmptyState {\n  title: string;\n  subtitle: string;\n  cta: {\n    label: string;\n    href: string;\n  };\n}\n\nexport function CartEmptyState({ title, subtitle, cta }: CartEmptyState) {\n  return (\n    <SectionLayout className=\"text-center font-[family-name:var(--cart-font-family,var(--font-family-body))]\">\n      <h1 className=\"mb-3 text-center font-[family-name:var(--cart-title-font-family,var(--font-family-heading))] text-3xl leading-none text-[var(--cart-title,hsl(var(--foreground)))] @xl:text-4xl\">\n        {title}\n      </h1>\n      <p className=\"leading-normaltext-[var(--cart-subtitle,hsl(var(--contrast-500)))] mb-6 text-center @3xl:text-lg\">\n        {subtitle}\n      </p>\n      <ButtonLink href={cta.href}>{cta.label}</ButtonLink>\n    </SectionLayout>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/cart/schema.ts",
    "content": "import { z } from 'zod';\n\nexport const cartLineItemActionFormDataSchema = z.discriminatedUnion('intent', [\n  z.object({\n    intent: z.literal('increment'),\n    id: z.string(),\n  }),\n  z.object({\n    intent: z.literal('decrement'),\n    id: z.string(),\n  }),\n  z.object({\n    intent: z.literal('delete'),\n    id: z.string(),\n  }),\n]);\n\nexport const couponCodeActionFormDataSchema = ({\n  required_error = 'Please enter a valid promo code',\n}: {\n  required_error?: string;\n}) =>\n  z.discriminatedUnion('intent', [\n    z.object({\n      intent: z.literal('apply'),\n      couponCode: z.string({ required_error }),\n    }),\n    z.object({\n      intent: z.literal('delete'),\n      couponCode: z.string(),\n    }),\n  ]);\n\nexport const giftCertificateCodeActionFormDataSchema = ({\n  required_error = 'Please enter a valid gift certificate code',\n}: {\n  required_error?: string;\n}) =>\n  z.discriminatedUnion('intent', [\n    z.object({\n      intent: z.literal('apply'),\n      giftCertificateCode: z.string({ required_error }),\n    }),\n    z.object({\n      intent: z.literal('delete'),\n      giftCertificateCode: z.string(),\n    }),\n  ]);\n\nexport const shippingActionFormDataSchema = ({\n  required_error = 'Country is required',\n}: {\n  required_error?: string;\n}) =>\n  z.discriminatedUnion('intent', [\n    z.object({\n      intent: z.literal('add-address'),\n      country: z.string({ required_error }),\n      city: z.string().optional(),\n      state: z.string().optional(),\n      postalCode: z.string().optional(),\n    }),\n    z.object({\n      intent: z.literal('add-shipping'),\n      shippingOption: z.string(),\n    }),\n  ]);\n"
  },
  {
    "path": "core/vibes/soul/sections/cart/shipping-form/index.tsx",
    "content": "'use client';\n\nimport {\n  getFormProps,\n  getInputProps,\n  SubmissionResult,\n  useForm,\n  useInputControl,\n} from '@conform-to/react';\nimport { getZodConstraint, parseWithZod } from '@conform-to/zod';\nimport { clsx } from 'clsx';\nimport { useTranslations } from 'next-intl';\nimport { startTransition, useActionState, useEffect, useMemo, useState } from 'react';\nimport { useFormStatus } from 'react-dom';\n\nimport { FormStatus } from '@/vibes/soul/form/form-status';\nimport { Input } from '@/vibes/soul/form/input';\nimport { Label } from '@/vibes/soul/form/label';\nimport { RadioGroup } from '@/vibes/soul/form/radio-group';\nimport { Select } from '@/vibes/soul/form/select';\nimport { Button } from '@/vibes/soul/primitives/button';\n\nimport { shippingActionFormDataSchema } from '../schema';\n\ntype Action<State, Payload> = (state: Awaited<State>, payload: Payload) => State | Promise<State>;\n\nexport interface ShippingFormState {\n  lastResult: SubmissionResult | null;\n  address: Address | null;\n  shippingOptions: ShippingOption[] | null;\n  shippingOption: ShippingOption | null;\n  form: 'address' | 'shipping' | null;\n}\n\ninterface ShippingOption {\n  label: string;\n  value: string;\n  price: string;\n}\n\ninterface Country {\n  label: string;\n  value: string;\n}\n\ninterface States {\n  country: string;\n  states: Array<{\n    label: string;\n    value: string;\n  }>;\n}\n\ninterface Address {\n  country: string;\n  city?: string;\n  state?: string;\n  postalCode?: string;\n}\n\ninterface Props {\n  action: Action<ShippingFormState, FormData>;\n  countries?: Country[];\n  states?: States[];\n  address?: Address;\n  shippingOptions?: ShippingOption[];\n  shippingOption?: ShippingOption;\n  shippingLabel?: string;\n  addLabel?: string;\n  changeLabel?: string;\n  countryLabel?: string;\n  cityLabel?: string;\n  stateLabel?: string;\n  postalCodeLabel?: string;\n  updateShippingOptionsLabel?: string;\n  viewShippingOptionsLabel?: string;\n  cancelLabel?: string;\n  editAddressLabel?: string;\n  shippingOptionsLabel?: string;\n  updateShippingLabel?: string;\n  addShippingLabel?: string;\n  showShippingForm?: boolean;\n  noShippingOptionsLabel?: string;\n}\n\nexport function ShippingForm({\n  action,\n  address,\n  countries,\n  states,\n  shippingOptions,\n  shippingOption,\n  shippingLabel = 'Shipping',\n  addLabel = 'Add',\n  changeLabel = 'Change',\n  countryLabel = 'Country',\n  cityLabel = 'City',\n  stateLabel = 'State/Province',\n  postalCodeLabel = 'Postal code',\n  updateShippingOptionsLabel = 'Update shipping options',\n  viewShippingOptionsLabel = 'View shipping options',\n  cancelLabel = 'Cancel',\n  editAddressLabel = 'Edit address',\n  shippingOptionsLabel = 'Shipping options',\n  updateShippingLabel = 'Update shipping',\n  addShippingLabel = 'Add shipping',\n  showShippingForm = false,\n  noShippingOptionsLabel = 'There are no shipping options available for your address',\n}: Props) {\n  const t = useTranslations('Cart.CheckoutSummary.Shipping');\n  const schema = shippingActionFormDataSchema({ required_error: t('countryRequired') });\n  const [showForms, setShowForms] = useState(showShippingForm);\n  const [showAddressForm, setShowAddressForm] = useState(!address);\n\n  const [state, formAction] = useActionState(action, {\n    lastResult: null,\n    address: address ?? null,\n    shippingOptions: shippingOptions ?? null,\n    shippingOption: shippingOption ?? null,\n    form: null,\n  });\n\n  const [addressForm, addressFields] = useForm({\n    lastResult: state.form === 'address' ? state.lastResult : null,\n    constraint: getZodConstraint(schema),\n    shouldValidate: 'onBlur',\n    shouldRevalidate: 'onInput',\n    defaultValue: {\n      country: state.address?.country,\n      city: state.address?.city,\n      state: state.address?.state,\n      postalCode: state.address?.postalCode,\n    },\n    onValidate({ formData }) {\n      return parseWithZod(formData, { schema });\n    },\n    onSubmit(event, { formData }) {\n      event.preventDefault();\n\n      startTransition(() => {\n        formAction(formData);\n        setShowAddressForm(false);\n      });\n    },\n  });\n\n  const [shippingOptionsForm, shippingOptionsFields] = useForm({\n    lastResult: state.form === 'shipping' ? state.lastResult : null,\n    constraint: getZodConstraint(schema),\n    shouldValidate: 'onBlur',\n    shouldRevalidate: 'onInput',\n    defaultValue: {\n      shippingOption: state.shippingOption?.value,\n    },\n    onValidate({ formData }) {\n      return parseWithZod(formData, { schema });\n    },\n    onSubmit(event, { formData }) {\n      event.preventDefault();\n\n      startTransition(() => {\n        formAction(formData);\n        setShowForms(false);\n      });\n    },\n  });\n\n  const formattedAddress = useMemo(() => {\n    const country = Array.isArray(countries)\n      ? countries.find((c) => c.value === state.address?.country)?.label\n      : state.address?.country;\n\n    const city = state.address?.city;\n\n    const statesArray = Array.isArray(states)\n      ? states.find((s) => {\n          if (s.country === state.address?.country) {\n            return true;\n          }\n\n          return false;\n        })\n      : undefined;\n\n    const stateOrProvince = Array.isArray(statesArray?.states)\n      ? statesArray.states.find((s) => s.value === state.address?.state)?.label\n      : state.address?.state;\n\n    const cityAndState = [city, stateOrProvince].filter((item) => item !== undefined).join(', ');\n    const postalCode = state.address?.postalCode ?? '';\n\n    const firstLine = `${cityAndState} ${postalCode}`.trim();\n\n    return (\n      <div>\n        {Boolean(firstLine) && (\n          <>\n            {firstLine} <br />\n          </>\n        )}\n        {country}\n      </div>\n    );\n  }, [\n    countries,\n    state.address?.country,\n    state.address?.city,\n    state.address?.state,\n    state.address?.postalCode,\n    states,\n  ]);\n\n  useEffect(() => {\n    if (addressForm.errors && addressForm.errors.length > 0) {\n      if (state.shippingOptions) {\n        setShowForms(true);\n        setShowAddressForm(true);\n      }\n    }\n  }, [setShowForms, setShowAddressForm, addressForm.errors, state.shippingOptions]);\n\n  useEffect(() => {\n    if (shippingOptionsForm.errors && shippingOptionsForm.errors.length > 0) {\n      setShowForms(true);\n    }\n  }, [setShowForms, shippingOptionsForm.errors]);\n\n  const shippingOptionsControl = useInputControl(shippingOptionsFields.shippingOption);\n  const countryControl = useInputControl(addressFields.country);\n  const stateControl = useInputControl(addressFields.state);\n\n  return (\n    <div className=\"py-4\">\n      <div>\n        <div className=\"flex justify-between\">\n          <span>{shippingLabel}</span>\n          {state.shippingOption ? (\n            <span>{state.shippingOption.price}</span>\n          ) : (\n            !showForms && (\n              <button\n                className=\"font-semibold focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--button-focus,hsl(var(--primary)))] focus-visible:ring-offset-2\"\n                onClick={() => setShowForms(!showForms)}\n              >\n                {addLabel}\n              </button>\n            )\n          )}\n        </div>\n\n        {state.shippingOption && (\n          <div className=\"flex gap-1.5 text-xs\">\n            <span className=\"font-medium text-contrast-400\">{state.shippingOption.label}</span>\n            {!showForms && (\n              <button\n                className=\"font-semibold focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--button-focus,hsl(var(--primary)))] focus-visible:ring-offset-2\"\n                onClick={() => setShowForms(true)}\n              >\n                {changeLabel}\n              </button>\n            )}\n          </div>\n        )}\n      </div>\n\n      <div className={clsx('space-y-4', { hidden: !showForms })}>\n        <form\n          {...getFormProps(addressForm)}\n          action={formAction}\n          className={clsx('mt-4 space-y-4', { hidden: !showAddressForm })}\n        >\n          {Array.isArray(countries) ? (\n            <Select\n              errors={addressFields.country.errors}\n              key={addressFields.country.id}\n              label={countryLabel}\n              name={addressFields.country.name}\n              onBlur={countryControl.blur}\n              onFocus={countryControl.focus}\n              onValueChange={countryControl.change}\n              options={countries}\n              placeholder=\"\"\n              required\n              value={countryControl.value ?? ''}\n            />\n          ) : (\n            <Input\n              {...getInputProps(addressFields.country, { type: 'text' })}\n              errors={addressFields.country.errors}\n              key={addressFields.country.id}\n              label={countryLabel}\n              required\n            />\n          )}\n          <Input\n            {...getInputProps(addressFields.city, { type: 'text' })}\n            errors={addressFields.city.errors}\n            key={addressFields.city.id}\n            label={cityLabel}\n          />\n          <div className=\"flex gap-3\">\n            {Array.isArray(states) ? (\n              <Select\n                disabled={addressFields.country.value === undefined}\n                errors={addressFields.state.errors}\n                key={addressFields.state.id}\n                label={stateLabel}\n                name={addressFields.state.name}\n                onBlur={stateControl.blur}\n                onFocus={stateControl.focus}\n                onValueChange={stateControl.change}\n                options={\n                  states.find((s) => s.country === addressFields.country.value)?.states ?? []\n                }\n                placeholder=\"\"\n                value={stateControl.value ?? ''}\n              />\n            ) : (\n              <Input\n                {...getInputProps(addressFields.state, { type: 'text' })}\n                errors={addressFields.state.errors}\n                key={addressFields.state.id}\n                label={stateLabel}\n              />\n            )}\n            <Input\n              {...getInputProps(addressFields.postalCode, { type: 'text' })}\n              errors={addressFields.postalCode.errors}\n              key={addressFields.postalCode.id}\n              label={postalCodeLabel}\n            />\n          </div>\n\n          {addressForm.errors?.map((error, index) => (\n            <FormStatus key={index} type=\"error\">\n              {error}\n            </FormStatus>\n          ))}\n\n          <div className=\"flex gap-1.5\">\n            <SubmitButton className=\"grow\" name=\"intent\" value=\"add-address\">\n              {state.address ? updateShippingOptionsLabel : viewShippingOptionsLabel}\n            </SubmitButton>\n            <Button\n              className=\"shrink-0\"\n              onClick={() =>\n                state.shippingOptions && state.shippingOptions.length > 0\n                  ? setShowAddressForm(false)\n                  : setShowForms(false)\n              }\n              size=\"small\"\n              type=\"button\"\n              variant=\"tertiary\"\n            >\n              {cancelLabel}\n            </Button>\n          </div>\n        </form>\n\n        <div className={clsx('mt-4 space-y-2.5', { hidden: showAddressForm })}>\n          {formattedAddress}\n          <Button\n            onClick={() => setShowAddressForm(true)}\n            size=\"small\"\n            type=\"button\"\n            variant=\"tertiary\"\n          >\n            {editAddressLabel}\n          </Button>\n        </div>\n\n        <form\n          className={clsx('space-y-4', {\n            hidden: state.shippingOptions === null || state.shippingOptions.length === 0,\n          })}\n          {...getFormProps(shippingOptionsForm)}\n          action={formAction}\n        >\n          <div className=\"mt-4 space-y-3\">\n            <RadioGroup\n              {...getInputProps(shippingOptionsFields.shippingOption, {\n                type: 'radio',\n                required: true,\n              })}\n              errors={shippingOptionsFields.shippingOption.errors}\n              key={shippingOptionsFields.shippingOption.id}\n              label={shippingOptionsLabel}\n              name=\"shippingOption\"\n              onBlur={shippingOptionsControl.blur}\n              onFocus={shippingOptionsControl.focus}\n              onValueChange={shippingOptionsControl.change}\n              options={\n                state.shippingOptions?.map((option) => ({\n                  label: option.label,\n                  value: option.value,\n                  description: option.price,\n                })) ?? []\n              }\n              value={shippingOptionsControl.value}\n            />\n\n            {shippingOptionsForm.errors?.map((error, index) => (\n              <FormStatus key={index} type=\"error\">\n                {error}\n              </FormStatus>\n            ))}\n          </div>\n\n          <div className=\"flex gap-1.5\">\n            <SubmitButton className=\"grow\" name=\"intent\" value=\"add-shipping\">\n              {shippingOption ? updateShippingLabel : addShippingLabel}\n            </SubmitButton>\n            <Button\n              className=\"shrink-0\"\n              onClick={() => {\n                setShowForms(false);\n                setShowAddressForm(false);\n              }}\n              size=\"small\"\n              type=\"button\"\n              variant=\"tertiary\"\n            >\n              {cancelLabel}\n            </Button>\n          </div>\n        </form>\n\n        <div\n          className={clsx('mt-6 space-y-3', {\n            hidden: state.shippingOptions === null || state.shippingOptions.length > 0,\n          })}\n        >\n          <Label>{shippingOptionsLabel}</Label>\n          <p>{noShippingOptionsLabel}</p>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction SubmitButton(props: React.ComponentPropsWithoutRef<typeof Button>) {\n  const { pending } = useFormStatus();\n\n  return <Button {...props} loading={pending} size=\"small\" type=\"submit\" variant=\"secondary\" />;\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/compare-section/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { ArrowLeft, ArrowRight } from 'lucide-react';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport {\n  Carousel,\n  CarouselButtons,\n  CarouselContent,\n  CarouselItem,\n} from '@/vibes/soul/primitives/carousel';\nimport {\n  CompareCard,\n  CompareCardSkeleton,\n  type CompareProduct,\n} from '@/vibes/soul/primitives/compare-card';\nimport { CompareAddToCartAction } from '@/vibes/soul/primitives/compare-card/add-to-cart-form';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\n\ninterface CompareSectionProps {\n  className?: string;\n  title?: string;\n  products: Streamable<CompareProduct[]>;\n  emptyStateTitle?: Streamable<string>;\n  emptyStateSubtitle?: Streamable<string>;\n  addToCartLabel?: string;\n  previousLabel?: string;\n  nextLabel?: string;\n  descriptionLabel?: string;\n  noDescriptionLabel?: string;\n  ratingLabel?: string;\n  noRatingsLabel?: string;\n  otherDetailsLabel?: string;\n  noOtherDetailsLabel?: string;\n  viewOptionsLabel?: string;\n  preorderLabel?: string;\n  placeholderCount?: number;\n  addToCartAction?: CompareAddToCartAction;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --compare-section-title-font-family: var(--font-family-heading);\n *   --compare-section-title: hsl(var(--foreground));\n *   --compare-section-count: hsl(var(--contrast-300));\n *   --compare-section-empty-font-family: var(--font-family-body);\n *   --compare-section-empty-title-font-family: var(--font-family-heading);\n *   --compare-section-empty-title: hsl(var(--foreground));\n *   --compare-section-empty-subtitle: hsl(var(--contrast-500));\n * }\n * ```\n */\nexport function CompareSection({\n  className = '',\n  title = 'Compare products',\n  products: streamableProducts,\n  addToCartAction,\n  addToCartLabel,\n  emptyStateTitle = 'No products to compare',\n  emptyStateSubtitle = 'Browse our catalog to find products.',\n  previousLabel,\n  nextLabel,\n  descriptionLabel,\n  noDescriptionLabel,\n  ratingLabel,\n  noRatingsLabel,\n  otherDetailsLabel,\n  noOtherDetailsLabel,\n  viewOptionsLabel,\n  preorderLabel,\n  placeholderCount,\n}: CompareSectionProps) {\n  return (\n    <Stream\n      fallback={\n        <CompareSectionSkeleton className={className} placeholderCount={placeholderCount} />\n      }\n      value={streamableProducts}\n    >\n      {(products) => {\n        if (products.length === 0) {\n          return (\n            <CompareSectionEmptyState\n              className={className}\n              emptyStateSubtitle={emptyStateSubtitle}\n              emptyStateTitle={emptyStateTitle}\n              placeholderCount={placeholderCount}\n            />\n          );\n        }\n\n        return (\n          <div className={clsx('overflow-hidden @container', className)}>\n            <div className=\"mx-auto w-full max-w-screen-2xl px-4 py-10 @xl:px-6 @xl:py-14 @4xl:px-8 @4xl:py-20\">\n              <Carousel>\n                <div className=\"mb-8 flex w-full items-end justify-between gap-10 @xl:mb-10\">\n                  <h1 className=\"font-[family-name:var(--compare-section-title-font-family,var(--font-family-heading))] text-2xl leading-none text-[var(--compare-section-title,hsl(var(--foreground)))] @xl:text-3xl @4xl:text-4xl\">\n                    {title}{' '}\n                    <span className=\"text-[var(--compare-section-count,hsl(var(--contrast-300)))]\">\n                      {products.length}\n                    </span>\n                  </h1>\n                  <CarouselButtons\n                    className=\"hidden @md:flex\"\n                    nextLabel={nextLabel}\n                    previousLabel={previousLabel}\n                  />\n                </div>\n                <CarouselContent>\n                  {products.map((product) => (\n                    <CarouselItem\n                      className=\"basis-full @sm:basis-1/2 @md:basis-1/3 @4xl:basis-1/4\"\n                      key={product.id}\n                    >\n                      <CompareCard\n                        addToCartAction={addToCartAction}\n                        addToCartLabel={addToCartLabel}\n                        descriptionLabel={descriptionLabel}\n                        imageSizes=\"(min-width: 42rem) 25vw, (min-width: 32rem) 33vw, (min-width: 28rem) 50vw, 100vw\"\n                        key={product.id}\n                        noDescriptionLabel={noDescriptionLabel}\n                        noOtherDetailsLabel={noOtherDetailsLabel}\n                        noRatingsLabel={noRatingsLabel}\n                        otherDetailsLabel={otherDetailsLabel}\n                        preorderLabel={preorderLabel}\n                        product={product}\n                        ratingLabel={ratingLabel}\n                        viewOptionsLabel={viewOptionsLabel}\n                      />\n                    </CarouselItem>\n                  ))}\n                </CarouselContent>\n              </Carousel>\n            </div>\n          </div>\n        );\n      }}\n    </Stream>\n  );\n}\n\nexport function CompareSectionSkeleton({\n  className,\n  title = 'Compare products',\n  placeholderCount = 4,\n}: Pick<CompareSectionProps, 'className' | 'title' | 'placeholderCount'>) {\n  return (\n    <Skeleton.Root className={clsx('group/compare-section', className)} hideOverflow>\n      <div className=\"mx-auto w-full max-w-screen-2xl px-4 py-10 @xl:px-6 @xl:py-14 @4xl:px-8 @4xl:py-20\">\n        <div className=\"relative @container\">\n          <div className=\"mb-8 flex w-full items-end justify-between gap-10 @xl:mb-10\">\n            <h1 className=\"font-[family-name:var(--compare-section-title-font-family,var(--font-family-heading))] text-2xl leading-none text-[var(--compare-section-title,hsl(var(--foreground)))] @xl:text-3xl @4xl:text-4xl\">\n              {title}\n            </h1>\n            <div className=\"group-has-[[data-pending]]/compare-section:animate-pulse\" data-pending>\n              <div className=\"flex gap-2\">\n                <Skeleton.Icon\n                  icon={<ArrowLeft aria-hidden className=\"h-6 w-6\" strokeWidth={1.5} />}\n                />\n                <Skeleton.Icon\n                  icon={<ArrowRight aria-hidden className=\"h-6 w-6\" strokeWidth={1.5} />}\n                />\n              </div>\n            </div>\n          </div>\n          <div\n            className=\"w-full group-has-[[data-pending]]/compare-section:animate-pulse\"\n            data-pending\n          >\n            <div className=\"-ml-4 flex @2xl:-ml-5\">\n              {Array.from({ length: placeholderCount }).map((_, index) => (\n                <div\n                  className=\"min-w-0 shrink-0 grow-0 basis-full pl-4 @md:basis-1/2 @lg:basis-1/3 @2xl:basis-1/4 @2xl:pl-5\"\n                  key={index}\n                  role=\"group\"\n                >\n                  <CompareCardSkeleton />\n                </div>\n              ))}\n            </div>\n          </div>\n        </div>\n      </div>\n    </Skeleton.Root>\n  );\n}\n\nexport function CompareSectionEmptyState({\n  className,\n  emptyStateTitle,\n  emptyStateSubtitle,\n  placeholderCount = 4,\n}: Pick<\n  CompareSectionProps,\n  'className' | 'title' | 'emptyStateTitle' | 'emptyStateSubtitle' | 'placeholderCount'\n>) {\n  return (\n    <div className={clsx('overflow-hidden @container', className)}>\n      <div className=\"mx-auto w-full max-w-screen-2xl px-4 py-10 @xl:px-6 @xl:py-14 @4xl:px-8 @4xl:py-20\">\n        <div className=\"@container\">\n          <div className=\"relative w-full\">\n            <div className=\"-ml-4 flex [mask-image:linear-gradient(to_bottom,_black_0%,_transparent_90%)] @2xl:-ml-5\">\n              {Array.from({ length: placeholderCount }).map((_, index) => (\n                <div\n                  className=\"min-w-0 shrink-0 grow-0 basis-full pl-4 @md:basis-1/2 @lg:basis-1/3 @2xl:basis-1/4 @2xl:pl-5\"\n                  key={index}\n                  role=\"group\"\n                >\n                  <CompareCardSkeleton />\n                </div>\n              ))}\n            </div>\n            <div className=\"absolute inset-0 mx-auto px-3 py-16 pb-3 @4xl:px-10 @4xl:pb-10 @4xl:pt-28\">\n              <header className=\"mx-auto max-w-xl space-y-2 text-center font-[family-name:var(--compare-section-empty-font-family,var(--font-family-body))] @4xl:space-y-3\">\n                <h3 className=\"font-[family-name:var(--compare-section-empty-title-font-family,var(--font-family-heading))] text-2xl leading-tight text-[var(--compare-section-empty-title,hsl(var(--foreground)))] @4xl:text-4xl @4xl:leading-none\">\n                  {emptyStateTitle}\n                </h3>\n                <p className=\"text-sm text-[var(--compare-section-empty-subtitle,hsl(var(--contrast-500)))] @4xl:text-lg\">\n                  {emptyStateSubtitle}\n                </p>\n              </header>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/dynamic-form-section/index.tsx",
    "content": "import { clsx } from 'clsx';\n\nimport { DynamicForm, DynamicFormAction } from '@/vibes/soul/form/dynamic-form';\nimport {\n  Field,\n  FieldGroup,\n  FormErrorTranslationMap,\n  PasswordComplexitySettings,\n} from '@/vibes/soul/form/dynamic-form/schema';\nimport { SectionLayout } from '@/vibes/soul/sections/section-layout';\n\ninterface Props<F extends Field> {\n  title?: string;\n  subtitle?: string;\n  action: DynamicFormAction<F>;\n  fields: Array<F | FieldGroup<F>>;\n  submitLabel?: string;\n  className?: string;\n  passwordComplexity?: PasswordComplexitySettings | null;\n  errorTranslations?: FormErrorTranslationMap;\n  recaptchaSiteKey?: string;\n}\n\nexport function DynamicFormSection<F extends Field>({\n  className,\n  title,\n  subtitle,\n  fields,\n  submitLabel,\n  action,\n  passwordComplexity,\n  errorTranslations,\n  recaptchaSiteKey,\n}: Props<F>) {\n  return (\n    <SectionLayout className={clsx('mx-auto w-full max-w-4xl', className)} containerSize=\"lg\">\n      {title != null && title !== '' && (\n        <header className=\"pb-8 @2xl:pb-12 @4xl:pb-16\">\n          <h1 className=\"mb-5 font-heading text-4xl font-medium leading-none @xl:text-5xl\">\n            {title}\n          </h1>\n          {subtitle != null && subtitle !== '' && (\n            <p className=\"mb-10 text-base font-light leading-none @xl:text-lg\">{subtitle}</p>\n          )}\n        </header>\n      )}\n      <DynamicForm\n        action={action}\n        errorTranslations={errorTranslations}\n        fields={fields}\n        passwordComplexity={passwordComplexity}\n        recaptchaSiteKey={recaptchaSiteKey}\n        submitLabel={submitLabel}\n      />\n    </SectionLayout>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/error/index.tsx",
    "content": "import { Button } from '@/vibes/soul/primitives/button';\n\ninterface Props {\n  title: string;\n  subtitle: string;\n  ctaLabel?: string;\n  ctaAction?: () => void | Promise<void>;\n}\n\nexport function Error({\n  title = 'Something went wrong!',\n  subtitle = 'Please try again or contact our support team for assistance.',\n  ctaLabel = 'Try again',\n  ctaAction,\n}: Props) {\n  return (\n    <section className=\"@container\">\n      <div className=\"mx-auto max-w-3xl px-4 py-10 @xl:px-6 @xl:py-14 @4xl:px-8 @4xl:py-20\">\n        <h1 className=\"mb-3 font-heading text-3xl font-medium leading-none @xl:text-4xl @4xl:text-5xl\">\n          {title}\n        </h1>\n        <p className=\"text-lg text-contrast-500\">{subtitle}</p>\n\n        {ctaAction && (\n          <form action={ctaAction}>\n            <Button className=\"mt-8\" size=\"large\" type=\"submit\" variant=\"primary\">\n              {ctaLabel}\n            </Button>\n          </form>\n        )}\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/featured-blog-post-list/index.tsx",
    "content": "import { Streamable } from '@/vibes/soul/lib/streamable';\nimport { BlogPostCardBlogPost } from '@/vibes/soul/primitives/blog-post-card';\nimport { CursorPagination, CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination';\nimport { BlogPostList } from '@/vibes/soul/sections/blog-post-list';\nimport { Breadcrumb, Breadcrumbs } from '@/vibes/soul/sections/breadcrumbs';\nimport { SectionLayout } from '@/vibes/soul/sections/section-layout';\n\ninterface Props {\n  title: string;\n  description?: string;\n  posts: Streamable<BlogPostCardBlogPost[]>;\n  paginationInfo?: Streamable<CursorPaginationInfo>;\n  breadcrumbs?: Streamable<Breadcrumb[]>;\n  emptyStateSubtitle?: Streamable<string | null>;\n  emptyStateTitle?: Streamable<string | null>;\n  placeholderCount?: number;\n}\n\nexport function FeaturedBlogPostList({\n  title,\n  description,\n  posts,\n  paginationInfo,\n  breadcrumbs,\n  emptyStateSubtitle,\n  emptyStateTitle,\n  placeholderCount,\n}: Props) {\n  return (\n    <SectionLayout>\n      {breadcrumbs && <Breadcrumbs breadcrumbs={breadcrumbs} />}\n\n      <div className=\"pt-6\">\n        <h1 className=\"mb-3 font-heading text-4xl font-medium leading-none text-foreground @xl:text-5xl @4xl:text-6xl\">\n          {title}\n        </h1>\n\n        {description != null && description !== '' && (\n          <p className=\"max-w-lg text-lg text-contrast-500\">{description}</p>\n        )}\n\n        <BlogPostList\n          className=\"mb-8 mt-8 @4xl:mb-10 @4xl:mt-10\"\n          emptyStateSubtitle={emptyStateSubtitle}\n          emptyStateTitle={emptyStateTitle}\n          placeholderCount={placeholderCount}\n          posts={posts}\n        />\n\n        {paginationInfo && <CursorPagination info={paginationInfo} />}\n      </div>\n    </SectionLayout>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/featured-product-carousel/index.tsx",
    "content": "import { Streamable } from '@/vibes/soul/lib/streamable';\nimport { AnimatedUnderline } from '@/vibes/soul/primitives/animated-underline';\nimport { CarouselProduct, ProductCarousel } from '@/vibes/soul/sections/product-carousel';\nimport { SectionLayout } from '@/vibes/soul/sections/section-layout';\nimport { Link } from '~/components/link';\n\ninterface Link {\n  label: string;\n  href: string;\n}\n\nexport interface FeaturedProductCarouselProps {\n  title: string;\n  description?: string;\n  cta?: Link;\n  products: Streamable<CarouselProduct[]>;\n  emptyStateTitle?: Streamable<string>;\n  emptyStateSubtitle?: Streamable<string>;\n  placeholderCount?: number;\n  scrollbarLabel?: string;\n  previousLabel?: string;\n  nextLabel?: string;\n  hideOverflow?: boolean;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --featured-product-carousel-font-family: var(--font-family-body);\n *   --featured-product-carousel-title-font-family: var(--font-family-heading);\n *   --featured-product-carousel-title: hsl(var(--foreground));\n *   --featured-product-carousel-description: hsl(var(--contrast-500));\n * }\n * ```\n */\nexport function FeaturedProductCarousel({\n  title,\n  description,\n  cta,\n  products,\n  emptyStateTitle,\n  emptyStateSubtitle,\n  placeholderCount,\n  scrollbarLabel,\n  previousLabel,\n  nextLabel,\n  hideOverflow = false,\n}: FeaturedProductCarouselProps) {\n  return (\n    <SectionLayout containerSize=\"2xl\">\n      <div className=\"mb-6 flex w-full flex-row flex-wrap items-end justify-between gap-x-8 gap-y-6 @4xl:mb-8\">\n        <header className=\"font-[family-name:var(--featured-product-carousel-font-family,var(--font-family-body))]\">\n          <h2 className=\"font-[family-name:var(--featured-product-carousel-title-font-family,var(--font-family-heading))] text-2xl leading-none text-[var(--featured-product-carousel-title,hsl(var(--foreground)))] @xl:text-3xl @4xl:text-4xl\">\n            {title}\n          </h2>\n          {description != null && description !== '' && (\n            <p className=\"mt-3 max-w-xl leading-relaxed text-[var(--featured-product-carousel-description,hsl(var(--contrast-500)))]\">\n              {description}\n            </p>\n          )}\n        </header>\n        {cta != null && cta.href !== '' && cta.label !== '' && (\n          <Link className=\"group/underline focus:outline-none\" href={cta.href}>\n            <AnimatedUnderline className=\"mr-3\">{cta.label}</AnimatedUnderline>\n          </Link>\n        )}\n      </div>\n      <div className=\"group/product-carousel\">\n        <ProductCarousel\n          emptyStateSubtitle={emptyStateSubtitle}\n          emptyStateTitle={emptyStateTitle}\n          hideOverflow={hideOverflow}\n          nextLabel={nextLabel}\n          placeholderCount={placeholderCount}\n          previousLabel={previousLabel}\n          products={products}\n          scrollbarLabel={scrollbarLabel}\n        />\n      </div>\n    </SectionLayout>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/featured-product-list/index.tsx",
    "content": "import { Streamable } from '@/vibes/soul/lib/streamable';\nimport { ButtonLink } from '@/vibes/soul/primitives/button-link';\nimport { Product } from '@/vibes/soul/primitives/product-card';\nimport { ProductList } from '@/vibes/soul/sections/product-list';\nimport { StickySidebarLayout } from '@/vibes/soul/sections/sticky-sidebar-layout';\n\ninterface Link {\n  label: string;\n  href: string;\n}\n\nexport interface FeaturedProductsListProps {\n  title: string;\n  description?: string;\n  cta?: Link;\n  products: Streamable<Product[]>;\n  emptyStateTitle?: Streamable<string>;\n  emptyStateSubtitle?: Streamable<string>;\n  placeholderCount?: number;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --featured-product-list-font-family: var(--font-family-body);\n *   --featured-product-list-title-font-family: var(--font-family-heading);\n *   --featured-product-list-title: hsl(var(--foreground));\n *   --featured-product-list-description: hsl(var(--contrast-500));\n * }\n * ```\n */\nexport function FeaturedProductList({\n  title,\n  description,\n  cta,\n  products,\n  emptyStateTitle,\n  emptyStateSubtitle,\n  placeholderCount,\n}: FeaturedProductsListProps) {\n  return (\n    <StickySidebarLayout\n      sidebar={\n        <header className=\"font-[family-name:var(--featured-product-list-font-family,var(--font-family-body))]\">\n          <h2 className=\"mb-3 font-[family-name:var(--featured-product-list-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none text-[var(--featured-product-list-title,hsl(var(--foreground)))] @4xl:text-5xl\">\n            {title}\n          </h2>\n          {description != null && description !== '' && (\n            <p className=\"mb-8 max-w-xl text-lg leading-normal text-[var(--featured-product-list-description,hsl(var(--contrast-500)))]\">\n              {description}\n            </p>\n          )}\n\n          {cta?.href != null && cta.href !== '' && cta.label !== '' && (\n            <ButtonLink href={cta.href} variant=\"secondary\">\n              {cta.label}\n            </ButtonLink>\n          )}\n        </header>\n      }\n      sidebarSize=\"1/3\"\n    >\n      <div className=\"group/product-list flex-1\">\n        <ProductList\n          emptyStateSubtitle={emptyStateSubtitle}\n          emptyStateTitle={emptyStateTitle}\n          placeholderCount={placeholderCount}\n          products={products}\n          showCompare={false}\n        />\n      </div>\n    </StickySidebarLayout>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/footer/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { ReactNode } from 'react';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Logo } from '@/vibes/soul/primitives/logo';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\nimport { Link } from '~/components/link';\n\ninterface Image {\n  src: string;\n  alt: string;\n}\n\ninterface Link {\n  href: string;\n  label: string;\n}\n\nexport interface Section {\n  title?: string;\n  links: Link[];\n}\n\ninterface SocialMediaLink {\n  href: string;\n  icon: ReactNode;\n}\n\ninterface ContactInformation {\n  address?: string;\n  phone?: string;\n}\n\nexport interface FooterProps {\n  logo: Streamable<string | Image>;\n  sections: Streamable<Section[]>;\n  copyright?: Streamable<string>;\n  contactInformation?: Streamable<ContactInformation>;\n  paymentIcons?: Streamable<ReactNode[]>;\n  socialMediaLinks?: Streamable<SocialMediaLink[]>;\n  contactTitle?: string;\n  className?: string;\n  logoHref?: string;\n  logoLabel?: string;\n  logoWidth?: number;\n  logoHeight?: number;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --footer-focus: hsl(var(--primary));\n *   --footer-background: hsl(var(--background));\n *   --footer-border-top: hsl(var(--contrast-100));\n *   --footer-border-bottom: hsl(var(--primary));\n *   --footer-contact-title: hsl(var(--contrast-500));\n *   --footer-contact-text: hsl(var(--foreground));\n *   --footer-social-icon: hsl(var(--contrast-400));\n *   --footer-social-icon-hover: hsl(var(--foreground));\n *   --footer-section-title: hsl(var(--foreground));\n *   --footer-link: hsl(var(--contrast-500));\n *   --footer-link-hover: hsl(var(--foreground));\n *   --footer-copyright: hsl(var(--contrast-500));\n * }\n * ```\n */\nexport const Footer = ({\n  logo,\n  sections: streamableSections,\n  contactTitle = 'Contact Us',\n  contactInformation: streamableContactInformation,\n  paymentIcons: streamablePaymentIcons,\n  socialMediaLinks: streamableSocialMediaLinks,\n  copyright: streamableCopyright,\n  className,\n  logoHref = '#',\n  logoLabel = 'Home',\n  logoWidth = 200,\n  logoHeight = 40,\n}: FooterProps) => {\n  return (\n    <footer\n      className={clsx(\n        'group/footer border-b-4 border-t border-b-[var(--footer-border-bottom,hsl(var(--primary)))] border-t-[var(--footer-border-top,hsl(var(--contrast-100)))] bg-[var(--footer-background,hsl(var(--background)))] @container',\n        className,\n      )}\n    >\n      <div className=\"mx-auto max-w-screen-2xl px-4 py-6 @xl:px-6 @xl:py-10 @4xl:px-8 @4xl:py-12\">\n        <div className=\"flex flex-col justify-between gap-x-16 gap-y-12 @3xl:flex-row\">\n          <div className=\"flex flex-col gap-4 @3xl:w-1/3 @3xl:gap-6\">\n            {/* Logo Information */}\n            <div className=\"flex items-center justify-start self-stretch\">\n              <Logo\n                className=\"flex\"\n                height={logoHeight}\n                href={logoHref}\n                label={logoLabel}\n                logo={logo}\n                width={logoWidth}\n              />\n            </div>\n\n            {/* Contact Information */}\n            <Stream fallback={<FooterContactSkeleton />} value={streamableContactInformation}>\n              {(contactInformation) => {\n                if (contactInformation?.address != null || contactInformation?.phone != null) {\n                  return (\n                    <div className=\"mb-4 text-lg font-medium @lg:text-xl\">\n                      <h3 className=\"text-[var(--footer-contact-title,hsl(var(--contrast-500)))]\">\n                        {contactTitle}\n                      </h3>\n                      <div className=\"text-[var(--footer-contact-text,hsl(var(--foreground)))]\">\n                        {contactInformation.address != null &&\n                          contactInformation.address !== '' && <p>{contactInformation.address}</p>}\n                        {contactInformation.phone != null && contactInformation.phone !== '' && (\n                          <p>{contactInformation.phone}</p>\n                        )}\n                      </div>\n                    </div>\n                  );\n                }\n              }}\n            </Stream>\n\n            {/* Social Media Links */}\n            <Stream fallback={<SocialMediaLinksSkeleton />} value={streamableSocialMediaLinks}>\n              {(socialMediaLinks) => {\n                if (socialMediaLinks != null) {\n                  return (\n                    <div className=\"flex items-center gap-3\">\n                      {socialMediaLinks.map(({ href, icon }, i) => {\n                        return (\n                          <Link\n                            className=\"flex items-center justify-center rounded-lg fill-[var(--footer-social-icon,hsl(var(--contrast-400)))] p-1 ring-[var(--footer-focus,hsl(var(--primary)))] transition-colors duration-300 ease-out hover:fill-[var(--footer-social-icon-hover,hsl(var(--foreground)))] focus-visible:outline-0 focus-visible:ring-2\"\n                            href={href}\n                            key={i}\n                          >\n                            {icon}\n                          </Link>\n                        );\n                      })}\n                    </div>\n                  );\n                }\n              }}\n            </Stream>\n          </div>\n\n          {/* Footer Columns of Links */}\n          <Stream fallback={<FooterColumnsSkeleton />} value={streamableSections}>\n            {(sections) => {\n              if (sections.length > 0) {\n                return (\n                  <div\n                    className={clsx(\n                      'grid max-w-5xl grid-cols-1 gap-y-8 @sm:grid-cols-2 @xl:gap-y-10 @2xl:grid-cols-3 @6xl:[grid-template-columns:_repeat(auto-fill,_minmax(220px,_1fr))]',\n                    )}\n                  >\n                    {sections.map(({ title, links }, i) => (\n                      <div className=\"pr-8\" key={i}>\n                        {title != null && (\n                          <span className=\"mb-3 block font-semibold text-[var(--footer-section-title,hsl(var(--foreground)))]\">\n                            {title}\n                          </span>\n                        )}\n\n                        <ul>\n                          {links.map((link, idx) => {\n                            return (\n                              <li key={idx}>\n                                <Link\n                                  className=\"block rounded-lg py-2 text-sm font-medium text-[var(--footer-link,hsl(var(--contrast-500)))] ring-[var(--footer-focus,hsl(var(--primary)))] transition-colors duration-300 hover:text-[var(--footer-link-hover,hsl(var(--foreground)))] focus-visible:outline-0 focus-visible:ring-2\"\n                                  href={link.href}\n                                >\n                                  {link.label}\n                                </Link>\n                              </li>\n                            );\n                          })}\n                        </ul>\n                      </div>\n                    ))}\n                  </div>\n                );\n              }\n            }}\n          </Stream>\n        </div>\n\n        <div className=\"flex flex-col-reverse items-start gap-y-8 pt-16 @3xl:flex-row @3xl:items-center @3xl:pt-20\">\n          {/* Copyright */}\n          <Stream fallback={<CopyrightSkeleton />} value={streamableCopyright}>\n            {(copyright) => {\n              if (copyright != null) {\n                return (\n                  <p className=\"flex-1 text-sm text-[var(--footer-copyright,hsl(var(--contrast-500)))]\">\n                    {copyright}\n                  </p>\n                );\n              }\n            }}\n          </Stream>\n\n          {/* Payment Icons */}\n          <Stream fallback={<PaymentIconsSkeleton />} value={streamablePaymentIcons}>\n            {(paymentIcons) => {\n              if (paymentIcons != null) {\n                return <div className=\"flex flex-wrap gap-2\">{paymentIcons}</div>;\n              }\n            }}\n          </Stream>\n        </div>\n      </div>\n    </footer>\n  );\n};\n\nfunction FooterContactSkeleton() {\n  return (\n    <Skeleton.Root\n      className=\"mb-4 text-lg group-has-[[data-pending]]/footer:animate-pulse @lg:text-xl\"\n      pending\n    >\n      <Skeleton.Text characterCount={10} className=\"rounded\" data-pending />\n      <Skeleton.Text characterCount={15} className=\"rounded\" data-pending />\n      <Skeleton.Text characterCount={12} className=\"rounded\" data-pending />\n    </Skeleton.Root>\n  );\n}\n\nfunction SocialMediaLinksSkeleton() {\n  return (\n    <Skeleton.Root className=\"group-has-[[data-pending]]/footer:animate-pulse\" pending>\n      <div className=\"flex items-center gap-3\" data-pending>\n        {Array.from({ length: 4 }).map((_, idx) => (\n          <Skeleton.Box className=\"h-8 w-8 rounded-full\" key={idx} />\n        ))}\n      </div>\n    </Skeleton.Root>\n  );\n}\n\nfunction FooterColumnsSkeleton() {\n  return (\n    <Skeleton.Root\n      className=\"grid max-w-5xl grid-cols-1 gap-y-8 @container-normal group-has-[[data-pending]]/footer:animate-pulse @sm:grid-cols-2 @xl:gap-y-10 @2xl:grid-cols-3 @6xl:[grid-template-columns:_repeat(auto-fill,_minmax(220px,_1fr))]\"\n      pending\n    >\n      {Array.from({ length: 4 }).map((_, idx) => (\n        <div className=\"pr-8\" data-pending key={idx}>\n          <div className=\"mb-3 flex items-center\">\n            <Skeleton.Text characterCount={10} className=\"rounded\" />\n          </div>\n          <FooterColumnSkeleton />\n        </div>\n      ))}\n    </Skeleton.Root>\n  );\n}\n\nfunction FooterColumnSkeleton() {\n  return (\n    <ul>\n      {Array.from({ length: 4 }).map((_, idx) => (\n        <li className=\"py-2 text-sm\" key={idx}>\n          <Skeleton.Text characterCount={10} className=\"rounded\" />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction CopyrightSkeleton() {\n  return (\n    <Skeleton.Root\n      className=\"flex-1 text-sm @container-normal group-has-[[data-pending]]/footer:animate-pulse\"\n      pending\n    >\n      <Skeleton.Text characterCount={40} className=\"rounded\" data-pending />\n    </Skeleton.Root>\n  );\n}\n\nfunction PaymentIconsSkeleton() {\n  return (\n    <Skeleton.Root\n      className=\"flex flex-wrap gap-2 @container-normal group-has-[[data-pending]]/footer:animate-pulse\"\n      pending\n    >\n      {Array.from({ length: 6 }).map((_, idx) => (\n        <Skeleton.Box className=\"h-6 w-[2.1875rem] rounded\" data-pending key={idx} />\n      ))}\n    </Skeleton.Root>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/forgot-password-section/forgot-password-form.tsx",
    "content": "'use client';\n\nimport { getFormProps, getInputProps, SubmissionResult, useForm } from '@conform-to/react';\nimport { getZodConstraint } from '@conform-to/zod';\nimport { useTranslations } from 'next-intl';\nimport { useActionState } from 'react';\nimport { useFormStatus } from 'react-dom';\n\nimport { FormStatus } from '@/vibes/soul/form/form-status';\nimport { Input } from '@/vibes/soul/form/input';\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { parseWithZodTranslatedErrors } from '~/i18n/utils';\n\nimport { forgotPasswordErrorTranslations, schema } from './schema';\n\ntype Action<State, Payload> = (state: Awaited<State>, payload: Payload) => State | Promise<State>;\n\nexport type ForgotPasswordAction = Action<\n  { lastResult: SubmissionResult | null; successMessage?: string },\n  FormData\n>;\n\ninterface Props {\n  action: ForgotPasswordAction;\n  emailLabel?: string;\n  submitLabel?: string;\n}\n\nexport function ForgotPasswordForm({\n  action,\n  emailLabel = 'Email',\n  submitLabel = 'Reset password',\n}: Props) {\n  const t = useTranslations('Auth.Login.ForgotPassword');\n  const errorTranslations = forgotPasswordErrorTranslations(t);\n  const [{ lastResult, successMessage }, formAction] = useActionState(action, { lastResult: null });\n  const [form, fields] = useForm({\n    lastResult,\n    constraint: getZodConstraint(schema),\n    shouldValidate: 'onBlur',\n    shouldRevalidate: 'onInput',\n    onValidate({ formData }) {\n      return parseWithZodTranslatedErrors(formData, { schema, errorTranslations });\n    },\n  });\n\n  return (\n    <form {...getFormProps(form)} action={formAction} className=\"flex grow flex-col gap-5\">\n      <Input\n        {...getInputProps(fields.email, { type: 'text' })}\n        errors={fields.email.errors}\n        key={fields.email.id}\n        label={emailLabel}\n      />\n      <SubmitButton>{submitLabel}</SubmitButton>\n      {form.errors?.map((error, index) => (\n        <FormStatus key={index} type=\"error\">\n          {error}\n        </FormStatus>\n      ))}\n      {form.status === 'success' && successMessage != null && (\n        <FormStatus>{successMessage}</FormStatus>\n      )}\n    </form>\n  );\n}\n\nfunction SubmitButton({ children }: { children: React.ReactNode }) {\n  const { pending } = useFormStatus();\n\n  return (\n    <Button className=\"mt-auto w-full\" loading={pending} type=\"submit\" variant=\"secondary\">\n      {children}\n    </Button>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/forgot-password-section/index.tsx",
    "content": "import { ForgotPasswordAction, ForgotPasswordForm } from './forgot-password-form';\n\ninterface Props {\n  title?: string;\n  subtitle?: string;\n  action: ForgotPasswordAction;\n  emailLabel?: string;\n  submitLabel?: string;\n}\n\nexport function ForgotPasswordSection({\n  title = 'Forgot your password?',\n  subtitle = 'Enter the email associated with your account below. We’ll send you instructions to reset your password.',\n  emailLabel,\n  submitLabel,\n  action,\n}: Props) {\n  return (\n    <div className=\"@container\">\n      <div className=\"flex flex-col justify-center gap-y-24 px-3 py-10 @xl:flex-row @xl:px-6 @4xl:py-20 @5xl:px-20\">\n        <div className=\"flex w-full flex-col @xl:max-w-md @xl:pr-10 @4xl:pr-20\">\n          <h1 className=\"mb-5 text-4xl font-medium leading-none @xl:text-5xl\">{title}</h1>\n          <p className=\"mb-10 text-base font-light leading-none @xl:text-lg\">{subtitle}</p>\n          <ForgotPasswordForm action={action} emailLabel={emailLabel} submitLabel={submitLabel} />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/forgot-password-section/schema.ts",
    "content": "import { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { FormErrorTranslationMap } from '@/vibes/soul/form/dynamic-form/schema';\nimport { ExistingResultType } from '~/client/util';\n\nexport const forgotPasswordErrorTranslations = (\n  t: ExistingResultType<typeof getTranslations<'Auth.Login.ForgotPassword'>>,\n): FormErrorTranslationMap => ({\n  email: {\n    invalid_type: t('FieldErrors.emailRequired'),\n    invalid_string: t('FieldErrors.emailInvalid'),\n  },\n});\n\nexport const schema = z.object({\n  email: z.string().email().trim(),\n});\n"
  },
  {
    "path": "core/vibes/soul/sections/gift-certificate-balance-section/index.tsx",
    "content": "'use client';\n\nimport { FormMetadata, getFormProps, SubmissionResult, useForm } from '@conform-to/react';\nimport React, { useActionState, useRef } from 'react';\nimport { useFormStatus } from 'react-dom';\n\nimport { FormStatus } from '@/vibes/soul/form/form-status';\nimport { Input } from '@/vibes/soul/form/input';\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { GiftCertificateCard } from '@/vibes/soul/primitives/gift-certificate-card';\nimport { Breadcrumb, Breadcrumbs } from '@/vibes/soul/sections/breadcrumbs';\nimport { SectionLayout } from '@/vibes/soul/sections/section-layout';\n\ntype Action<S, P> = (state: Awaited<S>, payload: P) => S | Promise<S>;\n\ninterface State {\n  lastResult: SubmissionResult | null;\n  data: GiftCertificateData | null;\n  errorMessage?: string;\n}\n\ntype GetGiftCertificateByCodeAction = Action<State, FormData>;\n\ninterface Props {\n  action: GetGiftCertificateByCodeAction;\n  title?: string;\n  description?: string;\n  breadcrumbs?: Streamable<Breadcrumb[]>;\n  logo: string | { src: string; alt: string };\n  checkBalanceLabel?: string;\n  inputLabel?: string;\n  inputPlaceholder?: string;\n  expiresAtLabel?: string;\n  purchasedDateLabel?: string;\n  senderLabel?: string;\n}\n\nexport type GiftCertificateStatus = 'ACTIVE' | 'EXPIRED' | 'PENDING' | 'DISABLED';\n\nexport interface GiftCertificateData {\n  code: string;\n  currencyCode: string;\n  amount: string;\n  balance: string;\n  purchasedAt: string;\n  expiresAt?: string | null;\n  senderName: string | null;\n  recipientName: string | null;\n  status: GiftCertificateStatus;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --gift-certificate-title-font-family: var(--font-family-heading);\n *   --gift-certificate-title: hsl(var(--foreground));\n * }\n * ```\n */\nexport function GiftCertificateCheckBalanceSection({\n  action,\n  title = 'Check balance',\n  description = 'You can check the balance and get the information about your gift certificate by typing the code in the box below.',\n  breadcrumbs,\n  logo,\n  expiresAtLabel,\n  inputLabel = 'Code',\n  inputPlaceholder = 'Enter code',\n  checkBalanceLabel = 'Check balance',\n  purchasedDateLabel = 'Purchased at',\n  senderLabel = 'Sender',\n}: Props) {\n  const defaultValue = useRef<string>('');\n  const [{ lastResult, data, errorMessage }, formAction, isPending] = useActionState(action, {\n    lastResult: null,\n    data: null,\n  });\n\n  const [form] = useForm({\n    lastResult,\n    defaultValue: {\n      code: defaultValue.current,\n    },\n  });\n\n  const DetailsSection = () => (\n    <div className=\"flex flex-1 flex-col items-center justify-center space-y-4\">\n      <GiftCertificateCard\n        balance={data?.balance}\n        expiresAt={data?.expiresAt}\n        expiresAtLabel={expiresAtLabel}\n        loading={isPending}\n        logo={logo}\n        status={data?.status}\n      />\n      <dl className=\"flex w-full flex-row\">\n        {!isPending && (\n          <>\n            {data?.senderName != null && (\n              <div className=\"flex flex-1 flex-col\">\n                <dt className=\"font-mono text-xs uppercase\">{senderLabel}</dt>\n                <dd className=\"text-sm font-bold\">{data.senderName}</dd>\n              </div>\n            )}\n            {data?.purchasedAt != null && (\n              <div className=\"flex flex-1 flex-col\">\n                <dt className=\"font-mono text-xs uppercase\">{purchasedDateLabel}</dt>\n                <dd className=\"text-sm font-bold\">{data.purchasedAt}</dd>\n              </div>\n            )}\n          </>\n        )}\n      </dl>\n    </div>\n  );\n\n  return (\n    <SectionLayout containerSize=\"xl\">\n      {breadcrumbs && <Breadcrumbs breadcrumbs={breadcrumbs} />}\n\n      <div className=\"flex flex-col justify-center gap-x-4 py-10 @md:gap-x-6 @lg:gap-x-8 @xl:flex-row @xl:gap-y-24 @2xl:gap-x-12 @4xl:gap-x-24\">\n        <div className=\"flex w-full flex-1 flex-col space-y-6\">\n          <h1 className=\"font-[family-name:var(--gift-certificate-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none text-[var(--gift-certificate-title,hsl(var(--foreground)))] @xl:text-5xl\">\n            {title}\n          </h1>\n          <div className=\"text-contrast-500\">\n            <p>{description}</p>\n          </div>\n          <div className=\"flex flex-1 @container @xl:hidden\">\n            <DetailsSection />\n          </div>\n          <div className=\"flex flex-col @xl:space-y-4\">\n            {errorMessage != null && <FormStatus type=\"error\">{errorMessage}</FormStatus>}\n            <CheckBalanceForm\n              action={formAction}\n              checkBalanceLabel={checkBalanceLabel}\n              defaultValueRef={defaultValue}\n              form={form}\n              inputLabel={inputLabel}\n              inputPlaceholder={inputPlaceholder}\n            />\n          </div>\n        </div>\n\n        <div className=\"hidden flex-1 @container @xl:flex\">\n          <DetailsSection />\n        </div>\n      </div>\n    </SectionLayout>\n  );\n}\n\nfunction CheckBalanceForm({\n  action,\n  form,\n  defaultValueRef,\n  inputLabel,\n  inputPlaceholder,\n  checkBalanceLabel,\n}: {\n  action: (payload: FormData) => void;\n  form: FormMetadata<{ code: string }>;\n  defaultValueRef: React.RefObject<string>;\n  inputLabel: string;\n  inputPlaceholder: string;\n  checkBalanceLabel: string;\n}) {\n  return (\n    <form\n      {...getFormProps(form)}\n      action={action}\n      className=\"flex flex-col items-end space-x-2 space-y-2 @xl:flex-row\"\n    >\n      <Input\n        className=\"flex-1\"\n        label={inputLabel}\n        name=\"code\"\n        onChange={(e) => {\n          defaultValueRef.current = e.target.value;\n        }}\n        placeholder={inputPlaceholder}\n        required\n        style={{ height: '48px' }}\n      />\n      <SubmitButton label={checkBalanceLabel} />\n    </form>\n  );\n}\n\nfunction SubmitButton({ label }: { label: string }) {\n  const { pending } = useFormStatus();\n\n  return (\n    <Button\n      className=\"max-h-12 w-full @xl:w-auto\"\n      loading={pending}\n      size=\"medium\"\n      type=\"submit\"\n      variant=\"secondary\"\n    >\n      {label}\n    </Button>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/gift-certificate-balance-section/schema.ts",
    "content": "import { z } from 'zod';\n\nexport const giftCertificateCodeSchema = ({ required_error }: { required_error: string }) =>\n  z.object({\n    code: z.string({ required_error }).min(1),\n  });\n"
  },
  {
    "path": "core/vibes/soul/sections/gift-certificate-purchase-section/index.tsx",
    "content": "'use client';\n\nimport { SubmissionResult } from '@conform-to/react';\nimport { clsx } from 'clsx';\nimport { useFormatter, useTranslations } from 'next-intl';\nimport { ReactNode, useCallback, useState } from 'react';\n\nimport { DynamicForm, DynamicFormAction } from '@/vibes/soul/form/dynamic-form';\nimport { Field, FieldGroup, FormErrorTranslationMap } from '@/vibes/soul/form/dynamic-form/schema';\nimport { Streamable } from '@/vibes/soul/lib/streamable';\nimport { GiftCertificateCard } from '@/vibes/soul/primitives/gift-certificate-card';\nimport { toast } from '@/vibes/soul/primitives/toaster';\nimport { Breadcrumb, Breadcrumbs } from '@/vibes/soul/sections/breadcrumbs';\nimport { SectionLayout } from '@/vibes/soul/sections/section-layout';\n\ninterface Props {\n  action: DynamicFormAction<Field>;\n  formFields: Array<Field | FieldGroup<Field>>;\n  currencyCode?: string;\n  ctaLabel?: string;\n  title?: string;\n  subtitle?: string;\n  description?: string;\n  breadcrumbs?: Streamable<Breadcrumb[]>;\n  logo: string | { src: string; alt: string };\n  expiresAt?: string;\n  expiresAtLabel?: string;\n  settings: {\n    minCustomAmount?: number;\n    maxCustomAmount?: number;\n  };\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --gift-certificate-purchase-subtitle-font-family: var(--font-family-mono);\n *   --gift-certificate-purchase-title-font-family: var(--font-family-heading);\n *   --gift-certificate-description-text: hsl(var(--contrast-500));\n * }\n * ```\n */\nexport function GiftCertificatePurchaseSection({\n  action,\n  currencyCode,\n  formFields,\n  title = 'Purchase a gift certificate',\n  description = 'Explore our gift certificates, perfect for any occasion. Choose the amount and personalize your message.',\n  subtitle,\n  breadcrumbs,\n  logo,\n  settings,\n  expiresAt,\n  expiresAtLabel,\n  ctaLabel = 'Add to cart',\n}: Props) {\n  const t = useTranslations('GiftCertificates.Purchase');\n  const format = useFormatter();\n  const [formattedAmount, setFormattedAmount] = useState<string | undefined>(undefined);\n  const errorTranslations: FormErrorTranslationMap = {\n    amount: {\n      invalid_type: t('Form.Errors.amountRequired'),\n      invalid_string: t('Form.Errors.amountInvalid'),\n    },\n    senderName: {\n      invalid_type: t('Form.Errors.senderNameRequired'),\n    },\n    senderEmail: {\n      invalid_type: t('Form.Errors.senderEmailRequired'),\n      invalid_string: t('Form.Errors.emailInvalid'),\n    },\n    recipientName: {\n      invalid_type: t('Form.Errors.recipientNameRequired'),\n    },\n    recipientEmail: {\n      invalid_type: t('Form.Errors.recipientEmailRequired'),\n      invalid_string: t('Form.Errors.emailInvalid'),\n    },\n    nonRefundable: {\n      invalid_literal: t('Form.Errors.checkboxRequired'),\n    },\n    expirationConsent: {\n      invalid_literal: t('Form.Errors.checkboxRequired'),\n    },\n  };\n\n  const handleFormChange = (e: React.FormEvent<HTMLFormElement>) => {\n    if (!(e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement)) {\n      return;\n    }\n\n    if (e.target.name !== 'amount' && e.target.name !== 'amount_display') {\n      return;\n    }\n\n    if (\n      e.target.value.trim() === '' ||\n      Number.isNaN(Number(e.target.value)) ||\n      Number(e.target.value) === 0 ||\n      currencyCode == null\n    ) {\n      setFormattedAmount(undefined);\n\n      return;\n    }\n\n    if (e.target instanceof HTMLInputElement) {\n      e.target.value = e.target.value.replace(/[^0-9.]/g, '');\n\n      if (settings.maxCustomAmount && Number(e.target.value) > settings.maxCustomAmount) {\n        e.target.value = String(settings.maxCustomAmount);\n      }\n    }\n\n    const formatted = format.number(Number(e.target.value), {\n      style: 'currency',\n      currency: currencyCode,\n    });\n\n    setFormattedAmount(formatted);\n  };\n\n  const handleSuccess = useCallback((lastResult: SubmissionResult, successMessage: ReactNode) => {\n    toast.success(successMessage);\n  }, []);\n\n  return (\n    <SectionLayout containerSize=\"xl\">\n      {breadcrumbs && <Breadcrumbs breadcrumbs={breadcrumbs} />}\n\n      <div className=\"flex flex-col justify-center gap-x-16 py-6 @xl:flex-row\">\n        <div className=\"flex w-full flex-1 flex-col space-y-6\">\n          <div className=\"hidden flex-1 rounded-xl bg-contrast-100 @container @xl:flex\">\n            <div\n              className={clsx(\n                'flex flex-1 items-center justify-center p-2 @[250px]:p-4 @[300px]:p-8 @[350px]:p-12 @[450px]:p-16',\n              )}\n            >\n              <GiftCertificateCard\n                balance={formattedAmount}\n                expiresAt={expiresAt}\n                expiresAtLabel={expiresAtLabel}\n                logo={logo}\n              />\n            </div>\n          </div>\n        </div>\n\n        <div className=\"flex-1\">\n          <div className=\"mb-6 border-b border-contrast-100 pb-2\">\n            <div className=\"mb-4 @xl:mb-0\">\n              {subtitle != null && (\n                <p className=\"mb-1 font-[family-name:var(--gift-certificate-purchase-subtitle-font-family,var(--font-family-mono))] text-sm uppercase text-contrast-500\">\n                  {subtitle}\n                </p>\n              )}\n              <h1 className=\"font-[family-name:var(--gift-certificate-purchase-title-font-family,var(--font-family-heading))] text-2xl font-medium leading-none @xl:text-3xl @4xl:text-4xl\">\n                {title}\n              </h1>\n            </div>\n            <div className=\"flex w-full flex-1 flex-col space-y-6\">\n              <div className=\"flex flex-1 rounded-xl bg-contrast-100 @container @xl:hidden\">\n                <div\n                  className={clsx(\n                    'flex flex-1 items-center justify-center p-2 @[250px]:p-4 @[300px]:p-8 @[350px]:p-12 @[450px]:p-16',\n                  )}\n                >\n                  <GiftCertificateCard\n                    balance={formattedAmount}\n                    expiresAt={expiresAt}\n                    expiresAtLabel={expiresAtLabel}\n                    logo={logo}\n                  />\n                </div>\n              </div>\n            </div>\n            <p className=\"py-4 text-[var(--gift-certificate-description-text,hsl(var(--contrast-500)))]\">\n              {description}\n            </p>\n          </div>\n          <DynamicForm\n            action={action}\n            errorTranslations={errorTranslations}\n            fields={formFields}\n            key={JSON.stringify(formFields)}\n            onChange={handleFormChange}\n            onSuccess={handleSuccess}\n            submitLabel={ctaLabel}\n          />\n        </div>\n      </div>\n    </SectionLayout>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/gift-certificates-section/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { ArrowRightIcon } from 'lucide-react';\n\nimport { ButtonLink } from '@/vibes/soul/primitives/button-link';\nimport { GiftCertificateCard } from '@/vibes/soul/primitives/gift-certificate-card';\nimport { SectionLayout } from '@/vibes/soul/sections/section-layout';\n\ninterface Props {\n  title?: string;\n  description?: string;\n  logo: string | { src: string; alt: string };\n  checkBalanceLabel?: string;\n  checkBalanceHref: string;\n  exampleBalance?: string;\n  purchaseLabel?: string;\n  purchaseHref: string;\n  variant?: 'left' | 'right';\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --gift-certificate-title-font-family: var(--font-family-heading);\n *   --gift-certificate-title: hsl(var(--foreground));\n * }\n * ```\n */\nexport function GiftCertificatesSection({\n  title = 'Gift certificates',\n  description = 'Give the perfect gift that never goes out of style. Let friends and loved ones choose exactly what they want from our entire collection.',\n  logo,\n  purchaseLabel = 'Shop now',\n  purchaseHref,\n  checkBalanceLabel = 'Check Balance',\n  checkBalanceHref,\n  exampleBalance,\n  variant = 'left',\n}: Props) {\n  return (\n    <SectionLayout containerSize=\"xl\">\n      <div className=\"flex flex-col justify-center gap-x-4 gap-y-6 py-10 @md:gap-x-6 @lg:gap-x-8 @xl:flex-row @xl:gap-y-24 @2xl:gap-x-12 @4xl:gap-x-24\">\n        <div\n          className={clsx(\n            'flex w-full flex-1 flex-col',\n            {\n              left: 'order-1 @xl:order-1',\n              right: 'order-2 @xl:order-2',\n            }[variant],\n          )}\n        >\n          <h1 className=\"mb-10 font-[family-name:var(--gift-certificate-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none text-[var(--gift-certificate-title,hsl(var(--foreground)))] @xl:text-5xl\">\n            {title}\n          </h1>\n          <div className=\"text-contrast-500\">\n            <p>{description}</p>\n          </div>\n          <div className=\"hidden @xl:flex\">\n            <ButtonLink className=\"mt-10\" href={purchaseHref}>\n              <div className=\"flex items-center\">\n                {purchaseLabel}\n                <ArrowRightIcon className=\"ml-2\" size={20} />\n              </div>\n            </ButtonLink>\n            <ButtonLink className=\"ml-4 mt-10\" href={checkBalanceHref} variant=\"ghost\">\n              {checkBalanceLabel}\n            </ButtonLink>\n          </div>\n        </div>\n\n        <div\n          className={clsx(\n            'flex flex-1 rounded-xl bg-contrast-100 @container',\n            {\n              left: 'order-2 @xl:order-2',\n              right: 'order-1 @xl:order-1',\n            }[variant],\n          )}\n        >\n          <div\n            className={clsx(\n              'flex flex-1 items-center justify-center p-2 @[250px]:p-4 @[300px]:p-8 @[350px]:p-12 @[450px]:p-16',\n            )}\n          >\n            <GiftCertificateCard balance={exampleBalance ?? '....'} logo={logo} />\n          </div>\n        </div>\n\n        <div className=\"order-3 flex flex-col space-y-2 @xl:hidden\">\n          <ButtonLink href={purchaseHref}>\n            <div className=\"flex items-center\">\n              {purchaseLabel}\n              <ArrowRightIcon className=\"ml-2\" size={20} />\n            </div>\n          </ButtonLink>\n          <ButtonLink href={checkBalanceHref} variant=\"ghost\">\n            {checkBalanceLabel}\n          </ButtonLink>\n        </div>\n      </div>\n    </SectionLayout>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/header-section/index.tsx",
    "content": "'use client';\n\nimport { forwardRef, useEffect, useState } from 'react';\nimport Headroom from 'react-headroom';\n\nimport { Banner } from '@/vibes/soul/primitives/banner';\nimport { Navigation } from '@/vibes/soul/primitives/navigation';\n\ninterface Props {\n  navigation: React.ComponentPropsWithoutRef<typeof Navigation>;\n  banner?: React.ComponentPropsWithoutRef<typeof Banner>;\n}\n\nexport const HeaderSection = forwardRef<React.ComponentRef<'div'>, Props>(\n  ({ navigation, banner }, ref) => {\n    const [bannerElement, setBannerElement] = useState<HTMLElement | null>(null);\n    const [bannerHeight, setBannerHeight] = useState(0);\n    const [isFloating, setIsFloating] = useState(false);\n\n    useEffect(() => {\n      if (!bannerElement) return;\n\n      const resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {\n        // eslint-disable-next-line no-restricted-syntax\n        for (const entry of entries) {\n          setBannerHeight(entry.contentRect.height);\n        }\n      });\n\n      resizeObserver.observe(bannerElement);\n\n      return () => {\n        resizeObserver.disconnect();\n      };\n    }, [bannerElement]);\n\n    return (\n      <div ref={ref}>\n        {banner && <Banner ref={setBannerElement} {...banner} />}\n        <Headroom\n          onUnfix={() => setIsFloating(false)}\n          onUnpin={() => setIsFloating(true)}\n          pinStart={bannerHeight}\n        >\n          <div className=\"p-2\">\n            <Navigation {...navigation} isFloating={isFloating} />\n          </div>\n        </Headroom>\n      </div>\n    );\n  },\n);\n\nHeaderSection.displayName = 'HeaderSection';\n"
  },
  {
    "path": "core/vibes/soul/sections/maintenance/index.tsx",
    "content": "import { MailIcon, PhoneIcon } from 'lucide-react';\n\nimport { Logo } from '@/vibes/soul/primitives/logo';\nimport { SectionLayout } from '@/vibes/soul/sections/section-layout';\nimport { Link } from '~/components/link';\n\ninterface Image {\n  src: string;\n  alt: string;\n}\n\ninterface Props {\n  className?: string;\n  logo?: string | Image | null;\n  title?: string;\n  statusMessage?: string;\n  contactText?: string;\n  contactPhone?: string;\n  contactEmail?: string;\n}\n\nexport function Maintenance({\n  className = '',\n  logo,\n  title = 'We are down for maintenance',\n  statusMessage = \"Sorry for the inconvenience, we're currently working on improving our store.\",\n  contactText = 'Contact us',\n  contactPhone,\n  contactEmail,\n}: Props) {\n  return (\n    <SectionLayout className={className} containerSize=\"2xl\">\n      <div className=\"mx-auto my-auto max-w-3xl px-4 @xl:px-6 @4xl:px-8\">\n        {Boolean(logo) && (\n          <div className=\"mb-20\">\n            <Logo height={40} href=\"/\" logo={logo} width={200} />\n          </div>\n        )}\n\n        <h1 className=\"mb-3 font-heading text-3xl font-medium leading-none @xl:text-4xl @4xl:text-5xl\">\n          {title}\n        </h1>\n        <p className=\"text-md text-contrast-500 @xl:text-lg @4xl:text-xl\">{statusMessage}</p>\n\n        {(Boolean(contactPhone) || Boolean(contactEmail)) && (\n          <div className=\"mt-20\">\n            <div className=\"mb-6 text-lg font-medium leading-none @xl:text-xl @4xl:text-2xl\">\n              {contactText}\n            </div>\n            {Boolean(contactEmail) && (\n              <div>\n                <Link\n                  className=\"text-md my-2 inline-flex flex-row items-center font-medium text-[var(--footer-link,hsl(var(--contrast-400)))] ring-[var(--footer-focus,hsl(var(--primary)))] transition-colors duration-300 hover:text-[var(--footer-link-hover,hsl(var(--foreground)))] focus-visible:outline-0 focus-visible:ring-2 @xl:text-lg\"\n                  href={`mailto:${contactEmail}`}\n                >\n                  <span>\n                    <MailIcon />\n                  </span>\n                  <span className=\"ml-2 flex-1\">{contactEmail}</span>\n                </Link>\n              </div>\n            )}\n            {Boolean(contactPhone) && (\n              <div>\n                <Link\n                  className=\"text-md my-2 inline-flex flex-row items-center font-medium text-[var(--footer-link,hsl(var(--contrast-400)))] ring-[var(--footer-focus,hsl(var(--primary)))] transition-colors duration-300 hover:text-[var(--footer-link-hover,hsl(var(--foreground)))] focus-visible:outline-0 focus-visible:ring-2 @xl:text-lg\"\n                  href={`tel:${contactPhone}`}\n                >\n                  <span>\n                    <PhoneIcon />\n                  </span>\n                  <span className=\"ml-2 flex-1\">{contactPhone}</span>\n                </Link>\n              </div>\n            )}\n          </div>\n        )}\n      </div>\n    </SectionLayout>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/not-found/index.tsx",
    "content": "'use client';\n\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { SectionLayout } from '@/vibes/soul/sections/section-layout';\nimport { useSearch } from '~/lib/search';\n\nexport interface NotFoundProps {\n  title?: string;\n  subtitle?: string;\n  ctaLabel?: string;\n  className?: string;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --not-found-font-family: var(--font-family-body);\n *   --not-found-title-font-family: var(--font-family-heading);\n *   --not-found-title: hsl(var(--foreground));\n *   --not-found-subtitle: hsl(var(--contrast-500));\n * }\n * ```\n */\nexport function NotFound({\n  title = 'Not found',\n  subtitle = \"Take a look around if you're lost.\",\n  ctaLabel = 'Search',\n  className = '',\n}: NotFoundProps) {\n  const { setIsSearchOpen } = useSearch();\n\n  const handleOpenSearch = () => {\n    setIsSearchOpen(true);\n  };\n\n  return (\n    <SectionLayout className={className} containerSize=\"2xl\">\n      <header className=\"font-[family-name:var(--not-found-font-family,var(--font-family-body))]\">\n        <h1 className=\"mb-3 font-[family-name:var(--not-found-title-font-family,var(--font-family-heading))] text-3xl font-medium leading-none text-[var(--not-found-title,hsl(var(--foreground)))] @xl:text-4xl @4xl:text-5xl\">\n          {title}\n        </h1>\n        <p className=\"mb-4 text-lg text-[var(--not-found-subtitle,hsl(var(--contrast-500)))]\">\n          {subtitle}\n        </p>\n        <Button onClick={handleOpenSearch}>{ctaLabel}</Button>\n      </header>\n    </SectionLayout>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/order-details-section/index.tsx",
    "content": "import { ArrowLeft } from 'lucide-react';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Badge } from '@/vibes/soul/primitives/badge';\nimport { ButtonLink } from '@/vibes/soul/primitives/button-link';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\nimport { Image } from '~/components/image';\nimport { Link } from '~/components/link';\n\ninterface OrderPayment {\n  title: string;\n  subtitle?: string;\n  amount: string;\n}\n\ninterface PaymentsSummary {\n  title: string;\n  payments: OrderPayment[];\n}\n\ninterface Summary {\n  lineItems: Array<{\n    label: string;\n    value: string;\n    subtext?: string;\n  }>;\n  total: string;\n}\n\ninterface Address {\n  name?: string;\n  street1: string;\n  street2?: string;\n  city: string;\n  state: string;\n  zipcode?: string;\n  country?: string;\n}\n\ninterface TrackingWithUrl {\n  url: string;\n}\n\ninterface TrackingWithNumber {\n  number: string;\n}\n\ninterface TrackingWithUrlAndNumber {\n  url: string;\n  number: string;\n}\n\ninterface Shipment {\n  name: string;\n  status: string;\n  tracking?: TrackingWithUrl | TrackingWithNumber | TrackingWithUrlAndNumber;\n}\n\ninterface ShipmentLineItem {\n  id: string;\n  title: string;\n  subtitle?: string;\n  price: string;\n  totalPrice: string;\n  href?: string;\n  image?: { src: string; alt: string };\n  quantity: number;\n  metadata?: Array<{ label: string; value: string }>;\n}\n\ninterface Destination {\n  id: string;\n  title: string;\n  address: Address;\n  shipments: Shipment[];\n  lineItems: ShipmentLineItem[];\n}\n\ninterface EmailDestination {\n  title: string;\n  email: string;\n  lineItems: ShipmentLineItem[];\n}\n\nexport interface Order {\n  id: string;\n  status: string;\n  statusColor?: 'success' | 'warning' | 'error' | 'info';\n  date: string;\n  destinations: Destination[];\n  emailDestinations: EmailDestination[];\n  summary: Summary;\n  paymentsSummary: PaymentsSummary;\n}\n\nexport interface OrderDetailsSectionProps {\n  order: Streamable<Order>;\n  title?: string;\n  orderSummaryLabel?: string;\n  shipmentAddressLabel?: string;\n  shipmentMethodLabel?: string;\n  summaryTotalLabel?: string;\n  prevHref?: string;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --order-details-section-focus: hsl(var(--primary));\n *   --order-details-section-font-family: hsl(var(--font-family-body));\n *   --order-details-section-title-font-family: hsl(var(--font-family-heading));\n *   --order-details-text-primary: hsl(var(--foreground));\n *   --order-details-text-secondary: hsl(var(--contrast-500));\n *   --order-details-section-border: hsl(var(--contrast-100));\n *   --order-details-section-button-border: hsl(var(--contrast-100));\n *   --order-details-section-button-border-hover: hsl(var(--contrast-200));\n *   --order-details-section-button-icon: hsl(var(--foreground));\n *   --order-details-section-button-background: hsl(var(--background));\n *   --order-details-section-button-background-hover: hsl(var(--contrast-100));\n *   --order-details-section-image-background: hsl(var(--contrast-100));\n *   --order-details-section-line-item: hsl(var(--contrast-300))\n *   --order-details-section-line-item-subtitle: hsl(var(--contrast-500))\n *   --order-details-section-line-item-subtext: hsl(var(--contrast-400))\n * }\n * ```\n */\nexport function OrderDetailsSection({\n  order: streamableOrder,\n  title,\n  orderSummaryLabel = 'Order summary',\n  shipmentAddressLabel,\n  shipmentMethodLabel,\n  summaryTotalLabel,\n  prevHref = '/orders',\n}: OrderDetailsSectionProps) {\n  return (\n    <div className=\"font-[family-name:var(--order-details-section-font-family,var(--font-family-body))] text-[var(--order-details-text-primary,hsl(var(--foreground)))] @container\">\n      <Stream\n        fallback={<OrderDetailsSectionSkeleton prevHref={prevHref} />}\n        value={streamableOrder}\n      >\n        {(order) => (\n          <>\n            <div className=\"flex gap-4 border-b border-[var(--order-details-section-border,hsl(var(--contrast-100)))] pb-8\">\n              {prevHref !== '' && (\n                <ButtonLink href={prevHref} shape=\"circle\" size=\"small\" variant=\"ghost\">\n                  <ArrowLeft />\n                </ButtonLink>\n              )}\n              <div className=\"space-y-1\">\n                <div className=\"flex items-center gap-3\">\n                  <h1 className=\"font-[family-name:var(--order-details-section-title-font-family,var(--font-family-heading))] text-4xl\">\n                    {title ?? `Order #${order.id}`}\n                  </h1>\n                  <Badge variant={order.statusColor}>{order.status}</Badge>\n                </div>\n                <p className=\"text-base font-light\">{order.date}</p>\n              </div>\n            </div>\n            <div className=\"grid @3xl:flex\">\n              <div className=\"order-2 flex-1 pr-12 @3xl:order-1\">\n                {order.destinations.map((destination) => (\n                  <Shipment\n                    addressLabel={shipmentAddressLabel}\n                    destination={destination}\n                    key={destination.id}\n                    methodLabel={shipmentMethodLabel}\n                  />\n                ))}\n                {order.emailDestinations.map((destination, index) => (\n                  <EmailDestination destination={destination} key={`email-destination-${index}`} />\n                ))}\n              </div>\n              <div className=\"order-1 basis-72 pt-8 @3xl:order-2\">\n                <div className=\"flex flex-col gap-8\">\n                  <div className=\"flex-1\">\n                    <div className=\"font-[family-name:var(--order-details-section-title-font-family,var(--font-family-heading))] text-2xl font-medium\">\n                      {orderSummaryLabel}\n                    </div>\n                    <Summary summary={order.summary} totalLabel={summaryTotalLabel} />\n                  </div>\n                  <div className=\"flex-1\">\n                    <div className=\"font-[family-name:var(--order-details-section-title-font-family,var(--font-family-heading))] text-2xl font-medium\">\n                      {order.paymentsSummary.title || 'Payment methods'}\n                    </div>\n                    <PaymentsSummary payments={order.paymentsSummary.payments} />\n                  </div>\n                </div>\n              </div>\n            </div>\n          </>\n        )}\n      </Stream>\n    </div>\n  );\n}\n\nfunction Shipment({\n  destination,\n  addressLabel = 'Shipping address',\n  methodLabel = 'Shipping method',\n}: {\n  destination: Destination;\n  addressLabel?: string;\n  methodLabel?: string;\n}) {\n  return (\n    <div className=\"border-b border-[var(--order-details-section-border,hsl(var(--contrast-100)))] py-8 @container\">\n      <div className=\"space-y-6\">\n        <div className=\"font-[family-name:var(--order-details-section-title-font-family,var(--font-family-heading))] text-2xl font-medium\">\n          {destination.title}\n        </div>\n        <div className=\"grid gap-8 @xl:flex @xl:gap-20\">\n          <div className=\"text-sm\">\n            <h3 className=\"font-semibold\">{addressLabel}</h3>\n            <div className=\"text-[var(--order-details-text-secondary,hsl(var(--contrast-500)))]\">\n              <p>{destination.address.name}</p>\n              <p>{destination.address.street1}</p>\n              <p>{destination.address.street2}</p>\n              <p>\n                {`${destination.address.city}, ${destination.address.state} ${destination.address.zipcode}`}\n              </p>\n              <p>{destination.address.country}</p>\n            </div>\n          </div>\n          {destination.shipments.map((shipment, index) => (\n            <div className=\"text-sm\" key={`${shipment.name}-${index}`}>\n              <h3 className=\"font-semibold\">{methodLabel}</h3>\n              <div className=\"text-[var(--order-details-text-secondary,hsl(var(--contrast-500)))]\">\n                <p>{shipment.name}</p>\n                <p>{shipment.status}</p>\n                <ShipmentTracking tracking={shipment.tracking} />\n              </div>\n            </div>\n          ))}\n        </div>\n        {destination.lineItems.map((lineItem) => (\n          <ShipmentLineItem key={lineItem.id} lineItem={lineItem} />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction EmailDestination({ destination }: { destination: EmailDestination }) {\n  return (\n    <div className=\"border-b border-[var(--order-details-section-border,hsl(var(--contrast-100)))] py-8 @container\">\n      <div className=\"space-y-6\">\n        <div className=\"font-[family-name:var(--order-details-section-title-font-family,var(--font-family-heading))] text-2xl font-medium\">\n          {destination.title}\n        </div>\n        {destination.lineItems.map((lineItem) => (\n          <ShipmentLineItem key={lineItem.id} lineItem={lineItem} />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction ShipmentSkeleton({\n  shipmentsPlaceholderCount = 1,\n  lineItemsPlaceholderCount = 2,\n}: {\n  shipmentsPlaceholderCount?: number;\n  lineItemsPlaceholderCount?: number;\n}) {\n  return (\n    <div className=\"border-b border-[var(--order-details-section-border,hsl(var(--contrast-100)))] py-8 @container\">\n      <div className=\"space-y-6\">\n        <Skeleton.Text characterCount={8} className=\"rounded text-2xl\" />\n        <div className=\"grid gap-8 @xl:flex @xl:gap-20\">\n          <div className=\"text-sm\">\n            <Skeleton.Text characterCount={13} className=\"rounded\" />\n            <div>\n              <Skeleton.Text characterCount={8} className=\"rounded\" />\n              <Skeleton.Text characterCount={12} className=\"rounded\" />\n              <Skeleton.Text characterCount={16} className=\"rounded\" />\n              <Skeleton.Text characterCount={8} className=\"rounded\" />\n            </div>\n          </div>\n          {Array.from({ length: shipmentsPlaceholderCount }).map((_, index) => (\n            <div className=\"text-sm\" key={index}>\n              <Skeleton.Text characterCount={13} className=\"rounded\" />\n              <div>\n                <Skeleton.Text characterCount={16} className=\"rounded\" />\n                <Skeleton.Text characterCount={8} className=\"rounded\" />\n                <Skeleton.Text characterCount={24} className=\"rounded\" />\n              </div>\n            </div>\n          ))}\n        </div>\n        {Array.from({ length: lineItemsPlaceholderCount }).map((_, index) => (\n          <ShipmentLineItemSkeleton key={index} />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction ShipmentTracking({\n  tracking,\n}: {\n  tracking?: TrackingWithUrl | TrackingWithNumber | TrackingWithUrlAndNumber;\n}) {\n  if (!tracking) {\n    return null;\n  }\n\n  if ('url' in tracking && 'number' in tracking) {\n    return (\n      <p>\n        <Link href={tracking.url} target=\"_blank\">\n          {tracking.number}\n        </Link>\n      </p>\n    );\n  }\n\n  if ('url' in tracking) {\n    return (\n      <p>\n        <Link href={tracking.url} target=\"_blank\">\n          {tracking.url}\n        </Link>\n      </p>\n    );\n  }\n\n  return <p>{tracking.number}</p>;\n}\n\nfunction ShipmentLineItem({ lineItem }: { lineItem: ShipmentLineItem }) {\n  const LineItemWrapper = ({ children }: { children: React.ReactNode }) => {\n    if (lineItem.href) {\n      return (\n        <Link\n          className=\"group grid shrink-0 cursor-pointer gap-8 rounded-xl ring-[var(--order-details-section-focus,hsl(var(--primary)))] ring-offset-4 focus-visible:outline-none focus-visible:ring-2 @sm:flex @sm:rounded-2xl\"\n          href={lineItem.href}\n          id={lineItem.id}\n        >\n          {children}\n        </Link>\n      );\n    }\n\n    return (\n      <div\n        className=\"group grid shrink-0 gap-8 rounded-xl ring-[var(--order-details-section-focus,hsl(var(--primary)))] ring-offset-4 focus-visible:outline-none focus-visible:ring-2 @sm:flex @sm:rounded-2xl\"\n        id={lineItem.id}\n      >\n        {children}\n      </div>\n    );\n  };\n\n  return (\n    <LineItemWrapper>\n      <div className=\"relative aspect-square basis-40 overflow-hidden rounded-[inherit] border border-[var(--order-details-section-border,hsl(var(--contrast-100)))] bg-[var(--order-details-section-image-background,hsl(var(--contrast-100)))]\">\n        {lineItem.image?.src != null ? (\n          <Image\n            alt={lineItem.image.alt}\n            className=\"w-full scale-100 select-none object-cover transition-transform duration-500 ease-out group-hover:scale-110\"\n            fill\n            sizes=\"10rem\"\n            src={lineItem.image.src}\n          />\n        ) : (\n          <div className=\"pl-2 pt-3 text-4xl font-bold leading-[0.8] tracking-tighter text-[var(--order-details-section-line-item,hsl(var(--contrast-300)))] transition-transform duration-500 ease-out group-hover:scale-105\">\n            {lineItem.title}\n          </div>\n        )}\n      </div>\n\n      <div className=\"space-y-3 text-sm leading-snug\">\n        <div>\n          <div className=\"flex items-center gap-1 text-sm\">\n            <span className=\"font-semibold\">{lineItem.title}</span>\n            <span>×</span>\n            <span className=\"font-semibold\">{lineItem.quantity}</span>\n          </div>\n          {lineItem.subtitle != null && lineItem.subtitle !== '' && (\n            <div className=\"font-normal text-[var(--order-details-section-line-item-subtitle,hsl(var(--contrast-500)))]\">\n              {lineItem.subtitle}\n            </div>\n          )}\n        </div>\n        <div className=\"flex gap-1 text-sm\">\n          <span className=\"font-semibold\">{lineItem.totalPrice}</span>\n          {lineItem.quantity > 1 && <span className=\"font-normal\">({lineItem.price} each)</span>}\n        </div>\n        <div>\n          {lineItem.metadata?.map((metadata, index) => (\n            <div className=\"flex gap-1 text-sm\" key={`lineItem-meta-${metadata.label}-${index}`}>\n              <span className=\"font-semibold\">{metadata.label}:</span>\n              <span>{metadata.value}</span>\n            </div>\n          ))}\n        </div>\n      </div>\n    </LineItemWrapper>\n  );\n}\n\nfunction ShipmentLineItemSkeleton() {\n  return (\n    <div className=\"group grid shrink-0 gap-8 rounded-xl @sm:flex @sm:rounded-2xl\">\n      <div className=\"relative aspect-square basis-40 overflow-hidden rounded-[inherit]\">\n        <Skeleton.Box className=\"h-full w-full\" />\n      </div>\n\n      <div className=\"space-y-3 text-sm leading-snug\">\n        <div>\n          <div className=\"flex items-center gap-1 text-sm\">\n            <Skeleton.Text characterCount={24} className=\"rounded\" />\n          </div>\n          <Skeleton.Text characterCount={6} className=\"rounded\" />\n        </div>\n        <div className=\"flex gap-1 text-sm\">\n          <Skeleton.Text characterCount={5} className=\"rounded\" />\n          <Skeleton.Text characterCount={8} className=\"rounded\" />\n        </div>\n        <div>\n          <div className=\"flex gap-1 text-sm\">\n            <Skeleton.Text characterCount={7} className=\"rounded\" />\n            <Skeleton.Text characterCount={3} className=\"rounded\" />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction Summary({ summary, totalLabel = 'Total' }: { summary: Summary; totalLabel?: string }) {\n  return (\n    <div>\n      <div className=\"space-y-2 pb-3 pt-5\">\n        {summary.lineItems.map((lineItem, index) => (\n          <div className=\"flex justify-between\" key={index}>\n            <div>\n              <div className=\"text-sm\">{lineItem.label}</div>\n              {lineItem.subtext != null && lineItem.subtext !== '' && (\n                <div className=\"text-xs text-[var(--order-details-section-line-item-subtext,hsl(var(--contrast-400)))]\">\n                  {lineItem.subtext}\n                </div>\n              )}\n            </div>\n\n            <span className=\"text-sm\">{lineItem.value}</span>\n          </div>\n        ))}\n      </div>\n      <div className=\"flex justify-between border-t border-[var(--order-details-section-border,hsl(var(--contrast-100)))] py-3 font-semibold\">\n        <span>{totalLabel}</span>\n        <span>{summary.total}</span>\n      </div>\n    </div>\n  );\n}\n\nfunction SummarySkeleton({ placeholderCount = 2 }: { placeholderCount?: number }) {\n  return (\n    <div>\n      <div className=\"space-y-2 pb-3 pt-5\">\n        {Array.from({ length: placeholderCount }).map((_, index) => (\n          <div className=\"flex justify-between\" key={index}>\n            <div>\n              <Skeleton.Text characterCount={6} className=\"rounded text-sm\" />\n              <Skeleton.Text characterCount={12} className=\"rounded text-xs\" />\n            </div>\n\n            <Skeleton.Text characterCount={6} className=\"rounded text-sm\" />\n          </div>\n        ))}\n      </div>\n      <div className=\"flex justify-between border-t border-[var(--order-details-section-border,hsl(var(--contrast-100)))] py-3\">\n        <Skeleton.Text characterCount={6} className=\"rounded\" />\n        <Skeleton.Text characterCount={6} className=\"rounded\" />\n      </div>\n    </div>\n  );\n}\n\nfunction PaymentsSummary({ payments }: { payments: OrderPayment[] }) {\n  return (\n    <div>\n      <div className=\"space-y-2 pb-3 pt-5\">\n        {payments.map((payment, index) => (\n          <div className=\"flex justify-between\" key={index}>\n            <div>\n              <div className=\"text-sm\">{payment.title}</div>\n              {payment.subtitle != null && (\n                <div className=\"text-xs text-[var(--order-details-section-line-item-subtext,hsl(var(--contrast-400)))]\">\n                  {payment.subtitle}\n                </div>\n              )}\n            </div>\n\n            <span className=\"text-sm\">{payment.amount}</span>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction PaymentsSummarySkeleton({ placeholderCount = 2 }: { placeholderCount?: number }) {\n  return (\n    <div>\n      <div className=\"space-y-2 pb-3 pt-5\">\n        {Array.from({ length: placeholderCount }).map((_, index) => (\n          <div className=\"flex justify-between\" key={index}>\n            <div>\n              <Skeleton.Text characterCount={6} className=\"rounded text-sm\" />\n              <Skeleton.Text characterCount={12} className=\"rounded text-xs\" />\n            </div>\n\n            <Skeleton.Text characterCount={6} className=\"rounded text-sm\" />\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction OrderDetailsSectionSkeleton({\n  prevHref,\n  placeholderCount = 1,\n  lineItemsPlaceholderCount = 2,\n}: {\n  prevHref?: string;\n  placeholderCount?: number;\n  lineItemsPlaceholderCount?: number;\n}) {\n  return (\n    <div className=\"animate-pulse\">\n      <div className=\"flex gap-4 border-b border-[var(--order-details-section-border,hsl(var(--contrast-100)))] pb-8\">\n        {prevHref != null && prevHref !== '' && (\n          <ButtonLink href={prevHref} shape=\"circle\" size=\"small\" variant=\"ghost\">\n            <ArrowLeft />\n          </ButtonLink>\n        )}\n        <div className=\"space-y-1\">\n          <div className=\"flex items-center gap-3\">\n            <Skeleton.Text characterCount={8} className=\"rounded text-4xl\" />\n            <Skeleton.Text characterCount={8} className=\"rounded text-xs\" />\n          </div>\n          <Skeleton.Text characterCount={7} className=\"rounded text-base\" />\n        </div>\n      </div>\n      <div className=\"grid @3xl:flex\">\n        <div className=\"order-2 flex-1 pr-12 @3xl:order-1\">\n          {Array.from({ length: placeholderCount }).map((_, index) => (\n            <ShipmentSkeleton key={index} lineItemsPlaceholderCount={lineItemsPlaceholderCount} />\n          ))}\n        </div>\n        <div className=\"order-1 basis-72 pt-8 @3xl:order-2\">\n          <div className=\"flex flex-col gap-8\">\n            <div className=\"flex-1\">\n              <Skeleton.Text characterCount={10} className=\"rounded text-2xl\" />\n              <SummarySkeleton placeholderCount={3} />\n            </div>\n            <div className=\"flex-1\">\n              <Skeleton.Text characterCount={10} className=\"rounded text-2xl\" />\n              <PaymentsSummarySkeleton placeholderCount={3} />\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/order-list/index.tsx",
    "content": "import { clsx } from 'clsx';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Badge } from '@/vibes/soul/primitives/badge';\nimport { ButtonLink } from '@/vibes/soul/primitives/button-link';\nimport { CursorPagination, CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination';\nimport {\n  type Product,\n  ProductCard,\n  ProductCardSkeleton,\n} from '@/vibes/soul/primitives/product-card';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\n\nexport interface Order {\n  id: string;\n  totalPrice: string;\n  status: string;\n  href: string;\n  lineItems: OrderLineItem[];\n}\n\nexport interface OrderLineItem extends Product {\n  price: string;\n  totalPrice: string;\n}\n\nexport interface OrderListProps {\n  className?: string;\n  title?: string;\n  orders: Streamable<Order[]>;\n  paginationInfo?: Streamable<CursorPaginationInfo>;\n  orderNumberLabel?: string;\n  totalLabel?: string;\n  viewDetailsLabel?: string;\n  emptyStateTitle?: string;\n  emptyStateActionLabel?: string;\n  emptyStateActionHref?: string;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --order-list-title-font-family: var(--font-family-heading);\n *   --order-list-label-font-family: var(--font-family-mono);\n *   --order-list-title: hsl(var(--foreground));\n *   --order-list-label: hsl(var(--contrast-500));\n *   --order-list-info: hsl(var(--foreground));\n *   --order-list-border: hsl(var(--contrast-100));\n *   --order-list-empty-state-title: hsl(var(--foreground));\n * }\n * ```\n */\nexport function OrderList({\n  className,\n  title = 'Orders',\n  orders: streamableOrders,\n  paginationInfo,\n  orderNumberLabel = 'Order #',\n  totalLabel = 'Total',\n  viewDetailsLabel = 'View details',\n  emptyStateTitle = \"You don't have any orders\",\n  emptyStateActionLabel = 'Shop now',\n  emptyStateActionHref = '/',\n}: OrderListProps) {\n  return (\n    <section className=\"group/order-list w-full @container\">\n      <header className=\"mb-4 border-[var(--order-list-border,hsl(var(--contrast-100)))] @2xl:min-h-[72px] @2xl:border-b\">\n        <div className=\"mb-4 flex items-center justify-between\">\n          <h1 className=\"hidden font-[family-name:var(--order-list-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none tracking-tight text-[var(--order-list-title,hsl(var(--foreground)))] @2xl:block\">\n            {title}\n          </h1>\n        </div>\n      </header>\n      <Stream fallback={<OrderListSkeleton />} value={streamableOrders}>\n        {(orders) => {\n          if (orders.length === 0) {\n            return (\n              <OrderListEmptyState\n                emptyStateActionHref={emptyStateActionHref}\n                emptyStateActionLabel={emptyStateActionLabel}\n                emptyStateTitle={emptyStateTitle}\n              />\n            );\n          }\n\n          return (\n            <div className=\"@container\">\n              {orders.map((order) => (\n                <div\n                  className={clsx(\n                    'border-[var(--order-list-border,hsl(var(--contrast-100)))] pb-6 pt-5 last:border-b @lg:pb-10 @lg:pt-6',\n                    className,\n                  )}\n                  key={order.id}\n                >\n                  <div className=\"flex flex-col justify-between gap-x-10 gap-y-4 @lg:flex-row\">\n                    <div className=\"flex items-start gap-x-12 gap-y-4\">\n                      <div>\n                        <span className=\"font-[family-name:var(--order-list-label-font-family,var(--font-family-mono))] text-xs uppercase leading-normal text-[var(--order-list-label,hsl(var(--contrast-500)))]\">\n                          {orderNumberLabel}\n                        </span>\n                        <span className=\"block text-lg font-semibold leading-normal text-[var(--order-list-info,hsl(var(--foreground)))]\">\n                          {order.id}\n                        </span>\n                      </div>\n                      <div>\n                        <span className=\"font-[family-name:var(--order-list-label-font-family,var(--font-family-mono))] text-xs uppercase leading-normal text-[var(--order-list-label,hsl(var(--contrast-500)))]\">\n                          {totalLabel}\n                        </span>\n                        <span className=\"block text-lg font-semibold leading-normal text-[var(--order-list-info,hsl(var(--foreground)))]\">\n                          {order.totalPrice}\n                        </span>\n                      </div>\n                      <Badge className=\"mt-0.5\">{order.status}</Badge>\n                    </div>\n                    <ButtonLink href={order.href} size=\"small\">\n                      {viewDetailsLabel}\n                    </ButtonLink>\n                  </div>\n                  <div className=\"mt-6 flex gap-4 overflow-hidden [mask-image:linear-gradient(to_right,_black_0%,_black_80%,_transparent_98%)]\">\n                    {order.lineItems.map((lineItem) => (\n                      <ProductCard\n                        className=\"shrink-0 basis-32 @lg:basis-40\"\n                        key={lineItem.id}\n                        product={lineItem}\n                      />\n                    ))}\n                  </div>\n                </div>\n              ))}\n            </div>\n          );\n        }}\n      </Stream>\n      {paginationInfo && <CursorPagination info={paginationInfo} />}\n    </section>\n  );\n}\n\nfunction OrderListSkeleton() {\n  return (\n    <Skeleton.Root className=\"group-has-[[data-pending]]/order-list:animate-pulse\" pending>\n      {Array.from({ length: 3 }).map((_, id) => (\n        <div\n          className=\"border-[var(--order-list-border,hsl(var(--contrast-100)))] pb-6 pt-5 last:border-b @lg:pb-10 @lg:pt-6\"\n          data-pending\n          key={id}\n        >\n          <div className=\"flex flex-col justify-between gap-x-10 gap-y-4 @lg:flex-row\">\n            <div className=\"flex flex-wrap items-start gap-x-12 gap-y-4\">\n              <div>\n                <Skeleton.Text characterCount={7} className=\"rounded text-xs\" />\n                <Skeleton.Text characterCount={7} className=\"rounded text-lg\" />\n              </div>\n              <div>\n                <Skeleton.Text characterCount={8} className=\"rounded text-xs\" />\n                <Skeleton.Text characterCount={6} className=\"rounded text-lg\" />\n              </div>\n              <Skeleton.Box className=\"mt-0.5 h-[22px] w-[55px] rounded\" />\n            </div>\n            <Skeleton.Box className=\"h-[43px] min-w-[12ch] gap-x-2 rounded-full px-4 py-2.5\" />\n          </div>\n          <div className=\"mt-6 flex gap-4 overflow-hidden [mask-image:linear-gradient(to_right,_black_0%,_black_80%,_transparent_98%)]\">\n            {Array.from({ length: 8 }).map((__, idx) => (\n              <ProductCardSkeleton className=\"shrink-0 basis-32 @lg:basis-40\" key={idx} />\n            ))}\n          </div>\n        </div>\n      ))}\n    </Skeleton.Root>\n  );\n}\n\nfunction OrderListEmptyState({\n  emptyStateTitle,\n  emptyStateActionLabel,\n  emptyStateActionHref = '/',\n}: Pick<OrderListProps, 'emptyStateTitle' | 'emptyStateActionLabel' | 'emptyStateActionHref'>) {\n  return (\n    <div className=\"@container\">\n      <div className=\"py-20\">\n        <header className=\"mx-auto flex max-w-2xl flex-col items-center gap-5\">\n          <h2 className=\"text-center text-lg font-semibold text-[var(--order-list-empty-state-title,hsl(var(--foreground)))]\">\n            {emptyStateTitle}\n          </h2>\n          <ButtonLink className=\"w-fit\" href={emptyStateActionHref}>\n            {emptyStateActionLabel}\n          </ButtonLink>\n        </header>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/product-carousel/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { ArrowLeft, ArrowRight } from 'lucide-react';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport {\n  Carousel,\n  CarouselButtons,\n  CarouselContent,\n  CarouselItem,\n  CarouselScrollbar,\n} from '@/vibes/soul/primitives/carousel';\nimport {\n  type Product,\n  ProductCard,\n  ProductCardSkeleton,\n} from '@/vibes/soul/primitives/product-card';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\n\nexport type CarouselProduct = Product;\n\nexport interface ProductCarouselProps {\n  products: Streamable<CarouselProduct[]>;\n  className?: string;\n  colorScheme?: 'light' | 'dark';\n  aspectRatio?: '5:6' | '3:4' | '1:1';\n  emptyStateTitle?: Streamable<string>;\n  emptyStateSubtitle?: Streamable<string>;\n  scrollbarLabel?: string;\n  previousLabel?: string;\n  nextLabel?: string;\n  placeholderCount?: number;\n  showButtons?: boolean;\n  showScrollbar?: boolean;\n  hideOverflow?: boolean;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --product-carousel-light-empty-title: hsl(var(--foreground));\n *   --product-carousel-light-empty-subtitle: hsl(var(--contrast-500));\n *   --product-carousel-dark-empty-title: hsl(var(--background));\n *   --product-carousel-dark-empty-subtitle: hsl(var(--contrast-100));\n *   --product-carousel-empty-title-font-family: var(--font-family-heading);\n *   --product-carousel-empty-subtitle-font-family: var(--font-family-body);\n * }\n * ```\n */\nexport function ProductCarousel({\n  products: streamableProducts,\n  className,\n  colorScheme = 'light',\n  aspectRatio = '5:6',\n  emptyStateTitle = 'No products found',\n  emptyStateSubtitle = 'Try browsing our complete catalog of products.',\n  scrollbarLabel = 'Scroll',\n  previousLabel = 'Previous',\n  nextLabel = 'Next',\n  placeholderCount = 8,\n  showButtons = true,\n  showScrollbar = true,\n  hideOverflow = true,\n}: ProductCarouselProps) {\n  return (\n    <Stream\n      fallback={\n        <ProductsCarouselSkeleton\n          className={className}\n          hideOverflow={hideOverflow}\n          placeholderCount={placeholderCount}\n        />\n      }\n      value={streamableProducts}\n    >\n      {(products) => {\n        if (products.length === 0) {\n          return (\n            <ProductsCarouselEmptyState\n              className={className}\n              colorScheme={colorScheme}\n              emptyStateSubtitle={emptyStateSubtitle}\n              emptyStateTitle={emptyStateTitle}\n              hideOverflow={hideOverflow}\n              placeholderCount={placeholderCount}\n            />\n          );\n        }\n\n        return (\n          <Carousel className={className} hideOverflow={hideOverflow}>\n            <CarouselContent className=\"-ml-4 mb-10 @2xl:-ml-5\">\n              {products.map(({ id, ...product }) => (\n                <CarouselItem\n                  className=\"basis-full pl-4 @md:basis-1/2 @lg:basis-1/3 @2xl:basis-1/4 @2xl:pl-5\"\n                  key={id}\n                >\n                  <ProductCard\n                    aspectRatio={aspectRatio}\n                    colorScheme={colorScheme}\n                    imageSizes=\"(min-width: 42rem) 25vw, (min-width: 32rem) 33vw, (min-width: 28rem) 50vw, 100vw\"\n                    product={{ id, ...product }}\n                  />\n                </CarouselItem>\n              ))}\n            </CarouselContent>\n            {(showButtons || showScrollbar) && (\n              <div className=\"mt-10 flex w-full items-center justify-between gap-8\">\n                <CarouselScrollbar\n                  className={clsx(!showScrollbar && 'pointer-events-none invisible')}\n                  colorScheme={colorScheme}\n                  label={scrollbarLabel}\n                />\n                <CarouselButtons\n                  className={clsx(!showButtons && 'pointer-events-none invisible')}\n                  colorScheme={colorScheme}\n                  nextLabel={nextLabel}\n                  previousLabel={previousLabel}\n                />\n              </div>\n            )}\n          </Carousel>\n        );\n      }}\n    </Stream>\n  );\n}\n\nexport function ProductsCarouselSkeleton({\n  className,\n  placeholderCount = 8,\n  hideOverflow,\n}: Pick<ProductCarouselProps, 'className' | 'placeholderCount' | 'hideOverflow'>) {\n  return (\n    <Skeleton.Root\n      className={clsx('group-has-data-pending/product-carousel:animate-pulse', className)}\n      hideOverflow={hideOverflow}\n      pending\n    >\n      <div className=\"w-full\">\n        <div className=\"-ml-4 flex @2xl:-ml-5\">\n          {Array.from({ length: placeholderCount }).map((_, index) => (\n            <div\n              className=\"min-w-0 shrink-0 grow-0 basis-full pl-4 @md:basis-1/2 @lg:basis-1/3 @2xl:basis-1/4 @2xl:pl-5\"\n              key={index}\n            >\n              <ProductCardSkeleton />\n            </div>\n          ))}\n        </div>\n      </div>\n      <div className=\"mt-10 flex w-full items-center justify-between gap-8\">\n        <Skeleton.Box className=\"h-1 w-56 rounded\" />\n        <div className=\"flex gap-2\">\n          <Skeleton.Icon icon={<ArrowLeft aria-hidden className=\"h-6 w-6\" strokeWidth={1.5} />} />\n          <Skeleton.Icon icon={<ArrowRight aria-hidden className=\"h-6 w-6\" strokeWidth={1.5} />} />\n        </div>\n      </div>\n    </Skeleton.Root>\n  );\n}\n\nexport function ProductsCarouselEmptyState({\n  className,\n  placeholderCount = 8,\n  emptyStateTitle,\n  emptyStateSubtitle,\n  hideOverflow,\n  colorScheme = 'light',\n}: Pick<\n  ProductCarouselProps,\n  | 'className'\n  | 'placeholderCount'\n  | 'emptyStateTitle'\n  | 'emptyStateSubtitle'\n  | 'hideOverflow'\n  | 'colorScheme'\n>) {\n  return (\n    <Skeleton.Root className={clsx('relative', className)} hideOverflow={hideOverflow}>\n      <div className=\"w-full\">\n        <div className=\"-ml-4 flex [mask-image:linear-gradient(to_bottom,_black_0%,_transparent_90%)] @2xl:-ml-5\">\n          {Array.from({ length: placeholderCount }).map((_, index) => (\n            <div\n              className=\"min-w-0 shrink-0 grow-0 basis-full pl-4 @md:basis-1/2 @lg:basis-1/3 @2xl:basis-1/4 @2xl:pl-5\"\n              key={index}\n            >\n              <ProductCardSkeleton />\n            </div>\n          ))}\n        </div>\n      </div>\n      <div className=\"absolute inset-0 mx-auto px-3 py-16 pb-3 @4xl:px-10 @4xl:pb-10 @4xl:pt-28\">\n        <div className=\"mx-auto max-w-xl space-y-2 text-center @4xl:space-y-3\">\n          <h3\n            className={clsx(\n              'font-[family-name:var(--product-carousel-empty-title-font-family,var(--font-family-heading))] text-2xl leading-tight @4xl:text-4xl @4xl:leading-none',\n              {\n                light: 'text-[var(--product-carousel-light-empty-title,hsl(var(--foreground)))]',\n                dark: 'text-[var(--product-carousel-dark-empty-title,hsl(var(--background)))]',\n              }[colorScheme],\n            )}\n          >\n            {emptyStateTitle}\n          </h3>\n          <p\n            className={clsx(\n              'font-[family-name:var(--product-carousel-empty-subtitle-font-family,var(--font-family-body))] text-sm @4xl:text-lg',\n              {\n                light:\n                  'text-[var(--product-carousel-light-empty-subtitle,hsl(var(--contrast-500)))]',\n                dark: 'text-[var(--product-carousel-dark-empty-subtitle,hsl(var(--contrast-200)))]',\n              }[colorScheme],\n            )}\n          >\n            {emptyStateSubtitle}\n          </p>\n        </div>\n      </div>\n    </Skeleton.Root>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/product-detail/actions/revalidate-cart.ts",
    "content": "'use server';\n\nimport { revalidateTag } from 'next/cache';\n\nimport { TAGS } from '~/client/tags';\n\n// eslint-disable-next-line @typescript-eslint/require-await\nexport const revalidateCart = async () => revalidateTag(TAGS.cart, { expire: 0 });\n"
  },
  {
    "path": "core/vibes/soul/sections/product-detail/index.tsx",
    "content": "import { ReactNode } from 'react';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Accordion, AccordionItem } from '@/vibes/soul/primitives/accordion';\nimport { AnimatedUnderline } from '@/vibes/soul/primitives/animated-underline';\nimport { Price, PriceLabel } from '@/vibes/soul/primitives/price-label';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\nimport { type Breadcrumb, Breadcrumbs } from '@/vibes/soul/sections/breadcrumbs';\nimport {\n  ProductGallery,\n  ProductGalleryLoadMoreAction,\n} from '@/vibes/soul/sections/product-detail/product-gallery';\nimport { ReviewForm, SubmitReviewAction } from '@/vibes/soul/sections/reviews/review-form';\n\nimport {\n  BackorderDisplayData,\n  ProductDetailForm,\n  ProductDetailFormAction,\n  StockDisplayData,\n} from './product-detail-form';\nimport { RatingLink } from './rating-link';\nimport { Field } from './schema';\n\ninterface ProductDetailProduct {\n  id: string;\n  title: string;\n  href: string;\n  images: Streamable<{\n    images: Array<{ src: string; alt: string }>;\n    pageInfo?: { hasNextPage: boolean; endCursor: string | null };\n  }>;\n  price?: Streamable<Price | null>;\n  subtitle?: string;\n  badge?: string;\n  rating?: Streamable<number | null>;\n  reviewsEnabled?: boolean;\n  showRating?: boolean;\n  numberOfReviews?: number;\n  summary?: Streamable<string>;\n  description?: Streamable<string | ReactNode | null>;\n  accordions?: Streamable<\n    Array<{\n      title: string;\n      content: ReactNode;\n    }>\n  >;\n  minQuantity?: Streamable<number | null>;\n  maxQuantity?: Streamable<number | null>;\n  stockDisplayData?: Streamable<StockDisplayData | null>;\n  backorderDisplayData?: Streamable<BackorderDisplayData | null>;\n}\n\nexport interface ProductDetailProps<F extends Field> {\n  breadcrumbs?: Streamable<Breadcrumb[]>;\n  product: Streamable<ProductDetailProduct | null>;\n  action: ProductDetailFormAction<F>;\n  fields: Streamable<F[]>;\n  quantityLabel?: string;\n  incrementLabel?: string;\n  decrementLabel?: string;\n  emptySelectPlaceholder?: string;\n  ctaLabel?: Streamable<string | null>;\n  ctaDisabled?: Streamable<boolean | null>;\n  prefetch?: boolean;\n  thumbnailLabel?: string;\n  additionalInformationTitle?: string;\n  additionalActions?: ReactNode;\n  reviewFormEmailLabel?: string;\n  reviewFormModalTitle?: string;\n  reviewFormNameLabel?: string;\n  reviewFormRatingLabel?: string;\n  reviewFormReviewLabel?: string;\n  reviewFormSubmitLabel?: string;\n  reviewFormTitleLabel?: string;\n  reviewFormAction: SubmitReviewAction;\n  user: Streamable<{ email: string; name: string }>;\n  loadMoreImagesAction?: ProductGalleryLoadMoreAction;\n  recaptchaSiteKey?: string;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --product-detail-border: hsl(var(--contrast-100));\n *   --product-detail-subtitle-font-family: var(--font-family-mono);\n *   --product-detail-title-font-family: var(--font-family-heading);\n *   --product-detail-primary-text: hsl(var(--foreground));\n *   --product-detail-secondary-text:  hsl(var(--contrast-500));\n * }\n * ```\n */\nexport function ProductDetail<F extends Field>({\n  product: streamableProduct,\n  action,\n  fields: streamableFields,\n  breadcrumbs,\n  quantityLabel,\n  incrementLabel,\n  decrementLabel,\n  emptySelectPlaceholder,\n  ctaLabel: streamableCtaLabel,\n  ctaDisabled: streamableCtaDisabled,\n  prefetch,\n  thumbnailLabel,\n  additionalInformationTitle = 'Additional information',\n  additionalActions,\n  reviewFormEmailLabel,\n  reviewFormModalTitle,\n  reviewFormNameLabel,\n  reviewFormRatingLabel,\n  reviewFormReviewLabel,\n  reviewFormSubmitLabel,\n  reviewFormTitleLabel,\n  reviewFormAction,\n  user,\n  loadMoreImagesAction,\n  recaptchaSiteKey,\n}: ProductDetailProps<F>) {\n  return (\n    <section className=\"@container\">\n      <div className=\"group/product-detail mx-auto w-full max-w-screen-2xl px-4 py-10 @xl:px-6 @xl:py-14 @4xl:px-8 @4xl:py-20\">\n        {breadcrumbs && (\n          <div className=\"group/breadcrumbs mb-6\">\n            <Breadcrumbs breadcrumbs={breadcrumbs} />\n          </div>\n        )}\n        <Stream fallback={<ProductDetailSkeleton />} value={streamableProduct}>\n          {(product) =>\n            product && (\n              <div className=\"grid grid-cols-1 items-stretch gap-x-8 gap-y-8 @2xl:grid-cols-2 @5xl:gap-x-12\">\n                <div className=\"group/product-gallery hidden @2xl:block\">\n                  <Stream fallback={<ProductGallerySkeleton />} value={product.images}>\n                    {(imagesData) => (\n                      <ProductGallery\n                        images={imagesData.images}\n                        loadMoreAction={loadMoreImagesAction}\n                        pageInfo={imagesData.pageInfo}\n                        productId={Number(product.id)}\n                      />\n                    )}\n                  </Stream>\n                </div>\n                {/* Product Details */}\n                <div className=\"text-[var(--product-detail-primary-text,hsl(var(--foreground)))]\">\n                  {Boolean(product.subtitle) && (\n                    <p className=\"font-[family-name:var(--product-detail-subtitle-font-family,var(--font-family-mono))] text-sm uppercase\">\n                      {product.subtitle}\n                    </p>\n                  )}\n                  <h1 className=\"mb-3 mt-2 font-[family-name:var(--product-detail-title-font-family,var(--font-family-heading))] text-2xl font-medium leading-none @xl:mb-4 @xl:text-3xl @4xl:text-4xl\">\n                    {product.title}\n                  </h1>\n                  {product.reviewsEnabled && (\n                    <div className=\"group/product-rating\">\n                      <ReviewForm\n                        action={reviewFormAction}\n                        formEmailLabel={reviewFormEmailLabel}\n                        formModalTitle={reviewFormModalTitle}\n                        formNameLabel={reviewFormNameLabel}\n                        formRatingLabel={reviewFormRatingLabel}\n                        formReviewLabel={reviewFormReviewLabel}\n                        formSubmitLabel={reviewFormSubmitLabel}\n                        formTitleLabel={reviewFormTitleLabel}\n                        productId={Number(product.id)}\n                        recaptchaSiteKey={recaptchaSiteKey}\n                        streamableImages={product.images}\n                        streamableProduct={{ name: product.title }}\n                        streamableUser={user}\n                        trigger={\n                          <AnimatedUnderline className=\"cursor-pointer\">\n                            Write a review\n                          </AnimatedUnderline>\n                        }\n                      />\n                    </div>\n                  )}\n                  {product.showRating && (\n                    <div className=\"group/product-rating\">\n                      <Stream\n                        fallback={<RatingSkeleton />}\n                        value={Streamable.all([product.rating, product.numberOfReviews])}\n                      >\n                        {([rating, numberOfReviews]) => (\n                          <RatingLink\n                            numberOfReviews={numberOfReviews ?? 0}\n                            rating={rating ?? 0}\n                            scrollTargetId=\"reviews\"\n                          />\n                        )}\n                      </Stream>\n                    </div>\n                  )}\n                  <div className=\"group/product-price\">\n                    <Stream fallback={<PriceLabelSkeleton />} value={product.price}>\n                      {(price) => (\n                        <PriceLabel className=\"my-3 text-xl @xl:text-2xl\" price={price ?? ''} />\n                      )}\n                    </Stream>\n                  </div>\n                  <div className=\"group/product-gallery mb-8 @2xl:hidden\">\n                    <Stream fallback={<ProductGallerySkeleton />} value={product.images}>\n                      {(imagesData) => (\n                        <ProductGallery\n                          images={imagesData.images}\n                          loadMoreAction={loadMoreImagesAction}\n                          pageInfo={imagesData.pageInfo}\n                          productId={Number(product.id)}\n                          thumbnailLabel={thumbnailLabel}\n                        />\n                      )}\n                    </Stream>\n                  </div>\n                  <div className=\"group/product-summary\">\n                    <Stream fallback={<ProductSummarySkeleton />} value={product.summary}>\n                      {(summary) =>\n                        Boolean(summary) && (\n                          <p className=\"text-[var(--product-detail-secondary-text,hsl(var(--contrast-500)))]\">\n                            {summary}\n                          </p>\n                        )\n                      }\n                    </Stream>\n                  </div>\n                  <div className=\"group/product-detail-form\">\n                    <Stream\n                      fallback={<ProductDetailFormSkeleton />}\n                      value={Streamable.all([\n                        streamableFields,\n                        streamableCtaLabel,\n                        streamableCtaDisabled,\n                        product.minQuantity,\n                        product.maxQuantity,\n                        product.stockDisplayData,\n                        product.backorderDisplayData,\n                      ])}\n                    >\n                      {([\n                        fields,\n                        ctaLabel,\n                        ctaDisabled,\n                        minQuantity,\n                        maxQuantity,\n                        stockDisplayData,\n                        backorderDisplayData,\n                      ]) => (\n                        <ProductDetailForm\n                          action={action}\n                          additionalActions={additionalActions}\n                          backorderDisplayData={backorderDisplayData ?? undefined}\n                          ctaDisabled={ctaDisabled ?? undefined}\n                          ctaLabel={ctaLabel ?? undefined}\n                          decrementLabel={decrementLabel}\n                          emptySelectPlaceholder={emptySelectPlaceholder}\n                          fields={fields}\n                          incrementLabel={incrementLabel}\n                          maxQuantity={maxQuantity ?? undefined}\n                          minQuantity={minQuantity ?? undefined}\n                          prefetch={prefetch}\n                          productId={product.id}\n                          quantityLabel={quantityLabel}\n                          stockDisplayData={stockDisplayData ?? undefined}\n                        />\n                      )}\n                    </Stream>\n                  </div>\n                  <div className=\"group/product-description\">\n                    <Stream fallback={<ProductDescriptionSkeleton />} value={product.description}>\n                      {(description) =>\n                        Boolean(description) && (\n                          <div className=\"prose prose-sm max-w-none border-t border-[var(--product-detail-border,hsl(var(--contrast-100)))] py-8 [&>div>*:first-child]:mt-0 [&>div>*:last-child]:mb-0\">\n                            {description}\n                          </div>\n                        )\n                      }\n                    </Stream>\n                  </div>\n                  <h2 className=\"sr-only\">{additionalInformationTitle}</h2>\n                  <div className=\"group/product-accordion\">\n                    <Stream fallback={<ProductAccordionsSkeleton />} value={product.accordions}>\n                      {(accordions) =>\n                        accordions && (\n                          <Accordion\n                            className=\"border-t border-[var(--product-detail-border,hsl(var(--contrast-100)))] pt-4\"\n                            type=\"multiple\"\n                          >\n                            {accordions.map((accordion, index) => (\n                              <AccordionItem\n                                key={index}\n                                title={accordion.title}\n                                value={index.toString()}\n                              >\n                                {accordion.content}\n                              </AccordionItem>\n                            ))}\n                          </Accordion>\n                        )\n                      }\n                    </Stream>\n                  </div>\n                </div>\n              </div>\n            )\n          }\n        </Stream>\n      </div>\n    </section>\n  );\n}\n\nfunction ProductGallerySkeleton() {\n  return (\n    <Skeleton.Root className=\"group-has-[[data-pending]]/product-gallery:animate-pulse\" pending>\n      <div className=\"w-full overflow-hidden rounded-xl @xl:rounded-2xl\">\n        <div className=\"flex\">\n          <Skeleton.Box className=\"aspect-[4/5] h-full w-full shrink-0 grow-0 basis-full\" />\n        </div>\n      </div>\n      <div className=\"mt-2 flex max-w-full gap-2 overflow-x-auto\">\n        {Array.from({ length: 5 }).map((_, idx) => (\n          <Skeleton.Box className=\"h-12 w-12 shrink-0 rounded-lg @md:h-16 @md:w-16\" key={idx} />\n        ))}\n      </div>\n    </Skeleton.Root>\n  );\n}\n\nfunction PriceLabelSkeleton() {\n  return <Skeleton.Box className=\"my-5 h-4 w-20 rounded-md\" />;\n}\n\nfunction RatingSkeleton() {\n  return (\n    <Skeleton.Root\n      className=\"flex w-[136px] items-center gap-1 group-has-[[data-pending]]/product-rating:animate-pulse\"\n      pending\n    >\n      <Skeleton.Box className=\"h-4 w-[100px] rounded-md\" />\n      <Skeleton.Box className=\"h-6 w-8 rounded-xl\" />\n    </Skeleton.Root>\n  );\n}\n\nfunction ProductSummarySkeleton() {\n  return (\n    <Skeleton.Root\n      className=\"flex w-full flex-col gap-3.5 pb-6 group-has-[[data-pending]]/product-summary:animate-pulse\"\n      pending\n    >\n      {Array.from({ length: 3 }).map((_, idx) => (\n        <Skeleton.Box className=\"h-2.5 w-full\" key={idx} />\n      ))}\n    </Skeleton.Root>\n  );\n}\n\nfunction ProductDescriptionSkeleton() {\n  return (\n    <Skeleton.Root\n      className=\"flex w-full flex-col gap-3.5 pb-6 group-has-[[data-pending]]/product-description:animate-pulse\"\n      pending\n    >\n      {Array.from({ length: 2 }).map((_, idx) => (\n        <Skeleton.Box className=\"h-2.5 w-full\" key={idx} />\n      ))}\n      <Skeleton.Box className=\"h-2.5 w-3/4\" />\n    </Skeleton.Root>\n  );\n}\n\nfunction ProductDetailFormSkeleton() {\n  return (\n    <Skeleton.Root\n      className=\"flex flex-col gap-8 py-8 group-has-[[data-pending]]/product-detail-form:animate-pulse\"\n      pending\n    >\n      <div className=\"flex flex-col gap-5\">\n        <Skeleton.Box className=\"h-2 w-10 rounded-md\" />\n        <div className=\"flex gap-2\">\n          {Array.from({ length: 3 }).map((_, idx) => (\n            <Skeleton.Box className=\"h-11 w-[72px] rounded-full\" key={idx} />\n          ))}\n        </div>\n      </div>\n      <div className=\"flex flex-col gap-5\">\n        <Skeleton.Box className=\"h-3 w-16 rounded-md\" />\n        <div className=\"flex gap-4\">\n          {Array.from({ length: 5 }).map((_, idx) => (\n            <Skeleton.Box className=\"h-10 w-10 rounded-full\" key={idx} />\n          ))}\n        </div>\n      </div>\n      <div className=\"flex gap-2\">\n        <Skeleton.Box className=\"h-12 w-[120px] rounded-lg\" />\n        <Skeleton.Box className=\"h-12 w-[216px] rounded-full\" />\n      </div>\n    </Skeleton.Root>\n  );\n}\n\nfunction ProductAccordionsSkeleton() {\n  return (\n    <Skeleton.Root\n      className=\"flex h-[600px] w-full flex-col gap-8 pt-4 group-has-[[data-pending]]/product-accordion:animate-pulse\"\n      pending\n    >\n      <div className=\"flex items-center justify-between\">\n        <Skeleton.Box className=\"h-2 w-20 rounded-sm\" />\n        <Skeleton.Box className=\"h-3 w-3 rounded-sm\" />\n      </div>\n      <div className=\"mb-1 flex flex-col gap-4\">\n        <Skeleton.Box className=\"h-3 w-full rounded-sm\" />\n        <Skeleton.Box className=\"h-3 w-full rounded-sm\" />\n        <Skeleton.Box className=\"h-3 w-3/5 rounded-sm\" />\n      </div>\n      <div className=\"flex items-center justify-between\">\n        <Skeleton.Box className=\"h-2 w-24 rounded-sm\" />\n        <Skeleton.Box className=\"h-3 w-3 rounded-full\" />\n      </div>\n      <div className=\"flex items-center justify-between\">\n        <Skeleton.Box className=\"h-2 w-20 rounded-sm\" />\n        <Skeleton.Box className=\"h-3 w-3 rounded-full\" />\n      </div>\n      <div className=\"flex items-center justify-between\">\n        <Skeleton.Box className=\"h-2 w-32 rounded-sm\" />\n        <Skeleton.Box className=\"h-3 w-3 rounded-full\" />\n      </div>\n    </Skeleton.Root>\n  );\n}\n\nexport function ProductDetailSkeleton() {\n  return (\n    <Skeleton.Root\n      className=\"grid grid-cols-1 items-stretch gap-x-6 gap-y-8 group-has-[[data-pending]]/product-detail:animate-pulse @2xl:grid-cols-2 @5xl:gap-x-12\"\n      pending\n    >\n      <div className=\"hidden @2xl:block\">\n        <ProductGallerySkeleton />\n      </div>\n      <div>\n        <Skeleton.Box className=\"mb-6 h-4 w-20 rounded-lg\" />\n        <Skeleton.Box className=\"mb-6 h-6 w-72 rounded-lg\" />\n        <RatingSkeleton />\n        <PriceLabelSkeleton />\n        <ProductSummarySkeleton />\n        <div className=\"mb-8 @2xl:hidden\">\n          <ProductGallerySkeleton />\n        </div>\n        <ProductDetailFormSkeleton />\n      </div>\n    </Skeleton.Root>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/product-detail/product-detail-form.tsx",
    "content": "'use client';\n\nimport {\n  FieldMetadata,\n  FormProvider,\n  FormStateInput,\n  getFormProps,\n  SubmissionResult,\n  useForm,\n  useInputControl,\n} from '@conform-to/react';\nimport { getZodConstraint, parseWithZod } from '@conform-to/zod';\nimport { clsx } from 'clsx';\nimport { useTranslations } from 'next-intl';\nimport { createSerializer, parseAsString, useQueryStates } from 'nuqs';\nimport { ReactNode, startTransition, useActionState, useCallback, useEffect, useMemo } from 'react';\nimport { useFormStatus } from 'react-dom';\nimport { z } from 'zod';\n\nimport { ButtonRadioGroup } from '@/vibes/soul/form/button-radio-group';\nimport { CardRadioGroup } from '@/vibes/soul/form/card-radio-group';\nimport { Checkbox } from '@/vibes/soul/form/checkbox';\nimport { DatePicker } from '@/vibes/soul/form/date-picker';\nimport { FormStatus } from '@/vibes/soul/form/form-status';\nimport { Input } from '@/vibes/soul/form/input';\nimport { NumberInput } from '@/vibes/soul/form/number-input';\nimport { RadioGroup } from '@/vibes/soul/form/radio-group';\nimport { Select } from '@/vibes/soul/form/select';\nimport { SwatchRadioGroup } from '@/vibes/soul/form/swatch-radio-group';\nimport { Textarea } from '@/vibes/soul/form/textarea';\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { toast } from '@/vibes/soul/primitives/toaster';\nimport { useEvents } from '~/components/analytics/events';\nimport { usePathname, useRouter } from '~/i18n/routing';\n\nimport { revalidateCart } from './actions/revalidate-cart';\nimport { Field, schema, SchemaRawShape } from './schema';\n\ntype Action<S, P> = (state: Awaited<S>, payload: P) => S | Promise<S>;\n\ninterface State<F extends Field> {\n  fields: F[];\n  lastResult: SubmissionResult | null;\n  successMessage?: ReactNode;\n}\n\nexport type ProductDetailFormAction<F extends Field> = Action<State<F>, FormData>;\n\nexport interface StockDisplayData {\n  stockLevelMessage?: string | null;\n  backorderAvailabilityPrompt?: string | null;\n}\n\nexport interface BackorderDisplayData {\n  availableOnHand: number;\n  availableForBackorder: number;\n  unlimitedBackorder: boolean;\n  showQuantityOnBackorder: boolean;\n  backorderMessage: string | null;\n}\n\nexport interface ProductDetailFormProps<F extends Field> {\n  fields: F[];\n  action: ProductDetailFormAction<F>;\n  productId: string;\n  ctaLabel?: string;\n  quantityLabel?: string;\n  incrementLabel?: string;\n  decrementLabel?: string;\n  emptySelectPlaceholder?: string;\n  ctaDisabled?: boolean;\n  prefetch?: boolean;\n  additionalActions?: ReactNode;\n  minQuantity?: number;\n  maxQuantity?: number;\n  stockDisplayData?: StockDisplayData;\n  backorderDisplayData?: BackorderDisplayData;\n}\n\nexport function ProductDetailForm<F extends Field>({\n  action,\n  fields,\n  productId,\n  ctaLabel = 'Add to cart',\n  quantityLabel = 'Quantity',\n  incrementLabel = 'Increase quantity',\n  decrementLabel = 'Decrease quantity',\n  emptySelectPlaceholder = 'Select an option',\n  ctaDisabled = false,\n  prefetch = false,\n  additionalActions,\n  minQuantity,\n  maxQuantity,\n  stockDisplayData,\n  backorderDisplayData,\n}: ProductDetailFormProps<F>) {\n  const router = useRouter();\n  const pathname = usePathname();\n  const events = useEvents();\n  const t = useTranslations('Product.ProductDetails');\n\n  const searchParams = fields.reduce<Record<string, typeof parseAsString>>((acc, field) => {\n    return field.persist === true ? { ...acc, [field.name]: parseAsString } : acc;\n  }, {});\n\n  const [params] = useQueryStates(searchParams, { shallow: false });\n\n  const onPrefetch = (fieldName: string, value: string) => {\n    if (prefetch) {\n      const serialize = createSerializer(searchParams);\n\n      const newUrl = serialize(pathname, { ...params, [fieldName]: value });\n\n      router.prefetch(newUrl);\n    }\n  };\n\n  const defaultValue = fields.reduce<{\n    [Key in keyof SchemaRawShape]?: z.infer<SchemaRawShape[Key]>;\n  }>(\n    (acc, field) => {\n      // Checkbox field has to be handled separately because we want to convert checked or unchecked value to true or undefined respectively.\n      // This is because the form expects a boolean value, but we want to store the checked or unchecked value in the query params.\n      if (field.type === 'checkbox') {\n        if (params[field.name] === field.checkedValue) {\n          return {\n            ...acc,\n            [field.name]: 'true',\n          };\n        }\n\n        if (params[field.name] === field.uncheckedValue) {\n          return {\n            ...acc,\n            [field.name]: undefined,\n          };\n        }\n\n        return {\n          ...acc,\n          [field.name]: field.defaultValue, // Default value is either 'true' or undefined\n        };\n      }\n\n      return {\n        ...acc,\n        [field.name]: params[field.name] ?? field.defaultValue,\n      };\n    },\n    { quantity: minQuantity ?? 1 },\n  );\n\n  const [{ lastResult, successMessage }, formAction] = useActionState(action, {\n    fields,\n    lastResult: null,\n  });\n\n  useEffect(() => {\n    if (lastResult?.status === 'success') {\n      toast.success(successMessage);\n\n      startTransition(async () => {\n        // This is needed to refresh the Data Cache after the product has been added to the cart.\n        // The cart id is not picked up after the first time the cart is created/updated.\n        await revalidateCart();\n      });\n    }\n  }, [lastResult, successMessage, router]);\n\n  const [form, formFields] = useForm({\n    lastResult,\n    constraint: getZodConstraint(schema(fields, minQuantity, maxQuantity)),\n    onValidate({ formData }) {\n      return parseWithZod(formData, { schema: schema(fields, minQuantity, maxQuantity) });\n    },\n    onSubmit(event, { formData }) {\n      event.preventDefault();\n\n      startTransition(() => {\n        formAction(formData);\n\n        events.onAddToCart?.(formData);\n      });\n    },\n    // @ts-expect-error: `defaultValue` types are conflicting with `onValidate`.\n    defaultValue,\n    shouldValidate: 'onSubmit',\n    shouldRevalidate: 'onInput',\n  });\n\n  const backorderMessages = useMemo(() => {\n    const {\n      availableForBackorder,\n      availableOnHand,\n      backorderMessage,\n      showQuantityOnBackorder,\n      unlimitedBackorder,\n    } = backorderDisplayData || { availableForBackorder: 0, availableOnHand: 0 };\n\n    if (!showQuantityOnBackorder && !backorderMessage) {\n      return undefined;\n    }\n\n    const orderQuantity = Number(formFields.quantity.value);\n\n    if (Number.isNaN(orderQuantity) || orderQuantity <= availableOnHand) {\n      return {\n        backorderQuantityMessage: undefined,\n        backorderInfoMessage: undefined,\n      };\n    }\n\n    if (!showQuantityOnBackorder) {\n      return {\n        backorderQuantityMessage: undefined,\n        backorderInfoMessage: backorderMessage ?? undefined,\n      };\n    }\n\n    return {\n      backorderQuantityMessage: t('backorderQuantity', {\n        quantity: unlimitedBackorder\n          ? orderQuantity - availableOnHand\n          : Math.min(orderQuantity - availableOnHand, availableForBackorder),\n      }),\n      backorderInfoMessage: backorderMessage ?? undefined,\n    };\n  }, [backorderDisplayData, formFields.quantity.value, t]);\n\n  const quantityControl = useInputControl(formFields.quantity);\n\n  return (\n    <FormProvider context={form.context}>\n      <FormStateInput />\n      <form {...getFormProps(form)} action={formAction}>\n        <input name=\"id\" type=\"hidden\" value={productId} />\n        <div className=\"space-y-6 pb-8\">\n          {fields.map((field) => {\n            return (\n              <FormField\n                emptySelectPlaceholder={emptySelectPlaceholder}\n                field={field}\n                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n                formField={formFields[field.name]!}\n                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n                key={formFields[field.name]!.id}\n                onPrefetch={onPrefetch}\n              />\n            );\n          })}\n          {form.errors?.map((error, index) => (\n            <FormStatus className=\"pt-3\" key={index} type=\"error\">\n              {error}\n            </FormStatus>\n          ))}\n\n          <div className=\"h-[3.2rem] sm:h-[2.6rem]\">\n            {!!stockDisplayData?.stockLevelMessage && (\n              <div\n                className={clsx(\n                  'flex flex-wrap justify-start gap-x-2.5 gap-y-2 text-sm text-[var(--product-detail-secondary-text,hsl(var(--contrast-500)))]',\n                  'transition-transform duration-200 ease-in-out',\n                  backorderMessages?.backorderQuantityMessage ||\n                    backorderMessages?.backorderInfoMessage\n                    ? 'translate-y-0'\n                    : 'translate-y-[calc(100%+4px)]',\n                )}\n              >\n                <div className=\"flex-none whitespace-nowrap font-semibold text-black\">\n                  {stockDisplayData.stockLevelMessage}\n                </div>\n                {!!stockDisplayData.backorderAvailabilityPrompt && (\n                  <div className=\"flex-none whitespace-nowrap border-s border-gray-300 pl-2.5\">\n                    {stockDisplayData.backorderAvailabilityPrompt}\n                  </div>\n                )}\n              </div>\n            )}\n            {!!backorderMessages && (\n              <div\n                className={clsx(\n                  'mt-1 flex flex-wrap justify-start gap-x-2.5 gap-y-2 text-sm text-[var(--product-detail-secondary-text,hsl(var(--contrast-500)))]',\n                  'ease-initial transition-opacity',\n                  backorderMessages.backorderQuantityMessage ||\n                    backorderMessages.backorderInfoMessage\n                    ? 'duration-400 opacity-100'\n                    : 'opacity-0 delay-0 duration-100',\n                )}\n              >\n                <div className=\"flex-none whitespace-nowrap font-semibold text-black\">\n                  {backorderMessages.backorderQuantityMessage}\n                </div>\n                {!!backorderMessages.backorderInfoMessage && (\n                  <div className=\"flex-none whitespace-nowrap border-s border-gray-300 pl-2.5\">\n                    {backorderMessages.backorderInfoMessage}\n                  </div>\n                )}\n              </div>\n            )}\n          </div>\n\n          <div className=\"flex gap-x-3\">\n            <NumberInput\n              aria-label={quantityLabel}\n              decrementLabel={decrementLabel}\n              incrementLabel={incrementLabel}\n              max={maxQuantity}\n              min={minQuantity ?? 1}\n              name={formFields.quantity.name}\n              onBlur={quantityControl.blur}\n              onChange={(e) => quantityControl.change(e.currentTarget.value)}\n              onFocus={quantityControl.focus}\n              required\n              value={quantityControl.value}\n            />\n            <SubmitButton disabled={ctaDisabled}>{ctaLabel}</SubmitButton>\n            {additionalActions}\n          </div>\n        </div>\n      </form>\n    </FormProvider>\n  );\n}\n\nfunction SubmitButton({ children, disabled }: { children: ReactNode; disabled?: boolean }) {\n  const { pending } = useFormStatus();\n\n  return (\n    <Button\n      className=\"w-auto @xl:w-56\"\n      disabled={disabled}\n      loading={pending}\n      size=\"medium\"\n      type=\"submit\"\n    >\n      {children}\n    </Button>\n  );\n}\n\n// eslint-disable-next-line complexity\nfunction FormField({\n  field,\n  formField,\n  onPrefetch,\n  emptySelectPlaceholder,\n}: {\n  field: Field;\n  formField: FieldMetadata<string | number | boolean | Date | undefined>;\n  onPrefetch: (fieldName: string, value: string) => void;\n  emptySelectPlaceholder?: string;\n}) {\n  const controls = useInputControl(formField);\n\n  const [, setParams] = useQueryStates(\n    field.persist === true ? { [field.name]: parseAsString.withOptions({ shallow: false }) } : {},\n  );\n\n  const handleChange = useCallback(\n    (value: string) => {\n      // Checkbox field has to be handled separately because we want to convert 'true' or '' to the checked or unchecked value respectively.\n      if (field.type === 'checkbox') {\n        void setParams({ [field.name]: value ? field.checkedValue : field.uncheckedValue });\n      } else {\n        void setParams({ [field.name]: value || null }); // Passing `null` to remove the value from the query params if fieldValue is falsey\n      }\n\n      controls.change(value || ''); // If fieldValue is falsey, we set it to an empty string\n    },\n    [setParams, field, controls],\n  );\n\n  const handleOnOptionMouseEnter = (value: string) => {\n    if (field.persist === true) {\n      onPrefetch(field.name, value);\n    }\n  };\n\n  switch (field.type) {\n    case 'number':\n      return (\n        <NumberInput\n          decrementLabel={field.decrementLabel}\n          errors={formField.errors}\n          incrementLabel={field.incrementLabel}\n          key={formField.id}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onChange={(e) => handleChange(e.currentTarget.value)}\n          onFocus={controls.focus}\n          required={formField.required}\n          value={controls.value ?? ''}\n        />\n      );\n\n    case 'text':\n      return (\n        <Input\n          errors={formField.errors}\n          key={formField.id}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onChange={(e) => handleChange(e.currentTarget.value)}\n          onFocus={controls.focus}\n          required={formField.required}\n          value={controls.value ?? ''}\n        />\n      );\n\n    case 'date':\n      return (\n        <DatePicker\n          defaultValue={controls.value}\n          errors={formField.errors}\n          key={formField.id}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onChange={(e) => handleChange(e.currentTarget.value)}\n          onFocus={controls.focus}\n          required={formField.required}\n        />\n      );\n\n    case 'textarea':\n      return (\n        <Textarea\n          errors={formField.errors}\n          key={formField.id}\n          label={field.label}\n          maxLength={field.maxLength}\n          minLength={field.minLength}\n          name={formField.name}\n          onBlur={controls.blur}\n          onChange={(e) => handleChange(e.currentTarget.value)}\n          onFocus={controls.focus}\n          required={formField.required}\n          value={controls.value ?? ''}\n        />\n      );\n\n    case 'checkbox':\n      return (\n        <Checkbox\n          checked={controls.value === 'true'}\n          errors={formField.errors}\n          key={formField.id}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onCheckedChange={(value) => handleChange(value ? 'true' : '')}\n          onFocus={controls.focus}\n          required={formField.required}\n          value={controls.value ?? ''}\n        />\n      );\n\n    case 'select':\n      return (\n        <Select\n          errors={formField.errors}\n          key={formField.id}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onFocus={controls.focus}\n          onOptionMouseEnter={handleOnOptionMouseEnter}\n          onValueChange={handleChange}\n          options={field.options}\n          placeholder={emptySelectPlaceholder}\n          required={formField.required}\n          value={controls.value ?? ''}\n        />\n      );\n\n    case 'radio-group':\n      return (\n        <RadioGroup\n          errors={formField.errors}\n          key={formField.id}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onFocus={controls.focus}\n          onOptionMouseEnter={handleOnOptionMouseEnter}\n          onValueChange={handleChange}\n          options={field.options}\n          required={formField.required}\n          value={controls.value ?? ''}\n        />\n      );\n\n    case 'swatch-radio-group':\n      return (\n        <SwatchRadioGroup\n          errors={formField.errors}\n          key={formField.id}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onFocus={controls.focus}\n          onOptionMouseEnter={handleOnOptionMouseEnter}\n          onValueChange={handleChange}\n          options={field.options}\n          required={formField.required}\n          value={controls.value ?? ''}\n        />\n      );\n\n    case 'card-radio-group':\n      return (\n        <CardRadioGroup\n          errors={formField.errors}\n          key={formField.id}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onFocus={controls.focus}\n          onOptionMouseEnter={handleOnOptionMouseEnter}\n          onValueChange={handleChange}\n          options={field.options}\n          required={formField.required}\n          value={controls.value ?? ''}\n        />\n      );\n\n    case 'button-radio-group':\n      return (\n        <ButtonRadioGroup\n          errors={formField.errors}\n          key={formField.id}\n          label={field.label}\n          name={formField.name}\n          onBlur={controls.blur}\n          onFocus={controls.focus}\n          onOptionMouseEnter={handleOnOptionMouseEnter}\n          onValueChange={handleChange}\n          options={field.options}\n          required={formField.required}\n          value={controls.value ?? ''}\n        />\n      );\n  }\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/product-detail/product-gallery.tsx",
    "content": "'use client';\n\nimport { clsx } from 'clsx';\nimport { EmblaCarouselType, EngineType } from 'embla-carousel';\nimport useEmblaCarousel from 'embla-carousel-react';\nimport { useTranslations } from 'next-intl';\nimport { startTransition, useCallback, useEffect, useRef, useState } from 'react';\n\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\nimport { Image } from '~/components/image';\n\nexport type ProductGalleryLoadMoreAction = (\n  productId: number,\n  cursor: string,\n  limit?: number,\n) => Promise<{\n  images: Array<{ src: string; alt: string }>;\n  pageInfo: { hasNextPage: boolean; endCursor: string | null };\n}>;\n\nexport interface ProductGalleryProps {\n  images: Array<{ alt: string; src: string }>;\n  className?: string;\n  thumbnailLabel?: string;\n  aspectRatio?:\n    | '1:1'\n    | '4:5'\n    | '5:4'\n    | '3:4'\n    | '4:3'\n    | '2:3'\n    | '3:2'\n    | '16:9'\n    | '9:16'\n    | '5:6'\n    | '6:5';\n  fit?: 'contain' | 'cover';\n  pageInfo?: { hasNextPage: boolean; endCursor: string | null };\n  productId?: number;\n  loadMoreAction?: ProductGalleryLoadMoreAction;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --product-gallery-focus: hsl(var(--primary));\n *   --product-gallery-image-background: hsl(var(--contrast-100));\n *   --product-gallery-image-border: hsl(var(--contrast-100));\n *   --product-gallery-image-border-active: hsl(var(--foreground));\n *   --product-gallery-load-more: hsl(var(--foreground));\n * }\n * ```\n */\nexport function ProductGallery({\n  images: initialImages,\n  className,\n  thumbnailLabel = 'View image number',\n  aspectRatio = '4:5',\n  fit = 'contain',\n  pageInfo: initialPageInfo,\n  productId,\n  loadMoreAction,\n}: ProductGalleryProps) {\n  const t = useTranslations('Product.ProductDetails');\n\n  const [images, setImages] = useState(initialImages);\n  const [pageInfo, setPageInfo] = useState(initialPageInfo);\n  const [hasMoreToLoad, setHasMoreToLoad] = useState(initialPageInfo?.hasNextPage ?? false);\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  const [isLoading, setIsLoading] = useState(false);\n  const [loadingStatus, setLoadingStatus] = useState('');\n\n  const scrollListenerRef = useRef<() => void>(() => undefined);\n  const listenForScrollRef = useRef(true);\n  const hasMoreToLoadRef = useRef(hasMoreToLoad);\n\n  const [emblaRef, emblaApi] = useEmblaCarousel();\n  const [emblaThumbsRef, emblaThumbsApi] = useEmblaCarousel({\n    containScroll: 'keepSnaps',\n    dragFree: true,\n  });\n\n  // Keep ref in sync with state\n  useEffect(() => {\n    hasMoreToLoadRef.current = hasMoreToLoad;\n  }, [hasMoreToLoad]);\n\n  const onThumbClick = useCallback(\n    (index: number) => {\n      if (!emblaApi || !emblaThumbsApi) return;\n      emblaApi.goTo(index);\n    },\n    [emblaApi, emblaThumbsApi],\n  );\n\n  const onSelect = useCallback(() => {\n    if (!emblaApi || !emblaThumbsApi) return;\n    setSelectedIndex(emblaApi.selectedSnap());\n\n    emblaThumbsApi.goTo(emblaApi.selectedSnap());\n  }, [emblaApi, emblaThumbsApi]);\n\n  useEffect(() => {\n    if (!emblaApi) return;\n    onSelect();\n    emblaApi.on('select', onSelect);\n\n    return () => {\n      emblaApi.off('select', onSelect);\n    };\n  }, [emblaApi, onSelect]);\n\n  const onSlideChanges = useCallback((carouselApi: EmblaCarouselType) => {\n    const reloadEmbla = (): void => {\n      const oldEngine = carouselApi.internalEngine();\n\n      carouselApi.reInit();\n\n      const newEngine = carouselApi.internalEngine();\n      const copyEngineModules: Array<keyof EngineType> = [\n        'scrollBody',\n        'location',\n        'offsetLocation',\n        'previousLocation',\n        'target',\n      ];\n\n      copyEngineModules.forEach((engineModule) => {\n        Object.assign(newEngine[engineModule], oldEngine[engineModule]);\n      });\n\n      newEngine.translate.to(oldEngine.location.get());\n\n      const { index } = newEngine.scrollTarget.byDistance(0, false);\n\n      newEngine.indexCurrent.set(index);\n      newEngine.animation.start();\n\n      listenForScrollRef.current = true;\n    };\n\n    const reloadAfterPointerUp = (): void => {\n      carouselApi.off('pointerup', reloadAfterPointerUp);\n      reloadEmbla();\n    };\n\n    const engine = carouselApi.internalEngine();\n\n    if (hasMoreToLoadRef.current && engine.dragHandler.pointerDown()) {\n      const boundsActive = engine.limit.pastMaxBound(engine.target.get());\n\n      engine.scrollBounds.toggleActive(boundsActive);\n      carouselApi.on('pointerup', reloadAfterPointerUp);\n    } else {\n      reloadEmbla();\n    }\n  }, []);\n\n  const loadMore = useCallback(\n    (thumbsApi: EmblaCarouselType) => {\n      const endCursor = pageInfo?.endCursor;\n\n      if (!loadMoreAction || !productId || !endCursor || isLoading) return;\n\n      listenForScrollRef.current = false;\n      setIsLoading(true);\n      setLoadingStatus(t('loadingMoreImages'));\n\n      startTransition(async () => {\n        const result = await loadMoreAction(productId, endCursor);\n\n        if (!result.pageInfo.hasNextPage) {\n          setHasMoreToLoad(false);\n          thumbsApi.off('scroll', scrollListenerRef.current);\n        }\n\n        setImages((prev) => [...prev, ...result.images]);\n        setPageInfo(result.pageInfo);\n        setIsLoading(false);\n        setLoadingStatus(t('imagesLoaded', { count: result.images.length }));\n      });\n    },\n    [loadMoreAction, productId, pageInfo?.endCursor, isLoading, t],\n  );\n\n  const onThumbsScroll = useCallback(\n    (thumbsApi: EmblaCarouselType) => {\n      if (!listenForScrollRef.current) return;\n\n      const slideCount = thumbsApi.slideNodes().length;\n      const lastSlideIndex = slideCount - 1;\n      const secondLastSlideIndex = slideCount - 2;\n      const slidesInView = thumbsApi.slidesInView();\n\n      // Trigger when last or second-to-last thumbnail is in view\n      const shouldLoadMore =\n        slidesInView.includes(lastSlideIndex) || slidesInView.includes(secondLastSlideIndex);\n\n      if (shouldLoadMore) {\n        loadMore(thumbsApi);\n      }\n    },\n    [loadMore],\n  );\n\n  const addThumbsScrollListener = useCallback(\n    (thumbsApi: EmblaCarouselType) => {\n      scrollListenerRef.current = () => onThumbsScroll(thumbsApi);\n      thumbsApi.on('scroll', scrollListenerRef.current);\n    },\n    [onThumbsScroll],\n  );\n\n  useEffect(() => {\n    if (!emblaThumbsApi) return;\n\n    addThumbsScrollListener(emblaThumbsApi);\n\n    const onResize = () => emblaThumbsApi.reInit();\n\n    window.addEventListener('resize', onResize);\n    emblaThumbsApi.on('destroy', () => window.removeEventListener('resize', onResize));\n    emblaThumbsApi.on('slideschanged', onSlideChanges);\n\n    return () => {\n      emblaThumbsApi.off('scroll', scrollListenerRef.current);\n      emblaThumbsApi.off('slideschanged', onSlideChanges);\n    };\n  }, [emblaThumbsApi, addThumbsScrollListener, onSlideChanges]);\n\n  return (\n    <div className={clsx('sticky top-4 flex flex-col gap-2', className)}>\n      <div aria-live=\"polite\" className=\"sr-only\" role=\"status\">\n        {loadingStatus}\n      </div>\n      <div className=\"w-full overflow-hidden rounded-xl @xl:rounded-2xl\" ref={emblaRef}>\n        <div className=\"flex\">\n          {images.map((image, idx) => (\n            <div\n              className={clsx(\n                'relative w-full shrink-0 grow-0 basis-full',\n                {\n                  '5:6': 'aspect-[5/6]',\n                  '3:4': 'aspect-[3/4]',\n                  '4:5': 'aspect-[4/5]',\n                  '3:2': 'aspect-[3/2]',\n                  '2:3': 'aspect-[2/3]',\n                  '16:9': 'aspect-[16/9]',\n                  '9:16': 'aspect-[9/16]',\n                  '6:5': 'aspect-[6/5]',\n                  '5:4': 'aspect-[5/4]',\n                  '4:3': 'aspect-[4/3]',\n                  '1:1': 'aspect-square',\n                }[aspectRatio],\n              )}\n              key={idx}\n            >\n              <Image\n                alt={image.alt}\n                className={clsx(\n                  'bg-[var(--product-gallery-image-background,hsl(var(--contrast-100)))]',\n                  {\n                    contain: 'object-contain',\n                    cover: 'object-cover',\n                  }[fit],\n                )}\n                fill\n                preload={idx === 0}\n                sizes=\"(min-width: 42rem) 50vw, 100vw\"\n                src={image.src}\n              />\n            </div>\n          ))}\n        </div>\n      </div>\n\n      <div className=\"flex max-w-full shrink-0 flex-col gap-2\">\n        <div className=\"overflow-hidden\" ref={emblaThumbsRef}>\n          <div className=\"flex flex-row gap-2 p-1\">\n            {images.map((image, index) => (\n              <button\n                aria-label={`${thumbnailLabel} ${index + 1}`}\n                className={clsx(\n                  'relative h-12 w-12 shrink-0 overflow-hidden rounded-lg border transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--product-gallery-focus,hsl(var(--primary)))] focus-visible:ring-offset-2 @md:h-16 @md:w-16',\n                  index === selectedIndex\n                    ? 'border-[var(--product-gallery-image-border-active,hsl(var(--foreground)))]'\n                    : 'border-transparent',\n                )}\n                key={index}\n                onClick={() => onThumbClick(index)}\n                type=\"button\"\n              >\n                <div\n                  className={clsx(\n                    index === selectedIndex ? 'opacity-100' : 'opacity-50',\n                    'transition-all duration-300 hover:opacity-100',\n                  )}\n                >\n                  <Image\n                    alt={image.alt}\n                    className=\"bg-[var(--product-gallery-image-background,hsl(var(--contrast-100)))] object-cover\"\n                    fill\n                    sizes=\"(min-width: 28rem) 4rem, 3rem\"\n                    src={image.src}\n                  />\n                </div>\n              </button>\n            ))}\n            {hasMoreToLoad && (\n              <div className=\"flex animate-pulse gap-2\">\n                <Skeleton.Box className=\"h-12 w-12 shrink-0 rounded-lg @md:h-16 @md:w-16\" />\n                <Skeleton.Box className=\"h-12 w-12 shrink-0 rounded-lg @md:h-16 @md:w-16\" />\n                <Skeleton.Box className=\"h-12 w-12 shrink-0 rounded-lg @md:h-16 @md:w-16\" />\n                <Skeleton.Box className=\"h-12 w-12 shrink-0 rounded-lg @md:h-16 @md:w-16\" />\n                <Skeleton.Box className=\"h-12 w-12 shrink-0 rounded-lg @md:h-16 @md:w-16\" />\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/product-detail/rating-link.tsx",
    "content": "'use client';\n\nimport { Rating, type Props as RatingProps } from '@/vibes/soul/primitives/rating';\n\ninterface Props extends RatingProps {\n  scrollTargetId: string;\n}\n\nexport function RatingLink({ scrollTargetId, ...ratingProps }: Props) {\n  const handleClick = () => {\n    const element = document.getElementById(scrollTargetId);\n\n    if (element) {\n      element.scrollIntoView({ behavior: 'smooth', block: 'start' });\n    }\n  };\n\n  return (\n    <button\n      aria-label=\"Scroll to reviews\"\n      className=\"cursor-pointer text-left\"\n      onClick={handleClick}\n      type=\"button\"\n    >\n      <Rating {...ratingProps} />\n    </button>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/product-detail/schema.ts",
    "content": "import { z } from 'zod';\n\ninterface FormField {\n  name: string;\n  label: string;\n  errors?: string[];\n  required?: boolean;\n  persist?: boolean;\n}\n\ntype RadioField = {\n  type: 'radio-group';\n  options: Array<{ label: string; value: string }>;\n  defaultValue?: string;\n} & FormField;\n\ntype SelectField = {\n  type: 'select';\n  options: Array<{ label: string; value: string }>;\n  defaultValue?: string;\n} & FormField;\n\ntype CheckboxField = {\n  type: 'checkbox';\n  defaultValue?: string;\n  checkedValue: string;\n  uncheckedValue: string;\n} & FormField;\n\ntype NumberInputField = {\n  type: 'number';\n  defaultValue?: string;\n  min?: number;\n  max?: number;\n  incrementLabel?: string;\n  decrementLabel?: string;\n} & FormField;\n\ntype TextInputField = {\n  type: 'text';\n  defaultValue?: string;\n  pattern?: string;\n} & FormField;\n\ntype TextAreaField = {\n  type: 'textarea';\n  defaultValue?: string;\n  pattern?: string;\n  minLength?: number;\n  maxLength?: number;\n} & FormField;\n\ntype DateField = {\n  type: 'date';\n  defaultValue?: string;\n  pattern?: string;\n} & FormField;\n\ntype SwatchRadioFieldOption =\n  | {\n      type: 'color';\n      value: string;\n      label: string;\n      color: string;\n      disabled?: boolean;\n    }\n  | {\n      type: 'image';\n      value: string;\n      label: string;\n      image: { src: string; alt: string };\n      disabled?: boolean;\n    };\n\ntype SwatchRadioField = {\n  type: 'swatch-radio-group';\n  defaultValue?: string;\n  options: SwatchRadioFieldOption[];\n} & FormField;\n\ntype CardRadioField = {\n  type: 'card-radio-group';\n  defaultValue?: string;\n  options: Array<{\n    value: string;\n    label: string;\n    image?: { src: string; alt: string };\n    disabled?: boolean;\n  }>;\n} & FormField;\n\ntype ButtonRadioField = {\n  type: 'button-radio-group';\n  defaultValue?: string;\n  pattern?: string;\n  options: Array<{\n    value: string;\n    label: string;\n    disabled?: boolean;\n  }>;\n} & FormField;\n\nexport type Field =\n  | RadioField\n  | CheckboxField\n  | NumberInputField\n  | TextInputField\n  | TextAreaField\n  | DateField\n  | SwatchRadioField\n  | CardRadioField\n  | ButtonRadioField\n  | SelectField;\n\nexport interface SchemaRawShape {\n  [key: string]:\n    | z.ZodString\n    | z.ZodOptional<z.ZodString>\n    | z.ZodNumber\n    | z.ZodOptional<z.ZodNumber>;\n  id: z.ZodString;\n  quantity: z.ZodNumber;\n}\n\nexport function schema(\n  fields: Field[],\n  minQuantity?: number,\n  maxQuantity?: number,\n): z.ZodObject<SchemaRawShape> {\n  let quantitySchema = z.number().min(minQuantity ?? 1);\n\n  if (maxQuantity != null) {\n    quantitySchema = quantitySchema.max(maxQuantity);\n  }\n\n  const shape: SchemaRawShape = {\n    id: z.string(),\n    quantity: quantitySchema,\n  };\n\n  fields.forEach((field) => {\n    let fieldSchema: z.ZodString | z.ZodNumber;\n\n    switch (field.type) {\n      case 'number':\n        fieldSchema = z.number();\n\n        if (field.min != null) fieldSchema = fieldSchema.min(field.min);\n        if (field.max != null) fieldSchema = fieldSchema.max(field.max);\n\n        shape[field.name] = fieldSchema;\n        break;\n\n      case 'textarea':\n        fieldSchema = z.string();\n        if (field.minLength != null) fieldSchema = fieldSchema.min(field.minLength);\n        if (field.maxLength != null) fieldSchema = fieldSchema.max(field.maxLength);\n\n        shape[field.name] = fieldSchema;\n        break;\n\n      default:\n        fieldSchema = z.string();\n\n        shape[field.name] = fieldSchema;\n        break;\n    }\n\n    if (field.required !== true) shape[field.name] = fieldSchema.optional();\n  });\n\n  return z.object(shape);\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/product-list/index.tsx",
    "content": "import { clsx } from 'clsx';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { CompareDrawer, CompareDrawerProvider } from '@/vibes/soul/primitives/compare-drawer';\nimport {\n  type Product,\n  ProductCard,\n  ProductCardSkeleton,\n} from '@/vibes/soul/primitives/product-card';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\n\ninterface ProductListProps {\n  products: Streamable<Product[]>;\n  showRating?: boolean;\n  compareProducts?: Streamable<Product[]>;\n  className?: string;\n  colorScheme?: 'light' | 'dark';\n  aspectRatio?: '5:6' | '3:4' | '1:1';\n  showCompare?: Streamable<boolean>;\n  compareHref?: string;\n  compareLabel?: Streamable<string>;\n  compareParamName?: string;\n  emptyStateTitle?: Streamable<string>;\n  emptyStateSubtitle?: Streamable<string>;\n  placeholderCount?: number;\n  removeLabel?: Streamable<string>;\n  maxItems?: number;\n  maxCompareLimitMessage?: Streamable<string>;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --product-list-light-empty-title: hsl(var(--foreground));\n *   --product-list-light-empty-subtitle: hsl(var(--contrast-500));\n *   --product-list-dark-empty-title: hsl(var(--background));\n *   --product-list-dark-empty-subtitle: hsl(var(--contrast-100));\n *   --product-list-empty-state-title-font-family: var(--font-family-heading);\n *   --product-list-empty-state-subtitle-font-family: var(--font-family-body);\n * }\n * ```\n */\nexport function ProductList({\n  products: streamableProducts,\n  showRating,\n  className,\n  colorScheme = 'light',\n  aspectRatio = '5:6',\n  showCompare: streamableShowCompare = true,\n  compareHref,\n  compareProducts: streamableCompareProducts = [],\n  compareLabel: streamableCompareLabel = 'Compare',\n  compareParamName = 'compare',\n  emptyStateTitle = 'No products found',\n  emptyStateSubtitle = 'Try browsing our complete catalog of products.',\n  placeholderCount = 8,\n  removeLabel: streamableRemoveLabel,\n  maxItems,\n  maxCompareLimitMessage: streamableMaxCompareLimitMessage,\n}: ProductListProps) {\n  return (\n    <Stream\n      fallback={<ProductListSkeleton placeholderCount={placeholderCount} />}\n      value={Streamable.all([\n        streamableProducts,\n        streamableCompareLabel,\n        streamableShowCompare,\n        streamableCompareProducts,\n        streamableRemoveLabel,\n        streamableMaxCompareLimitMessage,\n      ])}\n    >\n      {([\n        products,\n        compareLabel,\n        showCompare,\n        compareProducts,\n        removeLabel,\n        maxCompareLimitMessage,\n      ]) => {\n        if (products.length === 0) {\n          return (\n            <ProductListEmptyState\n              emptyStateSubtitle={emptyStateSubtitle}\n              emptyStateTitle={emptyStateTitle}\n              placeholderCount={placeholderCount}\n            />\n          );\n        }\n\n        return (\n          <CompareDrawerProvider\n            items={compareProducts}\n            maxCompareLimitMessage={maxCompareLimitMessage}\n            maxItems={maxItems}\n          >\n            <div className={clsx('w-full @container', className)}>\n              <div className=\"mx-auto grid grid-cols-1 gap-x-4 gap-y-6 @sm:grid-cols-2 @2xl:grid-cols-3 @2xl:gap-x-5 @2xl:gap-y-8 @5xl:grid-cols-4 @7xl:grid-cols-5\">\n                {products.map((product) => (\n                  <ProductCard\n                    aspectRatio={aspectRatio}\n                    colorScheme={colorScheme}\n                    compareLabel={compareLabel}\n                    compareParamName={compareParamName}\n                    imageSizes=\"(min-width: 80rem) 20vw, (min-width: 64rem) 25vw, (min-width: 42rem) 33vw, (min-width: 24rem) 50vw, 100vw\"\n                    key={product.id}\n                    product={product}\n                    showCompare={showCompare}\n                    showRating={showRating}\n                  />\n                ))}\n              </div>\n            </div>\n            {showCompare && compareProducts.length > 0 && (\n              <CompareDrawer\n                href={compareHref}\n                paramName={compareParamName}\n                removeLabel={removeLabel}\n                submitLabel={compareLabel}\n              />\n            )}\n          </CompareDrawerProvider>\n        );\n      }}\n    </Stream>\n  );\n}\n\nexport function ProductListSkeleton({\n  className,\n  placeholderCount = 8,\n}: Pick<ProductListProps, 'className' | 'placeholderCount'>) {\n  return (\n    <Skeleton.Root\n      className={clsx('group-has-data-pending/product-list:animate-pulse', className)}\n      pending\n    >\n      <div className=\"mx-auto grid grid-cols-1 gap-x-4 gap-y-6 @sm:grid-cols-2 @2xl:grid-cols-3 @2xl:gap-x-5 @2xl:gap-y-8 @5xl:grid-cols-4 @7xl:grid-cols-5\">\n        {Array.from({ length: placeholderCount }).map((_, index) => (\n          <ProductCardSkeleton key={index} />\n        ))}\n      </div>\n    </Skeleton.Root>\n  );\n}\n\nexport function ProductListEmptyState({\n  className,\n  placeholderCount = 8,\n  emptyStateTitle,\n  emptyStateSubtitle,\n}: Pick<\n  ProductListProps,\n  'className' | 'placeholderCount' | 'emptyStateTitle' | 'emptyStateSubtitle'\n>) {\n  return (\n    <Skeleton.Root className={clsx('relative', className)}>\n      <div\n        className={clsx(\n          'mx-auto grid grid-cols-1 gap-x-4 gap-y-6 [mask-image:linear-gradient(to_bottom,_black_0%,_transparent_90%)] @sm:grid-cols-2 @2xl:grid-cols-3 @2xl:gap-x-5 @2xl:gap-y-8 @5xl:grid-cols-4 @7xl:grid-cols-5',\n        )}\n      >\n        {Array.from({ length: placeholderCount }).map((_, index) => (\n          <ProductCardSkeleton key={index} />\n        ))}\n      </div>\n      <div className=\"absolute inset-0 mx-auto px-3 py-16 pb-3 @4xl:px-10 @4xl:pb-10 @4xl:pt-28\">\n        <div className=\"mx-auto max-w-xl space-y-2 text-center @4xl:space-y-3\">\n          <h3 className=\"font-[family-name:var(--product-list-empty-state-title-font-family,var(--font-family-heading))] text-2xl leading-tight text-[var(--product-list-empty-state-title,hsl(var(--foreground)))] @4xl:text-4xl @4xl:leading-none\">\n            {emptyStateTitle}\n          </h3>\n          <p className=\"font-[family-name:var(--product-list-empty-state-subtitle-font-family,var(--font-family-body))] text-sm text-[var(--product-list-empty-state-subtitle,hsl(var(--contrast-500)))] @4xl:text-lg\">\n            {emptyStateSubtitle}\n          </p>\n        </div>\n      </div>\n    </Skeleton.Root>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/products-list-section/filter-parsers.ts",
    "content": "import { parseAsArrayOf, parseAsInteger, parseAsString, ParserBuilder } from 'nuqs/server';\n\nimport { Filter } from './filters-panel';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function getFilterParsers(filters: Filter[]): Record<string, ParserBuilder<any>> {\n  return filters\n    .filter((filter) => filter.type !== 'link-group')\n    .reduce((acc, filter) => {\n      switch (filter.type) {\n        case 'range':\n          return {\n            ...acc,\n            [filter.minParamName]: parseAsInteger,\n            [filter.maxParamName]: parseAsInteger,\n          };\n\n        case 'toggle-group':\n          return {\n            ...acc,\n            [filter.paramName]: parseAsArrayOf(parseAsString),\n          };\n\n        case 'rating':\n          return {\n            ...acc,\n            [filter.paramName]: parseAsArrayOf(parseAsString),\n          };\n\n        default:\n          return {\n            ...acc,\n          };\n      }\n    }, {});\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/products-list-section/filters-panel.tsx",
    "content": "/* eslint-disable @typescript-eslint/no-unsafe-member-access */\n/* eslint-disable @typescript-eslint/no-unsafe-call */\n/* eslint-disable @typescript-eslint/no-unsafe-argument */\n/* eslint-disable @typescript-eslint/no-unsafe-assignment */\n'use client';\n\nimport { clsx } from 'clsx';\nimport { parseAsString, useQueryStates } from 'nuqs';\nimport { useOptimistic, useState, useTransition } from 'react';\n\nimport { Checkbox } from '@/vibes/soul/form/checkbox';\nimport { RangeInput } from '@/vibes/soul/form/range-input';\nimport { ToggleGroup } from '@/vibes/soul/form/toggle-group';\nimport { Stream, Streamable, useStreamable } from '@/vibes/soul/lib/streamable';\nimport { Accordion, AccordionItem } from '@/vibes/soul/primitives/accordion';\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination';\nimport { Rating } from '@/vibes/soul/primitives/rating';\nimport { Link } from '~/components/link';\n\nimport { getFilterParsers } from './filter-parsers';\n\nexport interface LinkGroupFilter {\n  type: 'link-group';\n  label: string;\n  links: Array<{ label: string; href: string }>;\n}\n\nexport interface ToggleGroupFilter {\n  type: 'toggle-group';\n  paramName: string;\n  label: string;\n  options: Array<{ label: string; value: string; disabled?: boolean }>;\n}\n\nexport interface RatingFilter {\n  type: 'rating';\n  paramName: string;\n  label: string;\n  disabled?: boolean;\n}\n\nexport interface RangeFilter {\n  type: 'range';\n  label: string;\n  minParamName: string;\n  maxParamName: string;\n  min?: number;\n  max?: number;\n  minLabel?: string;\n  maxLabel?: string;\n  minPrepend?: React.ReactNode;\n  maxPrepend?: React.ReactNode;\n  minPlaceholder?: string;\n  maxPlaceholder?: string;\n  disabled?: boolean;\n}\n\nexport type Filter = ToggleGroupFilter | RangeFilter | RatingFilter | LinkGroupFilter;\n\ninterface Props {\n  className?: string;\n  filters: Streamable<Filter[]>;\n  resetFiltersLabel?: Streamable<string>;\n  paginationInfo?: Streamable<CursorPaginationInfo>;\n  rangeFilterApplyLabel?: Streamable<string>;\n}\n\ntype InnerProps = Props & { filters: Filter[] };\n\nfunction getParamCountLabel(params: Record<string, string | null | string[]>, key: string) {\n  const value = params[key];\n\n  if (Array.isArray(value) && value.length > 0) return `(${value.length})`;\n\n  return '';\n}\n\nexport function FiltersPanel({\n  className,\n  filters: streamableFilters,\n  resetFiltersLabel,\n  rangeFilterApplyLabel,\n}: Props) {\n  return (\n    <Stream fallback={<FiltersSkeleton />} value={streamableFilters}>\n      {(filters) => (\n        <FiltersPanelInner\n          className={className}\n          filters={filters}\n          rangeFilterApplyLabel={rangeFilterApplyLabel}\n          resetFiltersLabel={resetFiltersLabel}\n        />\n      )}\n    </Stream>\n  );\n}\n\nexport function FiltersPanelInner({\n  className,\n  filters,\n  resetFiltersLabel: streamableResetFiltersLabel,\n  rangeFilterApplyLabel: streamableRangeFilterApplyLabel,\n  paginationInfo: streamablePaginationInfo,\n}: InnerProps) {\n  const resetFiltersLabel = useStreamable(streamableResetFiltersLabel) ?? 'Reset filters';\n  const rangeFilterApplyLabel = useStreamable(streamableRangeFilterApplyLabel);\n  const paginationInfo = useStreamable(streamablePaginationInfo);\n  const startCursorParamName = paginationInfo?.startCursorParamName ?? 'before';\n  const endCursorParamName = paginationInfo?.endCursorParamName ?? 'after';\n  const [params, setParams] = useQueryStates(\n    {\n      ...getFilterParsers(filters),\n      [startCursorParamName]: parseAsString,\n      [endCursorParamName]: parseAsString,\n    },\n    {\n      shallow: false,\n      history: 'push',\n    },\n  );\n  const [isPending, startTransition] = useTransition();\n  const [optimisticParams, setOptimisticParams] = useOptimistic(params);\n  const [expandedItems, setExpandedItems] = useState(() => {\n    const initial = new Set<string>();\n\n    filters\n      .filter((filter) => filter.type !== 'link-group')\n      .slice(0, 3)\n      .forEach((filter) => {\n        initial.add(filter.label.toLowerCase());\n      });\n\n    return initial;\n  });\n\n  const accordionItems = filters\n    .filter((filter) => filter.type !== 'link-group')\n    .map((filter) => {\n      return {\n        key: filter.label.toLowerCase(),\n        value: filter.label.toLowerCase(),\n        filter,\n        expanded: expandedItems.has(filter.label.toLowerCase()),\n      };\n    });\n\n  if (filters.length === 0) return null;\n\n  const linkGroupFilters = filters.filter(\n    (filter): filter is LinkGroupFilter => filter.type === 'link-group',\n  );\n\n  return (\n    <div className={clsx('space-y-5', className)} data-pending={isPending ? true : null}>\n      {linkGroupFilters.map((linkGroup, index) => (\n        <div key={index.toString()}>\n          <h3 className=\"py-4 font-mono text-sm uppercase text-contrast-400\">{linkGroup.label}</h3>\n          <ul>\n            {linkGroup.links.map((link, linkIndex) => (\n              <li className=\"py-2\" key={linkIndex.toString()}>\n                <Link\n                  className=\"font-body text-base font-medium text-contrast-500 transition-colors duration-300 ease-out hover:text-foreground\"\n                  href={link.href}\n                >\n                  {link.label}\n                </Link>\n              </li>\n            ))}\n          </ul>\n        </div>\n      ))}\n      <Accordion\n        onValueChange={(items) => {\n          setExpandedItems(new Set(items));\n        }}\n        type=\"multiple\"\n        value={accordionItems.filter((item) => item.expanded).map((item) => item.value)}\n      >\n        {accordionItems.map((accordionItem) => {\n          const { key, value, filter } = accordionItem;\n\n          switch (filter.type) {\n            case 'toggle-group':\n              return (\n                <AccordionItem\n                  key={key}\n                  title={`${filter.label}${getParamCountLabel(optimisticParams, filter.paramName)}`}\n                  value={value}\n                >\n                  <ToggleGroup\n                    onValueChange={(toggleGroupValues) => {\n                      startTransition(async () => {\n                        const nextParams = {\n                          ...optimisticParams,\n                          [startCursorParamName]: null,\n                          [endCursorParamName]: null,\n                          [filter.paramName]:\n                            toggleGroupValues.length === 0 ? null : toggleGroupValues,\n                        };\n\n                        setOptimisticParams(nextParams);\n                        await setParams(nextParams);\n                      });\n                    }}\n                    options={filter.options}\n                    type=\"multiple\"\n                    value={optimisticParams[filter.paramName] ?? []}\n                  />\n                </AccordionItem>\n              );\n\n            case 'range':\n              return (\n                <AccordionItem key={key} title={filter.label} value={value}>\n                  <RangeInput\n                    applyLabel={rangeFilterApplyLabel}\n                    disabled={filter.disabled}\n                    max={filter.max}\n                    maxLabel={filter.maxLabel}\n                    maxName={filter.maxParamName}\n                    maxPlaceholder={filter.maxPlaceholder}\n                    maxPrepend={filter.maxPrepend}\n                    min={filter.min}\n                    minLabel={filter.minLabel}\n                    minName={filter.minParamName}\n                    minPlaceholder={filter.minPlaceholder}\n                    minPrepend={filter.minPrepend}\n                    onChange={({ min, max }) => {\n                      startTransition(async () => {\n                        const nextParams = {\n                          ...optimisticParams,\n                          [filter.minParamName]: min,\n                          [filter.maxParamName]: max,\n                          [startCursorParamName]: null,\n                          [endCursorParamName]: null,\n                        };\n\n                        setOptimisticParams(nextParams);\n                        await setParams(nextParams);\n                      });\n                    }}\n                    value={{\n                      min: optimisticParams[filter.minParamName] ?? null,\n                      max: optimisticParams[filter.maxParamName] ?? null,\n                    }}\n                  />\n                </AccordionItem>\n              );\n\n            case 'rating':\n              return (\n                <AccordionItem key={key} title={filter.label} value={value}>\n                  <div className=\"space-y-3\">\n                    {[5, 4, 3, 2, 1].map((rating) => (\n                      <Checkbox\n                        checked={\n                          optimisticParams[filter.paramName]?.includes(rating.toString()) ?? false\n                        }\n                        disabled={filter.disabled}\n                        key={rating}\n                        label={<Rating rating={rating} showRating={false} />}\n                        onCheckedChange={(checked) =>\n                          startTransition(async () => {\n                            const ratings = new Set(optimisticParams[filter.paramName]);\n\n                            if (checked === true) ratings.add(rating.toString());\n                            else ratings.delete(rating.toString());\n\n                            const nextParams = {\n                              ...optimisticParams,\n                              [filter.paramName]: Array.from(ratings),\n                              [startCursorParamName]: null,\n                              [endCursorParamName]: null,\n                            };\n\n                            setOptimisticParams(nextParams);\n                            await setParams(nextParams);\n                          })\n                        }\n                      />\n                    ))}\n                  </div>\n                </AccordionItem>\n              );\n\n            default:\n              return null;\n          }\n        })}\n      </Accordion>\n\n      <Button\n        onClick={() => {\n          startTransition(async () => {\n            const nextParams = {\n              ...Object.fromEntries(Object.entries(optimisticParams).map(([key]) => [key, null])),\n              [startCursorParamName]: optimisticParams[startCursorParamName],\n              [endCursorParamName]: optimisticParams[endCursorParamName],\n            };\n\n            setOptimisticParams(nextParams);\n            await setParams(nextParams);\n          });\n        }}\n        size=\"small\"\n        variant=\"secondary\"\n      >\n        {resetFiltersLabel}\n      </Button>\n    </div>\n  );\n}\n\nexport function FiltersSkeleton() {\n  return (\n    <div className=\"space-y-5\">\n      <AccordionSkeleton>\n        <ToggleGroupSkeleton options={4} seed={2} />\n      </AccordionSkeleton>\n      <AccordionSkeleton>\n        <ToggleGroupSkeleton options={3} seed={1} />\n      </AccordionSkeleton>\n      <AccordionSkeleton>\n        <RangeSkeleton />\n      </AccordionSkeleton>\n      {/* Reset Filters Button */}\n      <div className=\"h-10 w-[10ch] animate-pulse rounded-full bg-contrast-100\" />\n    </div>\n  );\n}\n\nfunction AccordionSkeleton({ children }: { children: React.ReactNode }) {\n  return (\n    <div>\n      <div className=\"items-start py-3 font-mono text-sm uppercase last:flex @md:py-4\">\n        <div className=\"inline-flex h-[1lh] items-center\">\n          <div className=\"h-2 w-[10ch] flex-1 animate-pulse rounded-sm bg-contrast-100\" />\n        </div>\n      </div>\n      <div className=\"pb-5\">{children}</div>\n    </div>\n  );\n}\n\nfunction ToggleGroupSkeleton({ options, seed = 0 }: { options: number; seed?: number }) {\n  return (\n    <div className=\"flex flex-wrap gap-2\">\n      {Array.from({ length: options }, (_, i) => {\n        const width = Math.floor(((i * 3 + 7 + seed) % 8) + 6);\n\n        return (\n          <div\n            className=\"h-12 w-[var(--width)] animate-pulse rounded-full bg-contrast-100 px-4\"\n            key={i}\n            // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n            style={{ '--width': `${width}ch` } as React.CSSProperties}\n          />\n        );\n      })}\n    </div>\n  );\n}\n\nfunction RangeSkeleton() {\n  return (\n    <div className=\"flex items-center gap-2\">\n      <div className=\"h-12 w-[10ch] animate-pulse rounded-lg bg-contrast-100\" />\n      <div className=\"h-12 w-[10ch] animate-pulse rounded-lg bg-contrast-100\" />\n      <div className=\"h-10 w-10 shrink-0 animate-pulse rounded-full bg-contrast-100\" />\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/products-list-section/index.tsx",
    "content": "import { Sliders } from 'lucide-react';\nimport { Suspense } from 'react';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { CursorPagination, CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination';\nimport { Product } from '@/vibes/soul/primitives/product-card';\nimport * as SidePanel from '@/vibes/soul/primitives/side-panel';\nimport { Breadcrumb, Breadcrumbs, BreadcrumbsSkeleton } from '@/vibes/soul/sections/breadcrumbs';\nimport { ProductList } from '@/vibes/soul/sections/product-list';\nimport { Filter, FiltersPanel } from '@/vibes/soul/sections/products-list-section/filters-panel';\nimport {\n  Sorting,\n  SortingSkeleton,\n  Option as SortOption,\n} from '@/vibes/soul/sections/products-list-section/sorting';\n\ninterface Props {\n  breadcrumbs?: Streamable<Breadcrumb[]>;\n  title?: Streamable<string | null>;\n  totalCount: Streamable<string>;\n  products: Streamable<Product[]>;\n  filters: Streamable<Filter[]>;\n  sortOptions: Streamable<SortOption[]>;\n  compareProducts?: Streamable<Product[]>;\n  paginationInfo?: Streamable<CursorPaginationInfo>;\n  compareHref?: string;\n  compareLabel?: Streamable<string>;\n  showCompare?: Streamable<boolean>;\n  filterLabel?: string;\n  filtersPanelTitle?: Streamable<string>;\n  resetFiltersLabel?: Streamable<string>;\n  showRating?: boolean;\n  rangeFilterApplyLabel?: Streamable<string>;\n  sortLabel?: Streamable<string | null>;\n  sortPlaceholder?: Streamable<string | null>;\n  sortParamName?: string;\n  sortDefaultValue?: string;\n  compareParamName?: string;\n  emptyStateSubtitle?: Streamable<string>;\n  emptyStateTitle?: Streamable<string>;\n  placeholderCount?: number;\n  removeLabel?: Streamable<string>;\n  maxItems?: number;\n  maxCompareLimitMessage?: Streamable<string>;\n}\n\nexport function ProductsListSection({\n  breadcrumbs: streamableBreadcrumbs,\n  title = 'Products',\n  totalCount,\n  products,\n  showRating,\n  compareProducts,\n  sortOptions: streamableSortOptions,\n  sortDefaultValue,\n  filters,\n  compareHref,\n  compareLabel,\n  showCompare,\n  paginationInfo,\n  filterLabel = 'Filters',\n  filtersPanelTitle: streamableFiltersPanelTitle = 'Filters',\n  resetFiltersLabel,\n  rangeFilterApplyLabel,\n  sortLabel: streamableSortLabel,\n  sortPlaceholder: streamableSortPlaceholder,\n  sortParamName,\n  compareParamName,\n  emptyStateSubtitle,\n  emptyStateTitle,\n  placeholderCount = 8,\n  removeLabel,\n  maxItems,\n  maxCompareLimitMessage,\n}: Props) {\n  return (\n    <div className=\"group/products-list-section @container\">\n      <div className=\"mx-auto max-w-screen-2xl px-4 py-10 @xl:px-6 @xl:py-14 @4xl:px-8 @4xl:py-12\">\n        <div>\n          <Stream fallback={<BreadcrumbsSkeleton />} value={streamableBreadcrumbs}>\n            {(breadcrumbs) =>\n              breadcrumbs && breadcrumbs.length > 1 && <Breadcrumbs breadcrumbs={breadcrumbs} />\n            }\n          </Stream>\n          <div className=\"flex flex-wrap items-center justify-between gap-4 pb-8 pt-6 text-foreground\">\n            <h1 className=\"flex items-center gap-2 font-heading text-3xl font-medium leading-none @lg:text-4xl @2xl:text-5xl\">\n              <Suspense\n                fallback={\n                  <span className=\"inline-flex h-[1lh] w-[6ch] animate-pulse rounded-lg bg-contrast-100\" />\n                }\n              >\n                {title}\n              </Suspense>\n              <Suspense\n                fallback={\n                  <span className=\"inline-flex h-[1lh] w-[2ch] animate-pulse rounded-lg bg-contrast-100\" />\n                }\n              >\n                <span className=\"text-contrast-300\">{totalCount}</span>\n              </Suspense>\n            </h1>\n            <div className=\"flex gap-2\">\n              <Stream\n                fallback={<SortingSkeleton />}\n                value={Streamable.all([\n                  streamableSortLabel,\n                  streamableSortOptions,\n                  streamableSortPlaceholder,\n                ])}\n              >\n                {([label, options, placeholder]) => (\n                  <Sorting\n                    defaultValue={sortDefaultValue}\n                    label={label}\n                    options={options}\n                    paramName={sortParamName}\n                    placeholder={placeholder}\n                  />\n                )}\n              </Stream>\n              <div className=\"block @3xl:hidden\">\n                <SidePanel.Root>\n                  <SidePanel.Trigger asChild>\n                    <Button size=\"medium\" variant=\"secondary\">\n                      {filterLabel}\n                      <span className=\"hidden @xl:block\">\n                        <Sliders size={20} />\n                      </span>\n                    </Button>\n                  </SidePanel.Trigger>\n                  <Stream value={streamableFiltersPanelTitle}>\n                    {(filtersPanelTitle) => (\n                      <SidePanel.Content title={filtersPanelTitle}>\n                        <FiltersPanel\n                          filters={filters}\n                          paginationInfo={paginationInfo}\n                          rangeFilterApplyLabel={rangeFilterApplyLabel}\n                          resetFiltersLabel={resetFiltersLabel}\n                        />\n                      </SidePanel.Content>\n                    )}\n                  </Stream>\n                </SidePanel.Root>\n              </div>\n            </div>\n          </div>\n        </div>\n        <div className=\"flex items-stretch gap-8 @4xl:gap-10\">\n          <aside className=\"hidden w-52 @3xl:block @4xl:w-60\">\n            <Stream value={streamableFiltersPanelTitle}>\n              {(filtersPanelTitle) => <h2 className=\"sr-only\">{filtersPanelTitle}</h2>}\n            </Stream>\n            <FiltersPanel\n              className=\"sticky top-4\"\n              filters={filters}\n              paginationInfo={paginationInfo}\n              rangeFilterApplyLabel={rangeFilterApplyLabel}\n              resetFiltersLabel={resetFiltersLabel}\n            />\n          </aside>\n\n          <div className=\"group-has-data-pending/products-list-section:animate-pulse flex-1\">\n            <ProductList\n              compareHref={compareHref}\n              compareLabel={compareLabel}\n              compareParamName={compareParamName}\n              compareProducts={compareProducts}\n              emptyStateSubtitle={emptyStateSubtitle}\n              emptyStateTitle={emptyStateTitle}\n              maxCompareLimitMessage={maxCompareLimitMessage}\n              maxItems={maxItems}\n              placeholderCount={placeholderCount}\n              products={products}\n              removeLabel={removeLabel}\n              showCompare={showCompare}\n              showRating={showRating}\n            />\n\n            {paginationInfo && <CursorPagination info={paginationInfo} />}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/products-list-section/sorting.tsx",
    "content": "'use client';\n\nimport { parseAsString, useQueryStates } from 'nuqs';\nimport { useOptimistic, useTransition } from 'react';\n\nimport { Select } from '@/vibes/soul/form/select';\nimport { Streamable, useStreamable } from '@/vibes/soul/lib/streamable';\n\nexport interface Option {\n  label: string;\n  value: string;\n}\n\nexport function Sorting({\n  label: streamableLabel,\n  options: streamableOptions,\n  paramName = 'sort',\n  defaultValue = '',\n  placeholder: streamablePlaceholder,\n}: {\n  label?: Streamable<string | null>;\n  options: Streamable<Option[]>;\n  paramName?: string;\n  defaultValue?: string;\n  placeholder?: Streamable<string | null>;\n}) {\n  const [params, setParams] = useQueryStates(\n    {\n      [paramName]: parseAsString.withDefault(defaultValue),\n      before: parseAsString,\n      after: parseAsString,\n    },\n    { shallow: false, history: 'push' },\n  );\n  const [optimisticParam, setOptimisticParam] = useOptimistic(params[paramName] ?? defaultValue);\n  const [isPending, startTransition] = useTransition();\n  const options = useStreamable(streamableOptions);\n  const label = useStreamable(streamableLabel) ?? 'Sort';\n  const placeholder = useStreamable(streamablePlaceholder) ?? 'Sort by';\n\n  return (\n    <Select\n      hideLabel\n      label={label}\n      name={paramName}\n      onValueChange={(value) => {\n        startTransition(async () => {\n          setOptimisticParam(value);\n          await setParams({\n            [paramName]: value,\n            before: null,\n            after: null,\n          });\n        });\n      }}\n      options={options}\n      pending={isPending}\n      placeholder={placeholder}\n      value={optimisticParam}\n      variant=\"round\"\n    />\n  );\n}\n\nexport function SortingSkeleton() {\n  return <div className=\"h-[50px] w-[12ch] animate-pulse rounded-full bg-contrast-100\" />;\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/reset-password-section/index.tsx",
    "content": "import { PasswordComplexitySettings } from '@/vibes/soul/form/dynamic-form/schema';\n\nimport { ResetPasswordAction, ResetPasswordForm } from './reset-password-form';\n\ninterface Props {\n  title?: string;\n  subtitle?: string;\n  action: ResetPasswordAction;\n  submitLabel?: string;\n  newPasswordLabel?: string;\n  confirmPasswordLabel?: string;\n  passwordComplexitySettings?: PasswordComplexitySettings | null;\n}\n\nexport function ResetPasswordSection({\n  title = 'Reset password',\n  subtitle = 'Enter a new password below to reset your account password.',\n  submitLabel,\n  newPasswordLabel,\n  confirmPasswordLabel,\n  passwordComplexitySettings,\n  action,\n}: Props) {\n  return (\n    <div className=\"@container\">\n      <div className=\"flex flex-col justify-center gap-y-24 px-3 py-10 @xl:flex-row @xl:px-6 @4xl:py-20 @5xl:px-20\">\n        <div className=\"flex w-full flex-col @xl:max-w-md @xl:pr-10 @4xl:pr-20\">\n          <h1 className=\"mb-5 text-4xl font-medium leading-none @xl:text-5xl\">{title}</h1>\n          <p className=\"mb-10 text-base font-light leading-none @xl:text-lg\">{subtitle}</p>\n          <ResetPasswordForm\n            action={action}\n            confirmPasswordLabel={confirmPasswordLabel}\n            newPasswordLabel={newPasswordLabel}\n            passwordComplexitySettings={passwordComplexitySettings}\n            submitLabel={submitLabel}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/reset-password-section/reset-password-form.tsx",
    "content": "'use client';\n\nimport { getFormProps, getInputProps, SubmissionResult, useForm } from '@conform-to/react';\nimport { getZodConstraint } from '@conform-to/zod';\nimport { useTranslations } from 'next-intl';\nimport { useActionState } from 'react';\n\nimport { PasswordComplexitySettings } from '@/vibes/soul/form/dynamic-form/schema';\nimport { FormStatus } from '@/vibes/soul/form/form-status';\nimport { Input } from '@/vibes/soul/form/input';\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { parseWithZodTranslatedErrors } from '~/i18n/utils';\n\nimport { resetPasswordErrorTranslations, resetPasswordSchema } from './schema';\n\ntype Action<State, Payload> = (state: Awaited<State>, payload: Payload) => State | Promise<State>;\n\nexport type ResetPasswordAction = Action<\n  { lastResult: SubmissionResult | null; successMessage?: string },\n  FormData\n>;\n\ninterface Props {\n  action: ResetPasswordAction;\n  submitLabel?: string;\n  newPasswordLabel?: string;\n  confirmPasswordLabel?: string;\n  passwordComplexitySettings?: PasswordComplexitySettings | null;\n}\n\nexport function ResetPasswordForm({\n  action,\n  newPasswordLabel = 'New password',\n  confirmPasswordLabel = 'Confirm Password',\n  submitLabel = 'Update',\n  passwordComplexitySettings,\n}: Props) {\n  const t = useTranslations('Auth.ChangePassword');\n  const errorTranslations = resetPasswordErrorTranslations(t, passwordComplexitySettings);\n  const schema = resetPasswordSchema(passwordComplexitySettings, errorTranslations);\n  const [{ lastResult, successMessage }, formAction, isPending] = useActionState(action, {\n    lastResult: null,\n  });\n  const [form, fields] = useForm({\n    lastResult,\n    constraint: getZodConstraint(schema),\n    shouldValidate: 'onBlur',\n    shouldRevalidate: 'onInput',\n    onValidate({ formData }) {\n      return parseWithZodTranslatedErrors(formData, { schema, errorTranslations });\n    },\n  });\n\n  return (\n    <form {...getFormProps(form)} action={formAction} className=\"space-y-5\">\n      <Input\n        {...getInputProps(fields.password, { type: 'password' })}\n        errors={fields.password.errors}\n        key={fields.password.id}\n        label={newPasswordLabel}\n      />\n      <Input\n        {...getInputProps(fields.confirmPassword, { type: 'password' })}\n        className=\"mb-6\"\n        errors={fields.confirmPassword.errors}\n        key={fields.confirmPassword.id}\n        label={confirmPasswordLabel}\n      />\n      <Button loading={isPending} size=\"small\" type=\"submit\" variant=\"secondary\">\n        {submitLabel}\n      </Button>\n      {form.errors?.map((error, index) => (\n        <FormStatus key={index} type=\"error\">\n          {error}\n        </FormStatus>\n      ))}\n      {form.status === 'success' && successMessage != null && (\n        <FormStatus>{successMessage}</FormStatus>\n      )}\n    </form>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/reset-password-section/schema.ts",
    "content": "import { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport {\n  FormErrorTranslationMap,\n  getPasswordSchema,\n  PasswordComplexitySettings,\n} from '@/vibes/soul/form/dynamic-form/schema';\nimport { ExistingResultType } from '~/client/util';\n\nexport const resetPasswordErrorTranslations = (\n  t: ExistingResultType<typeof getTranslations<'Auth.ChangePassword'>>,\n  passwordComplexity?: PasswordComplexitySettings | null,\n): FormErrorTranslationMap => ({\n  password: {\n    invalid_type: t('FieldErrors.passwordRequired'),\n    too_small: t('FieldErrors.passwordTooSmall', {\n      minLength: passwordComplexity?.minimumPasswordLength ?? 0,\n    }),\n    lowercase_required: t('FieldErrors.passwordLowercaseRequired'),\n    uppercase_required: t('FieldErrors.passwordUppercaseRequired'),\n    number_required: t('FieldErrors.passwordNumberRequired', {\n      minNumbers: passwordComplexity?.minimumNumbers ?? 1,\n    }),\n    special_character_required: t('FieldErrors.passwordSpecialCharacterRequired'),\n    passwords_must_match: t('FieldErrors.passwordsMustMatch'),\n  },\n  confirmPassword: {\n    invalid_type: t('FieldErrors.passwordRequired'),\n  },\n});\n\nexport const resetPasswordSchema = (\n  passwordComplexity?: PasswordComplexitySettings | null,\n  errorTranslations?: FormErrorTranslationMap,\n) => {\n  const passwordSchema = getPasswordSchema(passwordComplexity, errorTranslations);\n\n  return z\n    .object({\n      currentPassword: z.string().trim(),\n      password: passwordSchema,\n      confirmPassword: z.string(),\n    })\n    .superRefine(({ confirmPassword, password }, ctx) => {\n      if (confirmPassword !== password) {\n        ctx.addIssue({\n          code: 'custom',\n          message:\n            errorTranslations?.password?.passwords_must_match ?? 'The passwords do not match',\n          path: ['confirmPassword'],\n        });\n      }\n    });\n};\n"
  },
  {
    "path": "core/vibes/soul/sections/reviews/index.tsx",
    "content": "import { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { CursorPagination, CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination';\nimport { Rating } from '@/vibes/soul/primitives/rating';\nimport { StickySidebarLayout } from '@/vibes/soul/sections/sticky-sidebar-layout';\n\nimport { ReviewForm, SubmitReviewAction } from './review-form';\n\ninterface Review {\n  id: string;\n  rating: number;\n  review: string;\n  name: string;\n  date: string;\n}\n\ninterface Props {\n  reviews: Streamable<Review[]>;\n  averageRating: Streamable<number>;\n  totalCount?: Streamable<number>;\n  paginationInfo?: Streamable<CursorPaginationInfo>;\n  nextLabel?: Streamable<string>;\n  previousLabel?: Streamable<string>;\n  emptyStateMessage?: string;\n  reviewsLabel?: string;\n  productId: number;\n  action: SubmitReviewAction;\n  formButtonLabel?: string;\n  formModalTitle?: string;\n  formSubmitLabel?: string;\n  formCancelLabel?: string;\n  formRatingLabel?: string;\n  formTitleLabel?: string;\n  formReviewLabel?: string;\n  formNameLabel?: string;\n  formEmailLabel?: string;\n  streamableImages: Streamable<{\n    images: Array<{ src: string; alt: string }>;\n    pageInfo?: { hasNextPage: boolean; endCursor: string | null };\n  }>;\n  streamableProduct: Streamable<{ name: string }>;\n  streamableUser: Streamable<{ email: string; name: string }>;\n  recaptchaSiteKey?: string;\n}\n\nexport function Reviews({\n  productId,\n  reviews: streamableReviews,\n  averageRating: streamableAverageRating,\n  totalCount: streamableTotalCount,\n  paginationInfo: streamablePaginationInfo,\n  nextLabel,\n  previousLabel,\n  emptyStateMessage,\n  reviewsLabel = 'Reviews',\n  action,\n  formButtonLabel = 'Write a review',\n  formModalTitle,\n  formSubmitLabel,\n  formCancelLabel,\n  formRatingLabel,\n  formTitleLabel,\n  formReviewLabel,\n  formNameLabel,\n  formEmailLabel,\n  streamableProduct,\n  streamableImages,\n  streamableUser,\n  recaptchaSiteKey,\n}: Readonly<Props>) {\n  return (\n    <Stream fallback={<ReviewsSkeleton reviewsLabel={reviewsLabel} />} value={streamableReviews}>\n      {(reviews) => {\n        if (reviews.length === 0)\n          return (\n            <ReviewsEmptyState\n              action={action}\n              formButtonLabel={formButtonLabel}\n              formCancelLabel={formCancelLabel}\n              formEmailLabel={formEmailLabel}\n              formModalTitle={formModalTitle}\n              formNameLabel={formNameLabel}\n              formRatingLabel={formRatingLabel}\n              formReviewLabel={formReviewLabel}\n              formSubmitLabel={formSubmitLabel}\n              formTitleLabel={formTitleLabel}\n              message={emptyStateMessage}\n              productId={productId}\n              recaptchaSiteKey={recaptchaSiteKey}\n              reviewsLabel={reviewsLabel}\n              streamableImages={streamableImages}\n              streamableProduct={streamableProduct}\n              streamableUser={streamableUser}\n            />\n          );\n\n        return (\n          <StickySidebarLayout\n            sidebar={\n              <>\n                <Stream\n                  fallback={\n                    <div className=\"animate-pulse\">\n                      <h2 className=\"mb-4 mt-0 text-xl font-medium @xl:my-5 @xl:text-2xl\">\n                        {reviewsLabel}\n                      </h2>\n                    </div>\n                  }\n                  value={streamableTotalCount}\n                >\n                  {(totalCount) => (\n                    <h2 className=\"mb-4 mt-0 text-xl font-medium @xl:my-5 @xl:text-2xl\">\n                      {reviewsLabel} <span className=\"text-contrast-300\">{totalCount}</span>\n                    </h2>\n                  )}\n                </Stream>\n                <Stream\n                  fallback={\n                    <div className=\"animate-pulse\">\n                      <div className=\"mb-2 h-[1lh] w-[3ch] rounded-md bg-contrast-100 font-heading text-5xl leading-none tracking-tighter @2xl:text-6xl\" />\n                      <div className=\"h-5 w-32 rounded-md bg-contrast-100\" />\n                    </div>\n                  }\n                  value={streamableAverageRating}\n                >\n                  {(averageRating) => (\n                    <>\n                      <div className=\"mb-2 font-heading text-5xl leading-none tracking-tighter @2xl:text-6xl\">\n                        {parseFloat(averageRating.toFixed(1))}\n                      </div>\n                      <Rating rating={averageRating} showRating={false} />\n                    </>\n                  )}\n                </Stream>\n                <ReviewForm\n                  action={action}\n                  formEmailLabel={formEmailLabel}\n                  formModalTitle={formModalTitle}\n                  formNameLabel={formNameLabel}\n                  formRatingLabel={formRatingLabel}\n                  formReviewLabel={formReviewLabel}\n                  formSubmitLabel={formSubmitLabel}\n                  formTitleLabel={formTitleLabel}\n                  productId={productId}\n                  recaptchaSiteKey={recaptchaSiteKey}\n                  streamableImages={streamableImages}\n                  streamableProduct={streamableProduct}\n                  streamableUser={streamableUser}\n                  trigger={\n                    <Button className=\"mx-auto mt-8\" size=\"small\" variant=\"tertiary\">\n                      {formButtonLabel}\n                    </Button>\n                  }\n                />\n              </>\n            }\n            sidebarSize=\"medium\"\n          >\n            <div className=\"flex-1 border-t border-contrast-100\">\n              {reviews.map(({ id, rating, review, name, date }) => {\n                return (\n                  <div className=\"border-b border-contrast-100 py-6\" key={id}>\n                    <Rating rating={rating} />\n                    <p className=\"mt-5 text-lg font-semibold text-foreground\">{name}</p>\n                    <p className=\"mb-8 mt-2 leading-normal text-contrast-500\">{review}</p>\n                    <p className=\"text-sm text-contrast-500\">{date}</p>\n                  </div>\n                );\n              })}\n\n              <Stream value={streamablePaginationInfo}>\n                {(paginationInfo) =>\n                  paginationInfo && (\n                    <CursorPagination\n                      info={paginationInfo}\n                      nextLabel={nextLabel}\n                      previousLabel={previousLabel}\n                      scroll={false}\n                    />\n                  )\n                }\n              </Stream>\n            </div>\n          </StickySidebarLayout>\n        );\n      }}\n    </Stream>\n  );\n}\n\nexport function ReviewsEmptyState({\n  message = 'No reviews have been added for this product',\n  reviewsLabel = 'Reviews',\n  productId,\n  action,\n  formButtonLabel = 'Write a review',\n  formModalTitle,\n  formSubmitLabel,\n  formCancelLabel,\n  formRatingLabel,\n  formTitleLabel,\n  formReviewLabel,\n  formNameLabel,\n  formEmailLabel,\n  streamableProduct,\n  streamableImages,\n  streamableUser,\n  recaptchaSiteKey,\n}: {\n  message?: string;\n  reviewsLabel?: string;\n  productId: number;\n  action: SubmitReviewAction;\n  formButtonLabel?: string;\n  formModalTitle?: string;\n  formSubmitLabel?: string;\n  formCancelLabel?: string;\n  formRatingLabel?: string;\n  formTitleLabel?: string;\n  formReviewLabel?: string;\n  formNameLabel?: string;\n  formEmailLabel?: string;\n  streamableImages: Streamable<{\n    images: Array<{ src: string; alt: string }>;\n    pageInfo?: { hasNextPage: boolean; endCursor: string | null };\n  }>;\n  streamableProduct: Streamable<{ name: string }>;\n  streamableUser: Streamable<{ email: string; name: string }>;\n  recaptchaSiteKey?: string;\n}) {\n  return (\n    <StickySidebarLayout\n      sidebar={\n        <>\n          <h2 className=\"mb-4 mt-0 text-xl font-medium @xl:my-5 @xl:text-2xl\">\n            {reviewsLabel} <span className=\"text-contrast-300\">0</span>\n          </h2>\n          <div className=\"mb-2 font-heading text-5xl leading-none tracking-tighter @2xl:text-6xl\">\n            0\n          </div>\n          <Rating rating={0} showRating={false} />\n        </>\n      }\n      sidebarSize=\"medium\"\n    >\n      <div className=\"flex flex-1 flex-col border-t border-contrast-100 py-12\">\n        <p className=\"text-center\">{message}</p>\n        <ReviewForm\n          action={action}\n          formCancelLabel={formCancelLabel}\n          formEmailLabel={formEmailLabel}\n          formModalTitle={formModalTitle}\n          formNameLabel={formNameLabel}\n          formRatingLabel={formRatingLabel}\n          formReviewLabel={formReviewLabel}\n          formSubmitLabel={formSubmitLabel}\n          formTitleLabel={formTitleLabel}\n          productId={productId}\n          recaptchaSiteKey={recaptchaSiteKey}\n          streamableImages={streamableImages}\n          streamableProduct={streamableProduct}\n          streamableUser={streamableUser}\n          trigger={\n            <Button className=\"mx-auto mt-8\" size=\"small\" variant=\"tertiary\">\n              {formButtonLabel}\n            </Button>\n          }\n        />\n      </div>\n    </StickySidebarLayout>\n  );\n}\n\nexport function ReviewsSkeleton({ reviewsLabel = 'Reviews' }: { reviewsLabel?: string }) {\n  return (\n    <StickySidebarLayout\n      sidebar={\n        <div className=\"animate-pulse\">\n          <h2 className=\"mb-4 mt-0 text-xl font-medium @xl:my-5 @xl:text-2xl\">{reviewsLabel}</h2>\n          <div className=\"mb-2 h-[1lh] w-[3ch] rounded-md bg-contrast-100 font-heading text-5xl leading-none tracking-tighter @2xl:text-6xl\" />\n          <div className=\"h-5 w-32 rounded-md bg-contrast-100\" />\n        </div>\n      }\n      sidebarSize=\"medium\"\n    >\n      <div className=\"flex-1 animate-pulse border-t border-contrast-100\">\n        {Array.from({ length: 3 }).map((_, index) => (\n          <div className=\"border-b border-contrast-100 py-6\" key={index}>\n            <div className=\"h-5 w-32 rounded-md bg-contrast-100\" />\n            <div className=\"mt-5 h-[1lh] rounded-md bg-contrast-100 text-lg font-semibold\" />\n            <div className=\"mb-8 mt-2 h-[1lh] w-1/2 rounded-md bg-contrast-100 leading-normal\" />\n            <div className=\"h-[1lh] w-24 rounded-md bg-contrast-100 text-sm\" />\n          </div>\n        ))}\n      </div>\n    </StickySidebarLayout>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/reviews/review-form.tsx",
    "content": "'use client';\n\nimport { getFormProps, SubmissionResult, useForm, useInputControl } from '@conform-to/react';\nimport { getZodConstraint } from '@conform-to/zod';\nimport { useTranslations } from 'next-intl';\nimport { startTransition, useActionState, useEffect, useRef, useState } from 'react';\nimport { useFormStatus } from 'react-dom';\nimport RecaptchaWidget from 'react-google-recaptcha';\n\nimport { FormStatus } from '@/vibes/soul/form/form-status';\nimport { Input } from '@/vibes/soul/form/input';\nimport { RatingRadioGroup } from '@/vibes/soul/form/rating-radio-group';\nimport { Textarea } from '@/vibes/soul/form/textarea';\nimport { Stream, Streamable, useStreamable } from '@/vibes/soul/lib/streamable';\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { Modal } from '@/vibes/soul/primitives/modal';\nimport { toast } from '@/vibes/soul/primitives/toaster';\nimport { Image } from '~/components/image';\nimport { parseWithZodTranslatedErrors } from '~/i18n/utils';\n\nimport { reviewFormErrorTranslations, schema } from './schema';\n\ntype Action<S, P> = (state: Awaited<S>, payload: P) => S | Promise<S>;\n\nexport type SubmitReviewAction = Action<\n  { lastResult: SubmissionResult | null; successMessage?: string },\n  FormData\n>;\n\ninterface Props {\n  productId: number;\n  action: SubmitReviewAction;\n  trigger: React.ReactNode;\n  formModalTitle?: string;\n  formSubmitLabel?: string;\n  formCancelLabel?: string;\n  formRatingLabel?: string;\n  formTitleLabel?: string;\n  formReviewLabel?: string;\n  formNameLabel?: string;\n  formEmailLabel?: string;\n  streamableImages: Streamable<{\n    images: Array<{ src: string; alt: string }>;\n    pageInfo?: { hasNextPage: boolean; endCursor: string | null };\n  }>;\n  streamableProduct: Streamable<{ name: string }>;\n  streamableUser: Streamable<{ email: string; name: string }>;\n  recaptchaSiteKey?: string;\n}\n\nexport const ReviewForm = ({\n  productId,\n  action,\n  trigger,\n  formModalTitle = 'Write a review',\n  formSubmitLabel = 'Submit',\n  formCancelLabel = 'Cancel',\n  formRatingLabel = 'Rating',\n  formTitleLabel = 'Title',\n  formReviewLabel = 'Review',\n  formNameLabel = 'Name',\n  formEmailLabel = 'Email',\n  streamableProduct,\n  streamableImages,\n  streamableUser,\n  recaptchaSiteKey,\n}: Props) => {\n  const t = useTranslations('Product.Reviews.Form');\n  const errorTranslations = reviewFormErrorTranslations(t);\n  const [isOpen, setIsOpen] = useState(false);\n  const [{ lastResult, successMessage }, formAction] = useActionState(action, {\n    lastResult: null,\n  });\n  const formRef = useRef<HTMLFormElement>(null);\n\n  const user = useStreamable(streamableUser);\n\n  const [form, fields] = useForm({\n    lastResult,\n    constraint: getZodConstraint(schema),\n    shouldValidate: 'onSubmit',\n    shouldRevalidate: 'onInput',\n    defaultValue: {\n      email: user.email,\n      author: user.name,\n    },\n    onValidate({ formData }) {\n      return parseWithZodTranslatedErrors(formData, { schema, errorTranslations });\n    },\n    onSubmit(event, { formData }) {\n      event.preventDefault();\n\n      startTransition(() => {\n        formAction(formData);\n      });\n    },\n  });\n\n  const ratingControl = useInputControl(fields.rating);\n  const titleControl = useInputControl(fields.title);\n  const textControl = useInputControl(fields.text);\n  const authorControl = useInputControl(fields.author);\n  const emailControl = useInputControl(fields.email);\n\n  const isEmailDisabled = user.email !== '';\n  const isAuthorDisabled = user.name !== '';\n\n  useEffect(() => {\n    if (lastResult?.status === 'success' && successMessage) {\n      toast.success(successMessage);\n      setIsOpen(false);\n      formRef.current?.reset();\n    }\n  }, [lastResult, successMessage]);\n\n  return (\n    <Modal\n      className=\"w-full md:min-w-[768px]\"\n      isOpen={isOpen}\n      setOpen={setIsOpen}\n      title={formModalTitle}\n      trigger={trigger}\n    >\n      <div className=\"flex flex-col gap-6 md:flex-row md:gap-8\">\n        <div className=\"shrink-0 md:w-48\">\n          <Stream\n            fallback={\n              <div className=\"animate-pulse\">\n                <div className=\"mb-4 aspect-square w-full max-w-[200px] rounded-md bg-contrast-100 md:max-w-none\" />\n                <div className=\"h-6 w-32 rounded-md bg-contrast-100\" />\n              </div>\n            }\n            value={Streamable.all([streamableProduct, streamableImages])}\n          >\n            {([product, imagesData]) => {\n              const firstImage = imagesData.images[0];\n\n              return (\n                <>\n                  {firstImage && (\n                    <div className=\"relative mb-4 aspect-square w-full max-w-[200px] overflow-hidden rounded-md md:w-full md:max-w-none\">\n                      <Image\n                        alt={firstImage.alt}\n                        className=\"object-cover\"\n                        fill\n                        sizes=\"(min-width: 768px) 192px, 200px\"\n                        src={firstImage.src}\n                      />\n                    </div>\n                  )}\n                  <h3 className=\"text-lg font-semibold\">{product.name}</h3>\n                </>\n              );\n            }}\n          </Stream>\n        </div>\n        <div className=\"min-w-0 flex-1\">\n          <form\n            {...getFormProps(form)}\n            action={formAction}\n            className=\"flex w-full flex-col gap-5\"\n            ref={formRef}\n          >\n            <input name=\"productEntityId\" type=\"hidden\" value={productId} />\n            <RatingRadioGroup\n              errors={fields.rating.errors}\n              label={formRatingLabel}\n              max={5}\n              name={fields.rating.name}\n              onBlur={ratingControl.blur}\n              onFocus={ratingControl.focus}\n              onValueChange={ratingControl.change}\n              required={fields.rating.required}\n              value={typeof ratingControl.value === 'string' ? ratingControl.value : ''}\n            />\n            <Input\n              errors={fields.title.errors}\n              label={formTitleLabel}\n              name={fields.title.name}\n              onBlur={titleControl.blur}\n              onChange={(e) => titleControl.change(e.currentTarget.value)}\n              onFocus={titleControl.focus}\n              required={fields.title.required}\n              type=\"text\"\n              value={typeof titleControl.value === 'string' ? titleControl.value : ''}\n            />\n            <Textarea\n              errors={fields.text.errors}\n              label={formReviewLabel}\n              name={fields.text.name}\n              onBlur={textControl.blur}\n              onChange={(e) => textControl.change(e.currentTarget.value)}\n              onFocus={textControl.focus}\n              required={fields.text.required}\n              value={typeof textControl.value === 'string' ? textControl.value : ''}\n            />\n            <Input\n              errors={fields.author.errors}\n              label={formNameLabel}\n              name={fields.author.name}\n              onBlur={authorControl.blur}\n              onChange={(e) => authorControl.change(e.currentTarget.value)}\n              onFocus={authorControl.focus}\n              readOnly={isAuthorDisabled}\n              required={fields.author.required}\n              type=\"text\"\n              value={typeof authorControl.value === 'string' ? authorControl.value : ''}\n            />\n            <Input\n              errors={fields.email.errors}\n              label={formEmailLabel}\n              name={fields.email.name}\n              onBlur={emailControl.blur}\n              onChange={(e) => emailControl.change(e.currentTarget.value)}\n              onFocus={emailControl.focus}\n              readOnly={isEmailDisabled}\n              required={fields.email.required}\n              type=\"email\"\n              value={typeof emailControl.value === 'string' ? emailControl.value : ''}\n            />\n            {form.errors?.map((error, index) => (\n              <FormStatus key={index} type=\"error\">\n                {error}\n              </FormStatus>\n            ))}\n            {recaptchaSiteKey ? (\n              <div>\n                <RecaptchaWidget sitekey={recaptchaSiteKey} />\n              </div>\n            ) : null}\n            <div className=\"mt-auto flex justify-end gap-3\">\n              <Button onClick={() => setIsOpen(false)} size=\"small\" type=\"button\" variant=\"ghost\">\n                {formCancelLabel}\n              </Button>\n              <SubmitButton>{formSubmitLabel}</SubmitButton>\n            </div>\n          </form>\n        </div>\n      </div>\n    </Modal>\n  );\n};\n\nfunction SubmitButton({ children }: { children: React.ReactNode }) {\n  const { pending } = useFormStatus();\n\n  return (\n    <Button loading={pending} size=\"small\" type=\"submit\" variant=\"secondary\">\n      {children}\n    </Button>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/reviews/schema.ts",
    "content": "import { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { FormErrorTranslationMap } from '@/vibes/soul/form/dynamic-form/schema';\nimport { ExistingResultType } from '~/client/util';\n\nexport const reviewFormErrorTranslations = (\n  t: ExistingResultType<typeof getTranslations<'Product.Reviews.Form'>>,\n): FormErrorTranslationMap => ({\n  title: {\n    invalid_type: t('FieldErrors.titleRequired'),\n  },\n  author: {\n    invalid_type: t('FieldErrors.authorRequired'),\n  },\n  email: {\n    invalid_type: t('FieldErrors.emailRequired'),\n    invalid_string: t('FieldErrors.emailInvalid'),\n  },\n  text: {\n    invalid_type: t('FieldErrors.textRequired'),\n  },\n  rating: {\n    invalid_type: t('FieldErrors.ratingRequired'),\n    too_small: t('FieldErrors.ratingTooSmall'),\n    too_big: t('FieldErrors.ratingTooLarge'),\n  },\n});\n\nexport const schema = z.object({\n  productEntityId: z.number(),\n  title: z.string().min(1),\n  author: z.string().min(1),\n  email: z.string().email(),\n  text: z.string().min(1),\n  rating: z.number().min(1).max(5),\n});\n"
  },
  {
    "path": "core/vibes/soul/sections/section-layout/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { ReactNode } from 'react';\n\nexport interface SectionLayoutProps {\n  className?: string;\n  children: ReactNode;\n  containerSize?: 'md' | 'lg' | 'xl' | '2xl' | 'full';\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --section-max-width-medium: 768px;\n *   --section-max-width-lg: 1024px;\n *   --section-max-width-xl: 1280px;\n *   --section-max-width-2xl: 1536px;\n * }\n * ```\n */\nexport function SectionLayout({ className, children, containerSize = '2xl' }: SectionLayoutProps) {\n  return (\n    <section className={clsx('overflow-hidden @container', className)}>\n      <div\n        className={clsx(\n          'mx-auto px-4 py-10 @xl:px-6 @xl:py-14 @4xl:px-8 @4xl:py-20',\n          {\n            md: 'max-w-[var(--section-max-width-md,768px)]',\n            lg: 'max-w-[var(--section-max-width-lg,1024px)]',\n            xl: 'max-w-[var(--section-max-width-xl,1280px)]',\n            '2xl': 'max-w-[var(--section-max-width-2xl,1536px)]',\n            full: 'max-w-none',\n          }[containerSize],\n        )}\n      >\n        {children}\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/sidebar-menu/index.tsx",
    "content": "import { ComponentPropsWithoutRef } from 'react';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\n\nimport { SidebarMenuLink } from './sidebar-menu-link';\nimport { SidebarMenuSelect } from './sidebar-menu-select';\n\ninterface MenuLink {\n  href: string;\n  label: string;\n  prefetch?: ComponentPropsWithoutRef<typeof SidebarMenuLink>['prefetch'];\n}\n\ninterface Props {\n  links: Streamable<MenuLink[]>;\n  placeholderCount?: number;\n}\n\nexport function SidebarMenu({ links: streamableLinks, placeholderCount = 5 }: Props) {\n  return (\n    <Stream\n      fallback={<SidebarMenuSkeleton placeholderCount={placeholderCount} />}\n      value={streamableLinks}\n    >\n      {(links) => {\n        if (!links.length) {\n          return null;\n        }\n\n        return (\n          <nav>\n            <ul className=\"hidden @2xl:block\">\n              {links.map((link, index) => (\n                <li key={index}>\n                  <SidebarMenuLink href={link.href} prefetch={link.prefetch}>\n                    {link.label}\n                  </SidebarMenuLink>\n                </li>\n              ))}\n            </ul>\n            <div className=\"@2xl:hidden\">\n              <SidebarMenuSelect links={links} />\n            </div>\n          </nav>\n        );\n      }}\n    </Stream>\n  );\n}\n\nfunction SidebarMenuSkeleton({ placeholderCount }: { placeholderCount: number }) {\n  return (\n    <>\n      <div className=\"hidden [mask-image:linear-gradient(to_bottom,_black_0%,_transparent_90%)] @2xl:block\">\n        <div className=\"w-full animate-pulse\">\n          {Array.from({ length: placeholderCount }).map((_, index) => (\n            <div className=\"flex h-10 items-center px-3\" key={index}>\n              <div className=\"h-[1lh] flex-1 rounded-lg bg-contrast-100\" />\n            </div>\n          ))}\n        </div>\n      </div>\n      <div className=\"@2xl:hidden\">\n        <div className=\"h-[50px] w-full rounded-lg bg-contrast-100\" />\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/sidebar-menu/sidebar-menu-link.tsx",
    "content": "'use client';\n\nimport { clsx } from 'clsx';\nimport React from 'react';\n\nimport { Link } from '~/components/link';\nimport { usePathname } from '~/i18n/routing';\n\nexport function SidebarMenuLink({\n  className,\n  href,\n  ...rest\n}: React.ComponentPropsWithoutRef<typeof Link>) {\n  const pathname = usePathname();\n  const linkPathname = typeof href === 'string' ? href : (href.pathname ?? null);\n\n  return (\n    <Link\n      {...rest}\n      className={clsx(\n        'flex min-h-10 items-center rounded-md px-3 text-sm font-semibold',\n        linkPathname !== null && pathname.includes(linkPathname)\n          ? 'bg-contrast-100'\n          : 'hover:bg-contrast-100',\n        className,\n      )}\n      href={href}\n    />\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/sidebar-menu/sidebar-menu-select.tsx",
    "content": "'use client';\n\nimport { Select } from '@/vibes/soul/form/select';\nimport { usePathname, useRouter } from '~/i18n/routing';\n\nexport function SidebarMenuSelect({ links }: { links: Array<{ href: string; label: string }> }) {\n  const pathname = usePathname();\n  const router = useRouter();\n\n  return (\n    <Select\n      name=\"sidebar-layout-link-select\"\n      onValueChange={(value) => {\n        router.push(value);\n      }}\n      options={links.map((link) => ({ value: link.href, label: link.label }))}\n      value={pathname}\n    />\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/sign-in-section/index.tsx",
    "content": "import { AnimatedUnderline } from '@/vibes/soul/primitives/animated-underline';\nimport { Link } from '~/components/link';\n\nimport { SignInAction, SignInForm } from './sign-in-form';\n\ninterface Props {\n  children?: React.ReactNode;\n  title?: string;\n  action: SignInAction;\n  submitLabel?: string;\n  emailLabel?: string;\n  passwordLabel?: string;\n  forgotPasswordHref?: string;\n  forgotPasswordLabel?: string;\n  error?: string;\n}\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --sign-in-title-font-family: var(--font-family-heading);\n *   --sign-in-title: hsl(var(--foreground));\n * }\n * ```\n */\nexport function SignInSection({\n  title = 'Sign In',\n  children,\n  action,\n  submitLabel,\n  emailLabel,\n  passwordLabel,\n  forgotPasswordHref = '/forgot-password',\n  forgotPasswordLabel = 'Forgot your password?',\n  error,\n}: Props) {\n  return (\n    <div className=\"@container\">\n      <div className=\"flex flex-col justify-center gap-y-24 px-3 py-10 @xl:flex-row @xl:px-6 @4xl:py-20 @5xl:px-20\">\n        <div className=\"w-full @xl:max-w-md @xl:border-r @xl:pr-10 @4xl:pr-20\">\n          <h1 className=\"mb-10 font-[family-name:var(--sign-in-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none text-[var(--sign-in-title,hsl(var(--foreground)))] @xl:text-5xl\">\n            {title}\n          </h1>\n          <SignInForm\n            action={action}\n            emailLabel={emailLabel}\n            error={error}\n            passwordLabel={passwordLabel}\n            submitLabel={submitLabel}\n          />\n          <Link className=\"group/underline focus:outline-none\" href={forgotPasswordHref}>\n            <AnimatedUnderline className=\"mt-4 block w-fit text-sm font-semibold\">\n              {forgotPasswordLabel}\n            </AnimatedUnderline>\n          </Link>\n        </div>\n\n        <div className=\"flex w-full flex-col @xl:max-w-md @xl:pl-10 @4xl:pl-20\">{children}</div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/sign-in-section/schema.ts",
    "content": "import { getTranslations } from 'next-intl/server';\nimport { z } from 'zod';\n\nimport { FormErrorTranslationMap } from '@/vibes/soul/form/dynamic-form/schema';\nimport { ExistingResultType } from '~/client/util';\n\nexport const loginErrorTranslations = (\n  t: ExistingResultType<typeof getTranslations<'Auth.Login'>>,\n): FormErrorTranslationMap => ({\n  email: {\n    invalid_type: t('FieldErrors.emailRequired'),\n    invalid_string: t('FieldErrors.emailInvalid'),\n  },\n  password: {\n    invalid_type: t('FieldErrors.passwordRequired'),\n  },\n});\n\nexport const schema = z.object({\n  email: z.string().email(),\n  password: z.string(),\n});\n"
  },
  {
    "path": "core/vibes/soul/sections/sign-in-section/sign-in-form.tsx",
    "content": "'use client';\n\nimport { getFormProps, getInputProps, SubmissionResult, useForm } from '@conform-to/react';\nimport { getZodConstraint } from '@conform-to/zod';\nimport { useTranslations } from 'next-intl';\nimport { startTransition, useActionState, useEffect } from 'react';\nimport { useFormStatus } from 'react-dom';\n\nimport { FormStatus } from '@/vibes/soul/form/form-status';\nimport { Input } from '@/vibes/soul/form/input';\nimport { Button } from '@/vibes/soul/primitives/button';\nimport { parseWithZodTranslatedErrors } from '~/i18n/utils';\n\nimport { loginErrorTranslations, schema } from './schema';\n\ntype Action<State, Payload> = (state: Awaited<State>, payload: Payload) => State | Promise<State>;\n\nexport type SignInAction = Action<SubmissionResult | null, FormData>;\n\ninterface Props {\n  action: SignInAction;\n  emailLabel?: string;\n  passwordLabel?: string;\n  submitLabel?: string;\n  error?: string;\n}\n\nexport function SignInForm({\n  action,\n  emailLabel = 'Email',\n  passwordLabel = 'Password',\n  submitLabel = 'Sign in',\n  error,\n}: Props) {\n  const t = useTranslations('Auth.Login');\n  const errorTranslations = loginErrorTranslations(t);\n  const [lastResult, formAction] = useActionState(action, null);\n  const [form, fields] = useForm({\n    lastResult,\n    defaultValue: { email: '', password: '' },\n    constraint: getZodConstraint(schema),\n    shouldValidate: 'onBlur',\n    shouldRevalidate: 'onInput',\n    onSubmit(event, { formData }) {\n      event.preventDefault();\n      startTransition(() => {\n        formAction(formData);\n      });\n    },\n    onValidate({ formData }) {\n      return parseWithZodTranslatedErrors(formData, { schema, errorTranslations });\n    },\n  });\n\n  useEffect(() => {\n    // If the form errors change when an \"error\" search param is in the URL,\n    // the search param should be removed to prevent showing stale errors.\n    if (form.errors) {\n      const url = new URL(window.location.href);\n\n      if (url.searchParams.has('error')) {\n        url.searchParams.delete('error');\n        window.history.replaceState({}, '', url.toString());\n      }\n    }\n  }, [form.errors]);\n\n  const formErrors = () => {\n    // Form errors should take precedence over the error prop that is passed in.\n    // This ensures that the most recent errors are displayed to avoid confusion.\n    if (form.errors) {\n      return form.errors;\n    }\n\n    if (error) {\n      return [error];\n    }\n\n    return [];\n  };\n\n  return (\n    <form {...getFormProps(form)} className=\"flex grow flex-col gap-5\">\n      <Input\n        {...getInputProps(fields.email, { type: 'text' })}\n        errors={fields.email.errors}\n        key={fields.email.id}\n        label={emailLabel}\n      />\n      <Input\n        {...getInputProps(fields.password, { type: 'password' })}\n        className=\"mb-6\"\n        errors={fields.password.errors}\n        key={fields.password.id}\n        label={passwordLabel}\n      />\n      <SubmitButton>{submitLabel}</SubmitButton>\n      {formErrors().map((err, index) => (\n        <FormStatus key={index} type=\"error\">\n          {err}\n        </FormStatus>\n      ))}\n    </form>\n  );\n}\n\nfunction SubmitButton({ children }: { children: React.ReactNode }) {\n  const { pending } = useFormStatus();\n\n  return (\n    <Button className=\"mt-auto w-full\" loading={pending} type=\"submit\" variant=\"secondary\">\n      {children}\n    </Button>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/slideshow/index.tsx",
    "content": "'use client';\n\nimport { clsx } from 'clsx';\nimport { EmblaCarouselType } from 'embla-carousel';\nimport Autoplay from 'embla-carousel-autoplay';\nimport Fade from 'embla-carousel-fade';\nimport useEmblaCarousel from 'embla-carousel-react';\nimport { Pause, Play } from 'lucide-react';\nimport { ComponentPropsWithoutRef, useCallback, useEffect, useState } from 'react';\n\nimport { ButtonLink } from '@/vibes/soul/primitives/button-link';\nimport { Image } from '~/components/image';\n\ntype ButtonLinkProps = ComponentPropsWithoutRef<typeof ButtonLink>;\n\ninterface Slide {\n  title: string;\n  description?: string;\n  showDescription?: boolean;\n  image?: { alt: string; blurDataUrl?: string; src: string };\n  cta?: {\n    label: string;\n    href: string;\n    variant?: ButtonLinkProps['variant'];\n    size?: ButtonLinkProps['size'];\n    shape?: ButtonLinkProps['shape'];\n  };\n  showCta?: boolean;\n}\n\ninterface Props {\n  slides: Slide[];\n  playOnInit?: boolean;\n  interval?: number;\n  className?: string;\n}\n\ninterface UseProgressButtonType {\n  selectedIndex: number;\n  scrollSnaps: number[];\n  onProgressButtonClick: (index: number) => void;\n}\n\nconst useProgressButton = (\n  emblaApi: EmblaCarouselType | undefined,\n  onButtonClick?: (emblaApi: EmblaCarouselType) => void,\n): UseProgressButtonType => {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  const [scrollSnaps, setScrollSnaps] = useState<number[]>([]);\n\n  const onProgressButtonClick = useCallback(\n    (index: number) => {\n      if (!emblaApi) return;\n      emblaApi.goTo(index);\n      if (onButtonClick) onButtonClick(emblaApi);\n    },\n    [emblaApi, onButtonClick],\n  );\n\n  const onInit = useCallback((emblaAPI: EmblaCarouselType) => {\n    setScrollSnaps(emblaAPI.snapList());\n  }, []);\n\n  const onSelect = useCallback((emblaAPI: EmblaCarouselType) => {\n    setSelectedIndex(emblaAPI.selectedSnap());\n  }, []);\n\n  useEffect(() => {\n    if (!emblaApi) return;\n\n    onInit(emblaApi);\n    onSelect(emblaApi);\n\n    emblaApi.on('reinit', onInit).on('reinit', onSelect).on('select', onSelect);\n  }, [emblaApi, onInit, onSelect]);\n\n  return {\n    selectedIndex,\n    scrollSnaps,\n    onProgressButtonClick,\n  };\n};\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --slideshow-focus: hsl(var(--primary));\n *   --slideshow-mask: hsl(var(--foreground) / 80%);\n *   --slideshow-background: color-mix(in oklab, hsl(var(--primary)), black 75%);\n *   --slideshow-title: hsl(var(--background));\n *   --slideshow-title-font-family: var(--font-family-heading);\n *   --slideshow-description: hsl(var(--background) / 80%);\n *   --slideshow-description-font-family: var(--font-family-body);\n *   --slideshow-pagination: hsl(var(--background));\n *   --slideshow-play-border: hsl(var(--contrast-300) / 50%);\n *   --slideshow-play-border-hover: hsl(var(--contrast-300) / 80%);\n *   --slideshow-play-text: hsl(var(--background));\n *   --slideshow-number: hsl(var(--background));\n *   --slideshow-number-font-family: var(--font-family-mono);\n * }\n * ```\n */\nexport function Slideshow({ slides, playOnInit = true, interval = 5000, className }: Props) {\n  const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, duration: 20 }, [\n    Autoplay({ delay: interval, active: playOnInit }),\n    Fade(),\n  ]);\n  const { selectedIndex, scrollSnaps, onProgressButtonClick } = useProgressButton(emblaApi);\n  const [isPlaying, setIsPlaying] = useState(false);\n  const [playCount, setPlayCount] = useState(0);\n\n  const toggleAutoplay = useCallback(() => {\n    const autoplay = emblaApi?.plugins().autoplay;\n\n    if (!autoplay) return;\n\n    const playOrStop = autoplay.isPlaying() ? autoplay.stop : autoplay.play;\n\n    playOrStop();\n  }, [emblaApi]);\n\n  const resetAutoplay = useCallback(() => {\n    const autoplay = emblaApi?.plugins().autoplay;\n\n    if (!autoplay) return;\n\n    autoplay.reset();\n  }, [emblaApi]);\n\n  useEffect(() => {\n    const autoplay = emblaApi?.plugins().autoplay;\n\n    if (!autoplay) return;\n\n    setIsPlaying(autoplay.isPlaying());\n    emblaApi\n      .on('autoplay:play', () => {\n        setIsPlaying(true);\n        setPlayCount(playCount + 1);\n      })\n      .on('autoplay:stop', () => {\n        setIsPlaying(false);\n      })\n      .on('reinit', () => {\n        setIsPlaying(autoplay.isPlaying());\n      });\n  }, [emblaApi, playCount]);\n\n  return (\n    <section\n      className={clsx(\n        'relative h-[80vh] bg-[var(--slideshow-background,color-mix(in_oklab,hsl(var(--primary)),black_75%))] @container',\n        className,\n      )}\n    >\n      <div className=\"h-full overflow-hidden\" ref={emblaRef}>\n        <div className=\"flex h-full\">\n          {slides.map(\n            ({ title, description, showDescription = true, image, cta, showCta = true }, idx) => {\n              return (\n                <div\n                  className=\"relative h-full w-full min-w-0 shrink-0 grow-0 basis-full\"\n                  key={idx}\n                >\n                  <div className=\"absolute inset-x-0 bottom-0 z-10 bg-gradient-to-t from-[var(--slideshow-mask,hsl(var(--foreground)/80%))] to-transparent\">\n                    <div className=\"mx-auto w-full max-w-screen-2xl text-balance px-4 pb-16 pt-12 @xl:px-6 @xl:pb-20 @xl:pt-16 @4xl:px-8 @4xl:pt-20\">\n                      <h1 className=\"m-0 max-w-xl font-[family-name:var(--slideshow-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none text-[var(--slideshow-title,hsl(var(--background)))] @2xl:text-5xl @2xl:leading-[.9] @4xl:text-6xl\">\n                        {title}\n                      </h1>\n                      {showDescription && (\n                        <p className=\"mt-2 max-w-xl font-[family-name:var(--slideshow-description-font-family,var(--font-family-body))] text-base leading-normal text-[var(--slideshow-description,hsl(var(--background)/80%))] @xl:mt-3 @xl:text-lg\">\n                          {description}\n                        </p>\n                      )}\n                      {showCta && (\n                        <ButtonLink\n                          className=\"mt-6 @xl:mt-8\"\n                          href={cta?.href ?? '#'}\n                          shape={cta?.shape ?? 'pill'}\n                          size={cta?.size ?? 'large'}\n                          variant={cta?.variant ?? 'tertiary'}\n                        >\n                          {cta?.label ?? 'Learn more'}\n                        </ButtonLink>\n                      )}\n                    </div>\n                  </div>\n\n                  {image?.src != null && image.src !== '' && (\n                    <Image\n                      alt={image.alt}\n                      blurDataURL={image.blurDataUrl}\n                      className=\"block h-20 w-full object-cover\"\n                      fill\n                      placeholder={\n                        image.blurDataUrl != null && image.blurDataUrl !== '' ? 'blur' : 'empty'\n                      }\n                      preload={idx === 0}\n                      sizes=\"100vw\"\n                      src={image.src}\n                    />\n                  )}\n                </div>\n              );\n            },\n          )}\n        </div>\n      </div>\n\n      {/* Controls */}\n      <div className=\"absolute bottom-4 left-1/2 flex w-full max-w-screen-2xl -translate-x-1/2 flex-wrap items-center px-4 @xl:bottom-6 @xl:px-6 @4xl:px-8\">\n        {/* Progress Buttons */}\n        {scrollSnaps.map((_: number, index: number) => {\n          return (\n            <button\n              aria-label={`View image number ${index + 1}`}\n              className=\"rounded-lg px-1.5 py-2 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-[var(--slideshow-focus,hsl(var(--primary)))]\"\n              key={index}\n              onClick={() => {\n                onProgressButtonClick(index);\n                resetAutoplay();\n              }}\n            >\n              <div className=\"relative overflow-hidden\">\n                {/* White Bar - Current Index Indicator / Progress Bar */}\n                <div\n                  className={clsx(\n                    'absolute h-0.5 bg-[var(--slideshow-pagination,hsl(var(--background)))]',\n                    'opacity-0 fill-mode-forwards',\n                    isPlaying ? 'running' : 'paused',\n                    index === selectedIndex\n                      ? 'opacity-100 ease-linear animate-in slide-in-from-left'\n                      : 'ease-out animate-out fade-out',\n                  )}\n                  key={`progress-${playCount}`} // Force the animation to restart when pressing \"Play\", to match animation with embla's autoplay timer\n                  style={{\n                    animationDuration: index === selectedIndex ? `${interval}ms` : '200ms',\n                    width: `${150 / slides.length}px`,\n                  }}\n                />\n                {/* Grey Bar BG */}\n                <div\n                  className=\"h-0.5 bg-[var(--slideshow-pagination,hsl(var(--background)))] opacity-30\"\n                  style={{ width: `${150 / slides.length}px` }}\n                />\n              </div>\n            </button>\n          );\n        })}\n\n        {/* Carousel Count - \"01/03\" */}\n        <span className=\"ml-auto mr-3 mt-px font-[family-name:var(--slideshow-number-font-family,var(--font-family-mono))] text-sm text-[var(--slideshow-number,hsl(var(--background)))]\">\n          {selectedIndex + 1 < 10 ? `0${selectedIndex + 1}` : selectedIndex + 1}/\n          {slides.length < 10 ? `0${slides.length}` : slides.length}\n        </span>\n\n        {/* Stop / Start Button */}\n        <button\n          aria-label={isPlaying ? 'Pause' : 'Play'}\n          className=\"flex h-7 w-7 items-center justify-center rounded-lg border border-[var(--slideshow-play-border,hsl(var(--contrast-300)/50%))] text-[var(--slideshow-play-text,hsl(var(--background)))] ring-[var(--slideshow-focus)] transition-opacity duration-300 hover:border-[var(--slideshow-play-border-hover,hsl(var(--contrast-300)/80%))] focus-visible:outline-0 focus-visible:ring-2\"\n          onClick={toggleAutoplay}\n          type=\"button\"\n        >\n          {isPlaying ? (\n            <Pause className=\"pointer-events-none\" size={16} strokeWidth={1.5} />\n          ) : (\n            <Play className=\"pointer-events-none\" size={16} strokeWidth={1.5} />\n          )}\n        </button>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/sticky-sidebar-layout/index.tsx",
    "content": "import { clsx } from 'clsx';\n\n// eslint-disable-next-line valid-jsdoc\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --section-max-width-medium: 768px;\n *   --section-max-width-lg: 1024px;\n *   --section-max-width-xl: 1280px;\n *   --section-max-width-2xl: 1536px;\n * }\n * ```\n */\nexport function StickySidebarLayout({\n  className,\n  sidebar,\n  children,\n  sidebarSize = '1/3',\n  sidebarPosition = 'before',\n  containerSize = '2xl',\n  hideOverflow = false,\n}: {\n  className?: string;\n  sidebar: React.ReactNode;\n  children: React.ReactNode;\n  containerSize?: 'md' | 'lg' | 'xl' | '2xl';\n  sidebarSize?: '1/4' | '1/3' | '1/2' | 'small' | 'medium' | 'large';\n  sidebarPosition?: 'before' | 'after';\n  hideOverflow?: boolean;\n}) {\n  return (\n    <section\n      className={clsx('group/pending @container', hideOverflow && 'overflow-hidden', className)}\n    >\n      <div\n        className={clsx(\n          'mx-auto flex flex-col items-stretch gap-x-16 gap-y-10 px-4 py-10 @xl:px-6 @xl:py-14 @4xl:flex-row @4xl:px-8 @4xl:py-20',\n          {\n            md: 'max-w-[var(--section-max-width-md,768px)]',\n            lg: 'max-w-[var(--section-max-width-lg,1024px)]',\n            xl: 'max-w-[var(--section-max-width-xl,1280px)]',\n            '2xl': 'max-w-[var(--section-max-width-2xl,1536px)]',\n          }[containerSize],\n        )}\n      >\n        <div\n          className={clsx(\n            'min-w-0',\n            sidebarPosition === 'after' ? 'order-2' : 'order-1',\n            {\n              '1/3': '@4xl:w-1/3',\n              '1/2': '@4xl:w-1/2',\n              '1/4': '@4xl:w-1/4',\n              small: '@4xl:w-48',\n              medium: '@4xl:w-60',\n              large: '@4xl:w-80',\n            }[sidebarSize],\n          )}\n        >\n          <div className=\"sticky top-10\">{sidebar}</div>\n        </div>\n        <div\n          className={clsx(\n            'min-w-0',\n            sidebarPosition === 'after' ? 'order-1' : 'order-2',\n            {\n              '1/3': '@4xl:w-2/3',\n              '1/2': '@4xl:w-1/2',\n              '1/4': '@4xl:w-3/4',\n              small: '@4xl:flex-1',\n              medium: '@4xl:flex-1',\n              large: '@4xl:flex-1',\n            }[sidebarSize],\n          )}\n        >\n          {children}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/subscribe/index.tsx",
    "content": "import { SubmissionResult } from '@conform-to/react';\nimport { clsx } from 'clsx';\n\nimport { InlineEmailForm } from '@/vibes/soul/primitives/inline-email-form';\nimport { Image } from '~/components/image';\n\ntype Action<State, Payload> = (state: Awaited<State>, payload: Payload) => State | Promise<State>;\n\nexport function Subscribe({\n  action,\n  image,\n  title,\n  description,\n  placeholder,\n}: {\n  action: Action<{ lastResult: SubmissionResult | null; successMessage?: string }, FormData>;\n  image?: { src: string; alt: string };\n  title: string;\n  description?: string;\n  placeholder?: string;\n}) {\n  return (\n    <section className=\"bg-primary-shadow @container\">\n      <div className=\"flex flex-col items-start @4xl:flex-row @4xl:items-stretch\">\n        {image && (\n          <div className=\"relative min-h-96 w-full bg-primary/10 @4xl:flex-1\">\n            <Image\n              alt={image.alt}\n              className=\"object-cover\"\n              fill\n              sizes=\"(min-width: 56rem) 50vw, 100vw\"\n              src={image.src}\n            />\n          </div>\n        )}\n\n        <div className=\"w-full flex-1\">\n          <div\n            className={clsx(\n              'flex w-full flex-col gap-10 px-4 py-10 @xl:px-6 @xl:py-14 @4xl:gap-16 @4xl:px-8 @4xl:py-20',\n              image != null ? '@4xl:max-w-4xl' : 'mx-auto max-w-screen-2xl @4xl:flex-row',\n            )}\n          >\n            <div className=\"flex-1\">\n              <h2 className=\"mb-4 font-heading text-2xl font-medium leading-none text-primary-highlight @xl:text-3xl @4xl:text-4xl\">\n                {title}\n              </h2>\n              <p className=\"text-primary-highlight opacity-75\">{description}</p>\n            </div>\n            <InlineEmailForm action={action} className=\"flex-1\" placeholder={placeholder} />\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/wishlist-details/index.tsx",
    "content": "import { clsx } from 'clsx';\nimport { ArrowLeft } from 'lucide-react';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { ButtonLink } from '@/vibes/soul/primitives/button-link';\nimport { CursorPagination, CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\nimport {\n  WishlistItem,\n  WishlistItemCard,\n  WishlistItemSkeleton,\n} from '@/vibes/soul/primitives/wishlist-item-card';\nimport { RemoveWishlistItemAction } from '@/vibes/soul/primitives/wishlist-item-card/remove-wishlist-item';\nimport { AddWishlistItemToCartAction } from '@/vibes/soul/primitives/wishlist-item-card/wishlist-item-add-to-cart';\n\nexport interface Wishlist {\n  id: string;\n  name: string;\n  href: string;\n  items: Streamable<WishlistItem[]>;\n  publicUrl?: string;\n  visibility: {\n    isPublic: boolean;\n    label: string;\n    publicLabel: string;\n    privateLabel: string;\n  };\n  totalItems: {\n    value: number;\n    label: string;\n  };\n}\n\ninterface Props {\n  className?: string;\n  wishlist: Streamable<Wishlist>;\n  paginationInfo?: Streamable<CursorPaginationInfo>;\n  prevHref?: string;\n  emptyStateText?: Streamable<string | null>;\n  placeholderCount?: number;\n  headerActions?: React.ReactNode | ((wishlist?: Wishlist) => React.ReactNode);\n  action: AddWishlistItemToCartAction;\n  removeAction?: RemoveWishlistItemAction;\n  removeButtonTitle?: string;\n}\n\nexport const WishlistDetails = ({\n  className = '',\n  wishlist: streamableWishlist,\n  emptyStateText,\n  paginationInfo,\n  headerActions,\n  prevHref,\n  placeholderCount,\n  action,\n  removeAction,\n  removeButtonTitle,\n}: Props) => {\n  return (\n    <Stream\n      fallback={\n        <WishlistDetailSkeleton\n          className={className}\n          headerActions={typeof headerActions === 'function' ? headerActions() : headerActions}\n          placeholderCount={placeholderCount}\n          prevHref={prevHref}\n        />\n      }\n      value={streamableWishlist}\n    >\n      {(wishlist) => {\n        const { name, totalItems, items } = wishlist;\n\n        return (\n          <section className={clsx('w-full @container', className)}>\n            <header className=\"mb-4 flex flex-col gap-4 @lg:flex-row @lg:justify-between\">\n              <div className=\"flex flex-1 gap-2\">\n                {prevHref != null && prevHref !== '' && (\n                  <ButtonLink href={prevHref} shape=\"circle\" size=\"small\" variant=\"ghost\">\n                    <ArrowLeft />\n                  </ButtonLink>\n                )}\n                <div className=\"flex flex-1 flex-col gap-2\">\n                  <h1 className=\"font-heading text-3xl font-medium leading-none @7xl:text-5xl\">\n                    {name}\n                  </h1>\n                  <div className=\"text-sm text-contrast-500 @7xl:text-base\">{totalItems.label}</div>\n                </div>\n              </div>\n              {typeof headerActions === 'function' ? headerActions(wishlist) : headerActions}\n            </header>\n\n            <WishlistItems\n              action={action}\n              emptyStateText={emptyStateText}\n              items={items}\n              placeholderCount={placeholderCount}\n              removeAction={removeAction}\n              removeButtonTitle={removeButtonTitle}\n              wishlistId={wishlist.id}\n            />\n\n            {paginationInfo && <CursorPagination info={paginationInfo} />}\n          </section>\n        );\n      }}\n    </Stream>\n  );\n};\n\nfunction WishlistItems({\n  wishlistId,\n  emptyStateText,\n  items: streamableWishlistItems,\n  placeholderCount,\n  action,\n  removeAction,\n  removeButtonTitle,\n}: {\n  wishlistId: string;\n  items: Streamable<WishlistItem[]>;\n  emptyStateText?: Streamable<string | null>;\n  placeholderCount?: number;\n  action: AddWishlistItemToCartAction;\n  removeAction?: RemoveWishlistItemAction;\n  removeButtonTitle?: string;\n}) {\n  return (\n    <Stream\n      fallback={<WishlistItemsSkeleton pending placeholderCount={8} />}\n      value={streamableWishlistItems}\n    >\n      {(items) => {\n        if (items.length === 0) {\n          return (\n            <WishlistItemsEmptyState\n              emptyStateText={emptyStateText}\n              placeholderCount={placeholderCount}\n            />\n          );\n        }\n\n        return (\n          <div className=\"w-full @container\">\n            <div className=\"mx-auto grid grid-cols-2 gap-x-4 gap-y-6 @sm:grid-cols-3 @2xl:grid-cols-4 @2xl:gap-x-5 @2xl:gap-y-8 @5xl:grid-cols-5 @7xl:grid-cols-6\">\n              {items.map((item, index) => (\n                <WishlistItemCard\n                  action={action}\n                  item={item}\n                  key={index}\n                  removeAction={removeAction}\n                  removeButtonTitle={removeButtonTitle}\n                  wishlistId={wishlistId}\n                />\n              ))}\n            </div>\n          </div>\n        );\n      }}\n    </Stream>\n  );\n}\n\nfunction WishlistItemsEmptyState({\n  emptyStateText = \"You haven't added products to your wish list.\",\n  placeholderCount = 8,\n}: {\n  emptyStateText?: Streamable<string | null>;\n  placeholderCount?: number;\n}) {\n  return (\n    <div className=\"relative\">\n      <div className=\"[mask-image:linear-gradient(to_bottom,_black_25%,_transparent_100%)]\">\n        <WishlistItemsSkeleton placeholderCount={placeholderCount} />\n      </div>\n      <div className=\"absolute inset-0 mx-auto px-3 py-24 pb-3 @4xl:px-10 @4xl:pb-10 @4xl:pt-24\">\n        <div className=\"mx-auto max-w-xl space-y-2 text-center @4xl:space-y-3\">\n          <p className=\"text-sm text-contrast-500 @4xl:text-lg\">{emptyStateText}</p>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction WishlistItemsSkeleton({\n  className = '',\n  placeholderCount = 8,\n  pending = false,\n}: {\n  className?: string;\n  placeholderCount?: number;\n  pending?: boolean;\n}) {\n  return (\n    <div\n      className={clsx(\n        'mx-auto grid grid-cols-2 gap-x-4 gap-y-6 [mask-image:linear-gradient(to_bottom,_black_0%,_transparent_100%)] @sm:grid-cols-3 @2xl:grid-cols-4 @2xl:gap-x-5 @2xl:gap-y-8 @5xl:grid-cols-5 @7xl:grid-cols-6',\n        pending ? 'animate-pulse' : '',\n        className,\n      )}\n    >\n      {Array.from({ length: placeholderCount }).map((_, index) => (\n        <WishlistItemSkeleton key={index} />\n      ))}\n    </div>\n  );\n}\n\nfunction WishlistDetailSkeleton({\n  className = '',\n  prevHref,\n  placeholderCount = 8,\n  headerActions,\n}: {\n  prevHref?: string;\n  className?: string;\n  placeholderCount?: number;\n  headerActions?: React.ReactNode;\n}) {\n  return (\n    <section className={clsx('w-full animate-pulse @container', className)}>\n      <header className=\"mb-4 flex flex-col gap-4 @lg:flex-row @lg:justify-between\">\n        <div className=\"flex flex-1 gap-2\">\n          {prevHref != null &&\n            prevHref !== '' &&\n            (prevHref ? (\n              <ButtonLink href={prevHref} shape=\"circle\" size=\"small\" variant=\"ghost\">\n                <ArrowLeft />\n              </ButtonLink>\n            ) : (\n              <Skeleton.Box className=\"h-10 w-10 rounded-full\" />\n            ))}\n          <div className=\"flex flex-1 flex-col gap-2\">\n            <Skeleton.Text\n              characterCount={12}\n              className=\"rounded text-3xl leading-none @7xl:text-5xl\"\n            />\n            <Skeleton.Text characterCount={5} className=\"rounded text-sm @7xl:text-base\" />\n          </div>\n        </div>\n        {headerActions}\n      </header>\n\n      <WishlistItemsSkeleton placeholderCount={placeholderCount} />\n    </section>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/wishlist-list/index.tsx",
    "content": "import { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Wishlist } from '@/vibes/soul/sections/wishlist-details';\nimport {\n  WishlistItemActions,\n  WishlistListItem,\n  WishlistListItemSkeleton,\n} from '@/vibes/soul/sections/wishlist-list-item';\n\ninterface Props {\n  wishlists: Streamable<Wishlist[]>;\n  emptyStateCallToAction?: React.ReactNode;\n  emptyStateTitle?: Streamable<string | null>;\n  emptyWishlistStateText?: Streamable<string | null>;\n  viewWishlistLabel?: string;\n  itemActions?: WishlistItemActions;\n}\n\nexport const WishlistList = ({\n  wishlists: streamableWishlists,\n  emptyStateCallToAction,\n  emptyStateTitle,\n  emptyWishlistStateText,\n  viewWishlistLabel,\n  itemActions,\n}: Props) => {\n  return (\n    <div className=\"@container\">\n      <Stream\n        fallback={<WishlistListSkeleton itemActions={itemActions} pending />}\n        value={streamableWishlists}\n      >\n        {(wishlists) => {\n          if (wishlists.length === 0) {\n            return (\n              <WishlistListEmptyState\n                emptyStateCallToAction={emptyStateCallToAction}\n                emptyStateTitle={emptyStateTitle}\n                itemActions={itemActions}\n              />\n            );\n          }\n\n          return wishlists.map((wishlist) => (\n            <WishlistListItem\n              className=\"border-b border-b-contrast-100 last:border-b-transparent\"\n              emptyStateText={emptyWishlistStateText}\n              itemActions={itemActions}\n              key={wishlist.id}\n              viewWishlistLabel={viewWishlistLabel}\n              wishlist={wishlist}\n            />\n          ));\n        }}\n      </Stream>\n    </div>\n  );\n};\n\nfunction WishlistListEmptyState({\n  emptyStateCallToAction,\n  emptyStateTitle = \"You don't have any wish list\",\n}: Omit<Props, 'wishlists'>) {\n  return (\n    <div className=\"@container\">\n      <div className=\"py-20\">\n        <header className=\"mx-auto flex max-w-2xl flex-col items-center gap-5\">\n          <h2 className=\"text-center text-lg font-semibold text-[var(--order-list-empty-state-title,hsl(var(--foreground)))]\">\n            {emptyStateTitle}\n          </h2>\n          {emptyStateCallToAction}\n        </header>\n      </div>\n    </div>\n  );\n}\n\nfunction WishlistListSkeleton({\n  itemActions,\n  placeholderCount = 1,\n  pending = false,\n}: {\n  itemActions?: WishlistItemActions;\n  placeholderCount?: number;\n  pending?: boolean;\n}) {\n  return Array.from({ length: placeholderCount }).map((_, index) => (\n    <WishlistListItemSkeleton itemActions={itemActions} key={index} pending={pending} />\n  ));\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/wishlist-list-item/index.tsx",
    "content": "import { clsx } from 'clsx';\n\nimport { Stream, Streamable } from '@/vibes/soul/lib/streamable';\nimport { Badge } from '@/vibes/soul/primitives/badge';\nimport { ButtonLink } from '@/vibes/soul/primitives/button-link';\nimport { ProductCard, ProductCardSkeleton } from '@/vibes/soul/primitives/product-card';\nimport * as Skeleton from '@/vibes/soul/primitives/skeleton';\nimport { WishlistItem } from '@/vibes/soul/primitives/wishlist-item-card';\nimport { Wishlist } from '@/vibes/soul/sections/wishlist-details';\n\nexport interface WishlistItemActions {\n  component: (wishlist?: Wishlist) => React.ReactNode;\n  position?: 'left' | 'right';\n}\n\ninterface Props {\n  wishlist: Streamable<Wishlist>;\n  itemActions?: WishlistItemActions;\n  viewWishlistLabel?: string;\n  className?: string;\n  placeholderCount?: number;\n  emptyStateText?: Streamable<string | null>;\n}\n\nexport const WishlistListItem = ({\n  className = '',\n  itemActions,\n  wishlist: streamableWishlist,\n  viewWishlistLabel = 'View list',\n  placeholderCount,\n  emptyStateText,\n}: Props) => {\n  const { component: actionsComponent, position: actionsPosition = 'right' } = itemActions ?? {};\n\n  return (\n    <Stream\n      fallback={\n        <WishlistListItemSkeleton\n          itemActions={itemActions}\n          pending\n          placeholderCount={placeholderCount}\n        />\n      }\n      value={streamableWishlist}\n    >\n      {(wishlist) => {\n        const { name, visibility, items, totalItems, href, id } = wishlist;\n\n        return (\n          <section\n            aria-describedby={`wishlist-description-${id}`}\n            aria-labelledby={`wishlist-title-${id}`}\n            className={clsx('my-4 flex flex-col @container', className)}\n          >\n            <div className=\"flex flex-1 flex-col justify-between @sm:flex-row @sm:items-center\">\n              <div className=\"flex flex-col\">\n                <div className=\"flex items-center gap-2\">\n                  <h2 className=\"text-lg font-semibold\" id={`wishlist-title-${id}`}>\n                    {name}\n                  </h2>\n                  <Badge variant={visibility.isPublic ? 'primary' : 'info'}>\n                    {visibility.label}\n                  </Badge>\n                </div>\n                <div className=\"text-sm text-contrast-500\" id={`wishlist-description-${id}`}>\n                  {totalItems.label}\n                </div>\n              </div>\n              <div className=\"my-4 flex gap-2 whitespace-nowrap @sm:my-0 @sm:ml-2 @sm:items-center\">\n                {actionsPosition === 'left' && actionsComponent?.(wishlist)}\n                <ButtonLink className=\"flex-1\" href={href} size=\"small\" variant=\"primary\">\n                  {viewWishlistLabel}\n                </ButtonLink>\n                {actionsPosition === 'right' && actionsComponent?.(wishlist)}\n              </div>\n            </div>\n            <WishlistListItemItems\n              emptyStateText={emptyStateText}\n              items={items}\n              placeholderCount={placeholderCount}\n            />\n          </section>\n        );\n      }}\n    </Stream>\n  );\n};\n\nfunction WishlistListItemItems({\n  emptyStateText,\n  items: streamableWishlistItems,\n  placeholderCount,\n}: {\n  items: Streamable<WishlistItem[]>;\n  emptyStateText?: Streamable<string | null>;\n  placeholderCount?: number;\n}) {\n  return (\n    <Stream\n      fallback={<WishlistListItemItemsSkeleton placeholderCount={placeholderCount} />}\n      value={streamableWishlistItems}\n    >\n      {(items) => {\n        if (items.length === 0) {\n          return (\n            <WishlistListItemItemsEmptyState\n              emptyStateText={emptyStateText}\n              placeholderCount={placeholderCount}\n            />\n          );\n        }\n\n        return (\n          <div className=\"my-8 flex flex-1 gap-4 overflow-hidden [mask-image:linear-gradient(to_right,_black_70%,_transparent_100%)]\">\n            {items.map(({ product }) => (\n              <div className=\"min-w-36\" key={product.id}>\n                <ProductCard aspectRatio=\"1:1\" product={product} />\n              </div>\n            ))}\n          </div>\n        );\n      }}\n    </Stream>\n  );\n}\n\nfunction WishlistListItemItemsEmptyState({\n  emptyStateText = \"You haven't added products to your wish list.\",\n  placeholderCount = 8,\n}: {\n  emptyStateText?: Streamable<string | null>;\n  placeholderCount?: number;\n}) {\n  return (\n    <div className=\"relative\">\n      <div className=\"[mask-image:linear-gradient(to_bottom,_black_25%,_transparent_100%)]\">\n        <WishlistListItemItemsSkeleton placeholderCount={placeholderCount} />\n      </div>\n      <div className=\"absolute inset-0 mx-auto px-3 py-24 pb-3 @4xl:px-10 @4xl:pb-10 @4xl:pt-24\">\n        <div className=\"mx-auto max-w-xl space-y-2 text-center @4xl:space-y-3\">\n          <p className=\"text-sm text-contrast-500 @4xl:text-lg\">{emptyStateText}</p>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction WishlistListItemItemsSkeleton({\n  className = '',\n  placeholderCount = 8,\n}: {\n  className?: string;\n  placeholderCount?: number;\n}) {\n  return (\n    <div className=\"my-8 flex flex-1 gap-4 overflow-hidden [mask-image:linear-gradient(to_right,_black_70%,_transparent_100%)]\">\n      {Array.from({ length: placeholderCount }).map((_, index) => (\n        <div className={clsx('min-w-36', className)} key={index}>\n          <ProductCardSkeleton aspectRatio=\"1:1\" />\n        </div>\n      ))}\n    </div>\n  );\n}\n\nexport function WishlistListItemSkeleton({\n  className = '',\n  itemActions,\n  placeholderCount,\n  pending = false,\n}: {\n  className?: string;\n  itemActions?: WishlistItemActions;\n  placeholderCount?: number;\n  pending?: boolean;\n}) {\n  const { component, position: actionsPosition = 'right' } = itemActions ?? {};\n\n  return (\n    <div\n      className={clsx('my-4 flex flex-col @container', pending ? 'animate-pulse' : '', className)}\n      data-pending={pending ? '' : undefined}\n    >\n      <div className=\"flex flex-1 flex-col justify-between @sm:flex-row @sm:items-center\">\n        <div className=\"flex flex-col\">\n          <div className=\"flex items-center gap-2\">\n            <Skeleton.Text characterCount={12} className=\"rounded text-lg\" />\n            <Skeleton.Text characterCount={5} className=\"rounded px-2 py-0.5\" />\n          </div>\n          <Skeleton.Text characterCount={5} className=\"rounded\" />\n        </div>\n        <div className=\"my-4 flex gap-2 @sm:my-0 @sm:ml-2 @sm:items-center\">\n          {actionsPosition === 'left' && component?.()}\n          <Skeleton.Box className=\"h-10 min-w-[9ch] flex-1 rounded-full\" />\n          {actionsPosition === 'right' && component?.()}\n        </div>\n      </div>\n      <WishlistListItemItemsSkeleton placeholderCount={placeholderCount} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "core/vibes/soul/sections/wishlists-section/index.tsx",
    "content": "import { Streamable } from '@/vibes/soul/lib/streamable';\nimport { CursorPagination, CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination';\nimport { Wishlist } from '@/vibes/soul/sections/wishlist-details';\nimport { WishlistList } from '@/vibes/soul/sections/wishlist-list';\nimport { WishlistItemActions } from '@/vibes/soul/sections/wishlist-list-item';\n\ninterface Props {\n  title: string;\n  wishlists: Streamable<Wishlist[]>;\n  paginationInfo?: Streamable<CursorPaginationInfo>;\n  emptyStateCallToAction?: React.ReactNode;\n  emptyStateTitle?: Streamable<string | null>;\n  emptyWishlistStateText?: Streamable<string | null>;\n  viewWishlistLabel?: string;\n  actions?: React.ReactNode;\n  itemActions?: WishlistItemActions;\n}\n\n/**\n * This component supports various CSS variables for theming. Here's a comprehensive list, along\n * with their default values:\n *\n * ```css\n * :root {\n *   --wishlists-section-title-font-family: var(--font-family-heading);\n *   --wishlists-section-title: hsl(var(--foreground));\n *   --wishlists-section-border: hsl(var(--contrast-100));\n * }\n * ```\n */\n\nexport const WishlistsSection = ({\n  title,\n  wishlists,\n  paginationInfo,\n  emptyStateCallToAction,\n  emptyStateTitle,\n  emptyWishlistStateText,\n  viewWishlistLabel,\n  actions,\n  itemActions,\n}: Props) => {\n  return (\n    <section className=\"w-full\">\n      <header className=\"mb-4 border-[var(--wishlists-section-border,hsl(var(--contrast-100)))] @2xl:min-h-[72px] @2xl:border-b\">\n        <div className=\"mb-4 flex items-center justify-between\">\n          <h1 className=\"hidden font-[family-name:var(--wishlists-section-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none tracking-tight text-[var(--wishlists-section-title,hsl(var(--foreground)))] @2xl:block\">\n            {title}\n          </h1>\n          {actions}\n        </div>\n      </header>\n\n      <WishlistList\n        emptyStateCallToAction={emptyStateCallToAction}\n        emptyStateTitle={emptyStateTitle}\n        emptyWishlistStateText={emptyWishlistStateText}\n        itemActions={itemActions}\n        viewWishlistLabel={viewWishlistLabel}\n        wishlists={wishlists}\n      />\n\n      {paginationInfo && <CursorPagination info={paginationInfo} />}\n    </section>\n  );\n};\n"
  },
  {
    "path": "graphql.config.json",
    "content": "{\n  \"schema\": \"core/schema.graphql\",\n  \"documents\": [\n    \"core/client/queries/**/*.ts\",\n    \"core/client/mutations/**/*.ts\",\n    \"core/client/fragments/**/*.ts\"\n  ]\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@bigcommerce/catalyst\",\n  \"description\": \"Catalyst is BigCommerce's composable front-end framework designed for building custom ecommerce storefronts.\",\n  \"keywords\": [\n    \"bigcommerce\",\n    \"ecommerce\",\n    \"storefront\",\n    \"headless\",\n    \"composable\",\n    \"nextjs\"\n  ],\n  \"license\": \"MIT\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"packageManager\": \"pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184\",\n  \"scripts\": {\n    \"dev\": \"dotenv -e .env.local -- turbo run dev\",\n    \"build\": \"dotenv -e .env.local -- turbo run build\",\n    \"lint\": \"dotenv -e .env.local -- turbo lint\",\n    \"test\": \"turbo run test\",\n    \"test:scripts\": \"node --test .github/scripts/__tests__/*.test.mts\",\n    \"typecheck\": \"turbo typecheck\"\n  },\n  \"devDependencies\": {\n    \"@changesets/changelog-github\": \"^0.5.1\",\n    \"@changesets/cli\": \"^2.29.4\",\n    \"@types/node\": \"^22.15.30\",\n    \"dotenv-cli\": \"^8.0.0\",\n    \"prettier\": \"^3.6.2\",\n    \"prettier-plugin-tailwindcss\": \"^0.6.12\",\n    \"turbo\": \"^2.5.4\",\n    \"typescript\": \"^5.8.3\",\n    \"unlighthouse\": \"^0.16.3\"\n  }\n}\n"
  },
  {
    "path": "packages/catalyst/.eslintrc.cjs",
    "content": "// @ts-check\n\n/** @type {import('eslint').Linter.LegacyConfig} */\nconst config = {\n  root: true,\n  extends: ['@bigcommerce/catalyst/base', '@bigcommerce/catalyst/prettier'],\n  ignorePatterns: ['/dist/**'],\n  rules: {\n    '@typescript-eslint/naming-convention': 'off',\n  },\n  overrides: [\n    {\n      files: ['./src/cli/**'],\n      rules: {\n        'no-console': 'off',\n      },\n    },\n  ],\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "packages/catalyst/README.md",
    "content": "# @bigcommerce/catalyst\n\nCLI\n"
  },
  {
    "path": "packages/catalyst/commander.d.ts",
    "content": "declare module 'commander' {\n  export * from '@commander-js/extra-typings';\n}\n"
  },
  {
    "path": "packages/catalyst/package.json",
    "content": "{\n  \"name\": \"@bigcommerce/catalyst\",\n  \"version\": \"0.1.0\",\n  \"type\": \"module\",\n  \"bin\": {\n    \"catalyst\": \"dist/cli.js\"\n  },\n  \"files\": [\n    \"dist\",\n    \"templates\"\n  ],\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"lint\": \"eslint . --max-warnings 0\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"test:coverage\": \"vitest run --coverage\",\n    \"build\": \"tsup\"\n  },\n  \"engines\": {\n    \"node\": \"^20.0.0 || ^22.0.0 || ^24.0.0\"\n  },\n  \"dependencies\": {\n    \"@segment/analytics-node\": \"^2.2.1\",\n    \"adm-zip\": \"^0.5.16\",\n    \"commander\": \"^14.0.0\",\n    \"conf\": \"^13.1.0\",\n    \"consola\": \"^3.4.2\",\n    \"dotenv\": \"^16.5.0\",\n    \"execa\": \"^9.6.0\",\n    \"yocto-spinner\": \"^1.0.0\",\n    \"zod\": \"^4.0.5\"\n  },\n  \"devDependencies\": {\n    \"@bigcommerce/eslint-config\": \"^2.11.0\",\n    \"@bigcommerce/eslint-config-catalyst\": \"workspace:^\",\n    \"@commander-js/extra-typings\": \"^14.0.0\",\n    \"@types/adm-zip\": \"^0.5.7\",\n    \"@types/node\": \"^22.15.30\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"@vitest/ui\": \"^3.2.4\",\n    \"eslint\": \"^8.57.1\",\n    \"msw\": \"^2.9.0\",\n    \"prettier\": \"^3.6.2\",\n    \"tsup\": \"^8.5.0\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"@opennextjs/cloudflare\": \"^1.8.0\"\n  }\n}\n"
  },
  {
    "path": "packages/catalyst/prettier.config.cjs",
    "content": "// @ts-check\n\n/** @type {import(\"prettier\").Config} */\nconst config = {\n  printWidth: 100,\n  singleQuote: true,\n  trailingComma: 'all',\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "packages/catalyst/src/cli/commands/build.spec.ts",
    "content": "import { Command } from 'commander';\nimport { execa } from 'execa';\nimport { join } from 'node:path';\nimport { expect, test, vi } from 'vitest';\n\nimport { program } from '../program';\n\nimport { build } from './build';\n\n// eslint-disable-next-line @typescript-eslint/consistent-type-assertions\nvi.spyOn(process, 'exit').mockImplementation(() => null as never);\n\nvi.mock('node:fs', () => ({\n  existsSync: vi.fn(() => true),\n}));\n\nvi.mock('execa', () => ({\n  execa: vi.fn(() => Promise.resolve({ stdout: '' })),\n  __esModule: true,\n}));\n\ntest('properly configured Command instance', () => {\n  expect(build).toBeInstanceOf(Command);\n  expect(build.name()).toBe('build');\n  expect(build.options).toEqual(\n    expect.arrayContaining([\n      expect.objectContaining({ long: '--framework' }),\n      expect.objectContaining({ long: '--project-uuid' }),\n    ]),\n  );\n});\n\ntest('calls execa with Next.js build if framework is nextjs', async () => {\n  await program.parseAsync(['node', 'catalyst', 'build', '--framework', 'nextjs', '--debug']);\n\n  expect(execa).toHaveBeenCalledWith(\n    join('node_modules', '.bin', 'next'),\n    ['build', '--debug'],\n    expect.objectContaining({\n      stdio: 'inherit',\n      cwd: process.cwd(),\n    }),\n  );\n});\n"
  },
  {
    "path": "packages/catalyst/src/cli/commands/build.ts",
    "content": "import { Command, Option } from 'commander';\nimport { execa } from 'execa';\nimport { existsSync } from 'node:fs';\nimport { copyFile, cp, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { getModuleCliPath } from '../lib/get-module-cli-path';\nimport { consola } from '../lib/logger';\nimport { getProjectConfig } from '../lib/project-config';\nimport { getWranglerConfig } from '../lib/wrangler-config';\n\nconst WRANGLER_VERSION = '4.24.3';\n\nexport const build = new Command('build')\n  .allowUnknownOption()\n  // The unknown options end up in program.args, not in program.opts(). Commander does not take a guess at how to interpret the unknown options.\n  .argument(\n    '[next-build-options...]',\n    'Next.js `build` options (see: https://nextjs.org/docs/app/api-reference/cli/next#next-build-options)',\n  )\n  .addOption(\n    new Option(\n      '--project-uuid <uuid>',\n      'Project UUID to be included in the deployment configuration.',\n    ).env('BIGCOMMERCE_PROJECT_UUID'),\n  )\n  .addOption(\n    new Option('--framework <framework>', 'The framework to use for the build.').choices([\n      'nextjs',\n      'catalyst',\n    ]),\n  )\n  .action(async (nextBuildOptions, options) => {\n    const coreDir = process.cwd();\n\n    try {\n      const config = getProjectConfig();\n      const framework = options.framework ?? config.get('framework');\n\n      if (framework === 'nextjs') {\n        const nextBin = join('node_modules', '.bin', 'next');\n\n        if (!existsSync(nextBin)) {\n          throw new Error(\n            `Next.js is not installed in ${coreDir}. Are you in a valid Next.js project?`,\n          );\n        }\n\n        await execa(nextBin, ['build', ...nextBuildOptions], {\n          stdio: 'inherit',\n          cwd: coreDir,\n        });\n      }\n\n      if (framework === 'catalyst') {\n        const openNextOutDir = join(coreDir, '.open-next');\n        const bigcommerceDistDir = join(coreDir, '.bigcommerce', 'dist');\n\n        const projectUuid = options.projectUuid ?? config.get('projectUuid');\n\n        if (!projectUuid) {\n          throw new Error(\n            'Project UUID is required. Please run `catalyst project create` or `catalyst project link` or this command again with --project-uuid <uuid>.',\n          );\n        }\n\n        const wranglerConfig = getWranglerConfig(projectUuid, 'PLACEHOLDER_KV_ID');\n\n        consola.start('Copying templates...');\n\n        await copyFile(\n          join(getModuleCliPath(), 'templates', 'open-next.config.ts'),\n          join(coreDir, '.bigcommerce', 'open-next.config.ts'),\n        );\n        await writeFile(\n          join(coreDir, '.bigcommerce', 'wrangler.jsonc'),\n          JSON.stringify(wranglerConfig, null, 2),\n        );\n\n        consola.success('Templates copied');\n\n        consola.start('Building project...');\n\n        await execa(\n          'pnpm',\n          [\n            'exec',\n            'opennextjs-cloudflare',\n            'build',\n            '--skipWranglerConfigCheck',\n            '--openNextConfigPath',\n            join(coreDir, '.bigcommerce', 'open-next.config.ts'),\n          ],\n          {\n            stdout: ['pipe', 'inherit'],\n            cwd: coreDir,\n          },\n        );\n\n        await execa(\n          'pnpm',\n          [\n            'dlx',\n            `wrangler@${WRANGLER_VERSION}`,\n            'deploy',\n            '--config',\n            join(coreDir, '.bigcommerce', 'wrangler.jsonc'),\n            '--keep-vars',\n            '--outdir',\n            bigcommerceDistDir,\n            '--dry-run',\n          ],\n          {\n            stdout: ['pipe', 'inherit'],\n            cwd: coreDir,\n          },\n        );\n\n        consola.success('Project built');\n\n        await cp(join(openNextOutDir, 'assets'), join(bigcommerceDistDir, 'assets'), {\n          recursive: true,\n          force: true,\n        });\n      }\n    } catch (error) {\n      consola.error(error);\n      process.exit(1);\n    }\n  });\n"
  },
  {
    "path": "packages/catalyst/src/cli/commands/deploy.spec.ts",
    "content": "import AdmZip from 'adm-zip';\nimport { Command } from 'commander';\nimport { http, HttpResponse } from 'msw';\nimport { mkdir, realpath, stat, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport {\n  afterAll,\n  afterEach,\n  beforeAll,\n  beforeEach,\n  describe,\n  expect,\n  MockInstance,\n  test,\n  vi,\n} from 'vitest';\n\nimport { server } from '../../../tests/mocks/node';\nimport { textHistory } from '../../../tests/mocks/spinner';\nimport { consola } from '../lib/logger';\nimport { mkTempDir } from '../lib/mk-temp-dir';\nimport { program } from '../program';\n\nimport {\n  createDeployment,\n  deploy,\n  generateBundleZip,\n  generateUploadSignature,\n  getDeploymentStatus,\n  parseEnvironmentVariables,\n  uploadBundleZip,\n} from './deploy';\n\n// eslint-disable-next-line import/dynamic-import-chunkname\nvi.mock('yocto-spinner', () => import('../../../tests/mocks/spinner'));\n\nlet exitMock: MockInstance;\n\nlet tmpDir: string;\nlet cleanup: () => Promise<void>;\nlet outputZip: string;\n\nconst projectUuid = 'a23f5785-fd99-4a94-9fb3-945551623923';\nconst storeHash = 'test-store';\nconst accessToken = 'test-token';\nconst apiHost = 'api.bigcommerce.com';\nconst uploadUuid = '0e93ce5f-6f91-4236-87ec-ca79627f31ba';\nconst uploadUrl = 'https://mock-upload-url.com';\nconst deploymentUuid = '5b29c3c0-5f68-44fe-99e5-06492babf7be';\n\nbeforeAll(async () => {\n  consola.mockTypes(() => vi.fn());\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  exitMock = vi.spyOn(process, 'exit').mockImplementation(() => null as never);\n\n  [tmpDir, cleanup] = await mkTempDir();\n\n  // Normalize to /private/var to avoid /var vs /private/var mismatches\n  tmpDir = await realpath(tmpDir);\n\n  const workerPath = join(tmpDir, '.bigcommerce', 'dist', 'worker.js');\n  const assetsDir = join(tmpDir, '.bigcommerce', 'dist', 'assets');\n\n  outputZip = join(tmpDir, '.bigcommerce', 'bundle.zip');\n\n  await mkdir(dirname(workerPath), { recursive: true });\n  await writeFile(workerPath, 'console.log(\"worker\");');\n  await mkdir(assetsDir, { recursive: true });\n  await writeFile(join(assetsDir, 'test.txt'), 'asset file');\n});\n\nbeforeEach(() => {\n  process.chdir(tmpDir);\n});\n\nafterEach(() => {\n  vi.clearAllMocks();\n\n  // Resets spinner text history\n  textHistory.length = 0;\n});\n\nafterAll(async () => {\n  await cleanup();\n});\n\ntest('properly configured Command instance', () => {\n  expect(deploy).toBeInstanceOf(Command);\n  expect(deploy.name()).toBe('deploy');\n  expect(deploy.description()).toBe('Deploy your application to Cloudflare.');\n  expect(deploy.options).toEqual(\n    expect.arrayContaining([\n      expect.objectContaining({ flags: '--store-hash <hash>' }),\n      expect.objectContaining({ flags: '--access-token <token>' }),\n      expect.objectContaining({ flags: '--api-host <host>', defaultValue: 'api.bigcommerce.com' }),\n      expect.objectContaining({ flags: '--project-uuid <uuid>' }),\n      expect.objectContaining({ flags: '--secret <secrets...>' }),\n      expect.objectContaining({ flags: '--dry-run' }),\n    ]),\n  );\n});\n\ndescribe('bundle zip generation and upload', () => {\n  test('creates bundle.zip from build output', async () => {\n    await generateBundleZip();\n\n    // Check file exists\n    const stats = await stat(outputZip);\n\n    expect(stats.size).toBeGreaterThan(0);\n\n    expect(consola.info).toHaveBeenCalledWith('Generating bundle...');\n    expect(consola.success).toHaveBeenCalledWith(`Bundle created at: ${outputZip}`);\n  });\n\n  test('zip contains output folder with assets and worker.js', async () => {\n    await generateBundleZip();\n\n    // Check file exists\n    const stats = await stat(outputZip);\n\n    expect(stats.size).toBeGreaterThan(0);\n\n    const zip = new AdmZip(outputZip);\n    const entries = zip.getEntries().map((e) => e.entryName);\n\n    // Check for output/ folder\n    expect(entries.every((e) => e.startsWith('output/'))).toBe(true);\n    // Check for output/assets/ directory\n    expect(entries.some((e) => e.startsWith('output/assets/'))).toBe(true);\n    // Check for output/worker.js\n    expect(entries).toContain('output/worker.js');\n\n    expect(consola.success).toHaveBeenCalledWith(`Bundle created at: ${outputZip}`);\n  });\n\n  test('fetches upload signature', async () => {\n    const signature = await generateUploadSignature(storeHash, accessToken, apiHost);\n\n    expect(consola.info).toHaveBeenCalledWith('Generating upload signature...');\n    expect(consola.success).toHaveBeenCalledWith('Upload signature generated.');\n\n    expect(signature.upload_url).toBe(uploadUrl);\n    expect(signature.upload_uuid).toBe(uploadUuid);\n  });\n\n  test('fetches upload signature and uploads bundle zip', async () => {\n    const uploadResult = await uploadBundleZip(uploadUrl);\n\n    expect(consola.info).toHaveBeenCalledWith('Uploading bundle...');\n    expect(consola.success).toHaveBeenCalledWith('Bundle uploaded successfully.');\n\n    expect(uploadResult).toBe(true);\n  });\n});\n\ndescribe('deployment and event streaming', () => {\n  test('creates a deployment', async () => {\n    const deployment = await createDeployment(\n      projectUuid,\n      uploadUuid,\n      storeHash,\n      accessToken,\n      apiHost,\n    );\n\n    expect(deployment.deployment_uuid).toBe(deploymentUuid);\n  });\n\n  test('streams deployment status until completion', async () => {\n    await getDeploymentStatus(deploymentUuid, storeHash, accessToken, apiHost);\n\n    expect(consola.info).toHaveBeenCalledWith('Fetching deployment status...');\n\n    expect(textHistory).toEqual([\n      'Fetching...',\n      'Processing...',\n      'Finalizing...',\n      'Deployment completed successfully.',\n    ]);\n  });\n\n  test('warns if event stream is incomplete or unable to be parsed', async () => {\n    const encoder = new TextEncoder();\n\n    server.use(\n      http.get(\n        'https://:apiHost/stores/:storeHash/v3/infrastructure/deployments/:deploymentUuid/events',\n        () => {\n          const stream = new ReadableStream({\n            start(controller) {\n              controller.enqueue(\n                encoder.encode(\n                  `data: {\"deployment_status\":\"in_progress\",\"deployment_uuid\":\"${deploymentUuid}\",\"event\":{\"step\":\"processing\",\"progress\":75}}`,\n                ),\n              );\n              setTimeout(() => {\n                // Incomplete stream data\n                controller.enqueue(encoder.encode(`data: {\"deployment_status\":\"in_progress\",`));\n              }, 10);\n              setTimeout(() => {\n                controller.enqueue(\n                  encoder.encode(\n                    `data: {\"deployment_status\":\"in_progress\",\"deployment_uuid\":\"${deploymentUuid}\",\"event\":{\"step\":\"finalizing\",\"progress\":99}}`,\n                  ),\n                );\n                controller.close();\n              }, 20);\n            },\n          });\n\n          return new HttpResponse(stream, {\n            status: 200,\n            headers: { 'Content-Type': 'text/event-stream' },\n          });\n        },\n      ),\n    );\n\n    await getDeploymentStatus(deploymentUuid, storeHash, accessToken, apiHost);\n\n    expect(consola.info).toHaveBeenCalledWith('Fetching deployment status...');\n\n    expect(textHistory).toEqual([\n      'Fetching...',\n      'Processing...',\n      'Finalizing...',\n      'Deployment completed successfully.',\n    ]);\n\n    expect(consola.warn).toHaveBeenCalledWith(\n      expect.stringContaining('Failed to parse event, dropping from stream.'),\n      expect.any(Error),\n    );\n  });\n\n  test('handles deployment errors', async () => {\n    const encoder = new TextEncoder();\n\n    server.use(\n      http.get(\n        'https://:apiHost/stores/:storeHash/v3/infrastructure/deployments/:deploymentUuid/events',\n        () => {\n          const stream = new ReadableStream({\n            start(controller) {\n              controller.enqueue(\n                encoder.encode(\n                  `data: {\"deployment_status\":\"in_progress\",\"deployment_uuid\":\"${deploymentUuid}\",\"event\":{\"step\":\"processing\",\"progress\":75}}`,\n                ),\n              );\n              setTimeout(() => {\n                controller.enqueue(\n                  encoder.encode(\n                    `data: {\"deployment_status\":\"in_progress\",\"deployment_uuid\":\"${deploymentUuid}\",\"event\":{\"step\":\"unzipping\",\"progress\":99},\"error\":{\"code\":30}}`,\n                  ),\n                );\n              }, 10);\n            },\n          });\n\n          return new HttpResponse(stream, {\n            status: 200,\n            headers: { 'Content-Type': 'text/event-stream' },\n          });\n        },\n      ),\n    );\n\n    await expect(\n      getDeploymentStatus(deploymentUuid, storeHash, accessToken, apiHost),\n    ).rejects.toThrow('Deployment failed with error code: 30');\n\n    expect(consola.info).toHaveBeenCalledWith('Fetching deployment status...');\n\n    expect(textHistory).toEqual(['Fetching...', 'Processing...']);\n  });\n});\n\ntest('--dry-run skips upload and deployment', async () => {\n  await program.parseAsync([\n    'node',\n    'catalyst',\n    'deploy',\n    '--store-hash',\n    storeHash,\n    '--access-token',\n    accessToken,\n    '--api-host',\n    apiHost,\n    '--project-uuid',\n    projectUuid,\n    '--dry-run',\n  ]);\n\n  expect(consola.info).toHaveBeenCalledWith('Generating bundle...');\n  expect(consola.success).toHaveBeenCalledWith(`Bundle created at: ${outputZip}`);\n  expect(consola.info).toHaveBeenCalledWith(\n    'Dry run enabled — skipping upload and deployment steps.',\n  );\n  expect(consola.info).toHaveBeenCalledWith('Next steps (skipped):');\n  expect(consola.info).toHaveBeenCalledWith('- Generate upload signature');\n  expect(consola.info).toHaveBeenCalledWith('- Upload bundle.zip');\n  expect(consola.info).toHaveBeenCalledWith('- Create deployment');\n  expect(exitMock).toHaveBeenCalledWith(0);\n});\n\ntest('reads from env options', () => {\n  const envVariables = parseEnvironmentVariables([\n    'BIGCOMMERCE_STORE_HASH=123',\n    'BIGCOMMERCE_STOREFRONT_TOKEN=456',\n  ]);\n\n  expect(envVariables).toEqual([\n    {\n      type: 'secret',\n      key: 'BIGCOMMERCE_STORE_HASH',\n      value: '123',\n    },\n    {\n      type: 'secret',\n      key: 'BIGCOMMERCE_STOREFRONT_TOKEN',\n      value: '456',\n    },\n  ]);\n\n  expect(() => parseEnvironmentVariables(['foo_bar'])).toThrow(\n    'Invalid secret format: foo_bar. Expected format: KEY=VALUE',\n  );\n});\n"
  },
  {
    "path": "packages/catalyst/src/cli/commands/deploy.ts",
    "content": "import AdmZip from 'adm-zip';\nimport { Command, Option } from 'commander';\nimport { access, readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport yoctoSpinner from 'yocto-spinner';\nimport { z } from 'zod';\n\nimport { consola } from '../lib/logger';\nimport { getProjectConfig } from '../lib/project-config';\nimport { Telemetry } from '../lib/telemetry';\n\nconst telemetry = new Telemetry();\n\nconst stepsEnum = z.enum([\n  'initializing',\n  'downloading',\n  'unzipping',\n  'processing',\n  'deploying',\n  'finalizing',\n  'complete',\n]);\n\nconst STEPS: Record<z.infer<typeof stepsEnum>, string> = {\n  initializing: 'Initializing...',\n  downloading: 'Downloading...',\n  unzipping: 'Unzipping...',\n  processing: 'Processing...',\n  deploying: 'Deploying...',\n  finalizing: 'Finalizing...',\n  complete: 'Complete',\n};\n\nconst UploadSignatureSchema = z.object({\n  data: z.object({\n    upload_url: z.url(),\n    upload_uuid: z.string(),\n  }),\n});\n\nconst CreateDeploymentSchema = z.object({\n  data: z.object({\n    deployment_uuid: z.uuid(),\n  }),\n});\n\nconst DeploymentStatusSchema = z.object({\n  deployment_uuid: z.uuid(),\n  deployment_status: z.enum(['queued', 'in_progress', 'failed', 'completed']),\n  event: z\n    .object({\n      step: stepsEnum,\n      progress: z.number(),\n    })\n    .nullable(),\n  error: z\n    .object({\n      code: z.number(),\n    })\n    .optional(),\n});\n\nexport const generateBundleZip = async () => {\n  consola.info('Generating bundle...');\n\n  const bigcommerceDir = join(process.cwd(), '.bigcommerce');\n  const distDir = join(process.cwd(), '.bigcommerce', 'dist');\n\n  // Check if .bigcommerce/dist exists\n  try {\n    await access(distDir);\n  } catch {\n    throw new Error(`Dist directory not found: ${distDir}`);\n  }\n\n  // Check if .bigcommerce/dist is not empty\n  const buildDirContents = await readdir(distDir);\n\n  if (buildDirContents.length === 0) {\n    throw new Error(`Dist directory is empty: ${distDir}`);\n  }\n\n  const outputZip = join(bigcommerceDir, 'bundle.zip');\n\n  // Use AdmZip to create the zip\n  const zip = new AdmZip();\n\n  zip.addLocalFolder(distDir, 'output');\n  zip.writeZip(outputZip);\n\n  consola.success(`Bundle created at: ${outputZip}`);\n};\n\nexport const generateUploadSignature = async (\n  storeHash: string,\n  accessToken: string,\n  apiHost: string,\n) => {\n  consola.info('Generating upload signature...');\n\n  const response = await fetch(\n    `https://${apiHost}/stores/${storeHash}/v3/infrastructure/deployments/uploads`,\n    {\n      method: 'POST',\n      headers: {\n        'X-Auth-Token': accessToken,\n        'Content-Type': 'application/json',\n        Accept: 'application/json',\n      },\n      body: JSON.stringify({}),\n    },\n  );\n\n  if (!response.ok) {\n    throw new Error(`Failed to fetch upload signature: ${response.status} ${response.statusText}`);\n  }\n\n  const res: unknown = await response.json();\n  const { data } = UploadSignatureSchema.parse(res);\n\n  consola.success('Upload signature generated.');\n\n  return data;\n};\n\nexport const uploadBundleZip = async (uploadUrl: string) => {\n  consola.info('Uploading bundle...');\n\n  const zipPath = join(process.cwd(), '.bigcommerce', 'bundle.zip');\n\n  // Read the zip file as a buffer\n  const fileBuffer = await readFile(zipPath);\n\n  const response = await fetch(uploadUrl, {\n    method: 'PUT',\n    headers: {\n      'Content-Type': 'application/zip',\n    },\n    body: fileBuffer,\n  });\n\n  if (!response.ok) {\n    throw new Error(`Failed to upload bundle: ${response.status} ${response.statusText}`);\n  }\n\n  consola.success('Bundle uploaded successfully.');\n\n  return true;\n};\n\nexport const parseEnvironmentVariables = (secretOption?: string[]) => {\n  return secretOption?.map((envVar) => {\n    const [key, value] = envVar.split('=');\n\n    if (!key || !value) {\n      throw new Error(`Invalid secret format: ${envVar}. Expected format: KEY=VALUE`);\n    }\n\n    return {\n      type: 'secret' as const,\n      key: key.trim(),\n      value: value.trim(),\n    };\n  });\n};\n\nexport const createDeployment = async (\n  projectUuid: string,\n  uploadUuid: string,\n  storeHash: string,\n  accessToken: string,\n  apiHost: string,\n  environmentVariables?: Array<{ type: 'secret' | 'plain_text'; key: string; value: string }>,\n) => {\n  consola.info('Creating deployment...');\n\n  const response = await fetch(\n    `https://${apiHost}/stores/${storeHash}/v3/infrastructure/deployments`,\n    {\n      method: 'POST',\n      headers: {\n        'X-Auth-Token': accessToken,\n        'Content-Type': 'application/json',\n        Accept: 'application/json',\n      },\n      body: JSON.stringify({\n        project_uuid: projectUuid,\n        upload_uuid: uploadUuid,\n        environment_variables: environmentVariables,\n      }),\n    },\n  );\n\n  if (!response.ok) {\n    throw new Error(`Failed to create deployment: ${response.status} ${response.statusText}`);\n  }\n\n  const res: unknown = await response.json();\n  const { data } = CreateDeploymentSchema.parse(res);\n\n  consola.success('Deployment started...');\n\n  return data;\n};\n\nexport const getDeploymentStatus = async (\n  deploymentUuid: string,\n  storeHash: string,\n  accessToken: string,\n  apiHost: string,\n) => {\n  consola.info('Fetching deployment status...');\n\n  const spinner = yoctoSpinner().start('Fetching...');\n\n  const response = await fetch(\n    `https://${apiHost}/stores/${storeHash}/v3/infrastructure/deployments/${deploymentUuid}/events`,\n    {\n      method: 'GET',\n      headers: {\n        'X-Auth-Token': accessToken,\n        Accept: 'text/event-stream',\n        Connection: 'keep-alive',\n      },\n    },\n  );\n\n  if (!response.ok) {\n    throw new Error(`Failed to open event stream: ${response.status} ${response.statusText}`);\n  }\n\n  const reader = response.body?.getReader();\n\n  if (!reader) {\n    throw new Error('Failed to read event stream.');\n  }\n\n  const decoder = new TextDecoder();\n  let done = false;\n\n  while (!done) {\n    // eslint-disable-next-line no-await-in-loop\n    const { value, done: streamDone } = await reader.read();\n    let json: unknown;\n\n    if (value) {\n      const chunk = decoder.decode(value, { stream: true }).trim();\n      const split = chunk\n        .split('\\n\\n')\n        .map((s) => s.replace('data:', '').trim())\n        .filter(Boolean);\n\n      split.forEach((event) => {\n        try {\n          json = JSON.parse(event);\n        } catch (error) {\n          consola.warn(`Failed to parse event, dropping from stream. Event: ${event}`, error);\n\n          return;\n        }\n\n        const data = DeploymentStatusSchema.parse(json);\n\n        if (data.error) {\n          throw new Error(`Deployment failed with error code: ${data.error.code}`);\n        }\n\n        if (data.event && STEPS[data.event.step] !== spinner.text) {\n          spinner.text = STEPS[data.event.step];\n        }\n      });\n    }\n\n    done = streamDone;\n  }\n\n  spinner.success('Deployment completed successfully.');\n};\n\nexport const deploy = new Command('deploy')\n  .description('Deploy your application to Cloudflare.')\n  .addOption(\n    new Option(\n      '--store-hash <hash>',\n      'BigCommerce store hash. Can be found in the URL of your store Control Panel.',\n    )\n      .env('BIGCOMMERCE_STORE_HASH')\n      .makeOptionMandatory(),\n  )\n  .addOption(\n    new Option(\n      '--access-token <token>',\n      'BigCommerce access token. Can be found after creating a store-level API account.',\n    )\n      .env('BIGCOMMERCE_ACCESS_TOKEN')\n      .makeOptionMandatory(),\n  )\n  .addOption(\n    new Option('--api-host <host>', 'BigCommerce API host. The default is api.bigcommerce.com.')\n      .env('BIGCOMMERCE_API_HOST')\n      .default('api.bigcommerce.com'),\n  )\n  .addOption(\n    new Option(\n      '--project-uuid <uuid>',\n      'BigCommerce intrastructure project UUID. Can be found via the BigCommerce API (GET /v3/infrastructure/projects).',\n    ).env('BIGCOMMERCE_PROJECT_UUID'),\n  )\n  .addOption(\n    new Option(\n      '--secret <secrets...>',\n      'Secrets to set for the deployment. Format: SECRET_1=FOO SECRET_2=BAR',\n    ),\n  )\n  .option('--dry-run', 'Run the command to generate the bundle without uploading or deploying.')\n\n  .action(async (options) => {\n    try {\n      const config = getProjectConfig();\n\n      await telemetry.identify(options.storeHash);\n\n      const projectUuid = options.projectUuid ?? config.get('projectUuid');\n\n      if (!projectUuid) {\n        throw new Error(\n          'Project UUID is required. Please run either `catalyst project link` or `catalyst project create` or this command again with --project-uuid <uuid>.',\n        );\n      }\n\n      await generateBundleZip();\n\n      if (options.dryRun) {\n        consola.info('Dry run enabled — skipping upload and deployment steps.');\n        consola.info('Next steps (skipped):');\n        consola.info('- Generate upload signature');\n        consola.info('- Upload bundle.zip');\n        consola.info('- Create deployment');\n\n        process.exit(0);\n      }\n\n      const uploadSignature = await generateUploadSignature(\n        options.storeHash,\n        options.accessToken,\n        options.apiHost,\n      );\n\n      await uploadBundleZip(uploadSignature.upload_url);\n\n      const environmentVariables = parseEnvironmentVariables(options.secret);\n\n      const { deployment_uuid: deploymentUuid } = await createDeployment(\n        projectUuid,\n        uploadSignature.upload_uuid,\n        options.storeHash,\n        options.accessToken,\n        options.apiHost,\n        environmentVariables,\n      );\n\n      await getDeploymentStatus(\n        deploymentUuid,\n        options.storeHash,\n        options.accessToken,\n        options.apiHost,\n      );\n    } catch (error) {\n      consola.error(error);\n      process.exit(1);\n    }\n  });\n"
  },
  {
    "path": "packages/catalyst/src/cli/commands/dev.spec.ts",
    "content": "import { Command } from 'commander';\nimport { execa } from 'execa';\nimport { join } from 'node:path';\nimport { expect, test, vi } from 'vitest';\n\nimport { program } from '../program';\n\nimport { dev } from './dev';\n\nvi.mock('node:fs', () => ({\n  existsSync: vi.fn(() => true),\n}));\n\nvi.mock('execa', () => ({\n  execa: vi.fn(() => Promise.resolve({ stdout: '' })),\n  __esModule: true,\n}));\n\ntest('properly configured Command instance', () => {\n  expect(dev).toBeInstanceOf(Command);\n  expect(dev.name()).toBe('dev');\n  expect(dev.description()).toBe('Start the Catalyst development server.');\n});\n\ntest('calls execa with Next.js development server', async () => {\n  await program.parseAsync(['node', 'catalyst', 'dev', '-p', '3001']);\n\n  expect(execa).toHaveBeenCalledWith(\n    join('node_modules', '.bin', 'next'),\n    ['dev', '-p', '3001'],\n    expect.objectContaining({\n      stdio: 'inherit',\n      cwd: process.cwd(),\n    }),\n  );\n});\n"
  },
  {
    "path": "packages/catalyst/src/cli/commands/dev.ts",
    "content": "import { Command } from 'commander';\nimport { execa } from 'execa';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { consola } from '../lib/logger';\n\nexport const dev = new Command('dev')\n  .description('Start the Catalyst development server.')\n  // Proxy `--help` to the underlying `next dev` command\n  .helpOption(false)\n  .allowUnknownOption(true)\n  // The unknown options end up in program.args, not in program.opts(). Commander does not take a guess at how to interpret the unknown options.\n  .argument(\n    '[options...]',\n    'Next.js `dev` options (see: https://nextjs.org/docs/app/api-reference/cli/next#next-dev-options)',\n  )\n  .action(async (options) => {\n    try {\n      const nextBin = join('node_modules', '.bin', 'next');\n\n      if (!existsSync(nextBin)) {\n        throw new Error(\n          `Next.js is not installed in ${process.cwd()}. Are you in a valid Next.js project?`,\n        );\n      }\n\n      await execa(nextBin, ['dev', ...options], {\n        stdio: 'inherit',\n        cwd: process.cwd(),\n      });\n    } catch (error) {\n      consola.error(error instanceof Error ? error.message : error);\n      process.exit(1);\n    }\n  });\n"
  },
  {
    "path": "packages/catalyst/src/cli/commands/project.spec.ts",
    "content": "import { Command } from 'commander';\nimport Conf from 'conf';\nimport { http, HttpResponse } from 'msw';\nimport { afterAll, afterEach, beforeAll, describe, expect, MockInstance, test, vi } from 'vitest';\n\nimport { server } from '../../../tests/mocks/node';\nimport { consola } from '../lib/logger';\nimport { mkTempDir } from '../lib/mk-temp-dir';\nimport { getProjectConfig, ProjectConfigSchema } from '../lib/project-config';\nimport { program } from '../program';\n\nimport { link, project } from './project';\n\nlet exitMock: MockInstance;\n\nlet tmpDir: string;\nlet cleanup: () => Promise<void>;\nlet config: Conf<ProjectConfigSchema>;\n\nconst { mockIdentify } = vi.hoisted(() => ({\n  mockIdentify: vi.fn(),\n}));\n\nconst projectUuid1 = 'a23f5785-fd99-4a94-9fb3-945551623923';\nconst projectUuid2 = 'b23f5785-fd99-4a94-9fb3-945551623924';\nconst projectUuid3 = 'c23f5785-fd99-4a94-9fb3-945551623925';\nconst storeHash = 'test-store';\nconst accessToken = 'test-token';\n\nbeforeAll(async () => {\n  consola.mockTypes(() => vi.fn());\n\n  vi.mock('../lib/telemetry', () => {\n    return {\n      Telemetry: vi.fn().mockImplementation(() => {\n        return {\n          identify: mockIdentify,\n          isEnabled: vi.fn(() => true),\n          track: vi.fn(),\n          analytics: {\n            closeAndFlush: vi.fn(),\n          },\n        };\n      }),\n    };\n  });\n\n  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n  exitMock = vi.spyOn(process, 'exit').mockImplementation(() => null as never);\n\n  [tmpDir, cleanup] = await mkTempDir();\n\n  config = getProjectConfig(tmpDir);\n});\n\nafterEach(() => {\n  vi.clearAllMocks();\n});\n\nafterAll(async () => {\n  vi.restoreAllMocks();\n  exitMock.mockRestore();\n\n  await cleanup();\n});\n\ndescribe('project', () => {\n  test('has create, link, and list subcommands', () => {\n    expect(project).toBeInstanceOf(Command);\n    expect(project.name()).toBe('project');\n    expect(project.description()).toBe('Manage your BigCommerce infrastructure project.');\n\n    const createCmd = project.commands.find((cmd) => cmd.name() === 'create');\n\n    expect(createCmd).toBeDefined();\n    expect(createCmd?.description()).toContain('Create a new BigCommerce infrastructure project');\n\n    const linkCmd = project.commands.find((cmd) => cmd.name() === 'link');\n\n    expect(linkCmd).toBeDefined();\n    expect(linkCmd?.description()).toContain(\n      'Link your local Catalyst project to a BigCommerce infrastructure project',\n    );\n\n    const listCmd = project.commands.find((cmd) => cmd.name() === 'list');\n\n    expect(listCmd).toBeDefined();\n    expect(listCmd?.description()).toContain('List BigCommerce infrastructure projects');\n  });\n});\n\ndescribe('project create', () => {\n  test('prompts for name and creates project', async () => {\n    const consolaPromptMock = vi.spyOn(consola, 'prompt').mockResolvedValue('My New Project');\n\n    await program.parseAsync([\n      'node',\n      'catalyst',\n      'project',\n      'create',\n      '--store-hash',\n      storeHash,\n      '--access-token',\n      accessToken,\n      '--root-dir',\n      tmpDir,\n    ]);\n\n    expect(mockIdentify).toHaveBeenCalledWith(storeHash);\n    expect(consolaPromptMock).toHaveBeenCalledWith(\n      'Enter a name for the new project:',\n      expect.any(Object),\n    );\n    expect(consola.success).toHaveBeenCalledWith('Project \"New Project\" created successfully.');\n    expect(consola.start).toHaveBeenCalledWith(\n      'Writing project UUID to .bigcommerce/project.json...',\n    );\n    expect(consola.success).toHaveBeenCalledWith(\n      'Project UUID written to .bigcommerce/project.json.',\n    );\n    expect(exitMock).toHaveBeenCalledWith(0);\n\n    expect(config.get('projectUuid')).toBe('c23f5785-fd99-4a94-9fb3-945551623925');\n    expect(config.get('framework')).toBe('catalyst');\n\n    consolaPromptMock.mockRestore();\n  });\n\n  test('with insufficient credentials exits with error', async () => {\n    // Unset env so Commander doesn't pick up BIGCOMMERCE_* and trigger the create flow (which would prompt for name)\n    const savedStoreHash = process.env.BIGCOMMERCE_STORE_HASH;\n    const savedAccessToken = process.env.BIGCOMMERCE_ACCESS_TOKEN;\n\n    delete process.env.BIGCOMMERCE_STORE_HASH;\n    delete process.env.BIGCOMMERCE_ACCESS_TOKEN;\n\n    await program.parseAsync(['node', 'catalyst', 'project', 'create', '--root-dir', tmpDir]);\n\n    if (savedStoreHash !== undefined) process.env.BIGCOMMERCE_STORE_HASH = savedStoreHash;\n    if (savedAccessToken !== undefined) process.env.BIGCOMMERCE_ACCESS_TOKEN = savedAccessToken;\n\n    expect(consola.error).toHaveBeenCalledWith('Insufficient information to create a project.');\n    expect(consola.info).toHaveBeenCalledWith(\n      'Provide both --store-hash and --access-token (or set BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN).',\n    );\n    expect(exitMock).toHaveBeenCalledWith(1);\n  });\n\n  test('propagates create project API errors', async () => {\n    server.use(\n      http.post('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () =>\n        HttpResponse.json({}, { status: 502 }),\n      ),\n    );\n\n    const promptMock = vi.spyOn(consola, 'prompt').mockResolvedValue('Duplicate');\n\n    await program.parseAsync([\n      'node',\n      'catalyst',\n      'project',\n      'create',\n      '--store-hash',\n      storeHash,\n      '--access-token',\n      accessToken,\n      '--root-dir',\n      tmpDir,\n    ]);\n\n    promptMock.mockRestore();\n\n    expect(consola.error).toHaveBeenCalledWith(\n      'Failed to create project, is the name already in use?',\n    );\n    expect(exitMock).toHaveBeenCalledWith(1);\n  });\n});\n\ndescribe('project list', () => {\n  test('fetches and displays projects', async () => {\n    await program.parseAsync([\n      'node',\n      'catalyst',\n      'project',\n      'list',\n      '--store-hash',\n      storeHash,\n      '--access-token',\n      accessToken,\n    ]);\n\n    expect(mockIdentify).toHaveBeenCalledWith(storeHash);\n    expect(consola.start).toHaveBeenCalledWith('Fetching projects...');\n    expect(consola.success).toHaveBeenCalledWith('Projects fetched.');\n    expect(consola.log).toHaveBeenCalledWith('Project One (a23f5785-fd99-4a94-9fb3-945551623923)');\n    expect(consola.log).toHaveBeenCalledWith('Project Two (b23f5785-fd99-4a94-9fb3-945551623924)');\n    expect(exitMock).toHaveBeenCalledWith(0);\n  });\n\n  test('with insufficient credentials exits with error', async () => {\n    const savedStoreHash = process.env.BIGCOMMERCE_STORE_HASH;\n    const savedAccessToken = process.env.BIGCOMMERCE_ACCESS_TOKEN;\n\n    delete process.env.BIGCOMMERCE_STORE_HASH;\n    delete process.env.BIGCOMMERCE_ACCESS_TOKEN;\n\n    await program.parseAsync(['node', 'catalyst', 'project', 'list']);\n\n    if (savedStoreHash !== undefined) process.env.BIGCOMMERCE_STORE_HASH = savedStoreHash;\n    if (savedAccessToken !== undefined) process.env.BIGCOMMERCE_ACCESS_TOKEN = savedAccessToken;\n\n    expect(consola.error).toHaveBeenCalledWith('Insufficient information to list projects.');\n    expect(consola.info).toHaveBeenCalledWith(\n      'Provide both --store-hash and --access-token (or set BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN).',\n    );\n    expect(exitMock).toHaveBeenCalledWith(1);\n  });\n});\n\ndescribe('project link', () => {\n  test('properly configured Command instance', () => {\n    expect(link).toBeInstanceOf(Command);\n    expect(link.name()).toBe('link');\n    expect(link.description()).toBe(\n      'Link your local Catalyst project to a BigCommerce infrastructure project. You can provide a project UUID directly, or fetch and select from available projects using your store credentials.',\n    );\n    expect(link.options).toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({ flags: '--store-hash <hash>' }),\n        expect.objectContaining({ flags: '--access-token <token>' }),\n        expect.objectContaining({\n          flags: '--api-host <host>',\n          defaultValue: 'api.bigcommerce.com',\n        }),\n        expect.objectContaining({ flags: '--project-uuid <uuid>' }),\n        expect.objectContaining({ flags: '--root-dir <path>', defaultValue: process.cwd() }),\n      ]),\n    );\n  });\n\n  test('sets projectUuid when called with --project-uuid', async () => {\n    await program.parseAsync([\n      'node',\n      'catalyst',\n      'project',\n      'link',\n      '--project-uuid',\n      projectUuid1,\n      '--root-dir',\n      tmpDir,\n    ]);\n\n    expect(consola.start).toHaveBeenCalledWith(\n      'Writing project UUID to .bigcommerce/project.json...',\n    );\n    expect(consola.success).toHaveBeenCalledWith(\n      'Project UUID written to .bigcommerce/project.json.',\n    );\n    expect(exitMock).toHaveBeenCalledWith(0);\n    expect(config.get('projectUuid')).toBe(projectUuid1);\n    expect(config.get('framework')).toBe('catalyst');\n  });\n\n  test('fetches projects and prompts user to select one', async () => {\n    const consolaPromptMock = vi\n      .spyOn(consola, 'prompt')\n      .mockImplementation(async (message, opts) => {\n        expect(message).toContain(\n          'Select a project or create a new project (Press <enter> to select).',\n        );\n\n        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n        const options = (opts as { options: Array<{ label: string; value: string }> }).options;\n\n        expect(options).toHaveLength(3);\n        expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 });\n        expect(options[1]).toMatchObject({\n          label: 'Project Two',\n          value: projectUuid2,\n        });\n        expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' });\n\n        return new Promise((resolve) => resolve(projectUuid2));\n      });\n\n    await program.parseAsync([\n      'node',\n      'catalyst',\n      'project',\n      'link',\n      '--store-hash',\n      storeHash,\n      '--access-token',\n      accessToken,\n      '--root-dir',\n      tmpDir,\n    ]);\n\n    expect(mockIdentify).toHaveBeenCalledWith(storeHash);\n\n    expect(consola.start).toHaveBeenCalledWith('Fetching projects...');\n    expect(consola.success).toHaveBeenCalledWith('Projects fetched.');\n\n    expect(consola.start).toHaveBeenCalledWith(\n      'Writing project UUID to .bigcommerce/project.json...',\n    );\n    expect(consola.success).toHaveBeenCalledWith(\n      'Project UUID written to .bigcommerce/project.json.',\n    );\n\n    expect(exitMock).toHaveBeenCalledWith(0);\n\n    expect(config.get('projectUuid')).toBe(projectUuid2);\n    expect(config.get('framework')).toBe('catalyst');\n\n    consolaPromptMock.mockRestore();\n  });\n\n  test('prompts to create a new project', async () => {\n    const consolaPromptMock = vi\n      .spyOn(consola, 'prompt')\n      .mockImplementationOnce(async (message, opts) => {\n        expect(message).toContain(\n          'Select a project or create a new project (Press <enter> to select).',\n        );\n\n        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n        const options = (opts as { options: Array<{ label: string; value: string }> }).options;\n\n        expect(options).toHaveLength(3);\n        expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 });\n        expect(options[1]).toMatchObject({\n          label: 'Project Two',\n          value: projectUuid2,\n        });\n        expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' });\n\n        return new Promise((resolve) => resolve('create'));\n      })\n      .mockImplementationOnce(async (message) => {\n        expect(message).toBe('Enter a name for the new project:');\n\n        return new Promise((resolve) => resolve('New Project'));\n      });\n\n    await program.parseAsync([\n      'node',\n      'catalyst',\n      'project',\n      'link',\n      '--store-hash',\n      storeHash,\n      '--access-token',\n      accessToken,\n      '--root-dir',\n      tmpDir,\n    ]);\n\n    expect(mockIdentify).toHaveBeenCalledWith(storeHash);\n\n    expect(consola.start).toHaveBeenCalledWith('Fetching projects...');\n    expect(consola.success).toHaveBeenCalledWith('Projects fetched.');\n\n    expect(consola.success).toHaveBeenCalledWith('Project \"New Project\" created successfully.');\n\n    expect(exitMock).toHaveBeenCalledWith(0);\n\n    expect(config.get('projectUuid')).toBe(projectUuid3);\n    expect(config.get('framework')).toBe('catalyst');\n\n    consolaPromptMock.mockRestore();\n  });\n\n  test('errors when create project API fails', async () => {\n    server.use(\n      http.post('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () =>\n        HttpResponse.json({}, { status: 502 }),\n      ),\n    );\n\n    const consolaPromptMock = vi\n      .spyOn(consola, 'prompt')\n      .mockImplementationOnce(async (message, opts) => {\n        expect(message).toContain(\n          'Select a project or create a new project (Press <enter> to select).',\n        );\n\n        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n        const options = (opts as { options: Array<{ label: string; value: string }> }).options;\n\n        expect(options).toHaveLength(3);\n        expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 });\n        expect(options[1]).toMatchObject({\n          label: 'Project Two',\n          value: projectUuid2,\n        });\n        expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' });\n\n        return new Promise((resolve) => resolve('create'));\n      })\n      .mockImplementationOnce(async (message) => {\n        expect(message).toBe('Enter a name for the new project:');\n\n        return new Promise((resolve) => resolve('New Project'));\n      });\n\n    await program.parseAsync([\n      'node',\n      'catalyst',\n      'project',\n      'link',\n      '--store-hash',\n      storeHash,\n      '--access-token',\n      accessToken,\n      '--root-dir',\n      tmpDir,\n    ]);\n\n    expect(mockIdentify).toHaveBeenCalledWith(storeHash);\n\n    expect(consola.start).toHaveBeenCalledWith('Fetching projects...');\n    expect(consola.success).toHaveBeenCalledWith('Projects fetched.');\n\n    expect(consola.error).toHaveBeenCalledWith(\n      'Failed to create project, is the name already in use?',\n    );\n\n    expect(exitMock).toHaveBeenCalledWith(1);\n\n    consolaPromptMock.mockRestore();\n  });\n\n  test('errors when infrastructure projects API is not found', async () => {\n    server.use(\n      http.get('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () =>\n        HttpResponse.json({}, { status: 403 }),\n      ),\n    );\n\n    await program.parseAsync([\n      'node',\n      'catalyst',\n      'project',\n      'link',\n      '--store-hash',\n      storeHash,\n      '--access-token',\n      accessToken,\n      '--root-dir',\n      tmpDir,\n    ]);\n\n    expect(mockIdentify).toHaveBeenCalledWith(storeHash);\n\n    expect(consola.start).toHaveBeenCalledWith('Fetching projects...');\n    expect(consola.error).toHaveBeenCalledWith(\n      'Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it.',\n    );\n  });\n\n  test('errors when no projectUuid, storeHash, or accessToken are provided', async () => {\n    await program.parseAsync(['node', 'catalyst', 'project', 'link', '--root-dir', tmpDir]);\n\n    expect(consola.start).not.toHaveBeenCalled();\n    expect(consola.success).not.toHaveBeenCalled();\n    expect(consola.error).toHaveBeenCalledWith('Insufficient information to link a project.');\n    expect(consola.info).toHaveBeenCalledWith('Provide a project UUID with --project-uuid, or');\n    expect(consola.info).toHaveBeenCalledWith(\n      'Provide both --store-hash and --access-token to fetch and select a project.',\n    );\n\n    expect(exitMock).toHaveBeenCalledWith(1);\n  });\n});\n"
  },
  {
    "path": "packages/catalyst/src/cli/commands/project.ts",
    "content": "import { Command, Option } from 'commander';\n\nimport { consola } from '../lib/logger';\nimport { createProject, fetchProjects } from '../lib/project';\nimport { getProjectConfig } from '../lib/project-config';\nimport { Telemetry } from '../lib/telemetry';\n\nconst telemetry = new Telemetry();\n\nconst list = new Command('list')\n  .description('List BigCommerce infrastructure projects for your store.')\n  .addOption(\n    new Option(\n      '--store-hash <hash>',\n      'BigCommerce store hash. Can be found in the URL of your store Control Panel.',\n    ).env('BIGCOMMERCE_STORE_HASH'),\n  )\n  .addOption(\n    new Option(\n      '--access-token <token>',\n      'BigCommerce access token. Can be found after creating a store-level API account.',\n    ).env('BIGCOMMERCE_ACCESS_TOKEN'),\n  )\n  .addOption(\n    new Option('--api-host <host>', 'BigCommerce API host. The default is api.bigcommerce.com.')\n      .env('BIGCOMMERCE_API_HOST')\n      .default('api.bigcommerce.com'),\n  )\n  .action(async (options) => {\n    try {\n      if (!options.storeHash || !options.accessToken) {\n        consola.error('Insufficient information to list projects.');\n        consola.info(\n          'Provide both --store-hash and --access-token (or set BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN).',\n        );\n        process.exit(1);\n\n        return;\n      }\n\n      await telemetry.identify(options.storeHash);\n\n      consola.start('Fetching projects...');\n\n      const projects = await fetchProjects(options.storeHash, options.accessToken, options.apiHost);\n\n      consola.success('Projects fetched.');\n\n      if (projects.length === 0) {\n        consola.info('No projects found.');\n        process.exit(0);\n\n        return;\n      }\n\n      projects.forEach((p) => {\n        consola.log(`${p.name} (${p.uuid})`);\n      });\n\n      process.exit(0);\n    } catch (error) {\n      consola.error(error instanceof Error ? error.message : error);\n      process.exit(1);\n    }\n  });\n\nconst create = new Command('create')\n  .description(\n    'Create a new BigCommerce infrastructure project and link it to your local Catalyst project.',\n  )\n  .addOption(\n    new Option(\n      '--store-hash <hash>',\n      'BigCommerce store hash. Can be found in the URL of your store Control Panel.',\n    ).env('BIGCOMMERCE_STORE_HASH'),\n  )\n  .addOption(\n    new Option(\n      '--access-token <token>',\n      'BigCommerce access token. Can be found after creating a store-level API account.',\n    ).env('BIGCOMMERCE_ACCESS_TOKEN'),\n  )\n  .addOption(\n    new Option('--api-host <host>', 'BigCommerce API host. The default is api.bigcommerce.com.')\n      .env('BIGCOMMERCE_API_HOST')\n      .default('api.bigcommerce.com'),\n  )\n  .option(\n    '--root-dir <path>',\n    'Path to the root directory of your Catalyst project (default: current working directory).',\n    process.cwd(),\n  )\n  .action(async (options) => {\n    try {\n      if (!options.storeHash || !options.accessToken) {\n        consola.error('Insufficient information to create a project.');\n        consola.info(\n          'Provide both --store-hash and --access-token (or set BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN).',\n        );\n        process.exit(1);\n\n        return;\n      }\n\n      await telemetry.identify(options.storeHash);\n\n      const newProjectName = await consola.prompt('Enter a name for the new project:', {\n        type: 'text',\n      });\n\n      const data = await createProject(\n        newProjectName,\n        options.storeHash,\n        options.accessToken,\n        options.apiHost,\n      );\n\n      consola.success(`Project \"${data.name}\" created successfully.`);\n\n      const config = getProjectConfig(options.rootDir);\n\n      consola.start('Writing project UUID to .bigcommerce/project.json...');\n      config.set('projectUuid', data.uuid);\n      config.set('framework', 'catalyst');\n      consola.success('Project UUID written to .bigcommerce/project.json.');\n\n      process.exit(0);\n    } catch (error) {\n      consola.error(error instanceof Error ? error.message : error);\n      process.exit(1);\n    }\n  });\n\nexport const link = new Command('link')\n  .description(\n    'Link your local Catalyst project to a BigCommerce infrastructure project. You can provide a project UUID directly, or fetch and select from available projects using your store credentials.',\n  )\n  .addOption(\n    new Option(\n      '--store-hash <hash>',\n      'BigCommerce store hash. Can be found in the URL of your store Control Panel.',\n    ).env('BIGCOMMERCE_STORE_HASH'),\n  )\n  .addOption(\n    new Option(\n      '--access-token <token>',\n      'BigCommerce access token. Can be found after creating a store-level API account.',\n    ).env('BIGCOMMERCE_ACCESS_TOKEN'),\n  )\n  .addOption(\n    new Option('--api-host <host>', 'BigCommerce API host. The default is api.bigcommerce.com.')\n      .env('BIGCOMMERCE_API_HOST')\n      .default('api.bigcommerce.com'),\n  )\n  .option(\n    '--project-uuid <uuid>',\n    'BigCommerce infrastructure project UUID. Can be found via the BigCommerce API (GET /v3/infrastructure/projects). Use this to link directly without fetching projects.',\n  )\n  .option(\n    '--root-dir <path>',\n    'Path to the root directory of your Catalyst project (default: current working directory).',\n    process.cwd(),\n  )\n  .action(async (options) => {\n    try {\n      const config = getProjectConfig(options.rootDir);\n\n      const writeProjectConfig = (uuid: string) => {\n        consola.start('Writing project UUID to .bigcommerce/project.json...');\n        config.set('projectUuid', uuid);\n        config.set('framework', 'catalyst');\n        consola.success('Project UUID written to .bigcommerce/project.json.');\n      };\n\n      if (options.projectUuid) {\n        writeProjectConfig(options.projectUuid);\n\n        process.exit(0);\n      }\n\n      if (options.storeHash && options.accessToken) {\n        await telemetry.identify(options.storeHash);\n\n        consola.start('Fetching projects...');\n\n        const projects = await fetchProjects(\n          options.storeHash,\n          options.accessToken,\n          options.apiHost,\n        );\n\n        consola.success('Projects fetched.');\n\n        const promptOptions = [\n          ...projects.map((proj) => ({\n            label: proj.name,\n            value: proj.uuid,\n            hint: proj.uuid,\n          })),\n          {\n            label: 'Create a new project',\n            value: 'create',\n            hint: 'Create a new infrastructure project for this BigCommerce store.',\n          },\n        ];\n\n        let projectUuid = await consola.prompt(\n          'Select a project or create a new project (Press <enter> to select).',\n          {\n            type: 'select',\n            options: promptOptions,\n            cancel: 'reject',\n          },\n        );\n\n        if (projectUuid === 'create') {\n          const newProjectName = await consola.prompt('Enter a name for the new project:', {\n            type: 'text',\n          });\n\n          const data = await createProject(\n            newProjectName,\n            options.storeHash,\n            options.accessToken,\n            options.apiHost,\n          );\n\n          projectUuid = data.uuid;\n\n          consola.success(`Project \"${data.name}\" created successfully.`);\n        }\n\n        writeProjectConfig(projectUuid);\n\n        process.exit(0);\n      }\n\n      consola.error('Insufficient information to link a project.');\n      consola.info('Provide a project UUID with --project-uuid, or');\n      consola.info('Provide both --store-hash and --access-token to fetch and select a project.');\n      process.exit(1);\n    } catch (error) {\n      consola.error(error instanceof Error ? error.message : error);\n      process.exit(1);\n    }\n  });\n\nexport const project = new Command('project')\n  .description('Manage your BigCommerce infrastructure project.')\n  .addCommand(create)\n  .addCommand(list)\n  .addCommand(link);\n"
  },
  {
    "path": "packages/catalyst/src/cli/commands/start.spec.ts",
    "content": "import { Command } from 'commander';\nimport { execa } from 'execa';\nimport { join } from 'node:path';\nimport { afterEach, beforeAll, beforeEach, expect, test, vi } from 'vitest';\n\nimport { consola } from '../lib/logger';\nimport { program } from '../program';\n\nimport { start } from './start';\n\nvi.mock('node:fs', () => ({\n  existsSync: vi.fn(() => true),\n}));\n\nvi.mock('execa', () => ({\n  execa: vi.fn(() => Promise.resolve({})),\n  __esModule: true,\n}));\n\nvi.mock('../../src/lib/project-config', () => ({\n  getProjectConfig: vi.fn(() => ({\n    get: vi.fn((key) => {\n      if (key === 'framework') {\n        return 'catalyst';\n      }\n\n      return undefined;\n    }),\n  })),\n}));\n\nbeforeAll(() => {\n  consola.wrapAll();\n});\n\nbeforeEach(() => {\n  consola.mockTypes(() => vi.fn());\n});\n\nafterEach(() => {\n  vi.clearAllMocks();\n});\n\ntest('properly configured Command instance', () => {\n  expect(start).toBeInstanceOf(Command);\n  expect(start.name()).toBe('start');\n  expect(start.description()).toBe('Start your Catalyst storefront in optimized production mode.');\n  expect(start.options).toEqual(\n    expect.arrayContaining([\n      expect.objectContaining({\n        flags: '--framework <framework>',\n        argChoices: ['catalyst', 'nextjs'],\n      }),\n    ]),\n  );\n});\n\ntest('calls execa with Next.js production optimized server', async () => {\n  await program.parseAsync([\n    'node',\n    'catalyst',\n    'start',\n    '--port',\n    '3001',\n    '--framework',\n    'nextjs',\n  ]);\n\n  expect(execa).toHaveBeenCalledWith(\n    join('node_modules', '.bin', 'next'),\n    ['start', '--port', '3001'],\n    expect.objectContaining({\n      stdio: 'inherit',\n      cwd: process.cwd(),\n    }),\n  );\n});\n\ntest('calls execa with OpenNext production optimized server', async () => {\n  await program.parseAsync([\n    'node',\n    'catalyst',\n    'start',\n    '--port',\n    '3001',\n    '--framework',\n    'catalyst',\n  ]);\n\n  expect(execa).toHaveBeenCalledWith(\n    'pnpm',\n    [\n      'exec',\n      'opennextjs-cloudflare',\n      'preview',\n      '--config',\n      join('.bigcommerce', 'wrangler.jsonc'),\n      '--port',\n      '3001',\n    ],\n    expect.objectContaining({\n      stdio: 'inherit',\n      cwd: process.cwd(),\n    }),\n  );\n});\n"
  },
  {
    "path": "packages/catalyst/src/cli/commands/start.ts",
    "content": "import { Command, Option } from 'commander';\nimport { execa } from 'execa';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { consola } from '../lib/logger';\nimport { getProjectConfig } from '../lib/project-config';\n\nexport const start = new Command('start')\n  .description('Start your Catalyst storefront in optimized production mode.')\n  .allowUnknownOption(true)\n  // The unknown options end up in program.args, not in program.opts(). Commander does not take a guess at how to interpret the unknown options.\n  .argument(\n    '[start-options...]',\n    'Pass additional options to the start command. If framework is Next.js, see https://nextjs.org/docs/api-reference/cli#start for available options.',\n  )\n  .addOption(\n    new Option('--framework <framework>', 'The framework to use for the preview').choices([\n      'catalyst',\n      'nextjs',\n    ]),\n  )\n  .action(async (startOptions, options) => {\n    try {\n      const config = getProjectConfig();\n      const framework = options.framework ?? config.get('framework');\n\n      if (framework === 'nextjs') {\n        const nextBin = join('node_modules', '.bin', 'next');\n\n        if (!existsSync(nextBin)) {\n          throw new Error(\n            `Next.js is not installed in ${process.cwd()}. Are you in a valid Next.js project?`,\n          );\n        }\n\n        await execa(nextBin, ['start', ...startOptions], {\n          stdio: 'inherit',\n          cwd: process.cwd(),\n        });\n      }\n\n      await execa(\n        'pnpm',\n        [\n          'exec',\n          'opennextjs-cloudflare',\n          'preview',\n          '--config',\n          join('.bigcommerce', 'wrangler.jsonc'),\n          ...startOptions,\n        ],\n        {\n          stdio: 'inherit',\n          cwd: process.cwd(),\n        },\n      );\n    } catch (error) {\n      consola.error(error instanceof Error ? error.message : error);\n    }\n  });\n"
  },
  {
    "path": "packages/catalyst/src/cli/commands/telemetry.ts",
    "content": "import { Argument, Command, Option } from 'commander';\nimport { colorize } from 'consola/utils';\n\nimport { consola } from '../lib/logger';\nimport { Telemetry } from '../lib/telemetry';\n\nconst telemetryService = new Telemetry();\nlet isEnabled = telemetryService.isEnabled();\n\nexport const telemetry = new Command('telemetry')\n  .addArgument(new Argument('[arg]').choices(['disable', 'enable', 'status']))\n  .addOption(new Option('--enable', `Enables CLI telemetry collection.`).conflicts('disable'))\n  .option('--disable', `Disables CLI telemetry collection.`)\n  .action((arg, options) => {\n    if (options.enable || arg === 'enable') {\n      telemetryService.setEnabled(true);\n      isEnabled = true;\n\n      consola.success('Success!\\n');\n    } else if (options.disable || arg === 'disable') {\n      telemetryService.setEnabled(false);\n\n      if (isEnabled) {\n        consola.success('Your preference has been saved to .bigcommerce/project.json');\n      } else {\n        consola.info(`Catalyst CLI telemetry collection is already disabled.`);\n      }\n\n      isEnabled = false;\n    } else {\n      consola.info('Catalyst CLI Telemetry\\n');\n    }\n\n    consola.info(\n      `Status: ${colorize('bold', isEnabled ? colorize('green', 'Enabled') : colorize('red', 'Disabled'))}`,\n    );\n\n    if (!isEnabled) {\n      consola.info(\n        `You have opted-out of Catalyst CLI telemetry. No data will be collected from your machine.`,\n      );\n    }\n  });\n"
  },
  {
    "path": "packages/catalyst/src/cli/commands/version.spec.ts",
    "content": "import { Command } from '@commander-js/extra-typings';\nimport { beforeAll, expect, test, vi } from 'vitest';\n\nimport { consola } from '../lib/logger';\nimport { program } from '../program';\n\nimport { version } from './version';\n\nbeforeAll(() => {\n  consola.mockTypes(() => vi.fn());\n});\n\ntest('properly configured Command instance', () => {\n  expect(version).toBeInstanceOf(Command);\n  expect(version.name()).toBe('version');\n  expect(version.description()).toBe('Display detailed version information.');\n});\n\ntest('displays version information when executed', async () => {\n  await program.parseAsync(['node', 'catalyst', 'version']);\n\n  expect(consola.log).toHaveBeenCalledWith(expect.stringContaining('Version Information:'));\n\n  expect(consola.log).toHaveBeenCalledWith(\n    expect.stringContaining(`CLI Version: ${process.env.npm_package_version}`),\n  );\n\n  expect(consola.log).toHaveBeenCalledWith(\n    expect.stringContaining(`Node Version: ${process.version}`),\n  );\n\n  expect(consola.log).toHaveBeenCalledWith(\n    expect.stringContaining(`Platform: ${process.platform} (${process.arch})`),\n  );\n});\n"
  },
  {
    "path": "packages/catalyst/src/cli/commands/version.ts",
    "content": "import { Command } from 'commander';\n\nimport PACKAGE_INFO from '../../../package.json';\nimport { consola } from '../lib/logger';\n\nexport const version = new Command('version')\n  .description('Display detailed version information.')\n  .action(() => {\n    consola.log('Version Information:');\n    consola.log(`CLI Version: ${PACKAGE_INFO.version}`);\n    consola.log(`Node Version: ${process.version}`);\n    consola.log(`Platform: ${process.platform} (${process.arch})`);\n  });\n"
  },
  {
    "path": "packages/catalyst/src/cli/hooks/telemetry.ts",
    "content": "import { Command } from '@commander-js/extra-typings';\n\nimport { Telemetry } from '../lib/telemetry';\n\nconst telemetry = new Telemetry();\n\nconst allowlistArguments = ['--keep-temp-dir', '--api-host', '--project-uuid'];\n\nfunction parseArguments(args: string[]) {\n  return args.reduce<Record<string, string>>((result, arg, index, array) => {\n    if (arg.includes('=')) {\n      const [key, value] = arg.split('=');\n\n      if (allowlistArguments.includes(key)) {\n        return {\n          ...result,\n          [key]: value,\n        };\n      }\n    }\n\n    if (allowlistArguments.includes(arg)) {\n      const nextValue =\n        array[index + 1] && !array[index + 1].startsWith('--') ? array[index + 1] : null;\n\n      if (nextValue && !nextValue.includes('--')) {\n        return {\n          ...result,\n          [arg]: nextValue,\n        };\n      }\n    }\n\n    return result;\n  }, {});\n}\n\nexport const telemetryPreHook = async (command: Command) => {\n  const [commandName, ...args] = command.args;\n\n  // Return the await to get a proper stack trace.\n  // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression\n  return await telemetry.track(commandName, {\n    ...parseArguments(args),\n  });\n};\n\nexport const telemetryPostHook = async () => {\n  await telemetry.analytics.closeAndFlush();\n};\n"
  },
  {
    "path": "packages/catalyst/src/cli/index.spec.ts",
    "content": "import { Command } from '@commander-js/extra-typings';\nimport { describe, expect, test, vi } from 'vitest';\n\nvi.mock('./hooks/telemetry', () => ({\n  telemetryPreHook: vi.fn().mockResolvedValue(undefined),\n  telemetryPostHook: vi.fn().mockResolvedValue(undefined),\n}));\n\nimport { telemetryPostHook, telemetryPreHook } from './hooks/telemetry';\nimport { program } from './program';\n\ndescribe('CLI program', () => {\n  test('properly configured', () => {\n    expect(program).toBeInstanceOf(Command);\n    expect(program.name()).toBe(process.env.npm_package_name);\n    expect(program.version()).toBe(process.env.npm_package_version);\n    expect(program.description()).toBe('CLI tool for Catalyst development');\n  });\n\n  test('has expected commands', () => {\n    const commands = program.commands.map((cmd) => cmd.name());\n\n    expect(commands).toContain('version');\n    expect(commands).toContain('dev');\n    expect(commands).toContain('start');\n    expect(commands).toContain('build');\n    expect(commands).toContain('deploy');\n    expect(commands).toContain('project');\n\n    const projectCmd = program.commands.find((cmd) => cmd.name() === 'project');\n\n    expect(projectCmd?.commands.map((c) => c.name())).toEqual(\n      expect.arrayContaining(['create', 'list', 'link']),\n    );\n  });\n\n  test('telemetry hooks are called when executing version command', async () => {\n    vi.mocked(telemetryPreHook).mockClear();\n    vi.mocked(telemetryPostHook).mockClear();\n\n    await program.parseAsync(['version'], { from: 'user' });\n\n    expect(telemetryPreHook).toHaveBeenCalledTimes(1);\n    expect(telemetryPostHook).toHaveBeenCalledTimes(1);\n\n    expect(telemetryPreHook).toHaveBeenCalledWith(expect.any(Command), expect.any(Command));\n  });\n});\n"
  },
  {
    "path": "packages/catalyst/src/cli/index.ts",
    "content": "#!/usr/bin/env node\nimport { program } from './program';\n\nprogram.parse(process.argv);\n"
  },
  {
    "path": "packages/catalyst/src/cli/lib/get-module-cli-path.ts",
    "content": "/* eslint-disable no-underscore-dangle */\n\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport function getModuleCliPath() {\n  const __filename = fileURLToPath(import.meta.url);\n  const __dirname = dirname(__filename);\n\n  return join(__dirname, '..');\n}\n"
  },
  {
    "path": "packages/catalyst/src/cli/lib/logger.ts",
    "content": "import { createConsola } from 'consola';\n\nexport const consola = createConsola({\n  level: process.env.CONSOLA_LEVEL ? parseInt(process.env.CONSOLA_LEVEL, 10) : 3,\n});\n"
  },
  {
    "path": "packages/catalyst/src/cli/lib/mk-temp-dir.spec.ts",
    "content": "import { access, mkdir, stat, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { expect, test } from 'vitest';\n\nimport { mkTempDir } from './mk-temp-dir';\n\ntest('creates directory that actually exists', async () => {\n  const [path, cleanup] = await mkTempDir('catalyst-build-');\n\n  const stats = await stat(path);\n\n  expect(stats.isDirectory()).toBe(true);\n\n  await cleanup();\n});\n\ntest('cleanup function removes the directory', async () => {\n  const [path, cleanup] = await mkTempDir('catalyst-build-');\n\n  await expect(access(path)).resolves.not.toThrow();\n\n  await cleanup();\n\n  await expect(access(path)).rejects.toThrow();\n});\n\ntest('cleanup removes directory recursively with contents', async () => {\n  const [path, cleanup] = await mkTempDir('catalyst-build-');\n\n  await mkdir(join(path, 'subdir'));\n  await writeFile(join(path, 'file.txt'), 'test content');\n  await writeFile(join(path, 'subdir', 'nested.txt'), 'nested content');\n\n  await cleanup();\n\n  await expect(access(path)).rejects.toThrow();\n});\n\ntest('cleanup is idempotent', async () => {\n  const [, cleanup] = await mkTempDir('catalyst-build-');\n\n  await cleanup();\n  await expect(cleanup()).resolves.not.toThrow();\n});\n"
  },
  {
    "path": "packages/catalyst/src/cli/lib/mk-temp-dir.ts",
    "content": "import { mkdtemp, rm } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { consola } from '../lib/logger';\n\nexport async function mkTempDir(prefix = '/') {\n  const tmp = join(tmpdir(), prefix);\n\n  const path = await mkdtemp(tmp);\n\n  consola.info(`Created temporary directory: ${path}`);\n\n  return [\n    path,\n    async () => {\n      try {\n        consola.info(`Cleaning up temporary directory: ${path}`);\n        await rm(path, { recursive: true, force: true });\n        consola.success('Cleanup complete');\n      } catch (error) {\n        consola.warn(`Failed to clean up temporary directory: ${path}`, error);\n      }\n    },\n  ] as const;\n}\n"
  },
  {
    "path": "packages/catalyst/src/cli/lib/project-config.spec.ts",
    "content": "import Conf from 'conf';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { afterAll, beforeAll, expect, test } from 'vitest';\n\nimport { mkTempDir } from './mk-temp-dir';\nimport { getProjectConfig, ProjectConfigSchema } from './project-config';\n\nlet tmpDir: string;\nlet cleanup: () => Promise<void>;\nlet config: Conf<ProjectConfigSchema>;\n\nconst projectUuid = 'a23f5785-fd99-4a94-9fb3-945551623923';\n\nbeforeAll(async () => {\n  [tmpDir, cleanup] = await mkTempDir();\n\n  config = getProjectConfig(tmpDir);\n});\n\nafterAll(async () => {\n  await cleanup();\n});\n\ntest('throws error if field does not match schema', async () => {\n  const projectJsonPath = join(tmpDir, '.bigcommerce/project.json');\n\n  await mkdir(dirname(projectJsonPath), { recursive: true });\n  await writeFile(projectJsonPath, JSON.stringify({ projectUuid: 'invalid-uuid' }));\n\n  expect(() => config.get('projectUuid')).toThrowError(\n    'Config schema violation: `projectUuid` must match format \"uuid\"',\n  );\n});\n\ntest('writes and reads field from .bigcommerce/project.json', async () => {\n  const projectJsonPath = join(tmpDir, '.bigcommerce/project.json');\n\n  await mkdir(dirname(projectJsonPath), { recursive: true });\n  await writeFile(projectJsonPath, JSON.stringify({}));\n\n  config.set('projectUuid', projectUuid);\n\n  const modifiedProjectUuid = config.get('projectUuid');\n\n  expect(modifiedProjectUuid).toBe(projectUuid);\n});\n\ntest('sets default framework to nextjs', async () => {\n  const projectJsonPath = join(tmpDir, '.bigcommerce/project.json');\n\n  await mkdir(dirname(projectJsonPath), { recursive: true });\n  await writeFile(projectJsonPath, JSON.stringify({}));\n\n  expect(config.get('framework')).toBe('nextjs');\n});\n"
  },
  {
    "path": "packages/catalyst/src/cli/lib/project-config.ts",
    "content": "import Conf from 'conf';\nimport { join } from 'path';\n\nexport interface ProjectConfigSchema {\n  projectUuid: string;\n  framework: 'catalyst' | 'nextjs';\n  telemetry: {\n    enabled: boolean;\n    anonymousId: string;\n  };\n}\n\nexport function getProjectConfig(rootDir = process.cwd()) {\n  return new Conf<ProjectConfigSchema>({\n    cwd: join(rootDir, '.bigcommerce'),\n    projectSuffix: '',\n    configName: 'project',\n    schema: {\n      projectUuid: { type: 'string', format: 'uuid' },\n      framework: {\n        type: 'string',\n        enum: ['catalyst', 'nextjs'],\n        default: 'nextjs',\n      },\n      telemetry: {\n        type: 'object',\n        properties: {\n          enabled: { type: 'boolean' },\n          anonymousId: { type: 'string' },\n        },\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "packages/catalyst/src/cli/lib/project.ts",
    "content": "import { z } from 'zod';\n\nconst fetchProjectsSchema = z.object({\n  data: z.array(\n    z.object({\n      uuid: z.string(),\n      name: z.string(),\n    }),\n  ),\n});\n\nexport interface ProjectListItem {\n  uuid: string;\n  name: string;\n}\n\nexport async function fetchProjects(\n  storeHash: string,\n  accessToken: string,\n  apiHost: string,\n): Promise<ProjectListItem[]> {\n  const response = await fetch(\n    `https://${apiHost}/stores/${storeHash}/v3/infrastructure/projects`,\n    {\n      method: 'GET',\n      headers: {\n        'X-Auth-Token': accessToken,\n      },\n    },\n  );\n\n  if (response.status === 403) {\n    throw new Error(\n      'Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it.',\n    );\n  }\n\n  if (!response.ok) {\n    throw new Error(`Failed to fetch projects: ${response.statusText}`);\n  }\n\n  const res: unknown = await response.json();\n\n  const { data } = fetchProjectsSchema.parse(res);\n\n  return data;\n}\n\nconst createProjectSchema = z.object({\n  data: z.object({\n    uuid: z.string(),\n    name: z.string(),\n    date_created: z.coerce.date(),\n    date_modified: z.coerce.date(),\n  }),\n});\n\nexport interface CreateProjectResult {\n  uuid: string;\n  name: string;\n  date_created: Date;\n  date_modified: Date;\n}\n\nexport async function createProject(\n  name: string,\n  storeHash: string,\n  accessToken: string,\n  apiHost: string,\n): Promise<CreateProjectResult> {\n  const response = await fetch(\n    `https://${apiHost}/stores/${storeHash}/v3/infrastructure/projects`,\n    {\n      method: 'POST',\n      headers: {\n        'X-Auth-Token': accessToken,\n        Accept: 'application/json',\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({ name }),\n    },\n  );\n\n  if (response.status === 502) {\n    throw new Error('Failed to create project, is the name already in use?');\n  }\n\n  if (response.status === 403) {\n    throw new Error(\n      'Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it.',\n    );\n  }\n\n  if (!response.ok) {\n    throw new Error(`Failed to create project: ${response.statusText}`);\n  }\n\n  const res: unknown = await response.json();\n\n  const { data } = createProjectSchema.parse(res);\n\n  return data;\n}\n"
  },
  {
    "path": "packages/catalyst/src/cli/lib/telemetry.ts",
    "content": "import { Analytics } from '@segment/analytics-node';\nimport Conf from 'conf';\nimport { randomBytes } from 'node:crypto';\n\nimport PACKAGE_INFO from '../../../package.json';\n\nimport { getProjectConfig, ProjectConfigSchema } from './project-config';\n\nconst TELEMETRY_KEY_ENABLED = 'telemetry.enabled';\nconst TELEMETRY_KEY_ID = `telemetry.anonymousId`;\n\nexport class Telemetry {\n  readonly sessionId: string;\n  readonly analytics: Analytics;\n\n  private projectConfig: Conf<ProjectConfigSchema>;\n  private CATALYST_TELEMETRY_DISABLED: string | undefined;\n\n  private readonly projectName = 'catalyst-cli';\n  private readonly projectVersion = PACKAGE_INFO.version;\n\n  constructor() {\n    this.CATALYST_TELEMETRY_DISABLED = process.env.CATALYST_TELEMETRY_DISABLED;\n\n    this.projectConfig = getProjectConfig();\n\n    this.sessionId = randomBytes(32).toString('hex');\n    this.analytics = new Analytics({\n      writeKey: process.env.CLI_SEGMENT_WRITE_KEY ?? 'not-a-valid-segment-write-key',\n    });\n  }\n\n  async track(eventName: string, payload: Record<string, unknown>) {\n    if (!this.isEnabled()) {\n      return Promise.resolve(undefined);\n    }\n\n    this.analytics.track({\n      event: eventName,\n      anonymousId: this.getAnonymousId(),\n      properties: {\n        ...payload,\n        sessionId: this.sessionId,\n      },\n      context: {\n        app: {\n          name: this.projectName,\n          version: this.projectVersion,\n        },\n      },\n    });\n  }\n\n  async identify(storeHash?: string) {\n    if (!this.isEnabled()) {\n      return Promise.resolve(undefined);\n    }\n\n    if (!storeHash) {\n      return Promise.resolve(undefined);\n    }\n\n    this.analytics.identify({\n      userId: storeHash,\n      anonymousId: this.getAnonymousId(),\n      context: {\n        app: {\n          name: this.projectName,\n          version: this.projectVersion,\n        },\n      },\n    });\n  }\n\n  setEnabled = (_enabled: boolean) => {\n    const enabled = Boolean(_enabled);\n\n    this.projectConfig.set('telemetry.enabled', enabled);\n  };\n\n  isEnabled() {\n    return (\n      !this.CATALYST_TELEMETRY_DISABLED &&\n      this.projectConfig.get<typeof TELEMETRY_KEY_ENABLED, boolean>(TELEMETRY_KEY_ENABLED, true)\n    );\n  }\n\n  private getAnonymousId(): string {\n    const val = this.projectConfig.get<typeof TELEMETRY_KEY_ID, string>(TELEMETRY_KEY_ID);\n\n    if (val) {\n      return val;\n    }\n\n    const generated = randomBytes(32).toString('hex');\n\n    this.projectConfig.set(TELEMETRY_KEY_ID, generated);\n\n    return generated;\n  }\n}\n"
  },
  {
    "path": "packages/catalyst/src/cli/lib/wrangler-config.spec.ts",
    "content": "import { expect, test } from 'vitest';\n\nimport { getWranglerConfig } from './wrangler-config';\n\ntest('returns a config with name identical to worker self reference service', () => {\n  const config = getWranglerConfig('uuid', 'kv-namespace-id');\n\n  expect(config.name).toBe(`project-uuid`);\n  expect(\n    config.services.find((service) => service.binding === 'WORKER_SELF_REFERENCE')?.service,\n  ).toBe(`project-uuid`);\n});\n"
  },
  {
    "path": "packages/catalyst/src/cli/lib/wrangler-config.ts",
    "content": "export function getWranglerConfig(projectUuid: string, kvNamespaceId: string) {\n  return {\n    $schema: 'node_modules/wrangler/config-schema.json',\n    main: '../.open-next/worker.js',\n    name: `project-${projectUuid}`,\n    compatibility_date: '2025-07-15',\n    compatibility_flags: ['nodejs_compat', 'global_fetch_strictly_public'],\n    observability: {\n      enabled: true,\n      head_sampling_rate: 0.05,\n      logs: {\n        enabled: true,\n        head_sampling_rate: 1,\n        invocation_logs: false,\n      },\n    },\n    assets: {\n      directory: '../.open-next/assets',\n      binding: 'ASSETS',\n    },\n    services: [\n      {\n        binding: 'WORKER_SELF_REFERENCE',\n        service: `project-${projectUuid}`,\n      },\n    ],\n    kv_namespaces: [\n      {\n        binding: 'NEXT_INC_CACHE_KV',\n        id: kvNamespaceId,\n      },\n    ],\n    durable_objects: {\n      bindings: [\n        {\n          name: 'NEXT_CACHE_DO_QUEUE',\n          class_name: 'DOQueueHandler',\n        },\n        {\n          name: 'NEXT_TAG_CACHE_DO_SHARDED',\n          class_name: 'DOShardedTagCache',\n        },\n        {\n          name: 'NEXT_CACHE_DO_PURGE',\n          class_name: 'BucketCachePurge',\n        },\n      ],\n    },\n    migrations: [\n      {\n        tag: 'v1',\n        new_sqlite_classes: ['DOQueueHandler', 'DOShardedTagCache', 'BucketCachePurge'],\n      },\n    ],\n  };\n}\n"
  },
  {
    "path": "packages/catalyst/src/cli/program.ts",
    "content": "import { Command } from 'commander';\nimport { colorize } from 'consola/utils';\nimport { config } from 'dotenv';\nimport { resolve } from 'node:path';\n\nimport PACKAGE_INFO from '../../package.json';\n\nimport { build } from './commands/build';\nimport { deploy } from './commands/deploy';\nimport { dev } from './commands/dev';\nimport { project } from './commands/project';\nimport { start } from './commands/start';\nimport { telemetry } from './commands/telemetry';\nimport { version } from './commands/version';\nimport { telemetryPostHook, telemetryPreHook } from './hooks/telemetry';\nimport { consola } from './lib/logger';\n\nexport const program = new Command();\n\nconfig({\n  path: [\n    resolve(process.cwd(), '.env'),\n    resolve(process.cwd(), '.env.local'),\n    // Assumes the parent directory is the monorepo root:\n    resolve(process.cwd(), '..', '.env'),\n    resolve(process.cwd(), '..', '.env.local'),\n  ],\n});\n\nconsola.log(colorize('cyanBright', `◢ ${PACKAGE_INFO.name} v${PACKAGE_INFO.version}\\n`));\n\nprogram\n  .name(PACKAGE_INFO.name)\n  .version(PACKAGE_INFO.version)\n  .description('CLI tool for Catalyst development')\n  .addCommand(version)\n  .addCommand(dev)\n  .addCommand(start)\n  .addCommand(build)\n  .addCommand(deploy)\n  .addCommand(project)\n  .addCommand(telemetry)\n  .hook('preAction', telemetryPreHook)\n  .hook('postAction', telemetryPostHook);\n"
  },
  {
    "path": "packages/catalyst/templates/open-next.config.ts",
    "content": "import { defineCloudflareConfig, type OpenNextConfig } from '@opennextjs/cloudflare';\nimport { purgeCache } from '@opennextjs/cloudflare/overrides/cache-purge/index';\nimport kvIncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache';\nimport doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';\nimport queueCache from '@opennextjs/cloudflare/overrides/queue/queue-cache';\nimport doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';\n\nconst cloudflareConfig = defineCloudflareConfig({\n  incrementalCache: kvIncrementalCache,\n  queue: queueCache(doQueue, {\n    regionalCacheTtlSec: 5,\n  }),\n  routePreloadingBehavior: 'withWaitUntil',\n  tagCache: doShardedTagCache({\n    baseShardSize: 12,\n    regionalCache: true,\n    regionalCacheTtlSec: 5,\n    shardReplication: {\n      numberOfSoftReplicas: 4,\n      numberOfHardReplicas: 2,\n      regionalReplication: {\n        defaultRegion: 'enam',\n      },\n    },\n  }),\n  enableCacheInterception: false,\n  cachePurge: purgeCache({ type: 'durableObject' }),\n});\n\nconst config: OpenNextConfig = {\n  buildCommand: 'node_modules/.bin/next build',\n  ...cloudflareConfig,\n};\n\nexport default config;\n"
  },
  {
    "path": "packages/catalyst/tests/mocks/handlers.ts",
    "content": "import { http, HttpResponse } from 'msw';\n\nconst encoder = new TextEncoder();\n\nexport const handlers = [\n  // Handler for generateUploadSignature\n  http.post('https://:apiHost/stores/:storeHash/v3/infrastructure/deployments/uploads', () =>\n    HttpResponse.json({\n      data: {\n        upload_url: 'https://mock-upload-url.com',\n        upload_uuid: '0e93ce5f-6f91-4236-87ec-ca79627f31ba',\n      },\n    }),\n  ),\n\n  // Handler for uploadBundleZip\n  http.put('https://mock-upload-url.com', () => new HttpResponse(null, { status: 200 })),\n\n  // Handler for createDeployment\n  http.post('https://:apiHost/stores/:storeHash/v3/infrastructure/deployments', () =>\n    HttpResponse.json({\n      data: {\n        deployment_uuid: '5b29c3c0-5f68-44fe-99e5-06492babf7be',\n      },\n    }),\n  ),\n\n  // Handler for fetchProjects\n  http.get('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () =>\n    HttpResponse.json({\n      data: [\n        { uuid: 'a23f5785-fd99-4a94-9fb3-945551623923', name: 'Project One' },\n        { uuid: 'b23f5785-fd99-4a94-9fb3-945551623924', name: 'Project Two' },\n      ],\n    }),\n  ),\n\n  // Handler for getDeploymentStatus\n  http.get(\n    'https://:apiHost/stores/:storeHash/v3/infrastructure/deployments/:deploymentUuid/events',\n    ({ params }) => {\n      const stream = new ReadableStream({\n        start(controller) {\n          controller.enqueue(\n            encoder.encode(\n              // eslint-disable-next-line @typescript-eslint/restrict-template-expressions\n              `data: {\"deployment_status\":\"in_progress\",\"deployment_uuid\":\"${params.deploymentUuid}\",\"event\":{\"step\":\"processing\",\"progress\":75}}`,\n            ),\n          );\n          setTimeout(() => {\n            controller.enqueue(\n              encoder.encode(\n                // eslint-disable-next-line @typescript-eslint/restrict-template-expressions\n                `data: {\"deployment_status\":\"in_progress\",\"deployment_uuid\":\"${params.deploymentUuid}\",\"event\":{\"step\":\"finalizing\",\"progress\":99}}`,\n              ),\n            );\n          }, 10);\n          setTimeout(() => {\n            controller.enqueue(\n              encoder.encode(\n                // eslint-disable-next-line @typescript-eslint/restrict-template-expressions\n                `data: {\"deployment_status\":\"completed\",\"deployment_uuid\":\"${params.deploymentUuid}\",\"event\":null}`,\n              ),\n            );\n            controller.close();\n          }, 20);\n        },\n      });\n\n      return new HttpResponse(stream, {\n        status: 200,\n        headers: { 'Content-Type': 'text/event-stream' },\n      });\n    },\n  ),\n\n  // Handle for createProjects\n  http.post('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () =>\n    HttpResponse.json({\n      data: {\n        uuid: 'c23f5785-fd99-4a94-9fb3-945551623925',\n        name: 'New Project',\n        date_created: new Date().toISOString(),\n        date_modified: new Date().toISOString(),\n      },\n    }),\n  ),\n];\n"
  },
  {
    "path": "packages/catalyst/tests/mocks/node.ts",
    "content": "import { setupServer } from 'msw/node';\n\nimport { handlers } from './handlers';\n\nexport const server = setupServer(...handlers);\n"
  },
  {
    "path": "packages/catalyst/tests/mocks/spinner.ts",
    "content": "import { vi } from 'vitest';\n\nexport const textHistory: string[] = [];\n\nexport default vi.fn().mockImplementation(({ text }: { text?: string } = {}) => {\n  if (text) textHistory.push(text);\n\n  const spinner = {\n    _text: text ?? '',\n    start: vi.fn().mockImplementation((methodText?: string) => {\n      if (methodText) textHistory.push(methodText);\n\n      return spinner;\n    }),\n    stop: vi.fn().mockImplementation((methodText?: string) => {\n      if (methodText) textHistory.push(methodText);\n\n      return spinner;\n    }),\n    success: vi.fn().mockImplementation((methodText?: string) => {\n      if (methodText) textHistory.push(methodText);\n\n      return spinner;\n    }),\n    error: vi.fn().mockImplementation((methodText?: string) => {\n      if (methodText) textHistory.push(methodText);\n\n      return spinner;\n    }),\n    warning: vi.fn().mockImplementation((methodText?: string) => {\n      if (methodText) textHistory.push(methodText);\n\n      return spinner;\n    }),\n    info: vi.fn().mockImplementation((methodText?: string) => {\n      if (methodText) textHistory.push(methodText);\n\n      return spinner;\n    }),\n    get text() {\n      return this._text;\n    },\n    set text(value: string) {\n      this._text = value;\n      textHistory.push(value);\n    },\n  };\n\n  return spinner;\n});\n"
  },
  {
    "path": "packages/catalyst/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Node\",\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true\n  }\n}\n"
  },
  {
    "path": "packages/catalyst/tsup.config.ts",
    "content": "import { defineConfig, Options } from 'tsup';\n\nexport default defineConfig((options: Options) => ({\n  entry: {\n    cli: 'src/cli/index.ts',\n  },\n  format: ['esm'],\n  clean: !options.watch,\n  sourcemap: true,\n  env: {\n    CLI_SEGMENT_WRITE_KEY: process.env.CLI_SEGMENT_WRITE_KEY ?? 'not-a-valid-segment-write-key',\n    CONSOLA_LEVEL: process.env.NODE_ENV === 'production' ? '2' : '3',\n  },\n  ...options,\n}));\n"
  },
  {
    "path": "packages/catalyst/vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  test: {\n    environment: 'node',\n    setupFiles: './vitest.setup.ts',\n    include: ['src/**/*.spec.ts'],\n    exclude: ['**/node_modules/**', '**/dist/**'],\n    coverage: {\n      include: ['src/**/*.ts'],\n      exclude: ['src/cli/index.ts'],\n      thresholds: {\n        100: true,\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "packages/catalyst/vitest.setup.ts",
    "content": "import { afterAll, afterEach, beforeAll, vi } from 'vitest';\n\nimport { server } from './tests/mocks/node';\n\nvi.mock('../src/lib/telemetry', () => ({\n  Telemetry: vi.fn().mockImplementation(() => ({\n    sessionId: 'test-session-id',\n    analytics: {\n      track: vi.fn(),\n      identify: vi.fn(),\n      closeAndFlush: vi.fn().mockResolvedValue(undefined),\n    },\n    track: vi.fn().mockResolvedValue(undefined),\n    identify: vi.fn().mockResolvedValue(undefined),\n    setEnabled: vi.fn(),\n    isEnabled: vi.fn().mockReturnValue(false),\n  })),\n}));\n\nbeforeAll(() => server.listen());\nafterEach(() => server.resetHandlers());\nafterAll(() => server.close());\n"
  },
  {
    "path": "packages/client/.eslintrc.cjs",
    "content": "// @ts-check\n\n/** @type {import('eslint').Linter.LegacyConfig} */\nconst config = {\n  root: true,\n  extends: ['@bigcommerce/catalyst/base', '@bigcommerce/catalyst/prettier'],\n  rules: {\n    '@typescript-eslint/consistent-type-assertions': 'off',\n    '@typescript-eslint/naming-convention': 'off',\n    'no-underscore-dangle': ['error', { allow: ['__typename'] }],\n    'check-file/filename-naming-convention': 'off',\n  },\n  ignorePatterns: ['/src/generated/**', '/dist/**'],\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "packages/client/CHANGELOG.md",
    "content": "# Changelog\n\n## 1.0.1\n\n### Patch Changes\n\n- [#2563](https://github.com/bigcommerce/catalyst/pull/2563) [`707ec24`](https://github.com/bigcommerce/catalyst/commit/707ec24745b6a0040551328d64657ff40df4e252) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Replace usage of Date.now with performance.now for compatibility with upcoming Next.js Composable Cache feature\n\n- [#2565](https://github.com/bigcommerce/catalyst/pull/2565) [`a27054f`](https://github.com/bigcommerce/catalyst/commit/a27054f4f22013707d40a100b15122c22354c956) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Truncate performance.now to 2 decimal places\n\n## 1.0.0\n\n### Major Changes\n\n- [#2435](https://github.com/bigcommerce/catalyst/pull/2435) [`cd4bd60`](https://github.com/bigcommerce/catalyst/commit/cd4bd604739b0cea4b622b08ebbde4cea953fcae) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Release 1.0.0 (see [`core/CHANGELOG.md`](../../core/CHANGELOG.md#100) for more details)\n\n### Minor Changes\n\n- [#2370](https://github.com/bigcommerce/catalyst/pull/2370) [`20b8788`](https://github.com/bigcommerce/catalyst/commit/20b87882e089438c6183e83a506267e432a4f741) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Remove the `xAuthToken` config parameter from `@bigcommerce/catalyst-client`. The client no longer has any dependency on a BigCommerce access token, now that we have replaced the `/v2/shipping/zones` REST API call with an appropriate GraphQL field (`site.settings.shipping.supportedShippingDestinations`).\n\n  Migration:\n  1. If you are using the version of the client published to NPM, simply ensure you are using at least `@bigcommerce/catalyst-client@0.16.0` or higher.\n  2. If you are using the client in your pnpm workspace, simply remove the `xAuthToken` references in `packages/client/src/client.ts` as well as the `fetchShippingZones` method.\n  3. Remove the reference to `xAuthToken` in `core/client/index.ts`\n\n## 0.15.0\n\n### Minor Changes\n\n- [#1914](https://github.com/bigcommerce/catalyst/pull/1914) [`f039b2c`](https://github.com/bigcommerce/catalyst/commit/f039b2c7235118626d7a727bff5271ac8982f910) Thanks [@jorgemoya](https://github.com/jorgemoya)! - GQL requests that respond as `200` but have an `errors` field will now be properly handled by the client and throw a proper `BigCommerceGQLError` response with the message reason from the API. This will provide a more detailed description of why the GQL request errored out.\n\n  API errors will still be handled and attribute the errored status as the message with this change as `BigCommerceAPIError`.\n\n- [`0aa23e2`](undefined) - Add an `onError` callback to in order to handle auth and invalid sessions.\n\n- [#2124](https://github.com/bigcommerce/catalyst/pull/2124) [`4a00a27`](https://github.com/bigcommerce/catalyst/commit/4a00a27acea733b6f3fef221b3d1472b145d25f0) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add an `errorPolicy` option for GQL requests. Accepts `none`, `ignore`, `all`. Defaults to `none` which throws an error if there are GQL errors, `ignore` returns the data without error object, and `all` returns both data and errors.\n\n### Patch Changes\n\n- [`c830100`](undefined) - Manual changes on a dependency bumps.\n\n- [#2226](https://github.com/bigcommerce/catalyst/pull/2226) [`3b14d66`](https://github.com/bigcommerce/catalyst/commit/3b14d668d32e7ebe37e31b1851b3db8f8be46bec) Thanks [@bookernath](https://github.com/bookernath)! - Add GraphQL operation name and type to GraphQL URL as query parameters to improve server logging of GraphQL operations\n\n## 0.14.0\n\n### Minor Changes\n\n- [#1636](https://github.com/bigcommerce/catalyst/pull/1636) [`23abacf`](https://github.com/bigcommerce/catalyst/commit/23abacfb8ff4ff9d269e51821a6a992a9cb2d4f5) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Add optional error to BigCommerceResponse type\n\n## 0.13.0\n\n### Minor Changes\n\n- [#1623](https://github.com/bigcommerce/catalyst/pull/1623) [`16e3a76`](https://github.com/bigcommerce/catalyst/commit/16e3a763571324dccd9031a79e400409eff9ee0c) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Adds async support to beforeRequest hook\n\n## 0.12.0\n\n### Minor Changes\n\n- [#1262](https://github.com/bigcommerce/catalyst/pull/1262) [`0c2023b`](https://github.com/bigcommerce/catalyst/commit/0c2023bae650039cd79ba51b1161b5c8c16f0b8d) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Removes all usages of the customer impersonation token. Also updates the docs to correspond with the Storefront API Token.\n\n- [#1262](https://github.com/bigcommerce/catalyst/pull/1262) [`0c2023b`](https://github.com/bigcommerce/catalyst/commit/0c2023bae650039cd79ba51b1161b5c8c16f0b8d) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Allows the ability to consume a [storefront token](https://developer.bigcommerce.com/docs/rest-authentication/tokens#storefront-tokens). This new token will allow Catalyst to create `customerAccessToken`'s whenever a user logs into their account. This change doesn't include consuming the either token, only adding the ability to pass it in.\n\n## 0.11.0\n\n### Minor Changes\n\n- [#1483](https://github.com/bigcommerce/catalyst/pull/1483) [`d4120d3`](https://github.com/bigcommerce/catalyst/commit/d4120d39c10398e842a7ebe14ada685ec8aae3a8) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Start collecting telemetry.\n\n## 0.10.0\n\n### Minor Changes\n\n- [#1449](https://github.com/bigcommerce/catalyst/pull/1449) [`2d1526a`](https://github.com/bigcommerce/catalyst/commit/2d1526a50402b2eb677abd55f19fb904234d1a84) Thanks [@bookernath](https://github.com/bookernath)! - Support Trusted Proxy in client to support higher-traffic stores\n\n## 0.9.0\n\n### Minor Changes\n\n- [#1384](https://github.com/bigcommerce/catalyst/pull/1384) [`17692ca`](https://github.com/bigcommerce/catalyst/commit/17692caa3ff9b25180359d8a020470ece3e589f6) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Add the ability to hook into the fetchOptions before the request is sent.\n\n## 0.8.0\n\n### Minor Changes\n\n- [#1350](https://github.com/bigcommerce/catalyst/pull/1350) [`88663d1`](https://github.com/bigcommerce/catalyst/commit/88663d165691380b35f83726f0589896bdc73bf2) Thanks [@deini](https://github.com/deini)! - remove graphql and use @0no-co/graphql.web for a smaller bundle size\n\n## 0.7.0\n\n### Minor Changes\n\n- [#1261](https://github.com/bigcommerce/catalyst/pull/1261) [`f715067`](https://github.com/bigcommerce/catalyst/commit/f715067aa36616b3818c9424c57fa08e28936cde) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Remove the need of fetching shipping countries by using the GraphQL data.\n\n## 0.6.0\n\n### Minor Changes\n\n- [#1200](https://github.com/bigcommerce/catalyst/pull/1200) [`51704d9`](https://github.com/bigcommerce/catalyst/commit/51704d9b9a7158c625c84f79e2ba95f98c6dc673) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Remove the `fetchAvailableCountries` query as it is no longer needed in Catalyst. This helps us remove queries that are dependent on the access token.\n\n## 0.5.0\n\n### Minor Changes\n\n- [#1098](https://github.com/bigcommerce/catalyst/pull/1098) [`405e791`](https://github.com/bigcommerce/catalyst/commit/405e791af8e7ecc1422f2ce18cb216a8c04cc73b) Thanks [@bookernath](https://github.com/bookernath)! - Move Sitemap Index fetching into the client & normalize user agents\n\n### Patch Changes\n\n- [#994](https://github.com/bigcommerce/catalyst/pull/994) [`8766305`](https://github.com/bigcommerce/catalyst/commit/8766305b65ca10422e7921b2fd15796e0a09d27a) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add channelId param to client to allow fetching from multiple channels with the same client.\n\n- [#1055](https://github.com/bigcommerce/catalyst/pull/1055) [`52214a3`](https://github.com/bigcommerce/catalyst/commit/52214a376bba1fdaa584de31c36f7d6cdc078624) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Add getChannelId param to dynamically fetch a channel on requests.\n\n## 0.4.0\n\n### Minor Changes\n\n- [#910](https://github.com/bigcommerce/catalyst/pull/910) [`d0352c0`](https://github.com/bigcommerce/catalyst/commit/d0352c08b43e76b4cd838cb7916f9993228e3fa0) Thanks [@deini](https://github.com/deini)! - removes fetch cart redirect from client and fetch it with gql\n\n## 0.3.0\n\n### Minor Changes\n\n- [#753](https://github.com/bigcommerce/catalyst/pull/753) [`48c040e`](https://github.com/bigcommerce/catalyst/commit/48c040e94745134f4c60b15cadcdb0a0bbcb2a36) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Deprecate `node@18` in favor of latest LTS version `node@20`.\n\n## 0.2.2\n\n### Patch Changes\n\n- [#740](https://github.com/bigcommerce/catalyst/pull/740) [`d586c21`](https://github.com/bigcommerce/catalyst/commit/d586c2122bf6513b2f7d923957636c7ea8aaf2ce) Thanks [@dependabot](https://github.com/apps/dependabot)! - chore: bump next-auth and use string for user id\n\n## 0.2.1\n\n### Patch Changes\n\n- [#735](https://github.com/bigcommerce/catalyst/pull/735) [`3db9c5f`](https://github.com/bigcommerce/catalyst/commit/3db9c5fa603299a5c5a9a12bd5408f9024677b20) Thanks [@deini](https://github.com/deini)! - Bump dependencies\n\n## 0.2.0\n\n### Minor Changes\n\n- [#685](https://github.com/bigcommerce/catalyst/pull/685) [`ac733cc`](https://github.com/bigcommerce/catalyst/commit/ac733cc0308b3ebe1189fe6a7d20214dbc382b3f) Thanks [@deini](https://github.com/deini)! - adds support for DocumentNode\n  All notable changes to this project will be documented in this file.\n"
  },
  {
    "path": "packages/client/README.md",
    "content": "# packages/client\n"
  },
  {
    "path": "packages/client/package.json",
    "content": "{\n  \"name\": \"@bigcommerce/catalyst-client\",\n  \"description\": \"BigCommerce API client for Catalyst.\",\n  \"version\": \"1.0.1\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bigcommerce/catalyst\",\n    \"directory\": \"packages/client\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"lint\": \"eslint . --ext .ts,.js,.cjs --max-warnings 0\",\n    \"lint-fix\": \"eslint . --ext .ts,.js,.cjs --fix\",\n    \"gen-types\": \"dotenv -e .env.local -- node scripts/types.js\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"dependencies\": {\n    \"@0no-co/graphql.web\": \"^1.1.2\",\n    \"std-env\": \"^3.9.0\"\n  },\n  \"devDependencies\": {\n    \"@bigcommerce/eslint-config\": \"^2.11.0\",\n    \"@bigcommerce/eslint-config-catalyst\": \"workspace:^\",\n    \"@types/node\": \"^22.15.30\",\n    \"dotenv-cli\": \"^8.0.0\",\n    \"eslint\": \"^8.57.1\",\n    \"prettier\": \"^3.6.2\",\n    \"tsup\": \"^8.5.0\",\n    \"typescript\": \"^5.8.3\"\n  }\n}\n"
  },
  {
    "path": "packages/client/prettier.config.js",
    "content": "// @ts-check\n\n/** @type {import(\"prettier\").Config} */\nconst config = {\n  printWidth: 100,\n  singleQuote: true,\n  trailingComma: 'all',\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "packages/client/src/api-error.ts",
    "content": "export class BigCommerceAPIError extends Error {\n  constructor(\n    public status: number,\n    public graphqlErrors: unknown[] = [],\n  ) {\n    const message = `\n    BigCommerce API returned ${status}\n    ${graphqlErrors.map((error) => JSON.stringify(error, null, 2)).join('\\n')}\n    `;\n\n    super(message);\n    this.name = 'BigCommerceAPIError';\n  }\n\n  static async createFromResponse(response: Response) {\n    try {\n      const errorResponse: unknown = await response.json();\n\n      assertIsErrorResponse(errorResponse);\n\n      return new BigCommerceAPIError(response.status, errorResponse.errors);\n    } catch {\n      return new BigCommerceAPIError(response.status);\n    }\n  }\n}\n\nfunction assertIsErrorResponse(value: unknown): asserts value is { errors: unknown[] } {\n  if (typeof value !== 'object' || value === null) {\n    throw new Error('Expected maybeError to be an object');\n  }\n\n  if (!('errors' in value)) {\n    throw new Error('Expected maybeError to have an errors property');\n  }\n}\n"
  },
  {
    "path": "packages/client/src/client.ts",
    "content": "import { BigCommerceAPIError } from './api-error';\nimport { BigCommerceAuthError } from './gql-auth-error';\nimport { BigCommerceGQLError } from './gql-error';\nimport { parseGraphQLError } from './lib/error';\nimport { DocumentDecoration } from './types';\nimport { getOperationInfo } from './utils/getOperationName';\nimport { normalizeQuery } from './utils/normalizeQuery';\nimport { getBackendUserAgent } from './utils/userAgent';\n\nexport const graphqlApiDomain: string =\n  process.env.BIGCOMMERCE_GRAPHQL_API_DOMAIN ?? 'mybigcommerce.com';\n\nexport const adminApiHostname: string =\n  process.env.BIGCOMMERCE_ADMIN_API_HOST ?? 'api.bigcommerce.com';\n\ninterface Config<FetcherRequestInit extends RequestInit = RequestInit> {\n  storeHash: string;\n  storefrontToken: string;\n  channelId?: string;\n  platform?: string;\n  backendUserAgentExtensions?: string;\n  logger?: boolean;\n  getChannelId?: (defaultChannelId: string) => Promise<string> | string;\n  beforeRequest?: (\n    fetchOptions?: FetcherRequestInit,\n  ) => Promise<Partial<FetcherRequestInit> | undefined> | Partial<FetcherRequestInit> | undefined;\n  onError?: (\n    error: BigCommerceGQLError,\n    queryType: 'query' | 'mutation' | 'subscription',\n  ) => Promise<void> | void;\n}\n\ninterface BigCommerceResponseError {\n  message: string;\n  locations: Array<{\n    line: number;\n    column: number;\n  }>;\n  path: string[];\n}\n\ninterface BigCommerceResponse<T> {\n  data: T;\n  errors?: BigCommerceResponseError[];\n}\n\ntype GraphQLErrorPolicy = 'none' | 'all' | 'auth' | 'ignore';\n\nclass Client<FetcherRequestInit extends RequestInit = RequestInit> {\n  private backendUserAgent: string;\n  private readonly defaultChannelId: string;\n  private getChannelId: (defaultChannelId: string) => Promise<string> | string;\n  private beforeRequest?: (\n    fetchOptions?: FetcherRequestInit,\n  ) => Promise<Partial<FetcherRequestInit> | undefined> | Partial<FetcherRequestInit> | undefined;\n  private onError?: (\n    error: BigCommerceGQLError,\n    queryType: 'query' | 'mutation' | 'subscription',\n  ) => Promise<void> | void;\n\n  private trustedProxySecret = process.env.BIGCOMMERCE_TRUSTED_PROXY_SECRET;\n\n  constructor(private config: Config<FetcherRequestInit>) {\n    if (!config.channelId) {\n      throw new Error('Client configuration must include a channelId.');\n    }\n\n    this.defaultChannelId = config.channelId;\n    this.backendUserAgent = getBackendUserAgent(config.platform, config.backendUserAgentExtensions);\n\n    this.getChannelId =\n      config.getChannelId ??\n      function defaultChannelIdFn(defaultChannelId) {\n        return defaultChannelId;\n      };\n\n    this.beforeRequest = config.beforeRequest;\n    this.onError = config.onError;\n  }\n\n  // Overload for documents that require variables\n  async fetch<TResult, TVariables extends Record<string, unknown>>(config: {\n    document: DocumentDecoration<TResult, TVariables>;\n    variables: TVariables;\n    customerAccessToken?: string;\n    fetchOptions?: FetcherRequestInit;\n    channelId?: string;\n    errorPolicy?: GraphQLErrorPolicy;\n    validateCustomerAccessToken?: boolean;\n  }): Promise<BigCommerceResponse<TResult>>;\n\n  // Overload for documents that do not require variables\n  async fetch<TResult>(config: {\n    document: DocumentDecoration<TResult, Record<string, never>>;\n    variables?: undefined;\n    customerAccessToken?: string;\n    fetchOptions?: FetcherRequestInit;\n    channelId?: string;\n    errorPolicy?: GraphQLErrorPolicy;\n    validateCustomerAccessToken?: boolean;\n  }): Promise<BigCommerceResponse<TResult>>;\n\n  async fetch<TResult, TVariables>({\n    document,\n    variables,\n    customerAccessToken,\n    fetchOptions = {} as FetcherRequestInit,\n    channelId,\n    errorPolicy = 'none',\n    validateCustomerAccessToken = true,\n  }: {\n    document: DocumentDecoration<TResult, TVariables>;\n    variables?: TVariables;\n    customerAccessToken?: string;\n    fetchOptions?: FetcherRequestInit;\n    channelId?: string;\n    errorPolicy?: GraphQLErrorPolicy;\n    validateCustomerAccessToken?: boolean;\n  }): Promise<BigCommerceResponse<TResult>> {\n    const { headers = {}, ...rest } = fetchOptions;\n    const query = normalizeQuery(document);\n    const log = this.requestLogger(query);\n    const operationInfo = getOperationInfo(query);\n\n    const graphqlUrl = await this.getGraphQLEndpoint(\n      channelId,\n      operationInfo.name,\n      operationInfo.type,\n    );\n    const { headers: additionalFetchHeaders = {}, ...additionalFetchOptions } =\n      (await this.beforeRequest?.(fetchOptions)) ?? {};\n\n    const response = await fetch(graphqlUrl, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        Authorization: `Bearer ${this.config.storefrontToken}`,\n        'User-Agent': this.backendUserAgent,\n        ...(customerAccessToken && { 'X-Bc-Customer-Access-Token': customerAccessToken }),\n        ...(validateCustomerAccessToken && {\n          'X-Bc-Error-On-Invalid-Customer-Access-Token': 'true',\n        }),\n        ...(this.trustedProxySecret && { 'X-BC-Trusted-Proxy-Secret': this.trustedProxySecret }),\n        ...Object.fromEntries(new Headers(additionalFetchHeaders).entries()),\n        ...Object.fromEntries(new Headers(headers).entries()),\n      },\n      body: JSON.stringify({\n        query,\n        ...(variables && { variables }),\n      }),\n      ...additionalFetchOptions,\n      ...rest,\n    });\n\n    if (!response.ok) {\n      throw await BigCommerceAPIError.createFromResponse(response);\n    }\n\n    log(response);\n\n    const result = (await response.json()) as BigCommerceResponse<TResult>;\n\n    const { errors, ...data } = result;\n\n    // If errorPolicy is 'none', we throw an error if there are any errors\n    if (errorPolicy === 'none' && errors) {\n      const error = parseGraphQLError(errors);\n\n      await this.onError?.(error, operationInfo.type);\n\n      throw error;\n    }\n\n    if (errorPolicy === 'auth' && errors) {\n      const error = parseGraphQLError(errors);\n\n      if (error instanceof BigCommerceAuthError) {\n        await this.onError?.(error, operationInfo.type);\n\n        throw error;\n      }\n    }\n\n    // If errorPolicy is 'ignore', we return the data and ignore the errors\n    if (errorPolicy === 'ignore') {\n      return data;\n    }\n\n    // If errorPolicy is 'all', we return the errors with the data\n    return result;\n  }\n\n  async fetchSitemapIndex(channelId?: string): Promise<string> {\n    const sitemapIndexUrl = `${await this.getCanonicalUrl(channelId)}/xmlsitemap.php`;\n\n    const response = await fetch(sitemapIndexUrl, {\n      method: 'GET',\n      headers: {\n        Accept: 'application/xml',\n        'Content-Type': 'application/xml',\n        'User-Agent': this.backendUserAgent,\n        ...(this.trustedProxySecret && { 'X-BC-Trusted-Proxy-Secret': this.trustedProxySecret }),\n      },\n    });\n\n    if (!response.ok) {\n      throw new Error(`Unable to get Sitemap Index: ${response.statusText}`);\n    }\n\n    return response.text();\n  }\n\n  private async getCanonicalUrl(channelId?: string) {\n    const resolvedChannelId = channelId ?? (await this.getChannelId(this.defaultChannelId));\n\n    return `https://store-${this.config.storeHash}-${resolvedChannelId}.${graphqlApiDomain}`;\n  }\n\n  private async getGraphQLEndpoint(\n    channelId?: string,\n    operationName?: string,\n    operationType?: string,\n  ) {\n    const baseUrl = new URL(`${await this.getCanonicalUrl(channelId)}/graphql`);\n\n    if (operationName) {\n      baseUrl.searchParams.set('operation', operationName);\n    }\n\n    if (operationType) {\n      baseUrl.searchParams.set('type', operationType);\n    }\n\n    return baseUrl.toString();\n  }\n\n  private requestLogger(document: string) {\n    if (!this.config.logger) {\n      return () => {\n        // noop\n      };\n    }\n\n    const { name, type } = getOperationInfo(document);\n\n    const timeStart = performance.now();\n\n    return (response: Response) => {\n      const timeEnd = performance.now();\n      const duration = (timeEnd - timeStart).toFixed(2);\n\n      const complexity = response.headers.get('x-bc-graphql-complexity');\n\n      // eslint-disable-next-line no-console\n      console.log(\n        `[BigCommerce] ${type} ${name ?? 'anonymous'} - ${duration}ms - complexity ${complexity ?? 'unknown'}`,\n      );\n    };\n  }\n}\n\nexport function createClient<FetcherRequestInit extends RequestInit = RequestInit>(\n  config: Config<FetcherRequestInit>,\n) {\n  return new Client<FetcherRequestInit>(config);\n}\n"
  },
  {
    "path": "packages/client/src/gql-auth-error.ts",
    "content": "import { BigCommerceGQLError, GQLError, GQLErrorCode } from './gql-error';\n\nexport class BigCommerceAuthError extends BigCommerceGQLError {\n  readonly code: GQLErrorCode;\n\n  constructor(\n    errorCode: GQLErrorCode,\n    public errors: GQLError[] = [],\n  ) {\n    super(errors);\n\n    this.name = 'BigCommerceAuthError';\n    this.code = errorCode;\n  }\n}\n"
  },
  {
    "path": "packages/client/src/gql-error.ts",
    "content": "export enum GQLErrorCode {\n  INVALID_CAT = 'INVALID_CUSTOMER_ACCESS_TOKEN',\n  MISSING_CAT = 'MISSING_CUSTOMER_ACCESS_TOKEN',\n}\n\nexport interface GQLError {\n  message: string;\n  path: string[];\n  locations: Array<{ line: number; column: number }>;\n  extensions?: {\n    [key: string]: unknown;\n    code?: GQLErrorCode;\n  };\n}\n\nexport class BigCommerceGQLError extends Error {\n  constructor(public errors: GQLError[] = []) {\n    const message = errors.map((error) => JSON.stringify(error, null, 2)).join('\\n');\n\n    super(message);\n    this.name = 'BigCommerceGQLError';\n  }\n}\n"
  },
  {
    "path": "packages/client/src/index.ts",
    "content": "export { BigCommerceAPIError } from './api-error';\nexport { BigCommerceGQLError } from './gql-error';\nexport { BigCommerceAuthError } from './gql-auth-error';\nexport { MissingCustomerAccessTokenError } from './missing-cat-error';\nexport { InvalidCustomerAccessTokenError } from './invalid-cat-error';\nexport { createClient } from './client';\nexport { removeEdgesAndNodes } from './utils/removeEdgesAndNodes';\n"
  },
  {
    "path": "packages/client/src/invalid-cat-error.ts",
    "content": "import { BigCommerceAuthError } from './gql-auth-error';\nimport { GQLError, GQLErrorCode } from './gql-error';\n\nexport class InvalidCustomerAccessTokenError extends BigCommerceAuthError {\n  constructor(public errors: GQLError[] = []) {\n    super(GQLErrorCode.INVALID_CAT, errors);\n\n    this.name = 'InvalidCustomerAccessTokenError';\n  }\n}\n"
  },
  {
    "path": "packages/client/src/lib/error.ts",
    "content": "import { BigCommerceGQLError, GQLError, GQLErrorCode } from '../gql-error';\nimport { InvalidCustomerAccessTokenError } from '../invalid-cat-error';\nimport { MissingCustomerAccessTokenError } from '../missing-cat-error';\n\nexport function parseGraphQLError(result: unknown) {\n  try {\n    assertIsGQLErrorResponse(result);\n\n    const extendedError = result.find((error) => error.extensions && 'code' in error.extensions);\n\n    if (extendedError) {\n      switch (extendedError.extensions?.code) {\n        case GQLErrorCode.MISSING_CAT:\n          return new MissingCustomerAccessTokenError(result);\n\n        case GQLErrorCode.INVALID_CAT:\n          return new InvalidCustomerAccessTokenError(result);\n      }\n    }\n\n    return new BigCommerceGQLError(result);\n  } catch {\n    return new BigCommerceGQLError([{ message: 'Unknown error', path: [], locations: [] }]);\n  }\n}\n\nfunction assertIsGQLErrorResponse(value: unknown): asserts value is GQLError[] {\n  if (!Array.isArray(value)) {\n    throw new Error('Expected maybeError to be an array');\n  }\n\n  if (value.some((error) => typeof error !== 'object' || error === null)) {\n    throw new Error('Expected maybeError to be an array of objects');\n  }\n\n  if (value.some((error) => !('message' in error))) {\n    throw new Error('Expected maybeError to have a message property');\n  }\n}\n"
  },
  {
    "path": "packages/client/src/missing-cat-error.ts",
    "content": "import { BigCommerceAuthError } from './gql-auth-error';\nimport { GQLError, GQLErrorCode } from './gql-error';\n\nexport class MissingCustomerAccessTokenError extends BigCommerceAuthError {\n  constructor(public errors: GQLError[] = []) {\n    super(GQLErrorCode.MISSING_CAT, errors);\n\n    this.name = 'MissingCustomerAccessTokenError';\n  }\n}\n"
  },
  {
    "path": "packages/client/src/types.ts",
    "content": "// Taken from gql.tada repo\n/* eslint-disable @typescript-eslint/no-explicit-any */\nexport interface DocumentDecoration<Result = Record<string, any>, Variables = Record<string, any>> {\n  /** Type to support `@graphql-typed-document-node/core`\n   * @internal\n   */\n  __apiType?: (variables: Variables) => Result;\n  /** Type to support `TypedQueryDocumentNode` from `graphql`\n   * @internal\n   */\n  __ensureTypesOfVariablesAndResultMatching?: (variables: Variables) => Result;\n}\n"
  },
  {
    "path": "packages/client/src/utils/getOperationName.ts",
    "content": "import { DefinitionNode, OperationDefinitionNode, parse } from '@0no-co/graphql.web';\n\nfunction isOperationDefinitionNode(node: DefinitionNode): node is OperationDefinitionNode {\n  // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison\n  return node.kind === 'OperationDefinition';\n}\n\ninterface OperationInfo {\n  name?: string;\n  type: 'query' | 'mutation' | 'subscription';\n}\n\nexport const getOperationInfo = (document: string): OperationInfo => {\n  const documentNode = parse(document);\n\n  const operationInfo = documentNode.definitions.filter(isOperationDefinitionNode).map((def) => {\n    return {\n      name: def.name?.value,\n      type: def.operation,\n    };\n  })[0];\n\n  return operationInfo;\n};\n"
  },
  {
    "path": "packages/client/src/utils/normalizeQuery.ts",
    "content": "import { DocumentNode, print } from '@0no-co/graphql.web';\n\nimport { DocumentDecoration } from '../types';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function normalizeQuery(query: string | DocumentNode | DocumentDecoration<any, any>) {\n  if (typeof query === 'string') {\n    return query;\n  }\n\n  if (query instanceof String) {\n    return query.toString();\n  }\n\n  if ('kind' in query) {\n    return print(query);\n  }\n\n  throw new Error('Invalid query type');\n}\n"
  },
  {
    "path": "packages/client/src/utils/removeEdgesAndNodes.ts",
    "content": "export type Maybe<T> = T | null;\n\nexport interface Connection<T> {\n  edges?: Maybe<Array<Maybe<Edge<T>>>> | undefined;\n}\n\nexport interface Edge<T> {\n  node: T;\n}\n\nexport const removeEdgesAndNodes = <T>(array: Connection<T>) => {\n  if (!array.edges) {\n    return [];\n  }\n\n  return array.edges.filter((edge): edge is Edge<T> => edge !== null).map((edge) => edge.node);\n};\n"
  },
  {
    "path": "packages/client/src/utils/userAgent.ts",
    "content": "import { nodeVersion, process, provider, runtime } from 'std-env';\n\nimport packageInfo from '../../package.json';\n\nconst { name, version } = packageInfo;\n\n/*\n Attempt to detect hosting platform and environment information to add to user agent\n*/\nconst getPlatform = () => {\n  const keysOfInterest = [runtime, provider, nodeVersion, process.env.NODE_ENV].filter(Boolean);\n\n  return keysOfInterest.join('; ');\n};\n\nconst detectedPlatform = getPlatform();\n\n/*\n    Construct a User-Agent header value for use in API requests to BigCommerce.\n    Reference https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent\n    for more information on the conventions used here.\n*/\nexport const getBackendUserAgent = (platform?: string, extensions?: string): string => {\n  // Version of this client which is directly making the request\n  const userAgentParts = [`${name}/${version}`];\n\n  // Used for host information\n  const platformValue = platform ?? detectedPlatform;\n\n  userAgentParts.push(`(${platformValue})`);\n\n  // Used for any extensions such as framework or plugin versions,\n  // assumed to already be in a valid user-agent format\n  if (extensions) {\n    userAgentParts.push(extensions);\n  }\n\n  return userAgentParts.join(' ');\n};\n"
  },
  {
    "path": "packages/client/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Default\",\n  \"compilerOptions\": {\n    \"composite\": false,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"inlineSources\": false,\n    \"incremental\": true,\n    \"isolatedModules\": true,\n    \"moduleResolution\": \"node\",\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"preserveWatchOutput\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\",\n    \"resolveJsonModule\": true,\n    \"lib\": [\"esnext\"]\n  },\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/client/tsup.config.ts",
    "content": "import { defineConfig, Options } from 'tsup';\n\nexport default defineConfig((options: Options) => ({\n  entry: ['src/index.ts'],\n  format: ['cjs'],\n  dts: true,\n  clean: !options.watch,\n  ...options,\n}));\n"
  },
  {
    "path": "packages/create-catalyst/.eslintrc.cjs",
    "content": "// @ts-check\n\n/** @type {import('eslint').Linter.LegacyConfig} */\nconst config = {\n  root: true,\n  extends: ['@bigcommerce/catalyst/base', '@bigcommerce/catalyst/prettier'],\n  rules: {\n    'no-console': 'off',\n    'import/no-named-as-default': 'off',\n    '@typescript-eslint/naming-convention': 'off',\n  },\n  ignorePatterns: ['/dist/**'],\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "packages/create-catalyst/CHANGELOG.md",
    "content": "# Changelog\n\n## 1.0.3\n\n### Patch Changes\n\n- [#2993](https://github.com/bigcommerce/catalyst/pull/2993) [`ed76224`](https://github.com/bigcommerce/catalyst/commit/ed7622453edc667a3582646074e0ccb72eb7b714) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Update the client id to the new one.\n\n## 1.0.2\n\n### Patch Changes\n\n- [#2940](https://github.com/bigcommerce/catalyst/pull/2940) [`a4b614d`](https://github.com/bigcommerce/catalyst/commit/a4b614d99a208f21b4d4ee1462666581f21335d8) Thanks [@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)! - Align Node.js engine requirement with v24. The `engines.node` field in `create-catalyst` now matches the runtime version gate (`^24.0.0`), ensuring `pnpm create @bigcommerce/catalyst` correctly rejects unsupported Node versions before installation begins.\n\n## 1.0.1\n\n### Patch Changes\n\n- [#2591](https://github.com/bigcommerce/catalyst/pull/2591) [`f791fef`](https://github.com/bigcommerce/catalyst/commit/f791fef1283e1d5a0fabf81fa64140317e99c84e) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Removes `chalk` dependency in favor of `consola` \"colorize\" utility function (which only depends on `node:tty`)\n\n## 1.0.0\n\n### Major Changes\n\n- [#2435](https://github.com/bigcommerce/catalyst/pull/2435) [`cd4bd60`](https://github.com/bigcommerce/catalyst/commit/cd4bd604739b0cea4b622b08ebbde4cea953fcae) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Release 1.0.0 (see [`core/CHANGELOG.md`](../../core/CHANGELOG.md#100) for more details)\n\n## 0.22.0\n\n### Minor Changes\n\n- [#2296](https://github.com/bigcommerce/catalyst/pull/2296) [`da1f486`](https://github.com/bigcommerce/catalyst/commit/da1f486685544ebe117881caa97d7b0539171531) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Expands the supported Node.js version of the `create-catalyst` CLI to `^20` and `^22`. The version of Node.js required to run the CLI is not necessarily tied to the version of Node.js required to run Catalyst; the CLI requires at least version 18 to run because it depends on global Fetch API support being enabled by default. More context in [#2296](https://github.com/bigcommerce/catalyst/pull/2296).\n\n- [#2136](https://github.com/bigcommerce/catalyst/pull/2136) [`e5f1ac9`](https://github.com/bigcommerce/catalyst/commit/e5f1ac9879319a66187d410a3dc6b2764b15a789) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Support node 22.\n\n### Patch Changes\n\n- [#2017](https://github.com/bigcommerce/catalyst/pull/2017) [`c1519d4`](https://github.com/bigcommerce/catalyst/commit/c1519d4098baa7d21553415e4c794c185b27ec07) Thanks [@RomanKrasinskyi](https://github.com/RomanKrasinskyi)! - Add es-419 to list of allowed locale for selecting in CLI\n\n## 0.21.0\n\n### Minor Changes\n\n- [#1986](https://github.com/bigcommerce/catalyst/pull/1986) [`b3d55a2`](https://github.com/bigcommerce/catalyst/commit/b3d55a2f6df2de2f0ba448eebadd4b6e10e92708) Thanks [@RomanKrasinskyi](https://github.com/RomanKrasinskyi)! - Allow configuration of locales when creating a new channel\n\n### Patch Changes\n\n- [#1957](https://github.com/bigcommerce/catalyst/pull/1957) [`9e1d942`](https://github.com/bigcommerce/catalyst/commit/9e1d942f9376d4e55e92b295dc9febd2310cb22a) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Removes the ability for command injection in the CLI\n\n## 0.20.0\n\n### Minor Changes\n\n- [#1859](https://github.com/bigcommerce/catalyst/pull/1859) [`62d178e`](https://github.com/bigcommerce/catalyst/commit/62d178e61070b4c9be431fe4a02c9344e8b2500c) Thanks [@bookernath](https://github.com/bookernath)! - Set default gh-ref to @bigcommerce/catalyst-core@latest\n\n### Patch Changes\n\n- [#1859](https://github.com/bigcommerce/catalyst/pull/1859) [`62d178e`](https://github.com/bigcommerce/catalyst/commit/62d178e61070b4c9be431fe4a02c9344e8b2500c) Thanks [@bookernath](https://github.com/bookernath)! - Add notice that preview storefront has been deployed\n\n- [#1859](https://github.com/bigcommerce/catalyst/pull/1859) [`62d178e`](https://github.com/bigcommerce/catalyst/commit/62d178e61070b4c9be431fe4a02c9344e8b2500c) Thanks [@bookernath](https://github.com/bookernath)! - Fix --reset-main and improve OTP copying experience\n\n## 0.19.0\n\n### Minor Changes\n\n- [#1820](https://github.com/bigcommerce/catalyst/pull/1820) [`401417d`](https://github.com/bigcommerce/catalyst/commit/401417dfebcf9f9fd3e24a6808205b2563380f1a) Thanks [@bookernath](https://github.com/bookernath)! - Update CLI to use new APIs for consistency with One-Click Catalyst in the control panel\n\n## 0.18.0\n\n### Minor Changes\n\n- [#1732](https://github.com/bigcommerce/catalyst/pull/1732) [`df4912c`](https://github.com/bigcommerce/catalyst/commit/df4912cd4a24a6f8d36359d80834c378df6c6297) Thanks [@jamesqquick](https://github.com/jamesqquick)! - Create command now accepts a --reset-main flag which resets the main branch to the provided git ref if present.\n\n## 0.17.1\n\n### Patch Changes\n\n- [#1700](https://github.com/bigcommerce/catalyst/pull/1700) [`12923a5`](https://github.com/bigcommerce/catalyst/commit/12923a58692c19ae317eac55784f47690de36a1f) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Adds a platform check to check if a command is installed.\n\n## 0.17.0\n\n### Minor Changes\n\n- [#1541](https://github.com/bigcommerce/catalyst/pull/1541) [`20c08da`](https://github.com/bigcommerce/catalyst/commit/20c08dae691b43149099892ffd1fa717310b602f) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Introduce an additional project creation scenario in which projects can be created without prompts if the CLI is provided with a valid `--store-hash`, `--channel-id`, and `--storefront-token`\n\n## 0.16.0\n\n### Minor Changes\n\n- [#1262](https://github.com/bigcommerce/catalyst/pull/1262) [`0c2023b`](https://github.com/bigcommerce/catalyst/commit/0c2023bae650039cd79ba51b1161b5c8c16f0b8d) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Generates a storefront token when using the CLI to init or create a Catalyst storefront.\n\n- [#1262](https://github.com/bigcommerce/catalyst/pull/1262) [`0c2023b`](https://github.com/bigcommerce/catalyst/commit/0c2023bae650039cd79ba51b1161b5c8c16f0b8d) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Remove generating a customer impersonation token as we are using the API Token + Customer Access Token approach\"\n\n## 0.15.0\n\n### Minor Changes\n\n- [#1518](https://github.com/bigcommerce/catalyst/pull/1518) [`739bd25`](https://github.com/bigcommerce/catalyst/commit/739bd254593a76cfe351ce216a225d957e390823) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Add the ability to pass in extra, arbitrary environment variables to add to the `.env.local` file created by the Catalyst CLI installer in the format of `pnpm create @bigcommerce/catalyst@latest --env VAR_ONE=VALUE_ONE --env VAR_TWO=VALUE_TWO`\n\n- [#1519](https://github.com/bigcommerce/catalyst/pull/1519) [`630e2ed`](https://github.com/bigcommerce/catalyst/commit/630e2ed4dcca3b245b98efc2de66be81585f1bb5) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Ensures a command is always passed as an event name and provides a allowlist of args to pass as additional properties to the event.\n\n### Patch Changes\n\n- [#1501](https://github.com/bigcommerce/catalyst/pull/1501) [`513a740`](https://github.com/bigcommerce/catalyst/commit/513a7407feab03f5b7c16eb82d6d2826d238cb27) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Adds a identify call on init.\n\n## 0.14.3\n\n### Patch Changes\n\n- [#1492](https://github.com/bigcommerce/catalyst/pull/1492) [`0b28a4c`](https://github.com/bigcommerce/catalyst/commit/0b28a4c7d8f71f677e81788655d2bc70d41c4882) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Default to the `create` command when running the preCommand hook.\n\n## 0.14.2\n\n### Patch Changes\n\n- [#1488](https://github.com/bigcommerce/catalyst/pull/1488) [`1bbc3f8`](https://github.com/bigcommerce/catalyst/commit/1bbc3f85fd56572b3a6cfe24e5be551d0e8f8488) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Noop commit to rebuild CLI.\n\n## 0.14.1\n\n### Patch Changes\n\n- [#1480](https://github.com/bigcommerce/catalyst/pull/1480) [`eb1707b`](https://github.com/bigcommerce/catalyst/commit/eb1707b7845f9f6ca68afa32c1469459c58b9505) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Fix the CLI from hanging while waiting for segment.\n\n## 0.14.0\n\n### Minor Changes\n\n- [#1478](https://github.com/bigcommerce/catalyst/pull/1478) [`7d66252`](https://github.com/bigcommerce/catalyst/commit/7d6625263bf87aa19b6c05c190729d8b147ca7a8) Thanks [@bookernath](https://github.com/bookernath)! - Update OAuth scopes to future needs\n\n- [#1479](https://github.com/bigcommerce/catalyst/pull/1479) [`a7ce4b3`](https://github.com/bigcommerce/catalyst/commit/a7ce4b341ad8b69a001e03ff5050e3c70c7dca1b) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Adds telemetry collection to the CLI. If users want to opt out of CLI telemetry collection, use `pnpm create @bigcommerce/catalyst telemetry disable` or use the `CATALYST_TELEMETRY_DISABLED` environment variable to opt-out.\n\n## 0.13.0\n\n### Minor Changes\n\n- [#1443](https://github.com/bigcommerce/catalyst/pull/1443) [`c166d53`](https://github.com/bigcommerce/catalyst/commit/c166d536394ec8b32831aa384868d0cabc5d86e2) Thanks [@matthewvolk](https://github.com/matthewvolk)! - BREAKING: Remove automatic generation of GraphQL type definitions on project creation. This results in faster project creation and generation will happen already as part of starting the development sever or kicking off a build\n\n- [#1438](https://github.com/bigcommerce/catalyst/pull/1438) [`d12c0d2`](https://github.com/bigcommerce/catalyst/commit/d12c0d22ec121f0effb95e1fab347a05ca84c7af) Thanks [@matthewvolk](https://github.com/matthewvolk)! - BREAKING: Refactor `cloneCatalyst` so that it simply clones the `bigcommerce/catalyst` repo, configures remotes, and checks out an optional ref\n\n- [#1440](https://github.com/bigcommerce/catalyst/pull/1440) [`5b3cbbd`](https://github.com/bigcommerce/catalyst/commit/5b3cbbd75ec05b6a21062b600a930f15e1c004a4) Thanks [@matthewvolk](https://github.com/matthewvolk)! - BREAKING: Refactor `installDependencies` so that it installs all dependencies found in the root package.json file of the monorepo\n\n- [#1435](https://github.com/bigcommerce/catalyst/pull/1435) [`b38209f`](https://github.com/bigcommerce/catalyst/commit/b38209f93345ebc6584fe3486e10ca5baadf17ec) Thanks [@matthewvolk](https://github.com/matthewvolk)! - BREAKING: Ensure `pnpm` is installed on the machine running the CLI\n\n- [#1434](https://github.com/bigcommerce/catalyst/pull/1434) [`c105d07`](https://github.com/bigcommerce/catalyst/commit/c105d07695f1d1070ce6774e4b33037633e97e28) Thanks [@matthewvolk](https://github.com/matthewvolk)! - BREAKING: Change required Node.js version to `^20` (instead of `>=20`)\n\n- [#1441](https://github.com/bigcommerce/catalyst/pull/1441) [`5463157`](https://github.com/bigcommerce/catalyst/commit/5463157ed6060880dd22e60e1c7caba38dd3cbb5) Thanks [@matthewvolk](https://github.com/matthewvolk)! - BREAKING: Remove `pm` utility because the package manager must be `pnpm` when working in the monorepo\n\n- [#1436](https://github.com/bigcommerce/catalyst/pull/1436) [`673bea2`](https://github.com/bigcommerce/catalyst/commit/673bea2bef3d7b80267c7f0c8b204b652fd09f34) Thanks [@matthewvolk](https://github.com/matthewvolk)! - BREAKING: Ensure `git` is installed on the machine running the CLI\n\n- [#1437](https://github.com/bigcommerce/catalyst/pull/1437) [`6db8527`](https://github.com/bigcommerce/catalyst/commit/6db8527042d3c0b04b6b0a61c56f3cc2ef8eeff7) Thanks [@matthewvolk](https://github.com/matthewvolk)! - BREAKING: Remove `applyIntegrations`. Integrations will now be applied by simply fetching the appropriate remote `integrations/*` branch from upstream, and cherry-picking the integration code\n\n### Patch Changes\n\n- [#1439](https://github.com/bigcommerce/catalyst/pull/1439) [`addf5e9`](https://github.com/bigcommerce/catalyst/commit/addf5e98a08427631e03ef152efe6949a5d01b9e) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Removes unused `getLatestCoreTag` function\n\n- [#1433](https://github.com/bigcommerce/catalyst/pull/1433) [`ea74be2`](https://github.com/bigcommerce/catalyst/commit/ea74be2b0c066b8f9e99c1e1b64ef1b97ea4b7f5) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Add function to allow user to specify an arbitrary ref to checkout after cloning\n\n- [#1431](https://github.com/bigcommerce/catalyst/pull/1431) [`3a3370e`](https://github.com/bigcommerce/catalyst/commit/3a3370e2323a82dd753cf22042b9cd9130c3a7a0) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Adds a typeguard to narrow Errors thrown by `execSync` to be of the type `ExecException`\n\n- [#1432](https://github.com/bigcommerce/catalyst/pull/1432) [`5a2a86e`](https://github.com/bigcommerce/catalyst/commit/5a2a86ecbe8ab831c54b60d2f723274cabc00d98) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Adds a function to check if a user is set up with SSH authentication for GitHub\n\n## 0.12.0\n\n### Minor Changes\n\n- [#1365](https://github.com/bigcommerce/catalyst/pull/1365) [`896b4d3`](https://github.com/bigcommerce/catalyst/commit/896b4d359cea39536f41c3fa3427fb6bf429d196) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Changes the default behavior of the `create-catalyst` CLI such that it no longer writes the access token created by the OAuth device flow to the created project's `.env.local` file\n\n- [#1366](https://github.com/bigcommerce/catalyst/pull/1366) [`6d7c508`](https://github.com/bigcommerce/catalyst/commit/6d7c508b453d9e2cbe073b9ab7a7844220c2d22c) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Renames leftover `NEXT_PUBLIC_DEFAULT_REVALIDATE_TARGET` environment variables - continuation of bigcommerce/catalyst#1317\n\n## 0.11.0\n\n### Minor Changes\n\n- [#1267](https://github.com/bigcommerce/catalyst/pull/1267) [`d442efc`](https://github.com/bigcommerce/catalyst/commit/d442efcbdbf73f3d6f2e57ddc18049fffc727deb) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Added initial Makeswift integration source folder, which allows developers to create new Catalyst storefronts that are integrated with Makeswift out of the box by running:\n\n  ```sh\n  pnpm create @bigcommerce/catalyst@latest --integration=makeswift\n  ```\n\n- [#1266](https://github.com/bigcommerce/catalyst/pull/1266) [`6fa0d48`](https://github.com/bigcommerce/catalyst/commit/6fa0d4874e5f7c05cc43019efe8ca4838b504ca1) Thanks [@matthewvolk](https://github.com/matthewvolk)! - remove already completed todo comment\n\n## 0.10.0\n\n### Minor Changes\n\n- [#1192](https://github.com/bigcommerce/catalyst/pull/1192) [`d7d5a96`](https://github.com/bigcommerce/catalyst/commit/d7d5a961498053182a2d075ceb01f45c06f9cbec) Thanks [@matthewvolk](https://github.com/matthewvolk)! - add `integration` command to help with developing native Catalyst integrations\n\n- [#1193](https://github.com/bigcommerce/catalyst/pull/1193) [`aa72351`](https://github.com/bigcommerce/catalyst/commit/aa72351dc37094518e29849fc590dd10044fa955) Thanks [@matthewvolk](https://github.com/matthewvolk)! - add `--integration` option to `create-catalyst` to apply an integration to your newly created storefront\n\n- [#1242](https://github.com/bigcommerce/catalyst/pull/1242) [`733535a`](https://github.com/bigcommerce/catalyst/commit/733535ae4c12b93972b26471b1022cdfd925ed96) Thanks [@matthewvolk](https://github.com/matthewvolk)! - fix: define a single source of truth for integrations manifest file\n\n### Patch Changes\n\n- [#1240](https://github.com/bigcommerce/catalyst/pull/1240) [`dc2cc0d`](https://github.com/bigcommerce/catalyst/commit/dc2cc0d92ce81ae84d61881b2e933e3693f73151) Thanks [@matthewvolk](https://github.com/matthewvolk)! - refactor: parse env using `dotenv` package\n\n## 0.9.0\n\n### Minor Changes\n\n- [#1083](https://github.com/bigcommerce/catalyst/pull/1083) [`bd6be02`](https://github.com/bigcommerce/catalyst/commit/bd6be02d3d3547be6c014c581e1937278ed20d0a) Thanks [@bookernath](https://github.com/bookernath)! - Generate multi-channel GraphQL Storefront API tokens on catalyst provisioning\n\n## 0.8.0\n\n### Minor Changes\n\n- [#936](https://github.com/bigcommerce/catalyst/pull/936) [`8416cd0`](https://github.com/bigcommerce/catalyst/commit/8416cd068c541c9e298ca31422ba5954a59dc868) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Adds more helpful error messaging when using older versions of Node (<12) with the CLI\n\n### Patch Changes\n\n- [#935](https://github.com/bigcommerce/catalyst/pull/935) [`9e9c0e9`](https://github.com/bigcommerce/catalyst/commit/9e9c0e93d707edd160d5cfd7156ab26e45a4847a) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Move CLI command configuration closer to command action handlers\n\n## 0.7.0\n\n### Minor Changes\n\n- [#921](https://github.com/bigcommerce/catalyst/pull/921) [`e093e7c`](https://github.com/bigcommerce/catalyst/commit/e093e7c6db06920718fa76591a7a776f3c575ae4) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Removes unnecessary lint task from create command\n\n## 0.6.0\n\n### Minor Changes\n\n- [#782](https://github.com/bigcommerce/catalyst/pull/782) [`32f9d7c`](https://github.com/bigcommerce/catalyst/commit/32f9d7cad0529591d771106efe3e9cace48e50db) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Adds the `.vscode/settings.json` file pointing to the correct typescript sdk for gql-tada support.\n\n- [#806](https://github.com/bigcommerce/catalyst/pull/806) [`5655f81`](https://github.com/bigcommerce/catalyst/commit/5655f81f93041e6b1253d0c67ce50f70f99828bf) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Adds an option to include the functional test suite as part of the create command. Defaults to false.\n\n- [#876](https://github.com/bigcommerce/catalyst/pull/876) [`56735be`](https://github.com/bigcommerce/catalyst/commit/56735be7bef1f528642e333b20400268613dace6) Thanks [@matthewvolk](https://github.com/matthewvolk)! - The `create-catalyst` CLI will now create channel menus for new Catalyst channels\n\n### Patch Changes\n\n- [#839](https://github.com/bigcommerce/catalyst/pull/839) [`0e5e513`](https://github.com/bigcommerce/catalyst/commit/0e5e5139ced4410e0930e36b7eafa5841e2301c5) Thanks [@jorgemoya](https://github.com/jorgemoya)! - Remove cloning logic for components.\n\n## 0.5.0\n\n### Minor Changes\n\n- [#753](https://github.com/bigcommerce/catalyst/pull/753) [`48c040e`](https://github.com/bigcommerce/catalyst/commit/48c040e94745134f4c60b15cadcdb0a0bbcb2a36) Thanks [@chanceaclark](https://github.com/chanceaclark)! - Deprecate `node@18` in favor of latest LTS version `node@20`.\n\n## 0.4.1\n\n### Patch Changes\n\n- [#740](https://github.com/bigcommerce/catalyst/pull/740) [`d586c21`](https://github.com/bigcommerce/catalyst/commit/d586c2122bf6513b2f7d923957636c7ea8aaf2ce) Thanks [@dependabot](https://github.com/apps/dependabot)! - chore: bump next-auth and use string for user id\n\n## 0.4.0\n\n### Minor Changes\n\n- [#712](https://github.com/bigcommerce/catalyst/pull/712) [`8ad9d15`](https://github.com/bigcommerce/catalyst/commit/8ad9d15ffd6cb1cc0a53b2df1eff76efe21527a4) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Change the default GitHub Ref that the CLI uses to clone `bigcommerce/catalyst-core` from `main` to latest published release (e.g., `@bigcommerce/catalyst-core@0.3.0`)\n\n### Patch Changes\n\n- [#735](https://github.com/bigcommerce/catalyst/pull/735) [`3db9c5f`](https://github.com/bigcommerce/catalyst/commit/3db9c5fa603299a5c5a9a12bd5408f9024677b20) Thanks [@deini](https://github.com/deini)! - Bump dependencies\n\n## 0.3.0\n\n### Minor Changes\n\n- [#696](https://github.com/bigcommerce/catalyst/pull/696) [`6deba4a`](https://github.com/bigcommerce/catalyst/commit/6deba4a0713b0d14a76439f0cd01baf35f5185e2) Thanks [@deini](https://github.com/deini)! - removes graphql codegen setup, all graphql calls are done using gql.tada\n\n### Patch Changes\n\n- [#699](https://github.com/bigcommerce/catalyst/pull/699) [`30f6515`](https://github.com/bigcommerce/catalyst/commit/30f65153a94abf689b053fbc9acff3cd297398c0) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Support `@` characters in the `create-catalyst` `--gh-ref` CLI flag\n\n## 0.2.2\n\n### Patch Changes\n\n- [#686](https://github.com/bigcommerce/catalyst/pull/686) [`278ad5f`](https://github.com/bigcommerce/catalyst/commit/278ad5f9d389b8cb20dd32007850e937b8d494bd) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Install latest versions of all Catalyst workspace dependencies during project creation\n\n## 0.2.1\n\n### Patch Changes\n\n- [#684](https://github.com/bigcommerce/catalyst/pull/684) [`1e12797`](https://github.com/bigcommerce/catalyst/commit/1e127977eedca306c58f3e243d7367f52ea7f077) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Adjust order of `package.json` properties created by the CLI\n\n## 0.2.0\n\n### Minor Changes\n\n- [#658](https://github.com/bigcommerce/catalyst/pull/658) [`8ff2eb6`](https://github.com/bigcommerce/catalyst/commit/8ff2eb65acaf973cf7d30833c14238338c57ec44) Thanks [@matthewvolk](https://github.com/matthewvolk)! - create graphql schema using gql.tada\n\n### Patch Changes\n\n- [#670](https://github.com/bigcommerce/catalyst/pull/670) [`efd6387`](https://github.com/bigcommerce/catalyst/commit/efd63874361726077798bf29a1f531c58bfdd0aa) Thanks [@matthewvolk](https://github.com/matthewvolk)! - use `--prefix` npm flag to set `cwd` for GraphQL schema generation insead of `exec`'s `cwd` option\n\n## 0.1.1\n\n### Patch Changes\n\n- [#648](https://github.com/bigcommerce/catalyst/pull/648) [`604f60a`](https://github.com/bigcommerce/catalyst/commit/604f60a7550a10de9e8127b4c195998b70fb98df) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Bump `zod-validation-error` in `@bigcommerce/create-catalyst` to `^3.0.3`. This fixes a recently discovered bug that was caused by `zod-validation-error@3.0.2`.\n\n- [#653](https://github.com/bigcommerce/catalyst/pull/653) [`ffaa8ee`](https://github.com/bigcommerce/catalyst/commit/ffaa8eef6da0b96bef58ab0a9b00e34c08cb3535) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Update `@bigcommerce/create-catalyst` readme with more detailed usage\n  All notable changes to this project will be documented in this file.\n"
  },
  {
    "path": "packages/create-catalyst/README.md",
    "content": "# @bigcommerce/create-catalyst\n\nCreate a new Catalyst project, and optionally connect the project to a BigCommerce store. Add `--help` to the end of any command to learn about available subcommands and options.\n\n## Usage\n\n> [!WARNING]\n> With yarn berry, you might run into a dependency issue with `stripAnsi`. You can circumvent this issue by setting the [nodeLinker](https://yarnpkg.com/configuration/yarnrc#nodeLinker) to either `pnpm` or `node-modules` while the dependency issue is resolved.\n\n### Create a new Catalyst project\n\n```sh\nnpm create @bigcommerce/catalyst@latest\n```\n\n```sh\npnpm create @bigcommerce/catalyst@latest\n```\n\n```sh\nyarn create @bigcommerce/catalyst@latest\n```\n\n### Connect an existing Catalyst project to a BigCommerce store\n\n```sh\nnpm create @bigcommerce/catalyst@latest init\n```\n\n```sh\npnpm create @bigcommerce/catalyst@latest init\n```\n\n```sh\nyarn create @bigcommerce/catalyst@latest init\n```\n"
  },
  {
    "path": "packages/create-catalyst/bin/index.cjs",
    "content": "#!/usr/bin/env node\n\nconst semver = require('semver');\nconst { CATALYST_REQUIRED_NODE_VERSIONS } = require('./supported-node-versions.cjs');\n\nconst catalystRequiredNodeVersions = CATALYST_REQUIRED_NODE_VERSIONS;\nconst userNodeVersion = process.version;\n\nif (!catalystRequiredNodeVersions.some((version) => semver.satisfies(userNodeVersion, version))) {\n  const prettyRequiredNodeVersions = catalystRequiredNodeVersions\n    .map((version) => semver.coerce(version).major)\n    .join(', ');\n\n  console.error(`\\n\\x1b[31mYou are using Node.js ${userNodeVersion}.`);\n  console.error(\n    `You must use one of the following Node.js versions: ${prettyRequiredNodeVersions}\\x1b[0m\\n`,\n  );\n  process.exit(1);\n}\n\n// eslint-disable-next-line import/dynamic-import-chunkname, import/extensions\nimport('../dist/index.js').catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/create-catalyst/bin/supported-node-versions.cjs",
    "content": "/**\n * Supported Node.js version ranges for Catalyst.\n * Odd-numbered versions are discouraged per the Node.js release policy.\n * @see https://nodejs.org/en/about/previous-releases#nodejs-releases\n */\nconst CATALYST_REQUIRED_NODE_VERSIONS = ['^24'];\n\nmodule.exports = { CATALYST_REQUIRED_NODE_VERSIONS };\n"
  },
  {
    "path": "packages/create-catalyst/jest.config.cjs",
    "content": "module.exports = {\n  transformIgnorePatterns: [],\n  transform: {\n    '^.+\\\\.(t|j)s?$': '@swc/jest',\n  },\n};\n"
  },
  {
    "path": "packages/create-catalyst/package.json",
    "content": "{\n  \"name\": \"@bigcommerce/create-catalyst\",\n  \"version\": \"1.0.3\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bigcommerce/catalyst\",\n    \"directory\": \"packages/create-catalyst\"\n  },\n  \"type\": \"module\",\n  \"bin\": \"bin/index.cjs\",\n  \"files\": [\n    \"bin\",\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"dev\": \"tsup --watch\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"lint\": \"eslint . --max-warnings 0\",\n    \"test\": \"jest\",\n    \"build\": \"tsup\"\n  },\n  \"engines\": {\n    \"node\": \"^24.0.0\"\n  },\n  \"dependencies\": {\n    \"@commander-js/extra-typings\": \"^14.0.0\",\n    \"@iarna/toml\": \"^2.2.5\",\n    \"@inquirer/core\": \"^10.1.13\",\n    \"@inquirer/figures\": \"^1.0.12\",\n    \"@inquirer/prompts\": \"^7.5.3\",\n    \"@inquirer/type\": \"^3.0.7\",\n    \"@segment/analytics-node\": \"^2.2.1\",\n    \"ansi-escapes\": \"^7.0.0\",\n    \"commander\": \"^14.0.0\",\n    \"conf\": \"^13.1.0\",\n    \"consola\": \"^3.4.2\",\n    \"cross-spawn\": \"^7.0.6\",\n    \"dotenv\": \"^16.5.0\",\n    \"fs-extra\": \"^11.3.0\",\n    \"giget\": \"^1.2.5\",\n    \"lodash.kebabcase\": \"^4.1.1\",\n    \"nypm\": \"^0.5.4\",\n    \"open\": \"^10.1.2\",\n    \"ora\": \"^8.2.0\",\n    \"semver\": \"^7.7.2\",\n    \"std-env\": \"^3.9.0\",\n    \"zod\": \"^3.25.51\",\n    \"zod-validation-error\": \"^3.4.1\"\n  },\n  \"devDependencies\": {\n    \"@bigcommerce/eslint-config\": \"^2.11.0\",\n    \"@bigcommerce/eslint-config-catalyst\": \"workspace:^\",\n    \"@swc/core\": \"^1.11.31\",\n    \"@swc/jest\": \"^0.2.38\",\n    \"@types/cross-spawn\": \"^6.0.6\",\n    \"@types/fs-extra\": \"^11.0.4\",\n    \"@types/jest\": \"^29.5.14\",\n    \"@types/lodash.kebabcase\": \"^4.1.9\",\n    \"@types/node\": \"^22.15.30\",\n    \"@types/prompts\": \"^2.4.9\",\n    \"@types/semver\": \"^7.7.0\",\n    \"eslint\": \"^8.57.1\",\n    \"jest\": \"^29.7.0\",\n    \"msw\": \"^2.9.0\",\n    \"prettier\": \"^3.6.2\",\n    \"tsup\": \"^8.5.0\",\n    \"typescript\": \"^5.8.3\"\n  }\n}\n"
  },
  {
    "path": "packages/create-catalyst/prettier.config.cjs",
    "content": "// @ts-check\n\n/** @type {import(\"prettier\").Config} */\nconst config = {\n  printWidth: 100,\n  singleQuote: true,\n  trailingComma: 'all',\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "packages/create-catalyst/src/commands/create.ts",
    "content": "import { Command, Option } from '@commander-js/extra-typings';\nimport { input, select } from '@inquirer/prompts';\nimport { execSync } from 'child_process';\nimport { colorize } from 'consola/utils';\nimport { pathExistsSync } from 'fs-extra/esm';\nimport kebabCase from 'lodash.kebabcase';\nimport { join } from 'path';\n\nimport { multiSelect } from '../prompts/multi-select';\nimport { CliApi } from '../utils/cli-api';\nimport { cloneCatalyst } from '../utils/clone-catalyst';\nimport { Https } from '../utils/https';\nimport { installDependencies } from '../utils/install-dependencies';\nimport { getAvailableLocales } from '../utils/localization';\nimport { login, storeCredentials } from '../utils/login';\nimport { Telemetry } from '../utils/telemetry/telemetry';\nimport { writeEnv } from '../utils/write-env';\n\ninterface Channel {\n  id: number;\n  name: string;\n  platform: string;\n}\n\ninterface ChannelsResponse {\n  data: Channel[];\n}\n\ninterface InitResponse {\n  data: {\n    makeswift_dev_api_key: string;\n    storefront_api_token: string;\n    envVars: Record<string, string>;\n  };\n}\n\ninterface CreateChannelResponse {\n  data: {\n    id: number;\n    storefront_api_token: string;\n    envVars: Record<string, string>;\n  };\n}\n\ninterface EligibilityResponse {\n  data: {\n    eligible: boolean;\n    message: string;\n  };\n}\n\nfunction getPlatformCheckCommand(command: string): string {\n  const isWindows = process.platform === 'win32';\n\n  return isWindows ? `where.exe ${command}` : `which ${command}`;\n}\n\nconst telemetry = new Telemetry();\n\nasync function handleChannelCreation(bc: Https, cliApi: CliApi) {\n  const newChannelName = await input({\n    message: 'What would you like to name your new channel?',\n  });\n\n  let availableLocales = [];\n\n  try {\n    availableLocales = await getAvailableLocales(bc);\n  } catch (error) {\n    if (error instanceof Error) {\n      console.error(colorize('red', error.message));\n    }\n\n    process.exit(1);\n  }\n\n  const storefrontLocale = await select({\n    message: 'Which default language would you like to set for your channel?',\n    default: 'en',\n    choices: availableLocales,\n    theme: {\n      style: {\n        help: () => colorize('dim', '(Select locale from the list or start typing the name)'),\n      },\n    },\n  });\n\n  const shouldAddAdditionalLocales = await select({\n    message: 'Would you like to add additional languages?',\n    choices: [\n      { name: 'Yes', value: true },\n      { name: 'No', value: false },\n    ],\n  });\n\n  let additionalLocales: string[] = [];\n\n  if (shouldAddAdditionalLocales) {\n    additionalLocales = await multiSelect({\n      choices: availableLocales.filter(({ value }) => value !== storefrontLocale),\n      message: 'Which additional languages would you like to add to your channel?',\n      theme: {\n        style: {\n          help: () => colorize('dim', '(Select locale from the list or start typing the name)'),\n        },\n      },\n      validate: (selections) => {\n        if (selections.length > 4) {\n          return 'You can only select up to 4 additional languages';\n        }\n\n        return true;\n      },\n    });\n  }\n\n  const shouldInstallSampleData = await select({\n    message: 'Would you like to install sample data?',\n    choices: [\n      { name: 'Yes', value: true },\n      { name: 'No', value: false },\n    ],\n  });\n\n  const response = await cliApi.createChannel(\n    newChannelName,\n    storefrontLocale,\n    additionalLocales,\n    shouldInstallSampleData,\n  );\n\n  if (!response.ok) {\n    console.error(\n      colorize(\n        'red',\n        `\\nPOST /channels/catalyst failed: ${response.status} ${response.statusText}\\n`,\n      ),\n    );\n    process.exit(1);\n  }\n\n  const channelData: unknown = await response.json();\n\n  if (!isCreateChannelResponse(channelData)) {\n    console.error(colorize('red', '\\nUnexpected response format from create channel endpoint\\n'));\n    process.exit(1);\n  }\n\n  return {\n    channelId: channelData.data.id,\n    storefrontToken: channelData.data.storefront_api_token,\n    envVars: channelData.data.envVars,\n  };\n}\n\nasync function handleChannelSelection(bc: Https) {\n  const channelSortOrder = ['catalyst', 'next', 'bigcommerce'];\n  const channelsResponse = await bc.fetch('/v3/channels?available=true&type=storefront');\n\n  if (!channelsResponse.ok) {\n    console.error(\n      colorize(\n        'red',\n        `\\nGET /v3/channels failed: ${channelsResponse.status} ${channelsResponse.statusText}\\n`,\n      ),\n    );\n    process.exit(1);\n  }\n\n  const availableChannels: unknown = await channelsResponse.json();\n\n  if (!isChannelsResponse(availableChannels)) {\n    console.error(colorize('red', '\\nUnexpected response format from channels endpoint\\n'));\n    process.exit(1);\n  }\n\n  const existingChannel = await select({\n    message: 'Which channel would you like to use?',\n    choices: availableChannels.data\n      .sort((a: Channel, b: Channel) => {\n        const aIndex = channelSortOrder.indexOf(a.platform);\n        const bIndex = channelSortOrder.indexOf(b.platform);\n\n        // If both platforms are not in the sort order, maintain their original order\n        if (aIndex === -1 && bIndex === -1) {\n          return 0;\n        }\n\n        // If one platform is not in the sort order, it should go to the end\n        if (aIndex === -1) return 1;\n        if (bIndex === -1) return -1;\n\n        // If both platforms are in the sort order, use their relative positions\n        return aIndex - bIndex;\n      })\n      .map((ch: Channel) => ({\n        name: ch.name,\n        value: ch,\n        description: `Channel Platform: ${\n          ch.platform === 'bigcommerce'\n            ? 'Stencil'\n            : ch.platform.charAt(0).toUpperCase() + ch.platform.slice(1)\n        }`,\n      })),\n  });\n\n  return existingChannel.id;\n}\n\nasync function getChannelInit(cliApi: CliApi, channelId: number) {\n  const initResponse = await cliApi.getChannelInit(channelId);\n\n  if (!initResponse.ok) {\n    console.error(\n      colorize(\n        'red',\n        `\\nGET /channels/${channelId}/init failed: ${initResponse.status} ${initResponse.statusText}\\n`,\n      ),\n    );\n    process.exit(1);\n  }\n\n  const initData: unknown = await initResponse.json();\n\n  if (!isInitResponse(initData)) {\n    console.error(colorize('red', '\\nUnexpected response format from init endpoint\\n'));\n    process.exit(1);\n  }\n\n  return {\n    storefrontToken: initData.data.storefront_api_token,\n    envVars: initData.data.envVars,\n  };\n}\n\nasync function setupProject(options: {\n  projectName?: string;\n  projectDir: string;\n}): Promise<{ projectName: string; projectDir: string }> {\n  let { projectName, projectDir } = options;\n\n  if (!pathExistsSync(projectDir)) {\n    console.error(colorize('red', `Error: --projectDir ${projectDir} is not a valid path\\n`));\n    process.exit(1);\n  }\n\n  if (projectName) {\n    projectName = kebabCase(projectName);\n    projectDir = join(options.projectDir, projectName);\n\n    if (pathExistsSync(projectDir)) {\n      console.error(colorize('red', `Error: ${projectDir} already exists\\n`));\n      process.exit(1);\n    }\n  }\n\n  if (!projectName) {\n    const validateProjectName = (i: string) => {\n      const formatted = kebabCase(i);\n\n      if (!formatted) return 'Project name is required';\n\n      const targetDir = join(options.projectDir, formatted);\n\n      if (pathExistsSync(targetDir)) return `Destination '${targetDir}' already exists`;\n\n      projectName = formatted;\n      projectDir = targetDir;\n\n      return true;\n    };\n\n    await input({\n      message: 'What is the name of your project?',\n      default: 'my-catalyst-app',\n      validate: validateProjectName,\n    });\n  }\n\n  if (!projectName) throw new Error('Something went wrong, projectName is not defined');\n  if (!projectDir) throw new Error('Something went wrong, projectDir is not defined');\n\n  return { projectName, projectDir };\n}\n\nfunction checkRequiredTools() {\n  try {\n    execSync(getPlatformCheckCommand('git'), { stdio: 'ignore' });\n  } catch {\n    console.error(colorize('red', 'Error: git is required to create a Catalyst project\\n'));\n    process.exit(1);\n  }\n\n  try {\n    execSync(getPlatformCheckCommand('pnpm'), { stdio: 'ignore' });\n  } catch {\n    console.error(colorize('red', 'Error: pnpm is required to create a Catalyst project\\n'));\n    console.error('Tip: Enable it by running `corepack enable pnpm`\\n');\n    process.exit(1);\n  }\n}\n\nexport const create = new Command('create')\n  .description('Command to scaffold and connect a Catalyst storefront to your BigCommerce store')\n  .option('--project-name <name>', 'Name of your Catalyst project')\n  .option('--project-dir <dir>', 'Directory in which to create your project', process.cwd())\n  .option('--store-hash <hash>', 'BigCommerce store hash')\n  .option('--access-token <token>', 'BigCommerce access token')\n  .option('--channel-id <id>', 'BigCommerce channel ID')\n  .option('--storefront-token <token>', 'BigCommerce storefront token')\n  .option(\n    '--gh-ref <ref>',\n    'Clone a specific ref from the source repository',\n    '@bigcommerce/catalyst-core@latest',\n  )\n  .option('--reset-main', 'Reset the main branch to the gh-ref')\n  .option('--repository <repository>', 'GitHub repository to clone from', 'bigcommerce/catalyst')\n  .option('--env <vars...>', 'Arbitrary environment variables to set in .env.local')\n  .addOption(\n    new Option('--bigcommerce-hostname <hostname>', 'BigCommerce hostname')\n      .default('bigcommerce.com')\n      .hideHelp(),\n  )\n  .addOption(\n    new Option('--cli-api-origin <origin>', 'Catalyst CLI API origin')\n      .default('https://cxm-prd.bigcommerceapp.com')\n      .hideHelp(),\n  )\n  // eslint-disable-next-line complexity\n  .action(async (options) => {\n    const { ghRef, repository } = options;\n\n    checkRequiredTools();\n\n    const { projectName, projectDir } = await setupProject({\n      projectName: options.projectName,\n      projectDir: options.projectDir,\n    });\n\n    let storeHash = options.storeHash;\n    let accessToken = options.accessToken;\n    let channelId;\n    let storefrontToken = options.storefrontToken;\n    let credentials;\n\n    if (options.channelId) {\n      channelId = parseInt(options.channelId, 10);\n    }\n\n    let envVars: Record<string, string> = {};\n\n    // Get credentials if needed\n    if ((!storeHash || !accessToken) && (!channelId || !storefrontToken)) {\n      credentials = await login(`https://login.${options.bigcommerceHostname}`);\n      storeHash = credentials.storeHash;\n      accessToken = credentials.accessToken;\n    }\n\n    // If store hash, channel ID, and storefront token are all provided, skip channel selection/creation\n    if (storeHash && channelId && storefrontToken) {\n      envVars.BIGCOMMERCE_STORE_HASH = storeHash;\n      envVars.BIGCOMMERCE_CHANNEL_ID = channelId.toString();\n      envVars.BIGCOMMERCE_STOREFRONT_API_TOKEN = storefrontToken;\n    } else {\n      if (!storeHash || !accessToken) {\n        // Create project without credentials\n        console.log(`\\nCreating '${projectName}' at '${projectDir}'\\n`);\n        cloneCatalyst({ repository, projectName, projectDir, ghRef, resetMain: options.resetMain });\n        await installDependencies(projectDir);\n\n        // Add any CLI-provided env vars\n        if (options.env) {\n          const cliEnvVars = options.env.reduce<Record<string, string>>((acc, env) => {\n            const [key, value] = env.split('=');\n\n            if (key && value) {\n              acc[key] = value;\n            }\n\n            return acc;\n          }, {});\n\n          Object.assign(envVars, cliEnvVars);\n        }\n\n        // Write env vars even if we don't have store credentials\n        writeEnv(projectDir, envVars);\n\n        console.log(\n          [\n            colorize('green', `\\nSuccess! Created '${projectName}' at '${projectDir}'\\n`),\n            `Next steps:`,\n            Object.keys(envVars).length > 0\n              ? colorize('yellow', `\\n- cd ${projectName} && pnpm run dev\\n`)\n              : [\n                  colorize('yellow', `\\n- cd ${projectName} && cp .env.example .env.local`),\n                  colorize(\n                    'yellow',\n                    `\\n- Populate .env.local with your BigCommerce API credentials\\n`,\n                  ),\n                ].join(''),\n          ].join('\\n'),\n        );\n\n        process.exit(0);\n      }\n\n      // At this point we should have a storeHash and can identify the account\n      await telemetry.identify(storeHash);\n\n      if (!channelId || !storefrontToken) {\n        const bc = new Https({\n          baseUrl: `https://api.${options.bigcommerceHostname}/stores/${storeHash}`,\n          accessToken,\n        });\n\n        const cliApi = new CliApi({\n          origin: options.cliApiOrigin,\n          storeHash,\n          accessToken,\n        });\n\n        // If we have channelId but no storefrontToken, just get the init data\n        if (channelId && !storefrontToken) {\n          const initData = await getChannelInit(cliApi, channelId);\n\n          envVars = { ...initData.envVars };\n          storefrontToken = initData.storefrontToken;\n        } else if (!channelId) {\n          const eligibilityResponse = await cliApi.checkEligibility();\n\n          if (!eligibilityResponse.ok) {\n            console.error(\n              colorize(\n                'red',\n                `\\nGET /channels/catalyst/eligibility failed: ${eligibilityResponse.status} ${eligibilityResponse.statusText}\\n`,\n              ),\n            );\n            process.exit(1);\n          }\n\n          const eligibilityData: unknown = await eligibilityResponse.json();\n\n          if (!isEligibilityResponse(eligibilityData)) {\n            console.error(\n              colorize('red', '\\nUnexpected response format from eligibility endpoint\\n'),\n            );\n            process.exit(1);\n          }\n\n          if (!eligibilityData.data.eligible) {\n            console.warn(colorize('yellow', eligibilityData.data.message));\n          }\n\n          let shouldCreateChannel;\n\n          if (eligibilityData.data.eligible) {\n            shouldCreateChannel = await select({\n              message: 'Would you like to create a new channel?',\n              choices: [\n                { name: 'Yes', value: true },\n                { name: 'No', value: false },\n              ],\n            });\n          }\n\n          if (shouldCreateChannel) {\n            const channelData = await handleChannelCreation(bc, cliApi);\n\n            channelId = channelData.channelId;\n            storefrontToken = channelData.storefrontToken;\n            envVars = { ...channelData.envVars };\n\n            console.log(colorize('green', `Channel created successfully`));\n            console.warn(\n              colorize(\n                'yellow',\n                '\\nNote: A preview storefront has been deployed in your BigCommerce control panel. This preview may look different from your local environment as it may be running different code.',\n              ),\n            );\n          }\n\n          if (!shouldCreateChannel) {\n            channelId = await handleChannelSelection(bc);\n\n            const initData = await getChannelInit(cliApi, channelId);\n\n            envVars = { ...initData.envVars };\n            storefrontToken = initData.storefrontToken;\n          }\n        }\n      }\n    }\n\n    // Add any CLI-provided env vars as overrides\n    if (options.env) {\n      const cliEnvVars = options.env.reduce<Record<string, string>>((acc, env) => {\n        const [key, value] = env.split('=');\n\n        if (key && value) {\n          acc[key] = value;\n        }\n\n        return acc;\n      }, {});\n\n      Object.assign(envVars, cliEnvVars);\n    }\n\n    // Add store hash, channel ID, and storefront token to envVars if provided\n    if (options.storeHash) {\n      envVars.BIGCOMMERCE_STORE_HASH = options.storeHash;\n    }\n\n    if (options.channelId) {\n      envVars.BIGCOMMERCE_CHANNEL_ID = options.channelId;\n    }\n\n    if (options.storefrontToken) {\n      envVars.BIGCOMMERCE_STOREFRONT_TOKEN = options.storefrontToken;\n    }\n\n    if (!channelId) throw new Error('Something went wrong, channelId is not defined');\n    if (!storefrontToken) throw new Error('Something went wrong, storefrontToken is not defined');\n\n    // Create the project with all necessary configuration\n    console.log(`\\nCreating '${projectName}' at '${projectDir}'\\n`);\n    cloneCatalyst({ repository, projectName, projectDir, ghRef, resetMain: options.resetMain });\n    await installDependencies(projectDir);\n\n    // Write env vars\n    writeEnv(projectDir, envVars);\n\n    // Store credentials after successful project creation\n    if (credentials) {\n      storeCredentials(projectDir, credentials);\n    }\n\n    console.log(\n      colorize('green', `\\nSuccess! Created '${projectName}' at '${projectDir}'\\n`),\n      '\\nNext steps:\\n',\n      colorize('yellow', `\\ncd ${projectName} && pnpm run dev\\n`),\n    );\n  });\n\nfunction isInitResponse(response: unknown): response is InitResponse {\n  return (\n    typeof response === 'object' &&\n    response !== null &&\n    'data' in response &&\n    typeof response.data === 'object' &&\n    response.data !== null &&\n    'storefront_api_token' in response.data &&\n    'envVars' in response.data\n  );\n}\n\nfunction isEligibilityResponse(response: unknown): response is EligibilityResponse {\n  return (\n    typeof response === 'object' &&\n    response !== null &&\n    'data' in response &&\n    typeof response.data === 'object' &&\n    response.data !== null &&\n    'eligible' in response.data &&\n    'message' in response.data\n  );\n}\n\nfunction isCreateChannelResponse(response: unknown): response is CreateChannelResponse {\n  return (\n    typeof response === 'object' &&\n    response !== null &&\n    'data' in response &&\n    typeof response.data === 'object' &&\n    response.data !== null &&\n    'id' in response.data &&\n    'storefront_api_token' in response.data &&\n    'envVars' in response.data\n  );\n}\n\nfunction isChannelsResponse(response: unknown): response is ChannelsResponse {\n  return (\n    typeof response === 'object' &&\n    response !== null &&\n    'data' in response &&\n    Array.isArray(response.data) &&\n    response.data.every(\n      (item) =>\n        typeof item === 'object' &&\n        item !== null &&\n        'id' in item &&\n        'name' in item &&\n        'platform' in item,\n    )\n  );\n}\n"
  },
  {
    "path": "packages/create-catalyst/src/commands/init.ts",
    "content": "import { Command, Option } from '@commander-js/extra-typings';\nimport { select } from '@inquirer/prompts';\nimport { colorize } from 'consola/utils';\n\nimport { CliApi } from '../utils/cli-api';\nimport { Config } from '../utils/config';\nimport { Https } from '../utils/https';\nimport { login, storeCredentials } from '../utils/login';\nimport { Telemetry } from '../utils/telemetry/telemetry';\nimport { writeEnv } from '../utils/write-env';\n\ninterface Channel {\n  id: number;\n  name: string;\n  platform: string;\n}\n\ninterface ChannelsResponse {\n  data: Channel[];\n}\n\ninterface InitResponse {\n  data: {\n    makeswift_dev_api_key: string;\n    storefront_api_token: string;\n    envVars: Record<string, string>;\n  };\n}\n\nfunction isChannelsResponse(response: unknown): response is ChannelsResponse {\n  return (\n    typeof response === 'object' &&\n    response !== null &&\n    'data' in response &&\n    Array.isArray(response.data) &&\n    response.data.every(\n      (item) =>\n        typeof item === 'object' &&\n        item !== null &&\n        'id' in item &&\n        'name' in item &&\n        'platform' in item,\n    )\n  );\n}\n\nfunction isInitResponse(response: unknown): response is InitResponse {\n  return (\n    typeof response === 'object' &&\n    response !== null &&\n    'data' in response &&\n    typeof response.data === 'object' &&\n    response.data !== null &&\n    'storefront_api_token' in response.data &&\n    'envVars' in response.data\n  );\n}\n\nconst telemetry = new Telemetry();\n\nexport const init = new Command('init')\n  .description('Connect a BigCommerce store with an existing Catalyst project')\n  .option('--store-hash <hash>', 'BigCommerce store hash')\n  .option('--access-token <token>', 'BigCommerce access token')\n  .option('--env <vars...>', 'Arbitrary environment variables to set in .env.local')\n  .addOption(\n    new Option('--bigcommerce-hostname <hostname>', 'BigCommerce hostname')\n      .default('bigcommerce.com')\n      .hideHelp(),\n  )\n  .addOption(\n    new Option('--cli-api-origin <origin>', 'Catalyst CLI API origin')\n      .default('https://cxm-prd.bigcommerceapp.com')\n      .hideHelp(),\n  )\n  .action(async (options) => {\n    const projectDir = process.cwd();\n\n    let storeHash = options.storeHash;\n    let accessToken = options.accessToken;\n\n    // Check for stored credentials\n    if (!storeHash || !accessToken) {\n      const config = new Config(projectDir);\n      const storedAuth = config.getAuth();\n\n      storeHash = storeHash ?? storedAuth.storeHash;\n      accessToken = accessToken ?? storedAuth.accessToken;\n    }\n\n    if (!storeHash || !accessToken) {\n      const credentials = await login(`https://login.${options.bigcommerceHostname}`);\n\n      storeHash = credentials.storeHash;\n      accessToken = credentials.accessToken;\n\n      // Store credentials after successful authentication\n      storeCredentials(projectDir, credentials);\n    }\n\n    await telemetry.identify(storeHash);\n\n    const bc = new Https({\n      baseUrl: `https://api.${options.bigcommerceHostname}/stores/${storeHash}`,\n      accessToken,\n    });\n\n    const cliApi = new CliApi({\n      origin: options.cliApiOrigin,\n      storeHash,\n      accessToken,\n    });\n\n    const channelSortOrder = ['catalyst', 'next', 'bigcommerce'];\n    const channelsResponse = await bc.fetch('/v3/channels?available=true&type=storefront');\n\n    if (!channelsResponse.ok) {\n      console.error(\n        colorize(\n          'red',\n          `\\nGET /v3/channels failed: ${channelsResponse.status} ${channelsResponse.statusText}\\n`,\n        ),\n      );\n      process.exit(1);\n    }\n\n    const availableChannels: unknown = await channelsResponse.json();\n\n    if (!isChannelsResponse(availableChannels)) {\n      console.error(colorize('red', '\\nUnexpected response format from channels endpoint\\n'));\n      process.exit(1);\n    }\n\n    const existingChannel = await select({\n      message: 'Which channel would you like to use?',\n      choices: availableChannels.data\n        .sort(\n          (a: Channel, b: Channel) =>\n            channelSortOrder.indexOf(a.platform) - channelSortOrder.indexOf(b.platform),\n        )\n        .map((ch: Channel) => ({\n          name: ch.name,\n          value: ch,\n          description: `Channel Platform: ${\n            ch.platform === 'bigcommerce'\n              ? 'Stencil'\n              : ch.platform.charAt(0).toUpperCase() + ch.platform.slice(1)\n          }`,\n        })),\n    });\n\n    const channelId = existingChannel.id;\n\n    // Get channel init data\n    const initResponse = await cliApi.getChannelInit(channelId);\n\n    if (!initResponse.ok) {\n      console.error(\n        colorize(\n          'red',\n          `\\nGET /channels/${channelId}/init failed: ${initResponse.status} ${initResponse.statusText}\\n`,\n        ),\n      );\n      process.exit(1);\n    }\n\n    const initData: unknown = await initResponse.json();\n\n    if (!isInitResponse(initData)) {\n      console.error(colorize('red', '\\nUnexpected response format from init endpoint\\n'));\n      process.exit(1);\n    }\n\n    const envVars = { ...initData.data.envVars };\n\n    // Add any CLI-provided env vars as overrides\n    if (options.env) {\n      const cliEnvVars = options.env.reduce<Record<string, string>>((acc, env) => {\n        const [key, value] = env.split('=');\n\n        if (key && value) {\n          acc[key] = value;\n        }\n\n        return acc;\n      }, {});\n\n      Object.assign(envVars, cliEnvVars);\n    }\n\n    writeEnv(projectDir, envVars);\n\n    console.log(\n      colorize('green', `\\n.env.local file created for channel ${existingChannel.name}!\\n`),\n    );\n    console.log(colorize('green', `\\nNext steps:\\n`));\n    console.log(colorize('yellow', `\\npnpm run dev\\n`));\n  });\n"
  },
  {
    "path": "packages/create-catalyst/src/commands/integration.ts",
    "content": "import { Command } from '@commander-js/extra-typings';\nimport { exec as execCb } from 'child_process';\nimport { parse } from 'dotenv';\nimport { outputFileSync, writeJsonSync } from 'fs-extra/esm';\nimport kebabCase from 'lodash.kebabcase';\nimport { coerce, compare } from 'semver';\nimport { promisify } from 'util';\nimport { z } from 'zod';\n\nconst exec = promisify(execCb);\n\nexport const ManifestSchema = z.object({\n  name: z.string(),\n  dependencies: z.object({ add: z.array(z.string()) }),\n  devDependencies: z.object({ add: z.array(z.string()) }),\n  environmentVariables: z.array(z.string()),\n});\n\ntype Manifest = z.infer<typeof ManifestSchema>;\n\nexport const integration = new Command('integration')\n  .argument('<integration-name>', 'Formatted name of the integration')\n  .option('--commit-hash <hash>', 'Override integration source branch with a specific commit hash')\n  .action(async (integrationNameRaw, options) => {\n    // @todo check for integration name conflicts\n    const integrationName = z.string().transform(kebabCase).parse(integrationNameRaw);\n\n    const manifest: Manifest = {\n      name: integrationName,\n      dependencies: { add: [] },\n      devDependencies: { add: [] },\n      environmentVariables: [],\n    };\n\n    await exec('git fetch --tags');\n\n    const { stdout: headRefStdOut } = await exec('git rev-parse --abbrev-ref HEAD');\n    let [sourceRef] = headRefStdOut.split('\\n');\n\n    if (options.commitHash) {\n      sourceRef = options.commitHash;\n    }\n\n    const { stdout: catalystTags } = await exec('git tag --list @bigcommerce/catalyst-core@\\\\*');\n    const [latestCoreTag] = catalystTags\n      .split('\\n')\n      .filter(Boolean)\n      .sort((a, b) => {\n        const versionA = coerce(a.replace('@bigcommerce/catalyst-core@', ''));\n        const versionB = coerce(b.replace('@bigcommerce/catalyst-core@', ''));\n\n        if (versionA && versionB) {\n          return compare(versionA, versionB);\n        }\n\n        return 0;\n      })\n      .reverse();\n\n    const PackageDependenciesSchema = z.object({\n      dependencies: z.object({}).passthrough(),\n      devDependencies: z.object({}).passthrough(),\n    });\n\n    const getPackageDeps = async (ref: string) => {\n      const { stdout } = await exec(`git show ${ref}:core/package.json`);\n\n      return PackageDependenciesSchema.parse(JSON.parse(stdout));\n    };\n\n    const integrationJson = await getPackageDeps(sourceRef);\n    const latestCoreTagJson = await getPackageDeps(latestCoreTag);\n\n    const diffObjectKeys = (a: Record<string, unknown>, b: Record<string, unknown>) => {\n      return Object.keys(a).filter((key) => !Object.keys(b).includes(key));\n    };\n\n    manifest.dependencies.add = diffObjectKeys(\n      integrationJson.dependencies,\n      latestCoreTagJson.dependencies,\n    );\n    manifest.devDependencies.add = diffObjectKeys(\n      integrationJson.devDependencies,\n      latestCoreTagJson.devDependencies,\n    );\n\n    const { stdout: latestCoreEnv } = await exec(`git show ${latestCoreTag}:core/.env.example`);\n    const { stdout: integrationEnv } = await exec(`git show ${sourceRef}:core/.env.example`);\n\n    manifest.environmentVariables = diffObjectKeys(parse(integrationEnv), parse(latestCoreEnv));\n\n    const { stdout: integrationDiff } = await exec(\n      `git diff ${latestCoreTag}...${sourceRef} -- ':(exclude)core/package.json' ':(exclude)pnpm-lock.yaml'`,\n    );\n\n    outputFileSync(`integrations/${integrationName}/integration.patch`, integrationDiff);\n    writeJsonSync(`integrations/${integrationName}/manifest.json`, manifest, {\n      spaces: 2,\n    });\n\n    console.log('Integration created successfully.');\n  });\n"
  },
  {
    "path": "packages/create-catalyst/src/commands/telemetry.ts",
    "content": "import { Argument, Command, Option } from '@commander-js/extra-typings';\n\nimport { catalystTelemetry, CatalystTelemetryOptions } from '../utils/telemetry';\n\nexport const telemetry = new Command('telemetry')\n  .addArgument(new Argument('[arg]').choices(['disable', 'enable', 'status']))\n  .addOption(new Option('--enable', `Enables CLI telemetry collection.`).conflicts('disable'))\n  .option('--disable', `Disables CLI telemetry collection.`)\n  .action((arg: string | undefined, options: CatalystTelemetryOptions) =>\n    catalystTelemetry(options, arg),\n  );\n"
  },
  {
    "path": "packages/create-catalyst/src/hooks/telemetry.ts",
    "content": "import { Command } from '@commander-js/extra-typings';\n\nimport { Telemetry } from '../utils/telemetry/telemetry';\n\nconst telemetry = new Telemetry();\n\nconst allowlistArguments = ['--gh-ref', '--repository', '--project-name'];\n\nfunction parseArguments(args: string[]) {\n  return args.reduce<Record<string, string>>((result, arg, index, array) => {\n    if (arg.includes('=')) {\n      const [key, value] = arg.split('=');\n\n      if (allowlistArguments.includes(key)) {\n        return {\n          ...result,\n          [key]: value,\n        };\n      }\n    }\n\n    if (allowlistArguments.includes(arg)) {\n      const nextValue =\n        array[index + 1] && !array[index + 1].startsWith('--') ? array[index + 1] : null;\n\n      if (nextValue && !nextValue.includes('--')) {\n        return {\n          ...result,\n          [arg]: nextValue,\n        };\n      }\n    }\n\n    return result;\n  }, {});\n}\n\nexport const telemetryPreHook = async (command: Command) => {\n  // @ts-expect-error _name is a private property\n  const availableCommands = command.commands.map((cmd) => cmd._name); // eslint-disable-line @typescript-eslint/no-unsafe-return, no-underscore-dangle\n\n  const [commandName = 'create', ...args] = command.args;\n\n  // When running `npm create @bigcommerce/catalyst`, the command defaults to\n  // the `create` command but commander doesn't pass it as part of the arguments.\n  //  We need to handle this case separately.\n  if (!availableCommands.includes(commandName)) {\n    // Return the await to get a proper stack trace.\n    // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression\n    return await telemetry.track('create', {\n      ...parseArguments(args),\n    });\n  }\n\n  // Return the await to get a proper stack trace.\n  // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression\n  return await telemetry.track(commandName, {\n    ...parseArguments(args),\n  });\n};\n\nexport const telemetryPostHook = async () => {\n  await telemetry.analytics.closeAndFlush();\n};\n"
  },
  {
    "path": "packages/create-catalyst/src/index.ts",
    "content": "#!/usr/bin/env node\n\nimport { program } from '@commander-js/extra-typings';\nimport { colorize } from 'consola/utils';\n\nimport PACKAGE_INFO from '../package.json';\n\nimport { create } from './commands/create';\nimport { init } from './commands/init';\nimport { integration } from './commands/integration';\nimport { telemetry } from './commands/telemetry';\nimport { telemetryPostHook, telemetryPreHook } from './hooks/telemetry';\n\nconsole.log(colorize('cyanBright', `\\n◢ ${PACKAGE_INFO.name} v${PACKAGE_INFO.version}\\n`));\n\nprogram\n  .name(PACKAGE_INFO.name)\n  .version(PACKAGE_INFO.version)\n  .description('A command line tool to create a new Catalyst project.')\n  .addCommand(create, { isDefault: true })\n  .addCommand(init)\n  .addCommand(integration)\n  .addCommand(telemetry)\n  .hook('preAction', telemetryPreHook)\n  .hook('postAction', telemetryPostHook);\n\nprogram.parse(process.argv);\n"
  },
  {
    "path": "packages/create-catalyst/src/prompts/multi-select/helpers.ts",
    "content": "import { Separator } from '@inquirer/core';\nimport figures from '@inquirer/figures';\nimport { colorize } from 'consola/utils';\n\nimport { Choice, Item, NormalizedChoice, SelectTheme } from './types';\n\nexport const selectTheme: SelectTheme = {\n  helpMode: 'auto',\n  icon: {\n    checked: colorize('green', figures.circleFilled),\n    unchecked: figures.circle,\n    cursor: figures.pointer,\n  },\n  style: {\n    description: (text: string) => colorize('cyan', text),\n    disabledChoice: (text: string) => colorize('dim', `- ${text}`),\n    renderSelectedChoices: (selectedChoices) =>\n      selectedChoices.map((choice) => choice.short).join(', '),\n  },\n};\n\nexport const isSelectable = <Value>(item: Item<Value>): item is NormalizedChoice<Value> =>\n  !Separator.isSeparator(item) && !item.disabled;\n\nexport const isChecked = <Value>(item: Item<Value>): item is NormalizedChoice<Value> =>\n  isSelectable(item) && Boolean(item.checked);\n\nexport const toggle = <Value>(item: Item<Value>): Item<Value> =>\n  isSelectable(item) ? { ...item, checked: !item.checked } : item;\n\nexport const normalizeChoices = <Value>(\n  choices: ReadonlyArray<string | Choice<Value> | Separator>,\n): Array<Item<Value>> =>\n  choices.map((choice) => {\n    if (Separator.isSeparator(choice)) {\n      return choice;\n    }\n\n    if (typeof choice === 'string') {\n      return {\n        checked: false,\n        disabled: false,\n        name: choice,\n        short: choice,\n        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n        value: choice as Value,\n      };\n    }\n\n    const name = choice.name ?? String(choice.value);\n\n    return {\n      checked: choice.checked ?? false,\n      description: choice.description,\n      disabled: choice.disabled ?? false,\n      name,\n      short: choice.short ?? name,\n      value: choice.value,\n    };\n  });\n"
  },
  {
    "path": "packages/create-catalyst/src/prompts/multi-select/index.ts",
    "content": "export * from './multi-select';\n"
  },
  {
    "path": "packages/create-catalyst/src/prompts/multi-select/multi-select.ts",
    "content": "import {\n  createPrompt,\n  isBackspaceKey,\n  isDownKey,\n  isEnterKey,\n  isNumberKey,\n  isSpaceKey,\n  isUpKey,\n  makeTheme,\n  Separator,\n  type Status,\n  useEffect,\n  useKeypress,\n  useMemo,\n  usePagination,\n  usePrefix,\n  useRef,\n  useState,\n  ValidationError,\n} from '@inquirer/core';\nimport ansiEscapes from 'ansi-escapes';\n\nimport { isChecked, isSelectable, normalizeChoices, selectTheme, toggle } from './helpers';\nimport { Item, MultiSelectConfig, SelectTheme } from './types';\n\nexport const multiSelect = createPrompt(\n  <Value>(config: MultiSelectConfig<Value>, done: (value: Value[]) => void) => {\n    const theme = makeTheme<SelectTheme>(selectTheme, config.theme);\n    const searchTimeoutRef = useRef<ReturnType<typeof setTimeout>>();\n    const firstRender = useRef(true);\n    const [items, setItems] = useState<ReadonlyArray<Item<Value>>>(\n      normalizeChoices(config.choices),\n    );\n    const [status, setStatus] = useState<Status>('idle');\n    const [errorMsg, setError] = useState<string>();\n    const prefix = usePrefix({ status, theme });\n    const { instructions, loop = true, pageSize = 7, required, validate = () => true } = config;\n\n    const bounds = useMemo(() => {\n      const first = items.findIndex(isSelectable);\n      const last = items.findLastIndex(isSelectable);\n\n      if (first === -1) {\n        throw new ValidationError(\n          '[multi select prompt] No selectable choices. All choices are disabled.',\n        );\n      }\n\n      return { first, last };\n    }, [items]);\n\n    const [showHelpTip, setShowHelpTip] = useState(true);\n    const [active, setActive] = useState(bounds.first);\n\n    useEffect(\n      () => () => {\n        clearTimeout(searchTimeoutRef.current);\n      },\n      [],\n    );\n\n    // eslint-disable-next-line complexity\n    useKeypress(async (key, rl) => {\n      clearTimeout(searchTimeoutRef.current);\n\n      if (isEnterKey(key)) {\n        const selection = items.filter(isChecked);\n        const isValid = await validate([...selection]);\n\n        if (required && !items.some(isChecked)) {\n          setError('At least one choice must be selected');\n        } else if (isValid === true) {\n          setStatus('done');\n          done(selection.map((choice) => choice.value));\n        } else {\n          setError(isValid || 'You must select a valid value');\n        }\n      } else if (isUpKey(key) || isDownKey(key)) {\n        rl.clearLine(0);\n\n        if (\n          loop ||\n          (isUpKey(key) && active !== bounds.first) ||\n          (isDownKey(key) && active !== bounds.last)\n        ) {\n          const offset = isUpKey(key) ? -1 : 1;\n          let next = active;\n\n          do {\n            next = (next + offset + items.length) % items.length;\n          } while (!isSelectable(items[next]));\n\n          setActive(next);\n        }\n      } else if (isNumberKey(key)) {\n        rl.clearLine(0);\n\n        const position = Number(key.name) - 1;\n\n        if (isSelectable(items[position])) {\n          setActive(position);\n        }\n      } else if (isBackspaceKey(key)) {\n        rl.clearLine(0);\n      } else if (isSpaceKey(key)) {\n        rl.clearLine(0);\n        setShowHelpTip(false);\n\n        const nextItems = items.map((choice, i) => (i === active ? toggle(choice) : choice));\n        const selection = nextItems.filter(isChecked);\n        const isValid = await validate([...selection]);\n\n        if (isValid === true) {\n          setError(undefined);\n          setItems(nextItems);\n        } else {\n          setError(isValid || 'You must select a valid value');\n        }\n      } else {\n        // Default to search\n        const searchTerm = rl.line.toLowerCase();\n        const matchIndex = items.findIndex((item) =>\n          Separator.isSeparator(item) || !isSelectable(item)\n            ? false\n            : item.name.toLowerCase().startsWith(searchTerm),\n        );\n\n        if (matchIndex !== -1) {\n          setActive(matchIndex);\n        }\n\n        searchTimeoutRef.current = setTimeout(() => {\n          rl.clearLine(0);\n        }, 700);\n      }\n    });\n\n    const message = theme.style.message(config.message, status);\n\n    let helpTipTop = '';\n    let helpTipBottom = '';\n    let description: string | undefined;\n\n    if (\n      theme.helpMode === 'always' ||\n      (theme.helpMode === 'auto' && showHelpTip && (instructions === undefined || instructions))\n    ) {\n      if (typeof instructions === 'string') {\n        helpTipTop = instructions;\n      } else {\n        const keys = [\n          `${theme.style.key('space')} to select`,\n          `${theme.style.key('enter')} to proceed`,\n        ];\n\n        helpTipTop = ` (Press ${keys.filter((key) => key !== '').join(', ')})`;\n      }\n\n      if (\n        items.length > pageSize &&\n        (theme.helpMode === 'always' ||\n          // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n          (theme.helpMode === 'auto' && firstRender.current))\n      ) {\n        helpTipBottom = `\\n${theme.style.help('(Use arrow keys to reveal more choices)')}`;\n        firstRender.current = false;\n      }\n    }\n\n    const page = usePagination({\n      items,\n      active,\n      renderItem({ item, isActive }) {\n        if (Separator.isSeparator(item)) {\n          return ` ${item.separator}`;\n        }\n\n        if (item.disabled) {\n          return theme.style.disabledChoice(\n            `${item.name} ${typeof item.disabled === 'string' ? item.disabled : '(disabled)'}`,\n          );\n        }\n\n        if (isActive) {\n          description = item.description;\n        }\n\n        const checkbox = item.checked ? theme.icon.checked : theme.icon.unchecked;\n        const color = isActive ? theme.style.highlight : (x: string) => x;\n        const cursor = isActive ? theme.icon.cursor : ` `;\n\n        return color(`${cursor}${checkbox} ${item.name}`);\n      },\n      pageSize,\n      loop,\n    });\n\n    if (status === 'done') {\n      return `${prefix} ${message} ${theme.style.answer(theme.style.renderSelectedChoices(items.filter(isChecked), items))}`;\n    }\n\n    const selectedChoices =\n      items.filter(isChecked).length > 0\n        ? ` (${theme.style.answer(theme.style.renderSelectedChoices(items.filter(isChecked), items))})`\n        : '';\n    const choiceDescription = description ? `\\n${theme.style.description(description)}` : ``;\n\n    let error = '';\n\n    if (errorMsg) {\n      error = `\\n${theme.style.error(errorMsg)}`;\n    }\n\n    return `${prefix} ${message}${selectedChoices}${helpTipTop}\\n${page}${helpTipBottom}${choiceDescription}${error}${ansiEscapes.cursorHide}`;\n  },\n);\n\nexport { Separator } from '@inquirer/core';\n"
  },
  {
    "path": "packages/create-catalyst/src/prompts/multi-select/types.ts",
    "content": "import { Separator, type Theme } from '@inquirer/core';\nimport type { PartialDeep } from '@inquirer/type';\n\nexport interface SelectTheme {\n  helpMode: 'always' | 'never' | 'auto';\n  icon: {\n    checked: string;\n    unchecked: string;\n    cursor: string;\n  };\n  style: {\n    description: (text: string) => string;\n    disabledChoice: (text: string) => string;\n    renderSelectedChoices: <T>(\n      selectedChoices: ReadonlyArray<NormalizedChoice<T>>,\n      allChoices: ReadonlyArray<NormalizedChoice<T> | Separator>,\n    ) => string;\n  };\n}\n\nexport interface Choice<Value> {\n  checked?: boolean;\n  description?: string;\n  disabled?: boolean | string;\n  name?: string;\n  short?: string;\n  type?: never;\n  value: Value;\n}\n\nexport interface NormalizedChoice<Value> {\n  checked: boolean;\n  description?: string;\n  disabled: boolean | string;\n  name: string;\n  short: string;\n  value: Value;\n}\n\nexport interface MultiSelectConfig<\n  Value,\n  ChoicesObject = ReadonlyArray<string | Separator> | ReadonlyArray<Choice<Value> | Separator>,\n> {\n  choices: ChoicesObject extends ReadonlyArray<string | Separator>\n    ? ChoicesObject\n    : ReadonlyArray<Choice<Value> | Separator>;\n  default?: unknown;\n  instructions?: string | boolean;\n  loop?: boolean;\n  message: string;\n  pageSize?: number;\n  required?: boolean;\n  theme?: PartialDeep<Theme<SelectTheme>>;\n  validate?: (\n    choices: ReadonlyArray<Choice<Value>>,\n  ) => boolean | string | Promise<string | boolean>;\n}\n\nexport type Item<Value> = NormalizedChoice<Value> | Separator;\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/auth.ts",
    "content": "import { z } from 'zod';\n\nimport { Https } from './https';\nimport { parse } from './parse';\n\ninterface AuthConfig {\n  baseUrl: string;\n}\n\nexport class Auth {\n  private client: Https;\n  private readonly DEVICE_OAUTH_CLIENT_ID = 'b8063bu6hhml4e0lqh22yut63atsbyv';\n\n  constructor({ baseUrl }: AuthConfig) {\n    this.client = new Https({ baseUrl });\n  }\n\n  async getDeviceCode() {\n    const response = await this.client.fetch('/device/token', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        scopes: [\n          'store_channel_settings',\n          'store_sites',\n          'store_storefront_api',\n          'store_v2_content',\n          'store_v2_information',\n          'store_v2_products',\n          'store_cart',\n        ].join(' '),\n        client_id: this.DEVICE_OAUTH_CLIENT_ID,\n      }),\n    });\n\n    const DeviceCodeSchema = z.object({\n      device_code: z.string(),\n      user_code: z.string(),\n      verification_uri: z.string(),\n      expires_in: z.number(),\n      interval: z.number(),\n    });\n\n    return parse(await response.json(), DeviceCodeSchema);\n  }\n\n  async checkDeviceCode(deviceCode: string) {\n    const response = await this.client.fetch('/device/token', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        device_code: deviceCode,\n        client_id: this.DEVICE_OAUTH_CLIENT_ID,\n      }),\n    });\n\n    if (response.status !== 200) {\n      throw new Error('Device code not yet verified');\n    }\n\n    const DeviceCodeSuccessSchema = z.object({\n      access_token: z.string(),\n      store_hash: z.string(),\n      context: z.string(),\n      api_uri: z.string().url(),\n    });\n\n    return parse(await response.json(), DeviceCodeSuccessSchema);\n  }\n}\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/checkout-ref.ts",
    "content": "import { sync as spawnSync } from 'cross-spawn';\n\nimport { isExecException } from './is-exec-exception';\n\nexport function checkoutRef(repoDir: string, ref: string): void {\n  try {\n    // Attempt to checkout the specified ref\n    const spawn = spawnSync('git', ['checkout', ref, '--'], {\n      cwd: repoDir,\n      encoding: 'utf8',\n      // Explicitly set shell to false to avoid shell injection\n      // Don't use shell: true as it's a security risk\n      shell: false,\n    });\n\n    const stderr = spawn.stderr.trim();\n\n    if (spawn.status !== 0 && stderr) {\n      throw new Error(stderr);\n    }\n\n    console.log(`Checked out ref ${ref} successfully.`);\n  } catch (error: unknown) {\n    // Handle the error safely according to ESLint rules\n    if (isExecException(error)) {\n      const stderr = error.stderr ? error.stderr.toString() : '';\n\n      // Check if the error message indicates that the ref was not found\n      if (\n        stderr.includes(`fatal: reference is not a tree: ${ref}`) ||\n        stderr.includes(`fatal: ambiguous argument '${ref}'`) ||\n        stderr.includes(`unknown revision or path not in the working tree`)\n      ) {\n        console.error(`Ref '${ref}' not found in the repository.`);\n      } else {\n        console.error(`Error checking out ref '${ref}':`, stderr.trim());\n      }\n    } else if (error instanceof Error) {\n      // General error handling\n      console.error(`Error checking out ref '${ref}':`, error.message);\n    } else {\n      // Unknown error type\n      console.error(`Unknown error occurred while checking out ref '${ref}'.`);\n    }\n\n    console.warn(`Falling back to the default branch.`);\n  }\n}\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/cli-api.ts",
    "content": "import { Https } from './https';\n\ninterface CliApiConfig {\n  origin: string;\n  storeHash: string;\n  accessToken: string;\n}\n\nexport class CliApi {\n  private client: Https;\n\n  constructor({ origin, storeHash, accessToken }: CliApiConfig) {\n    this.client = new Https({\n      baseUrl: `${origin}/stores/${storeHash}/cli-api/v3`,\n      accessToken,\n    });\n  }\n\n  async getChannelInit(channelId: number | string) {\n    return this.client.fetch(`/channels/${channelId}/init`, {\n      method: 'GET',\n    });\n  }\n\n  async checkEligibility() {\n    return this.client.fetch('/channels/catalyst/eligibility', {\n      method: 'GET',\n    });\n  }\n\n  async createChannel(\n    name: string,\n    storefrontLocale: string,\n    additionalLocales: string[],\n    installSampleData = false,\n  ) {\n    return this.client.fetch('/channels/catalyst', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        name,\n        initialData: {\n          type: installSampleData ? 'sample' : 'none',\n        },\n        deployStorefront: true,\n        devOrigin: 'http://localhost:3000',\n        storefrontLanguage: storefrontLocale,\n        additionalLocales,\n      }),\n    });\n  }\n}\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/clone-catalyst.ts",
    "content": "import { execSync } from 'child_process';\n\nimport { checkoutRef } from './checkout-ref';\nimport { hasGitHubSSH } from './has-github-ssh';\nimport { resetBranchToRef } from './reset-branch-to-ref';\n\nexport const cloneCatalyst = ({\n  repository,\n  projectName,\n  projectDir,\n  ghRef,\n  resetMain = false,\n}: {\n  repository: string;\n  projectName: string;\n  projectDir: string;\n  ghRef?: string;\n  resetMain?: boolean;\n}) => {\n  const useSSH = hasGitHubSSH();\n\n  console.log(`Cloning ${repository} using ${useSSH ? 'SSH' : 'HTTPS'}...\\n`);\n\n  const cloneCommand = `git clone ${\n    useSSH ? `git@github.com:${repository}` : `https://github.com/${repository}`\n  }.git${projectName ? ` ${projectName}` : ''}`;\n\n  execSync(cloneCommand, { stdio: 'inherit' });\n  console.log();\n\n  execSync('git remote rename origin upstream', { cwd: projectDir, stdio: 'inherit' });\n  console.log();\n\n  if (ghRef) {\n    if (resetMain) {\n      // Create and checkout a new main branch\n      execSync('git checkout -b main', { cwd: projectDir, stdio: 'inherit' });\n\n      // Reset it to the specified ref\n      resetBranchToRef(projectDir, ghRef);\n\n      console.log(`Reset main to ${ghRef} successfully.\\n`);\n\n      return;\n    }\n\n    checkoutRef(projectDir, ghRef);\n\n    console.log();\n  }\n};\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/config.ts",
    "content": "import { parse as parseTOML, stringify as stringifyTOML } from '@iarna/toml';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { dirname, join } from 'path';\n\ninterface CatalystConfig {\n  auth?: {\n    storeHash?: string;\n    accessToken?: string;\n  };\n}\n\ninterface TomlRecord {\n  [key: string]: string | number | boolean | TomlRecord | TomlRecord[];\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null;\n}\n\nfunction isCatalystConfig(obj: unknown): obj is CatalystConfig {\n  if (!isRecord(obj)) {\n    return false;\n  }\n\n  if ('auth' in obj) {\n    if (!isRecord(obj.auth)) {\n      return false;\n    }\n\n    const { storeHash, accessToken } = obj.auth;\n\n    return (\n      (storeHash === undefined || typeof storeHash === 'string') &&\n      (accessToken === undefined || typeof accessToken === 'string')\n    );\n  }\n\n  return true;\n}\n\nexport class Config {\n  private configPath: string;\n  private config: CatalystConfig;\n\n  constructor(projectDir: string) {\n    this.configPath = join(projectDir, '.catalyst');\n    this.config = this.read();\n  }\n\n  getAuth(): { storeHash?: string; accessToken?: string } {\n    return this.config.auth ?? {};\n  }\n\n  setAuth(storeHash: string, accessToken: string): void {\n    this.config.auth = { storeHash, accessToken };\n    this.save();\n  }\n\n  save(): void {\n    const configObj: TomlRecord = {};\n\n    if (this.config.auth?.storeHash && this.config.auth.accessToken) {\n      configObj.auth = {\n        storeHash: this.config.auth.storeHash,\n        accessToken: this.config.auth.accessToken,\n      };\n    }\n\n    const dir = dirname(this.configPath);\n\n    if (!existsSync(dir)) {\n      mkdirSync(dir, { recursive: true });\n    }\n\n    const preamble = `# DO NOT commit this file to your repository!\n# This file contains sensitive configuration specific to your local CLI setup.\n# It includes authentication tokens and store-specific information.\n# If using version control, make sure to add .catalyst to your .gitignore file.\n\n`;\n\n    writeFileSync(this.configPath, preamble + stringifyTOML(configObj));\n  }\n\n  private read(): CatalystConfig {\n    if (!existsSync(this.configPath)) {\n      return {};\n    }\n\n    try {\n      const contents = readFileSync(this.configPath, 'utf-8');\n      const parsed = parseTOML(contents);\n\n      if (isCatalystConfig(parsed)) {\n        return parsed;\n      }\n\n      console.warn('Invalid config format in .catalyst file, using defaults');\n\n      return {};\n    } catch {\n      console.warn('Failed to parse .catalyst config file, using defaults');\n\n      return {};\n    }\n  }\n}\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/has-github-ssh.ts",
    "content": "import { execSync } from 'child_process';\n\nimport { isExecException } from './is-exec-exception';\n\nexport function hasGitHubSSH(): boolean {\n  try {\n    // Attempt to connect to GitHub via SSH and capture the output\n    const output = execSync('ssh -T git@github.com', {\n      encoding: 'utf8',\n      stdio: 'pipe',\n    }).toString();\n\n    // Check the output for successful authentication\n    return output.includes('successfully authenticated');\n  } catch (error: unknown) {\n    // Use the type guard to check if error is an ExecException\n    if (isExecException(error)) {\n      const stdout = error.stdout ? error.stdout.toString() : '';\n      const stderr = error.stderr ? error.stderr.toString() : '';\n      const combinedOutput = stdout + stderr;\n\n      // Check if the output indicates successful authentication\n      if (combinedOutput.includes('successfully authenticated')) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n}\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/https.ts",
    "content": "import { getCLIUserAgent } from './user-agent';\n\ninterface HttpsConfig {\n  baseUrl: string;\n  accessToken?: string;\n}\n\nexport class Https {\n  private baseUrl: string;\n  private accessToken?: string;\n  private userAgent: string;\n\n  constructor({ baseUrl, accessToken }: HttpsConfig) {\n    this.baseUrl = baseUrl;\n    this.accessToken = accessToken;\n    this.userAgent = getCLIUserAgent();\n  }\n\n  async fetch(path: string, opts: RequestInit = {}) {\n    const { headers = {}, ...rest } = opts;\n\n    const options = {\n      headers: {\n        ...Object.fromEntries(new Headers(headers)),\n        Accept: 'application/json',\n        'User-Agent': this.userAgent,\n        ...(this.accessToken && { 'X-Auth-Token': this.accessToken }),\n      },\n      ...rest,\n    };\n\n    return fetch(`${this.baseUrl}${path}`, options);\n  }\n}\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/install-dependencies.ts",
    "content": "import { colorize } from 'consola/utils';\nimport { installDependencies as installDeps } from 'nypm';\n\nimport { spinner } from './spinner';\n\nconst installAllDeps = async (projectDir: string) => {\n  await installDeps({ cwd: projectDir, silent: true, packageManager: 'pnpm' });\n};\n\nexport const installDependencies = async (projectDir: string) =>\n  spinner(installAllDeps(projectDir), {\n    text: `Installing dependencies. This could take a minute...`,\n    successText: `Dependencies installed successfully`,\n    failText: (err) => colorize('red', `Failed to install dependencies: ${err.message}`),\n  });\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/is-exec-exception.ts",
    "content": "import { ExecException } from 'node:child_process';\n\nexport function isExecException(error: unknown): error is ExecException {\n  return typeof error === 'object' && error !== null && 'stdout' in error && 'stderr' in error;\n}\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/localization.ts",
    "content": "import { z } from 'zod';\n\nimport { Https } from './https';\n\nconst allowedLocales = [\n  'en',\n  'da',\n  'es-AR',\n  'es-CL',\n  'es-CO',\n  'es-MX',\n  'es-PE',\n  'es-419',\n  'es',\n  'it',\n  'nl',\n  'pl',\n  'pt',\n  'de',\n  'fr',\n  'ja',\n  'no',\n  'pt-BR',\n  'sv',\n];\n\nconst AvailableLocalesSuccessSchema = z.object({\n  data: z.array(\n    z.object({\n      id: z.string(),\n      name: z.string(),\n      fallback: z.string().nullable(),\n      is_supported: z.boolean(),\n    }),\n  ),\n});\n\nexport const getAvailableLocales = async (bc: Https) => {\n  const response = await bc.fetch('/v3/settings/store/available-locales');\n\n  if (!response.ok) {\n    throw new Error(\n      `GET /v3/settings/store/available-locales failed: ${response.status} ${response.statusText}`,\n    );\n  }\n\n  return AvailableLocalesSuccessSchema.parse(await response.json())\n    .data.filter(({ id }) => allowedLocales.includes(id))\n    .map(({ name, id }) => ({ name: `${name} (${id})`, value: id }));\n};\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/login.ts",
    "content": "import { colorize } from 'consola/utils';\nimport open from 'open';\nimport { createInterface } from 'readline';\n\nimport { Auth } from './auth';\nimport { Config } from './config';\nimport { spinner } from './spinner';\n\ninterface LoginResult {\n  storeHash: string;\n  accessToken: string;\n}\n\ninterface DeviceCodeCredentials {\n  store_hash: string;\n  access_token: string;\n}\n\nasync function pollDeviceCode(\n  auth: Auth,\n  deviceCode: string,\n  interval: number,\n): Promise<DeviceCodeCredentials | null> {\n  try {\n    const credentials = await auth.checkDeviceCode(deviceCode);\n\n    return credentials;\n  } catch {\n    await new Promise((resolve) => setTimeout(resolve, interval * 1000));\n\n    return null;\n  }\n}\n\nasync function waitForCredentials(\n  auth: Auth,\n  deviceCode: string,\n  interval: number,\n): Promise<DeviceCodeCredentials> {\n  const credentials = await pollDeviceCode(auth, deviceCode, interval);\n\n  if (credentials) {\n    return credentials;\n  }\n\n  return waitForCredentials(auth, deviceCode, interval);\n}\n\nasync function waitForKeyPress(prompt: string): Promise<boolean> {\n  const rl = createInterface({\n    input: process.stdin,\n    output: process.stdout,\n  });\n\n  // Enable raw mode to get individual keystrokes\n  process.stdin.setRawMode(true);\n  process.stdin.resume();\n\n  return new Promise((resolve) => {\n    process.stdin.once('data', (data) => {\n      // Restore normal stdin mode\n      process.stdin.setRawMode(false);\n      process.stdin.pause();\n      rl.close();\n\n      // Only proceed if Enter was pressed (13 or 10 are the ASCII codes for CR and LF)\n      const shouldProceed = data[0] === 13 || data[0] === 10;\n\n      // Add a newline since we're in raw mode\n      process.stdout.write('\\n');\n\n      resolve(shouldProceed);\n    });\n\n    // Display the prompt\n    rl.write(prompt);\n  });\n}\n\nexport async function login(baseUrl: string): Promise<LoginResult> {\n  const auth = new Auth({ baseUrl });\n\n  const deviceCode = await auth.getDeviceCode();\n\n  console.log(\n    colorize('yellow', '\\n! First copy your one-time code: ') +\n      colorize('bold', deviceCode.user_code),\n  );\n  console.log(`Press Enter to open ${deviceCode.verification_uri} in your browser...`);\n\n  const shouldOpenUrl = await waitForKeyPress('');\n\n  if (shouldOpenUrl) {\n    await open(deviceCode.verification_uri);\n  }\n\n  const credentials = await spinner(\n    () => waitForCredentials(auth, deviceCode.device_code, deviceCode.interval),\n    { text: 'Waiting for authentication...', successText: 'Authentication complete' },\n  );\n\n  return {\n    storeHash: credentials.store_hash,\n    accessToken: credentials.access_token,\n  };\n}\n\nexport function storeCredentials(projectDir: string, credentials: LoginResult): void {\n  const config = new Config(projectDir);\n\n  config.setAuth(credentials.storeHash, credentials.accessToken);\n}\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/node-version.spec.ts",
    "content": "import { satisfies } from 'semver';\n\nconst REQUIRED_NODE_VERSIONS = ['^24'];\n\nconst isNodeVersionSupported = (version: string) =>\n  REQUIRED_NODE_VERSIONS.some((range) => satisfies(version, range));\n\ndescribe('Node.js version gating (mirrors bin/index.cjs)', () => {\n  it('accepts the minimum supported version (24.0.0)', () => {\n    expect(isNodeVersionSupported('24.0.0')).toBe(true);\n  });\n\n  it('accepts a recent Node v24 release', () => {\n    expect(isNodeVersionSupported('24.1.0')).toBe(true);\n  });\n\n  it('rejects Node v20', () => {\n    expect(isNodeVersionSupported('20.0.0')).toBe(false);\n  });\n\n  it('rejects Node v22', () => {\n    expect(isNodeVersionSupported('22.0.0')).toBe(false);\n  });\n\n  it('rejects older Node v18', () => {\n    expect(isNodeVersionSupported('18.0.0')).toBe(false);\n  });\n\n  it('rejects odd-numbered Node v23', () => {\n    expect(isNodeVersionSupported('23.0.0')).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/parse.ts",
    "content": "import * as z from 'zod';\nimport { fromZodError } from 'zod-validation-error';\n\nexport const parse = <T>(data: unknown, schema: z.Schema<T>): T => {\n  try {\n    return schema.parse(data);\n  } catch (error) {\n    if (error instanceof z.ZodError) {\n      console.error(fromZodError(error).toString());\n    }\n\n    process.exit(1);\n  }\n};\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/reset-branch-to-ref.ts",
    "content": "import { sync as spawnSync } from 'cross-spawn';\n\nexport function resetBranchToRef(projectDir: string, ghRef: string) {\n  const spawn = spawnSync('git', ['reset', '--hard', ghRef, '--'], {\n    cwd: projectDir,\n    encoding: 'utf8',\n    // Explicitly set shell to false to avoid shell injection\n    // Don't use shell: true as it's a security risk\n    shell: false,\n  });\n\n  const stderr = spawn.stderr.trim();\n\n  if (spawn.status !== 0 && stderr) {\n    throw new Error(stderr);\n  }\n}\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/spinner.spec.ts",
    "content": "import { oraPromise } from 'ora';\n\nimport { spinner } from './spinner';\n\njest.mock('ora', () => ({\n  oraPromise: jest.fn().mockResolvedValue('Result'),\n}));\n\ndescribe('spinner', () => {\n  it('should call oraPromise with the provided action', async () => {\n    const mockAction = jest.fn();\n\n    await spinner(mockAction, { text: 'Loading', successText: 'Loaded' });\n\n    expect(oraPromise).toHaveBeenCalledWith(mockAction, {\n      spinner: 'triangle',\n      text: 'Loading',\n      successText: 'Loaded',\n    });\n  });\n\n  it('should return the resolved value of the action', async () => {\n    const mockAction = jest.fn().mockResolvedValue('Result');\n\n    const result = await spinner(mockAction, { text: 'Loading', successText: 'Loaded' });\n\n    expect(result).toBe('Result');\n  });\n});\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/spinner.ts",
    "content": "import { type Ora, oraPromise, type PromiseOptions } from 'ora';\n\nexport const spinner = async <T>(\n  action: PromiseLike<T> | ((spinner: Ora) => PromiseLike<T>),\n  oraOpts: PromiseOptions<T>,\n) => {\n  return oraPromise(action, {\n    spinner: 'triangle',\n    ...oraOpts,\n  }).catch(() => process.exit(1));\n};\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/telemetry/index.ts",
    "content": "import { colorize } from 'consola/utils';\n\nimport { Telemetry } from './telemetry';\n\nexport interface CatalystTelemetryOptions {\n  enable?: boolean;\n  disable?: boolean;\n}\n\nconst telemetry = new Telemetry();\nlet isEnabled = telemetry.isEnabled();\n\nconst catalystTelemetry = (options: CatalystTelemetryOptions, arg: string | undefined) => {\n  if (options.enable || arg === 'enable') {\n    telemetry.setEnabled(true);\n    isEnabled = true;\n\n    console.log('Success!');\n  } else if (options.disable || arg === 'disable') {\n    const path = telemetry.setEnabled(false);\n\n    if (isEnabled) {\n      console.log(`Your preference has been saved${path ? ` to ${path}` : ''}`);\n    } else {\n      console.log(`Catalyst CLI telemetry collection is already disabled.`);\n    }\n\n    isEnabled = false;\n  } else {\n    console.log('Catalyst CLI Telemetry');\n  }\n\n  console.log(\n    `\\nStatus: ${colorize('bold', isEnabled ? colorize('green', 'Enabled') : colorize('red', 'Disabled'))}`,\n  );\n\n  if (!isEnabled) {\n    console.log(\n      `\\nYou have opted-out of Catalyst CLI telemetry.\\nNo data will be collected from your machine.`,\n    );\n  }\n};\n\nexport { catalystTelemetry };\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/telemetry/telemetry.ts",
    "content": "import { Analytics } from '@segment/analytics-node';\nimport Conf from 'conf';\nimport { randomBytes } from 'crypto';\n\nimport PACKAGE_INFO from '../../../package.json';\n\n// This is the key that stores whether or not telemetry is enabled or disabled.\nconst TELEMETRY_KEY_ENABLED = 'telemetry.enabled';\n\n// This is a quasi-persistent identifier used to dedupe recurring events.\nconst TELEMETRY_KEY_ID = `telemetry.anonymousId`;\n\ninterface Config {\n  telemetry: {\n    enabled: boolean;\n    anonymousId: string;\n  };\n}\n\nexport class Telemetry {\n  readonly sessionId: string;\n  readonly analytics: Analytics;\n\n  private conf: Conf<Config> | null;\n  private CATALYST_TELEMETRY_DISABLED: string | undefined;\n\n  private readonly projectName = 'catalyst-cli';\n  private readonly projectVersion = PACKAGE_INFO.version;\n\n  constructor() {\n    this.CATALYST_TELEMETRY_DISABLED = process.env.CATALYST_TELEMETRY_DISABLED;\n\n    try {\n      this.conf = new Conf({\n        projectName: this.projectName,\n        projectVersion: this.projectVersion,\n      });\n    } catch {\n      this.conf = null;\n    }\n\n    this.sessionId = randomBytes(32).toString('hex');\n    this.analytics = new Analytics({\n      writeKey: process.env.CLI_SEGMENT_WRITE_KEY ?? 'not-a-valid-segment-write-key',\n    });\n  }\n\n  async track(eventName: string, payload: Record<string, unknown>) {\n    if (!this.isEnabled()) {\n      return Promise.resolve(undefined);\n    }\n\n    this.analytics.track({\n      event: eventName,\n      anonymousId: this.getAnonymousId(),\n      properties: {\n        ...payload,\n        sessionId: this.sessionId,\n      },\n      context: {\n        app: {\n          name: this.projectName,\n          version: this.projectVersion,\n        },\n      },\n    });\n  }\n\n  async identify(storeHash?: string) {\n    if (!this.isEnabled()) {\n      return Promise.resolve(undefined);\n    }\n\n    if (!storeHash) {\n      return Promise.resolve(undefined);\n    }\n\n    this.analytics.identify({\n      userId: storeHash,\n      anonymousId: this.getAnonymousId(),\n      context: {\n        app: {\n          name: this.projectName,\n          version: this.projectVersion,\n        },\n      },\n    });\n  }\n\n  setEnabled = (_enabled: boolean) => {\n    const enabled = Boolean(_enabled);\n\n    this.conf?.set(TELEMETRY_KEY_ENABLED, enabled);\n\n    return this.conf?.path;\n  };\n\n  isEnabled(): boolean {\n    return (\n      !this.CATALYST_TELEMETRY_DISABLED &&\n      !!this.conf &&\n      this.conf.get<typeof TELEMETRY_KEY_ENABLED, boolean>(TELEMETRY_KEY_ENABLED, true)\n    );\n  }\n\n  private getAnonymousId(): string {\n    const val = this.conf?.get<typeof TELEMETRY_KEY_ID, string>(TELEMETRY_KEY_ID);\n\n    if (val) {\n      return val;\n    }\n\n    const generated = randomBytes(32).toString('hex');\n\n    this.conf?.set(TELEMETRY_KEY_ID, generated);\n\n    return generated;\n  }\n}\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/user-agent.ts",
    "content": "import {\n  isDevelopment,\n  isLinux,\n  isMacOS,\n  isProduction,\n  isTest,\n  isWindows,\n  nodeVersion,\n  process,\n  provider,\n  runtime,\n} from 'std-env';\n\nimport packageInfo from '../../package.json';\n\nconst { name, version } = packageInfo;\n\nconst getOS = () => {\n  if (isWindows) return 'Windows';\n  if (isMacOS) return 'macOS';\n  if (isLinux) return 'Linux';\n\n  return 'Unknown OS';\n};\n\nconst getEnv = () => {\n  if (isDevelopment) return 'Development';\n  if (isTest) return 'Test';\n  if (isProduction) return 'Production';\n};\n\nconst getPlatform = () => {\n  const os = getOS();\n  const env = getEnv();\n\n  const keysOfInterest = [\n    os,\n    env,\n    runtime,\n    provider,\n    `Node ${nodeVersion}`,\n    process.env.NODE_ENV,\n  ].filter(Boolean);\n\n  return keysOfInterest.join('; ');\n};\n\nconst detectedPlatform = getPlatform();\n\nexport const getCLIUserAgent = (platform?: string, extensions?: string): string => {\n  const userAgentParts = [`${name}/${version}`];\n\n  const platformValue = platform ?? detectedPlatform;\n\n  userAgentParts.push(`(${platformValue})`);\n\n  if (extensions) {\n    userAgentParts.push(extensions);\n  }\n\n  return userAgentParts.join(' ');\n};\n"
  },
  {
    "path": "packages/create-catalyst/src/utils/write-env.ts",
    "content": "import { outputFileSync } from 'fs-extra/esm';\nimport { join } from 'path';\n\nexport const writeEnv = (projectDir: string, envVars: Record<string, string>) => {\n  outputFileSync(\n    join(projectDir, '.env.local'),\n    `${Object.entries(envVars)\n      .map(([key, value]) => `${key}=${value}`)\n      .join('\\n')}\\n`,\n  );\n};\n"
  },
  {
    "path": "packages/create-catalyst/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Node\",\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"esnext\"],\n    \"target\": \"es6\",\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true\n  }\n}\n"
  },
  {
    "path": "packages/create-catalyst/tsup.config.ts",
    "content": "import { defineConfig, Options } from 'tsup';\n\nexport default defineConfig((options: Options) => ({\n  entry: ['src/index.ts'],\n  format: ['esm'],\n  clean: !options.watch,\n  sourcemap: true,\n  env: {\n    CLI_SEGMENT_WRITE_KEY: process.env.CLI_SEGMENT_WRITE_KEY ?? 'not-a-valid-segment-write-key',\n  },\n  ...options,\n}));\n"
  },
  {
    "path": "packages/eslint-config-catalyst/CHANGELOG.md",
    "content": "# @bigcommerce/eslint-config-catalyst\n\n## 1.0.0\n\n### Major Changes\n\n- [#2435](https://github.com/bigcommerce/catalyst/pull/2435) [`cd4bd60`](https://github.com/bigcommerce/catalyst/commit/cd4bd604739b0cea4b622b08ebbde4cea953fcae) Thanks [@matthewvolk](https://github.com/matthewvolk)! - Release 1.0.0 (see [`core/CHANGELOG.md`](../../core/CHANGELOG.md#100) for more details)\n\n### Patch Changes\n\n- [#1933](https://github.com/bigcommerce/catalyst/pull/1933) [`f292236`](https://github.com/bigcommerce/catalyst/commit/f2922366ba94572293856cc7f2532dade0847c86) Thanks [@dependabot](https://github.com/apps/dependabot)! - Manual changes on a dependency bumps.\n\n## 0.1.3\n\n### Patch Changes\n\n- [#1623](https://github.com/bigcommerce/catalyst/pull/1623) [`16e3a76`](https://github.com/bigcommerce/catalyst/commit/16e3a763571324dccd9031a79e400409eff9ee0c) Thanks [@chanceaclark](https://github.com/chanceaclark)! - allow props not to be spread\n\n## 0.1.2\n\n### Patch Changes\n\n- [#1179](https://github.com/bigcommerce/catalyst/pull/1179) [`ae8d985`](https://github.com/bigcommerce/catalyst/commit/ae8d985a89c229f945a596d7a905828dfcbe490e) Thanks [@deini](https://github.com/deini)! - bump next to 14.2.5\n\n## 0.1.1\n\n### Patch Changes\n\n- [#735](https://github.com/bigcommerce/catalyst/pull/735) [`3db9c5f`](https://github.com/bigcommerce/catalyst/commit/3db9c5fa603299a5c5a9a12bd5408f9024677b20) Thanks [@deini](https://github.com/deini)! - Bump dependencies\n"
  },
  {
    "path": "packages/eslint-config-catalyst/base.js",
    "content": "/** @type {import(\"eslint\").Linter.Config} */\nconst config = {\n  extends: ['@bigcommerce/eslint-config/configs/base', 'prettier'],\n  plugins: ['check-file'],\n  parserOptions: {\n    ecmaVersion: 'latest',\n  },\n  overrides: [\n    {\n      files: ['*.ts', '*.tsx'],\n      extends: ['@bigcommerce/eslint-config/configs/typescript'],\n    },\n  ],\n  env: {\n    es2022: true,\n    node: true,\n  },\n  rules: {\n    'check-file/filename-naming-convention': [\n      'error',\n      {\n        '**/*.{jsx,tsx}': 'KEBAB_CASE',\n        '**/*.{js,ts}': 'KEBAB_CASE',\n      },\n      {\n        ignoreMiddleExtensions: true,\n      },\n    ],\n    \"check-file/folder-naming-convention\": [\n      \"error\",\n      {\n        \"**\": \"KEBAB_CASE\",\n      }\n    ]\n  },\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "packages/eslint-config-catalyst/next.js",
    "content": "/** @type {import('eslint').Linter.Config} */\nconst config = {\n  extends: [\"plugin:@next/next/recommended\"],\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "packages/eslint-config-catalyst/package.json",
    "content": "{\n  \"name\": \"@bigcommerce/eslint-config-catalyst\",\n  \"description\": \"Eslint configs for catalyst\",\n  \"version\": \"1.0.0\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bigcommerce/catalyst\",\n    \"directory\": \"packages/eslint-config-catalyst\"\n  },\n  \"files\": [\n    \"./base.js\",\n    \"./next.js\",\n    \"./prettier.js\",\n    \"./react.js\"\n  ],\n  \"dependencies\": {\n    \"@bigcommerce/eslint-config\": \"^2.11.0\",\n    \"@next/eslint-plugin-next\": \"^15.3.3\",\n    \"eslint-config-prettier\": \"^10.1.5\",\n    \"eslint-plugin-check-file\": \"^2.8.0\",\n    \"eslint-plugin-prettier\": \"^5.4.1\"\n  },\n  \"peerDependencies\": {\n    \"eslint\": \"^8.0.0\",\n    \"typescript\": \"^4.0.0 || ^5.0.0\"\n  },\n  \"devDependencies\": {\n    \"eslint\": \"^8.57.1\",\n    \"typescript\": \"^5.8.3\"\n  }\n}\n"
  },
  {
    "path": "packages/eslint-config-catalyst/prettier.js",
    "content": "/** @type {import(\"eslint\").Linter.Config} */\nconst config = {\n  extends: ['plugin:prettier/recommended'],\n  rules: {\n    'prettier/prettier': ['warn'],\n  }\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "packages/eslint-config-catalyst/react.js",
    "content": "/** @type {import(\"eslint\").Linter.Config} */\nconst config = {\n  extends: [\n    '@bigcommerce/eslint-config/configs/react',\n  ],\n  rules: {\n    'react/react-in-jsx-scope': 'off',\n    'react/prefer-read-only-props': 'off',\n    'react/destructuring-assignment': 'off',\n  }\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - core\n  - packages/*\n\nonlyBuiltDependencies:\n  - '@parcel/watcher'\n  - '@swc/core'\n  - '@vercel/speed-insights'\n  - esbuild\n  - msw\n  - protobufjs\n  - puppeteer\n  - sharp\n  - workerd\n\npublicHoistPattern:\n  - '@bigcommerce/eslint-*'\n  - '@bigcommerce/eslint-*'\n  - '@next/eslint-*'\n  - '@stylistic/eslint-*'\n  - '@typescript-eslint/*'\n  - eslint-config-*\n  - eslint-plugin-*\n"
  },
  {
    "path": "turbo.json",
    "content": "{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"remoteCache\": {\n    \"signature\": true\n  },\n  \"globalDependencies\": [\n    \".env.local\"\n  ],\n  \"tasks\": {\n    \"topo\": {\n      \"dependsOn\": [\n        \"^topo\"\n      ]\n    },\n    \"build\": {\n      \"env\": [\"*\"],\n      \"dependsOn\": [\n        \"^build\"\n      ],\n      \"outputs\": [\n        \"dist/**\",\n        \".next/**\",\n        \"!.next/cache/**\"\n      ]\n    },\n    \"lint\": {\n      \"env\": [\"*\"],\n      \"dependsOn\": [\n        \"^build\"\n      ]\n    },\n    \"typecheck\": {\n      \"env\": [\"*\"],\n      \"dependsOn\": [\n        \"topo\"\n      ],\n      \"outputs\": [\n        \"node_modules/.cache/tsbuildinfo.json\"\n      ]\n    },\n    \"test\": {\n      \"dependsOn\": [\n        \"topo\"\n      ]\n    },\n    \"dev\": {\n      \"env\": [\"*\"],\n      \"cache\": false,\n      \"persistent\": true\n    }\n  }\n}\n"
  },
  {
    "path": "unlighthouse.config.ts",
    "content": "import type { UserConfig } from \"unlighthouse\";\n\nexport default {\n  ci: {\n    buildStatic: true,\n    reporter: \"jsonExpanded\",\n    budget: {\n      // \"best-practices\": 100,\n      // \"accessibility\": 100,\n      // \"seo\": 100,\n      // performance: 80,\n    },\n  },\n  scanner: {\n    // Run each page multiple times and use the median to absorb cold start\n    // outliers across all discovered pages.\n    samples: 3,\n    dynamicSampling: 5,\n    exclude: [\n      \"/bundleb2b/\",\n      \"/invoices/\",\n      \"/bath/*/*\",\n      \"/garden/*/*\",\n      \"/kitchen/*/*\",\n      \"/publications/*/*\",\n      \"/early-access/*/*\",\n      \"/digital-test-product/\",\n      \"/blog/\\\\?tag=*\",\n      \"/brands/\",\n    ],\n    customSampling: {\n      \"/smith-journal-13/|/dustpan-brush/|/utility-caddy/|/canvas-laundry-cart/|/laundry-detergent/|/tiered-wire-basket/|/oak-cheese-grater/|/1-l-le-parfait-jar/|/chemex-coffeemaker-3-cup/|/sample-able-brewing-system/|/orbit-terrarium-small/|/orbit-terrarium-large/|/fog-linen-chambray-towel-beige-stripe/|/zz-plant/\":\n        { name: \"PDP\" },\n      \"/shop-all/|/bath/|/garden/|/kitchen/|/publications/|/early-access/\": {\n        name: \"PLP categories\",\n      },\n      \"/brands/sagaform/|/brands/ofs/|/brands/common-good/\": {\n        name: \"PLP brands\",\n      },\n    },\n    // Disable throttling to avoid issues with cold start and cold cache.\n    throttle: false,\n  },\n  lighthouseOptions: {\n    onlyCategories: [\"best-practices\", \"accessibility\", \"seo\", \"performance\"],\n    skipAudits: [\n      // Disabling `is-crawlable` as it's more relevant for production sites.\n      \"is-crawlable\",\n      // Disabling third-party cookies because the only third-party cookies we have is provided through Cloudflare for our CDN, which is not relevant for our audits.\n      \"third-party-cookies\",\n      // Disabling inspector issues as it's only providing third-party cookie issues, which are not relevant for our audits.\n      \"inspector-issues\",\n    ],\n  },\n} satisfies UserConfig;\n"
  }
]