diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..211a005 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + # Site dependencies — opens a PR when a new pagi-help (or vitepress) version is published. + - package-ecosystem: npm + directory: /site + schedule: + interval: daily + open-pull-requests-limit: 5 diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml new file mode 100644 index 0000000..95ee6e1 --- /dev/null +++ b/.github/workflows/deploy-site.yml @@ -0,0 +1,60 @@ +name: Deploy Site + +on: + push: + branches: [master] + paths: + - "site/**" + - ".github/workflows/deploy-site.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: site + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: site/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build site + run: npm run docs:build + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: site/docs/.vitepress/dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 0000000..29cb39b --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +docs/.vitepress/cache/ +docs/.vitepress/dist/ +docs/public/pagihelp.global.js +*.log +.DS_Store diff --git a/site/README.md b/site/README.md new file mode 100644 index 0000000..814183e --- /dev/null +++ b/site/README.md @@ -0,0 +1,19 @@ +# PagiHelp Documentation Site + +The documentation website for [`pagi-help`](https://github.com/Codebucket-Solutions/PagiHelp), built with [VitePress](https://vitepress.dev) — live at **https://codebucket-solutions.github.io/PagiHelp/**. + +This folder is self-contained and is **not** published to npm with the library. + +## Local development + +```bash +cd site +npm install +npm run docs:dev # start local dev server +npm run docs:build # production build -> docs/.vitepress/dist +npm run docs:preview # preview the production build +``` + +## Deployment + +Pushing changes under `site/` to `master` triggers [`deploy-site.yml`](../.github/workflows/deploy-site.yml), which builds and publishes the site to GitHub Pages. The demo/playground bundle the published `pagi-help` npm package — Dependabot opens a PR when a new version is released, and merging it updates the live site. diff --git a/site/docs/.vitepress/config.mjs b/site/docs/.vitepress/config.mjs new file mode 100644 index 0000000..b842c1d --- /dev/null +++ b/site/docs/.vitepress/config.mjs @@ -0,0 +1,177 @@ +import { defineConfig } from "vitepress"; +import { readFileSync } from "node:fs"; + +const REPO = "https://github.com/Codebucket-Solutions/PagiHelp"; + +// GitHub Pages serves under /PagiHelp/; override with DOCS_BASE (e.g. "/") for a domain root. +const base = process.env.DOCS_BASE || "/PagiHelp/"; + +// Version badge: live from the npm registry at build time, package.json as offline fallback. +const pkg = JSON.parse( + readFileSync(new URL("../../../package.json", import.meta.url), "utf8") +); +async function libVersion() { + try { + const res = await fetch("https://registry.npmjs.org/pagi-help/latest", { + signal: AbortSignal.timeout(5000), + }); + if (res.ok) return (await res.json()).version; + } catch {} + return pkg.version; +} +const version = await libVersion(); + +export default defineConfig({ + base, + title: "PagiHelp", + description: + "Pagination query builder for MySQL and PostgreSQL — filters, search, sorting, unions, and cursor pagination.", + lang: "en-US", + cleanUrls: true, + lastUpdated: true, + ignoreDeadLinks: false, + + // Dark-first identity; the appearance toggle still offers light mode. + appearance: "dark", + + markdown: { + theme: { light: "github-light", dark: "tokyo-night" }, + }, + + head: [ + ["link", { rel: "icon", href: `${base}favicon.svg`, type: "image/svg+xml" }], + ["meta", { name: "theme-color", content: "#7c5cff" }], + ["meta", { property: "og:title", content: "PagiHelp" }], + [ + "meta", + { + property: "og:description", + content: + "Pagination that compiles to SQL — for MySQL and PostgreSQL.", + }, + ], + ], + + themeConfig: { + logo: "/logo.svg", + + nav: [ + { text: "Guide", link: "/guide/getting-started", activeMatch: "/guide/" }, + { text: "API", link: "/v2/overview", activeMatch: "/v2/" }, + { text: "Dialects", link: "/dialects/mysql", activeMatch: "/dialects/" }, + { + text: "Examples", + link: "/examples/filtering", + activeMatch: "/examples/", + }, + { text: "Demo", link: "/examples/sample-data" }, + { text: "Playground", link: "/playground" }, + { text: "Reference", link: "/api/reference" }, + { + text: "v" + version, + items: [ + { text: "npm", link: "https://www.npmjs.com/package/pagi-help" }, + { text: "Migrating to v2", link: "/guide/migration" }, + ], + }, + ], + + sidebar: { + "/guide/": [ + { + text: "Introduction", + items: [ + { text: "Getting Started", link: "/guide/getting-started" }, + { text: "Installation", link: "/guide/installation" }, + ], + }, + { + text: "Migration", + items: [ + { text: "Migrating to v2", link: "/guide/migration" }, + ], + }, + ], + "/v2/": [ + { + text: "API", + items: [ + { text: "Overview", link: "/v2/overview" }, + { text: "Constructor", link: "/v2/constructor" }, + { text: "paginate()", link: "/v2/paginate" }, + { text: "Cursor Pagination", link: "/v2/cursor-pagination" }, + { + text: "Filters & Operators", + link: "/v2/filters-and-operators", + }, + { text: "Sorting", link: "/v2/sorting" }, + { text: "Search", link: "/v2/search" }, + { text: "Return Shape", link: "/v2/return-shape" }, + ], + }, + ], + "/dialects/": [ + { + text: "Dialects", + items: [ + { text: "MySQL", link: "/dialects/mysql" }, + { text: "PostgreSQL", link: "/dialects/postgres" }, + ], + }, + ], + "/examples/": [ + { + text: "By Feature", + items: [ + { text: "Filtering", link: "/examples/filtering" }, + { text: "Searching", link: "/examples/searching" }, + { text: "Sorting", link: "/examples/sorting" }, + { text: "Pagination", link: "/examples/pagination" }, + ], + }, + { + text: "By Query Shape", + items: [ + { text: "Single Table", link: "/examples/single-table" }, + { text: "Joined Table", link: "/examples/joined-table" }, + { text: "Multi-Table Union", link: "/examples/multi-table-union" }, + { text: "Cursor Pagination", link: "/examples/cursor" }, + ], + }, + { + text: "Interactive", + items: [ + { text: "Interactive Demo", link: "/examples/sample-data" }, + { text: "Playground", link: "/playground" }, + ], + }, + ], + "/api/": [ + { + text: "Reference", + items: [{ text: "API Reference", link: "/api/reference" }], + }, + ], + }, + + socialLinks: [ + { icon: "github", link: REPO, ariaLabel: "GitHub repository" }, + { + icon: { + svg: 'npm', + }, + link: "https://www.npmjs.com/package/pagi-help", + ariaLabel: "npm package", + }, + ], + + search: { + provider: "local", + }, + + footer: { + message: "Released under the MIT License.", + copyright: "Copyright © Codebucket Solutions · Author: Abhinav Gautam", + }, + }, +}); diff --git a/site/docs/.vitepress/shims/crypto.js b/site/docs/.vitepress/shims/crypto.js new file mode 100644 index 0000000..037c1e1 --- /dev/null +++ b/site/docs/.vitepress/shims/crypto.js @@ -0,0 +1,35 @@ +// Minimal browser shim for Node's `crypto` — enough for cursor fingerprints. +// paginate() (search/sort/filter/pagination) never calls this. + +function fnv1aHex(input) { + // 128-bit-ish hex by hashing the string four times with different seeds. + const seeds = [0x811c9dc5, 0x01000193, 0x9e3779b9, 0x85ebca6b]; + return seeds + .map((seed) => { + let h = seed >>> 0; + for (let i = 0; i < input.length; i++) { + h ^= input.codePointAt(i); + h = Math.imul(h, 0x01000193) >>> 0; + } + return ("00000000" + h.toString(16)).slice(-8); + }) + .join(""); +} + +function createHash() { + let buffer = ""; + return { + update(value) { + buffer += String(value); + return this; + }, + digest() { + return fnv1aHex(buffer); + }, + }; +} + +const cryptoShim = { createHash }; + +export { createHash }; +export default cryptoShim; diff --git a/site/docs/.vitepress/theme/ConsoleHome.vue b/site/docs/.vitepress/theme/ConsoleHome.vue new file mode 100644 index 0000000..1b412e7 --- /dev/null +++ b/site/docs/.vitepress/theme/ConsoleHome.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/Capabilities.vue b/site/docs/.vitepress/theme/components/Capabilities.vue new file mode 100644 index 0000000..ac89743 --- /dev/null +++ b/site/docs/.vitepress/theme/components/Capabilities.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/DataTable.vue b/site/docs/.vitepress/theme/components/DataTable.vue new file mode 100644 index 0000000..d6d46aa --- /dev/null +++ b/site/docs/.vitepress/theme/components/DataTable.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/DialectShowcase.vue b/site/docs/.vitepress/theme/components/DialectShowcase.vue new file mode 100644 index 0000000..2c672d3 --- /dev/null +++ b/site/docs/.vitepress/theme/components/DialectShowcase.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/FeatureIcon.vue b/site/docs/.vitepress/theme/components/FeatureIcon.vue new file mode 100644 index 0000000..22e79a4 --- /dev/null +++ b/site/docs/.vitepress/theme/components/FeatureIcon.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/HeroConsole.vue b/site/docs/.vitepress/theme/components/HeroConsole.vue new file mode 100644 index 0000000..374366a --- /dev/null +++ b/site/docs/.vitepress/theme/components/HeroConsole.vue @@ -0,0 +1,543 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/Pipeline.vue b/site/docs/.vitepress/theme/components/Pipeline.vue new file mode 100644 index 0000000..8015dcb --- /dev/null +++ b/site/docs/.vitepress/theme/components/Pipeline.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/Playground.vue b/site/docs/.vitepress/theme/components/Playground.vue new file mode 100644 index 0000000..13c2de8 --- /dev/null +++ b/site/docs/.vitepress/theme/components/Playground.vue @@ -0,0 +1,591 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/SampleDataDemo.vue b/site/docs/.vitepress/theme/components/SampleDataDemo.vue new file mode 100644 index 0000000..d318ef1 --- /dev/null +++ b/site/docs/.vitepress/theme/components/SampleDataDemo.vue @@ -0,0 +1,717 @@ + + + + + diff --git a/site/docs/.vitepress/theme/fonts/geist-mono-400.woff2 b/site/docs/.vitepress/theme/fonts/geist-mono-400.woff2 new file mode 100644 index 0000000..62a80b0 Binary files /dev/null and b/site/docs/.vitepress/theme/fonts/geist-mono-400.woff2 differ diff --git a/site/docs/.vitepress/theme/fonts/geist-mono-600.woff2 b/site/docs/.vitepress/theme/fonts/geist-mono-600.woff2 new file mode 100644 index 0000000..623df56 Binary files /dev/null and b/site/docs/.vitepress/theme/fonts/geist-mono-600.woff2 differ diff --git a/site/docs/.vitepress/theme/fonts/geist-sans-400.woff2 b/site/docs/.vitepress/theme/fonts/geist-sans-400.woff2 new file mode 100644 index 0000000..9867e87 Binary files /dev/null and b/site/docs/.vitepress/theme/fonts/geist-sans-400.woff2 differ diff --git a/site/docs/.vitepress/theme/fonts/geist-sans-500.woff2 b/site/docs/.vitepress/theme/fonts/geist-sans-500.woff2 new file mode 100644 index 0000000..f6ee691 Binary files /dev/null and b/site/docs/.vitepress/theme/fonts/geist-sans-500.woff2 differ diff --git a/site/docs/.vitepress/theme/fonts/space-grotesk-500.woff2 b/site/docs/.vitepress/theme/fonts/space-grotesk-500.woff2 new file mode 100644 index 0000000..0db251f Binary files /dev/null and b/site/docs/.vitepress/theme/fonts/space-grotesk-500.woff2 differ diff --git a/site/docs/.vitepress/theme/fonts/space-grotesk-700.woff2 b/site/docs/.vitepress/theme/fonts/space-grotesk-700.woff2 new file mode 100644 index 0000000..44604a0 Binary files /dev/null and b/site/docs/.vitepress/theme/fonts/space-grotesk-700.woff2 differ diff --git a/site/docs/.vitepress/theme/index.js b/site/docs/.vitepress/theme/index.js new file mode 100644 index 0000000..ea03a79 --- /dev/null +++ b/site/docs/.vitepress/theme/index.js @@ -0,0 +1,28 @@ +import DefaultTheme from "vitepress/theme"; +import { h } from "vue"; +import Playground from "./components/Playground.vue"; +import SampleDataDemo from "./components/SampleDataDemo.vue"; +import ConsoleHome from "./ConsoleHome.vue"; + +// Design layer — import order matters: tokens → fonts → base → surfaces. +import "./styles/tokens.css"; +import "./styles/fonts.css"; +import "./styles/base.css"; +import "./styles/nav.css"; +import "./styles/sidebar.css"; +import "./styles/code.css"; +import "./styles/home.css"; + +export default { + extends: DefaultTheme, + // index.md uses layout:home with no hero/features, so our injected sections render while nav/search/footer stay. + Layout() { + return h(DefaultTheme.Layout, null, { + "home-hero-before": () => h(ConsoleHome), + }); + }, + enhanceApp({ app }) { + app.component("Playground", Playground); + app.component("SampleDataDemo", SampleDataDemo); + }, +}; diff --git a/site/docs/.vitepress/theme/styles/base.css b/site/docs/.vitepress/theme/styles/base.css new file mode 100644 index 0000000..ef580c8 --- /dev/null +++ b/site/docs/.vitepress/theme/styles/base.css @@ -0,0 +1,181 @@ +/* Typography scale, links, selection, scrollbar, buttons, motion. */ + +html { + scroll-behavior: smooth; +} + +body { + font-family: var(--ph-font-body); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +h1, +h2, +h3, +.VPHero .name, +.VPHero .text { + font-family: var(--ph-font-display); + letter-spacing: -0.02em; +} + +h1 { + font-weight: 700; +} +h2, +h3 { + font-weight: 600; +} + +.vp-doc h1 { + font-size: 2.1rem; + line-height: 1.2; +} +.vp-doc h2 { + margin-top: 2.4rem; + padding-top: 1.6rem; +} + +.vp-doc p, +.vp-doc li { + line-height: 1.75; +} + +.vp-doc a { + font-weight: 500; + text-decoration-color: var(--vp-c-brand-soft); + text-underline-offset: 3px; + transition: color 0.15s ease, text-decoration-color 0.15s ease; +} +.vp-doc a:hover { + text-decoration-color: var(--ph-violet); +} + +::selection { + background: rgba(124, 92, 255, 0.32); + color: var(--ph-text-1); +} + +:where(a, button, input, select, textarea, summary):focus-visible { + outline: 2px solid var(--ph-violet); + outline-offset: 2px; + border-radius: 4px; +} + +* { + scrollbar-width: thin; + scrollbar-color: var(--ph-border) transparent; +} +*::-webkit-scrollbar { + width: 10px; + height: 10px; +} +*::-webkit-scrollbar-thumb { + background: var(--ph-border); + border-radius: 8px; + border: 2px solid transparent; + background-clip: padding-box; +} +*::-webkit-scrollbar-thumb:hover { + background: var(--ph-text-3); + background-clip: padding-box; +} + +.VPButton.brand { + border: none; + background: var(--ph-grad); + background-size: 160% 160%; + color: #fff; + box-shadow: 0 8px 24px rgba(124, 92, 255, 0.32); + transition: transform 0.22s ease, box-shadow 0.22s ease, + background-position 0.6s ease; +} +.VPButton.brand:hover { + transform: translateY(-2px); + background-position: 100% 50%; + box-shadow: 0 14px 32px rgba(124, 92, 255, 0.42); +} + +.VPButton.alt { + border: 1px solid var(--vp-c-divider); + background: transparent; + transition: transform 0.22s ease, border-color 0.22s ease, color 0.22s ease; +} +.VPButton.alt:hover { + transform: translateY(-2px); + border-color: var(--ph-violet); + color: var(--ph-violet-hi); +} + +:not(pre) > code { + font-family: var(--ph-font-mono); + border-radius: 6px; + background: var(--vp-c-brand-soft) !important; + color: var(--vp-c-brand-1) !important; + padding: 0.18em 0.4em !important; + font-size: 0.86em; +} + +.custom-block { + border-radius: 12px; + border-width: 0 0 0 3px; +} +.custom-block.tip { + border-left-color: var(--ph-cyan); +} +.custom-block.info { + border-left-color: var(--ph-violet); +} +.custom-block.warning { + border-left-color: #fbbf24; +} +.custom-block.danger { + border-left-color: #f87171; +} + +.VPFooter { + position: relative; + background: var(--vp-c-bg-alt); + border-top: 1px solid var(--vp-c-divider); +} +.VPFooter::before { + content: ""; + position: absolute; + top: -1px; + left: 0; + right: 0; + height: 2px; + background: var(--ph-grad); + opacity: 0.6; +} +.VPFooter .message, +.VPFooter .copyright { + font-family: var(--ph-font-mono); + font-size: 12.5px; +} + +.VPContent { + animation: ph-fade 0.32s ease; +} +@keyframes ph-fade { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: none; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + .VPContent, + .VPButton.brand, + .VPButton.alt { + animation: none; + transition: none; + } +} diff --git a/site/docs/.vitepress/theme/styles/code.css b/site/docs/.vitepress/theme/styles/code.css new file mode 100644 index 0000000..71ec361 --- /dev/null +++ b/site/docs/.vitepress/theme/styles/code.css @@ -0,0 +1,65 @@ +/* Code blocks — console window chrome over VitePress's Shiki output. */ + +.vp-doc div[class*="language-"] { + border: 1px solid var(--vp-c-divider); + border-radius: 12px; + background: var(--vp-c-bg-soft); + padding-top: 38px; + box-shadow: 0 10px 28px rgba(2, 6, 16, 0.18); +} + +.vp-doc div[class*="language-"]::before { + content: ""; + position: absolute; + top: 14px; + left: 16px; + width: 10px; + height: 10px; + border-radius: 50%; + background: #f87171; + box-shadow: 18px 0 0 #fbbf24, 36px 0 0 var(--ph-cyan); + z-index: 3; +} + +.vp-doc div[class*="language-"] > span.lang { + top: 11px; + right: 14px; + font-family: var(--ph-font-mono); + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ph-text-3); + opacity: 1; +} + +.vp-doc div[class*="language-"] > button.copy { + top: 7px; + right: 56px; + width: 28px; + height: 24px; + border: 1px solid var(--vp-c-divider); + border-radius: 6px; + background-color: var(--vp-c-bg-alt); + background-size: 14px; +} +.vp-doc div[class*="language-"] > button.copy:hover { + border-color: var(--ph-violet); +} + +.vp-doc .vp-code-group .tabs { + font-family: var(--ph-font-mono); +} +.vp-doc .vp-code-group .tabs label { + font-size: 12px; +} +.vp-doc .vp-code-group .tabs input:checked + label { + color: var(--vp-c-brand-1); +} +.vp-doc .vp-code-group .tabs::after { + background: var(--ph-grad); +} + +pre, +code { + font-family: var(--ph-font-mono); +} diff --git a/site/docs/.vitepress/theme/styles/fonts.css b/site/docs/.vitepress/theme/styles/fonts.css new file mode 100644 index 0000000..047980e --- /dev/null +++ b/site/docs/.vitepress/theme/styles/fonts.css @@ -0,0 +1,47 @@ +/* Self-hosted fonts (bundled by Vite, no runtime CDN). + Display: Space Grotesk · Body: Geist Sans · Mono: Geist Mono */ + +@font-face { + font-family: "Space Grotesk"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("../fonts/space-grotesk-500.woff2") format("woff2"); +} +@font-face { + font-family: "Space Grotesk"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("../fonts/space-grotesk-700.woff2") format("woff2"); +} + +@font-face { + font-family: "Geist Sans"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("../fonts/geist-sans-400.woff2") format("woff2"); +} +@font-face { + font-family: "Geist Sans"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("../fonts/geist-sans-500.woff2") format("woff2"); +} + +@font-face { + font-family: "Geist Mono"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("../fonts/geist-mono-400.woff2") format("woff2"); +} +@font-face { + font-family: "Geist Mono"; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("../fonts/geist-mono-600.woff2") format("woff2"); +} diff --git a/site/docs/.vitepress/theme/styles/home.css b/site/docs/.vitepress/theme/styles/home.css new file mode 100644 index 0000000..dcd1fca --- /dev/null +++ b/site/docs/.vitepress/theme/styles/home.css @@ -0,0 +1,16 @@ +/* Home — let the injected console-home sections sit flush and full-width + (the default VPHome hero/features render nothing without frontmatter). */ + +.VPHome { + margin-bottom: 0 !important; + padding-bottom: 0 !important; +} + +.VPHome > .VPHomeHero:empty, +.VPHome > .VPHomeFeatures:empty { + display: none; +} + +.VPContent.is-home { + padding: 0 !important; +} diff --git a/site/docs/.vitepress/theme/styles/nav.css b/site/docs/.vitepress/theme/styles/nav.css new file mode 100644 index 0000000..7bce589 --- /dev/null +++ b/site/docs/.vitepress/theme/styles/nav.css @@ -0,0 +1,76 @@ +/* Nav bar — glassy, mono wordmark, command-palette search. */ + +.VPNavBar { + border-bottom: 1px solid transparent; +} +.VPNavBar.has-sidebar .content, +.VPNavBar .wrapper { + backdrop-filter: saturate(160%) blur(12px); +} + +/* Translucent surface so content scrolls under the bar */ +.VPNavBar:not(.home) { + background: color-mix(in srgb, var(--ph-bg-alt) 72%, transparent); + border-bottom-color: var(--vp-c-divider); +} +.VPNavBar.home.top { + background: transparent; +} +.VPNavBar:not(.top) { + background: color-mix(in srgb, var(--ph-bg-alt) 78%, transparent); + border-bottom-color: var(--vp-c-divider); +} + +.VPNavBarTitle .title { + font-family: var(--ph-font-display); + font-weight: 700; + letter-spacing: -0.02em; + gap: 8px; +} + +.VPNavBarMenuLink, +.VPNavBarMenuGroup .text { + font-family: var(--ph-font-mono); + font-size: 13px; + letter-spacing: -0.01em; +} +.VPNavBarMenuLink { + position: relative; +} +.VPNavBarMenuLink.active { + color: var(--vp-c-brand-1); +} +.VPNavBarMenuLink.active::after { + content: ""; + position: absolute; + left: 12px; + right: 12px; + bottom: 6px; + height: 2px; + border-radius: 2px; + background: var(--ph-grad); +} + +.DocSearch-Button, +.VPNavBarSearchButton { + border: 1px solid var(--vp-c-divider); + border-radius: 8px; +} +.VPNavBarSearch .DocSearch-Button-Keys, +.VPNavBarSearch .DocSearch-Button-Placeholder { + font-family: var(--ph-font-mono); +} + +/* Brand-colored social icons: npm red, GitHub mark black/white per theme. */ +.VPSocialLink[aria-label="npm package"], +.VPSocialLink[aria-label="npm package"]:hover { + color: #cb3837; +} +.VPSocialLink[aria-label="GitHub repository"], +.VPSocialLink[aria-label="GitHub repository"]:hover { + color: #24292f; +} +.dark .VPSocialLink[aria-label="GitHub repository"], +.dark .VPSocialLink[aria-label="GitHub repository"]:hover { + color: #ffffff; +} diff --git a/site/docs/.vitepress/theme/styles/sidebar.css b/site/docs/.vitepress/theme/styles/sidebar.css new file mode 100644 index 0000000..85d7322 --- /dev/null +++ b/site/docs/.vitepress/theme/styles/sidebar.css @@ -0,0 +1,43 @@ +/* Sidebar — mono section labels, accent active item. */ + +.VPSidebar { + background: var(--vp-c-bg-alt); +} + +.VPSidebarItem.level-0 > .item > .text, +.VPSidebarItem.collapsible.level-0 > .item .text { + font-family: var(--ph-font-mono); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--ph-text-3); +} + +.VPSidebarItem .link .text { + transition: color 0.15s ease; +} + +.VPSidebarItem.is-active > .item .link .text, +.VPSidebarItem.is-active > .item .text { + color: var(--vp-c-brand-1); + font-weight: 600; +} +.VPSidebarItem.is-link.is-active > .item { + position: relative; +} +.VPSidebarItem.is-link.is-active > .item::before { + content: ""; + position: absolute; + left: -14px; + top: 50%; + transform: translateY(-50%); + width: 2px; + height: 16px; + border-radius: 2px; + background: var(--ph-grad); +} + +.VPDocAsideOutline .title, +.VPDocOutlineItem { + font-family: var(--ph-font-mono); +} diff --git a/site/docs/.vitepress/theme/styles/tokens.css b/site/docs/.vitepress/theme/styles/tokens.css new file mode 100644 index 0000000..0fcf858 --- /dev/null +++ b/site/docs/.vitepress/theme/styles/tokens.css @@ -0,0 +1,103 @@ +/* "Query Console" design tokens, mapped onto VitePress CSS variables. + Dark is the default theme; light is a toggle. */ + +:root { + /* Brand accents (shared across themes) */ + --ph-violet: #7c5cff; + --ph-violet-hi: #9d86ff; + --ph-cyan: #22d3ee; + --ph-grad: linear-gradient(110deg, #7c5cff 0%, #22d3ee 100%); + + /* SQL syntax accents (hero console + code theme) */ + --ph-sql-keyword: #a78bfa; + --ph-sql-string: #5eead4; + --ph-sql-number: #fbbf24; + --ph-sql-ident: #e6e9f0; + --ph-sql-punct: #7c879e; + + /* Light theme surfaces (toggle) */ + --ph-bg: #ffffff; + --ph-bg-alt: #f7f8fc; + --ph-bg-soft: #ffffff; + --ph-border: #e6e8f0; + --ph-text-1: #0b1220; + --ph-text-2: #4a5268; + --ph-text-3: #6b7488; + + /* In light mode the SQL identifiers should be dark, not near-white */ + --ph-sql-ident: #0b1220; + + /* Typography */ + --ph-font-display: "Space Grotesk", "Geist Sans", ui-sans-serif, system-ui, + -apple-system, "Segoe UI", sans-serif; + --ph-font-body: "Geist Sans", ui-sans-serif, system-ui, -apple-system, + "Segoe UI", Roboto, sans-serif; + --ph-font-mono: "Geist Mono", ui-monospace, "SF Mono", "Menlo", "Consolas", + monospace; + + /* Map onto VitePress vars (brand + fonts are theme-agnostic) */ + --vp-c-brand-1: var(--ph-violet); + --vp-c-brand-2: var(--ph-violet-hi); + --vp-c-brand-3: var(--ph-cyan); + --vp-c-brand-soft: rgba(124, 92, 255, 0.14); + + --vp-font-family-base: var(--ph-font-body); + --vp-font-family-mono: var(--ph-font-mono); + + /* Hero name uses the signature gradient */ + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: var(--ph-grad); + + /* Brand button → gradient handled in base.css */ + --vp-button-brand-bg: var(--ph-violet); + --vp-button-brand-hover-bg: var(--ph-violet-hi); + --vp-button-brand-active-bg: var(--ph-violet); + --vp-button-brand-border: transparent; + --vp-button-brand-hover-border: transparent; + --vp-button-brand-active-border: transparent; + + /* Light-mode VitePress surfaces */ + --vp-c-bg: var(--ph-bg); + --vp-c-bg-alt: var(--ph-bg-alt); + --vp-c-bg-soft: var(--ph-bg-alt); + --vp-c-bg-elv: var(--ph-bg-soft); + --vp-c-divider: var(--ph-border); + --vp-c-border: var(--ph-border); + --vp-c-gutter: var(--ph-border); + --vp-c-text-1: var(--ph-text-1); + --vp-c-text-2: var(--ph-text-2); + --vp-c-text-3: var(--ph-text-3); +} + +/* Dark theme (default identity) */ +.dark { + --ph-bg: #0a0c10; + --ph-bg-alt: #0d1017; + --ph-bg-soft: #11151f; + --ph-border: #1c2230; + --ph-text-1: #e6e9f0; + --ph-text-2: #9aa4b8; + --ph-text-3: #7c879e; + --ph-sql-ident: #e6e9f0; + + --vp-c-bg: var(--ph-bg); + --vp-c-bg-alt: var(--ph-bg-alt); + --vp-c-bg-soft: var(--ph-bg-soft); + --vp-c-bg-elv: var(--ph-bg-soft); + --vp-c-divider: var(--ph-border); + --vp-c-border: var(--ph-border); + --vp-c-gutter: #05070b; + --vp-c-text-1: var(--ph-text-1); + --vp-c-text-2: var(--ph-text-2); + --vp-c-text-3: var(--ph-text-3); + + --vp-code-block-bg: var(--ph-bg-soft); + --vp-code-line-highlight-color: rgba(124, 92, 255, 0.12); + + --vp-home-hero-image-background-image: linear-gradient( + 135deg, + rgba(124, 92, 255, 0.35), + rgba(34, 211, 238, 0.3) + ); + --vp-home-hero-image-filter: blur(64px); +} diff --git a/site/docs/.vitepress/theme/utils/demoData.js b/site/docs/.vitepress/theme/utils/demoData.js new file mode 100644 index 0000000..f5e0ca6 --- /dev/null +++ b/site/docs/.vitepress/theme/utils/demoData.js @@ -0,0 +1,326 @@ +// Sample data + an in-browser evaluator for the interactive demo (PagiHelp only builds SQL). + +export const TABLES = { + products: { + name: "products", + columns: [ + { name: "id", type: "INT", key: "PK", align: "right" }, + { name: "sku", type: "VARCHAR(32)" }, + { name: "name", type: "VARCHAR(120)" }, + { name: "category", type: "VARCHAR(40)" }, + { name: "price", type: "DECIMAL(10,2)", align: "right" }, + { name: "stock", type: "INT", align: "right" }, + { name: "created_at", type: "TIMESTAMP" }, + ], + searchColumns: ["sku", "name"], + rows: [ + { id: 1, sku: "KB-100", name: "Mechanical Keyboard", category: "Peripherals", price: 3499, stock: 120, created_at: "2026-01-12" }, + { id: 2, sku: "MS-200", name: "Wireless Mouse", category: "Peripherals", price: 1299, stock: 340, created_at: "2026-02-03" }, + { id: 3, sku: "MN-270", name: "27-inch Monitor", category: "Displays", price: 13999, stock: 45, created_at: "2026-02-18" }, + { id: 4, sku: "DK-010", name: "Standing Desk", category: "Furniture", price: 24999, stock: 12, created_at: "2026-03-01" }, + { id: 5, sku: "CH-330", name: "Ergonomic Chair", category: "Furniture", price: 15999, stock: 30, created_at: "2026-03-22" }, + { id: 6, sku: "HS-040", name: "USB Headset", category: "Audio", price: 1999, stock: 0, created_at: "2025-12-30" }, + { id: 7, sku: "WC-050", name: "HD Webcam", category: "Audio", price: 2799, stock: 88, created_at: "2026-04-10" }, + { id: 8, sku: "DP-410", name: "USB-C Dock", category: "Accessories", price: 5499, stock: 60, created_at: "2026-04-25" }, + { id: 9, sku: "CB-500", name: "HDMI Cable", category: "Accessories", price: 349, stock: 500, created_at: "2026-05-09" }, + { id: 10, sku: "SP-220", name: "Bluetooth Speaker", category: "Audio", price: 2499, stock: 150, created_at: "2026-05-30" }, + ], + }, + + orders: { + name: "orders", + columns: [ + { name: "id", type: "INT", key: "PK", align: "right" }, + { name: "reference", type: "VARCHAR(24)" }, + { name: "status", type: "ENUM" }, + { name: "total", type: "DECIMAL(10,2)", align: "right" }, + { name: "created_at", type: "TIMESTAMP" }, + ], + searchColumns: ["reference", "status"], + rows: [ + { id: 1001, reference: "ORD-1001", status: "Paid", total: 12450.5, created_at: "2026-05-01" }, + { id: 1002, reference: "ORD-1002", status: "Pending", total: 2499, created_at: "2026-05-03" }, + { id: 1003, reference: "ORD-1003", status: "Paid", total: 30999, created_at: "2026-05-06" }, + { id: 1004, reference: "ORD-1004", status: "Refunded", total: 1499, created_at: "2026-05-09" }, + { id: 1005, reference: "ORD-1005", status: "Paid", total: 8999, created_at: "2026-05-12" }, + { id: 1006, reference: "ORD-1006", status: "Paid", total: 22999, created_at: "2026-05-15" }, + { id: 1007, reference: "ORD-1007", status: "Pending", total: 999, created_at: "2026-05-18" }, + { id: 1008, reference: "ORD-1008", status: "Paid", total: 49999, created_at: "2026-05-21" }, + { id: 1009, reference: "ORD-1009", status: "Refunded", total: 4599, created_at: "2026-05-24" }, + { id: 1010, reference: "ORD-1010", status: "Pending", total: 17999, created_at: "2026-05-27" }, + ], + }, +}; + +// PagiHelp option block for a table (alias === column name in this demo). +export function optionsFor(tableKey) { + const t = TABLES[tableKey]; + return [ + { + tableName: t.name, + columnList: t.columns.map((c) => ({ name: c.name, alias: c.name })), + searchColumnList: t.searchColumns.map((name) => ({ name })), + }, + ]; +} + +// Worked examples; `sql` is generated by scripts/gen-demo-sql.mjs from the real library. +export const EXAMPLES = [ + { + id: "in-stock-products", + table: "products", + title: "In-stock products, newest first", + desc: "Filter on a single column, sort descending, take the first page.", + request: { + filters: [["stock", ">", 0]], + sort: { attributes: ["created_at"], sorts: ["desc"] }, + pageNo: 1, + itemsPerPage: 5, + }, + sql: { + mysql: + "SELECT id AS id,sku AS sku,name AS name,category AS category,price AS price,stock AS stock,created_at AS created_at FROM `products` WHERE (stock > ?) ORDER BY `created_at`DESC,`id`DESC LIMIT ?,?", + postgres: + 'SELECT id AS "id",sku AS "sku",name AS "name",category AS "category",price AS "price",stock AS "stock",created_at AS "created_at" FROM "products" WHERE (stock > ?) ORDER BY "created_at"DESC,"id"DESC LIMIT ? OFFSET ?', + }, + }, + { + id: "search-category", + table: "products", + title: "Search catalog + filter by category", + desc: "Free-text search across searchable columns, combined with an IN filter.", + request: { + search: "USB", + filters: [["category", "IN", ["Audio", "Accessories"]]], + sort: { attributes: ["name"], sorts: ["asc"] }, + pageNo: 1, + itemsPerPage: 5, + }, + sql: { + mysql: + "SELECT id AS id,sku AS sku,name AS name,category AS category,price AS price,stock AS stock,created_at AS created_at FROM `products` WHERE (category IN (?,?)) AND ( sku LIKE ? OR name LIKE ? ) ORDER BY `name`ASC,`id`DESC LIMIT ?,?", + postgres: + 'SELECT id AS "id",sku AS "sku",name AS "name",category AS "category",price AS "price",stock AS "stock",created_at AS "created_at" FROM "products" WHERE (category IN (?,?)) AND ( sku LIKE ? OR name LIKE ? ) ORDER BY "name"ASC,"id"DESC LIMIT ? OFFSET ?', + }, + }, + { + id: "high-value-orders", + table: "orders", + title: "High-value paid orders", + desc: "Two filters combined with AND, sorted by amount.", + request: { + filters: [ + ["status", "=", "Paid"], + ["total", ">=", 10000], + ], + sort: { attributes: ["total"], sorts: ["desc"] }, + pageNo: 1, + itemsPerPage: 5, + }, + sql: { + mysql: + "SELECT id AS id,reference AS reference,status AS status,total AS total,created_at AS created_at FROM `orders` WHERE (status = ? AND total >= ?) ORDER BY `total`DESC,`id`DESC LIMIT ?,?", + postgres: + 'SELECT id AS "id",reference AS "reference",status AS "status",total AS "total",created_at AS "created_at" FROM "orders" WHERE (status = ? AND total >= ?) ORDER BY "total"DESC,"id"DESC LIMIT ? OFFSET ?', + }, + }, + { + id: "page-two", + table: "orders", + title: "Plain pagination — page 2", + desc: "No filters: sort by date and jump to the second page.", + request: { + sort: { attributes: ["created_at"], sorts: ["asc"] }, + pageNo: 2, + itemsPerPage: 4, + }, + sql: { + mysql: + "SELECT id AS id,reference AS reference,status AS status,total AS total,created_at AS created_at FROM `orders` ORDER BY `created_at`ASC,`id`DESC LIMIT ?,?", + postgres: + 'SELECT id AS "id",reference AS "reference",status AS "status",total AS "total",created_at AS "created_at" FROM "orders" ORDER BY "created_at"ASC,"id"DESC LIMIT ? OFFSET ?', + }, + }, +]; + +// In-browser evaluator: reproduces the query semantics over the demo rows. + +function likeToRegex(pattern, caseInsensitive) { + let out = "^"; + for (const ch of String(pattern)) { + if (ch === "%") out += ".*"; + else if (ch === "_") out += "."; + else out += ch.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); + } + out += "$"; + return new RegExp(out, caseInsensitive ? "i" : ""); +} + +function compare(a, b) { + if (typeof a === "number" && typeof b === "number") return a - b; + const sa = String(a); + const sb = String(b); + if (sa < sb) return -1; + return sa > sb ? 1 : 0; +} + +function matchFilter(row, [field, op, value]) { + const v = row[field]; + switch (String(op).toUpperCase()) { + case "=": + return v == value; + case "!=": + case "<>": + return v != value; + case ">": + return compare(v, value) > 0; + case ">=": + return compare(v, value) >= 0; + case "<": + return compare(v, value) < 0; + case "<=": + return compare(v, value) <= 0; + case "IN": + return Array.isArray(value) && value.some((x) => x == v); + case "NOT IN": + return Array.isArray(value) && !value.some((x) => x == v); + case "LIKE": + return likeToRegex(value, false).test(String(v)); + case "ILIKE": + return likeToRegex(value, true).test(String(v)); + default: + return true; + } +} + +// Operators the evaluator can reproduce; anything else still builds correct SQL but skips the row preview. +const EVALUABLE_OPS = new Set([ + "=", "!=", "<>", ">", ">=", "<", "<=", "IN", "NOT IN", "LIKE", "ILIKE", +]); + +function isConditionTuple(node) { + return ( + Array.isArray(node) && + node.length === 3 && + typeof node[0] === "string" && + typeof node[1] === "string" + ); +} + +// Evaluate the filter tree (even depth = AND, odd = OR); throws `.unsupportedOp` if an op can't be evaluated. +function evalFilterNode(node, row, depth) { + if (isConditionTuple(node)) { + const op = String(node[1]).toUpperCase(); + if (!EVALUABLE_OPS.has(op)) { + const err = new Error("unsupported operator"); + err.unsupportedOp = node[1]; + throw err; + } + return matchFilter(row, node); + } + if (!Array.isArray(node)) return true; + const results = node.map((child) => evalFilterNode(child, row, depth + 1)); + return depth % 2 === 0 ? results.every(Boolean) : results.some(Boolean); +} + +// Which columns search applies to; `complex` flags statement/prefixed columns the evaluator can't model. +function searchColumnsFor(opt, table) { + if (!Array.isArray(opt.searchColumnList)) { + return { cols: table.searchColumns, complex: false }; + } + const complex = opt.searchColumnList.some((c) => c?.statement || c?.prefix); + const cols = opt.searchColumnList + .filter((c) => c?.name && !c.prefix && !c.statement) + .map((c) => c.name); + return { cols, complex }; +} + +// AND-ed additionalWhereConditions, then free-text search, then the request filters. +function applyFilters(rows, request, searchCols, awc) { + let out = rows; + if (awc?.length) out = out.filter((r) => evalFilterNode(awc, r, 0)); + if (request.search) { + const term = String(request.search).toLowerCase(); + out = out.filter((r) => + searchCols.some((c) => String(r[c]).toLowerCase().includes(term)) + ); + } + if (request.filters?.length) { + out = out.filter((r) => evalFilterNode(request.filters, r, 0)); + } + return out; +} + +function sortRows(rows, sort) { + if (!sort?.attributes?.length) return rows; + return rows.sort((a, b) => { + for (let i = 0; i < sort.attributes.length; i += 1) { + const dir = String(sort.sorts[i]).toUpperCase() === "DESC" ? -1 : 1; + const c = compare(a[sort.attributes[i]], b[sort.attributes[i]]); + if (c !== 0) return c * dir; + } + return 0; + }); +} + +function paginateRows(rows, request) { + let offset = 0; + let limit = rows.length; + if (request.pageNo !== undefined && request.itemsPerPage !== undefined) { + offset = (request.pageNo - 1) * request.itemsPerPage; + limit = request.itemsPerPage; + } else if (request.offset !== undefined && request.limit !== undefined) { + offset = request.offset; + limit = request.limit; + } + return rows.slice(offset, offset + limit); +} + +// Run a request over the demo rows, honoring the options block; returns { computable, rows, total, reason? }. +export function runRequest(tableKey, request, options) { + const table = TABLES[tableKey]; + try { + const opt = (Array.isArray(options) && options[0]) || {}; + + if (opt.joinQuery && String(opt.joinQuery).trim()) { + return { + computable: false, + reason: "a custom joinQuery (joins aren’t modelled in the browser)", + rows: [], + total: 0, + }; + } + + const { cols: searchCols, complex } = searchColumnsFor(opt, table); + if (complex && request.search) { + return { + computable: false, + reason: "a statement/prefixed search column", + rows: [], + total: 0, + }; + } + + const filtered = applyFilters( + table.rows.slice(), + request, + searchCols, + opt.additionalWhereConditions + ); + const sorted = sortRows(filtered, request.sort); + return { + computable: true, + rows: paginateRows(sorted, request), + total: sorted.length, + }; + } catch (e) { + return { + computable: false, + reason: e?.unsupportedOp + ? `the “${e.unsupportedOp}” operator` + : "this request shape", + rows: [], + total: 0, + }; + } +} diff --git a/site/docs/.vitepress/theme/utils/sql.js b/site/docs/.vitepress/theme/utils/sql.js new file mode 100644 index 0000000..9e48a90 --- /dev/null +++ b/site/docs/.vitepress/theme/utils/sql.js @@ -0,0 +1,33 @@ +// Lightweight SQL tokenizer shared by the hero console and dialect showcase. +// Returns [{ t, cls }] where cls ∈ k(eyword) s(tring) n(umber) p(unct) w(hitespace) i(dent). + +const KEYWORDS = new Set( + ("select from where order by limit offset as and or in not like ilike asc " + + "desc union all is null on join left right inner count distinct group having") + .split(" ") +); + +// Punctuation includes JS object syntax ({ } [ ] :) so request objects render, plus SQL/Postgres operators. +const PUNCT = String.raw`[(),.?*;:=<>!~@|&+\-/{}\[\]]+`; +const RE = new RegExp( + `('[^']*'|\\$\\d+|\\b\\d+\\b|[A-Za-z_][\\w]*|\`[^\`]*\`|"[^"]*"|\\s+|${PUNCT})`, + "g" +); +const PUNCT_RE = new RegExp(`^${PUNCT}$`); + +export function tokenize(sql) { + const out = []; + let m; + RE.lastIndex = 0; + while ((m = RE.exec(sql))) { + const t = m[0]; + let cls = "i"; + if (/^\s+$/.test(t)) cls = "w"; + else if (/^'.*'$/.test(t) || /^".*"$/.test(t) || /^`.*`$/.test(t)) cls = "s"; + else if (/^\$\d+$/.test(t) || /^\d+$/.test(t)) cls = "n"; + else if (/^[A-Za-z_]/.test(t) && KEYWORDS.has(t.toLowerCase())) cls = "k"; + else if (PUNCT_RE.test(t)) cls = "p"; + out.push({ t, cls }); + } + return out; +} diff --git a/site/docs/api/reference.md b/site/docs/api/reference.md new file mode 100644 index 0000000..b73a623 --- /dev/null +++ b/site/docs/api/reference.md @@ -0,0 +1,199 @@ +# API Reference + +This page summarizes the public surface of the **v2** API. Full TypeScript types +ship with the package (`v2.d.ts` and `index.d.ts`). + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); +``` + +## Constructor + +```ts +new PagiHelpV2(options?: { + dialect?: "mysql" | "postgres"; // default "mysql" + columnNameConverter?: (name: string) => string; + safeOptions?: { validate?: boolean }; // only `validate` is supported +}) +``` + +See [Constructor](/v2/constructor). + +## Methods + +### `paginate(paginationObject, options)` + +Build offset/page-based pagination SQL. + +```ts +paginate( + paginationObject: PaginationInput, + options: PaginationOption[], + safeOptions?: { validate?: boolean } +): PaginationResult +``` + +→ [paginate()](/v2/paginate) + +### `paginateSafe(paginationObject, options, safeOptions?)` + +Validate the input and throw an `Error` on problems, then build SQL. Same return +shape as `paginate()`. + +### `paginateCursor(paginationObject, options)` + +Build keyset/cursor pagination SQL (single table, `after` only). + +```ts +paginateCursor( + paginationObject: CursorPaginationInput, // requires `sort` and `limit` + options: PaginationOption[], + safeOptions?: { validate?: boolean } +): CursorPaginationResult // PaginationResult + cursorPlan +``` + +→ [Cursor Pagination](/v2/cursor-pagination) + +### `resolveCursorPage(rows, cursorPlan)` + +Trim the extra fetched row and derive cursor metadata. + +```ts +resolveCursorPage(rows: Row[], cursorPlan: CursorPlan): { + rows: Row[]; + pageInfo: { + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor: string | null; + endCursor: string | null; + nextCursor: string | null; + }; +} +``` + +### `encodeCursorFromRow(row, cursorPlan)` + +```ts +encodeCursorFromRow(row: Record, cursorPlan: CursorPlan): string +``` + +Build an opaque `after` token from one query row. + +### `decodeCursor(token)` + +```ts +decodeCursor(cursorToken: string): { + v: 1; + d: "mysql" | "postgres"; + fp: string; + s: [attribute: string, direction: "ASC" | "DESC"][]; + values: unknown[]; + dir: "after"; +} +``` + +Decode and validate the cursor token envelope. + +### Validation helpers + +```ts +validatePaginationInput(paginationObject, options): ValidationResult +validatePaginationObject(paginationObject): ValidationResult +validateOptions(options): ValidationResult +validateCursorPaginationInput(paginationObject, options): ValidationResult +``` + +Each returns: + +```ts +interface ValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; +} +``` + +## Key types + +### `PaginationInput` + +```ts +interface PaginationInput { + search?: string; + filters?: ConditionInput; // [field, operator, value] tuples / nested groups + sort?: { attributes: string[]; sorts: ("ASC" | "DESC" | "asc" | "desc")[] }; + pageNo?: number; + itemsPerPage?: number; + offset?: number; + limit?: number; +} +``` + +### `CursorPaginationInput` + +```ts +interface CursorPaginationInput { + search?: string; + filters?: ConditionInput; + sort: { attributes: string[]; sorts: ("ASC" | "DESC" | "asc" | "desc")[] }; // required + limit: number; // required + after?: string; + // before, pageNo, itemsPerPage, offset are rejected +} +``` + +### `PaginationOption` + +```ts +interface PaginationOption { + tableName: string; + columnList: ColumnDescriptor[]; + searchColumnList?: SearchColumnDescriptor[]; // no `alias` on v2 + joinQuery?: string; + additionalWhereConditions?: ConditionInput; +} +``` + +### `ColumnDescriptor` + +```ts +interface ColumnDescriptor { + name?: string; // exactly one of name | statement + statement?: string; // raw SQL expression (trusted) + prefix?: string; // table alias, e.g. "l" -> l.column + alias?: string; // output alias +} +``` + +### `PaginationResult` + +```ts +interface PaginationResult { + query: string; + countQuery: string; + totalCountQuery: string; + replacements: unknown[]; +} +``` + +### `CursorPlan` (excerpt) + +```ts +interface CursorPlan { + version: 1; + dialect: "mysql" | "postgres"; + direction: "forward"; + requestedLimit: number; + fetchLimit: number; // requestedLimit + 1 + normalizedSort: { attribute: string; direction: "ASC" | "DESC" }[]; + cursorAliases: string[]; + queryFingerprint: string; + after: string | null; +} +``` + +::: tip +For the authoritative, always-current types, read `v2.d.ts` in the +[repository](https://github.com/Codebucket-Solutions/PagiHelp). +::: diff --git a/site/docs/dialects/mysql.md b/site/docs/dialects/mysql.md new file mode 100644 index 0000000..e105da1 --- /dev/null +++ b/site/docs/dialects/mysql.md @@ -0,0 +1,75 @@ +# MySQL Dialect + +MySQL is the default dialect. You can set it explicitly: + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); +``` + +## Identifier quoting + +MySQL quotes generated table names and `ORDER BY` identifiers with **backticks**: + +```sql +FROM `events` +ORDER BY `created_at`DESC +``` + +## Pagination clause + +MySQL uses a single `LIMIT` with offset and count: + +```sql +LIMIT ?,? +``` + +Replacements are `[offset, limit]`. + +For [cursor pagination](/v2/cursor-pagination), MySQL uses `LIMIT ?,?` with +replacements `[0, limit + 1]`. + +## Operators + +MySQL keeps its native operator set, including: + +- `JSON_CONTAINS`, `JSON_OVERLAPS` +- `FIND_IN_SET` +- `RLIKE` +- `MEMBER OF` + +plus the shared comparison/`IN`/`LIKE` operators. See +[Filters & Operators](/v2/filters-and-operators#shared-operators-both-dialects). + +## Example + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const result = pagiHelp.paginate( + { + search: "Active", + filters: [["status", "IN", ["Active", "Paused"]]], + sort: { attributes: ["created_at"], sorts: ["desc"] }, + pageNo: 1, + itemsPerPage: 10, + }, + [ + { + tableName: "events", + columnList: [ + { name: "id", alias: "id" }, + { name: "status", alias: "status" }, + { name: "created_at", alias: "created_at" }, + ], + searchColumnList: [{ name: "status" }], + }, + ] +); +``` + +::: tip +Use MySQL-only functions (e.g. `IF()`) only inside trusted-input fields like +`statement`, `joinQuery`, and raw `additionalWhereConditions`. +::: diff --git a/site/docs/dialects/postgres.md b/site/docs/dialects/postgres.md new file mode 100644 index 0000000..b5abaaf --- /dev/null +++ b/site/docs/dialects/postgres.md @@ -0,0 +1,121 @@ +# PostgreSQL Dialect + +Opt into PostgreSQL on the constructor: + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "postgres" }); +``` + +## Identifier quoting + +PostgreSQL quotes generated table names and `ORDER BY` identifiers with +**double quotes**: + +```sql +FROM "audit"."licenses" +ORDER BY "created_at"DESC +``` + +## Schema-qualified names + +Schema-qualified table names are supported on v2: + +- `tableName: "audit.licenses"` renders `FROM "audit"."licenses"`. +- If you want a table alias, keep it in `joinQuery`, **not** inside `tableName`. +- Raw `additionalWhereConditions` can use fully-qualified fields like + `"audit.licenses.organization_id"`. +- Regular `filters` still resolve by `alias` or `prefix.column`, **not** by + `schema.table.column`. + +## Pagination clause + +PostgreSQL uses separate `LIMIT` and `OFFSET`: + +```sql +LIMIT ? OFFSET ? +``` + +Replacements are `[limit, offset]`. + +For [cursor pagination](/v2/cursor-pagination), PostgreSQL uses +`LIMIT ? OFFSET ?` with replacements `[limit + 1, 0]`. + +## Native operators + +PostgreSQL has its own native operator set on v2: + +| Operator | Meaning | +| --- | --- | +| `ILIKE` | case-insensitive `LIKE` | +| `~` `~*` `!~` `!~*` | regex (match / case-insensitive / negated) | +| `@>` `<@` | contains / contained-by | +| `?` `?|` `?&` | jsonb key exists / any / all | +| `&&` | array overlap | + +```js +filters: [ + ["metaInfo", "@>", { priority: "high" }], + ["tags", "?|", ["featured", "priority"]], + ["reference", "~*", "^lic-2026-"], +]; +``` + +## Operator compatibility + +To ease migration of shared MySQL-style code, PostgreSQL keeps compatibility +aliases that translate to native SQL: + +| MySQL-style | PostgreSQL rendering | +| --- | --- | +| `JSON_CONTAINS` | `@>` | +| `JSON_OVERLAPS` | emulated `jsonb` overlap SQL | +| `FIND_IN_SET` | `array_position(string_to_array(...), ?::text) IS NOT NULL` | +| `RLIKE` | `~` | +| `MEMBER OF` | `?::jsonb @> to_jsonb(field)` | +| `! IN` | `NOT IN` | + +## Example + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "postgres" }); + +const result = pagiHelp.paginate( + { + search: "LIC", + filters: [ + ["metaInfo", "@>", { priority: "high" }], + ["tags", "?|", ["featured", "priority"]], + ["reference", "~*", "^lic-2026-"], + ], + sort: { attributes: ["createdAt"], sorts: ["desc"] }, + pageNo: 2, + itemsPerPage: 10, + }, + [ + { + tableName: "audit.licenses", + columnList: [ + { name: "license_id", alias: "id" }, + { name: "created_at", alias: "createdAt" }, + { name: "meta_info", alias: "metaInfo" }, + { name: "tags", alias: "tags" }, + { name: "reference", alias: "reference" }, + { + statement: + "(CASE WHEN audit.licenses.tier = '1' THEN 'Premium' ELSE 'Standard' END)", + alias: "tierLabel", + }, + ], + searchColumnList: [{ name: "reference" }], + additionalWhereConditions: [["audit.licenses.organization_id", "=", 42]], + }, + ] +); +``` + +::: warning +Use PostgreSQL SQL inside `statement`, `joinQuery`, and raw +`additionalWhereConditions`. Do not reuse MySQL-only functions like `IF()` there. +::: diff --git a/site/docs/examples/cursor.md b/site/docs/examples/cursor.md new file mode 100644 index 0000000..636c988 --- /dev/null +++ b/site/docs/examples/cursor.md @@ -0,0 +1,96 @@ +# Cursor Pagination + +A full round-trip: build the first page, resolve it, then fetch the next page +using the returned cursor. See [Cursor Pagination](/v2/cursor-pagination) for the +API details and rules (single-table, `after` only, `sort` + `limit` required). + +```js +const PagiHelpV2 = require("pagi-help/v2"); + +const pagiHelp = new PagiHelpV2({ dialect: "postgres" }); + +// 1. First page +const initialQueries = pagiHelp.paginateCursor( + { + search: "ORD", + filters: [["status", "=", "Paid"]], + sort: { attributes: ["createdAt"], sorts: ["desc"] }, + limit: 20, + }, + [ + { + tableName: "orders", + columnList: [ + { name: "order_id", alias: "id" }, + { name: "reference", alias: "reference" }, + { name: "status", alias: "status" }, + { name: "created_at", alias: "createdAt" }, + ], + searchColumnList: [{ name: "reference" }, { name: "status" }], + }, + ] +); +``` + +Generated first-page query: + +```sql +SELECT order_id AS "id",reference AS "reference",status AS "status",created_at AS "createdAt" +FROM "orders" +WHERE (status = ?) AND ( reference LIKE ? OR status LIKE ? ) +ORDER BY "createdAt"DESC,"id"DESC +LIMIT ? OFFSET ? +-- replacements: ["Paid", "%ORD%", "%ORD%", 21, 0] (fetches limit + 1 = 21) +``` + +```js +// 2. Run initialQueries.query against your DB, then resolve the page. +// (rows below are illustrative) +const fetchedRows = [ + { id: 1006, reference: "ORD-1006", status: "Paid", createdAt: "2026-05-15" }, + { id: 1003, reference: "ORD-1003", status: "Paid", createdAt: "2026-05-06" }, +]; + +const resolvedPage = pagiHelp.resolveCursorPage( + fetchedRows, + initialQueries.cursorPlan +); +// resolvedPage.pageInfo => { hasNextPage, hasPreviousPage, startCursor, endCursor, nextCursor } + +// 3. Next page — pass the endCursor back in as `after`. +// Repeat the SAME search/filters/sort that produced the cursor — only +// `after` changes — or the token is rejected ("Cursor token does not +// match the current query"). +const nextQueries = pagiHelp.paginateCursor( + { + search: "ORD", + filters: [["status", "=", "Paid"]], + sort: { attributes: ["createdAt"], sorts: ["desc"] }, + limit: 20, + after: resolvedPage.pageInfo.endCursor, + }, + [ + { + tableName: "orders", + columnList: [ + { name: "order_id", alias: "id" }, + { name: "reference", alias: "reference" }, + { name: "status", alias: "status" }, + { name: "created_at", alias: "createdAt" }, + ], + searchColumnList: [{ name: "reference" }, { name: "status" }], + }, + ] +); +``` + +## Key points + +- `query` fetches `limit + 1` rows; `resolveCursorPage()` trims the extra one and + computes `pageInfo`. +- `pageInfo.endCursor` (or `nextCursor`) becomes the `after` value for the next + call. +- The cursor token is opaque and self-describing (it encodes the dialect, sort, + a query fingerprint, and the keyset values). +- `countQuery` / `totalCountQuery` stay aggregate, and include the cursor + predicate when `after` is present. diff --git a/site/docs/examples/filtering.md b/site/docs/examples/filtering.md new file mode 100644 index 0000000..2dd3560 --- /dev/null +++ b/site/docs/examples/filtering.md @@ -0,0 +1,225 @@ +# Filtering Examples + +Filters are `[field, operator, value]` tuples. Top-level tuples are joined with +**AND**; a nested array of tuples becomes an **OR** group. Values are always +parameterized into `replacements` (never concatenated). This page covers **every +operator** the library supports, for both dialects. + +All examples build against this neutral `orders` table: + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); // or "postgres" + +const options = [ + { + tableName: "orders", + columnList: [ + { name: "order_id", alias: "id" }, + { name: "reference", alias: "reference" }, + { name: "status", alias: "status" }, + { name: "total", alias: "total" }, + { name: "created_at", alias: "createdAt" }, + // extra columns used by the JSON / array / regex examples below: + { name: "meta_info", alias: "meta" }, + { name: "tags", alias: "tags" }, + { name: "note", alias: "note" }, + ], + searchColumnList: [{ name: "reference" }, { name: "status" }], + }, +]; +``` + +::: tip Try it live +Every scenario here is reproducible in the [interactive Playground](/playground). +::: + +## Combining conditions + +Top-level tuples → **AND**: + +```js +filters: [ + ["status", "=", "Paid"], + ["total", ">=", 10000], +]; +// WHERE (status = ? AND total >= ?) replacements: ["Paid", 10000] +``` + +A nested array of tuples → **OR** group: + +```js +filters: [[["status", "=", "Paid"], ["status", "=", "Pending"]]]; +// WHERE (( status = ? OR status = ?)) replacements: ["Paid", "Pending"] +``` + +Mix them — AND of an OR group: + +```js +filters: [ + ["total", ">=", 10000], + [["status", "=", "Paid"], ["status", "=", "Pending"]], +]; +// WHERE (total >= ? AND ( status = ? OR status = ?)) +``` + +::: tip Single-condition shorthand +A bare tuple like `filters: ["status", "IN", ["Paid"]]` is normalized to +`[["status", "IN", ["Paid"]]]` automatically. +::: + +## A complete query, both dialects + +The same request renders per dialect — note the identifier quoting and the +pagination clause (and the reversed replacement order): + +::: code-group + +```sql [MySQL] +SELECT order_id AS id,reference AS reference,status AS status,total AS total,created_at AS createdAt,meta_info AS meta,tags AS tags,note AS note +FROM `orders` +WHERE (status = ? AND total >= ?) AND ( reference LIKE ? OR status LIKE ? ) +ORDER BY `total`DESC,`id`DESC +LIMIT ?,? +-- replacements: ["Paid", 10000, "%ORD%", "%ORD%", 0, 10] +``` + +```sql [PostgreSQL] +SELECT order_id AS "id",reference AS "reference",status AS "status",total AS "total",created_at AS "createdAt",meta_info AS "meta",tags AS "tags",note AS "note" +FROM "orders" +WHERE (status = ? AND total >= ?) AND ( reference LIKE ? OR status LIKE ? ) +ORDER BY "total"DESC,"id"DESC +LIMIT ? OFFSET ? +-- replacements: ["Paid", 10000, "%ORD%", "%ORD%", 10, 0] +``` + +::: + +```js +// request that produced the SQL above +{ + search: "ORD", + filters: [["status", "=", "Paid"], ["total", ">=", 10000]], + sort: { attributes: ["total"], sorts: ["desc"] }, + pageNo: 1, + itemsPerPage: 10, +} +``` + +## Shared operators (both dialects) + +These render the same `WHERE` fragment on MySQL and PostgreSQL: + +| Filter | Generated `WHERE` | Replacements | +| --- | --- | --- | +| `["status", "=", "Paid"]` | `(status = ?)` | `["Paid"]` | +| `["status", "!=", "Paid"]` | `(status != ?)` | `["Paid"]` | +| `["total", ">=", 10000]` | `(total >= ?)` | `[10000]` | +| `["status", "IN", ["Paid", "Pending"]]` | `(status IN (?,?))` | `["Paid", "Pending"]` | +| `["status", "NOT IN", ["Refunded"]]` | `(status NOT IN (?))` | `["Refunded"]` | +| `["reference", "LIKE", "ORD-10%"]` | `(reference LIKE ?)` | `["ORD-10%"]` | + +Also available: `>`, `<`, `<=`, `<>`. + +### `IS NULL` differs by dialect + +::: code-group + +```sql [MySQL] +-- filters: [["note", "IS", null]] +WHERE (note IS ?) -- replacements: [null] +-- filters: [["note", "IS NOT", null]] +WHERE (note IS NOT ?) -- replacements: [null] +``` + +```sql [PostgreSQL] +-- filters: [["note", "IS", null]] +WHERE (note IS NULL) -- replacements: [] +-- filters: [["note", "IS NOT", null]] +WHERE (note IS NOT NULL) -- replacements: [] +``` + +::: + +::: warning Empty `IN` is rejected +An empty `IN` / `NOT IN` array throws on v2 — it would otherwise produce invalid +SQL. +::: + +## MySQL-only operators + +Use these only with `dialect: "mysql"`: + +| Filter | Generated `WHERE` | Replacements | +| --- | --- | --- | +| `["reference", "RLIKE", "^ORD-10"]` | `(reference RLIKE ?)` | `["^ORD-10"]` | +| `["meta", "JSON_CONTAINS", { featured: true }]` | `(JSON_CONTAINS(meta_info, ?))` | `['{"featured":true}']` | +| `["tags", "JSON_OVERLAPS", ["featured", "gold"]]` | `(JSON_OVERLAPS(tags, ?))` | `['["featured","gold"]']` | +| `["tags", "FIND_IN_SET", "featured"]` | `(FIND_IN_SET(?, tags))` | `["featured"]` | +| `["tags", "MEMBER OF", "featured"]` | `(tags MEMBER OF ?)` | `["featured"]` | + +Object values are JSON-stringified automatically for `JSON_CONTAINS` / +`JSON_OVERLAPS`. + +## PostgreSQL native operators + +Use these only with `dialect: "postgres"`: + +| Filter | Generated `WHERE` | Replacements | +| --- | --- | --- | +| `["reference", "ILIKE", "ord-10%"]` | `(reference ILIKE ?)` | `["ord-10%"]` | +| `["reference", "~", "^ORD"]` | `(reference ~ ?)` | `["^ORD"]` | +| `["note", "~*", "urgent"]` | `(note ~* ?)` | `["urgent"]` | +| `["reference", "!~", "^TMP"]` | `(reference !~ ?)` | `["^TMP"]` | +| `["meta", "@>", { priority: "high" }]` | `((meta_info)::jsonb @> (?::jsonb))` | `['{"priority":"high"}']` | +| `["meta", "<@", { a: 1, b: 2 }]` | `((meta_info)::jsonb <@ (?::jsonb))` | `['{"a":1,"b":2}']` | +| `["tags", "?|", ["featured", "gold"]]` | `((tags)::jsonb ?\| ARRAY[?,?])` | `["featured", "gold"]` | +| `["tags", "?&", ["featured", "gold"]]` | `((tags)::jsonb ?& ARRAY[?,?])` | `["featured", "gold"]` | +| `["tags", "&&", ["featured", "gold"]]` | `(tags && ARRAY[?,?])` | `["featured", "gold"]` | + +Also available: `!~*`, and `?` (single key exists). + +## PostgreSQL compatibility aliases + +So shared MySQL-style code keeps working under `dialect: "postgres"`, these +MySQL-style operators are translated to native PostgreSQL SQL: + +| MySQL-style filter | Rendered on PostgreSQL | +| --- | --- | +| `["meta", "JSON_CONTAINS", { featured: true }]` | `((meta_info)::jsonb @> (?::jsonb))` | +| `["tags", "FIND_IN_SET", "featured"]` | `(array_position(string_to_array(COALESCE(tags::text, ''), ','), ?::text) IS NOT NULL)` | +| `["reference", "RLIKE", "^ORD"]` | `(reference ~ ?)` | +| `["status", "! IN", ["Refunded"]]` | `(status NOT IN (?))` | + +::: warning `! IN` is Postgres-only +On PostgreSQL `! IN` is translated to `NOT IN`. On **MySQL** it renders literally +as `! IN`, which is **not valid MySQL** — use `NOT IN` for MySQL. +::: + +## Raw conditions: `additionalWhereConditions` + +Per option block, these are AND-ed into every query as **raw** SQL (trusted +input, not validated): + +```js +const options = [ + { + tableName: "orders", + columnList: [/* ... */], + searchColumnList: [/* ... */], + additionalWhereConditions: [["orders.org_id", "=", 42]], + }, +]; + +// with paginationObject.filters: [["status", "=", "Paid"]] +// WHERE (orders.org_id = ?) AND (status = ?) replacements: [42, "Paid"] +``` + +::: warning Trusted input only +Never build `additionalWhereConditions`, `statement`, or `joinQuery` from +untrusted user input. Use regular `filters` for anything user-controlled — those +are parameterized and operator-validated. +::: + +See [Filters & Operators](/v2/filters-and-operators) for the reference, and the +[PostgreSQL dialect](/dialects/postgres) page for native-operator details. diff --git a/site/docs/examples/joined-table.md b/site/docs/examples/joined-table.md new file mode 100644 index 0000000..51ea4ad --- /dev/null +++ b/site/docs/examples/joined-table.md @@ -0,0 +1,84 @@ +# Joined Table + +Use `joinQuery` to add a JOIN, `prefix` to qualify columns, and `statement` for +computed columns. This example joins `orders` to `shipments` and uses a +`columnNameConverter` to map camelCase to snake_case. + +```js +const PagiHelpV2 = require("pagi-help/v2"); + +const pagiHelp = new PagiHelpV2({ + dialect: "mysql", + columnNameConverter: (name) => + name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`), +}); + +const paginationObject = { + search: "NEW", + filters: [ + ["expedited", "=", "Yes"], + ["o.status", "IN", ["NEW", "PACKING"]], + ], + sort: { + attributes: ["createdDate"], + sorts: ["desc"], + }, + pageNo: 1, + itemsPerPage: 20, +}; + +const options = [ + { + tableName: "orders", + columnList: [ + { name: "order_id", prefix: "o", alias: "id" }, + { name: "created_date", prefix: "o", alias: "createdDate" }, + { name: "status", prefix: "o", alias: "status" }, + { + statement: '(SELECT IF(o.priority="1","Yes","No"))', + alias: "expedited", + }, + { name: "carrier", prefix: "s", alias: "carrier" }, + ], + searchColumnList: [ + { name: "carrier", prefix: "s" }, + { name: "status", prefix: "o" }, + ], + joinQuery: " o left join shipments s on o.order_id = s.order_id ", + additionalWhereConditions: [["o.archived", "=", 0]], + }, +]; + +const result = pagiHelp.paginate(paginationObject, options); +``` + +Generated MySQL query: + +```sql +SELECT o.order_id AS id,o.created_date AS createdDate,o.status AS status, + (SELECT IF(o.priority="1","Yes","No")) AS expedited,s.carrier AS carrier +FROM `orders` o left join shipments s on o.order_id = s.order_id +WHERE (o.archived = ?) + AND ((SELECT IF(o.priority="1","Yes","No")) = ? AND o.status IN (?,?)) + AND ( s.carrier LIKE ? OR o.status LIKE ? ) +ORDER BY `created_date`DESC,`id`DESC +LIMIT ?,? +-- replacements: [0, "Yes", "NEW", "PACKING", "%NEW%", "%NEW%", 0, 20] +``` + +## Notes + +- **`prefix`** renders `o.created_date`, `s.carrier`, etc. +- **`statement`** is raw SQL for computed/derived columns (trusted input). The + MySQL `IF()` above is fine in a MySQL context; use Postgres SQL for the + Postgres dialect. +- **Filters can reference** an `alias` (`expedited` → matches the `expedited` + computed column), or a `prefix.column` (`o.status`). +- **`joinQuery`** is concatenated verbatim after `FROM \`orders\``; include the + leading space and table alias yourself. +- **`additionalWhereConditions`** are raw conditions AND-ed into every query. + +::: warning +`joinQuery`, `statement`, and `additionalWhereConditions` are trusted-input SQL. +Never build them from untrusted user input. +::: diff --git a/site/docs/examples/multi-table-union.md b/site/docs/examples/multi-table-union.md new file mode 100644 index 0000000..6d6e93c --- /dev/null +++ b/site/docs/examples/multi-table-union.md @@ -0,0 +1,57 @@ +# Multi-Table Union + +Pass more than one block in `options` to combine several tables with `UNION ALL` +into one paginated result set. PagiHelp aligns the column lists by `alias`, +filling gaps with `NULL` so every block selects the same columns. + +```js +const PagiHelpV2 = require("pagi-help/v2"); + +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const paginationObject = { + search: "", + sort: { + attributes: ["id"], + sorts: ["asc"], + }, + offset: 10, + limit: 20, +}; + +const options = [ + { + tableName: "campaigns", + columnList: [ + { name: "campaign_id", alias: "id" }, + { name: "campaign_name", alias: "name" }, + ], + searchColumnList: [{ name: "campaign_name" }], + additionalWhereConditions: [["status", "=", "Active"]], + }, + { + tableName: "licenses", + columnList: [{ name: "license_id", alias: "id" }], + searchColumnList: [], + additionalWhereConditions: [["status", "=", "Active"]], + }, +]; + +const result = pagiHelp.paginate(paginationObject, options); + +console.log(JSON.stringify(result, null, 2)); +``` + +## How the union is built + +- Each option block becomes one `SELECT ... FROM ...`, joined with `UNION ALL`. +- Column lists are aligned by `alias`. Missing columns are filled with + `(NULL) AS alias` so each `SELECT` is union-compatible. +- `totalCountQuery` sums the per-table aggregate counts: + `SELECT SUM(countValue) AS countValue FROM ( ... UNION ALL ... ) AS totalCounts`. +- The shared `sort` and pagination window apply to the combined set. + +::: tip +Give tables that should not participate in search a `searchColumnList: []`, as the +`licenses` block does above. +::: diff --git a/site/docs/examples/pagination.md b/site/docs/examples/pagination.md new file mode 100644 index 0000000..11fb542 --- /dev/null +++ b/site/docs/examples/pagination.md @@ -0,0 +1,120 @@ +# Pagination Examples + +PagiHelp supports two windowing styles — **page-based** +(`pageNo` + `itemsPerPage`) and **offset-based** (`offset` + `limit`) — and emits +a dialect-specific `LIMIT` clause. For unbounded forward paging, see +[Cursor Pagination](/examples/cursor). + +All examples use this demo table: + +```js +const PagiHelpV2 = require("pagi-help/v2"); + +const options = [ + { + tableName: "orders", + columnList: [ + { name: "order_id", alias: "id" }, + { name: "reference", alias: "reference" }, + { name: "status", alias: "status" }, + { name: "total", alias: "total" }, + { name: "created_at", alias: "createdAt" }, + ], + searchColumnList: [{ name: "reference" }, { name: "status" }], + }, +]; +``` + +::: tip Try it live +Toggle page-based vs offset-based and watch the clause change in the +[interactive Playground](/playground). +::: + +## Page-based (MySQL) + +```js +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); +const result = pagiHelp.paginate({ pageNo: 2, itemsPerPage: 25 }, options); +``` + +```sql +... LIMIT ?,? -- [25, 25] +``` + +The offset is computed as `(pageNo - 1) * itemsPerPage` → `25`, with `25` rows. + +## Offset-based (MySQL) + +```js +const result = pagiHelp.paginate({ offset: 40, limit: 20 }, options); +``` + +```sql +... LIMIT ?,? -- [40, 20] +``` + +MySQL replacements are `[offset, limit]`. + +## Page-based (PostgreSQL) + +```js +const pagiHelp = new PagiHelpV2({ dialect: "postgres" }); +const result = pagiHelp.paginate({ pageNo: 2, itemsPerPage: 25 }, options); +``` + +```sql +... LIMIT ? OFFSET ? -- [25, 25] +``` + +## Offset-based (PostgreSQL) + +```js +const result = pagiHelp.paginate({ offset: 40, limit: 20 }, options); +``` + +```sql +... LIMIT ? OFFSET ? -- [20, 40] +``` + +PostgreSQL replacements are `[limit, offset]` — the reverse order of MySQL. + +## Clause & order cheat-sheet + +| Dialect | Clause | Replacements | +| --- | --- | --- | +| MySQL | `LIMIT ?,?` | `[offset, limit]` | +| PostgreSQL | `LIMIT ? OFFSET ?` | `[limit, offset]` | + +## Rules + +- Provide **either** `pageNo` + `itemsPerPage` **or** `offset` + `limit`. +- Each pair must be supplied together; supplying only one half is rejected. +- If both pairs are given, page-based values win. + +## Counting total rows + +Use the aggregate `totalCountQuery` (returns a `countValue` column) to compute the +total number of pages: + +::: code-group + +```js [mysql2] +const [[{ countValue }]] = await connection.query( + result.totalCountQuery, + result.replacements +); +const totalPages = Math.ceil(countValue / itemsPerPage); +``` + +```js [Sequelize] +const [{ countValue }] = await sequelize.query(result.totalCountQuery, { + replacements: result.replacements, + type: QueryTypes.SELECT, +}); +const totalPages = Math.ceil(countValue / itemsPerPage); +``` + +::: + +On v2, `countQuery` and `totalCountQuery` are the same aggregate query — run +either one, once. See [Return Shape](/v2/return-shape). diff --git a/site/docs/examples/sample-data.md b/site/docs/examples/sample-data.md new file mode 100644 index 0000000..a3d146b --- /dev/null +++ b/site/docs/examples/sample-data.md @@ -0,0 +1,10 @@ +--- +title: Interactive Demo +--- + +# Interactive Demo + +The fastest way to understand PagiHelp is to watch it work against a real table — +everything below runs the real engine in your browser. + + diff --git a/site/docs/examples/searching.md b/site/docs/examples/searching.md new file mode 100644 index 0000000..8fe30b0 --- /dev/null +++ b/site/docs/examples/searching.md @@ -0,0 +1,114 @@ +# Searching Examples + +Free-text search matches one `search` string against every column in +`searchColumnList` using `LIKE` with `%value%` wrapping — on **both** dialects. +The columns are OR-ed together and the whole group is AND-ed with your filters. +See [Search](/v2/search) for the rules. + +All examples use this neutral `orders` table: + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const options = [ + { + tableName: "orders", + columnList: [ + { name: "order_id", alias: "id" }, + { name: "reference", alias: "reference" }, + { name: "status", alias: "status" }, + { name: "total", alias: "total" }, + { name: "created_at", alias: "createdAt" }, + ], + searchColumnList: [{ name: "reference" }, { name: "status" }], + }, +]; +``` + +::: tip Try it live +Tweak the `search` box in the [interactive Playground](/playground). +::: + +## Search across multiple columns + +```js +const result = pagiHelp.paginate( + { search: "ORD", pageNo: 1, itemsPerPage: 10 }, + options +); +``` + +```sql +WHERE ( reference LIKE ? OR status LIKE ? ) -- ["%ORD%", "%ORD%"] +``` + +One replacement is pushed per search column. + +## Search a single column + +With `searchColumnList: [{ name: "reference" }]`: + +```sql +WHERE ( reference LIKE ? ) -- ["%ORD%"] +``` + +## Empty / missing search + +An empty string (or omitted `search`) produces **no** search predicate: + +```js +const result = pagiHelp.paginate( + { search: "", pageNo: 1, itemsPerPage: 10 }, + options +); +``` + +```sql +-- no WHERE from search +SELECT ... FROM `orders` LIMIT ?,? -- [0, 10] +``` + +## Search combined with filters + +The search group is AND-ed after your filters: + +```js +const result = pagiHelp.paginate( + { + search: "ORD", + filters: [["status", "=", "Paid"]], + pageNo: 1, + itemsPerPage: 10, + }, + options +); +``` + +```sql +WHERE (status = ?) AND ( reference LIKE ? OR status LIKE ? ) +-- ["Paid", "%ORD%", "%ORD%"] +``` + +## Excluding a table from search + +Give a table `searchColumnList: []` so it contributes no search predicate — +useful in [multi-table unions](/examples/multi-table-union). + +## Case-insensitive search on PostgreSQL + +`LIKE` is case-sensitive on PostgreSQL. For case-insensitive matching, express it +as a filter with the native `ILIKE` operator instead of relying on `search`: + +```js +const pg = new PagiHelpV2({ dialect: "postgres" }); + +const result = pg.paginate( + { filters: [["reference", "ILIKE", "ord-%"]], pageNo: 1, itemsPerPage: 10 }, + options +); +``` + +```sql +WHERE (reference ILIKE ?) -- ["ord-%"] +``` diff --git a/site/docs/examples/single-table.md b/site/docs/examples/single-table.md new file mode 100644 index 0000000..bc2980b --- /dev/null +++ b/site/docs/examples/single-table.md @@ -0,0 +1,58 @@ +# Single Table + +The simplest case: paginate one table with search, filters, and sorting. + +```js +const PagiHelpV2 = require("pagi-help/v2"); + +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const paginationObject = { + search: "campaign", + filters: [ + ["status", "=", "Active"], + ["created_date", ">=", "2026-01-01"], + ], + sort: { + attributes: ["created_date"], + sorts: ["desc"], + }, + pageNo: 1, + itemsPerPage: 10, +}; + +const options = [ + { + tableName: "campaigns", + columnList: [ + { name: "campaign_id", alias: "id" }, + { name: "campaign_name", alias: "campaign_name" }, + { name: "created_date", alias: "created_date" }, + { name: "status", alias: "status" }, + ], + searchColumnList: [{ name: "campaign_name" }, { name: "status" }], + }, +]; + +const result = pagiHelp.paginate(paginationObject, options); + +console.log(JSON.stringify(result, null, 2)); +``` + +## What you get back + +```js +{ + query, // SELECT ... FROM `campaigns` WHERE ... ORDER BY ... LIMIT ?,? + countQuery, // aggregate COUNT(*) returning countValue + totalCountQuery, // identical to countQuery on v2 + replacements, // positional values +} +``` + +- Top-level filters are AND-ed: `status = ? AND created_date >= ?`. +- `search` adds `( campaign_name LIKE ? OR status LIKE ? )`. +- A trailing `id DESC` tie-breaker is appended to the sort. + +See [Filters & Operators](/v2/filters-and-operators) and +[Return Shape](/v2/return-shape) for the details. diff --git a/site/docs/examples/sorting.md b/site/docs/examples/sorting.md new file mode 100644 index 0000000..4a1a5a5 --- /dev/null +++ b/site/docs/examples/sorting.md @@ -0,0 +1,95 @@ +# Sorting Examples + +Sorting uses two parallel arrays — `attributes` and `sorts` — of equal length. +A deterministic `id DESC` tie-breaker is appended automatically. See +[Sorting](/v2/sorting) for the rules. + +All examples use this demo table: + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const options = [ + { + tableName: "orders", + columnList: [ + { name: "order_id", alias: "id" }, + { name: "reference", alias: "reference" }, + { name: "status", alias: "status" }, + { name: "total", alias: "total" }, + { name: "created_at", alias: "createdAt" }, + ], + searchColumnList: [{ name: "reference" }, { name: "status" }], + }, +]; +``` + +::: tip Try it live +Add and reorder sort fields in the [interactive Playground](/playground). +::: + +## Single column, descending + +```js +sort: { attributes: ["createdAt"], sorts: ["desc"] } +``` + +```sql +ORDER BY `createdAt`DESC,`id`DESC +``` + +The trailing `` `id`DESC `` is the automatic tie-breaker. + +## Single column, ascending + +```js +sort: { attributes: ["total"], sorts: ["asc"] } +``` + +```sql +ORDER BY `total`ASC,`id`DESC +``` + +## Multiple columns + +Sort by status ascending, then total descending: + +```js +sort: { attributes: ["status", "total"], sorts: ["asc", "desc"] } +``` + +```sql +ORDER BY `status`ASC,`total`DESC,`id`DESC +``` + +::: warning +`attributes` and `sorts` must be the **same length**, and each `sorts` entry must +be `"asc"` or `"desc"` (case-insensitive). Anything else is rejected. +::: + +## PostgreSQL identifier quoting + +The same sort under `dialect: "postgres"` uses double quotes: + +```js +const pg = new PagiHelpV2({ dialect: "postgres" }); +// sort: { attributes: ["createdAt"], sorts: ["desc"] } +``` + +```sql +ORDER BY "createdAt"DESC,"id"DESC +``` + +## Your input is never mutated + +PagiHelp does not append `id` to the `sort` arrays you pass in — the tie-breaker +is added only to the generated SQL. Your original `paginationObject.sort` stays +untouched, so you can safely reuse it. + +```js +const sort = { attributes: ["createdAt"], sorts: ["desc"] }; +pagiHelp.paginate({ sort, pageNo: 1, itemsPerPage: 10 }, options); + +console.log(sort.attributes); // still ["createdAt"] — not mutated +``` diff --git a/site/docs/guide/getting-started.md b/site/docs/guide/getting-started.md new file mode 100644 index 0000000..0537f72 --- /dev/null +++ b/site/docs/guide/getting-started.md @@ -0,0 +1,111 @@ +# Getting Started + +PagiHelp (`pagi-help`) is a small helper that turns a pagination request into +safe, parameterized SQL. You describe **what** you want — filters, search, +sorting, page size — and PagiHelp builds the `SELECT`, `WHERE`, `ORDER BY`, and +`LIMIT` clauses plus a `replacements` array you hand to your database driver. + +It does **not** connect to a database. It only generates SQL strings and +replacements, so it works with any MySQL or PostgreSQL client (`mysql2`, `pg`, +Sequelize raw queries, etc.). + +## Install + +```bash +npm install pagi-help +``` + +The only runtime dependency is [`sqlstring`](https://www.npmjs.com/package/sqlstring). + +## Import + +Import the API (MySQL + PostgreSQL): + +```js +const PagiHelpV2 = require("pagi-help/v2"); +``` + +::: tip Existing v1 code? +A legacy **v1** API still ships from the package root (`require("pagi-help")`) for +existing MySQL codebases. New code should use the above — see +[Migrating to v2](/guide/migration). +::: + +## Your first query + +```js +const PagiHelpV2 = require("pagi-help/v2"); + +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const result = pagiHelp.paginate( + { + search: "Active", + filters: [["status", "IN", ["Active", "Paused"]]], + sort: { attributes: ["created_at"], sorts: ["desc"] }, + pageNo: 1, + itemsPerPage: 10, + }, + [ + { + tableName: "events", + columnList: [ + { name: "id", alias: "id" }, + { name: "status", alias: "status" }, + { name: "created_at", alias: "created_at" }, + ], + searchColumnList: [{ name: "status" }], + }, + ] +); + +console.log(result); +// { query, countQuery, totalCountQuery, replacements } +``` + +## Running the generated SQL + +PagiHelp gives you parameterized SQL. Pass `query` and `replacements` to your +driver: + +::: code-group + +```js [mysql2] +const [rows] = await connection.query(result.query, result.replacements); +const [[{ countValue }]] = await connection.query( + result.totalCountQuery, + result.replacements +); +``` + +```js [Sequelize] +const { QueryTypes } = require("sequelize"); + +const rows = await sequelize.query(result.query, { + replacements: result.replacements, + type: QueryTypes.SELECT, +}); +const [{ countValue }] = await sequelize.query(result.totalCountQuery, { + replacements: result.replacements, + type: QueryTypes.SELECT, +}); +``` + +::: + +Note the destructuring difference: mysql2 resolves to a `[rows, fields]` pair, +while Sequelize with `type: QueryTypes.SELECT` returns the rows directly — using +the mysql2 double-destructure there throws `object is not iterable`. + +::: tip +The `replacements` array is positional and already in the right order for the +generated SQL. Always pass it alongside the query — never interpolate values +into the SQL string yourself. +::: + +## How it fits together + +| You provide | PagiHelp builds | +| ----------- | --------------- | +| `paginationObject` (search, filters, sort, page/limit) | the dynamic `WHERE`, `ORDER BY`, and `LIMIT` | +| `options[]` (tableName, columnList, searchColumnList, joins) | the `SELECT ... FROM` and column list | diff --git a/site/docs/guide/installation.md b/site/docs/guide/installation.md new file mode 100644 index 0000000..f7f255b --- /dev/null +++ b/site/docs/guide/installation.md @@ -0,0 +1,52 @@ +# Installation + +## Requirements + +- Node.js (CommonJS `require` is used throughout the examples) +- A MySQL or PostgreSQL database and a client driver of your choice +- One runtime dependency, [`sqlstring`](https://www.npmjs.com/package/sqlstring), is installed automatically + +## Install the package + +::: code-group + +```bash [npm] +npm install pagi-help +``` + +```bash [yarn] +yarn add pagi-help +``` + +```bash [pnpm] +pnpm add pagi-help +``` + +::: + +## Import + +The API (MySQL + PostgreSQL): + +```js +const PagiHelpV2 = require("pagi-help/v2"); +``` + +Named exports are also available from the package root: + +```js +const { + PagiHelpV2, // the current API + PagiHelpV210, // compatibility alias for PagiHelpV2 + PagiHelpLegacy, // the legacy v1 API — see Migrating to v2 +} = require("pagi-help"); +``` + +`PagiHelpV210` is a compatibility alias for `PagiHelpV2`. The legacy v1 export +(`require("pagi-help")`) is covered in [Migrating to v2](/guide/migration). + +## TypeScript + +Type definitions ship with the package (`index.d.ts` and `v2.d.ts`), so no +`@types/*` install is needed. See the [API Reference](/api/reference) for the +exported types. diff --git a/site/docs/guide/migration.md b/site/docs/guide/migration.md new file mode 100644 index 0000000..b8e2aab --- /dev/null +++ b/site/docs/guide/migration.md @@ -0,0 +1,79 @@ +# Migrating to v2 + +v2 (`require("pagi-help/v2")`) is the current, actively developed API. The default +export `require("pagi-help")` is the legacy **v1** class — MySQL-only, its +`paginate()` behavior preserved unchanged for existing consumers. Both take the same +`paginationObject` and `options` shape, so migration is mostly an import swap plus a +few behavior changes. + +## What's new in v2 + +- **PostgreSQL** dialect alongside MySQL (native `@>`, `ILIKE`, `~*`, …) — v1 is MySQL-only +- **Cursor pagination** — [`paginateCursor()`](/v2/cursor-pagination) with opaque, fingerprinted tokens +- **Aggregate `countQuery`** returning `countValue` (v1's was a row-select) +- **Validation on by default** — `paginate()` rejects malformed input; helpers return `{ valid, errors, warnings }` reports +- **Cleaner behavior** — empty `search` handled safely (v1 turned it into `%undefined%`), no dangling `WHERE`, no caller `sort` mutation, no `console.log`, and `Error` objects instead of string throws + +## How to migrate + +### 1. Bump the version, then change the import + +If your `package.json` pins `"pagi-help": "^1.x"`, npm will **never** install 2.x +automatically — a caret stays within the same major. Move the range up first: + +```bash +npm install pagi-help@^2 +``` + +This is safe: `require("pagi-help")` still returns the legacy class with unchanged +`paginate()` behavior, so existing code keeps working. You opt into v2 per file by +importing the v2 entry: + +```js +// Before +const PagiHelp = require("pagi-help"); +const pagiHelp = new PagiHelp(); + +// After +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); +``` + +The constructor now accepts a [`dialect`](/v2/constructor) (`"mysql"` default or +`"postgres"`). + +### 2. Account for behavior changes + +Everything in [What's new in v2](#what-s-new-in-v2) applies automatically. The one +change that usually needs code is `countQuery` — it is now an aggregate returning +`countValue`, identical to `totalCountQuery` (run either one), so read that instead +of counting result rows: + +::: code-group + +```js [mysql2] +const [[{ countValue }]] = await connection.query( + result.countQuery, + result.replacements +); +``` + +```js [Sequelize] +const [{ countValue }] = await sequelize.query(result.countQuery, { + replacements: result.replacements, + type: QueryTypes.SELECT, +}); +``` + +::: + +Everything else — clean `search`, no `WHERE` dangling, no mutation or logging, +stricter validation, `Error` objects — needs no action and is strictly safer. + +### 3. (Optional) adopt new features + +Once on v2 you can use: + +- [PostgreSQL dialect](/dialects/postgres) +- [Cursor pagination](/v2/cursor-pagination) +- Built-in [validation helpers](/v2/overview#validation) diff --git a/site/docs/index.md b/site/docs/index.md new file mode 100644 index 0000000..bd9fb33 --- /dev/null +++ b/site/docs/index.md @@ -0,0 +1,9 @@ +--- +layout: home +title: Pagination that compiles to SQL +titleTemplate: Pagination query builder for MySQL & PostgreSQL + +# No `hero`/`features` frontmatter: the default home hero/features render +# nothing, and the bespoke "Query Console" landing is injected via the +# theme's `home-hero-before` slot (see .vitepress/theme/index.js). +--- diff --git a/site/docs/playground.md b/site/docs/playground.md new file mode 100644 index 0000000..31b42e4 --- /dev/null +++ b/site/docs/playground.md @@ -0,0 +1,35 @@ +--- +title: Playground +outline: false +--- + +# Interactive Playground + +Build a pagination request and watch PagiHelp generate the SQL **live** — this +runs the real `pagi-help/v2` engine in your browser. Switch dialects, add +filters, sorts, search, and a page window, then copy the output. + + + +## Demo schema + +The playground queries this table: + +```js +const options = [ + { + tableName: "orders", + columnList: [ + { name: "order_id", alias: "id" }, + { name: "reference", alias: "reference" }, + { name: "status", alias: "status" }, + { name: "total", alias: "total" }, + { name: "created_at", alias: "createdAt" }, + ], + searchColumnList: [{ name: "reference" }, { name: "status" }], + }, +]; +``` + +Want more recipes? See the worked [examples](/examples/filtering) for filtering, +searching, sorting, and pagination. diff --git a/site/docs/public/favicon.svg b/site/docs/public/favicon.svg new file mode 100644 index 0000000..7a8e428 --- /dev/null +++ b/site/docs/public/favicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/site/docs/public/logo.svg b/site/docs/public/logo.svg new file mode 100644 index 0000000..918b13f --- /dev/null +++ b/site/docs/public/logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/site/docs/v2/constructor.md b/site/docs/v2/constructor.md new file mode 100644 index 0000000..8824850 --- /dev/null +++ b/site/docs/v2/constructor.md @@ -0,0 +1,57 @@ +# Constructor + +```js +const PagiHelpV2 = require("pagi-help/v2"); + +const pagiHelp = new PagiHelpV2({ + dialect: "mysql", // default + columnNameConverter: (name) => + name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`), + safeOptions: { + validate: true, + }, +}); +``` + +## Options + +### `dialect` + +- May be `"mysql"` or `"postgres"`. +- Omitted `dialect` defaults to `"mysql"`. +- Controls identifier quoting, the operator set, and the pagination clause. See + [MySQL](/dialects/mysql) and [PostgreSQL](/dialects/postgres). + +### `columnNameConverter` + +A function applied to column `name` values (and `ORDER BY` identifiers) before +they are rendered. Use it to map your code-side naming (e.g. camelCase) to your +database column naming (e.g. snake_case): + +```js +columnNameConverter: (name) => + name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); +// createdAt -> created_at +``` + +If omitted, names are passed through unchanged (identity function). It does +**not** apply to `statement` expressions — those are raw SQL. + +### `safeOptions` + +```js +safeOptions: { + validate: true, // the only supported safeOptions key +} +``` + +- `safeOptions.validate` is the **only** supported `safeOptions` key. +- It defaults to `true`, so `paginate()` and the cursor builders validate input + and throw on errors. +- Any other `safeOptions` key is rejected. + +## Rules summary + +- `dialect` may be `"mysql"` or `"postgres"`; omitted defaults to `"mysql"`. +- `safeOptions.validate` is the only supported `safeOptions` key; any other key + is rejected. diff --git a/site/docs/v2/cursor-pagination.md b/site/docs/v2/cursor-pagination.md new file mode 100644 index 0000000..a534215 --- /dev/null +++ b/site/docs/v2/cursor-pagination.md @@ -0,0 +1,172 @@ +# Cursor Pagination + +`paginateCursor()` generates keyset (cursor) pagination SQL. It is available on +**v2 only** and uses opaque, self-describing cursor tokens. + +```js +const cursorQueries = pagiHelp.paginateCursor(paginationObject, options); +// { query, countQuery, totalCountQuery, replacements, cursorPlan } +``` + +## Phase-1 rules + +The current implementation is phase 1 and enforces: + +- **single-table only** (exactly one option block) +- **`after` only** (forward paging) +- `sort` is **required** +- `limit` is **required** +- `pageNo`, `itemsPerPage`, `offset`, and `before` are **rejected** +- the selected columns must include alias `id` + +## Building the first page + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "postgres" }); + +const cursorQueries = pagiHelp.paginateCursor( + { + search: "mail", + filters: [["stage", "=", "OPEN"]], + sort: { attributes: ["createdAt"], sorts: ["desc"] }, + limit: 20, + // after: existingCursorToken // omit for the first page + }, + [ + { + tableName: "audit.licenses", + columnList: [ + { name: "license_id", alias: "id" }, + { name: "created_at", alias: "createdAt" }, + { name: "stage", alias: "stage" }, + ], + searchColumnList: [{ name: "stage" }], + }, + ] +); +``` + +## Return shape + +```js +{ + countQuery, + totalCountQuery, + query, + replacements, + cursorPlan, // metadata consumed by the helpers below +} +``` + +- `query` fetches `limit + 1` rows (the extra row detects `hasNextPage`). +- `cursorPlan` carries the normalized sort, dialect, fetch size, and a query + fingerprint that ties a cursor to the query that produced it. + +## Resolving a page + +After running `query`, hand the rows and the `cursorPlan` to +`resolveCursorPage()`. It trims the extra row and returns `pageInfo`: + +::: code-group + +```js [mysql2] +const [rows] = await connection.query( + cursorQueries.query, + cursorQueries.replacements +); +``` + +```js [Sequelize] +const rows = await sequelize.query(cursorQueries.query, { + replacements: cursorQueries.replacements, + type: QueryTypes.SELECT, +}); +``` + +::: + +```js +const page = pagiHelp.resolveCursorPage(rows, cursorQueries.cursorPlan); +// { +// rows: [...], // trimmed to `limit` +// pageInfo: { +// hasNextPage, +// hasPreviousPage, +// startCursor, +// endCursor, +// nextCursor, +// } +// } +``` + +## Fetching the next page + +Feed `page.pageInfo.endCursor` (or `nextCursor`) back in as `after`. The cursor +token is bound to the query that produced it — dialect, `search`, `filters`, and +`sort` are baked into its fingerprint — so the next call **must repeat the same +`search`, `filters`, and `sort`**; only `after` changes. A mismatch is rejected +at runtime with `Cursor token does not match the current query`. + +```js +const next = pagiHelp.paginateCursor( + { + search: "mail", + filters: [["stage", "=", "OPEN"]], + sort: { attributes: ["createdAt"], sorts: ["desc"] }, + limit: 20, + after: page.pageInfo.endCursor, + }, + [ + { + tableName: "audit.licenses", + columnList: [ + { name: "license_id", alias: "id" }, + { name: "created_at", alias: "createdAt" }, + { name: "stage", alias: "stage" }, + ], + searchColumnList: [{ name: "stage" }], + }, + ] +); +``` + +## Cursor token helpers + +| Helper | Purpose | +| --- | --- | +| `encodeCursorFromRow(row, cursorPlan)` | Build an opaque `after` token from one row | +| `decodeCursor(token)` | Decode and validate the token envelope | +| `resolveCursorPage(rows, cursorPlan)` | Trim the extra row and derive `pageInfo` | + +The decoded token envelope looks like: + +```js +{ + v: 1, // version + d: "postgres", // dialect + fp: "...", // query fingerprint + s: [["createdAt", "DESC"], ["id", "DESC"]], // sort + appended id tie-breaker (directions upper-cased) + values: [...], // the keyset values + dir: "after", +} +``` + +## Count semantics + +- `countQuery` and `totalCountQuery` remain **aggregate** on v2. +- When `after` is present, **both** count queries include the cursor predicate. + +## Pagination clause + +- **MySQL** cursor pagination uses `LIMIT ?,?` with replacements `[0, limit + 1]`. +- **PostgreSQL** cursor pagination uses `LIMIT ? OFFSET ?` with replacements + `[limit + 1, 0]`. + +::: tip Roadmap +Phase 1 covers single-table forward (`after`) paging. Backward (`before`) paging +and multi-table cursors are reserved for future phases — `before` is currently +rejected at runtime. +::: + +See the full runnable script in [Examples → Cursor Pagination](/examples/cursor). diff --git a/site/docs/v2/filters-and-operators.md b/site/docs/v2/filters-and-operators.md new file mode 100644 index 0000000..918f7d1 --- /dev/null +++ b/site/docs/v2/filters-and-operators.md @@ -0,0 +1,114 @@ +# Filters & Operators + +Filters are how you express `WHERE` conditions. Each condition is a tuple: + +```js +[field, operator, value] +``` + +- `field` resolves against your `columnList` — by `alias`, by camelCase form of + the alias, or by `prefix.column`. +- `operator` must be one of the supported operators (validated). +- `value` is parameterized into `replacements` (never concatenated). + +`filters` is optional — omit it, or pass an empty `filters: []`, and no +conditions are added. + +## Combining conditions: AND / OR + +Top-level filters are joined with **AND**. Nesting an array of tuples creates an +**OR** group. + +```js +// status = 'Active' AND created_at >= '2026-01-01' +filters: [ + ["status", "=", "Active"], + ["created_at", ">=", "2026-01-01"], +]; +``` + +```js +// status = 'Active' AND ( type = 'A' OR type = 'B' ) +filters: [ + ["status", "=", "Active"], + [ + ["type", "=", "A"], + ["type", "=", "B"], + ], +]; +``` + +::: tip Single condition shorthand +A bare tuple like `filters: ["status", "IN", ["Active"]]` is normalized to +`[["status", "IN", ["Active"]]]` automatically. +::: + +## Values and `IN` + +- Array values render an `IN (?, ?, ...)` list with one replacement per element. +- On v2, an **empty** `IN [ ]` array is rejected (it would otherwise produce + invalid SQL). + +```js +filters: [["status", "IN", ["Active", "Paused"]]]; +// status IN (?,?) replacements: ["Active", "Paused"] +``` + +## Shared operators (both dialects) + +``` +> >= < <= = != <> +IN NOT IN ! IN +IS IS NOT +LIKE RLIKE +MEMBER OF +JSON_CONTAINS JSON_OVERLAPS FIND_IN_SET +``` + +- `! IN` is rewritten to `NOT IN` on **PostgreSQL only**. On MySQL it is emitted + verbatim (`field ! IN (...)`), which is not valid SQL — use `NOT IN` for MySQL. +- `JSON_CONTAINS(field, ?)` and `JSON_OVERLAPS(field, ?)` JSON-stringify object + values automatically. +- `FIND_IN_SET` renders as `FIND_IN_SET(?, field)`. +- Renderings shown are the **MySQL** forms; on Postgres these compat aliases + compile to jsonb/array expressions — see + [operator compatibility](/dialects/postgres#operator-compatibility). + +## PostgreSQL native operators + +When the dialect is `postgres`, these additional native operators are available: + +| Operator | Meaning | +| --- | --- | +| `ILIKE` | case-insensitive `LIKE` | +| `~` `~*` `!~` `!~*` | regex match / case-insensitive / negated | +| `@>` `<@` | contains / contained-by (jsonb, arrays) | +| `?` `?|` `?&` | key exists / any key / all keys (jsonb) | +| `&&` | array overlap | + +```js +filters: [ + ["metaInfo", "@>", { priority: "high" }], + ["tags", "?|", ["featured", "priority"]], + ["reference", "~*", "^ord-2026-"], +]; +``` + +Postgres also keeps compatibility aliases so shared MySQL-style code keeps +working — see [PostgreSQL dialect](/dialects/postgres#operator-compatibility). + +## Raw conditions: `additionalWhereConditions` + +`additionalWhereConditions` (per option block) are **raw** conditions AND-ed into +every generated query. They are trusted input — operators are not validated and +the SQL is concatenated as written: + +```js +additionalWhereConditions: [["audit.licenses.organization_id", "=", 42]]; +``` + +::: warning +Never build `additionalWhereConditions`, `statement`, or `joinQuery` from +untrusted user input. Use regular `filters` for anything user-controlled — those +are parameterized and operator-validated. +::: diff --git a/site/docs/v2/overview.md b/site/docs/v2/overview.md new file mode 100644 index 0000000..851f02b --- /dev/null +++ b/site/docs/v2/overview.md @@ -0,0 +1,76 @@ +# Overview + +PagiHelp turns a pagination request into safe, parameterized SQL. Import it +directly: + +```js +const PagiHelpV2 = require("pagi-help/v2"); +``` + +It supports both MySQL and PostgreSQL, generates aggregate count queries, never +mutates your input, and is where features like cursor pagination live. + +## The shape of a request + +Every paginate call takes two arguments: + +1. A **`paginationObject`** — the dynamic request: `search`, `filters`, `sort`, + and the page window (`pageNo`/`itemsPerPage` or `offset`/`limit`). +2. An **`options`** array — one block per table describing `tableName`, + `columnList`, `searchColumnList`, optional `joinQuery`, and optional + `additionalWhereConditions`. + +```js +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const result = pagiHelp.paginate(paginationObject, options); +// { query, countQuery, totalCountQuery, replacements } +``` + +## Methods + +| Method | Purpose | +| --- | --- | +| [`paginate(paginationObject, options)`](/v2/paginate) | Build offset/page-based SQL | +| [`paginateCursor(paginationObject, options)`](/v2/cursor-pagination) | Build keyset/cursor SQL (single table) | +| `paginateSafe(paginationObject, options, safeOptions)` | Validate then build, throwing on invalid input | +| [`resolveCursorPage(rows, cursorPlan)`](/v2/cursor-pagination#resolving-a-page) | Trim the extra row and derive `pageInfo` | +| [`encodeCursorFromRow(row, cursorPlan)`](/v2/cursor-pagination) | Build an opaque `after` token | +| [`decodeCursor(token)`](/v2/cursor-pagination) | Decode and validate a cursor token | +| `validatePaginationInput(paginationObject, options)` | Return a `{ valid, errors, warnings }` report | +| `validateCursorPaginationInput(...)` | Validation report for cursor input | + +## Column descriptors + +Columns in `columnList` are described by objects. A descriptor must define +**exactly one** of `name` or `statement`: + +```js +{ name: "created_at", alias: "createdAt" } // plain column +{ name: "id", prefix: "l", alias: "id" } // prefixed: l.id +{ statement: "(CASE WHEN ... END)", alias: "flag" } // raw SQL expression +``` + +- `name` — the column name (passed through `columnNameConverter`) +- `prefix` — optional table alias/prefix, e.g. `l` → `l.column` +- `statement` — raw SQL expression (trusted input), mutually exclusive with `name` +- `alias` — the output alias; recommended for filters, sorts, and unions + +`searchColumnList` uses the same descriptor shape **without** `alias` (v2 rejects +aliases there). + +## Validation + +v2 ships validation helpers that return a structured report instead of throwing: + +```js +const report = pagiHelp.validatePaginationInput(paginationObject, options); +// { valid: boolean, errors: string[], warnings: string[] } + +if (!report.valid) { + throw new Error(report.errors.join("\n")); +} +``` + +`paginateSafe()` runs this validation for you and throws an `Error` if the input +is invalid (controlled by `safeOptions.validate`, which defaults to `true`). diff --git a/site/docs/v2/paginate.md b/site/docs/v2/paginate.md new file mode 100644 index 0000000..2f9e379 --- /dev/null +++ b/site/docs/v2/paginate.md @@ -0,0 +1,124 @@ +# paginate() + +```js +const result = pagiHelp.paginate(paginationObject, options); +``` + +Builds offset/page-based pagination SQL. Returns `{ query, countQuery, +totalCountQuery, replacements }` — see [Return Shape](/v2/return-shape). + +## `paginationObject` + +```js +{ + search: "Active", // optional free-text search + filters: [["status", "IN", ["Active", "Paused"]]], // optional conditions + sort: { // optional ordering + attributes: ["created_at"], + sorts: ["desc"], + }, + pageNo: 1, // page-based window ... + itemsPerPage: 10, + // ... or offset-based window: + // offset: 10, + // limit: 20, +} +``` + +| Field | Description | +| --- | --- | +| `search` | Free-text value matched with `LIKE` against `searchColumnList`. See [Search](/v2/search). | +| `filters` | `[field, operator, value]` tuples, nestable into AND/OR groups. See [Filters & Operators](/v2/filters-and-operators). | +| `sort` | `{ attributes, sorts }` parallel arrays. See [Sorting](/v2/sorting). | +| `pageNo` + `itemsPerPage` | Page-based window. Both required together. | +| `offset` + `limit` | Offset-based window. Both required together. | + +::: tip Page window rules +Provide **either** `pageNo`/`itemsPerPage` **or** `offset`/`limit`. Each pair +must be supplied together. If both pairs are given, page-based values win. +Note: `offset: 0` emits **no** pagination clause — use `pageNo: 1` + +`itemsPerPage` for the first page. +::: + +## `options` + +An array of one or more table blocks. Multiple blocks are combined with +`UNION ALL` — see [Multi-Table Union](/examples/multi-table-union). + +```js +[ + { + tableName: "events", + columnList: [ + { name: "id", alias: "id" }, + { name: "status", alias: "status" }, + { name: "created_at", alias: "created_at" }, + ], + searchColumnList: [{ name: "status" }], + joinQuery: "", // optional raw JOIN SQL + additionalWhereConditions: [], // optional raw conditions (AND-ed in) + }, +] +``` + +| Field | Description | +| --- | --- | +| `tableName` | Table name. Postgres supports `schema.table`. | +| `columnList` | Column descriptors to select. Should include alias `id`. | +| `searchColumnList` | Columns matched by `search` (no `alias` allowed on v2). | +| `joinQuery` | Raw JOIN SQL appended after `FROM`. Trusted input. | +| `additionalWhereConditions` | Raw conditions AND-ed into every query. Trusted input. | + +::: warning Trusted-input fields +`joinQuery`, `statement`, and `additionalWhereConditions` are concatenated as raw +SQL (operators are not validated for these). Never build them from untrusted user +input. Regular `filters` and `search` values **are** parameterized. +::: + +## Full example + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const result = pagiHelp.paginate( + { + search: "Active", + filters: [["status", "IN", ["Active", "Paused"]]], + sort: { attributes: ["created_at"], sorts: ["desc"] }, + pageNo: 1, + itemsPerPage: 10, + }, + [ + { + tableName: "events", + columnList: [ + { name: "id", alias: "id" }, + { name: "status", alias: "status" }, + { name: "created_at", alias: "created_at" }, + ], + searchColumnList: [{ name: "status" }], + }, + ] +); +``` + +## paginateSafe() + +`paginateSafe(paginationObject, options, safeOptions)` validates the input first +(via `validatePaginationInput`) and throws an `Error` listing all problems before +building SQL. With v2 defaults (`validate: true`), `paginate()` already runs in a +hardened mode; `paginateSafe()` makes the validate-then-build step explicit. + +```js +const report = pagiHelp.validatePaginationInput(paginationObject, options); +if (!report.valid) throw new Error(report.errors.join("\n")); + +const result = pagiHelp.paginateSafe(paginationObject, options); +``` + +## Related + +- [Return Shape](/v2/return-shape) +- [Cursor Pagination](/v2/cursor-pagination) for keyset-based paging +- [Examples](/examples/single-table) diff --git a/site/docs/v2/return-shape.md b/site/docs/v2/return-shape.md new file mode 100644 index 0000000..fd6a1ce --- /dev/null +++ b/site/docs/v2/return-shape.md @@ -0,0 +1,60 @@ +# Return Shape + +Both `paginate()` and `paginateSafe()` return the same object: + +```js +{ + query, // the row SELECT with WHERE, ORDER BY, and LIMIT + countQuery, // aggregate COUNT(*) returning countValue + totalCountQuery, // identical to countQuery on v2 + replacements, // positional values for query / count queries +} +``` + +On v2, `countQuery` and `totalCountQuery` are the **same aggregate query** — both +fields exist because they differed in [v1](/guide/migration) (row-select vs +aggregate). Run either one, once. + +[`paginateCursor()`](/v2/cursor-pagination) returns the same fields **plus** a +`cursorPlan`. + +## Using the result + +::: code-group + +```js [mysql2] +const [rows] = await connection.query(result.query, result.replacements); +const [[{ countValue }]] = await connection.query( + result.totalCountQuery, + result.replacements +); +``` + +```js [Sequelize] +const { QueryTypes } = require("sequelize"); + +const rows = await sequelize.query(result.query, { + replacements: result.replacements, + type: QueryTypes.SELECT, +}); +const [{ countValue }] = await sequelize.query(result.totalCountQuery, { + replacements: result.replacements, + type: QueryTypes.SELECT, +}); +``` + +::: + +The aggregate count queries return a single column named `countValue`. mysql2 +resolves to a `[rows, fields]` pair (hence the double destructure); Sequelize +with `type: QueryTypes.SELECT` returns the rows directly. + +## Pagination clause by dialect + +When a page window is given, the `query` ends with a dialect-specific pagination +clause: + +- **MySQL**: `LIMIT ?,?` with replacements `[offset, limit]` +- **PostgreSQL**: `LIMIT ? OFFSET ?` with replacements `[limit, offset]` + +See [MySQL](/dialects/mysql) and [PostgreSQL](/dialects/postgres) for details. diff --git a/site/docs/v2/search.md b/site/docs/v2/search.md new file mode 100644 index 0000000..bbec770 --- /dev/null +++ b/site/docs/v2/search.md @@ -0,0 +1,52 @@ +# Search + +Free-text search matches a single `search` string against the columns listed in +each option block's `searchColumnList`, using `LIKE` with `%value%` wrapping — +on **both** dialects. (For case-insensitive search on PostgreSQL, use the native +`ILIKE` operator via [filters](/v2/filters-and-operators#postgresql-native-operators).) + +```js +const result = pagiHelp.paginate( + { + search: "campaign", + // ... + }, + [ + { + tableName: "campaigns", + columnList: [ + { name: "campaign_id", alias: "id" }, + { name: "campaign_name", alias: "name" }, + ], + searchColumnList: [{ name: "campaign_name" }, { name: "status" }], + }, + ] +); +// ... WHERE ( campaign_name LIKE ? OR status LIKE ? ) +// replacements include "%campaign%" once per search column +``` + +## How it works + +- Each column in `searchColumnList` produces `column LIKE ?`, joined with `OR`. +- The value `%${search}%` is pushed into `replacements` once per search column. +- The whole search group is AND-ed with your filters and additional conditions. + +## Rules + +- `searchColumnList` descriptors use the same shape as `columnList` **without** + `alias`. On v2, an `alias` in `searchColumnList` is **rejected**. +- A missing or empty `search` produces **no** search predicate. +- A missing `searchColumnList` is treated as `[]` (no search). + +::: tip +Provide `searchColumnList: []` for tables that should not participate in search — +useful in [multi-table unions](/examples/multi-table-union) where only some +tables are searchable. +::: + +## Dialect note + +`LIKE` is shared across dialects. For case-insensitive search on PostgreSQL you +can express conditions with the native `ILIKE` operator via +[filters](/v2/filters-and-operators#postgresql-native-operators). diff --git a/site/docs/v2/sorting.md b/site/docs/v2/sorting.md new file mode 100644 index 0000000..982d060 --- /dev/null +++ b/site/docs/v2/sorting.md @@ -0,0 +1,50 @@ +# Sorting + +Sorting is described by two parallel arrays inside `paginationObject.sort`: + +```js +sort: { + attributes: ["created_at", "name"], + sorts: ["desc", "asc"], +} +// your sort → ORDER BY `created_at`DESC,`name`ASC +// (plus an automatic `id`DESC tie-breaker — see below) +``` + +## Rules + +- `attributes` and `sorts` must be arrays of the **same length**. +- Each entry in `sorts` must be `"asc"` or `"desc"` (case-insensitive; normalized + to uppercase). Any other value is rejected. +- Sort identifiers are escaped and passed through your `columnNameConverter`. + +## Automatic `id` tie-breaker + +PagiHelp appends `id` (descending) as a final tie-breaker so ordering is +deterministic and stable across pages: + +```js +sort: { attributes: ["created_at"], sorts: ["desc"] } +// effective: ORDER BY `created_at`DESC,`id`DESC +``` + +For this to work, your `columnList` should expose a column with `alias: "id"`. + +::: tip Your input is not mutated +PagiHelp does not push `id` onto the `sort` arrays you pass in — your original +object is left untouched. +::: + +## Dialect quoting + +The `ORDER BY` identifiers are quoted per dialect: + +- **MySQL** uses backticks: `` ORDER BY `created_at`DESC `` +- **PostgreSQL** uses double quotes: `ORDER BY "created_at"DESC` + +## Cursor mode + +For [cursor pagination](/v2/cursor-pagination), `sort` is **required** and forms +the keyset ordering. An `id` tie-breaker is appended here too, but it follows the +direction of your **last** sort entry (not always `DESC`) so the keyset stays +consistent. diff --git a/site/package-lock.json b/site/package-lock.json new file mode 100644 index 0000000..5ef3d9e --- /dev/null +++ b/site/package-lock.json @@ -0,0 +1,2359 @@ +{ + "name": "pagi-help-site", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pagi-help-site", + "version": "1.0.0", + "dependencies": { + "pagi-help": "^2.5.4" + }, + "devDependencies": { + "vitepress": "^1.6.3" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.21.1.tgz", + "integrity": "sha512-Wia5/mNTfiU0PIUN25UMfAGGdASkkwuCS9nBAdmhqrNPY/ff7U/6MgBVdwFDPsa3sA1msutPtO50gvOzx6MOXA==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "dev": true, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.55.1.tgz", + "integrity": "sha512-miW8RzAtBgNiEJ9fGEhsOPgWUpekAe64YcVufqXrlykj0Jjmo5nj0a5f/HAzRVX5ZuU1GAVd7BkzFDx7q50P3A==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.55.1.tgz", + "integrity": "sha512-eR3J3kB9JX6DdCvDRi3I4KPfwO6fR9HWYRXhVke2TXIoOQafMKCRAneg33JRmIrb+DnnJ/eWApJLF1O1CLPERg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.55.1.tgz", + "integrity": "sha512-P5ak7EurwYqgAiDyb95mgA3WRR/Zu8CPMv36lWTISvL2AmlPyqQPy2nX/KEJRTcwaeTWwrk6wJV4/M93GfjOWw==", + "dev": true, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.55.1.tgz", + "integrity": "sha512-OVtj9uA//+pjvKQI5INnzbyLrf3ClNv3XRbWswwJ2kHIStQNHtBfHo+LofNB/WhM9xjuXlW5ANn2aMj65UGx7w==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.55.1.tgz", + "integrity": "sha512-oKlVFlp+qbIEe4p7E54zSiP2gEV/vDu972Ykv8VDMFwEvreS7m0YKA3a8hGGHwc7yiBUGGiR3LlwzMLfnJmy6Q==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.55.1.tgz", + "integrity": "sha512-BOVrld6vdtsFmotVDMTVQfYXwrVplJ+DUvy60JFi+tkWV698q2J9NNPKEO3dr5qxtSLKQP4vHF8n+3U5PDWhOQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.55.1.tgz", + "integrity": "sha512-GAqHl9zERhC3bbBfubwUu07G3UXO06gORvOcsiTBZB3et0s3auNUbHlYdYNp4VKa3sUZqH5AcD3OKzU/KDGXjQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.55.1.tgz", + "integrity": "sha512-BXZw+C+gsWL7pZvbnhJUnCXASiDLGcQxVV7h55Pyh2DmSzwdZIVccE5xc9RVD2trtrhIqk5smuODTxtaZqd0IA==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.55.1.tgz", + "integrity": "sha512-9g/ceZrZTqA62FA3588Xj0onRPjDNfu0pVQqefK0rrHp9H6Wblph/YmzGjZ2g8uqbTh0ZGIvAGCzErU8f7MHpA==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.55.1.tgz", + "integrity": "sha512-cZTIrGyAP+W4A6jDVwvWM/JOaoJKQkD/2a5eLUEeNdKAD45jN7BCpsMDONyhZlosLa4UwL8uiINQzj4iFy9nqg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.55.1.tgz", + "integrity": "sha512-N6I3leW0UO8Y9Zv90yo2UHgYGuxZO0mjbvzNxDIJDjO0qECEF7Z9XMvSNeUWXQh/iNDA9lr8MfEy3rmZGIcclw==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.55.1.tgz", + "integrity": "sha512-ukU5zeeFs44rQkzv+TRdYard+d+3lmPGs8lPZhHtWE8rfz+LlBSF6s9kP3VQ7LeOYL8Dz0u6tZfnyTrqrumbHQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.55.1.tgz", + "integrity": "sha512-lCwXyijwPm3vbYHpBXPRomMcD6mgiptmps27gnMCf4HK+u/AOeFPBnIFh4V3l4A5SnP9VRiKBZqwGBpUH0vaTg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "dev": true + }, + "node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "dev": true, + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.88", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.88.tgz", + "integrity": "sha512-+cvi1qCuvReL29ehi6t62L4fb7GDXe+UlGHFcsJcV7I2l9wtqn9XE2IBKcDr3CI5iGUGS5ISnXv699pSGpyx1Q==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "dev": true, + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "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==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", + "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.10.tgz", + "integrity": "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==", + "dev": true, + "dependencies": { + "@vue/devtools-kit": "^7.7.10" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.10.tgz", + "integrity": "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==", + "dev": true, + "dependencies": { + "@vue/devtools-shared": "^7.7.10", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.10.tgz", + "integrity": "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==", + "dev": true, + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "dev": true, + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "dev": true, + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "dev": true + }, + "node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "dev": true, + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/algoliasearch": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.55.1.tgz", + "integrity": "sha512-FyaFnnsbVPtevQwqSj/SdxE3jAsSsY0BEH8IVLf9rXxEBdAhAmT6VKCVSMWoaPIHVN1Eufh/1w8q6k8URpIkWw==", + "dev": true, + "dependencies": { + "@algolia/abtesting": "1.21.1", + "@algolia/client-abtesting": "5.55.1", + "@algolia/client-analytics": "5.55.1", + "@algolia/client-common": "5.55.1", + "@algolia/client-insights": "5.55.1", + "@algolia/client-personalization": "5.55.1", + "@algolia/client-query-suggestions": "5.55.1", + "@algolia/client-search": "5.55.1", + "@algolia/ingestion": "1.55.1", + "@algolia/monitoring": "1.55.1", + "@algolia/recommend": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "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 + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "dev": true, + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "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, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "dev": true, + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/pagi-help": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/pagi-help/-/pagi-help-2.5.4.tgz", + "integrity": "sha512-pYA5HWZEvm3IEb4XRpJ/kTcIZclr2Mdb9yi7PD5Tq8lNH4XWij6F56LphwO/AkFS1WC26rzcs5Vj+JJv+ffJZQ==", + "dependencies": { + "sqlstring": "^2.3.3" + } + }, + "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 + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.3.tgz", + "integrity": "sha512-D9NL1GAnJZhc3RndVs4gDdxEeU9TcHgywMrhhOsnpdlvFjdbx0gAsLUnH6JEhlJH5giL7Tx5biWPUSEXE/HPzw==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "peer": true + }, + "node_modules/shiki": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", + "dev": true, + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "dev": true + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", + "dev": true, + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/site/package.json b/site/package.json new file mode 100644 index 0000000..2048ab7 --- /dev/null +++ b/site/package.json @@ -0,0 +1,19 @@ +{ + "name": "pagi-help-site", + "version": "1.0.0", + "private": true, + "description": "Documentation website for the pagi-help library", + "type": "module", + "scripts": { + "build:lib": "node scripts/build-lib.mjs", + "docs:dev": "node scripts/build-lib.mjs && vitepress dev docs", + "docs:build": "node scripts/build-lib.mjs && vitepress build docs", + "docs:preview": "vitepress preview docs" + }, + "devDependencies": { + "vitepress": "^1.6.3" + }, + "dependencies": { + "pagi-help": "^2.5.4" + } +} diff --git a/site/scripts/build-lib.mjs b/site/scripts/build-lib.mjs new file mode 100644 index 0000000..3e5390c --- /dev/null +++ b/site/scripts/build-lib.mjs @@ -0,0 +1,33 @@ +// Pre-bundle the published pagi-help package into a browser global for the demo/playground. +// Output is a classic IIFE (sloppy mode), NOT an ES module: the library writes to a +// getter-only inherited static — a no-op in sloppy CJS but a throw under ESM strict mode. +import { build } from "esbuild"; +import { fileURLToPath } from "node:url"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); + +const entry = require.resolve("pagi-help/v2"); +const outfile = fileURLToPath( + new URL("../docs/public/pagihelp.global.js", import.meta.url) +); +const cryptoShim = fileURLToPath( + new URL("../docs/.vitepress/shims/crypto.js", import.meta.url) +); + +await build({ + entryPoints: [entry], + outfile, + bundle: true, + format: "iife", + globalName: "__PagiHelp", + platform: "browser", + target: "es2019", + legalComments: "none", + alias: { + // Node's crypto -> tiny browser shim (only used by cursor fingerprints). + crypto: cryptoShim, + }, +}); + +console.log(`[build-lib] wrote ${outfile}`); diff --git a/site/scripts/gen-demo-sql.mjs b/site/scripts/gen-demo-sql.mjs new file mode 100644 index 0000000..6295b29 --- /dev/null +++ b/site/scripts/gen-demo-sql.mjs @@ -0,0 +1,19 @@ +// Dev helper: prints the exact SQL the installed pagi-help produces for each demo +// example, so the strings baked into utils/demoData.js stay accurate. +// Run from the site/ dir: node scripts/gen-demo-sql.mjs +import { createRequire } from "node:module"; +import { EXAMPLES, optionsFor } from "../docs/.vitepress/theme/utils/demoData.js"; + +const require = createRequire(import.meta.url); +const PagiHelpV2 = require("pagi-help/v2"); + +for (const ex of EXAMPLES) { + const options = optionsFor(ex.table); + const out = {}; + for (const dialect of ["mysql", "postgres"]) { + out[dialect] = new PagiHelpV2({ dialect }).paginate(ex.request, options).query; + } + console.log(`\n# ${ex.id}`); + console.log("mysql :", out.mysql); + console.log("postgres:", out.postgres); +}