From ee2ee84de64de649b70c6e73e982cbe9dc9e6ae2 Mon Sep 17 00:00:00 2001 From: Chris Fuller Date: Thu, 22 Oct 2026 17:13:30 -0600 Subject: [PATCH] Initial commit: Everclone self-hosted Evernote alternative --- .dockerignore | 5 + .gitignore | 38 + Dockerfile | 33 + brave-extension/manifest.json | 18 + brave-extension/popup.html | 73 + brave-extension/popup.js | 183 + docker-compose.yml | 42 + entrypoint.sh | 8 + next.config.mjs | 17 + package-lock.json | 4954 +++++++++++++++++++++++ package.json | 64 + postcss.config.mjs | 9 + prisma/schema.prisma | 187 + public/icon.svg | 8 + public/manifest.json | 33 + src/app/api/email/route.ts | 77 + src/app/api/export/route.ts | 109 + src/app/api/extension/route.ts | 312 ++ src/app/api/health/route.ts | 14 + src/app/api/hooks/email/route.ts | 120 + src/app/api/hooks/note/route.ts | 378 ++ src/app/api/import/route.ts | 394 ++ src/app/api/keys/route.ts | 69 + src/app/api/notebooks/route.ts | 149 + src/app/api/notes/[id]/route.ts | 160 + src/app/api/notes/route.ts | 175 + src/app/api/saved-searches/route.ts | 50 + src/app/api/sidebar/route.ts | 93 + src/app/api/tags/route.ts | 79 + src/app/api/users/route.ts | 133 + src/app/globals.css | 195 + src/app/layout.tsx | 23 + src/app/page.tsx | 8 + src/components/EvercloneWorkspace.tsx | 3008 ++++++++++++++ src/components/HooksModal.tsx | 379 ++ src/components/ImportModal.tsx | 625 +++ src/components/PdfNoteComponents.tsx | 250 ++ src/components/RichNoteEditor.tsx | 571 +++ src/components/SettingsView.tsx | 1443 +++++++ src/components/ThumbnailEditorModal.tsx | 1122 +++++ src/lib/auth.ts | 57 + src/lib/email.ts | 331 ++ src/lib/importers.ts | 849 ++++ src/lib/pdfClient.ts | 130 + src/lib/prisma.ts | 156 + src/lib/search.ts | 298 ++ src/lib/storage.ts | 107 + src/types/everclone.ts | 110 + tailwind.config.ts | 26 + traefik-rule-example.yml | 29 + tsconfig.json | 27 + 51 files changed, 17728 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 brave-extension/manifest.json create mode 100644 brave-extension/popup.html create mode 100644 brave-extension/popup.js create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh create mode 100644 next.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 prisma/schema.prisma create mode 100644 public/icon.svg create mode 100644 public/manifest.json create mode 100644 src/app/api/email/route.ts create mode 100644 src/app/api/export/route.ts create mode 100644 src/app/api/extension/route.ts create mode 100644 src/app/api/health/route.ts create mode 100644 src/app/api/hooks/email/route.ts create mode 100644 src/app/api/hooks/note/route.ts create mode 100644 src/app/api/import/route.ts create mode 100644 src/app/api/keys/route.ts create mode 100644 src/app/api/notebooks/route.ts create mode 100644 src/app/api/notes/[id]/route.ts create mode 100644 src/app/api/notes/route.ts create mode 100644 src/app/api/saved-searches/route.ts create mode 100644 src/app/api/sidebar/route.ts create mode 100644 src/app/api/tags/route.ts create mode 100644 src/app/api/users/route.ts create mode 100644 src/app/globals.css create mode 100644 src/app/layout.tsx create mode 100644 src/app/page.tsx create mode 100644 src/components/EvercloneWorkspace.tsx create mode 100644 src/components/HooksModal.tsx create mode 100644 src/components/ImportModal.tsx create mode 100644 src/components/PdfNoteComponents.tsx create mode 100644 src/components/RichNoteEditor.tsx create mode 100644 src/components/SettingsView.tsx create mode 100644 src/components/ThumbnailEditorModal.tsx create mode 100644 src/lib/auth.ts create mode 100644 src/lib/email.ts create mode 100644 src/lib/importers.ts create mode 100644 src/lib/pdfClient.ts create mode 100644 src/lib/prisma.ts create mode 100644 src/lib/search.ts create mode 100644 src/lib/storage.ts create mode 100644 src/types/everclone.ts create mode 100644 tailwind.config.ts create mode 100644 traefik-rule-example.yml create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e9995a6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.next +.git +data +*.log diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2200de2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js build output +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# typescript +*.tsbuildinfo +next-env.d.ts + +# Everclone data & uploads +data/ +attachments/ +uploads/ +*.enex +*.zip diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c39180a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +FROM node:22-alpine AS base +RUN apk add --no-cache libc6-compat openssl + +FROM base AS deps +WORKDIR /app +COPY package.json ./ +RUN npm install --legacy-peer-deps + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npx prisma generate +RUN npm run build + +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/public ./public +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY entrypoint.sh ./entrypoint.sh +RUN sed -i 's/\r$//' ./entrypoint.sh && chmod +x ./entrypoint.sh && mkdir -p /data/attachments + +EXPOSE 3000 +ENTRYPOINT ["./entrypoint.sh"] diff --git a/brave-extension/manifest.json b/brave-extension/manifest.json new file mode 100644 index 0000000..7e03d79 --- /dev/null +++ b/brave-extension/manifest.json @@ -0,0 +1,18 @@ +{ + "manifest_version": 3, + "name": "Everclone Web Clipper", + "version": "1.0.0", + "description": "Clip articles (with all graphics), selections, and bookmarks from Brave/Chrome directly into Everclone.", + "permissions": [ + "activeTab", + "scripting", + "storage" + ], + "host_permissions": [ + "" + ], + "action": { + "default_popup": "popup.html", + "default_title": "Clip to Everclone" + } +} \ No newline at end of file diff --git a/brave-extension/popup.html b/brave-extension/popup.html new file mode 100644 index 0000000..c142d46 --- /dev/null +++ b/brave-extension/popup.html @@ -0,0 +1,73 @@ + + + + + + + +
+
+ + Everclone Web Clipper +
+ +
+ +
+ + + + + +
+ + + + + +
+ + + + +
+ + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/brave-extension/popup.js b/brave-extension/popup.js new file mode 100644 index 0000000..d7205ad --- /dev/null +++ b/brave-extension/popup.js @@ -0,0 +1,183 @@ +const DEFAULT_SERVER = "https://everclone.vloser.us"; +const DEFAULT_KEY = "evc_master_vloser_key_2026"; + +let currentMode = 'article'; +let activeTab = null; + +document.addEventListener('DOMContentLoaded', async () => { + const stored = await chrome.storage.local.get(['serverUrl', 'apiKey', 'lastNotebook']); + const serverUrl = (stored.serverUrl || DEFAULT_SERVER).replace(/\/$/, ''); + const apiKey = stored.apiKey || DEFAULT_KEY; + + document.getElementById('serverUrl').value = serverUrl; + document.getElementById('apiKey').value = apiKey; + + if (!apiKey || apiKey === '') { + document.getElementById('settingsPanel').style.display = 'block'; + } + + document.getElementById('toggleSettings').addEventListener('click', () => { + const p = document.getElementById('settingsPanel'); + p.style.display = p.style.display === 'block' ? 'none' : 'block'; + }); + + document.getElementById('saveSettingsBtn').addEventListener('click', async () => { + const s = document.getElementById('serverUrl').value.trim().replace(/\/$/, ''); + const k = document.getElementById('apiKey').value.trim(); + await chrome.storage.local.set({ serverUrl: s, apiKey: k }); + document.getElementById('settingsPanel').style.display = 'none'; + loadNotebooks(s, k); + }); + + document.querySelectorAll('.mode-btn').forEach((btn) => { + btn.addEventListener('click', () => { + document.querySelectorAll('.mode-btn').forEach((b) => b.classList.remove('active')); + btn.classList.add('active'); + currentMode = btn.getAttribute('data-mode'); + }); + }); + + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + activeTab = tab; + if (tab && tab.title) { + document.getElementById('noteTitle').value = tab.title; + } + + await loadNotebooks(serverUrl, apiKey, stored.lastNotebook); + + document.getElementById('clipBtn').addEventListener('click', handleClip); +}); + +async function loadNotebooks(serverUrl, apiKey, preferredNb) { + if (!serverUrl || !apiKey) return; + try { + const res = await fetch(`${serverUrl}/api/hooks/note?action=notebooks`, { + headers: { Authorization: `Bearer ${apiKey}` }, + }); + if (!res.ok) return; + const data = await res.json(); + if (Array.isArray(data.notebooks) && data.notebooks.length > 0) { + const sel = document.getElementById('notebookSelect'); + sel.innerHTML = ''; + data.notebooks.forEach((nb) => { + const opt = document.createElement('option'); + opt.value = nb.name; + opt.textContent = nb.name; + if (preferredNb ? nb.name === preferredNb : nb.name === 'Web Clips' || nb.isDefault) { + opt.selected = true; + } + sel.appendChild(opt); + }); + } + } catch { + // keep default options + } +} + +async function handleClip() { + const btn = document.getElementById('clipBtn'); + const statusBox = document.getElementById('statusBox'); + statusBox.className = 'status'; + btn.disabled = true; + btn.textContent = 'Clipping & saving graphics...'; + + try { + const serverUrl = document.getElementById('serverUrl').value.trim().replace(/\/$/, ''); + const apiKey = document.getElementById('apiKey').value.trim(); + const title = document.getElementById('noteTitle').value.trim(); + const notebook = document.getElementById('notebookSelect').value; + const tags = document.getElementById('noteTags').value.split(',').map((t) => t.trim()).filter(Boolean); + const comment = document.getElementById('noteComment').value.trim(); + + await chrome.storage.local.set({ serverUrl, apiKey, lastNotebook: notebook }); + + let captured = { pageHtml: '', selectionHtml: '', url: activeTab?.url || '' }; + if (activeTab?.id && currentMode !== 'bookmark') { + const results = await chrome.scripting.executeScript({ + target: { tabId: activeTab.id }, + func: () => { + // Clone document and resolve lazy-loaded images & relative URLs inside the live tab + const clone = document.documentElement.cloneNode(true); + clone.querySelectorAll('script, style, noscript, iframe').forEach((el) => el.remove()); + clone.querySelectorAll('img').forEach((img) => { + const best = + img.getAttribute('data-img-url') || + img.getAttribute('data-src') || + img.getAttribute('data-lazy-src') || + img.getAttribute('data-original') || + img.currentSrc || + img.src; + if (best) { + try { + img.setAttribute('src', new URL(best, location.href).href); + } catch {} + } + }); + let selHtml = ''; + const sel = window.getSelection(); + if (sel && sel.rangeCount > 0 && !sel.isCollapsed) { + const container = document.createElement('div'); + for (let i = 0; i < sel.rangeCount; i++) { + container.appendChild(sel.getRangeAt(i).cloneContents()); + } + container.querySelectorAll('img').forEach((img) => { + if (img.src) img.setAttribute('src', img.src); + }); + selHtml = container.innerHTML; + } + return { + pageHtml: '' + clone.innerHTML + '', + bodyHtml: clone.querySelector('body')?.innerHTML || '', + selectionHtml: selHtml, + url: location.href, + }; + }, + }); + if (results && results[0] && results[0].result) { + captured = results[0].result; + } + } + + const payload = { + title, + url: captured.url || activeTab?.url || '', + notebook, + tags, + comment, + clipMode: currentMode, + }; + + if (currentMode === 'article') { + payload.pageHtml = captured.pageHtml; + } else if (currentMode === 'selection') { + payload.html = captured.selectionHtml || '

No text selected on page.

'; + } else if (currentMode === 'full') { + payload.html = captured.bodyHtml || captured.pageHtml; + } else if (currentMode === 'bookmark') { + payload.html = `

Bookmarked: ${payload.url}

`; + } + + const res = await fetch(`${serverUrl}/api/hooks/note`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify(payload), + }); + + const data = await res.json(); + if (!res.ok) { + throw new Error(data.error || `HTTP ${res.status}`); + } + + statusBox.className = 'status ok'; + statusBox.innerHTML = `βœ“ Saved to ${notebook}! Open Note`; + btn.textContent = 'Clipped!'; + } catch (err) { + statusBox.className = 'status err'; + statusBox.textContent = 'Error: ' + (err.message || String(err)); + btn.disabled = false; + btn.textContent = 'Save Clip to Everclone'; + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..49a4176 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +services: + everclone-db: + image: postgres:17-alpine + container_name: everclone-db + restart: unless-stopped + environment: + POSTGRES_USER: everclone + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-everclone_secret_pw} + POSTGRES_DB: everclone + volumes: + - ./data/postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U everclone -d everclone"] + interval: 5s + timeout: 5s + retries: 10 + networks: + - default + + everclone: + build: . + image: everclone:latest + container_name: everclone + restart: unless-stopped + depends_on: + everclone-db: + condition: service_healthy + ports: + - "127.0.0.1:3088:3000" + environment: + DATABASE_URL: postgresql://everclone:${POSTGRES_PASSWORD:-everclone_secret_pw}@everclone-db:5432/everclone?schema=public + ATTACHMENTS_DIR: /data/attachments + EVERCLONE_MASTER_API_KEY: ${EVERCLONE_MASTER_API_KEY:-evc_master_vloser_key_2026} + volumes: + - ./data/attachments:/data/attachments + networks: + - default + - t3_proxy + +networks: + t3_proxy: + external: true diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..a9db91d --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +echo "Applying Prisma schema to PostgreSQL..." +node ./node_modules/prisma/build/index.js db push --skip-generate --accept-data-loss + +echo "Starting Everclone server on port 3000..." +exec node server.js diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..71ead4d --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,17 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', + experimental: { + serverActions: { + bodySizeLimit: '500mb', + }, + }, + eslint: { + ignoreDuringBuilds: true, + }, + typescript: { + ignoreBuildErrors: false, + }, +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9d7a2c4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4954 @@ +{ + "name": "everclone", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "everclone", + "version": "1.0.0", + "dependencies": { + "@mozilla/readability": "^0.5.0", + "@prisma/client": "^6.4.1", + "@tanstack/react-virtual": "^3.13.0", + "@tiptap/extension-color": "^2.11.5", + "@tiptap/extension-highlight": "^2.11.5", + "@tiptap/extension-image": "^2.11.5", + "@tiptap/extension-link": "^2.11.5", + "@tiptap/extension-placeholder": "^2.11.5", + "@tiptap/extension-subscript": "^2.11.5", + "@tiptap/extension-superscript": "^2.11.5", + "@tiptap/extension-table": "^2.11.5", + "@tiptap/extension-table-cell": "^2.11.5", + "@tiptap/extension-table-header": "^2.11.5", + "@tiptap/extension-table-row": "^2.11.5", + "@tiptap/extension-task-item": "^2.11.5", + "@tiptap/extension-task-list": "^2.11.5", + "@tiptap/extension-text-align": "^2.11.5", + "@tiptap/extension-text-style": "^2.11.5", + "@tiptap/extension-typography": "^2.11.5", + "@tiptap/extension-underline": "^2.11.5", + "@tiptap/pm": "^2.11.5", + "@tiptap/react": "^2.11.5", + "@tiptap/starter-kit": "^2.11.5", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "imapflow": "^1.0.171", + "jsdom": "^26.0.0", + "jszip": "^3.10.1", + "lucide-react": "^0.475.0", + "mailparser": "^3.7.2", + "marked": "^15.0.7", + "next": "^15.2.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "sax": "^1.4.1", + "turndown": "^7.2.0" + }, + "devDependencies": { + "@types/jsdom": "^21.1.7", + "@types/mailparser": "^3.4.6", + "@types/node": "^22.13.5", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@types/sax": "^1.2.7", + "@types/turndown": "^5.0.5", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.3", + "prisma": "^6.4.1", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "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.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "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.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "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.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "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.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "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.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "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.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "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.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "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.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "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.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mozilla/readability": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.5.0.tgz", + "integrity": "sha512-Z+CZ3QaosfFaTqvhQsIktyGrjFjSC0Fa4EMph4mqKnWhmyoGICsV/8QK+8HpXut6zV7zwfWwqDmEjtk1Qf6EgQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@next/env": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.28.tgz", + "integrity": "sha512-xxW5EBI8fMPuVTPBW7LL980IRbuDXlbkUAwp9DDhtb4Lnich3nJ13uYpvrX4VE74hWPf6AvHHAMmGCidcHjbfg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.28.tgz", + "integrity": "sha512-dPoQYXcfSIqx5o+TOXVXO7kONK5qbtNRDeiTfII0bT3puGIT6VF5e0QwmxNZfEZ5Zety5f3m9P/p4xqwfiLlPg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.28.tgz", + "integrity": "sha512-RgCJ7buBivTH+6sddqaVm+/v8kwMHlFkobvdt3DfETutws5HrxnToMIWSN9X3MZ3aAi7BbKrGZZWUDQLEycXqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.28.tgz", + "integrity": "sha512-OHXQe13olE59aKe++zHavGcJPh5SLHXi6Anret3BBeq1+qhSuOJDNAli/EDDfxs6UTqA4Qddo4uyYry2slyjiQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.28.tgz", + "integrity": "sha512-nTB7bB6vfn4ezCJNYP43ukt2o8L3+WxKF/sTcTxepdXsJh3SM9RhlU7wrZYbH7L4HO5M87dqDTwm8587vAkCiw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.28.tgz", + "integrity": "sha512-WXCfVI32rW3xCusoUX1RpZKtyChWU2Zwe27JUDmM3KrXwcDNc9vV4zEq0GKFAURnuQ/Ouo/XqPvB0yh445XZCg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.28.tgz", + "integrity": "sha512-Miq+NDb/HWsXuxseYxMM6fuEOQXgTziarkKIRpVKCn8W69cEfhQaQr+90pO+u4q2CbJR2wzXmUuCFyZaJs/Nng==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.28.tgz", + "integrity": "sha512-5LR79lemgDlgxvG7MQJ+zHCkVcrUoQuO7CiIvq3I58vNW4goSOE2b+19Z6FujRWMouvni4gvudYFuFuNxolGWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.28.tgz", + "integrity": "sha512-GNipvXUYROSTf4PXEyH95FrWImIB5zdaK6uEOg0RP1/X2drnvoMwiBkJ0VWdzX7w3iiBm3y5g2TxkKDGVTOQcg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@prisma/client": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", + "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", + "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.21.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", + "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", + "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.3", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", + "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", + "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3" + } + }, + "node_modules/@remirror/core-constants": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", + "license": "MIT" + }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.12.0.tgz", + "integrity": "sha512-oELmoyA6ML9jDRMV3kgcMQFKxUfBU0yFVn6yTctVaLT5ygXnxH52I3TZEgV9EhXJC68/uFvE5Daj1/25c0Xa/A==", + "license": "MIT", + "dependencies": { + "domelementtype": "~2.3.0", + "domhandler": "~5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + }, + "peerDependencies": { + "selderee": "~0.12.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.14.22", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.22.tgz", + "integrity": "sha512-zNiB9IF3prySTfAUrj9tPHjD/lonBYkfxrPP4v4ITUx+qwHajJR5cY/xirjvwc+KWkur10GX3TpsbKh7JkD8Lw==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.19.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.19.7", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.19.7.tgz", + "integrity": "sha512-gxy5SvH0mFRJ0YujtrflB/1HAu9Wn1KGvL0bjZfQkYVVMsIpQiwcf1mR06/ViN4NjllUb4HNCjqE9FGwgTvMCQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tiptap/core": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.3.tgz", + "integrity": "sha512-a5LfRbLpfaGI3hbL/LPHUYHI0I+FQHdSHsy8L4YnVIuu3hXcm3QZgkWbpEGf8hCz8krk6zEiu0+iFOjTySU2FA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.27.3.tgz", + "integrity": "sha512-NwK7FUFF0CKGJt/qNDR7plL/9vkJQBy8ziiyn8lk3j/j6tAFlCpgBMj420A1T26ItDpwOCyyaq+PMWHML+E4VQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.27.3.tgz", + "integrity": "sha512-d5tQLAl5nHNrHNkBEgHJ0GYQ52iAsq83fUiKnDxPWuF6leKMOtkUBt9rw918p/L6MPg/MHROZ3Qt4Q+lmVYbfQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.27.3.tgz", + "integrity": "sha512-08dt5pG9j3NTID1BnKT+FlfVgf16BPXEYtDCmPMLQq/jKHlOP49SRBAGhvYHZt8K7xkj0+JYhcgcJfytukTcKQ==", + "license": "MIT", + "dependencies": { + "tippy.js": "^6.3.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.27.3.tgz", + "integrity": "sha512-LEYkcuCCHYDm6NHWZRIl3Lpac1jXFhABZZpEj+V/ypPgPwKQSIzUMCySHXXNCUQOK3ipF0Z+O18kFnekag8ZLQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.27.3.tgz", + "integrity": "sha512-gETwHmS1NsQsBEeSOtd/erAQpiRRPyd2dg2HCrIASdiSApVbOfOnSfoHPrHDUp66S8WI5aR9j+d7e4Zz/gmorQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.27.3.tgz", + "integrity": "sha512-vzIt0orLs/59WlMOR8f9ULmwRLlOE1YB+zQuPdBVSRCOxVUYRUwtf7m1lOPNKKdorHadDpvWsT0hFMeNoyqQpQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-color": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-2.27.3.tgz", + "integrity": "sha512-+xHveJ8YfneusZIp87/8UW7VfdtCVgE5iN3JkTudC1TdYCnMqxbaIwQaGtsBH+UxinXhJSU9tcsD1P6QgKJSqg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/extension-text-style": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.27.3.tgz", + "integrity": "sha512-U10TnvBa6WTjBu64U4gn/HaxkBs/96q0U6mKH0PO9Ab0n3Bf/iN4HMtix2TEa3qWlJ8peVk9/RBwjyxNVNub4w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.27.3.tgz", + "integrity": "sha512-dIBb5AdfoNx2bCnwP3W1e3qez5s/XxrBBT3agtDt+VYOdx0Mi9MgT6GpGF+zfE2nnB7I0bYPfe2Z8cBqP9CU1w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.27.3.tgz", + "integrity": "sha512-4Be2efRPxLqZT0QB/IStVdVR1wP+FVPQxw/7qJ+xRk+m7EX45LVO0hvRdMK9aMO8Yd8JK2Wh9V3DbCN6cZ89vw==", + "license": "MIT", + "dependencies": { + "tippy.js": "^6.3.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.27.3.tgz", + "integrity": "sha512-GhK8Xl0jlJJkeJhdMfItCyselxGDt44UmaYTB3mSpH3dbvjT/NvYx7ABrPlRffogvRJ6eIi4ebU5FRuGYDmAuA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.27.3.tgz", + "integrity": "sha512-lvjELj0ZOgbgVNkUb3tQ0t96PLXybDTjL5nUyobkgIpkSUZTnuuXU9imiH7O/+frDiTCQ+Xp//VUB1XaMylfdg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.27.3.tgz", + "integrity": "sha512-VWcj9b5VAhMJSAeG8xZyzCaVCcwmBGJL2k77lE2ZQaCS/jSvKUTN5ntd/1vwLrmaduciB7FmdVgOmWFcODyX1Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-highlight": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.27.3.tgz", + "integrity": "sha512-NalKhoLGfXelD1P+2wThcpiorINDqeNz05brko28Tg+Ue9skOKO54Uh0XCoe93Mig/MvP/KQbi578LCYz8mqmw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-history": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.27.3.tgz", + "integrity": "sha512-btT7Teg9xtWbv9q6uc38aIQbZsbMhgNu1NGLf7nLf7Vqzm+GRCpRF5EzlF9RpwEPtC2j1bxrDzpy427Su1qF7w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.27.3.tgz", + "integrity": "sha512-G9ENe55ykj1dt5FCNAySQUh17ev5wvvGreSt3vvaCOHBPWaEdMARTGISn30X2fSPUVntTeYSB+oPP3XClunLEw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-image": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.27.3.tgz", + "integrity": "sha512-YCOxC+UOFisHVpowPc3UmUtJDV9tjKJLeHKef3DZ6h1ixOnp2LStuhy2ohcSjISg1mZlYrx+/GoYottjwj7pww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.27.3.tgz", + "integrity": "sha512-ycgP6h7QQ4WXojOlH7gQEWwzNEzkIkVzdfH6jAZtZ2nw71L5KO1t5kt6Iu382CmQa60skDTuErbfiBUx7a3e2A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.27.3.tgz", + "integrity": "sha512-0KF+iweOlegWjsRDJO7EZL1bmmcghNmjPbcSvvyeF19Lh7Nit44dYV0w1JCDvweRRAXECYFXpxZTcIvSf0fEng==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.27.3.tgz", + "integrity": "sha512-jWh5tZdNiZDx8X3jKV80EM6zMfUeRD3HKNVGcj5izqNncgH+/jJtF3hDmpP0nbFmhren8BQFo6W5f9L/GqtAVA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.27.3.tgz", + "integrity": "sha512-RvxSnE8rpiMSosD7ANCsyA+7XPEz0R6mDOFlORMqAf+NnPfCnH8dguVuKBGOdodPj69NcTSlA/6VBCS+3J0CLw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.27.3.tgz", + "integrity": "sha512-Nbevu3wZk212NDpp1FaN05UbTHA4NHP2B5bAmkP8z3I+/ITvEgp7p9vFqJxZhe7fmqCSFlzhKj8ibmBc+Iu4CQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-placeholder": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.27.3.tgz", + "integrity": "sha512-t71nSCWVLVnS4QxNmZsdaqgerypVxcYVSwBDCqNvPuGLgmj665gy6sOJ+MGjGgvAaFvKb+QiDS6Lb5GoP8iM3w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.27.3.tgz", + "integrity": "sha512-9Ax1UIRDOdPk/bGnHbKM39Bw+Zb0PV8IInlT6hr8K1or0wWZuiyE4zlKP3bhAB6IcDr4mJLJQ6TEmxKihivC5A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-subscript": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-subscript/-/extension-subscript-2.27.3.tgz", + "integrity": "sha512-x5Q1UVHLVP24wteh0xMSnwQ59WDKwNDOH0HzgbJ/UrDnN4f9H/haflQ4yZL0xdT2cYpvzJGPBugi2iKLcpeZjg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-superscript": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-superscript/-/extension-superscript-2.27.3.tgz", + "integrity": "sha512-dLZ9FBG6w5xlEeEsDR8T0nmI3ZDAYgLdkb/+bj8yOkzIUAbf9eDsE3HgqUrHPIm/7h9arJydnrdgOaxVDVlldg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-table": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.27.3.tgz", + "integrity": "sha512-xBDaT/ixkmrHKuCUlJNbzRolxjf0fFkHxybyeqfcGNDbVwOb6pOGZly6mGjBG1TXrx3U37y0jmFpwW6WU1zokA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-table-cell": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.27.3.tgz", + "integrity": "sha512-f697x9RDiva8EFZvLBxEp6Nx6sY5KRbHdNMhbCnqC1PrkRKNCTOib9ddWpqIERzs3HhmqZWL7XCPzvE/t8C+IQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-table-header": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.27.3.tgz", + "integrity": "sha512-x//GuYJlTzM3GavsDxBRDjGHMY9bbnmglQ0EAPVwQM5QHXpjwKzZN7wvDMM/RwxmxzYWd6Z+Fp7Zk8RYckpzhg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-table-row": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.27.3.tgz", + "integrity": "sha512-0ekXiw6aAESlcrKFmI90ar/j7XJmfdxeJEDeGZuMwOAQs+qq5RAB7ug5WPCZjG0Kqa+Kyxzb1jMw9pSkuTYp+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-task-item": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-2.27.3.tgz", + "integrity": "sha512-7ES+qMoMgjb2iXI8iRzgegMeM1pE4zQAekT/5lQyuTv6oEyZw5s3TUZWeTYCq5TI/MezYGu3X2sV5PQnfBAsdA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-task-list": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-2.27.3.tgz", + "integrity": "sha512-T8Y3S95ZzYlh82KGd0H81QOsJZVWweUVps6l7wkiOReNsJxhkb4qW/smNE0TeNhvbU307emiPL0CH5e+eHxjEg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.27.3.tgz", + "integrity": "sha512-9VnSK7qXUuZevNzE0FIymbc8BbY5X+0k4NxxO2dtPBOX4IoNHcWzmpLcuCggd3uN6XYLAXhDi/w2hkmBtig8Mw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-text-align": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.27.3.tgz", + "integrity": "sha512-1gUN+rdCkuYDePfY8/AixiftkXZDqDN1czZS+e5QIZCNcYnFPIIVu4qurE9kaE0NYo46/RPhPERFBQAbK+dWFw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-text-style": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.27.3.tgz", + "integrity": "sha512-Z4ZKju7vA2vUCeWVgEWERHvnln6XtgEMcb29xW8i25dSrw5Qefn/b+ym7JPkaMvjWbbQ+5VtECiaEYdL3rRH6Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-typography": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-2.27.3.tgz", + "integrity": "sha512-j86lW8JM8ZdgXMhmoEDUQEh/DYOuc65fz4zM9vcr/ih6TqsYbbrTX5QBrEwvj1vFXwzY6niKYn8QMVGV2vSycw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.27.3.tgz", + "integrity": "sha512-9qc6WtfF7j+gwlbz7ChmwjPqDRF8T2vbHMrZLoWERAez2pOi9VfRG7lj437sc1v9LPT31FlWyIkaTDTGqYMPWA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/pm": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.3.tgz", + "integrity": "sha512-E9mBCSwe8YdWXvpjRMqddx4Yd5lEj+EprNM+4J+MtBXPGFZCUAa90rkOKU4m+Pn7rd1N5s7wRsExqFco79twbg==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.23.0", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.4.1", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.42.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.27.3.tgz", + "integrity": "sha512-7kqQXcRv56QQjB6nnv4P3Lc0D5ChFxzvnWmyQ70eYXZ56pxPfAlgxdViVAb17X89bZ/ypWHbpWZRrd3gUydMwQ==", + "license": "MIT", + "dependencies": { + "@tiptap/extension-bubble-menu": "^2.27.3", + "@tiptap/extension-floating-menu": "^2.27.3", + "@types/use-sync-external-store": "^0.0.6", + "fast-deep-equal": "^3", + "use-sync-external-store": "^1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "2.27.3", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.27.3.tgz", + "integrity": "sha512-xqglSBavS4PDCWVFVruQPPRbmisWatXu7PZvzZSbR6HrLEX4ccRC3fNayRy9dUgDhuh2iLtEaXkVmy1Sd8DjBw==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^2.27.3", + "@tiptap/extension-blockquote": "^2.27.3", + "@tiptap/extension-bold": "^2.27.3", + "@tiptap/extension-bullet-list": "^2.27.3", + "@tiptap/extension-code": "^2.27.3", + "@tiptap/extension-code-block": "^2.27.3", + "@tiptap/extension-document": "^2.27.3", + "@tiptap/extension-dropcursor": "^2.27.3", + "@tiptap/extension-gapcursor": "^2.27.3", + "@tiptap/extension-hard-break": "^2.27.3", + "@tiptap/extension-heading": "^2.27.3", + "@tiptap/extension-history": "^2.27.3", + "@tiptap/extension-horizontal-rule": "^2.27.3", + "@tiptap/extension-italic": "^2.27.3", + "@tiptap/extension-list-item": "^2.27.3", + "@tiptap/extension-ordered-list": "^2.27.3", + "@tiptap/extension-paragraph": "^2.27.3", + "@tiptap/extension-strike": "^2.27.3", + "@tiptap/extension-text": "^2.27.3", + "@tiptap/extension-text-style": "^2.27.3", + "@tiptap/pm": "^2.27.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/mailparser": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@types/mailparser/-/mailparser-3.9.0.tgz", + "integrity": "sha512-YnhrRiRVDiEAphFvd72hKa1n8y2b9Q3rgSQiONZQyhQz67h5g4MZHNApET3XWXVEFdInWtHqDHKEwnyjxU3QVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "iconv-lite": "^0.6.3" + } + }, + "node_modules/@types/markdown-it": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/turndown": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.6.tgz", + "integrity": "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@zone-eu/mailsplit": { + "version": "5.4.16", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.16.tgz", + "integrity": "sha512-zQ9iXvlT3Wi/hazeC1MdI4rQc1UJwJ6IQ6QzSZ5KDxLZZWQSazWLOzImLFluXadKShJ9WJvI1xH+AyVS8b9azg==", + "license": "(MIT OR EUPL-1.1+)", + "dependencies": { + "libbase64": "1.3.0", + "libmime": "5.4.3", + "libqp": "2.1.1" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.1.tgz", + "integrity": "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.9", + "caniuse-lite": "^1.0.30001810", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.31.tgz", + "integrity": "sha512-b0k7wXlWjpuL/Uxb/pYlpjY0ej5BjAmurtPUk3eRGuloI4yjK32m8TiKUQ81l1ptcLjKS/KH2+uqIolZti1x4Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.29.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", + "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.29", + "caniuse-lite": "^1.0.30001816", + "electron-to-chromium": "^1.5.459", + "node-releases": "^2.0.59", + "update-browserslist-db": "^1.3.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001819", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", + "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==", + "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/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crelt": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", + "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/effect": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", + "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.463", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.463.tgz", + "integrity": "sha512-nVWNU6ltFjopvOdD44J4zoBkgO0M+WZiFKKFV7EjebOH9z0KNphk6bCEjplXtJofL21ARIuuwWOCsFkBVklm6w==", + "dev": true, + "license": "ISC" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encoding-japanese": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.3.0.tgz", + "integrity": "sha512-eQyh1vzHz13DUkZcJO+0IOAoKXRQwKV5IBffeuYsWZyRLGiSzfzXObCqWvqFXdX0UU8qOk+lBXbkUhMCpdJe4Q==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exsolve": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.3.tgz", + "integrity": "sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-to-text": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-10.0.1.tgz", + "integrity": "sha512-GiVhRI1BatGARSCmlXWNCjDT0cWrwBWoeduLoV0WSKAgaV/wa+hUWy5LiQLUs4UwiUrE52ZCMfBGiKD87TDPrg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "~0.12.0", + "deepmerge-ts": "^8.0.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^10.1.0", + "selderee": "~0.12.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, + "node_modules/html-to-text/node_modules/deepmerge-ts": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.2.tgz", + "integrity": "sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==", + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/deepmerge-ts" + } + ], + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imapflow": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/imapflow/-/imapflow-1.7.8.tgz", + "integrity": "sha512-dJoCIdZOJh26Rn2PdwEzwj0bRDgGBxxX38pio534FagIHVuR2l0SAfLr6sJo32YfuJHiSs/U2ntNDHnMg3/Hlg==", + "license": "MIT", + "dependencies": { + "@zone-eu/mailsplit": "5.4.16", + "encoding-japanese": "2.3.0", + "iconv-lite": "0.7.3", + "libbase64": "1.3.0", + "libmime": "5.4.3", + "libqp": "2.1.1", + "pino": "10.3.1", + "socks": "2.8.9" + } + }, + "node_modules/imapflow/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.7.4", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.4.tgz", + "integrity": "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jszip": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", + "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/leac": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.7.0.tgz", + "integrity": "sha512-qMrZeyEekgdRQ9o6a4NAB2EQZrv827GJdn1vnapwSJ90hWRB4TzUSunvacPkxQ2TnNqHNI1/zSt0hlo0crG8Jw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, + "node_modules/libbase64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz", + "integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==", + "license": "MIT" + }, + "node_modules/libmime": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.4.3.tgz", + "integrity": "sha512-di9BoDabBUMqjeD/wGj+hHpSgdqAph5ui7w6OdY6NpzU6O6VFLQsMOg9tqCjm/zf9OHzAM9EZxSOF7uIb8O8Hw==", + "license": "MIT", + "dependencies": { + "encoding-japanese": "2.3.0", + "iconv-lite": "0.7.3", + "libbase64": "1.3.0", + "libqp": "2.1.1" + } + }, + "node_modules/libmime/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/libqp": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz", + "integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==", + "license": "MIT" + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lilconfig": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.4.tgz", + "integrity": "sha512-zJZyBmQhArDRefuWTypZxwDpFDIYmrj8ypSBvwGD5P13Y95s6DjzKgu4DVdGbeQa5PDEsAOCAPNk4X+QOR006w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", + "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/lucide-react": { + "version": "0.475.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.475.0.tgz", + "integrity": "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/mailparser": { + "version": "3.12.4", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.12.4.tgz", + "integrity": "sha512-FdoMF+S89/AQaJHY7StwTfABPHZ8Vey+CDyUno5j+TO0jF0UxM+la4Pe9MeqKqBO4Y9ZMB2eomdyg5i+jzpfZw==", + "license": "MIT", + "dependencies": { + "@zone-eu/mailsplit": "5.4.29", + "he": "1.2.0", + "html-to-text": "10.0.1", + "libmime": "5.6.3", + "linkify-it": "5.0.2", + "nodemailer": "10.2.1", + "punycode.js": "2.3.1", + "tlds": "1.261.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/mailparser/node_modules/@zone-eu/mailsplit": { + "version": "5.4.29", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.29.tgz", + "integrity": "sha512-CCUNycmUjrV5M0wNf23waWAt9eOHczhjLbUQS7k0FNJvR0py9VSqHFt+tSl/D0+XI73VLG0U+PwYtq9R0zsm9Q==", + "license": "(MIT OR EUPL-1.1+)", + "dependencies": { + "libbase64": "1.4.1", + "libmime": "5.6.3", + "libqp": "3.0.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/mailparser/node_modules/encoding-japanese": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.4.0.tgz", + "integrity": "sha512-PE0KjlsDy9JSsoU4C2Q8NXb1kC4c+bZRac9ZvHXUE4q1Ht63gScKMdEy9jst9+Fi3M4eDkqzkEhBZvix4blP2A==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/mailparser/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mailparser/node_modules/libbase64": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.4.1.tgz", + "integrity": "sha512-Nyh3H90B3gRtrcxIe56Ayy7635J0jmh6AbxbQgQae8wrSPQMH5wFqJChiMTB13yKG512L3YspbA5DzumhFTITA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/mailparser/node_modules/libmime": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.6.3.tgz", + "integrity": "sha512-cuTUW/Jybvh5O4/U9p61gyutHOjDtZyGQYuY99+bfnaFwqcDf2Nb+R6hMHY6DDen2H9wLKQSMpofWUgn2TI5JQ==", + "license": "MIT", + "dependencies": { + "encoding-japanese": "2.4.0", + "iconv-lite": "0.7.3", + "libbase64": "1.4.1", + "libqp": "3.0.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/mailparser/node_modules/libqp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-3.0.1.tgz", + "integrity": "sha512-7QZxpPX25pga1WiMueduWxmrHMVBFrZPH6ZUaozGYa/H98JH6NVBSET3qVw+4NxzoraIW7rJ81YxSU9IalvCpg==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/markdown-it": { + "version": "14.3.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.3.tgz", + "integrity": "sha512-L//tDAkjTjJCU6pBUSBTgs3Kg4InVaZuUZog/b6V12UKhcDQ7S78877ibbCanRExwPuYP1CZD7Gd2dxSy1GXRg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "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": "15.5.28", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.28.tgz", + "integrity": "sha512-JjhLBBY1PNnuwPtcZ7t8yxVeJVYvD2Jyp633J+gRCNbzSt7N0E+xqCf6uQiff3+3qmUkwcF/ksfSsK1xsZ76Tw==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.28", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.28", + "@next/swc-darwin-x64": "15.5.28", + "@next/swc-linux-arm64-gnu": "15.5.28", + "@next/swc-linux-arm64-musl": "15.5.28", + "@next/swc-linux-x64-gnu": "15.5.28", + "@next/swc-linux-x64-musl": "15.5.28", + "@next/swc-win32-arm64-msvc": "15.5.28", + "@next/swc-win32-x64-msvc": "15.5.28", + "sharp": "^0.34.3 || ^0.35.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/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "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/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.60", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz", + "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nodemailer": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.2.1.tgz", + "integrity": "sha512-ZrDNsPFoDMwefNs7sJwrYiEJ/CG4ClJWDZv05iNJsEBWa8GRDry1QJHTJxsuTAY9eDDKjX4k+q3SxslXJOao7w==", + "license": "MIT-0", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.28.tgz", + "integrity": "sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==", + "license": "MIT" + }, + "node_modules/nypm": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.11.tgz", + "integrity": "sha512-wrVBTLWDu7nTUKGWplQebWOXWnhaIwyKQhBxobH/CGxJHoVw27r9vG5ba7kdZnxG+f4rC6dZd6m2oHmpkOcO7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.3.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/ohash": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseley": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.13.1.tgz", + "integrity": "sha512-uNBJZzmb60l6p6VWLTmevizNAGnE0xoSf1n0B4q3ntegDNzcS68NRCcBDZTcyXHxt2XhBChsCuqj4M+nChvE/A==", + "license": "MIT", + "dependencies": { + "leac": "^0.7.0", + "peberminta": "^0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/peberminta": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.10.0.tgz", + "integrity": "sha512-80B2AsU+I4Qdb0ZAPSfe9UwvGzwkM37IKIFEvdS3D/3Ndgv2bsuJ0bfG1+iEYO+l7Gfd4EUJmuRyq7efLgRMzQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.4.0.tgz", + "integrity": "sha512-07Fys7iVx0X6TcviMOgogM1Lk33ZyqOezMnU79S+kffWqwolG/5W9V8EgQSeYCIfFTYVgssRbnH630Oa2+soBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.3.1", + "exsolve": "^1.1.3", + "pathe": "^2.0.3" + } + }, + "node_modules/pkg-types/node_modules/confbox": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", + "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "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.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.5.tgz", + "integrity": "sha512-TEHFzb5KRDKaOgJ4TYE+0jyJtFbjLvkdVOu0fZ2b6MTWzoqZd4Le+61aEppWkvTlYePgOX4ATL1wsAIPMLTw6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prisma": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", + "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.3", + "@prisma/engines": "6.19.3" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.1.tgz", + "integrity": "sha512-oCkNVj4OKnKEcocVBcDfiIugN1/cE6rgfT3nCtzeqZPA8+rIxMaQ0xXzIkGP8Gj2qUaP1UR7uhJ1VD8lUCpzCg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/prosemirror-changeset": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.4.tgz", + "integrity": "sha512-Gyuz9jYdrAeA9I7AholyOes9KUmv/lEwsC5O9s5xF5VB3EgTPsXMGUxz4+H1B+wKVPdZbvRcg1Cp3x8qX+8ofA==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz", + "integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.4.tgz", + "integrity": "sha512-VmKxhcA6a+h5oPSYmfF1ZNeQqPbyYaIxqZsKqeYCyTlPtrjUgNaHL3P8vVaO19oqAV0zuaM/axkowZHrB1P3Tg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.1.tgz", + "integrity": "sha512-onlwnqKvgtFg6srZOmDOAhlAy1FIHQpOExORybX0S+DvJepE5tPALrB9iGet/2sqqqrelSnB10OoiOJqAvu2DA==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.9.tgz", + "integrity": "sha512-qAI3I9YdtjWHjTCBErc08ivr24KSrUjcTh5dYncCxFM+JS02kT0iHeWYjO35xeTPwt5t8rhFrelewXhXz16WZQ==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-menu": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.2.tgz", + "integrity": "sha512-6VgUJTYod0nMBlCaYJGhXGLu7Gt4AvcwcOq0YfJCY/6Uh+3S7UsWhpy6rJFCBFOmonq1hD8KyWOtZhkppd4YPg==", + "license": "MIT", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.12", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.12.tgz", + "integrity": "sha512-Ue2gTmXMa7EhpLNhC7J+h4+ykD8ha12K6rrZFFKKJHBForfIStw5gJ6Zrf1mqqAa9NmpmEB4wp9bKmA4eUVjWg==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.5.tgz", + "integrity": "sha512-McBWhfgOfThyu3Nltbs8SOUbM4t/h0pxteibaaHd1QR4S+F1t0u7M1ETaCE6/cveFoF+2fowTIBNkJOyeZQjNg==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "license": "MIT", + "dependencies": { + "@remirror/core-constants": "3.0.0", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.4.tgz", + "integrity": "sha512-2/WuPum3pn+O/Vi2vzpi8210ICUe2kIPXdLYjQ09Pv5DtT5bcKWMJhAKPBwvpPjpp5iOYUVwVEr0gOM1BY1Mgw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.42.7", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.7.tgz", + "integrity": "sha512-aTIAUN4zdD+VS7gsv9Po9Sty81B6yl6D3HggCosry9DLYTmVo4omjcqICDIOPnPJmskRUsqLInxBjaH7I+BQ4Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.12", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.1" + } + }, + "node_modules/read-cache": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", + "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/resolve": { + "version": "1.22.13", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.13.tgz", + "integrity": "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.17.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/selderee": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.12.0.tgz", + "integrity": "sha512-b1YMh3+DHZp59DLna3qVwQ5iOla/nrI6mLBNW02XxU77M3046Df6VLkoaJyFz20VsGIG5kkp+FK0kg4K4HnUFw==", + "license": "MIT", + "dependencies": { + "parseley": "~0.13.1" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "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/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.9.0" + } + }, + "node_modules/tlds": { + "version": "1.261.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", + "license": "MIT", + "bin": { + "tlds": "bin.js" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/turndown": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz", + "integrity": "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==", + "license": "MIT", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + }, + "engines": { + "node": ">=18", + "npm": ">=9" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5d75889 --- /dev/null +++ b/package.json @@ -0,0 +1,64 @@ +{ + "name": "everclone", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev -p 3000", + "build": "prisma generate && next build", + "start": "next start -p 3000", + "db:push": "prisma db push", + "db:generate": "prisma generate" + }, + "dependencies": { + "@mozilla/readability": "^0.5.0", + "@prisma/client": "^6.4.1", + "@tanstack/react-virtual": "^3.13.0", + "@tiptap/extension-color": "^2.11.5", + "@tiptap/extension-highlight": "^2.11.5", + "@tiptap/extension-image": "^2.11.5", + "@tiptap/extension-link": "^2.11.5", + "@tiptap/extension-placeholder": "^2.11.5", + "@tiptap/extension-subscript": "^2.11.5", + "@tiptap/extension-superscript": "^2.11.5", + "@tiptap/extension-table": "^2.11.5", + "@tiptap/extension-table-cell": "^2.11.5", + "@tiptap/extension-table-header": "^2.11.5", + "@tiptap/extension-table-row": "^2.11.5", + "@tiptap/extension-task-item": "^2.11.5", + "@tiptap/extension-task-list": "^2.11.5", + "@tiptap/extension-text-align": "^2.11.5", + "@tiptap/extension-text-style": "^2.11.5", + "@tiptap/extension-typography": "^2.11.5", + "@tiptap/extension-underline": "^2.11.5", + "@tiptap/pm": "^2.11.5", + "@tiptap/react": "^2.11.5", + "@tiptap/starter-kit": "^2.11.5", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "imapflow": "^1.0.171", + "jsdom": "^26.0.0", + "jszip": "^3.10.1", + "lucide-react": "^0.475.0", + "mailparser": "^3.7.2", + "marked": "^15.0.7", + "next": "^15.2.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "sax": "^1.4.1", + "turndown": "^7.2.0" + }, + "devDependencies": { + "@types/jsdom": "^21.1.7", + "@types/mailparser": "^3.4.6", + "@types/node": "^22.13.5", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@types/sax": "^1.2.7", + "@types/turndown": "^5.0.5", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.3", + "prisma": "^6.4.1", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..2ef30fc --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,9 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + +export default config; diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..422813d --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,187 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(cuid()) + email String @unique + name String + avatarColor String @default("#00a82d") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model NotebookStack { + id String @id @default(cuid()) + userId String @default("default") + name String + sortOrder Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + notebooks Notebook[] + + @@unique([userId, name]) + @@index([userId]) +} + +model Notebook { + id String @id @default(cuid()) + userId String @default("default") + name String + stackId String? + stack NotebookStack? @relation(fields: [stackId], references: [id], onDelete: SetNull) + isDefault Boolean @default(false) + isShortcut Boolean @default(false) + sortOrder Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + notes Note[] + + @@unique([userId, name]) + @@index([userId]) + @@index([stackId]) +} + +model Note { + id String @id @default(cuid()) + userId String @default("default") + title String @default("Untitled Note") + contentHtml String @default("") + contentText String @default("") + excerpt String @default("") + notebookId String + notebook Notebook @relation(fields: [notebookId], references: [id], onDelete: Cascade) + isPinned Boolean @default(false) + isShortcut Boolean @default(false) + isTrashed Boolean @default(false) + trashedAt DateTime? + thumbnailUrl String? + sourceUrl String? + author String? + latitude Float? + longitude Float? + reminderAt DateTime? + reminderDone Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) + tags NoteTag[] + attachments Attachment[] + + @@index([userId, isTrashed, isPinned, updatedAt(sort: Desc)]) + @@index([notebookId, isTrashed, updatedAt(sort: Desc)]) + @@index([isTrashed, isPinned, updatedAt(sort: Desc)]) + @@index([isShortcut, isTrashed]) + @@index([createdAt(sort: Desc)]) + @@index([title]) +} + +model Tag { + id String @id @default(cuid()) + userId String @default("default") + name String + color String @default("#00a82d") + parentId String? + parent Tag? @relation("TagHierarchy", fields: [parentId], references: [id], onDelete: SetNull) + children Tag[] @relation("TagHierarchy") + isShortcut Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + notes NoteTag[] + + @@unique([userId, name]) + @@index([userId]) + @@index([parentId]) +} + +model NoteTag { + noteId String + tagId String + note Note @relation(fields: [noteId], references: [id], onDelete: Cascade) + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + assignedAt DateTime @default(now()) + + @@id([noteId, tagId]) + @@index([tagId]) +} + +model Attachment { + id String @id @default(cuid()) + noteId String + note Note @relation(fields: [noteId], references: [id], onDelete: Cascade) + filename String + mimeType String + sizeBytes Int + md5Hash String + storagePath String + width Int? + height Int? + extractedText String? + createdAt DateTime @default(now()) + + @@index([noteId]) + @@index([md5Hash]) +} + +model SavedSearch { + id String @id @default(cuid()) + userId String @default("default") + name String + query String + isShortcut Boolean @default(false) + sortOrder Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([userId, name]) + @@index([userId]) +} + +model ApiKey { + id String @id @default(cuid()) + userId String @default("default") + name String + keyHash String @unique + keyPrefix String + lastUsedAt DateTime? + createdAt DateTime @default(now()) +} + +model ImportJob { + id String @id @default(cuid()) + userId String @default("default") + sourceType String + filename String + status String @default("running") + totalNotes Int @default(0) + processedNotes Int @default(0) + failedNotes Int @default(0) + errorLog String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model EmailConfig { + id String @id @default("default") + userId String @default("default") + enabled Boolean @default(false) + imapHost String @default("imap.gmail.com") + imapPort Int @default(993) + imapSecure Boolean @default(true) + imapUser String @default("") + imapPassword String @default("") + mailbox String @default("INBOX") + subjectFilter String @default("") + allowedSenders String @default("") + defaultNotebook String @default("Inbox") + defaultTags String @default("emailed") + markSeen Boolean @default(true) + lastCheckedAt DateTime? + lastStatus String? + emailsImported Int @default(0) + updatedAt DateTime @updatedAt +} diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000..39a5990 --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..942bb83 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,33 @@ +{ + "name": "Everclone", + "short_name": "Everclone", + "description": "Self-hosted Evernote clone for notes, web clips, and attachments", + "start_url": "/", + "display": "standalone", + "background_color": "#1a1a1a", + "theme_color": "#00a82d", + "icons": [ + { + "src": "/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ], + "share_target": { + "action": "/api/hooks/note?source=share-target", + "method": "POST", + "enctype": "multipart/form-data", + "params": { + "title": "title", + "text": "text", + "url": "url", + "files": [ + { + "name": "files", + "accept": ["image/*", "audio/*", "application/pdf", "text/*"] + } + ] + } + } +} diff --git a/src/app/api/email/route.ts b/src/app/api/email/route.ts new file mode 100644 index 0000000..bf9cd15 --- /dev/null +++ b/src/app/api/email/route.ts @@ -0,0 +1,77 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getOrCreateEmailConfig, pollImapMailboxOnce } from '@/lib/email'; + +export async function GET() { + try { + const cfg = await getOrCreateEmailConfig(); + return NextResponse.json({ + ...cfg, + hasPassword: Boolean(cfg.imapPassword), + imapPassword: cfg.imapPassword ? 'β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’' : '', + }); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to load email config' }, + { status: 500 } + ); + } +} + +export async function PUT(req: NextRequest) { + try { + const body = await req.json(); + const existing = await getOrCreateEmailConfig(); + + const updated = await prisma.emailConfig.update({ + where: { id: 'default' }, + data: { + enabled: Boolean(body.enabled), + imapHost: body.imapHost?.trim() ?? existing.imapHost, + imapPort: Number(body.imapPort) || 993, + imapSecure: body.imapSecure !== undefined ? Boolean(body.imapSecure) : true, + imapUser: body.imapUser?.trim() ?? existing.imapUser, + ...(body.imapPassword && !body.imapPassword.includes('β€’β€’β€’β€’') + ? { imapPassword: body.imapPassword } + : {}), + mailbox: body.mailbox?.trim() || 'INBOX', + subjectFilter: body.subjectFilter?.trim() ?? '', + allowedSenders: body.allowedSenders?.trim() ?? '', + defaultNotebook: body.defaultNotebook?.trim() || 'Inbox', + defaultTags: body.defaultTags?.trim() || 'emailed', + markSeen: body.markSeen !== undefined ? Boolean(body.markSeen) : true, + }, + }); + + return NextResponse.json({ + ...updated, + hasPassword: Boolean(updated.imapPassword), + imapPassword: updated.imapPassword ? 'β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’' : '', + }); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to update email config' }, + { status: 500 } + ); + } +} + +export async function POST() { + try { + const res = await pollImapMailboxOnce(); + const updatedCfg = await getOrCreateEmailConfig(); + return NextResponse.json({ + ...res, + config: { + ...updatedCfg, + hasPassword: Boolean(updatedCfg.imapPassword), + imapPassword: updatedCfg.imapPassword ? 'β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’' : '', + }, + }); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'IMAP poll failed' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/export/route.ts b/src/app/api/export/route.ts new file mode 100644 index 0000000..785e484 --- /dev/null +++ b/src/app/api/export/route.ts @@ -0,0 +1,109 @@ +import { NextRequest, NextResponse } from 'next/server'; +import TurndownService from 'turndown'; +import { prisma } from '@/lib/prisma'; +import { exportNotesToEnex, toEvernoteTimestamp } from '@/lib/importers'; + +export async function GET(req: NextRequest) { + try { + const sp = req.nextUrl.searchParams; + const noteId = sp.get('noteId'); + const notebookId = sp.get('notebookId'); + const format = (sp.get('format') || 'enex').toLowerCase(); + + const notes = await prisma.note.findMany({ + where: { + isTrashed: false, + ...(noteId ? { id: noteId } : {}), + ...(notebookId ? { notebookId } : {}), + }, + include: { + tags: { include: { tag: true } }, + notebook: true, + }, + orderBy: { updatedAt: 'desc' }, + }); + + if (notes.length === 0) { + return NextResponse.json({ error: 'No notes found to export' }, { status: 404 }); + } + + const baseFilename = + notes.length === 1 + ? notes[0].title.replace(/[^a-zA-Z0-9-_ ]/g, '').trim() || 'note' + : notebookId + ? notes[0].notebook.name.replace(/[^a-zA-Z0-9-_ ]/g, '').trim() || 'notebook' + : 'everclone-export'; + + if (format === 'md' || format === 'markdown') { + const td = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' }); + const mdBlocks = notes.map((n) => { + const mdBody = td.turndown(n.contentHtml || ''); + const tagLine = + n.tags.length > 0 + ? `**Tags:** ${n.tags.map((t) => t.tag.name).join(', ')}\n` + : ''; + return `# ${n.title}\n\n**Created:** ${toEvernoteTimestamp( + n.createdAt + )}\n**Updated:** ${toEvernoteTimestamp(n.updatedAt)}\n${tagLine}\n---\n\n${mdBody}\n`; + }); + + return new NextResponse(mdBlocks.join('\n\n====================\n\n'), { + status: 200, + headers: { + 'Content-Type': 'text/markdown; charset=utf-8', + 'Content-Disposition': `attachment; filename="${encodeURIComponent(baseFilename)}.md"`, + }, + }); + } + + if (format === 'html') { + const htmlDoc = ` + + + + ${baseFilename} + + + +${notes + .map( + (n) => `
+

${n.title}

+
Notebook: ${n.notebook.name} | Created: ${n.createdAt.toISOString()} | Updated: ${n.updatedAt.toISOString()}
+
${n.contentHtml}
+
` + ) + .join('\n')} + +`; + + return new NextResponse(htmlDoc, { + status: 200, + headers: { + 'Content-Type': 'text/html; charset=utf-8', + 'Content-Disposition': `attachment; filename="${encodeURIComponent(baseFilename)}.html"`, + }, + }); + } + + // Default: Evernote .enex + const enexXml = exportNotesToEnex(notes); + return new NextResponse(enexXml, { + status: 200, + headers: { + 'Content-Type': 'application/xml; charset=utf-8', + 'Content-Disposition': `attachment; filename="${encodeURIComponent(baseFilename)}.enex"`, + }, + }); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Export failed' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/extension/route.ts b/src/app/api/extension/route.ts new file mode 100644 index 0000000..e6fc66d --- /dev/null +++ b/src/app/api/extension/route.ts @@ -0,0 +1,312 @@ +import { NextRequest, NextResponse } from 'next/server'; +import JSZip from 'jszip'; + +function buildExtensionFiles(defaultServerUrl: string, defaultApiKey: string) { + const manifest = { + manifest_version: 3, + name: 'Everclone Web Clipper', + version: '1.0.0', + description: + 'Clip articles (with all graphics), selections, and bookmarks from Brave/Chrome directly into Everclone.', + permissions: ['activeTab', 'scripting', 'storage'], + host_permissions: [''], + action: { + default_popup: 'popup.html', + default_title: 'Clip to Everclone', + }, + }; + + const popupHtml = ` + + + + + + +
+
+ + Everclone Web Clipper +
+ +
+ +
+ + + + + +
+ + + + + +
+ + + + +
+ + + + + + + + + + + +
+ + + +`; + + const popupJs = `const DEFAULT_SERVER = ${JSON.stringify(defaultServerUrl)}; +const DEFAULT_KEY = ${JSON.stringify(defaultApiKey)}; + +let currentMode = 'article'; +let activeTab = null; + +document.addEventListener('DOMContentLoaded', async () => { + const stored = await chrome.storage.local.get(['serverUrl', 'apiKey', 'lastNotebook']); + const serverUrl = (stored.serverUrl || DEFAULT_SERVER).replace(/\\/$/, ''); + const apiKey = stored.apiKey || DEFAULT_KEY; + + document.getElementById('serverUrl').value = serverUrl; + document.getElementById('apiKey').value = apiKey; + + if (!apiKey || apiKey === '') { + document.getElementById('settingsPanel').style.display = 'block'; + } + + document.getElementById('toggleSettings').addEventListener('click', () => { + const p = document.getElementById('settingsPanel'); + p.style.display = p.style.display === 'block' ? 'none' : 'block'; + }); + + document.getElementById('saveSettingsBtn').addEventListener('click', async () => { + const s = document.getElementById('serverUrl').value.trim().replace(/\\/$/, ''); + const k = document.getElementById('apiKey').value.trim(); + await chrome.storage.local.set({ serverUrl: s, apiKey: k }); + document.getElementById('settingsPanel').style.display = 'none'; + loadNotebooks(s, k); + }); + + document.querySelectorAll('.mode-btn').forEach((btn) => { + btn.addEventListener('click', () => { + document.querySelectorAll('.mode-btn').forEach((b) => b.classList.remove('active')); + btn.classList.add('active'); + currentMode = btn.getAttribute('data-mode'); + }); + }); + + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + activeTab = tab; + if (tab && tab.title) { + document.getElementById('noteTitle').value = tab.title; + } + + await loadNotebooks(serverUrl, apiKey, stored.lastNotebook); + + document.getElementById('clipBtn').addEventListener('click', handleClip); +}); + +async function loadNotebooks(serverUrl, apiKey, preferredNb) { + if (!serverUrl || !apiKey) return; + try { + const res = await fetch(\`\${serverUrl}/api/hooks/note?action=notebooks\`, { + headers: { Authorization: \`Bearer \${apiKey}\` }, + }); + if (!res.ok) return; + const data = await res.json(); + if (Array.isArray(data.notebooks) && data.notebooks.length > 0) { + const sel = document.getElementById('notebookSelect'); + sel.innerHTML = ''; + data.notebooks.forEach((nb) => { + const opt = document.createElement('option'); + opt.value = nb.name; + opt.textContent = nb.name; + if (preferredNb ? nb.name === preferredNb : nb.name === 'Web Clips' || nb.isDefault) { + opt.selected = true; + } + sel.appendChild(opt); + }); + } + } catch { + // keep default options + } +} + +async function handleClip() { + const btn = document.getElementById('clipBtn'); + const statusBox = document.getElementById('statusBox'); + statusBox.className = 'status'; + btn.disabled = true; + btn.textContent = 'Clipping & saving graphics...'; + + try { + const serverUrl = document.getElementById('serverUrl').value.trim().replace(/\\/$/, ''); + const apiKey = document.getElementById('apiKey').value.trim(); + const title = document.getElementById('noteTitle').value.trim(); + const notebook = document.getElementById('notebookSelect').value; + const tags = document.getElementById('noteTags').value.split(',').map((t) => t.trim()).filter(Boolean); + const comment = document.getElementById('noteComment').value.trim(); + + await chrome.storage.local.set({ serverUrl, apiKey, lastNotebook: notebook }); + + let captured = { pageHtml: '', selectionHtml: '', url: activeTab?.url || '' }; + if (activeTab?.id && currentMode !== 'bookmark') { + const results = await chrome.scripting.executeScript({ + target: { tabId: activeTab.id }, + func: () => { + // Clone document and resolve lazy-loaded images & relative URLs inside the live tab + const clone = document.documentElement.cloneNode(true); + clone.querySelectorAll('script, style, noscript, iframe').forEach((el) => el.remove()); + clone.querySelectorAll('img').forEach((img) => { + const best = + img.getAttribute('data-img-url') || + img.getAttribute('data-src') || + img.getAttribute('data-lazy-src') || + img.getAttribute('data-original') || + img.currentSrc || + img.src; + if (best) { + try { + img.setAttribute('src', new URL(best, location.href).href); + } catch {} + } + }); + let selHtml = ''; + const sel = window.getSelection(); + if (sel && sel.rangeCount > 0 && !sel.isCollapsed) { + const container = document.createElement('div'); + for (let i = 0; i < sel.rangeCount; i++) { + container.appendChild(sel.getRangeAt(i).cloneContents()); + } + container.querySelectorAll('img').forEach((img) => { + if (img.src) img.setAttribute('src', img.src); + }); + selHtml = container.innerHTML; + } + return { + pageHtml: '' + clone.innerHTML + '', + bodyHtml: clone.querySelector('body')?.innerHTML || '', + selectionHtml: selHtml, + url: location.href, + }; + }, + }); + if (results && results[0] && results[0].result) { + captured = results[0].result; + } + } + + const payload = { + title, + url: captured.url || activeTab?.url || '', + notebook, + tags, + comment, + clipMode: currentMode, + }; + + if (currentMode === 'article') { + payload.pageHtml = captured.pageHtml; + } else if (currentMode === 'selection') { + payload.html = captured.selectionHtml || '

No text selected on page.

'; + } else if (currentMode === 'full') { + payload.html = captured.bodyHtml || captured.pageHtml; + } else if (currentMode === 'bookmark') { + payload.html = \`

Bookmarked: \${payload.url}

\`; + } + + const res = await fetch(\`\${serverUrl}/api/hooks/note\`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: \`Bearer \${apiKey}\`, + }, + body: JSON.stringify(payload), + }); + + const data = await res.json(); + if (!res.ok) { + throw new Error(data.error || \`HTTP \${res.status}\`); + } + + statusBox.className = 'status ok'; + statusBox.innerHTML = \`βœ“ Saved to \${notebook}! Open Note\`; + btn.textContent = 'Clipped!'; + } catch (err) { + statusBox.className = 'status err'; + statusBox.textContent = 'Error: ' + (err.message || String(err)); + btn.disabled = false; + btn.textContent = 'Save Clip to Everclone'; + } +} +`; + + return { + 'manifest.json': JSON.stringify(manifest, null, 2), + 'popup.html': popupHtml, + 'popup.js': popupJs, + }; +} + +export async function GET(req: NextRequest) { + try { + const sp = req.nextUrl.searchParams; + const serverUrl = sp.get('serverUrl') || 'https://everclone.vloser.us'; + const apiKey = sp.get('apiKey') || 'evc_master_vloser_key_2026'; + + const files = buildExtensionFiles(serverUrl, apiKey); + const zip = new JSZip(); + for (const [name, content] of Object.entries(files)) { + zip.file(name, content); + } + + const zipBytes = await zip.generateAsync({ type: 'uint8array' }); + + return new NextResponse(zipBytes.buffer as ArrayBuffer, { + status: 200, + headers: { + 'Content-Type': 'application/zip', + 'Content-Disposition': 'attachment; filename="everclone-brave-clipper.zip"', + }, + }); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to build extension ZIP' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..2714b83 --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function GET() { + try { + await prisma.$queryRaw`SELECT 1`; + return NextResponse.json({ status: 'ok', service: 'everclone' }); + } catch (err: unknown) { + return NextResponse.json( + { status: 'error', error: err instanceof Error ? err.message : 'DB unreachable' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/hooks/email/route.ts b/src/app/api/hooks/email/route.ts new file mode 100644 index 0000000..3dbccb8 --- /dev/null +++ b/src/app/api/hooks/email/route.ts @@ -0,0 +1,120 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { verifyApiKeyFromRequest } from '@/lib/auth'; +import { ingestEmailToNote, ingestRawMimeEmail } from '@/lib/email'; + +/** + * POST /api/hooks/email + * Inbound Email-to-Note Webhook (works with Cloudflare Email Routing, n8n, ForwardEmail, Mailgun, Postmark) + * Supports: + * - Raw MIME (.eml) body or JSON { rawMime: "..." } + * - JSON { subject, from, html, text, notebook, tags, attachments: [{ filename, mimeType, contentBase64 }] } + * - Multipart form-data (Mailgun / SendGrid / Postmark inbound parse format) + */ +export async function POST(req: NextRequest) { + try { + const isValidKey = await verifyApiKeyFromRequest(req); + if (!isValidKey) { + return NextResponse.json( + { + error: + 'Unauthorized. Provide Authorization: Bearer , X-API-Key header, or ?apiKey=', + }, + { status: 401 } + ); + } + + const contentType = req.headers.get('content-type') || ''; + + if ( + contentType.includes('message/rfc822') || + contentType.includes('text/plain') + ) { + const rawText = await req.text(); + const res = await ingestRawMimeEmail(rawText); + return NextResponse.json({ ok: true, ...res }, { status: 201 }); + } + + if (contentType.includes('application/json')) { + const body = await req.json(); + + if (body.rawMime) { + const res = await ingestRawMimeEmail(body.rawMime, { + defaultNotebook: body.notebook, + defaultTags: Array.isArray(body.tags) ? body.tags : undefined, + }); + return NextResponse.json({ ok: true, ...res }, { status: 201 }); + } + + const attachments = Array.isArray(body.attachments) + ? body.attachments.map( + (a: { + filename?: string; + mimeType?: string; + contentBase64?: string; + }) => ({ + filename: a.filename || 'attachment', + mimeType: a.mimeType || 'application/octet-stream', + content: Buffer.from(a.contentBase64 || '', 'base64'), + }) + ) + : []; + + const res = await ingestEmailToNote({ + subject: body.subject || 'Emailed Note', + from: body.from || body.sender || '', + html: body.html || body['body-html'] || undefined, + text: body.text || body['body-plain'] || undefined, + defaultNotebook: body.notebook || undefined, + defaultTags: Array.isArray(body.tags) ? body.tags : ['emailed'], + attachments, + }); + + return NextResponse.json({ ok: true, ...res }, { status: 201 }); + } + + // Multipart form-data (Mailgun / SendGrid inbound parse) + const formData = await req.formData(); + const subject = (formData.get('subject') as string) || 'Emailed Note'; + const from = (formData.get('from') as string) || (formData.get('sender') as string) || ''; + const html = + (formData.get('html') as string) || + (formData.get('body-html') as string) || + undefined; + const text = + (formData.get('text') as string) || + (formData.get('body-plain') as string) || + undefined; + + const attachments: Array<{ + filename: string; + mimeType: string; + content: Buffer; + }> = []; + + for (const [, val] of formData.entries()) { + if (val instanceof File) { + attachments.push({ + filename: val.name || 'attachment', + mimeType: val.type || 'application/octet-stream', + content: Buffer.from(await val.arrayBuffer()), + }); + } + } + + const res = await ingestEmailToNote({ + subject, + from, + html, + text, + attachments, + }); + + return NextResponse.json({ ok: true, ...res }, { status: 201 }); + } catch (err: unknown) { + console.error('POST /api/hooks/email error:', err); + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Email ingestion failed' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/hooks/note/route.ts b/src/app/api/hooks/note/route.ts new file mode 100644 index 0000000..57ce89d --- /dev/null +++ b/src/app/api/hooks/note/route.ts @@ -0,0 +1,378 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { marked } from 'marked'; +import { JSDOM } from 'jsdom'; +import { Readability } from '@mozilla/readability'; +import { prisma, ensureDefaultNotebook } from '@/lib/prisma'; +import { verifyApiKeyFromRequest } from '@/lib/auth'; +import { + clipUrlToNote, + getOrCreateNotebookByName, + getOrCreateTagIds, + prepareDomForReadability, + localizeClippedImages, +} from '@/lib/importers'; +import { + saveAttachmentBuffer, + stripHtmlToText, + makeExcerpt, +} from '@/lib/storage'; + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-Key', +}; + +export async function OPTIONS() { + return new NextResponse(null, { status: 204, headers: corsHeaders }); +} + +export async function POST(req: NextRequest) { + try { + const isValidKey = await verifyApiKeyFromRequest(req); + const isShareTarget = req.nextUrl.searchParams.get('source') === 'share-target'; + + if (!isValidKey && !isShareTarget) { + return NextResponse.json( + { + error: + 'Unauthorized. Provide Authorization: Bearer , X-API-Key header, or ?apiKey=', + }, + { status: 401, headers: corsHeaders } + ); + } + + const contentType = req.headers.get('content-type') || ''; + + let title = ''; + let content = ''; + let markdown = ''; + let html = ''; + let pageHtml = ''; + let clipMode = ''; + let comment = ''; + let url = ''; + let notebookName = ''; + let tags: string[] = ['inbox']; + let isShortcut = false; + const uploadedFiles: File[] = []; + + if (contentType.includes('application/json')) { + const body = await req.json(); + title = body.title || ''; + content = body.content || body.text || ''; + markdown = body.markdown || ''; + html = body.html || ''; + pageHtml = body.pageHtml || ''; + clipMode = body.clipMode || ''; + comment = body.comment || ''; + url = body.url || body.sourceUrl || ''; + notebookName = body.notebook || body.notebookName || ''; + isShortcut = Boolean(body.isShortcut); + if (Array.isArray(body.tags)) { + tags = body.tags; + } else if (typeof body.tags === 'string') { + tags = body.tags + .split(',') + .map((t: string) => t.trim()) + .filter(Boolean); + } + } else { + const formData = await req.formData(); + title = (formData.get('title') as string) || ''; + content = + (formData.get('content') as string) || + (formData.get('text') as string) || + ''; + markdown = (formData.get('markdown') as string) || ''; + html = (formData.get('html') as string) || ''; + url = (formData.get('url') as string) || ''; + notebookName = (formData.get('notebook') as string) || ''; + const rawTags = (formData.get('tags') as string) || ''; + if (rawTags) { + tags = rawTags + .split(',') + .map((t) => t.trim()) + .filter(Boolean); + } + const files = formData.getAll('files') as File[]; + const single = formData.get('file') as File | null; + if (single && files.length === 0) files.push(single); + uploadedFiles.push(...files); + } + + // Check if Android shared a URL inside the `text` field + if (!url && content && /^https?:\/\/\S+$/i.test(content.trim())) { + url = content.trim(); + content = ''; + } + + const notebookId = notebookName.trim() + ? await getOrCreateNotebookByName(notebookName.trim()) + : (await ensureDefaultNotebook()).id; + + // 1. If Brave Extension sent live rendered DOM (`pageHtml`) in `article` mode: + if (pageHtml && (clipMode === 'article' || !clipMode)) { + const baseUrl = url || 'https://example.com'; + const dom = new JSDOM(pageHtml, { url: baseUrl }); + const doc = dom.window.document; + const { heroImgSrc } = prepareDomForReadability(doc, baseUrl); + const reader = new Readability(doc); + const article = reader.parse(); + + const resolvedTitle = + title.trim() || + article?.title?.trim() || + doc.title?.trim() || + url || + 'Web Clip'; + + let extractedHtml = article?.content || doc.body?.innerHTML || pageHtml; + if (heroImgSrc && !extractedHtml.includes(heroImgSrc)) { + extractedHtml = `

\n${extractedHtml}`; + } + if (comment.trim()) { + extractedHtml = `
${comment + .trim() + .replace(/\n/g, '
')}
\n${extractedHtml}`; + } + + const tagIds = await getOrCreateTagIds(tags.length ? tags : ['web-clip']); + const note = await prisma.note.create({ + data: { + title: resolvedTitle, + contentHtml: extractedHtml, + contentText: stripHtmlToText(extractedHtml), + excerpt: makeExcerpt(article?.excerpt || stripHtmlToText(extractedHtml)), + notebookId, + sourceUrl: url || null, + isShortcut, + tags: { + create: tagIds.map((tagId) => ({ tagId })), + }, + }, + }); + + const localizedHtml = await localizeClippedImages( + note.id, + extractedHtml, + url || undefined + ); + if (localizedHtml !== extractedHtml) { + await prisma.note.update({ + where: { id: note.id }, + data: { contentHtml: localizedHtml }, + }); + } + + return NextResponse.json( + { ok: true, noteId: note.id, title: note.title, mode: 'extension-article' }, + { status: 201, headers: corsHeaders } + ); + } + + // 2. If URL is provided without body content and no files, clip the full article on server! + if (url && !content && !markdown && !html && uploadedFiles.length === 0) { + const clippedId = await clipUrlToNote(url, { + title: title || undefined, + notebookId, + tags, + isShortcut, + }); + + if (isShareTarget) { + return NextResponse.redirect(new URL(`/?noteId=${clippedId}`, req.url)); + } + + return NextResponse.json( + { ok: true, noteId: clippedId, mode: 'web-clip' }, + { status: 201, headers: corsHeaders } + ); + } + + let finalHtml = html; + if (!finalHtml && markdown) { + finalHtml = await marked.parse(markdown, { gfm: true, breaks: true }); + } else if (!finalHtml && content) { + finalHtml = await marked.parse(content, { gfm: true, breaks: true }); + } + if (!finalHtml) { + finalHtml = '

'; + } + if (comment.trim()) { + finalHtml = `
${comment + .trim() + .replace(/\n/g, '
')}
\n${finalHtml}`; + } + + const finalTitle = + title.trim() || + stripHtmlToText(finalHtml).slice(0, 60).trim() || + (uploadedFiles[0]?.name ?? 'Quick Note'); + + const tagIds = await getOrCreateTagIds(tags); + + const note = await prisma.note.create({ + data: { + title: finalTitle, + contentHtml: finalHtml, + contentText: stripHtmlToText(finalHtml), + excerpt: makeExcerpt(stripHtmlToText(finalHtml)), + notebookId, + sourceUrl: url || null, + isShortcut, + tags: { + create: tagIds.map((tagId) => ({ tagId })), + }, + }, + }); + + // Localize any remote in selection/HTML clip + if (finalHtml.includes('${filename}

`; + } else if (mimeType.startsWith('audio/')) { + appendedMediaHtml += `

`; + } else { + appendedMediaHtml += `

πŸ“Ž ${filename}

`; + } + } + + if (appendedMediaHtml) { + const combinedHtml = `${finalHtml}\n${appendedMediaHtml}`; + await prisma.note.update({ + where: { id: note.id }, + data: { + contentHtml: combinedHtml, + contentText: stripHtmlToText(combinedHtml), + excerpt: makeExcerpt(stripHtmlToText(combinedHtml)), + }, + }); + } + + if (isShareTarget) { + return NextResponse.redirect(new URL(`/?noteId=${note.id}`, req.url)); + } + + return NextResponse.json( + { ok: true, noteId: note.id, title: note.title }, + { status: 201, headers: corsHeaders } + ); + } catch (err: unknown) { + console.error('POST /api/hooks/note error:', err); + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Hook ingestion failed' }, + { status: 500, headers: corsHeaders } + ); + } +} + +export async function GET(req: NextRequest) { + try { + const isValidKey = await verifyApiKeyFromRequest(req); + if (!isValidKey) { + return NextResponse.json( + { error: 'Unauthorized: missing or invalid apiKey' }, + { status: 401, headers: corsHeaders } + ); + } + + const sp = req.nextUrl.searchParams; + + // Allow Brave Web Clipper Extension to list notebooks for its dropdown + if (sp.get('action') === 'notebooks') { + await ensureDefaultNotebook(); + const notebooks = await prisma.notebook.findMany({ + orderBy: [{ isDefault: 'desc' }, { name: 'asc' }], + select: { id: true, name: true, isDefault: true }, + }); + return NextResponse.json({ notebooks }, { headers: corsHeaders }); + } + + const url = sp.get('url') || ''; + const title = sp.get('title') || ''; + const text = sp.get('text') || ''; + const notebookName = sp.get('notebook') || ''; + const tags = (sp.get('tags') || 'web-clip') + .split(',') + .map((t) => t.trim()) + .filter(Boolean); + + const notebookId = notebookName + ? await getOrCreateNotebookByName(notebookName) + : (await ensureDefaultNotebook()).id; + + let noteId: string; + if (url && !text) { + noteId = await clipUrlToNote(url, { title: title || undefined, notebookId, tags }); + } else { + const contentHtml = text + ? `

${text.replace(/\n/g, '
')}

${ + url ? `

Source: ${url}

` : '' + }` + : `

${url}

`; + const tagIds = await getOrCreateTagIds(tags); + const created = await prisma.note.create({ + data: { + title: title || url || 'Quick Clip', + contentHtml, + contentText: stripHtmlToText(contentHtml), + excerpt: makeExcerpt(stripHtmlToText(contentHtml)), + notebookId, + sourceUrl: url || null, + tags: { + create: tagIds.map((tagId) => ({ tagId })), + }, + }, + }); + noteId = created.id; + } + + if (sp.get('redirect') === '1') { + return NextResponse.redirect(new URL(`/?noteId=${noteId}`, req.url)); + } + + return NextResponse.json( + { ok: true, noteId }, + { status: 201, headers: corsHeaders } + ); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Hook failed' }, + { status: 500, headers: corsHeaders } + ); + } +} diff --git a/src/app/api/import/route.ts b/src/app/api/import/route.ts new file mode 100644 index 0000000..ba4fd2d --- /dev/null +++ b/src/app/api/import/route.ts @@ -0,0 +1,394 @@ +import { NextRequest, NextResponse } from 'next/server'; +import fs from 'fs/promises'; +import { createReadStream } from 'fs'; +import path from 'path'; +import os from 'os'; +import JSZip from 'jszip'; +import { prisma, ensureDefaultNotebook } from '@/lib/prisma'; +import { + importEnexXml, + importMarkdownNote, + importHtmlNote, + clipUrlToNote, + getOrCreateNotebookByName, +} from '@/lib/importers'; + +function getChunkTempPath(jobId: string): string { + const safeId = jobId.replace(/[^a-zA-Z0-9_-]/g, ''); + return path.join(os.tmpdir(), `everclone-import-${safeId}.tmp`); +} + +/** + * Streams a potentially multi-GB .enex file directly from disk on the server, + * counting total blocks first and then importing note-by-note while updating + * ImportJob progress in PostgreSQL. + */ +async function processEnexFileFromDiskInBackground( + jobId: string, + filePath: string, + notebookId: string +) { + let processedNotes = 0; + let failedNotes = 0; + const errors: string[] = []; + + try { + // Pass 1: Quickly count total `` tags in file so progress bar has an exact denominator + let totalNotes = 0; + let tail = ''; + for await (const chunk of createReadStream(filePath, { encoding: 'utf8', highWaterMark: 1024 * 1024 })) { + const combined = tail + chunk; + const matches = combined.match(//g); + if (matches) totalNotes += matches.length; + tail = combined.slice(-10); + } + + await prisma.importJob.update({ + where: { id: jobId }, + data: { totalNotes, status: 'processing' }, + }); + + // Pass 2: Stream through file, extract each ..., and import + let carry = ''; + for await (const chunk of createReadStream(filePath, { encoding: 'utf8', highWaterMark: 1024 * 1024 })) { + carry += chunk; + + while (true) { + const startIdx = carry.indexOf(''); + if (startIdx === -1) { + if (carry.length > 64) carry = carry.slice(-64); + break; + } + const closeIdx = carry.indexOf('', startIdx); + if (closeIdx === -1) { + if (startIdx > 0) carry = carry.slice(startIdx); + break; + } + + const singleNoteXml = carry.slice(startIdx, closeIdx + 7); + carry = carry.slice(closeIdx + 7); + + const wrappedXml = `${singleNoteXml}`; + const res = await importEnexXml(wrappedXml, { notebookId }); + processedNotes += res.importedCount; + if (res.errors.length > 0) { + failedNotes += 1; + errors.push(...res.errors.slice(0, 3)); + } + + if ((processedNotes + failedNotes) % 3 === 0 || processedNotes === totalNotes) { + await prisma.importJob.update({ + where: { id: jobId }, + data: { + processedNotes, + failedNotes, + errorLog: errors.length > 0 ? errors.slice(0, 30).join('\n') : null, + }, + }); + } + } + } + + await prisma.importJob.update({ + where: { id: jobId }, + data: { + status: 'completed', + processedNotes, + failedNotes, + errorLog: errors.length > 0 ? errors.slice(0, 30).join('\n') : null, + }, + }); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + errors.push(msg); + await prisma.importJob + .update({ + where: { id: jobId }, + data: { + status: 'failed', + processedNotes, + failedNotes, + errorLog: errors.slice(0, 30).join('\n'), + }, + }) + .catch(() => {}); + } finally { + await fs.unlink(filePath).catch(() => {}); + } +} + +/** + * GET /api/import?jobId=... + * Returns live status of a running or completed background import job. + */ +export async function GET(req: NextRequest) { + try { + const jobId = req.nextUrl.searchParams.get('jobId'); + if (jobId) { + const job = await prisma.importJob.findUnique({ where: { id: jobId } }); + if (!job) { + return NextResponse.json({ error: 'Import job not found' }, { status: 404 }); + } + return NextResponse.json(job); + } + + const recentJobs = await prisma.importJob.findMany({ + orderBy: { createdAt: 'desc' }, + take: 10, + }); + return NextResponse.json({ jobs: recentJobs }); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to fetch import job' }, + { status: 500 } + ); + } +} + +export async function POST(req: NextRequest) { + try { + const action = req.nextUrl.searchParams.get('action'); + + // 1. Chunked binary upload protocol (strictly <= 2MB per request, immune to proxy limits) + if (action === 'initChunkUpload') { + const body = await req.json(); + const filename = body.filename || 'import.enex'; + const lower = filename.toLowerCase(); + const job = await prisma.importJob.create({ + data: { + sourceType: lower.endsWith('.enex') + ? 'enex' + : lower.endsWith('.zip') + ? 'zip' + : lower.endsWith('.md') + ? 'markdown' + : 'html', + filename, + status: 'uploading', + totalNotes: 0, + processedNotes: 0, + }, + }); + const tmpPath = getChunkTempPath(job.id); + await fs.writeFile(tmpPath, Buffer.alloc(0)); + return NextResponse.json({ jobId: job.id }); + } + + if (action === 'appendChunk') { + const jobId = req.nextUrl.searchParams.get('jobId'); + if (!jobId) { + return NextResponse.json({ error: 'Missing jobId' }, { status: 400 }); + } + const arrayBuf = await req.arrayBuffer(); + const tmpPath = getChunkTempPath(jobId); + await fs.appendFile(tmpPath, Buffer.from(arrayBuf)); + return NextResponse.json({ ok: true, bytesAppended: arrayBuf.byteLength }); + } + + if (action === 'finishChunkUpload') { + const body = await req.json(); + const { jobId, notebookId, notebookName, useFilenameAsNotebook } = body; + if (!jobId) { + return NextResponse.json({ error: 'Missing jobId' }, { status: 400 }); + } + + const job = await prisma.importJob.findUnique({ where: { id: jobId } }); + if (!job) { + return NextResponse.json({ error: 'Import job not found' }, { status: 404 }); + } + + let resolvedNbId = notebookId || ''; + if (!resolvedNbId) { + if (notebookName?.trim()) { + resolvedNbId = await getOrCreateNotebookByName(notebookName.trim()); + } else if (useFilenameAsNotebook) { + resolvedNbId = await getOrCreateNotebookByName( + job.filename.replace(/\.(enex|zip|md|html?)$/i, '') + ); + } else { + resolvedNbId = (await ensureDefaultNotebook()).id; + } + } + + const tmpPath = getChunkTempPath(jobId); + + if (job.sourceType === 'enex') { + // Launch background streaming import on the server and return immediately! + void processEnexFileFromDiskInBackground(jobId, tmpPath, resolvedNbId); + return NextResponse.json({ jobId, status: 'processing', background: true }); + } + + // For .md / .html / .zip uploaded via chunks: + const contentBuf = await fs.readFile(tmpPath); + await fs.unlink(tmpPath).catch(() => {}); + + let importedCount = 0; + const errors: string[] = []; + + if (job.sourceType === 'markdown') { + await importMarkdownNote(contentBuf.toString('utf8'), job.filename, resolvedNbId); + importedCount = 1; + } else if (job.sourceType === 'html') { + await importHtmlNote(contentBuf.toString('utf8'), job.filename, resolvedNbId); + importedCount = 1; + } else if (job.sourceType === 'zip') { + const zip = await JSZip.loadAsync(contentBuf); + const entries = Object.values(zip.files).filter((z) => !z.dir); + for (const entry of entries) { + const entryLower = entry.name.toLowerCase(); + const baseName = entry.name.split('/').pop() || entry.name; + if (baseName.startsWith('._') || baseName.startsWith('.DS_Store')) continue; + const parts = entry.name.split('/'); + const folderNbId = + useFilenameAsNotebook && parts.length > 1 + ? await getOrCreateNotebookByName(parts[0]) + : resolvedNbId; + + if (entryLower.endsWith('.enex')) { + const xml = await entry.async('string'); + const res = await importEnexXml(xml, { notebookId: folderNbId }); + importedCount += res.importedCount; + errors.push(...res.errors); + } else if (entryLower.endsWith('.md') || entryLower.endsWith('.markdown')) { + const md = await entry.async('string'); + await importMarkdownNote(md, baseName, folderNbId); + importedCount += 1; + } else if (entryLower.endsWith('.html') || entryLower.endsWith('.htm')) { + const html = await entry.async('string'); + await importHtmlNote(html, baseName, folderNbId); + importedCount += 1; + } + } + } + + await prisma.importJob.update({ + where: { id: jobId }, + data: { + status: 'completed', + totalNotes: importedCount, + processedNotes: importedCount, + errorLog: errors.length > 0 ? errors.join('\n') : null, + }, + }); + + return NextResponse.json({ + jobId, + status: 'completed', + importedCount, + errors, + }); + } + + const contentType = req.headers.get('content-type') || ''; + + // 2. Support JSON requests: either URL clipping OR direct ENEX XML batch import + if (contentType.includes('application/json')) { + const body = await req.json(); + + if (body.enexXml) { + const notebookId = + body.notebookId || + (body.notebookName + ? await getOrCreateNotebookByName(body.notebookName) + : (await ensureDefaultNotebook()).id); + + const res = await importEnexXml(body.enexXml, { notebookId }); + return NextResponse.json({ + importedCount: res.importedCount, + noteIds: res.noteIds, + errors: res.errors, + notebookId, + }); + } + + if (body.url) { + const notebookId = + body.notebookId || + (body.notebookName + ? await getOrCreateNotebookByName(body.notebookName) + : (await ensureDefaultNotebook()).id); + const noteId = await clipUrlToNote(body.url, { + title: body.title, + notebookId, + tags: body.tags, + isShortcut: body.isShortcut, + }); + return NextResponse.json({ importedCount: 1, noteIds: [noteId], errors: [] }); + } + + return NextResponse.json({ error: 'Invalid JSON import request' }, { status: 400 }); + } + + // 3. Legacy Multipart FormData upload (for small CLI / curl imports) + const formData = await req.formData(); + const targetNotebookId = (formData.get('notebookId') as string) || ''; + const targetNotebookName = (formData.get('notebookName') as string) || ''; + const useFilenameAsNotebook = formData.get('useFilenameAsNotebook') === 'true'; + + const files = formData.getAll('files') as File[]; + const singleFile = formData.get('file') as File | null; + if (singleFile && files.length === 0) { + files.push(singleFile); + } + + if (files.length === 0) { + return NextResponse.json({ error: 'No files provided' }, { status: 400 }); + } + + let resolvedBaseNotebookId = targetNotebookId; + if (!resolvedBaseNotebookId) { + if (targetNotebookName.trim()) { + resolvedBaseNotebookId = await getOrCreateNotebookByName(targetNotebookName.trim()); + } else { + resolvedBaseNotebookId = (await ensureDefaultNotebook()).id; + } + } + + let totalImported = 0; + const allNoteIds: string[] = []; + const allErrors: string[] = []; + + for (const file of files) { + const fname = file.name || 'imported'; + const lower = fname.toLowerCase(); + + if (lower.endsWith('.enex')) { + const text = await file.text(); + const nbId = useFilenameAsNotebook + ? await getOrCreateNotebookByName(fname.replace(/\.enex$/i, '')) + : resolvedBaseNotebookId; + + const res = await importEnexXml(text, { notebookId: nbId }); + totalImported += res.importedCount; + allNoteIds.push(...res.noteIds); + allErrors.push(...res.errors); + } else if ( + lower.endsWith('.md') || + lower.endsWith('.markdown') || + lower.endsWith('.txt') + ) { + const text = await file.text(); + const id = await importMarkdownNote(text, fname, resolvedBaseNotebookId); + totalImported += 1; + allNoteIds.push(id); + } else if (lower.endsWith('.html') || lower.endsWith('.htm')) { + const text = await file.text(); + const id = await importHtmlNote(text, fname, resolvedBaseNotebookId); + totalImported += 1; + allNoteIds.push(id); + } + } + + return NextResponse.json({ + importedCount: totalImported, + noteIds: allNoteIds, + errors: allErrors, + }); + } catch (err: unknown) { + console.error('Import error:', err); + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Import failed' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/keys/route.ts b/src/app/api/keys/route.ts new file mode 100644 index 0000000..bb65e5a --- /dev/null +++ b/src/app/api/keys/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { generateRawApiKey } from '@/lib/auth'; + +export async function GET() { + try { + const keys = await prisma.apiKey.findMany({ + orderBy: { createdAt: 'desc' }, + select: { + id: true, + name: true, + keyPrefix: true, + lastUsedAt: true, + createdAt: true, + }, + }); + return NextResponse.json({ keys }); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to list API keys' }, + { status: 500 } + ); + } +} + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const name = body.name?.trim() || 'Android / Web Clipper Key'; + const { rawKey, keyHash, keyPrefix } = generateRawApiKey(); + + const created = await prisma.apiKey.create({ + data: { + name, + keyHash, + keyPrefix, + }, + }); + + return NextResponse.json({ + id: created.id, + name: created.name, + keyPrefix: created.keyPrefix, + rawKey, // Returned once on creation + createdAt: created.createdAt, + }); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to create API key' }, + { status: 500 } + ); + } +} + +export async function DELETE(req: NextRequest) { + try { + const id = req.nextUrl.searchParams.get('id'); + if (!id) { + return NextResponse.json({ error: 'API Key ID is required' }, { status: 400 }); + } + await prisma.apiKey.delete({ where: { id } }); + return NextResponse.json({ deleted: true }); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to revoke API key' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/notebooks/route.ts b/src/app/api/notebooks/route.ts new file mode 100644 index 0000000..6250e67 --- /dev/null +++ b/src/app/api/notebooks/route.ts @@ -0,0 +1,149 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma, getCurrentUserId, ensureDefaultNotebook } from '@/lib/prisma'; + +export async function POST(req: NextRequest) { + try { + const userId = await getCurrentUserId(); + const body = await req.json(); + + if (body.type === 'stack') { + const stackName = body.name?.trim(); + if (!stackName) { + return NextResponse.json({ error: 'Stack name is required' }, { status: 400 }); + } + const stack = await prisma.notebookStack.upsert({ + where: { userId_name: { userId, name: stackName } }, + update: {}, + create: { userId, name: stackName }, + }); + return NextResponse.json(stack); + } + + const name = body.name?.trim(); + if (!name) { + return NextResponse.json({ error: 'Notebook name is required' }, { status: 400 }); + } + + let stackId: string | null = body.stackId || null; + if (body.stackName?.trim()) { + const st = await prisma.notebookStack.upsert({ + where: { userId_name: { userId, name: body.stackName.trim() } }, + update: {}, + create: { userId, name: body.stackName.trim() }, + }); + stackId = st.id; + } + + const nb = await prisma.notebook.create({ + data: { + userId, + name, + stackId, + isShortcut: Boolean(body.isShortcut), + }, + }); + + return NextResponse.json(nb); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to create notebook' }, + { status: 500 } + ); + } +} + +export async function PATCH(req: NextRequest) { + try { + const userId = await getCurrentUserId(); + const body = await req.json(); + + if (body.type === 'stack') { + const updatedStack = await prisma.notebookStack.update({ + where: { id: body.id }, + data: { name: body.name?.trim() }, + }); + return NextResponse.json(updatedStack); + } + + const { id, name, stackId, stackName, isDefault, isShortcut } = body; + if (!id) { + return NextResponse.json({ error: 'Notebook id required' }, { status: 400 }); + } + + let resolvedStackId = stackId; + if (stackName !== undefined) { + if (!stackName || !stackName.trim()) { + resolvedStackId = null; + } else { + const st = await prisma.notebookStack.upsert({ + where: { userId_name: { userId, name: stackName.trim() } }, + update: {}, + create: { userId, name: stackName.trim() }, + }); + resolvedStackId = st.id; + } + } + + if (isDefault === true) { + await prisma.notebook.updateMany({ + where: { userId }, + data: { isDefault: false }, + }); + } + + const updated = await prisma.notebook.update({ + where: { id }, + data: { + ...(name !== undefined ? { name: name.trim() } : {}), + ...(resolvedStackId !== undefined ? { stackId: resolvedStackId } : {}), + ...(isDefault !== undefined ? { isDefault: Boolean(isDefault) } : {}), + ...(isShortcut !== undefined ? { isShortcut: Boolean(isShortcut) } : {}), + }, + }); + + return NextResponse.json(updated); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to update notebook' }, + { status: 500 } + ); + } +} + +export async function DELETE(req: NextRequest) { + try { + const userId = await getCurrentUserId(); + const id = req.nextUrl.searchParams.get('id'); + const type = req.nextUrl.searchParams.get('type'); + if (!id) { + return NextResponse.json({ error: 'ID is required' }, { status: 400 }); + } + + if (type === 'stack') { + await prisma.notebookStack.delete({ where: { id } }); + return NextResponse.json({ deleted: true }); + } + + const defaultNb = await ensureDefaultNotebook(userId); + if (defaultNb.id === id) { + return NextResponse.json( + { error: 'Cannot delete the default notebook' }, + { status: 400 } + ); + } + + // Move notes in this notebook to the default notebook before deleting + await prisma.note.updateMany({ + where: { notebookId: id }, + data: { notebookId: defaultNb.id }, + }); + + await prisma.notebook.delete({ where: { id } }); + return NextResponse.json({ deleted: true }); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to delete notebook' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/notes/[id]/route.ts b/src/app/api/notes/[id]/route.ts new file mode 100644 index 0000000..69c65e4 --- /dev/null +++ b/src/app/api/notes/[id]/route.ts @@ -0,0 +1,160 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { stripHtmlToText, makeExcerpt } from '@/lib/storage'; +import { getOrCreateTagIds } from '@/lib/importers'; +import { Prisma } from '@prisma/client'; + +export async function GET( + _req: NextRequest, + context: { params: Promise<{ id: string }> } +) { + try { + const { id } = await context.params; + const note = await prisma.note.findUnique({ + where: { id }, + include: { + notebook: true, + tags: { include: { tag: true } }, + attachments: { orderBy: { createdAt: 'asc' } }, + }, + }); + + if (!note) { + return NextResponse.json({ error: 'Note not found' }, { status: 404 }); + } + + return NextResponse.json({ + ...note, + tags: note.tags.map((nt) => nt.tag), + }); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to fetch note' }, + { status: 500 } + ); + } +} + +export async function PATCH( + req: NextRequest, + context: { params: Promise<{ id: string }> } +) { + try { + const { id } = await context.params; + const body = await req.json(); + + const data: Prisma.NoteUncheckedUpdateInput = {}; + let shouldBumpUpdatedAt = false; + + if (body.title !== undefined) { + data.title = body.title; + shouldBumpUpdatedAt = true; + } + if (body.contentHtml !== undefined) { + data.contentHtml = body.contentHtml; + const text = stripHtmlToText(body.contentHtml); + data.contentText = text; + data.excerpt = makeExcerpt(text); + shouldBumpUpdatedAt = true; + } + if (body.notebookId !== undefined) { + data.notebookId = body.notebookId; + } + if (body.isPinned !== undefined) { + data.isPinned = Boolean(body.isPinned); + } + if (body.isShortcut !== undefined) { + data.isShortcut = Boolean(body.isShortcut); + } + if (body.isTrashed !== undefined) { + data.isTrashed = Boolean(body.isTrashed); + data.trashedAt = body.isTrashed ? new Date() : null; + } + if (body.sourceUrl !== undefined) { + data.sourceUrl = body.sourceUrl || null; + } + if (body.thumbnailUrl !== undefined) { + data.thumbnailUrl = body.thumbnailUrl || null; + } + if (body.author !== undefined) { + data.author = body.author || null; + } + if (body.reminderAt !== undefined) { + data.reminderAt = body.reminderAt ? new Date(body.reminderAt) : null; + } + if (body.reminderDone !== undefined) { + data.reminderDone = Boolean(body.reminderDone); + } + if (body.createdAt !== undefined && body.createdAt) { + const d = new Date(body.createdAt); + if (!isNaN(d.getTime())) data.createdAt = d; + } + if (body.updatedAt !== undefined && body.updatedAt) { + const d = new Date(body.updatedAt); + if (!isNaN(d.getTime())) { + data.updatedAt = d; + shouldBumpUpdatedAt = false; + } + } else if (shouldBumpUpdatedAt) { + data.updatedAt = new Date(); + } + + if (Array.isArray(body.tagNames)) { + const tagIds = await getOrCreateTagIds(body.tagNames); + await prisma.noteTag.deleteMany({ where: { noteId: id } }); + if (tagIds.length > 0) { + await prisma.noteTag.createMany({ + data: tagIds.map((tagId) => ({ noteId: id, tagId })), + skipDuplicates: true, + }); + } + } + + const updated = await prisma.note.update({ + where: { id }, + data, + include: { + notebook: true, + tags: { include: { tag: true } }, + attachments: { orderBy: { createdAt: 'asc' } }, + }, + }); + + return NextResponse.json({ + ...updated, + tags: updated.tags.map((nt) => nt.tag), + }); + } catch (err: unknown) { + console.error('PATCH /api/notes/[id] error:', err); + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to update note' }, + { status: 500 } + ); + } +} + +export async function DELETE( + req: NextRequest, + context: { params: Promise<{ id: string }> } +) { + try { + const { id } = await context.params; + const permanent = req.nextUrl.searchParams.get('permanent') === 'true'; + + if (permanent) { + await prisma.note.delete({ where: { id } }); + return NextResponse.json({ deleted: true, permanent: true }); + } else { + const trashed = await prisma.note.update({ + where: { id }, + data: { isTrashed: true, trashedAt: new Date() }, + }); + return NextResponse.json({ deleted: true, permanent: false, note: trashed }); + } + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to delete note' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/notes/route.ts b/src/app/api/notes/route.ts new file mode 100644 index 0000000..b9c3faf --- /dev/null +++ b/src/app/api/notes/route.ts @@ -0,0 +1,175 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma, getCurrentUserId, ensureDefaultNotebook } from '@/lib/prisma'; +import { buildNoteWhereClause } from '@/lib/search'; +import { stripHtmlToText, makeExcerpt } from '@/lib/storage'; +import { getOrCreateTagIds } from '@/lib/importers'; +import { Prisma } from '@prisma/client'; + +export async function GET(req: NextRequest) { + try { + const userId = await getCurrentUserId(); + const sp = req.nextUrl.searchParams; + const query = sp.get('q') || undefined; + const notebookId = sp.get('notebookId') || undefined; + const stackId = sp.get('stackId') || undefined; + const tagId = sp.get('tagId') || undefined; + const tagIdsRaw = sp.get('tagIds') || ''; + const tagIds = tagIdsRaw ? tagIdsRaw.split(',').filter(Boolean) : undefined; + const untagged = sp.get('untagged') === 'true'; + const hasImages = sp.get('hasImages') === 'true'; + const hasAttachments = sp.get('hasAttachments') === 'true'; + const hasTodo = sp.get('hasTodo') === 'true'; + const hasWebClip = sp.get('hasWebClip') === 'true'; + const onlyPinned = sp.get('onlyPinned') === 'true'; + const updatedSinceDays = parseInt(sp.get('updatedSinceDays') || '0', 10) || undefined; + const trashed = sp.get('trashed') === 'true'; + const shortcut = sp.get('shortcut') === 'true'; + const sortBy = sp.get('sortBy') || 'updatedAt'; + const sortDir = (sp.get('sortDir') === 'asc' ? 'asc' : 'desc') as 'asc' | 'desc'; + const limit = Math.min(parseInt(sp.get('limit') || '500', 10) || 500, 2000); + const offset = parseInt(sp.get('offset') || '0', 10) || 0; + + const where = buildNoteWhereClause({ + userId, + query, + notebookId, + stackId, + tagId, + tagIds, + untagged, + hasImages, + hasAttachments, + hasTodo, + hasWebClip, + onlyPinned, + updatedSinceDays, + trashed, + shortcut, + }); + + const orderBy: Prisma.NoteOrderByWithRelationInput[] = [ + { isPinned: 'desc' }, + sortBy === 'title' + ? { title: sortDir } + : sortBy === 'createdAt' + ? { createdAt: sortDir } + : { updatedAt: sortDir }, + ]; + + const [notes, total] = await Promise.all([ + prisma.note.findMany({ + where, + orderBy, + take: limit, + skip: offset, + select: { + id: true, + title: true, + excerpt: true, + notebookId: true, + isPinned: true, + isShortcut: true, + isTrashed: true, + thumbnailUrl: true, + sourceUrl: true, + reminderAt: true, + reminderDone: true, + createdAt: true, + updatedAt: true, + notebook: { + select: { id: true, name: true, stackId: true }, + }, + tags: { + select: { + tag: { select: { id: true, name: true, color: true } }, + }, + }, + attachments: { + select: { + id: true, + filename: true, + mimeType: true, + sizeBytes: true, + }, + take: 12, + }, + }, + }), + prisma.note.count({ where }), + ]); + + return NextResponse.json({ + notes: notes.map((n) => ({ + ...n, + tags: n.tags.map((nt) => nt.tag), + })), + total, + }); + } catch (err: unknown) { + console.error('GET /api/notes error:', err); + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to list notes' }, + { status: 500 } + ); + } +} + +export async function POST(req: NextRequest) { + try { + const userId = await getCurrentUserId(); + const body = await req.json(); + + // Support bulk actions such as emptyTrash + if (body.action === 'emptyTrash') { + const deleted = await prisma.note.deleteMany({ + where: { userId, isTrashed: true }, + }); + return NextResponse.json({ deletedCount: deleted.count }); + } + + const defaultNb = await ensureDefaultNotebook(userId); + const notebookId = body.notebookId || defaultNb.id; + const title = body.title?.trim() || 'Untitled Note'; + const contentHtml = body.contentHtml || '

'; + const contentText = stripHtmlToText(contentHtml); + const excerpt = makeExcerpt(contentText); + + const tagNames: string[] = Array.isArray(body.tagNames) ? body.tagNames : []; + const tagIds = await getOrCreateTagIds(tagNames, userId); + + const note = await prisma.note.create({ + data: { + userId, + title, + contentHtml, + contentText, + excerpt, + notebookId, + isPinned: Boolean(body.isPinned), + isShortcut: Boolean(body.isShortcut), + thumbnailUrl: body.thumbnailUrl || null, + sourceUrl: body.sourceUrl || null, + author: body.author || null, + tags: { + create: tagIds.map((tagId) => ({ tagId })), + }, + }, + include: { + notebook: true, + tags: { include: { tag: true } }, + attachments: true, + }, + }); + + return NextResponse.json({ + ...note, + tags: note.tags.map((nt) => nt.tag), + }); + } catch (err: unknown) { + console.error('POST /api/notes error:', err); + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to create note' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/saved-searches/route.ts b/src/app/api/saved-searches/route.ts new file mode 100644 index 0000000..e7e058e --- /dev/null +++ b/src/app/api/saved-searches/route.ts @@ -0,0 +1,50 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma, getCurrentUserId } from '@/lib/prisma'; + +export async function POST(req: NextRequest) { + try { + const userId = await getCurrentUserId(); + const body = await req.json(); + const name = body.name?.trim(); + const query = body.query?.trim(); + if (!name || !query) { + return NextResponse.json( + { error: 'Name and query are required' }, + { status: 400 } + ); + } + + const saved = await prisma.savedSearch.upsert({ + where: { userId_name: { userId, name } }, + update: { query, isShortcut: Boolean(body.isShortcut) }, + create: { + userId, + name, + query, + isShortcut: Boolean(body.isShortcut ?? true), + }, + }); + return NextResponse.json(saved); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to save search' }, + { status: 500 } + ); + } +} + +export async function DELETE(req: NextRequest) { + try { + const id = req.nextUrl.searchParams.get('id'); + if (!id) { + return NextResponse.json({ error: 'ID is required' }, { status: 400 }); + } + await prisma.savedSearch.delete({ where: { id } }); + return NextResponse.json({ deleted: true }); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to delete saved search' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/sidebar/route.ts b/src/app/api/sidebar/route.ts new file mode 100644 index 0000000..ccd3a1c --- /dev/null +++ b/src/app/api/sidebar/route.ts @@ -0,0 +1,93 @@ +import { NextResponse } from 'next/server'; +import { prisma, getCurrentUser, ensureDefaultNotebook } from '@/lib/prisma'; + +export async function GET() { + try { + const currentUser = await getCurrentUser(); + const userId = currentUser.id; + await ensureDefaultNotebook(userId); + + const [ + users, + stacks, + notebooks, + tags, + savedSearches, + shortcutNotes, + allNotesCount, + trashCount, + ] = await Promise.all([ + prisma.user.findMany({ + orderBy: { createdAt: 'asc' }, + }), + prisma.notebookStack.findMany({ + where: { userId }, + orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }], + }), + prisma.notebook.findMany({ + where: { userId }, + orderBy: [{ isDefault: 'desc' }, { sortOrder: 'asc' }, { name: 'asc' }], + include: { + _count: { + select: { + notes: { where: { isTrashed: false } }, + }, + }, + }, + }), + prisma.tag.findMany({ + where: { userId }, + orderBy: { name: 'asc' }, + include: { + _count: { + select: { + notes: { where: { note: { isTrashed: false, userId } } }, + }, + }, + }, + }), + prisma.savedSearch.findMany({ + where: { userId }, + orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }], + }), + prisma.note.findMany({ + where: { userId, isShortcut: true, isTrashed: false }, + select: { id: true, title: true, notebookId: true, updatedAt: true }, + orderBy: { updatedAt: 'desc' }, + }), + prisma.note.count({ where: { userId, isTrashed: false } }), + prisma.note.count({ where: { userId, isTrashed: true } }), + ]); + + return NextResponse.json({ + currentUser, + users, + stacks, + notebooks: notebooks.map((nb) => ({ + ...nb, + noteCount: nb._count.notes, + })), + tags: tags.map((t) => ({ + ...t, + noteCount: t._count.notes, + })), + savedSearches, + shortcuts: { + notes: shortcutNotes, + notebooks: notebooks.filter((n) => n.isShortcut), + tags: tags.filter((t) => t.isShortcut), + savedSearches: savedSearches.filter((s) => s.isShortcut), + }, + counts: { + allNotes: allNotesCount, + trash: trashCount, + }, + }); + } catch (err: unknown) { + console.error('Error loading sidebar:', err); + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to load sidebar' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/tags/route.ts b/src/app/api/tags/route.ts new file mode 100644 index 0000000..ba0b5a9 --- /dev/null +++ b/src/app/api/tags/route.ts @@ -0,0 +1,79 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma, getCurrentUserId } from '@/lib/prisma'; + +export async function POST(req: NextRequest) { + try { + const userId = await getCurrentUserId(); + const body = await req.json(); + const name = body.name?.trim(); + if (!name) { + return NextResponse.json({ error: 'Tag name is required' }, { status: 400 }); + } + + const tag = await prisma.tag.upsert({ + where: { userId_name: { userId, name } }, + update: { + ...(body.color ? { color: body.color } : {}), + ...(body.parentId !== undefined ? { parentId: body.parentId } : {}), + }, + create: { + userId, + name, + color: body.color || '#00a82d', + parentId: body.parentId || null, + isShortcut: Boolean(body.isShortcut), + }, + }); + + return NextResponse.json(tag); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to create tag' }, + { status: 500 } + ); + } +} + +export async function PATCH(req: NextRequest) { + try { + const body = await req.json(); + const { id, name, color, parentId, isShortcut } = body; + if (!id) { + return NextResponse.json({ error: 'Tag ID required' }, { status: 400 }); + } + + const updated = await prisma.tag.update({ + where: { id }, + data: { + ...(name !== undefined ? { name: name.trim() } : {}), + ...(color !== undefined ? { color } : {}), + ...(parentId !== undefined ? { parentId: parentId || null } : {}), + ...(isShortcut !== undefined ? { isShortcut: Boolean(isShortcut) } : {}), + }, + }); + + return NextResponse.json(updated); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to update tag' }, + { status: 500 } + ); + } +} + +export async function DELETE(req: NextRequest) { + try { + const id = req.nextUrl.searchParams.get('id'); + if (!id) { + return NextResponse.json({ error: 'Tag ID required' }, { status: 400 }); + } + + await prisma.tag.delete({ where: { id } }); + return NextResponse.json({ deleted: true }); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to delete tag' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts new file mode 100644 index 0000000..8a51fe2 --- /dev/null +++ b/src/app/api/users/route.ts @@ -0,0 +1,133 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma, getCurrentUser, ensureDefaultNotebook } from '@/lib/prisma'; + +const AVATAR_COLORS = [ + '#00a82d', + '#2563eb', + '#7c3aed', + '#db2777', + '#ea580c', + '#0d9488', + '#4f46e5', +]; + +export async function GET() { + try { + const currentUser = await getCurrentUser(); + const users = await prisma.user.findMany({ + orderBy: { createdAt: 'asc' }, + }); + + const usersWithCounts = await Promise.all( + users.map(async (u) => { + const noteCount = await prisma.note.count({ + where: { userId: u.id, isTrashed: false }, + }); + return { ...u, noteCount }; + }) + ); + + return NextResponse.json({ + currentUser, + users: usersWithCounts, + }); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to load users' }, + { status: 500 } + ); + } +} + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + + // Switch active user profile + if (body.action === 'switch' && body.userId) { + const target = await prisma.user.findUnique({ + where: { id: String(body.userId) }, + }); + if (!target) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + await ensureDefaultNotebook(target.id); + + const res = NextResponse.json({ currentUser: target }); + res.cookies.set('evc_user_id', target.id, { + path: '/', + maxAge: 60 * 60 * 24 * 365, + sameSite: 'lax', + }); + return res; + } + + // Create new user profile + const name = body.name?.trim(); + const email = + body.email?.trim()?.toLowerCase() || + `${(name || 'user').toLowerCase().replace(/[^a-z0-9]+/g, '.')}@everclone.local`; + + if (!name) { + return NextResponse.json( + { error: 'Display name is required' }, + { status: 400 } + ); + } + + const count = await prisma.user.count(); + const avatarColor = + body.avatarColor || AVATAR_COLORS[count % AVATAR_COLORS.length]; + + const user = await prisma.user.upsert({ + where: { email }, + update: { name, avatarColor }, + create: { + name, + email, + avatarColor, + }, + }); + + await ensureDefaultNotebook(user.id); + + const res = NextResponse.json({ currentUser: user, user }); + if (body.switchImmediately !== false) { + res.cookies.set('evc_user_id', user.id, { + path: '/', + maxAge: 60 * 60 * 24 * 365, + sameSite: 'lax', + }); + } + return res; + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to save user' }, + { status: 500 } + ); + } +} + +export async function PATCH(req: NextRequest) { + try { + const body = await req.json(); + const { id, name, email, avatarColor } = body; + if (!id) { + return NextResponse.json({ error: 'User ID required' }, { status: 400 }); + } + const updated = await prisma.user.update({ + where: { id }, + data: { + ...(name !== undefined ? { name: name.trim() } : {}), + ...(email !== undefined ? { email: email.trim().toLowerCase() } : {}), + ...(avatarColor !== undefined ? { avatarColor } : {}), + }, + }); + return NextResponse.json(updated); + } catch (err: unknown) { + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to update user' }, + { status: 500 } + ); + } +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..53c6845 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,195 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + body { + @apply bg-white text-gray-900 antialiased select-none; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + } +} + +/* Custom thin scrollbars */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: rgba(156, 163, 175, 0.45); + border-radius: 3px; +} +::-webkit-scrollbar-thumb:hover { + background: rgba(107, 114, 128, 0.7); +} + +/* TipTap / ProseMirror Evernote Editor Styles */ +.ProseMirror { + outline: none; + min-height: 420px; + line-height: 1.65; + color: #1f2937; + user-select: text; + font-size: 0.975rem; +} + +.ProseMirror p.is-editor-empty:first-child::before { + color: #9ca3af; + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; +} + +.ProseMirror h1 { + font-size: 1.75rem; + font-weight: 700; + margin-top: 1.25rem; + margin-bottom: 0.5rem; + line-height: 1.25; +} + +.ProseMirror h2 { + font-size: 1.375rem; + font-weight: 600; + margin-top: 1.1rem; + margin-bottom: 0.45rem; + line-height: 1.3; +} + +.ProseMirror h3 { + font-size: 1.125rem; + font-weight: 600; + margin-top: 0.9rem; + margin-bottom: 0.35rem; +} + +.ProseMirror p { + margin-bottom: 0.65rem; +} + +.ProseMirror ul { + list-style-type: disc; + padding-left: 1.5rem; + margin-bottom: 0.75rem; +} + +.ProseMirror ol { + list-style-type: decimal; + padding-left: 1.5rem; + margin-bottom: 0.75rem; +} + +/* TipTap Task List (Evernote Checklists) */ +.ProseMirror ul[data-type="taskList"] { + list-style: none; + padding: 0; + margin-bottom: 0.75rem; +} + +.ProseMirror ul[data-type="taskList"] li { + display: flex; + align-items: flex-start; + gap: 0.55rem; + margin-bottom: 0.3rem; +} + +.ProseMirror ul[data-type="taskList"] li > label { + flex: 0 0 auto; + margin-top: 0.25rem; + user-select: none; +} + +.ProseMirror ul[data-type="taskList"] li > label input[type="checkbox"] { + width: 1rem; + height: 1rem; + accent-color: #00a82d; + cursor: pointer; +} + +.ProseMirror ul[data-type="taskList"] li > div { + flex: 1 1 auto; +} + +.ProseMirror ul[data-type="taskList"] li[data-checked="true"] > div { + text-decoration: line-through; + color: #9ca3af; +} + +.ProseMirror blockquote { + border-left: 3px solid #00a82d; + padding-left: 1rem; + color: #4b5563; + font-style: italic; + margin: 0.85rem 0; +} + +.ProseMirror pre { + background: #1e293b; + color: #f8fafc; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + padding: 0.85rem 1rem; + border-radius: 0.5rem; + overflow-x: auto; + margin: 0.85rem 0; + font-size: 0.875rem; +} + +.ProseMirror code { + background: #f1f5f9; + color: #0f172a; + padding: 0.15rem 0.35rem; + border-radius: 0.25rem; + font-size: 0.875em; +} + +.ProseMirror pre code { + background: transparent; + color: inherit; + padding: 0; +} + +.ProseMirror hr { + border: none; + border-top: 1px solid #e5e7eb; + margin: 1.25rem 0; +} + +.ProseMirror a { + color: #00a82d; + text-decoration: underline; + cursor: pointer; +} + +.ProseMirror img { + max-width: 100%; + height: auto; + border-radius: 0.375rem; + margin: 0.5rem 0; +} + +.ProseMirror table { + border-collapse: collapse; + table-layout: fixed; + width: 100%; + margin: 1rem 0; + overflow: hidden; +} + +.ProseMirror td, +.ProseMirror th { + min-width: 1em; + border: 1px solid #d1d5db; + padding: 0.45rem 0.65rem; + vertical-align: top; + box-sizing: border-box; + position: relative; +} + +.ProseMirror th { + font-weight: 600; + text-align: left; + background-color: #f9fafb; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..9a11ea6 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,23 @@ +import type { Metadata } from 'next'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'Everclone β€” Notes, Web Clips & Knowledge Base', + description: 'Self-hosted modern Evernote clone with rich media, ENEX import, and Web/Android hooks', + manifest: '/manifest.json', + icons: { + icon: '/icon.svg', + }, +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..cacfb16 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,8 @@ +import EvercloneWorkspace from '@/components/EvercloneWorkspace'; + +export const dynamic = 'force-dynamic'; +export const revalidate = 0; + +export default function HomePage() { + return ; +} diff --git a/src/components/EvercloneWorkspace.tsx b/src/components/EvercloneWorkspace.tsx new file mode 100644 index 0000000..d77a8ab --- /dev/null +++ b/src/components/EvercloneWorkspace.tsx @@ -0,0 +1,3008 @@ +'use client'; + +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { formatDistanceToNow, format } from 'date-fns'; +import { + Plus, + Search, + Star, + FileText, + Book, + Folder, + Tag as TagIcon, + Bookmark, + Trash2, + Upload, + Smartphone, + Pin, + Info, + Download, + ChevronRight, + ChevronDown, + MoreHorizontal, + ExternalLink, + Paperclip, + RotateCcw, + Clock, + Check, + X, + PanelLeftClose, + PanelLeftOpen, + LayoutList, + LayoutGrid, + AlignJustify, + Sparkles, + Settings, + Image as ImageIcon, + Crop, + Filter, + CheckSquare, + Globe, + Menu, + ArrowLeft, +} from 'lucide-react'; +import { + SidebarData, + NoteSummary, + NoteDetail, + NotebookItem, +} from '@/types/everclone'; +import RichNoteEditor from './RichNoteEditor'; +import SettingsView from './SettingsView'; +import ThumbnailEditorModal from './ThumbnailEditorModal'; +import { PdfAutoThumbnail, InlinePdfViewerCard } from './PdfNoteComponents'; + +export default function EvercloneWorkspace() { + const [sidebar, setSidebar] = useState({ + stacks: [], + notebooks: [], + tags: [], + savedSearches: [], + shortcuts: { notes: [], notebooks: [], tags: [], savedSearches: [] }, + counts: { allNotes: 0, trash: 0 }, + }); + + // Navigation filter state + const [activeView, setActiveView] = useState< + 'all' | 'shortcuts' | 'notebook' | 'stack' | 'tag' | 'trash' | 'settings' + >('all'); + const [selectedNotebookId, setSelectedNotebookId] = useState(''); + const [selectedStackId, setSelectedStackId] = useState(''); + const [selectedTagId, setSelectedTagId] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [debouncedSearch, setDebouncedSearch] = useState(''); + const [sortBy, setSortBy] = useState<'updatedAt' | 'createdAt' | 'title'>('updatedAt'); + const [sortDir, setSortDir] = useState<'desc' | 'asc'>('desc'); + const [listMode, setListMode] = useState<'snippets' | 'cards' | 'compact'>('snippets'); + + // Interactive Filter Bar state + const [showFilterPanel, setShowFilterPanel] = useState(false); + const [tagFilterDropdownOpen, setTagFilterDropdownOpen] = useState(false); + const [tagFilterSearch, setTagFilterSearch] = useState(''); + const [sidebarTagSearch, setSidebarTagSearch] = useState(''); + const [filterNotebookId, setFilterNotebookId] = useState(''); + const [filterTagIds, setFilterTagIds] = useState([]); + const [filterUntagged, setFilterUntagged] = useState(false); + const [filterHasImages, setFilterHasImages] = useState(false); + const [filterHasAttachments, setFilterHasAttachments] = useState(false); + const [filterHasTodo, setFilterHasTodo] = useState(false); + const [filterHasWebClip, setFilterHasWebClip] = useState(false); + const [filterOnlyPinned, setFilterOnlyPinned] = useState(false); + const [filterUpdatedDays, setFilterUpdatedDays] = useState(0); + + // Multi-user profile switcher state + const [userMenuOpen, setUserMenuOpen] = useState(false); + const [creatingUser, setCreatingUser] = useState(false); + const [newUserName, setNewUserName] = useState(''); + const [newUserEmail, setNewUserEmail] = useState(''); + + const activeFilterCount = + (filterNotebookId ? 1 : 0) + + filterTagIds.length + + (filterUntagged ? 1 : 0) + + (filterHasImages ? 1 : 0) + + (filterHasAttachments ? 1 : 0) + + (filterHasTodo ? 1 : 0) + + (filterHasWebClip ? 1 : 0) + + (filterOnlyPinned ? 1 : 0) + + (filterUpdatedDays > 0 ? 1 : 0); + + const clearAllFilters = () => { + setFilterNotebookId(''); + setFilterTagIds([]); + setFilterUntagged(false); + setFilterHasImages(false); + setFilterHasAttachments(false); + setFilterHasTodo(false); + setFilterHasWebClip(false); + setFilterOnlyPinned(false); + setFilterUpdatedDays(0); + setSearchQuery(''); + }; + + // UI collapse & modals + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [mobilePane, setMobilePane] = useState<'list' | 'editor'>('list'); + const [expandedSections, setExpandedSections] = useState>({ + shortcuts: true, + notebooks: true, + tags: false, + savedSearches: false, + }); + const [expandedStacks, setExpandedStacks] = useState>({}); + const [showNoteInfo, setShowNoteInfo] = useState(false); + const [showExportMenu, setShowExportMenu] = useState(false); + const [attachmentsExpanded, setAttachmentsExpanded] = useState(false); + const [thumbnailEditorOpen, setThumbnailEditorOpen] = useState(false); + const [thumbnailInitialUrl, setThumbnailInitialUrl] = useState(null); + + // Notes list & active note detail + const [notes, setNotes] = useState([]); + const [totalNotes, setTotalNotes] = useState(0); + const [loadingList, setLoadingList] = useState(true); + const [selectedNoteId, setSelectedNoteId] = useState(''); + const [activeNote, setActiveNote] = useState(null); + const [saveState, setSaveState] = useState<'saved' | 'saving' | 'dirty'>('saved'); + + // Tag input in bottom bar + const [tagInput, setTagInput] = useState(''); + + // Quick creation dialogs in sidebar + const [creatingNotebook, setCreatingNotebook] = useState(false); + const [newNotebookName, setNewNotebookName] = useState(''); + const [newNotebookStack, setNewNotebookStack] = useState(''); + const [creatingTag, setCreatingTag] = useState(false); + const [newTagName, setNewTagName] = useState(''); + + const searchInputRef = useRef(null); + const saveTimerRef = useRef(null); + const listParentRef = useRef(null); + + // Debounce search query + useEffect(() => { + const t = setTimeout(() => setDebouncedSearch(searchQuery), 220); + return () => clearTimeout(t); + }, [searchQuery]); + + // Keyboard shortcut Ctrl+K for search, Alt+N for new note + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { + e.preventDefault(); + searchInputRef.current?.focus(); + } + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, []); + + const fetchSidebar = useCallback(async () => { + try { + const res = await fetch('/api/sidebar'); + if (!res.ok) return; + const data: SidebarData = await res.json(); + setSidebar(data); + } catch (e) { + console.error('Failed to fetch sidebar:', e); + } + }, []); + + const fetchNotesList = useCallback( + async (preferSelectNoteId?: string) => { + setLoadingList(true); + try { + const sp = new URLSearchParams(); + if (debouncedSearch.trim()) sp.set('q', debouncedSearch.trim()); + if (activeView === 'notebook' && selectedNotebookId) { + sp.set('notebookId', selectedNotebookId); + } else if (filterNotebookId) { + sp.set('notebookId', filterNotebookId); + } + if (activeView === 'stack' && selectedStackId) { + sp.set('stackId', selectedStackId); + } + if (activeView === 'tag' && selectedTagId) { + sp.set('tagId', selectedTagId); + } + if (filterTagIds.length > 0) { + sp.set('tagIds', filterTagIds.join(',')); + } + if (filterUntagged) sp.set('untagged', 'true'); + if (filterHasImages) sp.set('hasImages', 'true'); + if (filterHasAttachments) sp.set('hasAttachments', 'true'); + if (filterHasTodo) sp.set('hasTodo', 'true'); + if (filterHasWebClip) sp.set('hasWebClip', 'true'); + if (filterOnlyPinned) sp.set('onlyPinned', 'true'); + if (filterUpdatedDays > 0) sp.set('updatedSinceDays', String(filterUpdatedDays)); + if (activeView === 'trash') { + sp.set('trashed', 'true'); + } else if (activeView === 'shortcuts') { + sp.set('shortcut', 'true'); + } + sp.set('sortBy', sortBy); + sp.set('sortDir', sortDir); + sp.set('limit', '1000'); + + const res = await fetch(`/api/notes?${sp.toString()}`); + if (!res.ok) return; + const data = await res.json(); + const loadedNotes: NoteSummary[] = data.notes || []; + setNotes(loadedNotes); + setTotalNotes(data.total || 0); + + if (preferSelectNoteId) { + setSelectedNoteId(preferSelectNoteId); + } else if (loadedNotes.length > 0) { + setSelectedNoteId((prev) => + prev && loadedNotes.some((n) => n.id === prev) ? prev : loadedNotes[0].id + ); + } else { + setSelectedNoteId(''); + setActiveNote(null); + } + } catch (e) { + console.error('Failed to fetch notes:', e); + } finally { + setLoadingList(false); + } + }, + [ + debouncedSearch, + activeView, + selectedNotebookId, + selectedStackId, + selectedTagId, + filterNotebookId, + filterTagIds, + filterUntagged, + filterHasImages, + filterHasAttachments, + filterHasTodo, + filterHasWebClip, + filterOnlyPinned, + filterUpdatedDays, + sortBy, + sortDir, + ] + ); + + useEffect(() => { + fetchSidebar(); + }, [fetchSidebar]); + + useEffect(() => { + fetchNotesList(); + }, [fetchNotesList]); + + // Fetch full note details when selectedNoteId changes + useEffect(() => { + if (!selectedNoteId) { + setActiveNote(null); + return; + } + let cancelled = false; + (async () => { + try { + const res = await fetch(`/api/notes/${selectedNoteId}`); + if (!res.ok) return; + const data: NoteDetail = await res.json(); + if (!cancelled) { + setActiveNote(data); + setSaveState('saved'); + } + } catch (e) { + console.error('Error loading note detail:', e); + } + })(); + return () => { + cancelled = true; + }; + }, [selectedNoteId]); + + // Auto-save note edits with debounce + const scheduleNoteSave = useCallback( + (noteId: string, patch: Record) => { + setSaveState('dirty'); + if (saveTimerRef.current) clearTimeout(saveTimerRef.current); + + saveTimerRef.current = setTimeout(async () => { + setSaveState('saving'); + try { + const res = await fetch(`/api/notes/${noteId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(patch), + }); + if (res.ok) { + const updated: NoteDetail = await res.json(); + setNotes((prev) => + prev.map((n) => + n.id === noteId + ? { + ...n, + title: updated.title, + excerpt: updated.excerpt, + notebookId: updated.notebookId, + notebook: updated.notebook, + isPinned: updated.isPinned, + isShortcut: updated.isShortcut, + thumbnailUrl: updated.thumbnailUrl, + sourceUrl: updated.sourceUrl, + updatedAt: updated.updatedAt, + tags: updated.tags, + } + : n + ) + ); + setSaveState('saved'); + } + } catch (e) { + console.error('Auto-save failed:', e); + } + }, 550); + }, + [] + ); + + const handleCreateNote = async () => { + const targetNbId = + activeView === 'notebook' && selectedNotebookId + ? selectedNotebookId + : sidebar.notebooks.find((n) => n.isDefault)?.id || sidebar.notebooks[0]?.id; + + const activeTag = + activeView === 'tag' && selectedTagId + ? sidebar.tags.find((t) => t.id === selectedTagId)?.name + : undefined; + + const res = await fetch('/api/notes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: 'Untitled Note', + contentHtml: '

', + notebookId: targetNbId, + tagNames: activeTag ? [activeTag] : [], + }), + }); + + if (res.ok) { + const created: NoteDetail = await res.json(); + if (activeView === 'trash' || activeView === 'settings') setActiveView('all'); + setMobileMenuOpen(false); + openNoteInEditor(created.id); + await fetchSidebar(); + await fetchNotesList(created.id); + setActiveNote(created); + } + }; + + const handleImmediatePatch = async (patch: Record) => { + if (!activeNote) return; + const res = await fetch(`/api/notes/${activeNote.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(patch), + }); + if (res.ok) { + const updated: NoteDetail = await res.json(); + setActiveNote(updated); + await fetchSidebar(); + await fetchNotesList(updated.id); + } + }; + + const handleDeleteOrTrashNote = async (permanent = false) => { + if (!activeNote) return; + if (permanent && !window.confirm('Permanently delete this note? This cannot be undone.')) { + return; + } + await fetch(`/api/notes/${activeNote.id}?permanent=${permanent ? 'true' : 'false'}`, { + method: 'DELETE', + }); + await fetchSidebar(); + await fetchNotesList(); + }; + + const handleEmptyTrash = async () => { + if (!window.confirm('Permanently delete all notes in Trash?')) return; + await fetch('/api/notes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'emptyTrash' }), + }); + await fetchSidebar(); + await fetchNotesList(); + }; + + const handleUploadAttachments = async (files: File[]) => { + if (!activeNote) return []; + const formData = new FormData(); + formData.append('noteId', activeNote.id); + for (const f of files) { + formData.append('files', f); + } + const res = await fetch('/api/attachments', { + method: 'POST', + body: formData, + }); + if (!res.ok) return []; + const data = await res.json(); + const newAttachments = data.attachments || []; + setActiveNote((prev) => + prev + ? { + ...prev, + attachments: [...prev.attachments, ...newAttachments], + } + : prev + ); + return newAttachments; + }; + + const handleDeleteAttachment = async (attId: string) => { + await fetch(`/api/attachments/${attId}`, { method: 'DELETE' }); + setActiveNote((prev) => + prev + ? { + ...prev, + attachments: prev.attachments.filter((a) => a.id !== attId), + } + : prev + ); + }; + + const handleAddTagToNote = async (rawTagName: string) => { + const clean = rawTagName.trim(); + if (!clean || !activeNote) return; + const existingNames = activeNote.tags.map((t) => t.name); + if (existingNames.some((n) => n.toLowerCase() === clean.toLowerCase())) { + setTagInput(''); + return; + } + const updatedNames = [...existingNames, clean]; + setTagInput(''); + await handleImmediatePatch({ tagNames: updatedNames }); + }; + + const handleRemoveTagFromNote = async (tagName: string) => { + if (!activeNote) return; + const updatedNames = activeNote.tags + .map((t) => t.name) + .filter((n) => n !== tagName); + await handleImmediatePatch({ tagNames: updatedNames }); + }; + + const handleCreateNotebook = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newNotebookName.trim()) return; + await fetch('/api/notebooks', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: newNotebookName.trim(), + stackName: newNotebookStack.trim() || undefined, + }), + }); + setNewNotebookName(''); + setNewNotebookStack(''); + setCreatingNotebook(false); + await fetchSidebar(); + }; + + const handleCreateTag = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newTagName.trim()) return; + await fetch('/api/tags', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: newTagName.trim() }), + }); + setNewTagName(''); + setCreatingTag(false); + await fetchSidebar(); + }; + + // Android / Browser Back Button support when zoomed into a note or drawer on mobile + const openNoteInEditor = useCallback( + (noteId: string) => { + setSelectedNoteId(noteId); + if (typeof window !== 'undefined' && window.innerWidth < 768 && mobilePane !== 'editor') { + window.history.pushState({ evcMobilePane: 'editor', noteId }, ''); + } + setMobilePane('editor'); + }, + [mobilePane] + ); + + useEffect(() => { + const onPopState = () => { + if (mobileMenuOpen) { + setMobileMenuOpen(false); + } else if (activeView === 'settings') { + setActiveView('all'); + } else if (mobilePane === 'editor') { + setMobilePane('list'); + } + }; + window.addEventListener('popstate', onPopState); + return () => window.removeEventListener('popstate', onPopState); + }, [mobileMenuOpen, activeView, mobilePane]); + + const handleSaveCurrentSearch = async () => { + if (!searchQuery.trim()) return; + const name = window.prompt('Name for this Saved Search:', searchQuery.trim()); + if (!name?.trim()) return; + await fetch('/api/saved-searches', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: name.trim(), + query: searchQuery.trim(), + isShortcut: true, + }), + }); + await fetchSidebar(); + }; + + const handleMoveNoteToNotebook = async (noteId: string, notebookId: string) => { + if (!noteId || !notebookId) return; + const res = await fetch(`/api/notes/${noteId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ notebookId }), + }); + if (res.ok) { + const updated: NoteDetail = await res.json(); + if (activeNote?.id === noteId) { + setActiveNote(updated); + } + await fetchSidebar(); + await fetchNotesList(selectedNoteId); + } + }; + + const handleSwitchUser = async (userId: string) => { + setUserMenuOpen(false); + const res = await fetch('/api/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'switch', userId }), + }); + if (res.ok) { + setSelectedNoteId(''); + setActiveNote(null); + setActiveView('all'); + clearAllFilters(); + await fetchSidebar(); + await fetchNotesList(); + } + }; + + const handleCreateUser = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newUserName.trim()) return; + const res = await fetch('/api/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: newUserName.trim(), + email: newUserEmail.trim() || undefined, + switchImmediately: true, + }), + }); + if (res.ok) { + setNewUserName(''); + setNewUserEmail(''); + setCreatingUser(false); + setUserMenuOpen(false); + setSelectedNoteId(''); + setActiveNote(null); + setActiveView('all'); + clearAllFilters(); + await fetchSidebar(); + await fetchNotesList(); + } + }; + + // Virtualizer for middle note list pane + const rowVirtualizer = useVirtualizer({ + count: notes.length, + getScrollElement: () => listParentRef.current, + estimateSize: () => + listMode === 'compact' ? 52 : listMode === 'cards' ? 148 : 112, + overscan: 10, + }); + + // Compute current view title + const currentViewTitle = (() => { + if (activeView === 'trash') return 'Trash'; + if (activeView === 'shortcuts') return 'Shortcuts'; + if (activeView === 'notebook') { + return ( + sidebar.notebooks.find((n) => n.id === selectedNotebookId)?.name || 'Notebook' + ); + } + if (activeView === 'stack') { + return ( + sidebar.stacks.find((s) => s.id === selectedStackId)?.name || 'Notebook Stack' + ); + } + if (activeView === 'tag') { + return `#${sidebar.tags.find((t) => t.id === selectedTagId)?.name || 'Tag'}`; + } + return 'All Notes'; + })(); + + const notebooksByStack = sidebar.stacks.map((st) => ({ + stack: st, + notebooks: sidebar.notebooks.filter((nb) => nb.stackId === st.id), + })); + const unstackedNotebooks = sidebar.notebooks.filter((nb) => !nb.stackId); + + const wordCount = activeNote?.contentText + ? activeNote.contentText.trim().split(/\s+/).filter(Boolean).length + : 0; + + return ( +
+ {/* Mobile Sidebar Backdrop */} + {mobileMenuOpen && ( +
setMobileMenuOpen(false)} + className="fixed inset-0 bg-black/55 z-40 md:hidden backdrop-blur-xs" + /> + )} + + {/* 1. LEFT SIDEBAR (Classic Evernote Dark Theme, Slide-Over Drawer on Mobile) */} + + + {activeView === 'settings' ? ( + setActiveView('all')} + onDataChanged={async (firstNoteId) => { + await fetchSidebar(); + await fetchNotesList(firstNoteId); + }} + /> + ) : ( + <> + {/* 2. MIDDLE NOTE LIST PANE */} +
+ {/* List Header */} +
+
+
+ +

+ {currentViewTitle} +

+
+
+ {activeView !== 'all' && ( + + )} + + {activeView === 'notebook' && selectedNotebookId && ( + + )} + {activeView === 'trash' && notes.length > 0 && ( + + )} +
+
+ +
+ + {totalNotes} {totalNotes === 1 ? 'note' : 'notes'} + {debouncedSearch ? ` Β· "${debouncedSearch}"` : ''} + + +
+ {/* Filter Drawer Toggle Button */} + + + {/* Sort selector */} + + + {/* View mode switcher */} +
+ + + +
+
+
+ + {/* Collapsible Filter Panel */} + {showFilterPanel && ( +
+ {/* Quick Keyword Filter Input */} +
+ + setSearchQuery(e.target.value)} + placeholder="Filter by keyword, intitle:, tag:, source:…" + className="w-full pl-8 pr-7 py-1.5 bg-gray-50 border border-gray-300 rounded-lg text-xs text-gray-800 placeholder-gray-400 focus:outline-none focus:border-evernote-green" + /> + {searchQuery && ( + + )} +
+ + {/* Content Attribute Chips */} +
+
+ Contains / Attributes +
+
+ + + + + + + + + + + +
+
+ + {/* Notebook & Updated Date Dropdowns */} +
+ {activeView !== 'notebook' && ( +
+ + +
+ )} + +
+ + +
+
+ + {/* Collapsible Dropdown Multi-Tag Selector */} + {sidebar.tags.length > 0 && ( +
+ + + {tagFilterDropdownOpen && ( +
+ setTagFilterSearch(e.target.value)} + placeholder="Search tags…" + className="w-full px-2.5 py-1 bg-white border border-gray-300 rounded text-xs text-gray-800 placeholder-gray-400 focus:outline-none focus:border-evernote-green" + /> +
+ {sidebar.tags + .filter( + (t) => + !tagFilterSearch.trim() || + t.name + .toLowerCase() + .includes(tagFilterSearch.trim().toLowerCase()) + ) + .map((t) => { + const active = filterTagIds.includes(t.id); + return ( + + ); + })} +
+
+ )} +
+ )} + + {(activeFilterCount > 0 || searchQuery) && ( +
+ + +
+ )} +
+ )} + + {/* Active Filter Pills Strip (shown even when drawer is closed) */} + {!showFilterPanel && (activeFilterCount > 0 || debouncedSearch) && ( +
+ {debouncedSearch && ( + + “{debouncedSearch}” + + + )} + {filterNotebookId && ( + + + πŸ“– {sidebar.notebooks.find((n) => n.id === filterNotebookId)?.name} + + + + )} + {filterHasImages && ( + + Has Images + + + )} + {filterHasAttachments && ( + + Has Files + + + )} + {filterHasTodo && ( + + Checklists + + + )} + {filterHasWebClip && ( + + Web Clips + + + )} + {filterOnlyPinned && ( + + Pinned + + + )} + {filterUntagged && ( + + Untagged + + + )} + {filterUpdatedDays > 0 && ( + + Last {filterUpdatedDays}d + + + )} + {filterTagIds.map((tid) => { + const t = sidebar.tags.find((x) => x.id === tid); + if (!t) return null; + return ( + + #{t.name} + + + ); + })} + +
+ )} +
+ + {/* Note List Body: Card Grid vs Virtualized Snippet / Compact List */} +
+ {loadingList && notes.length === 0 ? ( +
Loading notes…
+ ) : notes.length === 0 ? ( +
+ +
No notes found
+

+ {activeFilterCount > 0 || debouncedSearch + ? 'No notes match your current filters.' + : 'Create a new note or import your Evernote .enex / Markdown files.'} +

+ {activeFilterCount > 0 || debouncedSearch ? ( + + ) : ( + activeView !== 'trash' && ( +
+ + +
+ ) + )} +
+ ) : listMode === 'cards' ? ( + /* 2-COLUMN VISUAL CARD GRID VIEW */ +
+ {notes.map((n) => { + const isSelected = n.id === selectedNoteId; + const firstImg = n.attachments.find((a) => + a.mimeType.startsWith('image/') + ); + const firstPdf = n.attachments.find( + (a) => + a.mimeType === 'application/pdf' || + a.filename.toLowerCase().endsWith('.pdf') + ); + const effectiveThumbUrl = + n.thumbnailUrl === 'none' + ? null + : n.thumbnailUrl || (firstImg ? `/api/attachments/${firstImg.id}` : null); + + return ( +
{ + e.dataTransfer.setData('text/everclone-note-id', n.id); + }} + onClick={() => openNoteInEditor(n.id)} + className={`group/card cursor-pointer rounded-xl border transition flex flex-col overflow-hidden shadow-xs ${ + isSelected + ? 'bg-emerald-50/60 border-evernote-green ring-2 ring-evernote-green/30' + : 'bg-white border-gray-200/90 hover:border-gray-300 hover:shadow-md' + }`} + > + {/* Top Hero Cover Image Banner (if present) */} + {effectiveThumbUrl ? ( +
+ + {firstPdf && ( + + PDF + + )} + +
+ ) : ( + n.thumbnailUrl !== 'none' && + firstPdf && ( + { + setSelectedNoteId(n.id); + setThumbnailInitialUrl(dataUrl); + setThumbnailEditorOpen(true); + }} + onCachedThumbnail={(nid, dataUrl) => { + setNotes((prev) => + prev.map((item) => + item.id === nid ? { ...item, thumbnailUrl: dataUrl } : item + ) + ); + }} + /> + ) + )} + + {/* Card Content */} +
+
+ {/* Notebook Mover & Pin/Star Row */} +
+
e.stopPropagation()} + className="inline-flex items-center gap-1 min-w-0 max-w-[140px] px-1.5 py-0.5 rounded bg-gray-100 hover:bg-emerald-50 text-gray-600 hover:text-evernote-green transition" + title="Move note to another notebook" + > + + +
+
+ {n.isPinned && ( + + )} + {n.isShortcut && ( + + )} +
+
+ +

+ {n.title || 'Untitled Note'} +

+ +

+ {n.excerpt || 'No additional text'} +

+
+ + {/* Card Footer */} +
+ + {format(new Date(n.updatedAt), 'MMM d')} + + +
+ {n.attachments.length > 0 && ( + + + {n.attachments.length} + + )} + {n.tags.slice(0, 1).map((t) => ( + + #{t.name} + + ))} +
+
+
+
+ ); + })} +
+ ) : ( + /* VIRTUALIZED SNIPPET & COMPACT LIST VIEWS */ +
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const n = notes[virtualRow.index]; + const isSelected = n.id === selectedNoteId; + const firstImg = n.attachments.find((a) => + a.mimeType.startsWith('image/') + ); + const firstPdf = n.attachments.find( + (a) => + a.mimeType === 'application/pdf' || + a.filename.toLowerCase().endsWith('.pdf') + ); + const effectiveThumbUrl = + n.thumbnailUrl === 'none' + ? null + : n.thumbnailUrl || (firstImg ? `/api/attachments/${firstImg.id}` : null); + + return ( +
{ + e.dataTransfer.setData('text/everclone-note-id', n.id); + }} + onClick={() => openNoteInEditor(n.id)} + style={{ + position: 'absolute', + top: 0, + left: 0, + width: '100%', + transform: `translateY(${virtualRow.start}px)`, + }} + className={`cursor-pointer border-b border-gray-200/80 transition px-4 ${ + listMode === 'compact' ? 'py-2' : 'py-3.5' + } ${ + isSelected + ? 'bg-emerald-50/75 border-l-4 border-l-evernote-green' + : 'bg-white hover:bg-gray-50' + }`} + > + {listMode === 'compact' ? ( + /* COMPACT SINGLE-LINE VIEW */ +
+
+ {n.isPinned && ( + + )} + {n.isShortcut && ( + + )} + + {n.title || 'Untitled Note'} + +
+
+ {n.tags[0] && ( + + #{n.tags[0].name} + + )} + {format(new Date(n.updatedAt), 'MMM d')} +
+
+ ) : ( + /* SNIPPET VIEW */ +
+
+
+ {n.isPinned && ( + + )} + {n.isShortcut && ( + + )} +

+ {n.title || 'Untitled Note'} +

+
+ +

+ {n.excerpt || 'No additional text'} +

+ +
+
e.stopPropagation()} + className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-gray-100 hover:bg-emerald-50 text-[10px] text-gray-600 hover:text-evernote-green font-medium transition max-w-[130px]" + title="Move note to another notebook" + > + + +
+ + + {formatDistanceToNow(new Date(n.updatedAt), { + addSuffix: true, + })} + + + {n.attachments.length > 0 && ( + + + {n.attachments.length} + + )} + + {n.tags.slice(0, 3).map((t) => ( + + {t.name} + + ))} +
+
+ + {effectiveThumbUrl ? ( +
+ + {firstPdf && ( + + PDF + + )} + +
+ ) : ( + n.thumbnailUrl !== 'none' && + firstPdf && ( + { + setSelectedNoteId(n.id); + setThumbnailInitialUrl(dataUrl); + setThumbnailEditorOpen(true); + }} + onCachedThumbnail={(nid, dataUrl) => { + setNotes((prev) => + prev.map((item) => + item.id === nid ? { ...item, thumbnailUrl: dataUrl } : item + ) + ); + }} + /> + ) + )} +
+ )} +
+ ); + })} +
+ )} +
+
+ + {/* 3. RIGHT NOTE EDITOR PANE */} +
+ {!activeNote ? ( +
+ +

+ Select a note or create a new one +

+
+ ) : ( + <> + {/* Top Note Action Bar */} +
+
+ {/* Mobile Back to Note List Button */} + + + {/* Notebook Mover Dropdown */} +
+ + +
+ + {activeNote.sourceUrl && ( + + + Source URL + + )} + + + + + {saveState === 'saving' + ? 'Saving…' + : saveState === 'dirty' + ? 'Unsaved edits' + : 'All changes saved'} + + +
+ + {/* Right Note Actions */} +
+ {activeNote.isTrashed ? ( + <> + + + + ) : ( + <> + + + + + + + + + {/* Export Menu */} +
+ + {showExportMenu && ( + + )} +
+ + + + )} +
+
+ + {/* Collapsible Note Info & Metadata Drawer */} + {showNoteInfo && ( +
+
+ + { + const val = e.target.value; + setActiveNote({ ...activeNote, sourceUrl: val }); + scheduleNoteSave(activeNote.id, { sourceUrl: val }); + }} + placeholder="https://…" + className="w-full px-2.5 py-1 bg-white border border-gray-300 rounded text-xs" + /> +
+
+ + { + const d = new Date(e.target.value); + if (!isNaN(d.getTime())) { + handleImmediatePatch({ createdAt: d.toISOString() }); + } + }} + className="w-full px-2.5 py-1 bg-white border border-gray-300 rounded text-xs" + /> +
+
+ +
+ {wordCount} words Β· {activeNote.attachments.length} attachment + {activeNote.attachments.length === 1 ? '' : 's'} Β· Updated{' '} + {format(new Date(activeNote.updatedAt), 'MMM d, yyyy HH:mm')} +
+
+
+ )} + + {/* Note Title Input */} +
+ { + const newTitle = e.target.value; + setActiveNote({ ...activeNote, title: newTitle }); + scheduleNoteSave(activeNote.id, { title: newTitle }); + }} + placeholder="Note Title" + className="w-full text-2xl font-bold text-gray-900 placeholder-gray-300 focus:outline-none" + /> +
+ + {/* Rich Text Editor + Inline PDF Viewers */} + { + setActiveNote((prev) => + prev ? { ...prev, contentHtml: newHtml } : prev + ); + scheduleNoteSave(activeNote.id, { contentHtml: newHtml }); + }} + onUploadFiles={handleUploadAttachments} + onSetThumbnail={(src) => handleImmediatePatch({ thumbnailUrl: src })} + onOpenThumbnailEditor={(src) => { + setThumbnailInitialUrl(src); + setThumbnailEditorOpen(true); + }} + > + {activeNote.attachments + .filter( + (att) => + att.mimeType === 'application/pdf' || + att.filename.toLowerCase().endsWith('.pdf') + ) + .map((pdfAtt) => ( + + handleImmediatePatch({ thumbnailUrl: dataUrl }) + } + onOpenThumbnailStudio={(dataUrl) => { + setThumbnailInitialUrl(dataUrl); + setThumbnailEditorOpen(true); + }} + /> + ))} + + + {/* Attachments Strip (Collapsed by Default) */} + {activeNote.attachments.length > 0 && ( +
+
+ + {!activeNote.isTrashed && ( + + )} +
+ + {attachmentsExpanded && ( +
+ {activeNote.attachments.map((att) => { + const attUrl = `/api/attachments/${att.id}`; + const isImg = att.mimeType.startsWith('image/'); + const isCurrentThumb = activeNote.thumbnailUrl === attUrl; + return ( +
+ + {att.filename} + + + {(att.sizeBytes / 1024).toFixed(1)} KB + + {isImg && !activeNote.isTrashed && ( + + )} + + + + {!activeNote.isTrashed && ( + + )} +
+ ); + })} +
+ )} +
+ )} + + {/* Bottom Tag Bar */} +
+ + {activeNote.tags.map((t) => ( + + {t.name} + {!activeNote.isTrashed && ( + + )} + + ))} + + {!activeNote.isTrashed && ( +
{ + e.preventDefault(); + handleAddTagToNote(tagInput); + }} + className="inline-flex items-center" + > + setTagInput(e.target.value)} + onBlur={() => { + if (tagInput.trim()) handleAddTagToNote(tagInput); + }} + placeholder="+ Add tag…" + className="px-2 py-0.5 text-xs text-gray-700 placeholder-gray-400 focus:outline-none focus:bg-gray-100 rounded" + /> + + {sidebar.tags.map((t) => ( + +
+ )} +
+ + )} +
+ + { + setThumbnailEditorOpen(false); + setThumbnailInitialUrl(null); + }} + onSaveThumbnail={async (thumbnailUrl) => { + await handleImmediatePatch({ thumbnailUrl }); + }} + /> + + )} +
+ ); +} + +function NotebookRow({ + nb, + active, + onSelect, + onDropNote, + onRefresh, +}: { + nb: NotebookItem; + active: boolean; + onSelect: () => void; + onDropNote?: (noteId: string) => void; + onRefresh: () => void; +}) { + const [menuOpen, setMenuOpen] = useState(false); + const [dragOver, setDragOver] = useState(false); + + return ( +
{ + if (onDropNote) { + e.preventDefault(); + setDragOver(true); + } + }} + onDragLeave={() => setDragOver(false)} + onDrop={(e) => { + setDragOver(false); + const droppedNoteId = e.dataTransfer.getData('text/everclone-note-id'); + if (droppedNoteId && onDropNote) { + e.preventDefault(); + onDropNote(droppedNoteId); + } + }} + className={`group relative flex items-center justify-between px-2 py-1.5 rounded cursor-pointer transition ${ + dragOver + ? 'bg-emerald-900/70 ring-1 ring-evernote-green text-white' + : active + ? 'bg-evernote-sidebarActive text-white font-medium' + : 'hover:bg-evernote-sidebarHover text-gray-300' + }`} + > + + +
+ {nb.noteCount} + +
+ + {menuOpen && ( +
setMenuOpen(false)} + className="absolute right-2 top-7 w-44 bg-neutral-900 border border-neutral-700 rounded-lg shadow-xl py-1 z-30 text-xs text-gray-200" + > + + + + + {!nb.isDefault && ( + + )} +
+ )} +
+ ); +} diff --git a/src/components/HooksModal.tsx b/src/components/HooksModal.tsx new file mode 100644 index 0000000..87ccf79 --- /dev/null +++ b/src/components/HooksModal.tsx @@ -0,0 +1,379 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { + Key, + Smartphone, + Bookmark, + Terminal, + Plus, + Trash2, + Copy, + Check, + Download, + X, +} from 'lucide-react'; + +interface ApiKeyRecord { + id: string; + name: string; + keyPrefix: string; + lastUsedAt: string | null; + createdAt: string; +} + +interface HooksModalProps { + open: boolean; + onClose: () => void; +} + +export default function HooksModal({ open, onClose }: HooksModalProps) { + const [keys, setKeys] = useState([]); + const [newKeyName, setNewKeyName] = useState('Android & Web Clipper'); + const [generatedKey, setGeneratedKey] = useState(''); + const [copied, setCopied] = useState(''); + + const origin = + typeof window !== 'undefined' ? window.location.origin : 'https://everclone.vloser.us'; + + const loadKeys = async () => { + try { + const res = await fetch('/api/keys'); + const data = await res.json(); + if (data.keys) setKeys(data.keys); + } catch { + // ignore + } + }; + + useEffect(() => { + if (open) { + loadKeys(); + } + }, [open]); + + if (!open) return null; + + const handleCreateKey = async (e: React.FormEvent) => { + e.preventDefault(); + const res = await fetch('/api/keys', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: newKeyName }), + }); + const data = await res.json(); + if (data.rawKey) { + setGeneratedKey(data.rawKey); + loadKeys(); + } + }; + + const handleDeleteKey = async (id: string) => { + await fetch(`/api/keys?id=${id}`, { method: 'DELETE' }); + loadKeys(); + }; + + const copyText = (text: string, label: string) => { + navigator.clipboard.writeText(text); + setCopied(label); + setTimeout(() => setCopied(''), 2000); + }; + + const activeToken = generatedKey || ''; + + const downloadAndroidHttpShortcuts = () => { + const shortcutsJson = { + version: 60, + categories: [ + { + id: 'everclone-cat-1', + name: 'Everclone', + layoutType: 'linear_list', + shortcuts: [ + { + id: 'everclone-share-url', + name: 'Clip Link / Text to Everclone', + description: 'Shares any URL or text selection from Android Share sheet to Everclone', + method: 'POST', + url: `${origin}/api/hooks/note`, + headers: [ + { key: 'Authorization', value: `Bearer ${activeToken}` }, + { key: 'Content-Type', value: 'application/json' }, + ], + bodyContent: JSON.stringify({ + title: '{share_title}', + content: '{share_text}', + tags: ['android'], + }), + requestBodyType: 'custom_text', + launcherShortcut: true, + quickSettingsTileShortcut: true, + }, + { + id: 'everclone-quick-note', + name: 'Quick Note to Everclone', + description: 'Prompts for a quick note and saves it to Everclone Inbox', + method: 'POST', + url: `${origin}/api/hooks/note`, + headers: [ + { key: 'Authorization', value: `Bearer ${activeToken}` }, + { key: 'Content-Type', value: 'application/json' }, + ], + bodyContent: JSON.stringify({ + title: '{note_title}', + markdown: '{note_body}', + tags: ['quick-note', 'android'], + }), + requestBodyType: 'custom_text', + launcherShortcut: true, + }, + ], + }, + ], + variables: [ + { + id: 'var-share-title', + key: 'share_title', + type: 'constant', + value: '', + isShareTitle: true, + }, + { + id: 'var-share-text', + key: 'share_text', + type: 'constant', + value: '', + isShareText: true, + }, + { + id: 'var-note-title', + key: 'note_title', + type: 'text', + title: 'Note Title', + Flags: 0, + }, + { + id: 'var-note-body', + key: 'note_body', + type: 'text', + title: 'Note Content (Markdown)', + isMultiline: true, + }, + ], + }; + + const blob = new Blob([JSON.stringify(shortcutsJson, null, 2)], { + type: 'application/json', + }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'everclone-android-http-shortcuts.json'; + a.click(); + URL.revokeObjectURL(url); + }; + + const bookmarkletHref = `javascript:(function(){var s=window.getSelection?window.getSelection().toString():'';var u=encodeURIComponent(location.href);var t=encodeURIComponent(document.title);var x=encodeURIComponent(s);window.open('${origin}/api/hooks/note?redirect=1&apiKey=${encodeURIComponent( + activeToken + )}&url='+u+'&title='+t+'&text='+x,'_blank');})();`; + + const curlSnippet = `curl -X POST "${origin}/api/hooks/note" \\ + -H "Authorization: Bearer ${activeToken}" \\ + -H "Content-Type: application/json" \\ + -d '{"title":"Idea from Android","markdown":"- [ ] Check this out","notebook":"Inbox","tags":["mobile","idea"]}'`; + + return ( +
+
+ {/* Header */} +
+
+ +

+ Web & Android Note Ingestion Hooks +

+
+ +
+ +
+ {/* 1. API Keys Section */} +
+
+ + Step 1: Generate an API Key for Hooks +
+ +
+ setNewKeyName(e.target.value)} + placeholder="Key description (e.g. Pixel 9 Pro / Bookmarklet)" + className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-evernote-green" + /> + +
+ + {generatedKey && ( +
+
+
+ New API Key Created (automatically embedded in snippets below): +
+ + {generatedKey} + +
+ +
+ )} + + {keys.length > 0 && ( +
+ {keys.map((k) => ( +
+
+ {k.name} + {k.keyPrefix} +
+
+ + {k.lastUsedAt + ? `Last used ${new Date(k.lastUsedAt).toLocaleDateString()}` + : 'Never used'} + + +
+
+ ))} +
+ )} +
+ + {/* 2. Android Integration */} +
+
+ + Step 2: Add Notes from Android (Share Sheet, HTTP Shortcuts & PWA) +
+

+ You can send links, articles, text, voice notes, or photos from Android in two ways: +

+
+
+
+ Option A: Android HTTP Shortcuts / Tasker +
+

+ Import this file into the free HTTP Shortcuts Android app to add{' '} + “Clip to Everclone” to your Android Share menu & home screen widgets. +

+ +
+ +
+
+ Option B: Install PWA Share Target +
+

+ Open {origin} in Chrome on Android and tap{' '} + “Add to Home screen / Install app”. Everclone will appear directly inside Android's native Share sheet! +

+
+
+
+ + {/* 3. Browser Web Clipper Bookmarklet */} +
+
+ + Step 3: Web Clipper Bookmarklet +
+

+ Drag the green button below to your browser's Bookmarks Bar. Click it on any web page to clip the full readable article (or highlighted text) directly into Everclone: +

+
+ e.preventDefault()} + draggable + className="inline-flex items-center gap-1.5 px-4 py-2 bg-evernote-green text-white text-xs font-semibold rounded-lg shadow hover:bg-evernote-greenHover cursor-grab" + > + πŸ“Ž Clip to Everclone + + +
+
+ + {/* 4. REST API / cURL Reference */} +
+
+
+ + REST API Webhook Reference (POST /api/hooks/note) +
+ +
+
+              {curlSnippet}
+            
+
+
+
+
+ ); +} diff --git a/src/components/ImportModal.tsx b/src/components/ImportModal.tsx new file mode 100644 index 0000000..3ce0006 --- /dev/null +++ b/src/components/ImportModal.tsx @@ -0,0 +1,625 @@ +'use client'; + +import React, { useState, useRef } from 'react'; +import { + Upload, + Globe, + X, + FileArchive, + CheckCircle2, + AlertCircle, + Loader2, +} from 'lucide-react'; +import { NotebookItem } from '@/types/everclone'; + +interface ImportModalProps { + open: boolean; + onClose: () => void; + notebooks: NotebookItem[]; + onImported: (firstNoteId?: string) => void; +} + +export default function ImportModal({ + open, + onClose, + notebooks, + onImported, +}: ImportModalProps) { + const [tab, setTab] = useState<'files' | 'url'>('files'); + const [selectedFiles, setSelectedFiles] = useState([]); + const [notebookId, setNotebookId] = useState(''); + const [newNotebookName, setNewNotebookName] = useState(''); + const [useFilenameAsNotebook, setUseFilenameAsNotebook] = useState(true); + + const [clipUrl, setClipUrl] = useState(''); + const [clipTitle, setClipTitle] = useState(''); + const [clipTags, setClipTags] = useState('web-clip'); + + const [loading, setLoading] = useState(false); + const [progressInfo, setProgressInfo] = useState<{ + phase: 'uploading' | 'processing'; + fileName: string; + bytesUploaded: number; + totalBytes: number; + processedNotes: number; + totalNotes: number; + } | null>(null); + + const [result, setResult] = useState<{ + importedCount: number; + errors: string[]; + } | null>(null); + + const abortRef = useRef(false); + const fileInputRef = useRef(null); + + if (!open) return null; + + /** + * Uploads any file in strict 1.5 MB raw binary chunks to /api/import?action=appendChunk, + * then triggers server-side background streaming import and polls job progress every 1s. + */ + const uploadAndProcessFileInChunks = async ( + file: File + ): Promise<{ importedCount: number; errors: string[] }> => { + const CHUNK_SIZE = 1536 * 1024; // 1.5 MB binary chunks (never trips proxy or OAuth limits) + + // 1. Initialize chunk upload job + const initRes = await fetch('/api/import?action=initChunkUpload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filename: file.name }), + }); + if (!initRes.ok) { + throw new Error(`Failed to initialize upload (${initRes.status})`); + } + const { jobId } = await initRes.json(); + + // 2. Upload binary slices sequentially with live progress bar + let offset = 0; + while (offset < file.size) { + if (abortRef.current) { + throw new Error('Upload stopped by user.'); + } + const end = Math.min(offset + CHUNK_SIZE, file.size); + const chunkBlob = file.slice(offset, end); + + const appendRes = await fetch( + `/api/import?action=appendChunk&jobId=${encodeURIComponent(jobId)}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/octet-stream' }, + body: chunkBlob, + } + ); + + if (!appendRes.ok) { + throw new Error(`Chunk upload failed at ${(offset / 1048576).toFixed(1)} MB (HTTP ${appendRes.status})`); + } + + offset = end; + setProgressInfo({ + phase: 'uploading', + fileName: file.name, + bytesUploaded: offset, + totalBytes: file.size, + processedNotes: 0, + totalNotes: 0, + }); + } + + // 3. Finalize upload and kick off server-side processing + setProgressInfo({ + phase: 'processing', + fileName: file.name, + bytesUploaded: file.size, + totalBytes: file.size, + processedNotes: 0, + totalNotes: 0, + }); + + const finishRes = await fetch('/api/import?action=finishChunkUpload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jobId, + notebookId: notebookId || undefined, + notebookName: newNotebookName.trim() || undefined, + useFilenameAsNotebook: + useFilenameAsNotebook && !notebookId && !newNotebookName.trim(), + }), + }); + + if (!finishRes.ok) { + throw new Error(`Failed to start server import (HTTP ${finishRes.status})`); + } + + const finishData = await finishRes.json(); + if (!finishData.background) { + return { + importedCount: finishData.importedCount || 0, + errors: finishData.errors || [], + }; + } + + // 4. Poll background job status every 900ms until completed or failed + let lastCount = 0; + while (!abortRef.current) { + await new Promise((r) => setTimeout(r, 900)); + const statusRes = await fetch( + `/api/import?jobId=${encodeURIComponent(jobId)}` + ); + if (!statusRes.ok) continue; + const job = await statusRes.json(); + + setProgressInfo({ + phase: 'processing', + fileName: file.name, + bytesUploaded: file.size, + totalBytes: file.size, + processedNotes: job.processedNotes || 0, + totalNotes: job.totalNotes || 0, + }); + + if ((job.processedNotes || 0) > lastCount) { + lastCount = job.processedNotes; + onImported(); + } + + if (job.status === 'completed' || job.status === 'failed') { + const errs = job.errorLog ? String(job.errorLog).split('\n').filter(Boolean) : []; + return { + importedCount: job.processedNotes || 0, + errors: errs, + }; + } + } + + return { importedCount: lastCount, errors: ['Stopped polling in UI (server job continues in background).'] }; + }; + + const handleFileImport = async (e: React.FormEvent) => { + e.preventDefault(); + if (selectedFiles.length === 0) return; + + setLoading(true); + setResult(null); + abortRef.current = false; + + let grandTotalImported = 0; + const allErrors: string[] = []; + + try { + for (const file of selectedFiles) { + if (abortRef.current) break; + setProgressInfo({ + phase: 'uploading', + fileName: file.name, + bytesUploaded: 0, + totalBytes: file.size, + processedNotes: 0, + totalNotes: 0, + }); + + const res = await uploadAndProcessFileInChunks(file); + grandTotalImported += res.importedCount; + allErrors.push(...res.errors); + onImported(); + } + + setResult({ + importedCount: grandTotalImported, + errors: allErrors, + }); + setSelectedFiles([]); + } catch (err: unknown) { + setResult({ + importedCount: grandTotalImported, + errors: [ + ...allErrors, + err instanceof Error ? err.message : 'Import failed', + ], + }); + } finally { + setLoading(false); + setProgressInfo(null); + } + }; + + const handleUrlClip = async (e: React.FormEvent) => { + e.preventDefault(); + if (!clipUrl.trim()) return; + + setLoading(true); + setResult(null); + + try { + const res = await fetch('/api/import', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + url: clipUrl.trim(), + title: clipTitle.trim() || undefined, + notebookId: notebookId || undefined, + notebookName: newNotebookName.trim() || undefined, + tags: clipTags + .split(',') + .map((t) => t.trim()) + .filter(Boolean), + }), + }); + const data = await res.json(); + if (!res.ok) { + throw new Error(data.error || 'URL clip failed'); + } + setResult({ + importedCount: data.importedCount || 1, + errors: data.errors || [], + }); + setClipUrl(''); + setClipTitle(''); + onImported(data.noteIds?.[0]); + } catch (err: unknown) { + setResult({ + importedCount: 0, + errors: [err instanceof Error ? err.message : 'URL clip failed'], + }); + } finally { + setLoading(false); + } + }; + + const pct = (() => { + if (!progressInfo) return 0; + if (progressInfo.phase === 'uploading') { + return progressInfo.totalBytes > 0 + ? Math.min( + 100, + Math.round( + (progressInfo.bytesUploaded / progressInfo.totalBytes) * 100 + ) + ) + : 0; + } + return progressInfo.totalNotes > 0 + ? Math.min( + 100, + Math.round( + (progressInfo.processedNotes / progressInfo.totalNotes) * 100 + ) + ) + : 50; + })(); + + return ( +
+
+ {/* Header */} +
+
+ +

+ Import Notes & Web Clips +

+
+ +
+ + {/* Mode Tabs */} +
+ + +
+ + {/* Content */} +
+ {tab === 'files' ? ( +
+
!loading && fileInputRef.current?.click()} + onDragOver={(e) => e.preventDefault()} + onDrop={(e) => { + e.preventDefault(); + if (!loading && e.dataTransfer.files?.length) { + setSelectedFiles(Array.from(e.dataTransfer.files)); + } + }} + className="border-2 border-dashed border-gray-300 hover:border-evernote-green rounded-xl p-6 text-center cursor-pointer transition bg-gray-50/50 hover:bg-emerald-50/20" + > + +

+ Click to choose files or drag & drop here +

+

+ Supports multi-GB .enex files (1.5 MB chunked upload + background server import),{' '} + .md, .html, or .zip +

+ { + if (e.target.files?.length) { + setSelectedFiles(Array.from(e.target.files)); + } + }} + /> +
+ + {selectedFiles.length > 0 && ( +
+
+ Selected ({selectedFiles.length} file + {selectedFiles.length > 1 ? 's' : ''}): +
+ {selectedFiles.map((f, idx) => ( +
+ {f.name} + + {f.size > 1024 * 1024 + ? `${(f.size / (1024 * 1024)).toFixed(1)} MB` + : `${(f.size / 1024).toFixed(1)} KB`} + +
+ ))} +
+ )} + + {progressInfo && ( +
+
+ + {progressInfo.phase === 'uploading' + ? `Step 1/2 β€” Uploading: ${progressInfo.fileName}` + : `Step 2/2 β€” Importing Notes: ${progressInfo.fileName}`} + + {pct}% +
+
+
+
+
+ + {progressInfo.phase === 'uploading' + ? `${(progressInfo.bytesUploaded / (1024 * 1024)).toFixed(1)} MB / ${( + progressInfo.totalBytes / + (1024 * 1024) + ).toFixed(1)} MB transferred` + : progressInfo.totalNotes > 0 + ? `${progressInfo.processedNotes} of ${progressInfo.totalNotes} notes imported into database` + : `Scanning notes in ${progressInfo.fileName}…`} + + +
+
+ )} + +
+
+ + +
+
+ + setNewNotebookName(e.target.value)} + placeholder="e.g. Imported Archive" + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-evernote-green" + /> +
+
+ + + +
+ + +
+ + ) : ( +
+
+ + setClipUrl(e.target.value)} + placeholder="https://example.com/article" + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-evernote-green" + /> +
+ +
+
+ + setClipTitle(e.target.value)} + placeholder="Auto-extracted from page" + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-evernote-green" + /> +
+
+ + setClipTags(e.target.value)} + placeholder="web-clip, reading" + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-evernote-green" + /> +
+
+ +
+ + +
+ +
+ + +
+
+ )} + + {result && ( +
0 && result.importedCount === 0 + ? 'bg-red-50 border-red-200 text-red-800' + : 'bg-emerald-50 border-emerald-200 text-emerald-900' + }`} + > +
+ {result.importedCount > 0 ? ( + + ) : ( + + )} + + Imported {result.importedCount} note + {result.importedCount === 1 ? '' : 's'}! + +
+ {result.errors.length > 0 && ( +
    + {result.errors.slice(0, 20).map((err, i) => ( +
  • {err}
  • + ))} +
+ )} +
+ )} +
+
+
+ ); +} diff --git a/src/components/PdfNoteComponents.tsx b/src/components/PdfNoteComponents.tsx new file mode 100644 index 0000000..85e2e7e --- /dev/null +++ b/src/components/PdfNoteComponents.tsx @@ -0,0 +1,250 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { + FileText, + ChevronDown, + ChevronRight, + ExternalLink, + Download, + Image as ImageIcon, + Crop, +} from 'lucide-react'; +import { renderPdfPageToDataUrl } from '@/lib/pdfClient'; +import { AttachmentItem } from '@/types/everclone'; + +/** + * Automatically extracts Page 1 of a PDF attachment as a crisp thumbnail, + * displays it in Snippet / Card View, and caches it to the note's thumbnailUrl in DB. + */ +export function PdfAutoThumbnail({ + noteId, + pdfAttachmentId, + mode, + onEditThumbnail, + onCachedThumbnail, +}: { + noteId: string; + pdfAttachmentId: string; + mode: 'snippets' | 'cards'; + onEditThumbnail: (dataUrl: string) => void; + onCachedThumbnail?: (noteId: string, dataUrl: string) => void; +}) { + const [dataUrl, setDataUrl] = useState(null); + const pdfUrl = `/api/attachments/${pdfAttachmentId}`; + + useEffect(() => { + let cancelled = false; + (async () => { + const rendered = await renderPdfPageToDataUrl(pdfUrl, 1, 360); + if (!cancelled && rendered) { + setDataUrl(rendered); + if (onCachedThumbnail) { + onCachedThumbnail(noteId, rendered); + } + // Persist the generated Page-1 thumbnail on the note so future list loads are instant + try { + await fetch(`/api/notes/${noteId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ thumbnailUrl: rendered }), + }); + } catch { + // ignore background cache error + } + } + })(); + return () => { + cancelled = true; + }; + }, [noteId, pdfUrl, onCachedThumbnail]); + + if (mode === 'cards') { + return ( +
+ {dataUrl ? ( + <> + PDF Page 1 + + PDF + + + + ) : ( +
+ + Rendering PDF… +
+ )} +
+ ); + } + + return ( +
+ {dataUrl ? ( + <> + PDF Page 1 + + PDF + + + + ) : ( + + PDF + + )} +
+ ); +} + +/** + * Evernote-style inline PDF viewer rendered directly inside the Note Editor pane. + */ +export function InlinePdfViewerCard({ + attachment, + readOnly, + onSetThumbnail, + onOpenThumbnailStudio, +}: { + attachment: AttachmentItem; + readOnly?: boolean; + onSetThumbnail: (dataUrl: string) => void; + onOpenThumbnailStudio: (dataUrl: string) => void; +}) { + const [expanded, setExpanded] = useState(true); + const [extractingThumb, setExtractingThumb] = useState(false); + const pdfUrl = `/api/attachments/${attachment.id}`; + + const handleUsePage1AsThumb = async (openStudio = false) => { + setExtractingThumb(true); + try { + const dataUrl = await renderPdfPageToDataUrl(pdfUrl, 1, 480); + if (dataUrl) { + if (openStudio) { + onOpenThumbnailStudio(dataUrl); + } else { + onSetThumbnail(dataUrl); + } + } + } finally { + setExtractingThumb(false); + } + }; + + return ( +
+ {/* PDF Header Bar */} +
+ + +
+ {!readOnly && ( + <> + + + + + )} + + + + + + + + +
+
+ + {/* Embedded PDF Document Pane */} + {expanded && ( +
+