diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..ab4d79f
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,11 @@
+.git
+.gitea
+.next
+node_modules
+data
+coverage
+.env
+.env.local
+npm-debug.log*
+*.tsbuildinfo
+tasks
diff --git a/.gitea/workflows/master.yml b/.gitea/workflows/master.yml
new file mode 100644
index 0000000..ada6d06
--- /dev/null
+++ b/.gitea/workflows/master.yml
@@ -0,0 +1,144 @@
+name: Master Production Deploy
+
+on:
+ push:
+ branches:
+ - master
+
+concurrency:
+ group: deploy-production
+ cancel-in-progress: false
+
+env:
+ REGISTRY: ${{ vars.CONTAINER_REGISTRY }}
+ WEB_IMAGE_REPO: ${{ vars.CONTAINER_REGISTRY }}/${{ github.repository_owner }}/${{ github.event.repository.name }}-web
+ RELEASE_ID: ${{ github.sha }}-${{ github.run_number }}
+ NPM_CONFIG_AUDIT: "false"
+ NPM_CONFIG_FUND: "false"
+ NPM_CONFIG_UPDATE_NOTIFIER: "false"
+
+jobs:
+ production:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: Setup npm
+ run: npm install -g npm@11.6.4
+
+ - name: Install dependencies
+ run: npm ci --prefer-offline --no-audit --no-fund
+
+ - name: Quality gate
+ run: npm run ci:quality
+
+ - name: Setup Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to Gitea Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ secrets.REGISTRY_USERNAME }}
+ password: ${{ secrets.REGISTRY_PASSWORD }}
+
+ - name: Build and push web image
+ id: build-web
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: deploy/docker/Dockerfile.web
+ push: true
+ tags: ${{ env.WEB_IMAGE_REPO }}:${{ github.sha }}
+ labels: |
+ org.opencontainers.image.revision=${{ github.sha }}
+ org.opencontainers.image.version=${{ env.RELEASE_ID }}
+
+ - name: Production pre-deploy checks
+ env:
+ ENVIRONMENT: production
+ STACK_NAME: ${{ vars.PRODUCTION_STACK_NAME }}
+ RELEASE_ID: ${{ env.RELEASE_ID }}
+ COMMIT_SHA: ${{ github.sha }}
+ WEB_IMAGE: ${{ env.WEB_IMAGE_REPO }}@${{ steps.build-web.outputs.digest }}
+ PUBLIC_HOST: ${{ vars.PRODUCTION_PUBLIC_HOST }}
+ TRAEFIK_NETWORK: ${{ vars.PRODUCTION_TRAEFIK_NETWORK }}
+ SWARM_MANAGER_HOST: ${{ secrets.PRODUCTION_SWARM_MANAGER_HOST }}
+ SWARM_SSH_USER: ${{ secrets.PRODUCTION_SWARM_SSH_USER }}
+ run: node deploy/scripts/predeploy-check.mjs
+
+ - name: Deploy production
+ env:
+ ENVIRONMENT: production
+ STACK_NAME: ${{ vars.PRODUCTION_STACK_NAME }}
+ RELEASE_ID: ${{ env.RELEASE_ID }}
+ COMMIT_SHA: ${{ github.sha }}
+ WEB_IMAGE: ${{ env.WEB_IMAGE_REPO }}@${{ steps.build-web.outputs.digest }}
+ PUBLIC_HOST: ${{ vars.PRODUCTION_PUBLIC_HOST }}
+ TRAEFIK_NETWORK: ${{ vars.PRODUCTION_TRAEFIK_NETWORK }}
+ SWARM_MANAGER_HOST: ${{ secrets.PRODUCTION_SWARM_MANAGER_HOST }}
+ SWARM_SSH_USER: ${{ secrets.PRODUCTION_SWARM_SSH_USER }}
+ SWARM_SSH_PORT: ${{ secrets.PRODUCTION_SWARM_SSH_PORT }}
+ REGISTRY: ${{ env.REGISTRY }}
+ REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
+ REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
+ WEB_REPLICAS: ${{ vars.PRODUCTION_WEB_REPLICAS }}
+ WEB_PLACEMENT_CONSTRAINT: ${{ vars.PRODUCTION_WEB_PLACEMENT_CONSTRAINT }}
+ TRAEFIK_ENTRYPOINTS: ${{ vars.TRAEFIK_ENTRYPOINTS }}
+ TRAEFIK_TLS: ${{ vars.TRAEFIK_TLS }}
+ REGION: ${{ vars.REGION }}
+ TEAM_ID: ${{ vars.TEAM_ID }}
+ DEPLOY_MIN_FREE_MB: ${{ vars.DEPLOY_MIN_FREE_MB }}
+ DEPLOY_PRUNE_BEFORE_PULL: ${{ vars.DEPLOY_PRUNE_BEFORE_PULL }}
+ DEPLOY_PRUNE_UNTIL: ${{ vars.DEPLOY_PRUNE_UNTIL }}
+ DEPLOY_CLEANUP_FAILED_IMAGES: ${{ vars.DEPLOY_CLEANUP_FAILED_IMAGES }}
+ VIDEOREG_DB_MEMORY_TTL_MS: ${{ vars.VIDEOREG_DB_MEMORY_TTL_MS }}
+ VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS: ${{ vars.VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS }}
+ run: deploy/scripts/deploy-over-ssh.sh
+
+ - name: Production smoke checks
+ id: smoke
+ continue-on-error: true
+ env:
+ BASE_URL: https://${{ vars.PRODUCTION_PUBLIC_HOST }}
+ EXPECTED_RELEASE_ID: ${{ env.RELEASE_ID }}
+ SMOKE_RESULT_PATH: smoke-result.json
+ run: node deploy/scripts/smoke-check.mjs
+
+ - name: Roll back production after failed smoke checks
+ if: steps.smoke.outcome == 'failure'
+ env:
+ ENVIRONMENT: production
+ STACK_NAME: ${{ vars.PRODUCTION_STACK_NAME }}
+ SWARM_MANAGER_HOST: ${{ secrets.PRODUCTION_SWARM_MANAGER_HOST }}
+ SWARM_SSH_USER: ${{ secrets.PRODUCTION_SWARM_SSH_USER }}
+ SWARM_SSH_PORT: ${{ secrets.PRODUCTION_SWARM_SSH_PORT }}
+ run: deploy/scripts/rollback-over-ssh.sh
+
+ - name: Fail after production smoke rollback
+ if: steps.smoke.outcome == 'failure'
+ run: exit 1
+
+ - name: Write release manifest
+ env:
+ COMMIT_SHA: ${{ github.sha }}
+ RELEASE_ID: ${{ env.RELEASE_ID }}
+ WEB_IMAGE_DIGEST: ${{ env.WEB_IMAGE_REPO }}@${{ steps.build-web.outputs.digest }}
+ SMOKE_RESULT_PATH: smoke-result.json
+ WORKFLOW_RUN_ID: ${{ github.run_id }}
+ run: node deploy/scripts/write-release-manifest.mjs > release-manifest.json
+
+ - name: Upload release manifest
+ uses: actions/upload-artifact@v4
+ with:
+ name: release-manifest-${{ env.RELEASE_ID }}
+ path: release-manifest.json
diff --git a/.gitea/workflows/pr.yml b/.gitea/workflows/pr.yml
new file mode 100644
index 0000000..3e9ece7
--- /dev/null
+++ b/.gitea/workflows/pr.yml
@@ -0,0 +1,36 @@
+name: PR Quality Gate
+
+on:
+ pull_request:
+ branches:
+ - master
+
+concurrency:
+ group: pr-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ NPM_CONFIG_AUDIT: "false"
+ NPM_CONFIG_FUND: "false"
+ NPM_CONFIG_UPDATE_NOTIFIER: "false"
+
+jobs:
+ quality:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: Setup npm
+ run: npm install -g npm@11.6.4
+
+ - name: Install dependencies
+ run: npm ci --prefer-offline --no-audit --no-fund
+
+ - name: Quality gate
+ run: npm run ci:quality
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3102e6b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.next/
+node_modules/
+data/
+coverage/
+.env
+.env.local
+npm-debug.log*
+*.tsbuildinfo
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..9f274d6
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,3 @@
+audit=false
+fund=false
+update-notifier=false
diff --git a/README.md b/README.md
index e69de29..89aa842 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,37 @@
+## videoreg.ru
+
+Small Next.js video registry using TypeScript, App Router, FSD-oriented folders, and a primitive file-backed JSON database.
+
+### Local development
+
+```bash
+npm install
+npm run dev
+```
+
+The app runs on [http://localhost:3000](http://localhost:3000).
+
+### Data storage
+
+By default records are stored in `data/videoreg.json`. Override the path with:
+
+```bash
+VIDEOREG_DB_FILE=/absolute/path/videoreg.json npm run dev
+```
+
+Reads are intentionally cached aggressively:
+
+- process memory cache for the primitive JSON database;
+- Next.js cache tag for dashboard/API reads;
+- long HTTP cache headers on the read API.
+
+Writes call `revalidateTag` and update the process cache.
+
+### Deployment
+
+Gitea Actions runs:
+
+- `.gitea/workflows/pr.yml`: quality gate for pull requests into `master`;
+- `.gitea/workflows/master.yml`: quality gate, image build, push, and direct Swarm deploy from `master`.
+
+The Swarm stack defaults to one replica on a manager node because the primitive database is a mounted JSON file. Use a real database before raising replicas above one or spreading the service across nodes.
diff --git a/deploy/docker/Dockerfile.web b/deploy/docker/Dockerfile.web
new file mode 100644
index 0000000..afddc0a
--- /dev/null
+++ b/deploy/docker/Dockerfile.web
@@ -0,0 +1,37 @@
+# syntax=docker/dockerfile:1
+
+FROM node:22-alpine AS deps
+WORKDIR /app
+
+COPY .npmrc package.json package-lock.json ./
+RUN npm install -g npm@11.6.4 \
+ && npm ci --prefer-offline --no-audit --no-fund
+
+FROM deps AS build
+WORKDIR /app
+ENV NEXT_TELEMETRY_DISABLED=1
+COPY next-env.d.ts next.config.ts tsconfig.json vitest.config.ts ./
+COPY src src
+COPY public public
+RUN npm run ci:quality
+
+FROM node:22-alpine AS runtime
+ENV NODE_ENV=production \
+ APP_ENV=production \
+ NEXT_TELEMETRY_DISABLED=1 \
+ VIDEOREG_DB_FILE=/data/videoreg.json \
+ PORT=3000
+WORKDIR /app
+
+RUN addgroup -S nodeapp && adduser -S nodeapp -G nodeapp && mkdir -p /data && chown nodeapp:nodeapp /data
+
+COPY --from=build /app/.next/standalone ./
+COPY --from=build /app/.next/static ./.next/static
+COPY --from=build /app/public ./public
+
+USER nodeapp
+EXPOSE 3000
+HEALTHCHECK --interval=10s --timeout=3s --start-period=20s --retries=3 \
+ CMD wget -qO- http://127.0.0.1:3000/health/ready >/dev/null || exit 1
+
+CMD ["node", "server.js"]
diff --git a/deploy/remote/deploy-stack.sh b/deploy/remote/deploy-stack.sh
new file mode 100755
index 0000000..7ec9ddb
--- /dev/null
+++ b/deploy/remote/deploy-stack.sh
@@ -0,0 +1,174 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+log() {
+ printf '{"event":"%s","environment":"%s","stack":"%s","releaseId":"%s","commitSha":"%s"}\n' \
+ "$1" "${ENVIRONMENT:-unknown}" "${STACK_NAME:-unknown}" "${RELEASE_ID:-unknown}" "${COMMIT_SHA:-unknown}"
+}
+
+require_var() {
+ local name="$1"
+ if [ -z "${!name:-}" ]; then
+ printf 'Required environment variable %s is missing\n' "$name" >&2
+ exit 2
+ fi
+}
+
+require_var ENVIRONMENT
+require_var STACK_NAME
+require_var RELEASE_ID
+require_var COMMIT_SHA
+require_var WEB_IMAGE
+require_var PUBLIC_HOST
+require_var TRAEFIK_NETWORK
+
+REMOTE_DIR="${REMOTE_DEPLOY_DIR:-/tmp/videoreg-deploy}"
+LOCK_FILE="${RELEASE_LOCK_FILE:-/tmp/${STACK_NAME}-${ENVIRONMENT}.lock}"
+
+exec 9>"$LOCK_FILE"
+if ! flock -n 9; then
+ echo "Another deployment is already running for ${STACK_NAME}/${ENVIRONMENT}" >&2
+ exit 75
+fi
+
+cleanup_release_images() {
+ if [ "${DEPLOY_CLEANUP_FAILED_IMAGES:-true}" != "true" ]; then
+ return 0
+ fi
+
+ echo "Cleaning up unused release image after failed deploy." >&2
+ docker image rm "$WEB_IMAGE" >/dev/null 2>&1 || true
+
+ if [ -n "${COMMIT_SHA:-}" ]; then
+ docker image prune -af --filter "label=org.opencontainers.image.revision=${COMMIT_SHA}" >&2 || true
+ fi
+}
+
+cleanup_on_exit() {
+ local status=$?
+ trap - EXIT
+
+ if [ "$status" -ne 0 ]; then
+ cleanup_release_images
+ fi
+
+ exit "$status"
+}
+
+trap cleanup_on_exit EXIT
+
+docker_disk_paths() {
+ local docker_root
+ docker_root="$(docker info --format '{{.DockerRootDir}}' 2>/dev/null || true)"
+
+ if [ -n "$docker_root" ] && [ -d "$docker_root" ]; then
+ printf '%s\n' "$docker_root"
+ fi
+
+ if [ -d /var/lib/containerd ]; then
+ printf '%s\n' /var/lib/containerd
+ fi
+}
+
+prune_docker_before_pull() {
+ if [ "${DEPLOY_PRUNE_BEFORE_PULL:-false}" != "true" ]; then
+ return 0
+ fi
+
+ local prune_until="${DEPLOY_PRUNE_UNTIL:-168h}"
+ echo "Pruning unused Docker images older than ${prune_until} before pulling release image." >&2
+ docker image prune -af --filter "until=${prune_until}" >&2 || true
+ docker builder prune -af --filter "until=${prune_until}" >&2 || true
+}
+
+check_docker_disk_space() {
+ local min_mb="${DEPLOY_MIN_FREE_MB:-2048}"
+ if [[ ! "$min_mb" =~ ^[0-9]+$ ]]; then
+ echo "DEPLOY_MIN_FREE_MB must be an integer MiB value, got: ${min_mb}" >&2
+ exit 2
+ fi
+
+ local min_kb=$((min_mb * 1024))
+ local failed=0
+ local path available_kb available_mb
+
+ while IFS= read -r path; do
+ available_kb="$(df -Pk "$path" | awk 'NR == 2 { print $4 }')"
+ if [ -z "$available_kb" ]; then
+ continue
+ fi
+
+ if [ "$available_kb" -lt "$min_kb" ]; then
+ available_mb=$((available_kb / 1024))
+ echo "Docker storage path ${path} has ${available_mb} MiB free; need at least ${min_mb} MiB before pulling release image." >&2
+ failed=1
+ fi
+ done < <(docker_disk_paths | awk '!seen[$0]++')
+
+ if [ "$failed" -ne 0 ]; then
+ docker system df >&2 || true
+ exit 1
+ fi
+}
+
+wait_service() {
+ local service="$1"
+ local timeout="${DEPLOY_WAIT_TIMEOUT_SECONDS:-300}"
+ local started_at
+ started_at="$(date +%s)"
+
+ while true; do
+ local update_state
+ update_state="$(docker service inspect --format '{{if .UpdateStatus}}{{.UpdateStatus.State}}{{end}}' "$service" 2>/dev/null || true)"
+ if [ "$update_state" = "rollback_started" ] || [ "$update_state" = "rollback_completed" ] || [ "$update_state" = "paused" ]; then
+ docker service ps --no-trunc "$service" >&2 || true
+ exit 1
+ fi
+
+ local desired running failed
+ desired="$(docker service inspect --format '{{if .Spec.Mode.Replicated}}{{.Spec.Mode.Replicated.Replicas}}{{else}}1{{end}}' "$service")"
+ running="$(docker service ps "$service" --filter desired-state=running --format '{{.CurrentState}}' | grep -c '^Running' || true)"
+ failed="$(docker service ps "$service" --no-trunc --format '{{.CurrentState}} {{.Error}}' | grep -Ec 'Rejected|Failed|No such image|unhealthy' || true)"
+
+ if [ "$failed" -gt 0 ]; then
+ docker service ps --no-trunc "$service" >&2 || true
+ exit 1
+ fi
+
+ if [ "$running" -ge "$desired" ]; then
+ log "service_converged"
+ return 0
+ fi
+
+ if [ "$(( $(date +%s) - started_at ))" -ge "$timeout" ]; then
+ docker service ps --no-trunc "$service" >&2 || true
+ echo "Timed out waiting for ${service} convergence" >&2
+ exit 1
+ fi
+
+ sleep 5
+ done
+}
+
+log deploy_start
+
+if [ -n "${REGISTRY:-}" ] && [ -n "${REGISTRY_USERNAME:-}" ] && [ -n "${REGISTRY_PASSWORD:-}" ]; then
+ printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY" --username "$REGISTRY_USERNAME" --password-stdin
+fi
+
+prune_docker_before_pull
+check_docker_disk_space
+
+docker image pull "$WEB_IMAGE"
+
+export ENVIRONMENT STACK_NAME RELEASE_ID COMMIT_SHA WEB_IMAGE PUBLIC_HOST TRAEFIK_NETWORK
+export REGION="${REGION:-}" TEAM_ID="${TEAM_ID:-}" WEB_REPLICAS="${WEB_REPLICAS:-1}"
+export WEB_PLACEMENT_CONSTRAINT="${WEB_PLACEMENT_CONSTRAINT:-node.role == manager}"
+export TRAEFIK_ENTRYPOINTS="${TRAEFIK_ENTRYPOINTS:-websecure}" TRAEFIK_TLS="${TRAEFIK_TLS:-true}"
+export VIDEOREG_DB_MEMORY_TTL_MS="${VIDEOREG_DB_MEMORY_TTL_MS:-3600000}"
+export VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS="${VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS:-3600}"
+
+docker stack deploy --with-registry-auth --prune -c "$REMOTE_DIR/docker-stack.yml" "$STACK_NAME"
+wait_service "${STACK_NAME}_web"
+
+log deploy_converged
diff --git a/deploy/remote/rollback-stack.sh b/deploy/remote/rollback-stack.sh
new file mode 100755
index 0000000..ee4082c
--- /dev/null
+++ b/deploy/remote/rollback-stack.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+require_var() {
+ local name="$1"
+ if [ -z "${!name:-}" ]; then
+ printf 'Required environment variable %s is missing\n' "$name" >&2
+ exit 2
+ fi
+}
+
+require_var ENVIRONMENT
+require_var STACK_NAME
+
+LOCK_FILE="${RELEASE_LOCK_FILE:-/tmp/${STACK_NAME}-${ENVIRONMENT}.lock}"
+exec 9>"$LOCK_FILE"
+flock -n 9 || {
+ echo "Another deployment operation is already running for ${STACK_NAME}/${ENVIRONMENT}" >&2
+ exit 75
+}
+
+printf '{"event":"rollback_start","environment":"%s","stack":"%s"}\n' "$ENVIRONMENT" "$STACK_NAME"
+docker service rollback "${STACK_NAME}_web" || true
+printf '{"event":"rollback_requested","environment":"%s","stack":"%s"}\n' "$ENVIRONMENT" "$STACK_NAME"
diff --git a/deploy/scripts/deploy-over-ssh.sh b/deploy/scripts/deploy-over-ssh.sh
new file mode 100755
index 0000000..37eaa0c
--- /dev/null
+++ b/deploy/scripts/deploy-over-ssh.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+require_var() {
+ local name="$1"
+ if [ -z "${!name:-}" ]; then
+ printf 'Required environment variable %s is missing\n' "$name" >&2
+ exit 2
+ fi
+}
+
+quote() {
+ printf '%q' "$1"
+}
+
+require_var SWARM_MANAGER_HOST
+require_var SWARM_SSH_USER
+require_var ENVIRONMENT
+require_var STACK_NAME
+require_var RELEASE_ID
+require_var COMMIT_SHA
+require_var WEB_IMAGE
+require_var PUBLIC_HOST
+require_var TRAEFIK_NETWORK
+
+SSH_PORT="${SWARM_SSH_PORT:-22}"
+SSH_TARGET="${SWARM_SSH_USER}@${SWARM_MANAGER_HOST}"
+REMOTE_DEPLOY_DIR="${REMOTE_DEPLOY_DIR:-/tmp/videoreg-deploy}"
+
+ssh -p "$SSH_PORT" "$SSH_TARGET" "mkdir -p $(quote "$REMOTE_DEPLOY_DIR")"
+scp -P "$SSH_PORT" deploy/swarm/docker-stack.yml "$SSH_TARGET:$REMOTE_DEPLOY_DIR/docker-stack.yml"
+scp -P "$SSH_PORT" deploy/remote/deploy-stack.sh "$SSH_TARGET:$REMOTE_DEPLOY_DIR/deploy-stack.sh"
+
+remote_vars=(
+ ENVIRONMENT STACK_NAME RELEASE_ID COMMIT_SHA WEB_IMAGE PUBLIC_HOST TRAEFIK_NETWORK
+ REGION TEAM_ID WEB_REPLICAS WEB_PLACEMENT_CONSTRAINT TRAEFIK_ENTRYPOINTS TRAEFIK_TLS REGISTRY REGISTRY_USERNAME REGISTRY_PASSWORD
+ RELEASE_LOCK_FILE REMOTE_DEPLOY_DIR DEPLOY_MIN_FREE_MB DEPLOY_PRUNE_BEFORE_PULL DEPLOY_PRUNE_UNTIL
+ DEPLOY_CLEANUP_FAILED_IMAGES VIDEOREG_DB_MEMORY_TTL_MS VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS
+)
+
+remote_command=""
+for name in "${remote_vars[@]}"; do
+ if [ -n "${!name:-}" ]; then
+ remote_command+="$name=$(quote "${!name}") "
+ fi
+done
+remote_command+="bash $(quote "$REMOTE_DEPLOY_DIR/deploy-stack.sh")"
+
+ssh -p "$SSH_PORT" "$SSH_TARGET" "$remote_command"
diff --git a/deploy/scripts/predeploy-check.mjs b/deploy/scripts/predeploy-check.mjs
new file mode 100644
index 0000000..d112a42
--- /dev/null
+++ b/deploy/scripts/predeploy-check.mjs
@@ -0,0 +1,32 @@
+const required = [
+ 'ENVIRONMENT',
+ 'STACK_NAME',
+ 'RELEASE_ID',
+ 'COMMIT_SHA',
+ 'WEB_IMAGE',
+ 'PUBLIC_HOST',
+ 'TRAEFIK_NETWORK',
+ 'SWARM_MANAGER_HOST',
+ 'SWARM_SSH_USER',
+];
+
+const missing = required.filter((name) => !process.env[name]);
+if (missing.length > 0) {
+ console.error(`Missing required deploy variables: ${missing.join(', ')}`);
+ process.exit(1);
+}
+
+if (!/@sha256:[a-f0-9]{64}$/i.test(process.env.WEB_IMAGE ?? '')) {
+ console.error(`WEB_IMAGE must be pinned by immutable digest, got: ${process.env.WEB_IMAGE}`);
+ process.exit(1);
+}
+
+console.log(
+ JSON.stringify({
+ event: 'predeploy_check_passed',
+ environment: process.env.ENVIRONMENT,
+ stack: process.env.STACK_NAME,
+ releaseId: process.env.RELEASE_ID,
+ commitSha: process.env.COMMIT_SHA,
+ }),
+);
diff --git a/deploy/scripts/rollback-over-ssh.sh b/deploy/scripts/rollback-over-ssh.sh
new file mode 100755
index 0000000..40e9872
--- /dev/null
+++ b/deploy/scripts/rollback-over-ssh.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+require_var() {
+ local name="$1"
+ if [ -z "${!name:-}" ]; then
+ printf 'Required environment variable %s is missing\n' "$name" >&2
+ exit 2
+ fi
+}
+
+quote() {
+ printf '%q' "$1"
+}
+
+require_var SWARM_MANAGER_HOST
+require_var SWARM_SSH_USER
+require_var ENVIRONMENT
+require_var STACK_NAME
+
+SSH_PORT="${SWARM_SSH_PORT:-22}"
+SSH_TARGET="${SWARM_SSH_USER}@${SWARM_MANAGER_HOST}"
+REMOTE_DEPLOY_DIR="${REMOTE_DEPLOY_DIR:-/tmp/videoreg-deploy}"
+
+ssh -p "$SSH_PORT" "$SSH_TARGET" "mkdir -p $(quote "$REMOTE_DEPLOY_DIR")"
+scp -P "$SSH_PORT" deploy/remote/rollback-stack.sh "$SSH_TARGET:$REMOTE_DEPLOY_DIR/rollback-stack.sh"
+
+remote_command="ENVIRONMENT=$(quote "$ENVIRONMENT") STACK_NAME=$(quote "$STACK_NAME") "
+if [ -n "${RELEASE_LOCK_FILE:-}" ]; then
+ remote_command+="RELEASE_LOCK_FILE=$(quote "$RELEASE_LOCK_FILE") "
+fi
+remote_command+="bash $(quote "$REMOTE_DEPLOY_DIR/rollback-stack.sh")"
+
+ssh -p "$SSH_PORT" "$SSH_TARGET" "$remote_command"
diff --git a/deploy/scripts/smoke-check.mjs b/deploy/scripts/smoke-check.mjs
new file mode 100644
index 0000000..0e65525
--- /dev/null
+++ b/deploy/scripts/smoke-check.mjs
@@ -0,0 +1,138 @@
+import { writeFileSync } from 'node:fs';
+
+const baseUrl = process.env.BASE_URL?.replace(/\/$/, '');
+const expectedReleaseId = process.env.EXPECTED_RELEASE_ID;
+const smokeWrite = process.env.SMOKE_WRITE === 'true';
+const outputPath = process.env.SMOKE_RESULT_PATH;
+const timeoutMs = Number(process.env.SMOKE_TIMEOUT_MS ?? 5000);
+const retryAttempts = Number(process.env.SMOKE_RETRY_ATTEMPTS ?? 12);
+const retryDelayMs = Number(process.env.SMOKE_RETRY_DELAY_MS ?? 5000);
+const retryableStatuses = new Set([404, 408, 425, 429, 500, 502, 503, 504]);
+
+if (!baseUrl) {
+ console.error('BASE_URL is required for smoke checks');
+ process.exit(2);
+}
+
+function sleep(ms) {
+ return new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+}
+
+async function requestOnce(path, options = {}) {
+ const response = await fetch(`${baseUrl}${path}`, {
+ ...options,
+ headers: {
+ 'content-type': 'application/json',
+ ...options.headers,
+ },
+ signal: AbortSignal.timeout(timeoutMs),
+ });
+ const text = await response.text();
+ let body;
+ try {
+ body = text ? JSON.parse(text) : undefined;
+ } catch {
+ body = text;
+ }
+
+ if (!response.ok) {
+ const error = new Error(`${path} returned ${response.status}: ${text}`);
+ error.status = response.status;
+ throw error;
+ }
+
+ return body;
+}
+
+async function request(path, options = {}) {
+ let lastError;
+
+ for (let attempt = 1; attempt <= retryAttempts; attempt += 1) {
+ try {
+ return await requestOnce(path, options);
+ } catch (error) {
+ lastError = error;
+ const status = error?.status;
+ const retryableStatus = typeof status === 'number' && retryableStatuses.has(status);
+ const retryableNetworkError = error?.name === 'TimeoutError' || error?.cause?.code;
+
+ if (attempt >= retryAttempts || (!retryableStatus && !retryableNetworkError)) {
+ throw error;
+ }
+
+ await sleep(retryDelayMs);
+ }
+ }
+
+ throw lastError;
+}
+
+function assertRelease(body) {
+ if (!expectedReleaseId) return;
+ const actual = body?.metadata?.releaseId;
+ if (actual !== expectedReleaseId) {
+ throw new Error(`service reports release ${actual}, expected ${expectedReleaseId}`);
+ }
+}
+
+async function run() {
+ const startedAt = Date.now();
+ const checks = [];
+
+ const live = await request('/health/live');
+ assertRelease(live);
+ checks.push({ name: 'web_live', status: 'ok' });
+
+ const ready = await request('/health/ready');
+ assertRelease(ready);
+ checks.push({ name: 'web_ready', status: 'ok' });
+
+ const registry = await request('/api/videos');
+ if (!registry.stats || !Array.isArray(registry.videos)) {
+ throw new Error('Video registry API returned an invalid shape');
+ }
+ checks.push({ name: 'registry_read', status: 'ok' });
+
+ if (smokeWrite) {
+ await request('/api/videos', {
+ method: 'POST',
+ body: JSON.stringify({
+ title: `Smoke video ${Date.now()}`,
+ owner: 'Deploy smoke',
+ sourceUrl: 'https://example.com/smoke',
+ status: 'draft',
+ }),
+ });
+ checks.push({ name: 'registry_write', status: 'ok' });
+ }
+
+ const result = {
+ status: 'ok',
+ baseUrl,
+ expectedReleaseId,
+ checks,
+ durationMs: Date.now() - startedAt,
+ };
+
+ if (outputPath) {
+ writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
+ }
+
+ console.log(JSON.stringify({ event: 'smoke_check_success', ...result }));
+}
+
+run().catch((error) => {
+ const result = {
+ status: 'error',
+ baseUrl,
+ expectedReleaseId,
+ error: error instanceof Error ? error.message : String(error),
+ };
+ if (outputPath) {
+ writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
+ }
+ console.error(JSON.stringify({ event: 'smoke_check_failure', ...result }));
+ process.exit(1);
+});
diff --git a/deploy/scripts/write-release-manifest.mjs b/deploy/scripts/write-release-manifest.mjs
new file mode 100644
index 0000000..9d3f7f4
--- /dev/null
+++ b/deploy/scripts/write-release-manifest.mjs
@@ -0,0 +1,30 @@
+import { readFileSync } from 'node:fs';
+
+function requireEnv(name) {
+ const value = process.env[name];
+ if (!value) {
+ throw new Error(`${name} is required`);
+ }
+ return value;
+}
+
+function readJson(path) {
+ return JSON.parse(readFileSync(path, 'utf8'));
+}
+
+const smoke = readJson(requireEnv('SMOKE_RESULT_PATH'));
+
+const manifest = {
+ commitSha: requireEnv('COMMIT_SHA'),
+ releaseId: requireEnv('RELEASE_ID'),
+ webImageDigest: requireEnv('WEB_IMAGE_DIGEST'),
+ deployTimestamp: new Date().toISOString(),
+ smokeResult: smoke,
+ workflowRunId: requireEnv('WORKFLOW_RUN_ID'),
+};
+
+if (!/@sha256:[a-f0-9]{64}$/i.test(manifest.webImageDigest)) {
+ throw new Error('webImageDigest must be digest-pinned');
+}
+
+console.log(JSON.stringify(manifest, null, 2));
diff --git a/deploy/swarm/docker-stack.yml b/deploy/swarm/docker-stack.yml
new file mode 100644
index 0000000..b21cf78
--- /dev/null
+++ b/deploy/swarm/docker-stack.yml
@@ -0,0 +1,58 @@
+version: "3.9"
+
+services:
+ web:
+ image: ${WEB_IMAGE}
+ environment:
+ NODE_ENV: production
+ APP_ENV: ${ENVIRONMENT}
+ PORT: "3000"
+ VIDEOREG_DB_FILE: /data/videoreg.json
+ VIDEOREG_DB_MEMORY_TTL_MS: ${VIDEOREG_DB_MEMORY_TTL_MS:-3600000}
+ VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS: ${VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS:-3600}
+ COMMIT_SHA: ${COMMIT_SHA}
+ RELEASE_ID: ${RELEASE_ID}
+ REGION: ${REGION:-}
+ TEAM_ID: ${TEAM_ID:-}
+ volumes:
+ - videoreg_data:/data
+ networks:
+ traefik: {}
+ healthcheck:
+ test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/health/ready >/dev/null || exit 1"]
+ interval: 10s
+ timeout: 3s
+ retries: 3
+ start_period: 20s
+ deploy:
+ replicas: ${WEB_REPLICAS:-1}
+ placement:
+ constraints:
+ - ${WEB_PLACEMENT_CONSTRAINT:-node.role == manager}
+ update_config:
+ order: start-first
+ parallelism: 1
+ failure_action: rollback
+ monitor: 45s
+ rollback_config:
+ order: start-first
+ parallelism: 1
+ monitor: 45s
+ restart_policy:
+ condition: on-failure
+ labels:
+ - "traefik.enable=true"
+ - "traefik.docker.network=${TRAEFIK_NETWORK}"
+ - "traefik.http.routers.${STACK_NAME}-web.rule=Host(`${PUBLIC_HOST}`) && PathPrefix(`/`)"
+ - "traefik.http.routers.${STACK_NAME}-web.entrypoints=${TRAEFIK_ENTRYPOINTS:-websecure}"
+ - "traefik.http.routers.${STACK_NAME}-web.tls=${TRAEFIK_TLS:-true}"
+ - "traefik.http.services.${STACK_NAME}-web.loadbalancer.server.port=3000"
+ stop_grace_period: 30s
+
+volumes:
+ videoreg_data:
+
+networks:
+ traefik:
+ external: true
+ name: ${TRAEFIK_NETWORK}
diff --git a/next-env.d.ts b/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/next.config.ts b/next.config.ts
new file mode 100644
index 0000000..c37b060
--- /dev/null
+++ b/next.config.ts
@@ -0,0 +1,10 @@
+import type { NextConfig } from 'next';
+
+const nextConfig: NextConfig = {
+ output: 'standalone',
+ turbopack: {
+ root: process.cwd(),
+ },
+};
+
+export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..8219c1a
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2587 @@
+{
+ "name": "videoreg",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "videoreg",
+ "version": "0.1.0",
+ "dependencies": {
+ "next": "16.2.4",
+ "react": "19.2.5",
+ "react-dom": "19.2.5"
+ },
+ "devDependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@tailwindcss/postcss": "4.2.4",
+ "@types/node": "24.10.1",
+ "@types/react": "19.2.14",
+ "@types/react-dom": "19.2.3",
+ "tailwindcss": "4.2.4",
+ "typescript": "5.9.3",
+ "vitest": "4.1.5"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "16.2.4",
+ "@next/swc-darwin-x64": "16.2.4",
+ "@next/swc-linux-arm64-gnu": "16.2.4",
+ "@next/swc-linux-arm64-musl": "16.2.4",
+ "@next/swc-linux-x64-gnu": "16.2.4",
+ "@next/swc-linux-x64-musl": "16.2.4",
+ "@next/swc-win32-arm64-msvc": "16.2.4",
+ "@next/swc-win32-x64-msvc": "16.2.4"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://npm.yandex-team.ru/@alloc%2fquick-lru/-/quick-lru-5.2.0.tgz?rbtorrent=e0ab50933e60c2c6f57f6cb5fadcde27aa0a1d3e",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://npm.yandex-team.ru/@emnapi%2fcore/-/core-1.10.0.tgz?rbtorrent=",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://npm.yandex-team.ru/@emnapi%2fruntime/-/runtime-1.10.0.tgz?rbtorrent=",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "devOptional": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://npm.yandex-team.ru/@emnapi%2fwasi-threads/-/wasi-threads-1.2.1.tgz?rbtorrent=",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@img/colour": {
+ "version": "1.1.0",
+ "resolved": "https://npm.yandex-team.ru/@img%2fcolour/-/colour-1.1.0.tgz?rbtorrent=",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.2.4",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.2.4",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
+ "version": "1.2.4",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
+ "version": "1.2.4",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.2.4",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.2.4",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.2.4",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-ppc64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-riscv64": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-riscv64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-wasm32/-/sharp-wasm32-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.7.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/@img%2fsharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://npm.yandex-team.ru/@jridgewell%2fgen-mapping/-/gen-mapping-0.3.13.tgz?rbtorrent=",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://npm.yandex-team.ru/@jridgewell%2fremapping/-/remapping-2.3.5.tgz?rbtorrent=",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://npm.yandex-team.ru/@jridgewell%2fresolve-uri/-/resolve-uri-3.1.2.tgz?rbtorrent=",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://npm.yandex-team.ru/@jridgewell%2fsourcemap-codec/-/sourcemap-codec-1.5.5.tgz?rbtorrent=",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://npm.yandex-team.ru/@jridgewell%2ftrace-mapping/-/trace-mapping-0.3.31.tgz?rbtorrent=",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.4",
+ "resolved": "https://npm.yandex-team.ru/@napi-rs%2fwasm-runtime/-/wasm-runtime-1.1.4.tgz?rbtorrent=",
+ "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@next/env": {
+ "version": "16.2.4",
+ "resolved": "https://npm.yandex-team.ru/@next%2fenv/-/env-16.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==",
+ "license": "MIT"
+ },
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "16.2.4",
+ "resolved": "https://npm.yandex-team.ru/@next%2fswc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "16.2.4",
+ "resolved": "https://npm.yandex-team.ru/@next%2fswc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "16.2.4",
+ "resolved": "https://npm.yandex-team.ru/@next%2fswc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "16.2.4",
+ "resolved": "https://npm.yandex-team.ru/@next%2fswc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-gnu": {
+ "version": "16.2.4",
+ "resolved": "https://npm.yandex-team.ru/@next%2fswc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-musl": {
+ "version": "16.2.4",
+ "resolved": "https://npm.yandex-team.ru/@next%2fswc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "16.2.4",
+ "resolved": "https://npm.yandex-team.ru/@next%2fswc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "16.2.4",
+ "resolved": "https://npm.yandex-team.ru/@next%2fswc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.127.0",
+ "resolved": "https://npm.yandex-team.ru/@oxc-project%2ftypes/-/types-0.127.0.tgz?rbtorrent=",
+ "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fbinding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/@rolldown%2fpluginutils/-/pluginutils-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://npm.yandex-team.ru/@standard-schema%2fspec/-/spec-1.1.0.tgz?rbtorrent=",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.15",
+ "resolved": "https://npm.yandex-team.ru/@swc%2fhelpers/-/helpers-0.5.15.tgz?rbtorrent=",
+ "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2fnode/-/node-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.19.0",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.2.4"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2foxide/-/oxide-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.2.4",
+ "@tailwindcss/oxide-darwin-arm64": "4.2.4",
+ "@tailwindcss/oxide-darwin-x64": "4.2.4",
+ "@tailwindcss/oxide-freebsd-x64": "4.2.4",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.2.4",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.2.4",
+ "@tailwindcss/oxide-linux-x64-musl": "4.2.4",
+ "@tailwindcss/oxide-wasm32-wasi": "4.2.4",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.2.4"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2foxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2foxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2foxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2foxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2foxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2foxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2foxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2foxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2foxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2foxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.8.1",
+ "@emnapi/runtime": "^1.8.1",
+ "@emnapi/wasi-threads": "^1.1.0",
+ "@napi-rs/wasm-runtime": "^1.1.1",
+ "@tybys/wasm-util": "^0.10.1",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2foxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2foxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/postcss": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/@tailwindcss%2fpostcss/-/postcss-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "@tailwindcss/node": "4.2.4",
+ "@tailwindcss/oxide": "4.2.4",
+ "postcss": "^8.5.6",
+ "tailwindcss": "4.2.4"
+ }
+ },
+ "node_modules/@tailwindcss/postcss/node_modules/postcss": {
+ "version": "8.5.13",
+ "resolved": "https://npm.yandex-team.ru/postcss/-/postcss-8.5.13.tgz?rbtorrent=",
+ "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://npm.yandex-team.ru/@tybys%2fwasm-util/-/wasm-util-0.10.1.tgz?rbtorrent=",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://npm.yandex-team.ru/@types%2fchai/-/chai-5.2.3.tgz?rbtorrent=",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://npm.yandex-team.ru/@types%2fdeep-eql/-/deep-eql-4.0.2.tgz?rbtorrent=86b21a6951ed2f4f095bfc44ad099f52e96a540f",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://npm.yandex-team.ru/@types%2festree/-/estree-1.0.8.tgz?rbtorrent=",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.10.1",
+ "resolved": "https://npm.yandex-team.ru/@types%2fnode/-/node-24.10.1.tgz?rbtorrent=",
+ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "resolved": "https://npm.yandex-team.ru/@types%2freact/-/react-19.2.14.tgz?rbtorrent=",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://npm.yandex-team.ru/@types%2freact-dom/-/react-dom-19.2.3.tgz?rbtorrent=",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.5",
+ "resolved": "https://npm.yandex-team.ru/@vitest%2fexpect/-/expect-4.1.5.tgz?rbtorrent=",
+ "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.5",
+ "@vitest/utils": "4.1.5",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.5",
+ "resolved": "https://npm.yandex-team.ru/@vitest%2fmocker/-/mocker-4.1.5.tgz?rbtorrent=",
+ "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.5",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.5",
+ "resolved": "https://npm.yandex-team.ru/@vitest%2fpretty-format/-/pretty-format-4.1.5.tgz?rbtorrent=",
+ "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.5",
+ "resolved": "https://npm.yandex-team.ru/@vitest%2frunner/-/runner-4.1.5.tgz?rbtorrent=",
+ "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.5",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.5",
+ "resolved": "https://npm.yandex-team.ru/@vitest%2fsnapshot/-/snapshot-4.1.5.tgz?rbtorrent=",
+ "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.5",
+ "@vitest/utils": "4.1.5",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.5",
+ "resolved": "https://npm.yandex-team.ru/@vitest%2fspy/-/spy-4.1.5.tgz?rbtorrent=",
+ "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.5",
+ "resolved": "https://npm.yandex-team.ru/@vitest%2futils/-/utils-4.1.5.tgz?rbtorrent=",
+ "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.5",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://npm.yandex-team.ru/assertion-error/-/assertion-error-2.0.1.tgz?rbtorrent=e44e2c582fce4c4572f92579fb25d52bc09ae144",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.25",
+ "resolved": "https://npm.yandex-team.ru/baseline-browser-mapping/-/baseline-browser-mapping-2.10.25.tgz?rbtorrent=",
+ "integrity": "sha512-QO/VHsXCQdnzADMfmkeOPvHdIAkoB7i0/rGjINPJEetLx75hNttVWGQ/jycHUDP9zZ9rupbm60WRxcwViB0MiA==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001791",
+ "resolved": "https://npm.yandex-team.ru/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz?rbtorrent=",
+ "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://npm.yandex-team.ru/chai/-/chai-6.2.2.tgz?rbtorrent=",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/client-only": {
+ "version": "0.0.1",
+ "resolved": "https://npm.yandex-team.ru/client-only/-/client-only-0.0.1.tgz?rbtorrent=36b9ca4d2b7c37c1c85069c7059d07dd68f648e4",
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://npm.yandex-team.ru/convert-source-map/-/convert-source-map-2.0.0.tgz?rbtorrent=8d247ba4886ecef994137a086a4d9817b81854db",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://npm.yandex-team.ru/csstype/-/csstype-3.2.3.tgz?rbtorrent=",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://npm.yandex-team.ru/detect-libc/-/detect-libc-2.1.2.tgz?rbtorrent=",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.21.0",
+ "resolved": "https://npm.yandex-team.ru/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz?rbtorrent=",
+ "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://npm.yandex-team.ru/es-module-lexer/-/es-module-lexer-2.1.0.tgz?rbtorrent=",
+ "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://npm.yandex-team.ru/estree-walker/-/estree-walker-3.0.3.tgz?rbtorrent=5c4a77cc1271e3f8348b7ca0183ec2896986ca35",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://npm.yandex-team.ru/expect-type/-/expect-type-1.3.0.tgz?rbtorrent=",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://npm.yandex-team.ru/fdir/-/fdir-6.5.0.tgz?rbtorrent=",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://npm.yandex-team.ru/fsevents/-/fsevents-2.3.3.tgz?rbtorrent=af48e653c1b69837ab1b663db0e3cccbadea9099",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://npm.yandex-team.ru/graceful-fs/-/graceful-fs-4.2.11.tgz?rbtorrent=251aa13f9ef4151aff391872f25b06d079421837",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://npm.yandex-team.ru/jiti/-/jiti-2.6.1.tgz?rbtorrent=",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://npm.yandex-team.ru/lightningcss/-/lightningcss-1.32.0.tgz?rbtorrent=",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://npm.yandex-team.ru/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz?rbtorrent=",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://npm.yandex-team.ru/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz?rbtorrent=",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://npm.yandex-team.ru/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz?rbtorrent=",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://npm.yandex-team.ru/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz?rbtorrent=",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://npm.yandex-team.ru/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz?rbtorrent=",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://npm.yandex-team.ru/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz?rbtorrent=",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://npm.yandex-team.ru/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz?rbtorrent=",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://npm.yandex-team.ru/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz?rbtorrent=",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://npm.yandex-team.ru/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz?rbtorrent=",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://npm.yandex-team.ru/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz?rbtorrent=",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://npm.yandex-team.ru/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz?rbtorrent=",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://npm.yandex-team.ru/magic-string/-/magic-string-0.30.21.tgz?rbtorrent=",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://npm.yandex-team.ru/nanoid/-/nanoid-3.3.12.tgz?rbtorrent=",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/next": {
+ "version": "16.2.4",
+ "resolved": "https://npm.yandex-team.ru/next/-/next-16.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@next/env": "16.2.4",
+ "@swc/helpers": "0.5.15",
+ "baseline-browser-mapping": "^2.9.19",
+ "caniuse-lite": "^1.0.30001579",
+ "postcss": "8.4.31",
+ "styled-jsx": "5.1.6"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "16.2.4",
+ "@next/swc-darwin-x64": "16.2.4",
+ "@next/swc-linux-arm64-gnu": "16.2.4",
+ "@next/swc-linux-arm64-musl": "16.2.4",
+ "@next/swc-linux-x64-gnu": "16.2.4",
+ "@next/swc-linux-x64-musl": "16.2.4",
+ "@next/swc-win32-arm64-msvc": "16.2.4",
+ "@next/swc-win32-x64-msvc": "16.2.4",
+ "sharp": "^0.34.5"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0",
+ "@playwright/test": "^1.51.1",
+ "babel-plugin-react-compiler": "*",
+ "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+ "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+ "sass": "^1.3.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@playwright/test": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.1.1",
+ "resolved": "https://npm.yandex-team.ru/obug/-/obug-2.1.1.tgz?rbtorrent=",
+ "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://npm.yandex-team.ru/pathe/-/pathe-2.0.3.tgz?rbtorrent=",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://npm.yandex-team.ru/picocolors/-/picocolors-1.1.1.tgz?rbtorrent=",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://npm.yandex-team.ru/picomatch/-/picomatch-4.0.4.tgz?rbtorrent=",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.31",
+ "resolved": "https://npm.yandex-team.ru/postcss/-/postcss-8.4.31.tgz?rbtorrent=217e20884324e43b47ce44404002fccc49a6ab44",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.5",
+ "resolved": "https://npm.yandex-team.ru/react/-/react-19.2.5.tgz?rbtorrent=",
+ "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.5",
+ "resolved": "https://npm.yandex-team.ru/react-dom/-/react-dom-19.2.5.tgz?rbtorrent=",
+ "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.5"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://npm.yandex-team.ru/rolldown/-/rolldown-1.0.0-rc.17.tgz?rbtorrent=",
+ "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.127.0",
+ "@rolldown/pluginutils": "1.0.0-rc.17"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-rc.17",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.17",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://npm.yandex-team.ru/scheduler/-/scheduler-0.27.0.tgz?rbtorrent=",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://npm.yandex-team.ru/semver/-/semver-7.7.4.tgz?rbtorrent=",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.34.5",
+ "resolved": "https://npm.yandex-team.ru/sharp/-/sharp-0.34.5.tgz?rbtorrent=",
+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@img/colour": "^1.0.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.7.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.34.5",
+ "@img/sharp-darwin-x64": "0.34.5",
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
+ "@img/sharp-libvips-linux-arm": "1.2.4",
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
+ "@img/sharp-libvips-linux-x64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
+ "@img/sharp-linux-arm": "0.34.5",
+ "@img/sharp-linux-arm64": "0.34.5",
+ "@img/sharp-linux-ppc64": "0.34.5",
+ "@img/sharp-linux-riscv64": "0.34.5",
+ "@img/sharp-linux-s390x": "0.34.5",
+ "@img/sharp-linux-x64": "0.34.5",
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
+ "@img/sharp-linuxmusl-x64": "0.34.5",
+ "@img/sharp-wasm32": "0.34.5",
+ "@img/sharp-win32-arm64": "0.34.5",
+ "@img/sharp-win32-ia32": "0.34.5",
+ "@img/sharp-win32-x64": "0.34.5"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://npm.yandex-team.ru/siginfo/-/siginfo-2.0.0.tgz?rbtorrent=c81f36a5482caeab614c579277ba7d9410225594",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://npm.yandex-team.ru/source-map-js/-/source-map-js-1.2.1.tgz?rbtorrent=",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://npm.yandex-team.ru/stackback/-/stackback-0.0.2.tgz?rbtorrent=bdf62245e16001accd64dbf9710961fd2a4ec3d6",
+ "integrity": "sha1-Gsig2Ug4SNFpXkGLbQMaPDzmjjs=",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "4.1.0",
+ "resolved": "https://npm.yandex-team.ru/std-env/-/std-env-4.1.0.tgz?rbtorrent=",
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/styled-jsx": {
+ "version": "5.1.6",
+ "resolved": "https://npm.yandex-team.ru/styled-jsx/-/styled-jsx-5.1.6.tgz?rbtorrent=",
+ "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
+ "license": "MIT",
+ "dependencies": {
+ "client-only": "0.0.1"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.2.4",
+ "resolved": "https://npm.yandex-team.ru/tailwindcss/-/tailwindcss-4.2.4.tgz?rbtorrent=",
+ "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://npm.yandex-team.ru/tapable/-/tapable-2.3.3.tgz?rbtorrent=",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://npm.yandex-team.ru/tinybench/-/tinybench-2.9.0.tgz?rbtorrent=",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.1.2",
+ "resolved": "https://npm.yandex-team.ru/tinyexec/-/tinyexec-1.1.2.tgz?rbtorrent=",
+ "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://npm.yandex-team.ru/tinyglobby/-/tinyglobby-0.2.16.tgz?rbtorrent=",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://npm.yandex-team.ru/tinyrainbow/-/tinyrainbow-3.1.0.tgz?rbtorrent=",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://npm.yandex-team.ru/tslib/-/tslib-2.8.1.tgz?rbtorrent=",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://npm.yandex-team.ru/typescript/-/typescript-5.9.3.tgz?rbtorrent=",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://npm.yandex-team.ru/undici-types/-/undici-types-7.16.0.tgz?rbtorrent=",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "8.0.10",
+ "resolved": "https://npm.yandex-team.ru/vite/-/vite-8.0.10.tgz?rbtorrent=",
+ "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.10",
+ "rolldown": "1.0.0-rc.17",
+ "tinyglobby": "^0.2.16"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/postcss": {
+ "version": "8.5.13",
+ "resolved": "https://npm.yandex-team.ru/postcss/-/postcss-8.5.13.tgz?rbtorrent=",
+ "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.5",
+ "resolved": "https://npm.yandex-team.ru/vitest/-/vitest-4.1.5.tgz?rbtorrent=",
+ "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.5",
+ "@vitest/mocker": "4.1.5",
+ "@vitest/pretty-format": "4.1.5",
+ "@vitest/runner": "4.1.5",
+ "@vitest/snapshot": "4.1.5",
+ "@vitest/spy": "4.1.5",
+ "@vitest/utils": "4.1.5",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.5",
+ "@vitest/browser-preview": "4.1.5",
+ "@vitest/browser-webdriverio": "4.1.5",
+ "@vitest/coverage-istanbul": "4.1.5",
+ "@vitest/coverage-v8": "4.1.5",
+ "@vitest/ui": "4.1.5",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://npm.yandex-team.ru/why-is-node-running/-/why-is-node-running-2.3.0.tgz?rbtorrent=",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ac719fa
--- /dev/null
+++ b/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "videoreg",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "typecheck": "tsc --noEmit",
+ "test": "vitest run",
+ "ci:quality": "npm run typecheck && npm test && npm run build"
+ },
+ "dependencies": {
+ "next": "16.2.4",
+ "react": "19.2.5",
+ "react-dom": "19.2.5"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "16.2.4",
+ "@next/swc-darwin-x64": "16.2.4",
+ "@next/swc-linux-arm64-gnu": "16.2.4",
+ "@next/swc-linux-arm64-musl": "16.2.4",
+ "@next/swc-linux-x64-gnu": "16.2.4",
+ "@next/swc-linux-x64-musl": "16.2.4",
+ "@next/swc-win32-arm64-msvc": "16.2.4",
+ "@next/swc-win32-x64-msvc": "16.2.4"
+ },
+ "devDependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@tailwindcss/postcss": "4.2.4",
+ "@types/node": "24.10.1",
+ "@types/react": "19.2.14",
+ "@types/react-dom": "19.2.3",
+ "tailwindcss": "4.2.4",
+ "typescript": "5.9.3",
+ "vitest": "4.1.5"
+ }
+}
diff --git a/public/.gitkeep b/public/.gitkeep
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/public/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/src/app/api/videos/route.ts b/src/app/api/videos/route.ts
new file mode 100644
index 0000000..60cf2b3
--- /dev/null
+++ b/src/app/api/videos/route.ts
@@ -0,0 +1,29 @@
+import { NextResponse } from 'next/server';
+import { revalidatePath, revalidateTag } from 'next/cache';
+import { getCachedVideoDashboard, VIDEO_COLLECTION_TAG } from '@/entities/video/api/videoCache';
+import { addVideo } from '@/entities/video/api/videoRepository';
+import { normalizeVideoDraft } from '@/entities/video/model/video';
+
+export const revalidate = 3600;
+
+export async function GET() {
+ const dashboard = await getCachedVideoDashboard();
+
+ return NextResponse.json(dashboard, {
+ headers: {
+ 'cache-control': 'public, max-age=60, s-maxage=3600, stale-while-revalidate=86400',
+ },
+ });
+}
+
+export async function POST(request: Request) {
+ const body = (await request.json().catch(() => undefined)) as
+ | { title?: string; owner?: string; sourceUrl?: string; status?: string }
+ | undefined;
+
+ const video = await addVideo(normalizeVideoDraft(body ?? {}));
+ revalidateTag(VIDEO_COLLECTION_TAG, { expire: 0 });
+ revalidatePath('/');
+
+ return NextResponse.json({ video }, { status: 201 });
+}
diff --git a/src/app/globals.css b/src/app/globals.css
new file mode 100644
index 0000000..cece514
--- /dev/null
+++ b/src/app/globals.css
@@ -0,0 +1,308 @@
+:root {
+ color-scheme: light;
+ --bg: #f6f6f3;
+ --panel: #ffffff;
+ --panel-muted: #efede7;
+ --text: #1f2528;
+ --muted: #667075;
+ --line: #d8d5cc;
+ --accent: #156f5b;
+ --accent-strong: #0c4f41;
+ --warning: #9a5c12;
+ --danger: #a33a3a;
+ --info: #345f9f;
+ --shadow: 0 10px 24px rgb(31 37 40 / 8%);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ min-height: 100%;
+}
+
+body {
+ margin: 0;
+ background: var(--bg);
+ color: var(--text);
+ font-family:
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+}
+
+button,
+input,
+select {
+ font: inherit;
+}
+
+.page-shell {
+ width: min(1180px, calc(100% - 32px));
+ margin: 0 auto;
+ padding: 24px 0 40px;
+}
+
+.topbar {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 24px;
+ border-bottom: 1px solid var(--line);
+ padding-bottom: 18px;
+}
+
+.eyebrow {
+ margin: 0 0 6px;
+ color: var(--accent);
+ font-size: 0.78rem;
+ font-weight: 700;
+ text-transform: uppercase;
+}
+
+h1,
+h2,
+h3,
+p {
+ overflow-wrap: anywhere;
+}
+
+h1 {
+ margin: 0;
+ font-size: 2rem;
+ line-height: 1.1;
+}
+
+h2 {
+ margin: 0;
+ font-size: 1.1rem;
+ line-height: 1.25;
+}
+
+h3 {
+ margin: 0;
+ font-size: 0.95rem;
+ line-height: 1.25;
+}
+
+.workbench {
+ display: grid;
+ grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
+ gap: 20px;
+ align-items: start;
+ padding-top: 22px;
+}
+
+.panel {
+ background: var(--panel);
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ box-shadow: var(--shadow);
+}
+
+.panel-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+ border-bottom: 1px solid var(--line);
+ padding: 16px;
+}
+
+.panel-body {
+ padding: 16px;
+}
+
+.form-grid {
+ display: grid;
+ gap: 14px;
+}
+
+.field {
+ display: grid;
+ gap: 6px;
+}
+
+.field span {
+ color: var(--muted);
+ font-size: 0.82rem;
+ font-weight: 650;
+}
+
+.field input,
+.field select {
+ min-height: 42px;
+ width: 100%;
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ background: #fff;
+ color: var(--text);
+ padding: 9px 10px;
+}
+
+.field input:focus,
+.field select:focus {
+ border-color: var(--accent);
+ outline: 3px solid rgb(21 111 91 / 16%);
+}
+
+.submit-button {
+ min-height: 42px;
+ border: 0;
+ border-radius: 6px;
+ background: var(--accent);
+ color: #fff;
+ cursor: pointer;
+ font-weight: 750;
+}
+
+.submit-button:hover {
+ background: var(--accent-strong);
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(110px, 1fr));
+ gap: 10px;
+}
+
+.metric {
+ min-height: 76px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: var(--panel-muted);
+ padding: 12px;
+}
+
+.metric span {
+ display: block;
+ color: var(--muted);
+ font-size: 0.76rem;
+ font-weight: 700;
+ text-transform: uppercase;
+}
+
+.metric strong {
+ display: block;
+ margin-top: 7px;
+ font-size: 1.45rem;
+}
+
+.video-list {
+ display: grid;
+ gap: 10px;
+ margin-top: 16px;
+}
+
+.video-card {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 14px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: #fff;
+ padding: 14px;
+}
+
+.video-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 8px;
+}
+
+.pill {
+ min-height: 24px;
+ border-radius: 999px;
+ background: var(--panel-muted);
+ color: var(--muted);
+ padding: 4px 9px;
+ font-size: 0.76rem;
+ font-weight: 700;
+}
+
+.pill.status-draft {
+ color: var(--warning);
+}
+
+.pill.status-published {
+ color: var(--accent);
+}
+
+.pill.status-archived {
+ color: var(--danger);
+}
+
+.cache-panel {
+ display: grid;
+ grid-template-columns: repeat(3, auto);
+ gap: 8px;
+ justify-content: end;
+}
+
+.cache-item {
+ min-height: 42px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: var(--panel);
+ padding: 7px 10px;
+}
+
+.cache-item span {
+ display: block;
+ color: var(--muted);
+ font-size: 0.7rem;
+ font-weight: 700;
+ text-transform: uppercase;
+}
+
+.cache-item strong {
+ display: block;
+ margin-top: 2px;
+ font-size: 0.84rem;
+}
+
+.empty-state {
+ border: 1px dashed var(--line);
+ border-radius: 8px;
+ color: var(--muted);
+ padding: 22px;
+ text-align: center;
+}
+
+@media (max-width: 900px) {
+ .topbar,
+ .workbench {
+ grid-template-columns: 1fr;
+ }
+
+ .topbar {
+ display: grid;
+ }
+
+ .cache-panel {
+ justify-content: stretch;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+
+ .stats-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 560px) {
+ .page-shell {
+ width: min(100% - 20px, 1180px);
+ padding-top: 16px;
+ }
+
+ h1 {
+ font-size: 1.55rem;
+ }
+
+ .cache-panel,
+ .stats-grid,
+ .video-card {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/src/app/health/live/route.ts b/src/app/health/live/route.ts
new file mode 100644
index 0000000..47b5e77
--- /dev/null
+++ b/src/app/health/live/route.ts
@@ -0,0 +1,14 @@
+import { NextResponse } from 'next/server';
+
+export const dynamic = 'force-dynamic';
+
+export function GET() {
+ return NextResponse.json({
+ status: 'ok',
+ service: 'videoreg-web',
+ metadata: {
+ commitSha: process.env.COMMIT_SHA ?? 'local',
+ releaseId: process.env.RELEASE_ID ?? 'local',
+ },
+ });
+}
diff --git a/src/app/health/ready/route.ts b/src/app/health/ready/route.ts
new file mode 100644
index 0000000..bbc00c7
--- /dev/null
+++ b/src/app/health/ready/route.ts
@@ -0,0 +1,17 @@
+import { NextResponse } from 'next/server';
+import { listVideos } from '@/entities/video/api/videoRepository';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ await listVideos();
+
+ return NextResponse.json({
+ status: 'ok',
+ service: 'videoreg-web',
+ metadata: {
+ commitSha: process.env.COMMIT_SHA ?? 'local',
+ releaseId: process.env.RELEASE_ID ?? 'local',
+ },
+ });
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 0000000..25d2435
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,19 @@
+import type { Metadata } from 'next';
+import './globals.css';
+
+export const metadata: Metadata = {
+ title: 'Video Registry',
+ description: 'Operational video registry with primitive storage and aggressive caching.',
+};
+
+type RootLayoutProps = {
+ children: React.ReactNode;
+};
+
+export default function RootLayout({ children }: RootLayoutProps) {
+ return (
+
+
{children}
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..c91fe5d
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,27 @@
+import { getCachedVideoDashboard } from '@/entities/video/api/videoCache';
+import { RegisterVideoForm } from '@/features/register-video/ui/RegisterVideoForm';
+import { CachePanel } from '@/widgets/cache-panel/ui/CachePanel';
+import { VideoDashboard } from '@/widgets/video-dashboard/ui/VideoDashboard';
+
+export const revalidate = 3600;
+
+export default async function Home() {
+ const dashboard = await getCachedVideoDashboard();
+
+ return (
+
+
+
+
videoreg.ru
+
Video Registry
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/entities/video/api/videoCache.ts b/src/entities/video/api/videoCache.ts
new file mode 100644
index 0000000..764d4e7
--- /dev/null
+++ b/src/entities/video/api/videoCache.ts
@@ -0,0 +1,29 @@
+import 'server-only';
+
+import { unstable_cache } from 'next/cache';
+import { DB_MEMORY_TTL_MS, NEXT_CACHE_REVALIDATE_SECONDS } from '@/shared/config/env';
+import { calculateVideoStats } from '@/entities/video/model/video';
+import { listVideos } from './videoRepository';
+
+export const VIDEO_COLLECTION_TAG = 'video-collection';
+
+export const getCachedVideoDashboard = unstable_cache(
+ async () => {
+ const videos = await listVideos();
+
+ return {
+ videos,
+ stats: calculateVideoStats(videos),
+ cache: {
+ generatedAt: new Date().toISOString(),
+ nextRevalidateSeconds: NEXT_CACHE_REVALIDATE_SECONDS,
+ memoryTtlMs: DB_MEMORY_TTL_MS,
+ },
+ };
+ },
+ ['video-dashboard-v1'],
+ {
+ revalidate: NEXT_CACHE_REVALIDATE_SECONDS,
+ tags: [VIDEO_COLLECTION_TAG],
+ },
+);
diff --git a/src/entities/video/api/videoRepository.ts b/src/entities/video/api/videoRepository.ts
new file mode 100644
index 0000000..4c4f288
--- /dev/null
+++ b/src/entities/video/api/videoRepository.ts
@@ -0,0 +1,21 @@
+import 'server-only';
+
+import { createVideo } from '@/entities/video/model/video';
+import type { VideoDraft } from '@/entities/video/model/types';
+import { readPrimitiveDb, writePrimitiveDb } from '@/shared/db/primitiveJsonDb';
+
+export async function listVideos() {
+ const db = await readPrimitiveDb();
+ return [...db.videos].sort((left, right) => right.createdAt.localeCompare(left.createdAt));
+}
+
+export async function addVideo(draft: VideoDraft) {
+ const video = createVideo(draft);
+
+ await writePrimitiveDb((current) => ({
+ ...current,
+ videos: [video, ...current.videos],
+ }));
+
+ return video;
+}
diff --git a/src/entities/video/model/types.ts b/src/entities/video/model/types.ts
new file mode 100644
index 0000000..ae46f2d
--- /dev/null
+++ b/src/entities/video/model/types.ts
@@ -0,0 +1,37 @@
+export const videoStatuses = ['draft', 'published', 'archived'] as const;
+
+export type VideoStatus = (typeof videoStatuses)[number];
+
+export type Video = {
+ id: string;
+ title: string;
+ owner: string;
+ sourceUrl: string;
+ status: VideoStatus;
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type VideoDraft = {
+ title: string;
+ owner: string;
+ sourceUrl: string;
+ status: VideoStatus;
+};
+
+export type VideoDashboardStats = {
+ total: number;
+ draft: number;
+ published: number;
+ archived: number;
+};
+
+export type VideoDashboardData = {
+ videos: Video[];
+ stats: VideoDashboardStats;
+ cache: {
+ generatedAt: string;
+ nextRevalidateSeconds: number;
+ memoryTtlMs: number;
+ };
+};
diff --git a/src/entities/video/model/video.test.ts b/src/entities/video/model/video.test.ts
new file mode 100644
index 0000000..60f81b6
--- /dev/null
+++ b/src/entities/video/model/video.test.ts
@@ -0,0 +1,45 @@
+import { describe, expect, it } from 'vitest';
+import { calculateVideoStats, normalizeVideoDraft } from './video';
+import type { Video } from './types';
+
+describe('video model', () => {
+ it('normalizes sparse form input', () => {
+ expect(
+ normalizeVideoDraft({
+ title: ' Demo reel ',
+ owner: '',
+ sourceUrl: 'not a url',
+ status: 'unexpected',
+ }),
+ ).toEqual({
+ title: 'Demo reel',
+ owner: 'Unassigned',
+ sourceUrl: 'https://example.com/video',
+ status: 'draft',
+ });
+ });
+
+ it('counts videos by status', () => {
+ const base = {
+ id: 'id',
+ title: 'Title',
+ owner: 'Ops',
+ sourceUrl: 'https://example.com/video',
+ createdAt: '2026-05-16T00:00:00.000Z',
+ updatedAt: '2026-05-16T00:00:00.000Z',
+ };
+ const videos: Video[] = [
+ { ...base, id: 'draft', status: 'draft' },
+ { ...base, id: 'published', status: 'published' },
+ { ...base, id: 'archived', status: 'archived' },
+ { ...base, id: 'published-2', status: 'published' },
+ ];
+
+ expect(calculateVideoStats(videos)).toEqual({
+ total: 4,
+ draft: 1,
+ published: 2,
+ archived: 1,
+ });
+ });
+});
diff --git a/src/entities/video/model/video.ts b/src/entities/video/model/video.ts
new file mode 100644
index 0000000..02b521b
--- /dev/null
+++ b/src/entities/video/model/video.ts
@@ -0,0 +1,68 @@
+import { videoStatuses, type Video, type VideoDashboardStats, type VideoDraft, type VideoStatus } from './types';
+
+const fallbackUrl = 'https://example.com/video';
+
+function normalizeText(value: string, fallback: string) {
+ const normalized = value.trim().replace(/\s+/g, ' ');
+ return normalized.length > 0 ? normalized : fallback;
+}
+
+function normalizeUrl(value: string) {
+ const normalized = value.trim();
+ if (normalized.length === 0) return fallbackUrl;
+
+ try {
+ return new URL(normalized).toString();
+ } catch {
+ return fallbackUrl;
+ }
+}
+
+export function isVideoStatus(value: string): value is VideoStatus {
+ return videoStatuses.includes(value as VideoStatus);
+}
+
+export function normalizeVideoDraft(input: {
+ title?: string;
+ owner?: string;
+ sourceUrl?: string;
+ status?: string;
+}): VideoDraft {
+ return {
+ title: normalizeText(input.title ?? '', 'Untitled video'),
+ owner: normalizeText(input.owner ?? '', 'Unassigned'),
+ sourceUrl: normalizeUrl(input.sourceUrl ?? ''),
+ status: input.status && isVideoStatus(input.status) ? input.status : 'draft',
+ };
+}
+
+export function createVideoId(now = new Date()) {
+ const suffix = Math.random().toString(36).slice(2, 8);
+ return `video_${now.getTime().toString(36)}_${suffix}`;
+}
+
+export function createVideo(draft: VideoDraft, now = new Date()): Video {
+ const timestamp = now.toISOString();
+ return {
+ ...draft,
+ id: createVideoId(now),
+ createdAt: timestamp,
+ updatedAt: timestamp,
+ };
+}
+
+export function calculateVideoStats(videos: Video[]): VideoDashboardStats {
+ return videos.reduce(
+ (stats, video) => ({
+ ...stats,
+ total: stats.total + 1,
+ [video.status]: stats[video.status] + 1,
+ }),
+ {
+ total: 0,
+ draft: 0,
+ published: 0,
+ archived: 0,
+ },
+ );
+}
diff --git a/src/features/register-video/model/actions.ts b/src/features/register-video/model/actions.ts
new file mode 100644
index 0000000..d16500d
--- /dev/null
+++ b/src/features/register-video/model/actions.ts
@@ -0,0 +1,20 @@
+'use server';
+
+import { revalidatePath, updateTag } from 'next/cache';
+import { VIDEO_COLLECTION_TAG } from '@/entities/video/api/videoCache';
+import { addVideo } from '@/entities/video/api/videoRepository';
+import { normalizeVideoDraft } from '@/entities/video/model/video';
+
+export async function registerVideoAction(formData: FormData) {
+ const draft = normalizeVideoDraft({
+ title: String(formData.get('title') ?? ''),
+ owner: String(formData.get('owner') ?? ''),
+ sourceUrl: String(formData.get('sourceUrl') ?? ''),
+ status: String(formData.get('status') ?? ''),
+ });
+
+ await addVideo(draft);
+
+ updateTag(VIDEO_COLLECTION_TAG);
+ revalidatePath('/');
+}
diff --git a/src/features/register-video/ui/RegisterVideoForm.tsx b/src/features/register-video/ui/RegisterVideoForm.tsx
new file mode 100644
index 0000000..6a64a69
--- /dev/null
+++ b/src/features/register-video/ui/RegisterVideoForm.tsx
@@ -0,0 +1,36 @@
+import { registerVideoAction } from '../model/actions';
+
+export function RegisterVideoForm() {
+ return (
+
+ );
+}
diff --git a/src/shared/config/env.ts b/src/shared/config/env.ts
new file mode 100644
index 0000000..da9cae5
--- /dev/null
+++ b/src/shared/config/env.ts
@@ -0,0 +1,5 @@
+export const APP_NAME = 'videoreg';
+export const DB_MEMORY_TTL_MS = Number(process.env.VIDEOREG_DB_MEMORY_TTL_MS ?? 60 * 60 * 1000);
+export const NEXT_CACHE_REVALIDATE_SECONDS = Number(
+ process.env.VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS ?? 60 * 60,
+);
diff --git a/src/shared/db/primitiveJsonDb.ts b/src/shared/db/primitiveJsonDb.ts
new file mode 100644
index 0000000..1db5662
--- /dev/null
+++ b/src/shared/db/primitiveJsonDb.ts
@@ -0,0 +1,134 @@
+import 'server-only';
+
+import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import { DB_MEMORY_TTL_MS } from '@/shared/config/env';
+import type { Video } from '@/entities/video/model/types';
+
+type PrimitiveDbSchema = {
+ videos: Video[];
+};
+
+type MemorySnapshot = {
+ loadedAt: number;
+ fileMtimeMs: number;
+ data: PrimitiveDbSchema;
+};
+
+const defaultSeed: PrimitiveDbSchema = {
+ videos: [
+ {
+ id: 'video_seed_launch',
+ title: 'Launch recording',
+ owner: 'Ops',
+ sourceUrl: 'https://example.com/launch-recording',
+ status: 'published',
+ createdAt: '2026-05-16T09:00:00.000Z',
+ updatedAt: '2026-05-16T09:00:00.000Z',
+ },
+ {
+ id: 'video_seed_cut',
+ title: 'Editing backlog',
+ owner: 'Media',
+ sourceUrl: 'https://example.com/editing-backlog',
+ status: 'draft',
+ createdAt: '2026-05-16T10:00:00.000Z',
+ updatedAt: '2026-05-16T10:00:00.000Z',
+ },
+ ],
+};
+
+let memorySnapshot: MemorySnapshot | undefined;
+let writeQueue = Promise.resolve();
+
+function getDbFilePath() {
+ if (process.env.VIDEOREG_DB_FILE) {
+ return process.env.VIDEOREG_DB_FILE;
+ }
+
+ return path.join(/* turbopackIgnore: true */ process.cwd(), 'data', 'videoreg.json');
+}
+
+async function ensureDbFile(filePath: string) {
+ await mkdir(path.dirname(filePath), { recursive: true });
+
+ try {
+ await stat(filePath);
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
+ throw error;
+ }
+ await writeFile(filePath, `${JSON.stringify(defaultSeed, null, 2)}\n`, 'utf8');
+ }
+}
+
+function parseSchema(raw: string): PrimitiveDbSchema {
+ const parsed = JSON.parse(raw) as Partial;
+ return {
+ videos: Array.isArray(parsed.videos) ? parsed.videos : [],
+ };
+}
+
+async function loadFromDisk(filePath: string): Promise {
+ await ensureDbFile(filePath);
+ const [fileStat, raw] = await Promise.all([stat(filePath), readFile(filePath, 'utf8')]);
+ const snapshot = {
+ loadedAt: Date.now(),
+ fileMtimeMs: fileStat.mtimeMs,
+ data: parseSchema(raw),
+ };
+ memorySnapshot = snapshot;
+ return snapshot;
+}
+
+export async function readPrimitiveDb() {
+ const filePath = getDbFilePath();
+ const now = Date.now();
+
+ if (memorySnapshot && now - memorySnapshot.loadedAt < DB_MEMORY_TTL_MS) {
+ return memorySnapshot.data;
+ }
+
+ const fileStat = await stat(filePath).catch((error: NodeJS.ErrnoException) => {
+ if (error.code === 'ENOENT') return undefined;
+ throw error;
+ });
+
+ if (memorySnapshot && fileStat && fileStat.mtimeMs === memorySnapshot.fileMtimeMs) {
+ memorySnapshot.loadedAt = now;
+ return memorySnapshot.data;
+ }
+
+ return (await loadFromDisk(filePath)).data;
+}
+
+export async function writePrimitiveDb(
+ updater: (current: PrimitiveDbSchema) => PrimitiveDbSchema | Promise,
+) {
+ const operation = writeQueue.then(async () => {
+ const filePath = getDbFilePath();
+ const current = await readPrimitiveDb();
+ const next = await updater(structuredClone(current));
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
+
+ await mkdir(path.dirname(filePath), { recursive: true });
+ await writeFile(temporaryPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
+ await rename(temporaryPath, filePath);
+
+ const fileStat = await stat(filePath);
+ memorySnapshot = {
+ loadedAt: Date.now(),
+ fileMtimeMs: fileStat.mtimeMs,
+ data: next,
+ };
+
+ return next;
+ });
+
+ writeQueue = operation.then(
+ () => undefined,
+ () => undefined,
+ );
+
+ return operation;
+}
diff --git a/src/shared/lib/date.ts b/src/shared/lib/date.ts
new file mode 100644
index 0000000..254775f
--- /dev/null
+++ b/src/shared/lib/date.ts
@@ -0,0 +1,6 @@
+export function formatDateTime(value: string) {
+ return new Intl.DateTimeFormat('en', {
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ }).format(new Date(value));
+}
diff --git a/src/widgets/cache-panel/ui/CachePanel.tsx b/src/widgets/cache-panel/ui/CachePanel.tsx
new file mode 100644
index 0000000..cac9ef1
--- /dev/null
+++ b/src/widgets/cache-panel/ui/CachePanel.tsx
@@ -0,0 +1,25 @@
+import type { VideoDashboardData } from '@/entities/video/model/types';
+import { formatDateTime } from '@/shared/lib/date';
+
+type CachePanelProps = {
+ metadata: VideoDashboardData['cache'];
+};
+
+export function CachePanel({ metadata }: CachePanelProps) {
+ return (
+
+ );
+}
diff --git a/src/widgets/video-dashboard/ui/VideoDashboard.tsx b/src/widgets/video-dashboard/ui/VideoDashboard.tsx
new file mode 100644
index 0000000..c017f98
--- /dev/null
+++ b/src/widgets/video-dashboard/ui/VideoDashboard.tsx
@@ -0,0 +1,57 @@
+import type { VideoDashboardData } from '@/entities/video/model/types';
+import { formatDateTime } from '@/shared/lib/date';
+
+type VideoDashboardProps = {
+ dashboard: VideoDashboardData;
+};
+
+export function VideoDashboard({ dashboard }: VideoDashboardProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {dashboard.videos.length > 0 ? (
+ dashboard.videos.map((video) => (
+
+
+
{video.title}
+
+ {video.status}
+ {video.owner}
+ {formatDateTime(video.createdAt)}
+
+
+
+ Source
+
+
+ ))
+ ) : (
+
No videos registered
+ )}
+
+
+
+ );
+}
+
+function Metric({ label, value }: { label: string; value: number }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
diff --git a/tasks/001-design-system.md b/tasks/001-design-system.md
new file mode 100644
index 0000000..791e86d
--- /dev/null
+++ b/tasks/001-design-system.md
@@ -0,0 +1,696 @@
+# Design System Task for Car Dashcam Website
+
+For: Designer + Frontend Design System Implementation Engineer
+Stack: React + Next.js + Tailwind CSS
+Architecture: Atomic Design + Design Tokens + Component-Driven Development
+
+---
+
+# Product Context
+
+The website is focused on car video registrators (dashcams), accessories, comparisons, reviews, installation guides, and purchase flows.
+
+The product positioning is:
+
+* technologically advanced
+* reliable in emergencies
+* automotive premium feel
+* minimal cognitive overload
+* optimized for mobile-first browsing
+* fast perceived loading
+* visually trustworthy for expensive electronics
+
+The visual language should combine:
+
+* modern automotive UI
+* Japanese/Korean automotive electronics aesthetics
+* dashboard HUD inspiration
+* premium marketplace clarity
+* clean editorial layouts
+
+Avoid:
+
+* aggressive gaming aesthetics
+* neon cyberpunk overload
+* cheap AliExpress visual style
+* overly corporate SaaS look
+* generic ecommerce templates
+
+The design should feel somewhere between:
+
+* Tesla interface
+* Blackmagic Design product pages
+* Sony camera product pages
+* premium automotive accessories catalog
+* modern editorial tech magazine
+
+---
+
+# Resolved Direction
+
+## Product Strategy
+
+Phase 1 should optimize for editorial trust and comparison-first decision making, not direct ecommerce conversion.
+
+The first proof artifact is a visual direction brief for a “Best Dashcams 2026” comparison page. Code implementation comes later, after the visual language is approved.
+
+The page should use:
+
+* one overall winner
+* use-case winners for different buyer needs
+* transparent scoring
+* editorial override notes when real-world testing contradicts a raw score
+
+Default score dimensions:
+
+* video
+* night
+* parking
+* reliability
+* usability
+* value
+
+Use the same six dimensions across categories, but allow category-specific weights.
+
+## Trust Model
+
+Product facts and claims must expose three public trust states:
+
+* verified
+* claimed
+* unknown
+
+The public UI should stay simple, but the internal data model should preserve verification method, for example:
+
+* measured in review
+* confirmed from firmware docs
+* confirmed from teardown
+* confirmed by multiple sources
+
+Trust labels should be quiet evidence chips with an icon and short label. Details such as method, source, and date should live in tooltips, detail panels, or source notes.
+
+## Visual Anchor
+
+The primary visual anchor is premium camera/product editorial, with automotive HUD cues used secondarily.
+
+Real product and media photography should dominate. UI, HUD, and camera-interface details should appear only as restrained overlays or supporting metadata.
+
+The default theme is light-first titanium brochure with dark product/media modules and full dark-mode parity.
+
+## First Viewport
+
+The first viewport of “Best Dashcams 2026” should prioritize trust and decision shortcuts:
+
+* overall pick
+* 3-5 use-case winners
+* methodology access
+* evidence labels
+* concise scoring context
+
+Avoid leading with a purely cinematic product hero if it delays comparison intent.
+
+## Comparison UX
+
+Mobile comparison should use stacked winner cards first, followed by a horizontal comparison table with sticky spec labels.
+
+The comparison table should lead with a compact hardware block before outcome rows.
+
+Core first rows:
+
+* sensor
+* resolution/FPS
+* bitrate
+* HDR
+* lens/FOV
+* storage
+* capacitor/battery
+* channels
+
+Then show verdict, score dimensions, known issues, price, and seller information.
+
+## Ecommerce Role
+
+Purchase CTAs should be secondary and evidence-adjacent. They should sit near price and seller information, but must not dominate winner cards or turn the page into an affiliate marketplace.
+
+## Visual Taste Guardrails
+
+Use muted, pastel, old-money color behavior. Avoid raw bright, screaming, neon, or cheap saturated colors.
+
+The lead accent family is desaturated petrol blue / steel blue.
+
+Keep semantic amber and recording red, but make them muted and low-chroma.
+
+Use tight geometry:
+
+* 4-8px radii
+* hairline borders
+* mostly flat surfaces
+* subtle tonal layering
+* restrained shadows
+
+Do not use:
+
+* neon accents
+* heavy glassmorphism
+* big rounded SaaS cards
+* stock gradients
+* loud affiliate blocks
+* crypto-dashboard styling
+* gaming HUD overload
+* generic ecommerce templates
+
+## Typography Decision
+
+Use an editorial sans for reading and UI, with technical monospace reserved for specs.
+
+Name concrete font candidates in the spec:
+
+* UI/editorial: Geist or Inter, with system sans fallback
+* technical fragments: IBM Plex Mono or Iosevka, with system monospace fallback
+
+---
+
+# Primary Goals
+
+## UX Goals
+
+1. Make product comparison extremely easy
+2. Build trust quickly
+3. Reduce noise in specification-heavy interfaces
+4. Support long-form technical reading
+5. Make mobile usage first-class
+6. Allow users to scan specs very fast
+7. Visually separate verified information from marketing claims
+8. Create reusable layouts for:
+
+ * reviews
+ * comparisons
+ * guides
+ * catalogs
+ * landing pages
+ * product detail pages
+
+---
+
+# Design Principles
+
+## 1. Information Density Without Chaos
+
+The system should support large amounts of technical data while remaining readable.
+
+Dashcam users compare:
+
+* bitrate
+* sensor model
+* HDR support
+* parking mode
+* CPL filters
+* heat resistance
+* night quality
+* codecs
+* Wi-Fi support
+* cloud integrations
+
+The UI must help scanning.
+
+---
+
+## 2. Automotive Precision
+
+Spacing, typography, borders, and interactions should feel engineered rather than decorative.
+
+Use:
+
+* precise spacing scales
+* subtle contrast
+* restrained shadows
+* clean hierarchy
+* predictable interaction patterns
+
+Avoid random rounded cards everywhere.
+
+---
+
+## 3. “Camera Hardware” Visual Language
+
+Visual references:
+
+* camera UI
+* automotive instrument clusters
+* photography gear
+* black anodized aluminum
+* dark matte surfaces
+* glass reflections
+* OLED dashboard typography
+
+---
+
+# Technical Requirements
+
+## Stack
+
+Implementation must use:
+
+* Tailwind CSS
+* CSS variables for tokens
+* React compound components
+* atomic architecture
+* accessibility-first approach
+* responsive-first approach
+* light-first default with full dark-mode parity
+* SSR-safe patterns for Next.js
+
+---
+
+# Atomic Architecture Structure
+
+## Atoms
+
+Examples:
+
+* Button
+* Badge
+* Chip
+* Tag
+* Typography
+* PriceLabel
+* RatingStars
+* Divider
+* Input
+* Select
+* Checkbox
+* IconButton
+* Tooltip
+* StatusIndicator
+* BatteryIndicator
+* SensorLabel
+* RecordingQualityBadge
+
+Requirements:
+
+* variants
+* sizes
+* semantic states
+* keyboard accessibility
+* token-driven styling only
+
+---
+
+## Molecules
+
+Examples:
+
+* ProductCard
+* ReviewCard
+* SpecRow
+* ComparisonRow
+* VideoPreview
+* PriceBlock
+* SellerBadge
+* NavigationItem
+* Breadcrumbs
+* SearchInput
+* FilterDropdown
+* StickyBuyPanel
+
+---
+
+## Organisms
+
+Examples:
+
+* ProductComparisonTable
+* ReviewHero
+* ProductGallery
+* SpecificationSection
+* VideoSamplesCarousel
+* ReviewSummary
+* InstallationGuide
+* ProductGrid
+* RecommendationRail
+* StickyMobileActions
+* ReviewTOC
+* Header
+* Footer
+
+---
+
+## Templates
+
+Examples:
+
+* Product Page
+* Review Article
+* Comparison Page
+* Category Catalog
+* Brand Page
+* Landing Page
+* Blog Article
+* Search Results
+* “Best Dashcams 2026” page
+
+---
+
+# Visual Direction
+
+## Color System
+
+### Base Palette
+
+Primary colors should feel:
+
+* automotive
+* metallic
+* technical
+* trustworthy
+
+Suggested direction:
+
+* graphite
+* gunmetal
+* matte black
+* titanium gray
+* cold white
+* deep navy accents
+
+Accent colors:
+
+* desaturated petrol blue / steel blue as the lead accent family
+* muted low-chroma amber for warnings, heat, caution, and parking-related states
+* muted low-chroma red for recording and critical states
+* success green only where meaningful
+
+Avoid rainbow interfaces and raw bright accents.
+
+---
+
+## Surface System
+
+Need layered surfaces:
+
+| Level | Usage |
+| -------------- | -------------------------- |
+| Surface 0 | Light titanium app background |
+| Surface 1 | Main editorial cards and comparison blocks |
+| Surface 2 | Elevated product/media modules |
+| Surface 3 | Interactive hover surfaces |
+| Surface Accent | Dark product/media blocks and premium highlights |
+
+Light mode is the primary default.
+
+Light mode should feel like:
+
+* premium automotive brochure
+* not plain white SaaS
+
+Dark mode must have full parity and should preserve the same hierarchy, contrast, and trust labeling.
+
+---
+
+# Typography System
+
+Typography is extremely important.
+
+The interface must support:
+
+* long-form reading
+* technical specs
+* comparisons
+* editorial content
+* ecommerce CTAs
+
+Recommended direction:
+
+Headings:
+
+* geometric sans
+* condensed possible for large titles
+
+Body:
+
+* highly readable
+* neutral
+* strong numeric clarity
+
+Monospace:
+
+* used for technical specs
+* bitrate
+* firmware
+* codecs
+
+Potential inspirations:
+
+* Inter
+* Geist
+* IBM Plex Sans
+* Iosevka for technical fragments
+
+Resolved direction:
+
+* UI/editorial: Geist or Inter, with system sans fallback
+* technical fragments: IBM Plex Mono or Iosevka, with system monospace fallback
+* monospace should be reserved for specs, firmware, codecs, bitrate, and compact technical metadata
+
+---
+
+# Layout System
+
+## Grid
+
+Need:
+
+* 4px spacing base
+* predictable responsive grid
+* container system
+* content widths for:
+
+ * article reading
+ * comparison tables
+ * dense catalogs
+
+---
+
+## Responsive Targets
+
+Must support:
+
+| Device | Priority |
+| ----------------- | -------- |
+| Mobile portrait | Highest |
+| Mobile landscape | High |
+| Tablet portrait | High |
+| Desktop ultrawide | Medium |
+
+Special focus:
+
+* sticky bottom actions on mobile
+* comparison tables on narrow screens
+* thumb-friendly navigation
+
+---
+
+# Motion System
+
+Motion should feel:
+
+* mechanical
+* precise
+* responsive
+
+Avoid:
+
+* playful animations
+* bouncing
+* exaggerated easing
+
+Allowed:
+
+* subtle fades
+* sliding panels
+* controlled transitions
+* progressive disclosure
+
+---
+
+# Icons
+
+Need unified icon language.
+
+Possible direction:
+
+* outline icons
+* automotive dashboard inspiration
+* camera hardware icons
+* signal indicators
+* storage/media indicators
+
+---
+
+# Content Components
+
+Must support:
+
+## Review Components
+
+* pros/cons
+* verdict block
+* sample footage embeds
+* firmware history
+* heat test results
+* night comparison sliders
+* speed readability examples
+
+---
+
+## Comparison Components
+
+Need highly optimized comparison UX:
+
+* sticky headers
+* horizontal scroll with snap
+* column pinning
+* spec highlighting
+* “best value” indicators
+
+Resolved Phase 1 behavior:
+
+* stacked winner cards first on mobile
+* horizontal comparison table after cards
+* sticky spec labels on narrow screens
+* first visible table rows should be the compact hardware block: sensor, resolution/FPS, bitrate, HDR, lens/FOV, storage, capacitor/battery, channels
+* outcome rows should follow the hardware block
+
+---
+
+# Ecommerce Components
+
+Need:
+
+* affiliate blocks
+* seller comparison
+* availability indicators
+* price history blocks
+* CTA hierarchy
+* coupon presentation
+
+Must avoid spammy affiliate aesthetics.
+
+---
+
+# Accessibility Requirements
+
+Minimum:
+
+* WCAG AA
+* keyboard navigation
+* visible focus states
+* semantic HTML
+* proper contrast
+* reduced motion support
+
+---
+
+# Tailwind Requirements
+
+## Must Produce
+
+1. Tailwind token structure
+2. Design token naming convention
+3. Component API documentation
+4. Variant strategy
+5. Responsive rules
+6. Dark/light theming system
+7. Atomic folder structure
+8. Figma component parity with React components
+
+---
+
+# Deliverables
+
+## Design Deliverables
+
+* Figma design system
+* typography scale
+* spacing system
+* icon system
+* color tokens
+* component library
+* responsive examples
+* interaction states
+* accessibility annotations
+
+---
+
+## Frontend Deliverables
+
+Implementation in:
+
+* React
+* Next.js
+* Tailwind CSS
+
+Including:
+
+* token-based theme
+* reusable atomic components
+* Storybook
+* dark/light support
+* mobile-first responsiveness
+* typed component APIs
+* zero hardcoded colors in components
+
+---
+
+# Important Constraints
+
+The final result must NOT look like:
+
+* generic Tailwind template
+* Bootstrap ecommerce
+* crypto dashboard
+* gaming UI
+* cheap gadget marketplace
+
+The interface should feel:
+
+* premium
+* calm
+* technical
+* engineered
+* trustworthy
+* fast
+
+Users should subconsciously feel:
+“This is a serious automotive electronics platform.”
+
+---
+
+# Suggested Initial Priority
+
+Phase 1:
+
+* typography
+* colors
+* spacing
+* visual direction brief for “Best Dashcams 2026”
+* buttons
+* cards
+* navigation
+* product cards
+* comparison table
+
+Phase 2:
+
+* article system
+* review system
+* ecommerce modules
+* filtering system
+
+Phase 3:
+
+* motion
+* advanced comparison UX
+* personalization
+* saved comparisons
+* dashboard-like features
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..752a139
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,41 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "es2022"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..1b0d162
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,14 @@
+import path from 'node:path';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ environment: 'node',
+ include: ['src/**/*.test.ts'],
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'src'),
+ },
+ },
+});