diff --git a/.gitignore b/.gitignore index 8d97424..2c3d84f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ coverage .yalc yalc.lock .epilot-docs +node-compile-cache +demo/screenshots/capture.mjs diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..8973371 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,13 @@ + + + + + + epilot Pricing Playground + + + +
+ + + diff --git a/demo/package.json b/demo/package.json new file mode 100644 index 0000000..00291f4 --- /dev/null +++ b/demo/package.json @@ -0,0 +1,25 @@ +{ + "name": "@epilot/pricing-demo", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3", + "vite": "^6.0.5" + } +} diff --git a/demo/pnpm-lock.yaml b/demo/pnpm-lock.yaml new file mode 100644 index 0000000..de1d89c --- /dev/null +++ b/demo/pnpm-lock.yaml @@ -0,0 +1,1670 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.12 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.7(@types/react@18.3.28) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@6.4.1(jiti@1.21.7)) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.27(postcss@8.5.8) + postcss: + specifier: ^8.4.49 + version: 8.5.8 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.19 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vite: + specifier: ^6.0.5 + version: 6.4.1(jiti@1.21.7) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.59.1': + resolution: {integrity: sha512-xB0b51TB7IfDEzAojXahmr+gfA00uYVInJGgNNkeQG6RPnCPGr7udsylFLTubuIUSRE6FkcI1NElyRt83PP5oQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.1': + resolution: {integrity: sha512-XOjPId0qwSDKHaIsdzHJtKCxX0+nH8MhBwvrNsT7tVyKmdTx1jJ4XzN5RZXCdTzMpufLb+B8llTC0D8uCrLhcw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.1': + resolution: {integrity: sha512-vQuRd28p0gQpPrS6kppd8IrWmFo42U8Pz1XLRjSZXq5zCqyMDYFABT7/sywL11mO1EL10Qhh7MVPEwkG8GiBeg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.1': + resolution: {integrity: sha512-x6VG6U29+Ivlnajrg1IHdzXeAwSoEHBFVO+CtC9Brugx6de712CUJobRUxsIA0KYrQvCmzNrMPFTT1A4CCqNTg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.1': + resolution: {integrity: sha512-Sgi0Uo6t1YCHJMNO3Y8+bm+SvOanUGkoZKn/VJPwYUe2kp31X5KnXmzKd/NjW8iA3gFcfNZ64zh14uOGrIllCQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.1': + resolution: {integrity: sha512-AM4xnwEZwukdhk7laMWfzWu9JGSVnJd+Fowt6Fd7QW1nrf3h0Hp7Qx5881M4aqrUlKBCybOxz0jofvIIfl7C5g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.1': + resolution: {integrity: sha512-KUizqxpwaR2AZdAUsMWfL/C94pUu7TKpoPd88c8yFVixJ+l9hejkrwoK5Zj3wiNh65UeyryKnJyxL1b7yNqFQA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.1': + resolution: {integrity: sha512-MZoQ/am77ckJtZGFAtPucgUuJWiop3m2R3lw7tC0QCcbfl4DRhQUBUkHWCkcrT3pqy5Mzv5QQgY6Dmlba6iTWg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.1': + resolution: {integrity: sha512-Sez95TP6xGjkWB1608EfhCX1gdGrO5wzyN99VqzRtC17x/1bhw5VU1V0GfKUwbW/Xr1J8mSasoFoJa6Y7aGGSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.1': + resolution: {integrity: sha512-9Cs2Seq98LWNOJzR89EGTZoiP8EkZ9UbQhBlDgfAkM6asVna1xJ04W2CLYWDN/RpUgOjtQvcv8wQVi1t5oQazA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.1': + resolution: {integrity: sha512-n9yqttftgFy7IrNEnHy1bOp6B4OSe8mJDiPkT7EqlM9FnKOwUMnCK62ixW0Kd9Clw0/wgvh8+SqaDXMFvw3KqQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.1': + resolution: {integrity: sha512-SfpNXDzVTqs/riak4xXcLpq5gIQWsqGWMhN1AGRQKB4qGSs4r0sEs3ervXPcE1O9RsQ5bm8Muz6zmQpQnPss1g==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.1': + resolution: {integrity: sha512-LjaChED0wQnjKZU+tsmGbN+9nN1XhaWUkAlSbTdhpEseCS4a15f/Q8xC2BN4GDKRzhhLZpYtJBZr2NZhR0jvNw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.1': + resolution: {integrity: sha512-ojW7iTJSIs4pwB2xV6QXGwNyDctvXOivYllttuPbXguuKDX5vwpqYJsHc6D2LZzjDGHML414Tuj3LvVPe1CT1A==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.1': + resolution: {integrity: sha512-FP+Q6WTcxxvsr0wQczhSE+tOZvFPV8A/mUE6mhZYFW9/eea/y/XqAgRoLLMuE9Cz0hfX5bi7p116IWoB+P237A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.1': + resolution: {integrity: sha512-L1uD9b/Ig8Z+rn1KttCJjwhN1FgjRMBKsPaBsDKkfUl7GfFq71pU4vWCnpOsGljycFEbkHWARZLf4lMYg3WOLw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.1': + resolution: {integrity: sha512-EZc9NGTk/oSUzzOD4nYY4gIjteo2M3CiozX6t1IXGCOdgxJTlVu/7EdPeiqeHPSIrxkLhavqpBAUCfvC6vBOug==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.1': + resolution: {integrity: sha512-NQ9KyU1Anuy59L8+HHOKM++CoUxrQWrZWXRik4BJFm+7i5NP6q/SW43xIBr80zzt+PDBJ7LeNmloQGfa0JGk0w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.1': + resolution: {integrity: sha512-GZkLk2t6naywsveSFBsEb0PLU+JC9ggVjbndsbG20VPhar6D1gkMfCx4NfP9owpovBXTN+eRdqGSkDGIxPHhmQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.1': + resolution: {integrity: sha512-1hjG9Jpl2KDOetr64iQd8AZAEjkDUUK5RbDkYWsViYLC1op1oNzdjMJeFiofcGhqbNTaY2kfgqowE7DILifsrA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.1': + resolution: {integrity: sha512-ARoKfflk0SiiYm3r1fmF73K/yB+PThmOwfWCk1sr7x/k9dc3uGLWuEE9if+Pw21el8MSpp3TMnG5vLNsJ/MMGQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.1': + resolution: {integrity: sha512-oOST61G6VM45Mz2vdzWMr1s2slI7y9LqxEV5fCoWi2MDONmMvgsJVHSXxce/I2xOSZPTZ47nDPOl1tkwKWSHcw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.1': + resolution: {integrity: sha512-x5WgLi5dWpRz7WclKBGEF15LcWTh0ewrHM6Cq4A+WUbkysUMZNeqt05bwPonOQ3ihPS/WMhAZV5zB1DfnI4Sxg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.1': + resolution: {integrity: sha512-wS+zHAJRVP5zOL0e+a3V3E/NTEwM2HEvvNKoDy5Xcfs0o8lljxn+EAFPkUsxihBdmDq1JWzXmmB9cbssCPdxxw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.1': + resolution: {integrity: sha512-rhHyrMeLpErT/C7BxcEsU4COHQUzHyrPYW5tOZUeUhziNtRuYxmDWvqQqzpuUt8xpOgmbKa1btGXfnA/ANVO+g==} + cpu: [x64] + os: [win32] + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.28': + resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + autoprefixer@10.4.27: + resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + baseline-browser-mapping@2.10.10: + resolution: {integrity: sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001780: + resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + electron-to-chromium@1.5.321: + resolution: {integrity: sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.59.1: + resolution: {integrity: sha512-iZKH8BeoCwTCBTZBZWQQMreekd4mdomwdjIQ40GC1oZm6o+8PnNMIxFOiCsGMWeS8iDJ7KZcl7KwmKk/0HOQpA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.59.1': + optional: true + + '@rollup/rollup-android-arm64@4.59.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.1': + optional: true + + '@rollup/rollup-darwin-x64@4.59.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.1': + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/estree@1.0.8': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react@18.3.28': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@vitejs/plugin-react@4.7.0(vite@6.4.1(jiti@1.21.7))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.1(jiti@1.21.7) + transitivePeerDependencies: + - supports-color + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + autoprefixer@10.4.27(postcss@8.5.8): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001780 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.8 + postcss-value-parser: 4.2.0 + + baseline-browser-mapping@2.10.10: {} + + binary-extensions@2.3.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.10 + caniuse-lite: 1.0.30001780 + electron-to-chromium: 1.5.321 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001780: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + commander@4.1.1: {} + + convert-source-map@2.0.0: {} + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + electron-to-chromium@1.5.321: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + fraction.js@5.3.4: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + node-releases@2.0.36: {} + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + postcss-import@15.1.0(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.8): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.8 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.8 + + postcss-nested@6.2.0(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-refresh@0.17.0: {} + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.59.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.1 + '@rollup/rollup-android-arm64': 4.59.1 + '@rollup/rollup-darwin-arm64': 4.59.1 + '@rollup/rollup-darwin-x64': 4.59.1 + '@rollup/rollup-freebsd-arm64': 4.59.1 + '@rollup/rollup-freebsd-x64': 4.59.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.1 + '@rollup/rollup-linux-arm-musleabihf': 4.59.1 + '@rollup/rollup-linux-arm64-gnu': 4.59.1 + '@rollup/rollup-linux-arm64-musl': 4.59.1 + '@rollup/rollup-linux-loong64-gnu': 4.59.1 + '@rollup/rollup-linux-loong64-musl': 4.59.1 + '@rollup/rollup-linux-ppc64-gnu': 4.59.1 + '@rollup/rollup-linux-ppc64-musl': 4.59.1 + '@rollup/rollup-linux-riscv64-gnu': 4.59.1 + '@rollup/rollup-linux-riscv64-musl': 4.59.1 + '@rollup/rollup-linux-s390x-gnu': 4.59.1 + '@rollup/rollup-linux-x64-gnu': 4.59.1 + '@rollup/rollup-linux-x64-musl': 4.59.1 + '@rollup/rollup-openbsd-x64': 4.59.1 + '@rollup/rollup-openharmony-arm64': 4.59.1 + '@rollup/rollup-win32-arm64-msvc': 4.59.1 + '@rollup/rollup-win32-ia32-msvc': 4.59.1 + '@rollup/rollup-win32-x64-gnu': 4.59.1 + '@rollup/rollup-win32-x64-msvc': 4.59.1 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + source-map-js@1.2.1: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.8 + postcss-import: 15.1.0(postcss@8.5.8) + postcss-js: 4.1.0(postcss@8.5.8) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8) + postcss-nested: 6.2.0(postcss@8.5.8) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-interface-checker@0.1.13: {} + + typescript@5.9.3: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + vite@6.4.1(jiti@1.21.7): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.1 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + jiti: 1.21.7 + + yallist@3.1.1: {} diff --git a/demo/postcss.config.js b/demo/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/demo/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/demo/src/App.tsx b/demo/src/App.tsx new file mode 100644 index 0000000..094da1f --- /dev/null +++ b/demo/src/App.tsx @@ -0,0 +1,168 @@ +import { useState } from 'react'; +import { OverviewDemo } from './sections/OverviewDemo'; +import { PerUnitDemo } from './sections/PerUnitDemo'; +import { TieredVolumeDemo } from './sections/TieredVolumeDemo'; +import { TieredGraduatedDemo } from './sections/TieredGraduatedDemo'; +import { TieredFlatFeeDemo } from './sections/TieredFlatFeeDemo'; +import { TaxDemo } from './sections/TaxDemo'; +import { DiscountDemo } from './sections/DiscountDemo'; +import { CompositePriceDemo } from './sections/CompositePriceDemo'; +import { RecurringBillingDemo } from './sections/RecurringBillingDemo'; +import { CurrencyDemo } from './sections/CurrencyDemo'; +import { DynamicTariffDemo } from './sections/DynamicTariffDemo'; +import { GetAGDemo } from './sections/GetAGDemo'; +import { ElectricityDemo } from './sections/ElectricityDemo'; +import { GasDemo } from './sections/GasDemo'; +import { HouseConnectionDemo } from './sections/HouseConnectionDemo'; +import { NonCommodityDemo } from './sections/NonCommodityDemo'; + +type SectionItem = { + id: string; + label: string; + icon: string; + component: React.ComponentType; +}; + +type SectionGroup = { + group: string; + items: SectionItem[]; +}; + +type Section = SectionItem | SectionGroup; + +function isGroup(s: Section): s is SectionGroup { + return 'group' in s; +} + +const sections: Section[] = [ + { id: 'overview', label: 'Overview', icon: '\uD83C\uDFE0', component: OverviewDemo }, + { + group: 'Energy & Utility Use Cases', + items: [ + { id: 'electricity', label: 'Electricity', icon: '\u26A1', component: ElectricityDemo }, + { id: 'gas', label: 'Gas', icon: '\uD83D\uDD25', component: GasDemo }, + { id: 'house-connection', label: 'House Connection', icon: '\uD83C\uDFE1', component: HouseConnectionDemo }, + { id: 'non-commodity', label: 'Non-Commodity', icon: '\uD83D\uDCCB', component: NonCommodityDemo }, + ], + }, + { + group: 'Capabilities', + items: [ + { id: 'per-unit', label: 'Per Unit', icon: '\uD83D\uDCE6', component: PerUnitDemo }, + { id: 'tiered-volume', label: 'Tiered Volume', icon: '\uD83D\uDCCA', component: TieredVolumeDemo }, + { id: 'tiered-graduated', label: 'Tiered Graduated', icon: '\uD83D\uDCC8', component: TieredGraduatedDemo }, + { id: 'tiered-flatfee', label: 'Tiered Flat Fee', icon: '\uD83C\uDFF7\uFE0F', component: TieredFlatFeeDemo }, + { id: 'tax', label: 'Tax Handling', icon: '\uD83E\uDDFE', component: TaxDemo }, + { id: 'discounts', label: 'Discounts & Coupons', icon: '\uD83C\uDF9F\uFE0F', component: DiscountDemo }, + { id: 'composite', label: 'Composite Pricing', icon: '\uD83E\uDDE9', component: CompositePriceDemo }, + { id: 'recurring', label: 'Recurring Billing', icon: '\uD83D\uDD04', component: RecurringBillingDemo }, + { id: 'currency', label: 'Currency & Formatting', icon: '\uD83D\uDCB1', component: CurrencyDemo }, + { id: 'dynamic-tariff', label: 'Dynamic Tariff', icon: '\u26A1', component: DynamicTariffDemo }, + { id: 'getag', label: 'GetAG Energy', icon: '\uD83D\uDD0C', component: GetAGDemo }, + ], + }, +]; + +function getAllSections(): SectionItem[] { + const result: SectionItem[] = []; + for (const s of sections) { + if (isGroup(s)) { + result.push(...s.items); + } else { + result.push(s); + } + } + return result; +} + +const allSections = getAllSections(); + +export default function App() { + const [activeSection, setActiveSection] = useState('overview'); + const [sidebarOpen, setSidebarOpen] = useState(true); + + const ActiveComponent = allSections.find((s) => s.id === activeSection)?.component ?? OverviewDemo; + const activeItem = allSections.find((s) => s.id === activeSection); + + return ( +
+ {/* Sidebar */} + + + {/* Main content */} +
+
+ +

+ {activeItem?.icon} {activeItem?.label} +

+
+
+ +
+
+
+ ); +} diff --git a/demo/src/components/CodeBlock.tsx b/demo/src/components/CodeBlock.tsx new file mode 100644 index 0000000..d50dc38 --- /dev/null +++ b/demo/src/components/CodeBlock.tsx @@ -0,0 +1,165 @@ +import { useMemo } from 'react'; + +interface CodeBlockProps { + code: string; + title?: string; + language?: 'typescript' | 'javascript' | 'json' | 'bash'; +} + +interface Token { + type: 'keyword' | 'string' | 'number' | 'comment' | 'property' | 'punctuation' | 'operator' | 'builtin' | 'plain'; + value: string; +} + +const JS_KEYWORDS = new Set([ + 'import', 'from', 'export', 'default', 'const', 'let', 'var', 'function', + 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', + 'continue', 'new', 'this', 'class', 'extends', 'async', 'await', 'try', + 'catch', 'throw', 'typeof', 'instanceof', 'in', 'of', 'true', 'false', + 'null', 'undefined', 'void', 'type', 'interface', 'enum', 'as', +]); + +const BUILTIN = new Set([ + 'console', 'Math', 'JSON', 'Array', 'Object', 'String', 'Number', + 'Boolean', 'Promise', 'Map', 'Set', 'Date', 'Error', 'RegExp', +]); + +function tokenize(code: string): Token[] { + const tokens: Token[] = []; + let i = 0; + + while (i < code.length) { + // Single-line comment + if (code[i] === '/' && code[i + 1] === '/') { + const end = code.indexOf('\n', i); + const commentEnd = end === -1 ? code.length : end; + tokens.push({ type: 'comment', value: code.slice(i, commentEnd) }); + i = commentEnd; + continue; + } + + // Multi-line comment + if (code[i] === '/' && code[i + 1] === '*') { + const end = code.indexOf('*/', i + 2); + const commentEnd = end === -1 ? code.length : end + 2; + tokens.push({ type: 'comment', value: code.slice(i, commentEnd) }); + i = commentEnd; + continue; + } + + // Strings (single, double, backtick) + if (code[i] === '"' || code[i] === "'" || code[i] === '`') { + const quote = code[i]; + let j = i + 1; + while (j < code.length && code[j] !== quote) { + if (code[j] === '\\') j++; // skip escaped chars + j++; + } + tokens.push({ type: 'string', value: code.slice(i, j + 1) }); + i = j + 1; + continue; + } + + // Numbers + if (/\d/.test(code[i]) && (i === 0 || !/\w/.test(code[i - 1]))) { + let j = i; + while (j < code.length && /[\d._eE]/.test(code[j])) j++; + tokens.push({ type: 'number', value: code.slice(i, j) }); + i = j; + continue; + } + + // Words (keywords, builtins, identifiers) + if (/[a-zA-Z_$]/.test(code[i])) { + let j = i; + while (j < code.length && /[\w$]/.test(code[j])) j++; + const word = code.slice(i, j); + + if (JS_KEYWORDS.has(word)) { + tokens.push({ type: 'keyword', value: word }); + } else if (BUILTIN.has(word)) { + tokens.push({ type: 'builtin', value: word }); + } else if (code[j] === ':' || (i > 0 && code.lastIndexOf('\n', i - 1) < code.lastIndexOf('{', i))) { + // Check if it looks like an object property (word followed by colon) + if (code[j] === ':') { + tokens.push({ type: 'property', value: word }); + } else { + tokens.push({ type: 'plain', value: word }); + } + } else { + tokens.push({ type: 'plain', value: word }); + } + i = j; + continue; + } + + // Operators + if ('=<>!+-*/%&|^~?'.includes(code[i])) { + let j = i; + while (j < code.length && '=<>!+-*/%&|^~?'.includes(code[j])) j++; + tokens.push({ type: 'operator', value: code.slice(i, j) }); + i = j; + continue; + } + + // Punctuation + if ('{}[]();:.,'.includes(code[i])) { + tokens.push({ type: 'punctuation', value: code[i] }); + i++; + continue; + } + + // Whitespace and other + let j = i; + while (j < code.length && !/[a-zA-Z_$\d"'`/=<>!+\-*%&|^~?{}[\]();:.,]/.test(code[j])) j++; + tokens.push({ type: 'plain', value: code.slice(i, j || i + 1) }); + i = j || i + 1; + } + + return tokens; +} + +const tokenColors: Record = { + keyword: 'text-purple-400', + string: 'text-green-400', + number: 'text-orange-300', + comment: 'text-gray-500 italic', + property: 'text-sky-300', + punctuation: 'text-gray-400', + operator: 'text-pink-400', + builtin: 'text-yellow-300', + plain: 'text-gray-100', +}; + +export function CodeBlock({ code, title, language = 'typescript' }: CodeBlockProps) { + const highlighted = useMemo(() => { + if (language === 'bash') { + // Simple bash highlighting: just color comments and strings + return code.split('\n').map((line, i) => { + if (line.trimStart().startsWith('#')) { + return
{line}
; + } + return
{line}
; + }); + } + + const tokens = tokenize(code); + return tokens.map((token, i) => ( + {token.value} + )); + }, [code, language]); + + return ( +
+ {title && ( +
+ {title} + {language && {language}} +
+ )} +
+        {highlighted}
+      
+
+ ); +} diff --git a/demo/src/components/ResultCard.tsx b/demo/src/components/ResultCard.tsx new file mode 100644 index 0000000..bd4781a --- /dev/null +++ b/demo/src/components/ResultCard.tsx @@ -0,0 +1,27 @@ +interface ResultCardProps { + label: string; + value: string | number; + sublabel?: string; + highlight?: boolean; + color?: 'default' | 'green' | 'red' | 'blue' | 'amber'; +} + +const colorMap = { + default: 'text-gray-900', + green: 'text-green-600', + red: 'text-red-600', + blue: 'text-primary-600', + amber: 'text-amber-600', +}; + +export function ResultCard({ label, value, sublabel, highlight, color = 'default' }: ResultCardProps) { + return ( +
+

{label}

+

{value}

+ {sublabel &&

{sublabel}

} +
+ ); +} diff --git a/demo/src/components/TierChart.tsx b/demo/src/components/TierChart.tsx new file mode 100644 index 0000000..f8aa109 --- /dev/null +++ b/demo/src/components/TierChart.tsx @@ -0,0 +1,44 @@ +interface TierBar { + label: string; + value: number; + active?: boolean; + sublabel?: string; +} + +interface TierChartProps { + bars: TierBar[]; + title?: string; + valueFormatter?: (v: number) => string; +} + +export function TierChart({ bars, title, valueFormatter = (v) => v.toFixed(2) }: TierChartProps) { + const maxValue = Math.max(...bars.map((b) => b.value), 1); + + return ( +
+ {title &&

{title}

} +
+ {bars.map((bar, i) => { + const height = Math.max((bar.value / maxValue) * 100, 4); + return ( +
+ {valueFormatter(bar.value)} +
+ {bar.label} + {bar.sublabel && ( + {bar.sublabel} + )} +
+ ); + })} +
+
+ ); +} diff --git a/demo/src/helpers.ts b/demo/src/helpers.ts new file mode 100644 index 0000000..fd181db --- /dev/null +++ b/demo/src/helpers.ts @@ -0,0 +1,148 @@ +/** Format cents integer to currency string */ +export function fmtCents(amount: number | undefined, currency = 'EUR'): string { + if (amount === undefined || amount === null) return '-'; + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: currency.toUpperCase(), + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount / 100); +} + +/** Format a decimal string to currency */ +export function fmtDecimal(amount: string | undefined, currency = 'EUR'): string { + if (!amount) return '-'; + const num = parseFloat(amount); + if (isNaN(num)) return '-'; + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: currency.toUpperCase(), + minimumFractionDigits: 2, + maximumFractionDigits: 6, + }).format(num); +} + +/** Create a minimal Tax object */ +export function makeTax(rate: number, id?: string) { + return { + _id: id || `tax-${rate}`, + _title: `Tax ${rate}%`, + _org: 'demo', + _schema: 'tax' as const, + _tags: [], + _created_at: new Date().toISOString(), + _updated_at: new Date().toISOString(), + type: 'VAT' as const, + rate, + active: true, + region: 'DE', + region_label: 'Germany', + }; +} + +/** Build a PriceItemDto for computation */ +export function buildPriceItemDto({ + unitAmountDecimal, + quantity, + currency = 'EUR', + pricingModel = 'per_unit', + type = 'one_time', + billingPeriod, + isTaxInclusive = true, + taxRate = 19, + tiers, + coupons, + description = 'Demo Price', + dynamicTariff, + getAg, +}: { + unitAmountDecimal: string; + quantity: number; + currency?: string; + pricingModel?: string; + type?: 'one_time' | 'recurring'; + billingPeriod?: string; + isTaxInclusive?: boolean; + taxRate?: number; + tiers?: any[]; + coupons?: any[]; + description?: string; + dynamicTariff?: any; + getAg?: any; +}) { + const tax = makeTax(taxRate); + const unitAmount = Math.round(parseFloat(unitAmountDecimal) * 100); + + const price: any = { + _id: 'demo-price-' + Math.random().toString(36).slice(2, 8), + unit_amount: unitAmount, + unit_amount_currency: currency, + unit_amount_decimal: unitAmountDecimal, + type, + billing_period: billingPeriod, + tax: [tax], + is_tax_inclusive: isTaxInclusive, + pricing_model: pricingModel, + description, + _title: description, + ...(tiers && { tiers }), + ...(dynamicTariff && { dynamic_tariff: dynamicTariff }), + ...(getAg && { get_ag: getAg }), + }; + + const item: any = { + quantity, + product_id: 'demo-product', + price_id: price._id, + taxes: [{ tax }], + _price: price, + _product: { name: 'Demo Product', type: 'product' }, + pricing_model: pricingModel, + is_tax_inclusive: isTaxInclusive, + }; + + if (coupons && coupons.length > 0) { + item._coupons = coupons; + } + + return item; +} + +export function makeCoupon({ + type, + category, + percentageValue, + fixedValue, + fixedValueDecimal, + currency = 'EUR', + name = 'Demo Coupon', + cashbackPeriod, +}: { + type: 'fixed' | 'percentage'; + category: 'discount' | 'cashback'; + percentageValue?: string; + fixedValue?: number; + fixedValueDecimal?: string; + currency?: string; + name?: string; + cashbackPeriod?: number; +}) { + return { + _id: 'coupon-' + Math.random().toString(36).slice(2, 8), + _title: name, + _org: 'demo', + _schema: 'coupon' as const, + _tags: [], + _created_at: new Date().toISOString(), + _updated_at: new Date().toISOString(), + name, + type, + category, + ...(percentageValue !== undefined && { percentage_value: percentageValue }), + ...(fixedValue !== undefined && { fixed_value: fixedValue }), + ...(fixedValueDecimal !== undefined && { fixed_value_decimal: fixedValueDecimal }), + fixed_value_currency: currency, + active: true, + ...(cashbackPeriod !== undefined && { cashback_period: cashbackPeriod }), + }; +} diff --git a/demo/src/index.css b/demo/src/index.css new file mode 100644 index 0000000..86b5ad9 --- /dev/null +++ b/demo/src/index.css @@ -0,0 +1,54 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + scroll-behavior: smooth; + } +} + +@layer components { + .card { + @apply bg-white rounded-xl shadow-sm border border-gray-200 p-6; + } + .input-field { + @apply w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-colors; + } + .select-field { + @apply w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-colors; + } + .btn-primary { + @apply px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700 transition-colors; + } + .btn-secondary { + @apply px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200 transition-colors; + } + .result-value { + @apply text-2xl font-bold text-gray-900; + } + .result-label { + @apply text-xs font-medium text-gray-500 uppercase tracking-wider; + } + .badge { + @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; + } + .badge-blue { + @apply badge bg-blue-100 text-blue-800; + } + .badge-green { + @apply badge bg-green-100 text-green-800; + } + .badge-amber { + @apply badge bg-amber-100 text-amber-800; + } + .badge-red { + @apply badge bg-red-100 text-red-800; + } + .section-title { + @apply text-2xl font-bold text-gray-900 mb-2; + } + .section-desc { + @apply text-gray-500 mb-6; + } +} diff --git a/demo/src/main.tsx b/demo/src/main.tsx new file mode 100644 index 0000000..9aa52ff --- /dev/null +++ b/demo/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/demo/src/sections/CompositePriceDemo.tsx b/demo/src/sections/CompositePriceDemo.tsx new file mode 100644 index 0000000..d23b925 --- /dev/null +++ b/demo/src/sections/CompositePriceDemo.tsx @@ -0,0 +1,326 @@ +import { useState, useMemo } from 'react'; +import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { CodeBlock } from '../components/CodeBlock'; +import { fmtCents, makeTax } from '../helpers'; + +interface Component { + name: string; + unitAmountDecimal: string; + quantity: number; + type: 'one_time' | 'recurring'; + billingPeriod?: string; +} + +const defaultComponents: Component[] = [ + { name: 'Installation Fee', unitAmountDecimal: '299.00', quantity: 1, type: 'one_time' }, + { name: 'Monthly Service', unitAmountDecimal: '49.99', quantity: 1, type: 'recurring', billingPeriod: 'monthly' }, + { name: 'Hardware Lease', unitAmountDecimal: '19.99', quantity: 2, type: 'recurring', billingPeriod: 'monthly' }, +]; + +export function CompositePriceDemo() { + const [components, setComponents] = useState(defaultComponents); + const [parentQty, setParentQty] = useState(1); + const [taxRate, setTaxRate] = useState(19); + + const result = useMemo(() => { + const tax = makeTax(taxRate); + + // Build a composite price item + const compositeItem: any = { + quantity: parentQty, + product_id: 'composite-product', + price_id: 'composite-price', + is_tax_inclusive: true, + pricing_model: 'per_unit', + taxes: [{ tax }], + _price: { + _id: 'composite-price', + is_composite_price: true, + pricing_model: 'per_unit', + is_tax_inclusive: true, + unit_amount: 0, + unit_amount_decimal: '0', + unit_amount_currency: 'EUR', + tax: [tax], + price_components: components.map((comp, i) => ({ + _id: `comp-${i}`, + unit_amount: Math.round(parseFloat(comp.unitAmountDecimal) * 100), + unit_amount_decimal: comp.unitAmountDecimal, + unit_amount_currency: 'EUR', + pricing_model: 'per_unit', + is_tax_inclusive: true, + type: comp.type, + billing_period: comp.billingPeriod, + tax: [tax], + description: comp.name, + _title: comp.name, + })), + }, + _product: { name: 'Bundle Package', type: 'product' }, + price_components: components.map((comp, i) => ({ + _id: `comp-${i}`, + quantity: comp.quantity, + unit_amount: Math.round(parseFloat(comp.unitAmountDecimal) * 100), + unit_amount_decimal: comp.unitAmountDecimal, + unit_amount_currency: 'EUR', + pricing_model: 'per_unit', + is_tax_inclusive: true, + type: comp.type, + billing_period: comp.billingPeriod, + tax: [tax], + description: comp.name, + _title: comp.name, + taxes: [{ tax }], + _price: { + _id: `comp-price-${i}`, + unit_amount: Math.round(parseFloat(comp.unitAmountDecimal) * 100), + unit_amount_decimal: comp.unitAmountDecimal, + unit_amount_currency: 'EUR', + pricing_model: 'per_unit', + is_tax_inclusive: true, + type: comp.type, + billing_period: comp.billingPeriod, + tax: [tax], + description: comp.name, + _title: comp.name, + }, + })), + }; + + return computeAggregatedAndPriceTotals([compositeItem]); + }, [components, parentQty, taxRate]); + + const updateComponent = (idx: number, field: keyof Component, value: any) => { + setComponents((prev) => prev.map((c, i) => (i === idx ? { ...c, [field]: value } : c))); + }; + + const addComponent = () => { + setComponents((prev) => [ + ...prev, + { name: `Component ${prev.length + 1}`, unitAmountDecimal: '10.00', quantity: 1, type: 'one_time' }, + ]); + }; + + const removeComponent = (idx: number) => { + if (components.length > 1) setComponents((prev) => prev.filter((_, i) => i !== idx)); + }; + + const oneTimeItems = result.items?.filter((item: any) => item._price?.type === 'one_time' || !item._price?.billing_period) ?? []; + const recurringItems = result.items?.filter((item: any) => item._price?.type === 'recurring') ?? []; + + return ( +
+

Composite Pricing

+

+ Bundle multiple price components into a single product. Each component can have its own pricing model, + recurrence, and quantity. Component quantities multiply with the parent quantity. +

+ +
+
+
+
+

Price Components

+ +
+
+ {components.map((comp, idx) => ( +
+
+ updateComponent(idx, 'name', e.target.value)} + className="text-sm font-medium bg-transparent border-none outline-none text-gray-700 flex-1" + /> + +
+
+
+ + updateComponent(idx, 'unitAmountDecimal', e.target.value)} + className="input-field text-xs" + /> +
+
+ + updateComponent(idx, 'quantity', Number(e.target.value))} + className="input-field text-xs" + /> +
+
+ + +
+ {comp.type === 'recurring' && ( +
+ + +
+ )} +
+
+ ))} +
+
+ +
+
+
+ + setParentQty(Number(e.target.value))} + className="input-field mt-1" + /> +
+
+ + +
+
+
+
+ +
+ {/* Component visual */} +
+

Bundle Breakdown

+
+ {components.map((comp, idx) => { + const price = parseFloat(comp.unitAmountDecimal) * comp.quantity * parentQty; + return ( +
+
+
+

{comp.name}

+

+ โ‚ฌ{comp.unitAmountDecimal} x {comp.quantity} + {parentQty > 1 ? ` x ${parentQty}` : ''} + {comp.type === 'recurring' && ` / ${comp.billingPeriod}`} +

+
+
+

โ‚ฌ{price.toFixed(2)}

+ + {comp.type === 'one_time' ? 'One-time' : comp.billingPeriod} + +
+
+ ); + })} +
+
+ + {/* Totals */} +
+

Computed Totals

+
+ + + + +
+ + {/* Recurrence breakdown */} + {(result.total_details?.breakdown?.recurrences?.length ?? 0) > 0 && ( +
+

By Recurrence:

+ {result.total_details?.breakdown?.recurrences?.map((r: any, i: number) => ( +
+ + {r.type === 'one_time' ? 'One-time' : r.billing_period} + + {fmtCents(r.amount_total)} +
+ ))} +
+ )} +
+
+
+ + {/* Usage */} +
+ ` { + unit_amount_decimal: '${c.unitAmountDecimal}', + pricing_model: 'per_unit', + is_tax_inclusive: true, + type: '${c.type}',${c.billingPeriod ? `\n billing_period: '${c.billingPeriod}',` : ''} + description: '${c.name}', + },`).join('\n')} + ], + }, + price_components: [ +${components.map((c) => ` { quantity: ${c.quantity}, unit_amount_decimal: '${c.unitAmountDecimal}', type: '${c.type}'${c.billingPeriod ? `, billing_period: '${c.billingPeriod}'` : ''} },`).join('\n')} + ], + taxes: [{ tax: { rate: ${taxRate} } }], +}; + +const result = computeAggregatedAndPriceTotals([compositeItem]); +// result.amount_total = ${fmtCents(result.amount_total)}`} + /> +
+
+ ); +} diff --git a/demo/src/sections/CurrencyDemo.tsx b/demo/src/sections/CurrencyDemo.tsx new file mode 100644 index 0000000..25f664f --- /dev/null +++ b/demo/src/sections/CurrencyDemo.tsx @@ -0,0 +1,214 @@ +import { useState, useMemo } from 'react'; +import { formatAmount, formatAmountFromString, getCurrencySymbol, toIntegerAmount } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { CodeBlock } from '../components/CodeBlock'; + +const currencies = [ + { code: 'EUR', name: 'Euro', locale: 'de-DE' }, + { code: 'USD', name: 'US Dollar', locale: 'en-US' }, + { code: 'GBP', name: 'British Pound', locale: 'en-GB' }, + { code: 'CHF', name: 'Swiss Franc', locale: 'de-CH' }, + { code: 'SEK', name: 'Swedish Krona', locale: 'sv-SE' }, + { code: 'PLN', name: 'Polish Zloty', locale: 'pl-PL' }, + { code: 'CZK', name: 'Czech Koruna', locale: 'cs-CZ' }, + { code: 'DKK', name: 'Danish Krone', locale: 'da-DK' }, +]; + +export function CurrencyDemo() { + const [amount, setAmount] = useState('1234.56'); + const [selectedCurrency, setSelectedCurrency] = useState('EUR'); + const [locale, setLocale] = useState('de-DE'); + + // formatAmount demo (integer cents) + const intAmount = useMemo(() => { + const parsed = parseFloat(amount); + if (isNaN(parsed)) return 0; + return Math.round(parsed * 100); + }, [amount]); + + const formattedAmount = useMemo(() => { + try { + return formatAmount({ amount: intAmount, currency: selectedCurrency as any, locale }); + } catch { + return `${selectedCurrency} ${parseFloat(amount).toFixed(2)}`; + } + }, [intAmount, selectedCurrency, locale]); + + // formatAmountFromString demo + const formattedFromString = useMemo(() => { + try { + return formatAmountFromString({ decimalAmount: amount, currency: selectedCurrency as any, locale }); + } catch { + return `${selectedCurrency} ${amount}`; + } + }, [amount, selectedCurrency, locale]); + + // Symbol + const symbol = useMemo(() => { + try { + return getCurrencySymbol(selectedCurrency as any, locale); + } catch { + return selectedCurrency; + } + }, [selectedCurrency, locale]); + + // toIntegerAmount demo + const integerAmount = useMemo(() => { + try { + return toIntegerAmount(amount); + } catch { + return Math.round(parseFloat(amount) * 100); + } + }, [amount]); + + // All currencies comparison + const allFormats = useMemo(() => { + return currencies.map((c) => { + try { + const formatted = formatAmount({ amount: intAmount, currency: c.code as any, locale: c.locale }); + const sym = getCurrencySymbol(c.code as any, c.locale); + return { ...c, formatted, symbol: sym }; + } catch { + return { ...c, formatted: `${c.code} ${parseFloat(amount).toFixed(2)}`, symbol: c.code }; + } + }); + }, [intAmount, amount]); + + return ( +
+

Currency & Formatting

+

+ Multi-currency support with locale-aware formatting. Uses Dinero.js for precise decimal arithmetic + and provides utilities for converting between string/integer representations. +

+ +
+
+
+

Configure

+
+ + setAmount(e.target.value)} + className="input-field mt-1" + /> +
+
+
+ + +
+
+ + setLocale(e.target.value)} + className="input-field mt-1" + placeholder="e.g., en-US" + /> +
+
+
+ + {/* Function Results */} +
+

Function Results

+
+
+

formatAmount({'{'} amount: {intAmount}, currency: '{selectedCurrency}', locale: '{locale}' {'}'})

+

{formattedAmount}

+
+
+

formatAmountFromString({'{'} decimalAmount: '{amount}', currency: '{selectedCurrency}', locale: '{locale}' {'}'})

+

{formattedFromString}

+
+
+

getCurrencySymbol('{selectedCurrency}', '{locale}')

+

{symbol}

+
+
+

toIntegerAmount('{amount}')

+

{integerAmount} (cents)

+
+
+
+
+ +
+ {/* All currencies */} +
+

Same Amount in All Currencies

+

{intAmount} cents formatted per locale

+
+ {allFormats.map((c) => ( +
+
+ + {c.symbol} + +
+

{c.name}

+

{c.code} ({c.locale})

+
+
+ {c.formatted} +
+ ))} +
+
+
+
+ + {/* Usage */} +
+ '${formattedAmount}' + +// Format from decimal string +formatAmountFromString({ decimalAmount: '${amount}', currency: '${selectedCurrency}', locale: '${locale}' }); +// => '${formattedFromString}' + +// Get currency symbol +getCurrencySymbol('${selectedCurrency}', '${locale}'); +// => '${symbol}' + +// Convert decimal string to integer (cents) +toIntegerAmount('${amount}'); +// => ${integerAmount}`} + /> +
+
+ ); +} diff --git a/demo/src/sections/DiscountDemo.tsx b/demo/src/sections/DiscountDemo.tsx new file mode 100644 index 0000000..39711d7 --- /dev/null +++ b/demo/src/sections/DiscountDemo.tsx @@ -0,0 +1,296 @@ +import { useState, useMemo } from 'react'; +import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { CodeBlock } from '../components/CodeBlock'; +import { buildPriceItemDto, fmtCents, makeCoupon } from '../helpers'; + +type CouponConfig = { + type: 'fixed' | 'percentage'; + category: 'discount' | 'cashback'; + value: string; +}; + +export function DiscountDemo() { + const [unitPrice, setUnitPrice] = useState('100.00'); + const [quantity, setQuantity] = useState(5); + const [taxRate, setTaxRate] = useState(19); + const [isTaxInclusive, setIsTaxInclusive] = useState(true); + const [couponConfig, setCouponConfig] = useState({ + type: 'percentage', + category: 'discount', + value: '15', + }); + + const baseResult = useMemo(() => { + const item = buildPriceItemDto({ + unitAmountDecimal: unitPrice, + quantity, + taxRate, + isTaxInclusive, + }); + return computeAggregatedAndPriceTotals([item]); + }, [unitPrice, quantity, taxRate, isTaxInclusive]); + + const discountResult = useMemo(() => { + const coupon = couponConfig.type === 'percentage' + ? makeCoupon({ + type: 'percentage', + category: couponConfig.category, + percentageValue: couponConfig.value, + name: `${couponConfig.value}% ${couponConfig.category}`, + ...(couponConfig.category === 'cashback' && { cashbackPeriod: 12 }), + }) + : makeCoupon({ + type: 'fixed', + category: couponConfig.category, + fixedValueDecimal: couponConfig.value, + fixedValue: Math.round(parseFloat(couponConfig.value) * 100), + name: `โ‚ฌ${couponConfig.value} ${couponConfig.category}`, + ...(couponConfig.category === 'cashback' && { cashbackPeriod: 12 }), + }); + + const item = buildPriceItemDto({ + unitAmountDecimal: unitPrice, + quantity, + taxRate, + isTaxInclusive, + coupons: [coupon], + }); + return computeAggregatedAndPriceTotals([item]); + }, [unitPrice, quantity, taxRate, isTaxInclusive, couponConfig]); + + const discountItem = discountResult.items?.[0]; + const savings = (baseResult.amount_total ?? 0) - (discountResult.amount_total ?? 0); + + const presets = [ + { label: '10% Off', config: { type: 'percentage' as const, category: 'discount' as const, value: '10' } }, + { label: '25% Off', config: { type: 'percentage' as const, category: 'discount' as const, value: '25' } }, + { label: 'โ‚ฌ50 Off', config: { type: 'fixed' as const, category: 'discount' as const, value: '50.00' } }, + { label: 'โ‚ฌ20 Cashback', config: { type: 'fixed' as const, category: 'cashback' as const, value: '20.00' } }, + { label: '10% Cashback', config: { type: 'percentage' as const, category: 'cashback' as const, value: '10' } }, + ]; + + return ( +
+

Discounts & Coupons

+

+ Apply fixed-value, percentage discounts, and cashback coupons. Coupons are prioritized: + cashback > discounts, percentage > fixed, highest value first. +

+ +
+
+ {/* Base Price Controls */} +
+

Base Price

+
+
+ + setUnitPrice(e.target.value)} + className="input-field mt-1" + /> +
+
+ + setQuantity(Number(e.target.value))} + className="input-field mt-1" + /> +
+
+
+ + ({taxRate}%) +
+
+ + {/* Coupon Config */} +
+

Coupon Configuration

+
+ {presets.map((p) => ( + + ))} +
+ +
+
+ + +
+
+ + +
+
+ + setCouponConfig((c) => ({ ...c, value: e.target.value }))} + className="input-field mt-1" + /> +
+
+
+
+ +
+ {/* Before/After */} +
+

Before vs After

+ +
+
+

Before

+

+ {fmtCents(baseResult.amount_total)} +

+
+ + + +
+

After

+

+ {fmtCents(discountResult.amount_total)} +

+
+
+ + {savings > 0 && ( +
+ + You save {fmtCents(savings)} + {discountItem?.discount_percentage + ? ` (${discountItem.discount_percentage}%)` + : ''} + +
+ )} +
+ + {/* Detailed Breakdown */} +
+

Detailed Breakdown

+
+ + + + + + {couponConfig.category === 'cashback' && ( + + )} +
+
+
+
+ + {/* Usage */} +
+ +
+
+ ); +} diff --git a/demo/src/sections/DynamicTariffDemo.tsx b/demo/src/sections/DynamicTariffDemo.tsx new file mode 100644 index 0000000..94d3715 --- /dev/null +++ b/demo/src/sections/DynamicTariffDemo.tsx @@ -0,0 +1,222 @@ +import { useState, useMemo } from 'react'; +import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { CodeBlock } from '../components/CodeBlock'; +import { buildPriceItemDto, fmtCents } from '../helpers'; + +export function DynamicTariffDemo() { + const [marketPrice, setMarketPrice] = useState('8.50'); + const [markup, setMarkup] = useState('2.00'); + const [quantity, setQuantity] = useState(1000); // kWh + const [taxRate, setTaxRate] = useState(19); + const [isTaxInclusive, setIsTaxInclusive] = useState(false); + + const result = useMemo(() => { + const totalPrice = (parseFloat(marketPrice) + parseFloat(markup)).toFixed(2); + const item = buildPriceItemDto({ + unitAmountDecimal: totalPrice, + quantity, + pricingModel: 'per_unit', + type: 'recurring', + billingPeriod: 'monthly', + taxRate, + isTaxInclusive, + description: 'Dynamic Energy Tariff', + dynamicTariff: { + mode: 'manual', + average_market_price_decimal: marketPrice, + markup_amount_decimal: markup, + markup_amount: Math.round(parseFloat(markup) * 100), + }, + }); + return computeAggregatedAndPriceTotals([item]); + }, [marketPrice, markup, quantity, taxRate, isTaxInclusive]); + + const totalPerUnit = parseFloat(marketPrice) + parseFloat(markup); + const lineItem = result.items?.[0]; + + // Simulate market price fluctuations + const priceHistory = useMemo(() => { + const base = parseFloat(marketPrice); + return Array.from({ length: 24 }, (_, i) => ({ + hour: `${String(i).padStart(2, '0')}:00`, + price: Math.max(0, base + (Math.sin(i / 4) * 3) + (Math.random() - 0.5) * 2), + })); + }, [marketPrice]); + + const avgHistPrice = priceHistory.reduce((s, p) => s + p.price, 0) / priceHistory.length; + const maxHistPrice = Math.max(...priceHistory.map((p) => p.price)); + + return ( +
+

Dynamic Tariff

+

+ Market-based pricing for energy products. Combines a day-ahead market price with a configurable markup. + Supports both automatic (day_ahead_market) and manual modes. +

+ +
+
+
+

Configuration

+
+
+ + setMarketPrice(e.target.value)} + className="input-field mt-1" + /> +
+
+ + setMarkup(e.target.value)} + className="input-field mt-1" + /> +
+
+ +
+ + setQuantity(Number(e.target.value))} + className="w-full mt-1 accent-primary-600" + /> +
+ 100 kWh + 10,000 kWh +
+
+ +
+ + ({taxRate}%) +
+
+ + {/* Price Composition */} +
+

Price Composition

+
+
+ Market Price + {marketPrice} ct/kWh +
+
+
+
+ Supplier Markup + {markup} ct/kWh +
+
=
+
+ Total Unit Price + {totalPerUnit.toFixed(2)} ct/kWh +
+
+
+
+ +
+ {/* Simulated Market Chart */} +
+

Simulated 24h Market Prices

+
+ {priceHistory.map((p, i) => { + const height = Math.max((p.price / maxHistPrice) * 100, 2); + return ( +
+ ); + })} +
+
+ 00:00 + Avg: {avgHistPrice.toFixed(2)} ct/kWh + 23:00 +
+
+ + {/* Results */} +
+

Computed Result

+
+ + + + + + +
+
+
+
+ + {/* Usage */} +
+ +
+
+ ); +} diff --git a/demo/src/sections/ElectricityDemo.tsx b/demo/src/sections/ElectricityDemo.tsx new file mode 100644 index 0000000..ddc0fe1 --- /dev/null +++ b/demo/src/sections/ElectricityDemo.tsx @@ -0,0 +1,404 @@ +import { useState, useMemo } from 'react'; +import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { CodeBlock } from '../components/CodeBlock'; +import { buildPriceItemDto, fmtCents } from '../helpers'; + +export function ElectricityDemo() { + const [tariffType, setTariffType] = useState<'single' | 'dual'>('single'); + const [basePrice, setBasePrice] = useState('96.00'); + const [workPriceHT, setWorkPriceHT] = useState('28.50'); + const [workPriceNT, setWorkPriceNT] = useState('22.30'); + const [markupHT, setMarkupHT] = useState('3.20'); + const [markupNT, setMarkupNT] = useState('2.10'); + const [consumptionHT, setConsumptionHT] = useState(2800); + const [consumptionNT, setConsumptionNT] = useState(1200); + const [taxRate, setTaxRate] = useState(19); + + const result = useMemo(() => { + const items: any[] = []; + + // Base price (Grundpreis) + items.push( + buildPriceItemDto({ + unitAmountDecimal: basePrice, + quantity: 1, + type: 'recurring', + billingPeriod: 'yearly', + taxRate, + isTaxInclusive: false, + description: 'Grundpreis (Base Price)', + }), + ); + + // HT work price + const totalHT = (parseFloat(workPriceHT) + parseFloat(markupHT)).toFixed(4); + items.push( + buildPriceItemDto({ + unitAmountDecimal: totalHT, + quantity: tariffType === 'dual' ? consumptionHT : consumptionHT + consumptionNT, + type: 'recurring', + billingPeriod: 'yearly', + taxRate, + isTaxInclusive: false, + description: tariffType === 'dual' ? 'Arbeitspreis HT (Peak)' : 'Arbeitspreis (Work Price)', + }), + ); + + // NT work price (dual tariff only) + if (tariffType === 'dual') { + const totalNT = (parseFloat(workPriceNT) + parseFloat(markupNT)).toFixed(4); + items.push( + buildPriceItemDto({ + unitAmountDecimal: totalNT, + quantity: consumptionNT, + type: 'recurring', + billingPeriod: 'yearly', + taxRate, + isTaxInclusive: false, + description: 'Arbeitspreis NT (Off-Peak)', + }), + ); + } + + return computeAggregatedAndPriceTotals(items); + }, [tariffType, basePrice, workPriceHT, workPriceNT, markupHT, markupNT, consumptionHT, consumptionNT, taxRate]); + + const totalConsumption = tariffType === 'dual' ? consumptionHT + consumptionNT : consumptionHT + consumptionNT; + const baseCost = parseFloat(basePrice); + const htRate = parseFloat(workPriceHT) + parseFloat(markupHT); + const ntRate = parseFloat(workPriceNT) + parseFloat(markupNT); + const htCost = htRate * (tariffType === 'dual' ? consumptionHT : totalConsumption); + const ntCost = tariffType === 'dual' ? ntRate * consumptionNT : 0; + const totalNet = baseCost + htCost + ntCost; + + return ( +
+

Electricity Tariff

+

+ Standard German electricity pricing with Grundpreis (base fee) and Arbeitspreis (work price per kWh). + Supports single-tariff and dual-tariff (HT/NT) meters for peak and off-peak consumption. +

+ +
+
+ {/* Tariff type toggle */} +
+

Meter Type

+
+ {(['single', 'dual'] as const).map((t) => ( + + ))} +
+
+ + {/* Base price */} +
+
+

Grundpreis (Base Price)

+
+ + setBasePrice(e.target.value)} + className="input-field mt-1" + /> +
+
+
+ + {/* Work prices */} +
+
+

+ {tariffType === 'dual' ? 'Arbeitspreis HT (Peak)' : 'Arbeitspreis (Work Price)'} +

+
+
+ + setWorkPriceHT(e.target.value)} + className="input-field mt-1" + /> +
+
+ + setMarkupHT(e.target.value)} + className="input-field mt-1" + /> +
+
+
+ + {tariffType === 'dual' && ( +
+

Arbeitspreis NT (Off-Peak)

+
+
+ + setWorkPriceNT(e.target.value)} + className="input-field mt-1" + /> +
+
+ + setMarkupNT(e.target.value)} + className="input-field mt-1" + /> +
+
+
+ )} +
+ + {/* Consumption */} +
+ {tariffType === 'dual' ? ( +
+
+ + setConsumptionHT(Number(e.target.value))} + className="w-full mt-1 accent-yellow-500" + /> +
+
+ + setConsumptionNT(Number(e.target.value))} + className="w-full mt-1 accent-indigo-500" + /> +
+

+ Total: {totalConsumption.toLocaleString()} kWh/year +

+
+ ) : ( +
+ + { + const val = Number(e.target.value); + setConsumptionHT(Math.round(val * 0.7)); + setConsumptionNT(Math.round(val * 0.3)); + }} + className="w-full mt-1 accent-primary-600" + /> +
+ 1,000 kWh + 10,000 kWh +
+
+ )} +
+
+ +
+ {/* Cost breakdown */} +
+

Annual Cost Breakdown

+
+ {[ + { value: baseCost, color: 'bg-blue-400', label: 'Base' }, + { value: htCost, color: 'bg-yellow-400', label: tariffType === 'dual' ? 'HT' : 'Work' }, + ...(tariffType === 'dual' ? [{ value: ntCost, color: 'bg-indigo-400', label: 'NT' }] : []), + ].map((seg) => ( +
+ {(seg.value / totalNet) * 100 > 10 ? seg.label : ''} +
+ ))} +
+ +
+
+
+
+

Grundpreis

+

EUR {parseFloat(basePrice).toFixed(2)}/year

+
+ EUR {baseCost.toFixed(2)} +
+
+
+
+

+ {tariffType === 'dual' ? 'Arbeitspreis HT' : 'Arbeitspreis'} +

+

+ {htRate.toFixed(2)} ct/kWh x {(tariffType === 'dual' ? consumptionHT : totalConsumption).toLocaleString()} kWh +

+
+ EUR {htCost.toFixed(2)} +
+ {tariffType === 'dual' && ( +
+
+
+

Arbeitspreis NT

+

+ {ntRate.toFixed(2)} ct/kWh x {consumptionNT.toLocaleString()} kWh +

+
+ EUR {ntCost.toFixed(2)} +
+ )} +
+
+ Net Total (annual) + EUR {totalNet.toFixed(2)} +
+
+
+
+ + {/* Computed results */} +
+

Computed via Library

+
+ + + + +
+
+ + {/* Rate comparison for dual tariff */} + {tariffType === 'dual' && ( +
+

HT vs NT Rate Comparison

+
+
+

HT (Peak)

+

{htRate.toFixed(2)} ct

+

{consumptionHT.toLocaleString()} kWh

+
+
+

NT (Off-Peak)

+

{ntRate.toFixed(2)} ct

+

{consumptionNT.toLocaleString()} kWh

+
+
+

+ Savings vs. all-HT: EUR {((htRate - ntRate) * consumptionNT).toFixed(2)}/year +

+
+ )} +
+
+ + {/* Usage */} +
+ +
+
+ ); +} diff --git a/demo/src/sections/GasDemo.tsx b/demo/src/sections/GasDemo.tsx new file mode 100644 index 0000000..0af9ce0 --- /dev/null +++ b/demo/src/sections/GasDemo.tsx @@ -0,0 +1,314 @@ +import { useState, useMemo } from 'react'; +import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { CodeBlock } from '../components/CodeBlock'; +import { buildPriceItemDto, fmtCents } from '../helpers'; + +export function GasDemo() { + const [basePrice, setBasePrice] = useState('144.00'); + const [workPrice, setWorkPrice] = useState('8.90'); + const [markup, setMarkup] = useState('1.80'); + const [consumption, setConsumption] = useState(15000); + const [co2Levy, setCo2Levy] = useState('0.546'); + const [gasStorageLevy, setGasStorageLevy] = useState('0.059'); + const [taxRate, setTaxRate] = useState(19); + + const result = useMemo(() => { + const items: any[] = []; + + // Base price + items.push( + buildPriceItemDto({ + unitAmountDecimal: basePrice, + quantity: 1, + type: 'recurring', + billingPeriod: 'yearly', + taxRate, + isTaxInclusive: false, + description: 'Grundpreis (Base Price)', + }), + ); + + // Work price + markup + levies per kWh + const totalPerKwh = + parseFloat(workPrice) + parseFloat(markup) + parseFloat(co2Levy) + parseFloat(gasStorageLevy); + items.push( + buildPriceItemDto({ + unitAmountDecimal: totalPerKwh.toFixed(4), + quantity: consumption, + type: 'recurring', + billingPeriod: 'yearly', + taxRate, + isTaxInclusive: false, + description: 'Arbeitspreis (Work Price)', + }), + ); + + return computeAggregatedAndPriceTotals(items); + }, [basePrice, workPrice, markup, consumption, co2Levy, gasStorageLevy, taxRate]); + + const baseCost = parseFloat(basePrice); + const workRate = parseFloat(workPrice) + parseFloat(markup); + const levyRate = parseFloat(co2Levy) + parseFloat(gasStorageLevy); + const workCost = workRate * consumption; + const levyCost = levyRate * consumption; + const totalNet = baseCost + workCost + levyCost; + + return ( +
+

Gas Tariff

+

+ German gas supply pricing with Grundpreis, Arbeitspreis, and gas-specific levies + including CO2 tax and gas storage levy. Typical household consumption: 10,000 - 25,000 kWh/year. +

+ +
+
+ {/* Base price */} +
+
+

Grundpreis (Base Price)

+
+ + setBasePrice(e.target.value)} + className="input-field mt-1" + /> +
+
+
+ + {/* Work price */} +
+
+

Arbeitspreis (Work Price)

+
+
+ + setWorkPrice(e.target.value)} + className="input-field mt-1" + /> +
+
+ + setMarkup(e.target.value)} + className="input-field mt-1" + /> +
+
+
+ + {/* Gas-specific levies */} +
+

Gas Levies

+
+
+ + setCo2Levy(e.target.value)} + className="input-field mt-1" + /> +
+
+ + setGasStorageLevy(e.target.value)} + className="input-field mt-1" + /> +
+
+
+
+ + {/* Consumption */} +
+ + setConsumption(Number(e.target.value))} + className="w-full mt-2 accent-primary-600" + /> +
+ 5,000 kWh + 50,000 kWh +
+
+
+ +
+ {/* Cost breakdown */} +
+

Annual Cost Breakdown

+
+ {[ + { value: baseCost, color: 'bg-blue-400', label: 'Base' }, + { value: workCost, color: 'bg-orange-400', label: 'Work' }, + { value: levyCost, color: 'bg-red-400', label: 'Levies' }, + ].map((seg) => ( +
+ {(seg.value / totalNet) * 100 > 10 ? seg.label : ''} +
+ ))} +
+ +
+
+
+
+

Grundpreis

+

EUR {parseFloat(basePrice).toFixed(2)}/year

+
+ EUR {baseCost.toFixed(2)} +
+
+
+
+

Arbeitspreis + Markup

+

+ {workRate.toFixed(2)} ct/kWh x {consumption.toLocaleString()} kWh +

+
+ EUR {workCost.toFixed(2)} +
+
+
+
+

Gas Levies

+

+ {levyRate.toFixed(3)} ct/kWh x {consumption.toLocaleString()} kWh (CO2 + storage) +

+
+ EUR {levyCost.toFixed(2)} +
+
+
+ Net Total (annual) + EUR {totalNet.toFixed(2)} +
+
+
+
+ + {/* Computed results */} +
+

Computed via Library

+
+ + + + +
+
+ + {/* Per-kWh breakdown */} +
+

Per-kWh Price Composition

+
+ {[ + { label: 'Work Price', value: parseFloat(workPrice), color: 'bg-orange-200' }, + { label: 'Markup', value: parseFloat(markup), color: 'bg-orange-400' }, + { label: 'CO2 Levy', value: parseFloat(co2Levy), color: 'bg-red-300' }, + { label: 'Gas Storage', value: parseFloat(gasStorageLevy), color: 'bg-red-200' }, + ].map((item) => { + const totalKwh = parseFloat(workPrice) + parseFloat(markup) + parseFloat(co2Levy) + parseFloat(gasStorageLevy); + return ( +
+ {item.label} +
+
+
+ {item.value.toFixed(3)} ct +
+ ); + })} +
+ + Total: {(parseFloat(workPrice) + parseFloat(markup) + parseFloat(co2Levy) + parseFloat(gasStorageLevy)).toFixed(3)} ct/kWh + +
+
+
+
+
+ + {/* Usage */} +
+ +
+
+ ); +} diff --git a/demo/src/sections/GetAGDemo.tsx b/demo/src/sections/GetAGDemo.tsx new file mode 100644 index 0000000..490c415 --- /dev/null +++ b/demo/src/sections/GetAGDemo.tsx @@ -0,0 +1,287 @@ +import { useState, useMemo } from 'react'; +import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { CodeBlock } from '../components/CodeBlock'; +import { buildPriceItemDto, fmtCents } from '../helpers'; + +export function GetAGDemo() { + const [basePrice, setBasePrice] = useState('120.00'); + const [workPrice, setWorkPrice] = useState('6.50'); + const [consumption, setConsumption] = useState(3500); + const [markupPerUnit, setMarkupPerUnit] = useState('1.50'); + const [additionalMarkup, setAdditionalMarkup] = useState('24.00'); + const [taxRate, setTaxRate] = useState(19); + + // Compute a simplified GetAG-like calculation + const result = useMemo(() => { + // Base price (fixed annual fee) + const baseItem = buildPriceItemDto({ + unitAmountDecimal: basePrice, + quantity: 1, + type: 'recurring', + billingPeriod: 'yearly', + taxRate, + isTaxInclusive: false, + description: 'Base Price (Grundpreis)', + }); + + // Work price (per kWh) + markup + const totalWorkPrice = (parseFloat(workPrice) + parseFloat(markupPerUnit)).toFixed(4); + const workItem = buildPriceItemDto({ + unitAmountDecimal: totalWorkPrice, + quantity: consumption, + type: 'recurring', + billingPeriod: 'yearly', + taxRate, + isTaxInclusive: false, + description: 'Work Price (Arbeitspreis)', + }); + + // Additional markup as flat fee + const additionalItem = buildPriceItemDto({ + unitAmountDecimal: additionalMarkup, + quantity: 1, + type: 'recurring', + billingPeriod: 'yearly', + taxRate, + isTaxInclusive: false, + description: 'Additional Markup', + }); + + return computeAggregatedAndPriceTotals([baseItem, workItem, additionalItem]); + }, [basePrice, workPrice, consumption, markupPerUnit, additionalMarkup, taxRate]); + + const baseCost = parseFloat(basePrice); + const workCost = (parseFloat(workPrice) + parseFloat(markupPerUnit)) * consumption; + const addCost = parseFloat(additionalMarkup); + const totalNet = baseCost + workCost + addCost; + const totalGross = totalNet * (1 + taxRate / 100); + + return ( +
+

GetAG Energy Pricing

+

+ German energy operator (GetAG) integration supporting base price + work price models + with tiered markups and additional fees. Used for electricity and gas tariffs. +

+ +
+
+ {/* GetAG Config */} +
+

Tariff Configuration

+ +
+
+

Grundpreis (Base Price)

+
+ + setBasePrice(e.target.value)} + className="input-field mt-1" + /> +
+
+ +
+

Arbeitspreis (Work Price)

+
+
+ + setWorkPrice(e.target.value)} + className="input-field mt-1" + /> +
+
+ + setMarkupPerUnit(e.target.value)} + className="input-field mt-1" + /> +
+
+
+ +
+

Additional Markup

+
+ + setAdditionalMarkup(e.target.value)} + className="input-field mt-1" + /> +
+
+
+
+ +
+ + setConsumption(Number(e.target.value))} + className="w-full mt-2 accent-primary-600" + /> +
+ 500 kWh + 10,000 kWh +
+
+
+ +
+ {/* Cost breakdown visual */} +
+

Annual Cost Breakdown

+ + {/* Stacked bar */} +
+ {[ + { value: baseCost, color: 'bg-blue-400', label: 'Base' }, + { value: workCost, color: 'bg-green-400', label: 'Work' }, + { value: addCost, color: 'bg-amber-400', label: 'Markup' }, + ].map((seg) => ( +
+ {(seg.value / totalNet) * 100 > 10 ? seg.label : ''} +
+ ))} +
+ +
+ {[ + { label: 'Grundpreis (Base)', value: baseCost, color: 'bg-blue-400', detail: `โ‚ฌ${parseFloat(basePrice).toFixed(2)}/year` }, + { + label: 'Arbeitspreis (Work)', + value: workCost, + color: 'bg-green-400', + detail: `${(parseFloat(workPrice) + parseFloat(markupPerUnit)).toFixed(2)} ct/kWh x ${consumption.toLocaleString()} kWh`, + }, + { label: 'Additional Markup', value: addCost, color: 'bg-amber-400', detail: `โ‚ฌ${parseFloat(additionalMarkup).toFixed(2)}/year` }, + ].map((item) => ( +
+
+
+

{item.label}

+

{item.detail}

+
+ โ‚ฌ{item.value.toFixed(2)} +
+ ))} +
+
+ Net Total (annual) + โ‚ฌ{totalNet.toFixed(2)} +
+
+
+
+ + {/* Computed Results */} +
+

Computed via Library

+
+ + + + +
+
+ + {/* Work price breakdown */} +
+

Work Price Detail

+
+
+

GetAG Price

+

{workPrice} ct

+
+ + +
+

Markup

+

{markupPerUnit} ct

+
+ = +
+

Total

+

+ {(parseFloat(workPrice) + parseFloat(markupPerUnit)).toFixed(2)} ct +

+
+
+
+
+
+ + {/* Usage */} +
+ +
+
+ ); +} diff --git a/demo/src/sections/HouseConnectionDemo.tsx b/demo/src/sections/HouseConnectionDemo.tsx new file mode 100644 index 0000000..dfde1a4 --- /dev/null +++ b/demo/src/sections/HouseConnectionDemo.tsx @@ -0,0 +1,302 @@ +import { useState, useMemo } from 'react'; +import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { CodeBlock } from '../components/CodeBlock'; +import { buildPriceItemDto, fmtCents } from '../helpers'; + +interface ConnectionItem { + name: string; + unitAmountDecimal: string; + quantity: number; + type: 'one_time' | 'recurring'; + billingPeriod?: string; +} + +const defaultItems: ConnectionItem[] = [ + { name: 'Electricity Connection', unitAmountDecimal: '1850.00', quantity: 1, type: 'one_time' }, + { name: 'Gas Connection', unitAmountDecimal: '1450.00', quantity: 1, type: 'one_time' }, + { name: 'Water Connection', unitAmountDecimal: '2200.00', quantity: 1, type: 'one_time' }, + { name: 'Construction Power Supply', unitAmountDecimal: '350.00', quantity: 1, type: 'one_time' }, + { name: 'Meter Installation Fee', unitAmountDecimal: '12.50', quantity: 1, type: 'recurring', billingPeriod: 'monthly' }, +]; + +export function HouseConnectionDemo() { + const [items, setItems] = useState(defaultItems); + const [taxRate, setTaxRate] = useState(19); + const [distance, setDistance] = useState(15); + const [perMeterRate, setPerMeterRate] = useState('85.00'); + + // Trench cost based on distance + const trenchCost = distance * parseFloat(perMeterRate); + + const result = useMemo(() => { + const priceItems = items.map((item) => + buildPriceItemDto({ + unitAmountDecimal: item.unitAmountDecimal, + quantity: item.quantity, + type: item.type, + billingPeriod: item.billingPeriod, + taxRate, + isTaxInclusive: false, + description: item.name, + }), + ); + + // Add trench/distance-based cost + priceItems.push( + buildPriceItemDto({ + unitAmountDecimal: trenchCost.toFixed(2), + quantity: 1, + type: 'one_time', + taxRate, + isTaxInclusive: false, + description: 'Trench Work (Tiefbau)', + }), + ); + + return computeAggregatedAndPriceTotals(priceItems); + }, [items, taxRate, trenchCost]); + + const updateItem = (idx: number, field: keyof ConnectionItem, value: any) => { + setItems((prev) => prev.map((item, i) => (i === idx ? { ...item, [field]: value } : item))); + }; + + const toggleItem = (idx: number) => { + setItems((prev) => + prev.map((item, i) => (i === idx ? { ...item, quantity: item.quantity > 0 ? 0 : 1 } : item)), + ); + }; + + const oneTimeCosts = items + .filter((i) => i.type === 'one_time' && i.quantity > 0) + .reduce((sum, i) => sum + parseFloat(i.unitAmountDecimal) * i.quantity, 0); + const recurringCosts = items + .filter((i) => i.type === 'recurring' && i.quantity > 0) + .reduce((sum, i) => sum + parseFloat(i.unitAmountDecimal) * i.quantity, 0); + const totalOneTime = oneTimeCosts + trenchCost; + + return ( +
+

House Connection

+

+ Hausanschluss (house connection) pricing for new builds and renovations. + Combines one-time connection fees, distance-based trench work, and recurring meter costs. +

+ +
+
+ {/* Connection items */} +
+

Connection Services

+
+ {items.map((item, idx) => ( +
0 + ? 'bg-white border-primary-200' + : 'bg-gray-50 border-gray-100 opacity-60' + }`} + > +
+ +
+

{item.name}

+ + {item.type === 'one_time' ? 'One-time' : `${item.billingPeriod}`} + +
+
+ updateItem(idx, 'unitAmountDecimal', e.target.value)} + className="input-field text-xs text-right" + disabled={item.quantity === 0} + /> +
+
+
+ ))} +
+
+ + {/* Distance-based pricing */} +
+
+

Trench Work (Tiefbau)

+
+ + setDistance(Number(e.target.value))} + className="w-full mt-1 accent-amber-500" + /> +
+ 5 m + 100 m +
+
+
+
+ + setPerMeterRate(e.target.value)} + className="input-field mt-1" + /> +
+
+
+

Total Trench Cost

+

EUR {trenchCost.toFixed(2)}

+
+
+
+
+
+
+ +
+ {/* Visual overview */} +
+

Cost Overview

+ + {/* Connection type breakdown */} +
+ {items + .filter((i) => i.quantity > 0 && i.type === 'one_time') + .map((item, idx) => { + const cost = parseFloat(item.unitAmountDecimal) * item.quantity; + return ( +
+ {item.name} + EUR {cost.toFixed(2)} +
+ ); + })} +
+ Trench Work ({distance}m x EUR {parseFloat(perMeterRate).toFixed(2)}) + EUR {trenchCost.toFixed(2)} +
+ {items + .filter((i) => i.quantity > 0 && i.type === 'recurring') + .map((item, idx) => ( +
+
+ {item.name} + /{item.billingPeriod} +
+ EUR {parseFloat(item.unitAmountDecimal).toFixed(2)} +
+ ))} +
+ +
+
+ One-time costs (net) + EUR {totalOneTime.toFixed(2)} +
+ {recurringCosts > 0 && ( +
+ Recurring costs (net) + EUR {recurringCosts.toFixed(2)}/month +
+ )} +
+
+ + {/* Computed results */} +
+

Computed via Library

+
+ + + + +
+ + {(result.total_details?.breakdown?.recurrences?.length ?? 0) > 0 && ( +
+

By Recurrence:

+ {result.total_details?.breakdown?.recurrences?.map((r: any, i: number) => ( +
+ + {r.type === 'one_time' ? 'One-time' : r.billing_period} + + {fmtCents(r.amount_total)} +
+ ))} +
+ )} +
+
+
+ + {/* Usage */} +
+ i.quantity > 0).map((i) => ` { + quantity: ${i.quantity}, + _price: { + unit_amount_decimal: '${i.unitAmountDecimal}', + unit_amount_currency: 'EUR', + pricing_model: 'per_unit', + is_tax_inclusive: false, + type: '${i.type}',${i.billingPeriod ? `\n billing_period: '${i.billingPeriod}',` : ''} + tax: [{ rate: ${taxRate}, type: 'VAT' }], + description: '${i.name}', + }, + taxes: [{ tax: { rate: ${taxRate} } }], + },`).join('\n')} + { + quantity: 1, + _price: { + unit_amount_decimal: '${trenchCost.toFixed(2)}', + unit_amount_currency: 'EUR', + pricing_model: 'per_unit', + is_tax_inclusive: false, + type: 'one_time', + tax: [{ rate: ${taxRate}, type: 'VAT' }], + description: 'Trench Work (${distance}m x EUR ${parseFloat(perMeterRate).toFixed(2)}/m)', + }, + taxes: [{ tax: { rate: ${taxRate} } }], + }, +]; + +const result = computeAggregatedAndPriceTotals(items); +// result.amount_total = ${fmtCents(result.amount_total)}`} + /> +
+
+ ); +} diff --git a/demo/src/sections/NonCommodityDemo.tsx b/demo/src/sections/NonCommodityDemo.tsx new file mode 100644 index 0000000..7d4567f --- /dev/null +++ b/demo/src/sections/NonCommodityDemo.tsx @@ -0,0 +1,366 @@ +import { useState, useMemo } from 'react'; +import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { CodeBlock } from '../components/CodeBlock'; +import { buildPriceItemDto, fmtCents } from '../helpers'; + +interface Product { + name: string; + category: string; + price: string; + quantity: number; + type: 'one_time' | 'recurring'; + billingPeriod?: string; + enabled: boolean; +} + +const defaultProducts: Product[] = [ + // Solar + { name: 'Solar Panel System (10 kWp)', category: 'solar', price: '12500.00', quantity: 1, type: 'one_time', enabled: true }, + { name: 'Battery Storage (10 kWh)', category: 'solar', price: '6800.00', quantity: 1, type: 'one_time', enabled: true }, + { name: 'Solar Installation', category: 'solar', price: '3200.00', quantity: 1, type: 'one_time', enabled: true }, + { name: 'Solar Maintenance Contract', category: 'solar', price: '29.90', quantity: 1, type: 'recurring', billingPeriod: 'monthly', enabled: true }, + // E-Mobility + { name: 'Wallbox (11 kW)', category: 'emobility', price: '899.00', quantity: 1, type: 'one_time', enabled: false }, + { name: 'Wallbox Installation', category: 'emobility', price: '450.00', quantity: 1, type: 'one_time', enabled: false }, + { name: 'Charging Flat Rate', category: 'emobility', price: '59.00', quantity: 1, type: 'recurring', billingPeriod: 'monthly', enabled: false }, + // Heating + { name: 'Heat Pump System', category: 'heating', price: '15800.00', quantity: 1, type: 'one_time', enabled: false }, + { name: 'Heat Pump Installation', category: 'heating', price: '4500.00', quantity: 1, type: 'one_time', enabled: false }, + { name: 'Heating Maintenance', category: 'heating', price: '39.90', quantity: 1, type: 'recurring', billingPeriod: 'monthly', enabled: false }, + // Smart Home + { name: 'Smart Thermostat', category: 'smarthome', price: '249.00', quantity: 1, type: 'one_time', enabled: false }, + { name: 'Energy Manager', category: 'smarthome', price: '499.00', quantity: 1, type: 'one_time', enabled: false }, +]; + +const categoryMeta: Record = { + solar: { label: 'Solar & Storage', color: 'bg-yellow-400', bg: 'bg-yellow-50', text: 'text-yellow-700', icon: '\u2600\uFE0F' }, + emobility: { label: 'E-Mobility', color: 'bg-blue-400', bg: 'bg-blue-50', text: 'text-blue-700', icon: '\uD83D\uDE97' }, + heating: { label: 'Heating', color: 'bg-red-400', bg: 'bg-red-50', text: 'text-red-700', icon: '\uD83C\uDF21\uFE0F' }, + smarthome: { label: 'Smart Home', color: 'bg-green-400', bg: 'bg-green-50', text: 'text-green-700', icon: '\uD83C\uDFE0' }, +}; + +export function NonCommodityDemo() { + const [products, setProducts] = useState(defaultProducts); + const [taxRate, setTaxRate] = useState(19); + + const toggleProduct = (idx: number) => { + setProducts((prev) => prev.map((p, i) => (i === idx ? { ...p, enabled: !p.enabled } : p))); + }; + + const updateProduct = (idx: number, field: keyof Product, value: any) => { + setProducts((prev) => prev.map((p, i) => (i === idx ? { ...p, [field]: value } : p))); + }; + + const toggleCategory = (category: string) => { + setProducts((prev) => { + const catProducts = prev.filter((p) => p.category === category); + const allEnabled = catProducts.every((p) => p.enabled); + return prev.map((p) => (p.category === category ? { ...p, enabled: !allEnabled } : p)); + }); + }; + + const result = useMemo(() => { + const items = products + .filter((p) => p.enabled) + .map((p) => + buildPriceItemDto({ + unitAmountDecimal: p.price, + quantity: p.quantity, + type: p.type, + billingPeriod: p.billingPeriod, + taxRate, + isTaxInclusive: false, + description: p.name, + }), + ); + + if (items.length === 0) { + return { amount_subtotal: 0, amount_tax: 0, amount_total: 0, items: [], total_details: { breakdown: { recurrences: [] } } }; + } + + return computeAggregatedAndPriceTotals(items); + }, [products, taxRate]); + + const enabledProducts = products.filter((p) => p.enabled); + const oneTimeCosts = enabledProducts + .filter((p) => p.type === 'one_time') + .reduce((sum, p) => sum + parseFloat(p.price) * p.quantity, 0); + const monthlyCosts = enabledProducts + .filter((p) => p.type === 'recurring') + .reduce((sum, p) => sum + parseFloat(p.price) * p.quantity, 0); + + const categories = ['solar', 'emobility', 'heating', 'smarthome']; + const categoryTotals = categories.map((cat) => { + const catProducts = enabledProducts.filter((p) => p.category === cat); + return { + category: cat, + ...categoryMeta[cat], + oneTime: catProducts.filter((p) => p.type === 'one_time').reduce((s, p) => s + parseFloat(p.price) * p.quantity, 0), + recurring: catProducts.filter((p) => p.type === 'recurring').reduce((s, p) => s + parseFloat(p.price) * p.quantity, 0), + count: catProducts.length, + }; + }).filter((c) => c.count > 0); + + return ( +
+

Non-Commodity Products

+

+ Products and services beyond the energy supply itself โ€” solar panels, battery storage, + wallboxes, heat pumps, and smart home devices. Combines one-time hardware and installation + costs with recurring service and maintenance contracts. +

+ +
+
+ {/* Category toggles */} +
+

Product Categories

+
+ {categories.map((cat) => { + const meta = categoryMeta[cat]; + const catProducts = products.filter((p) => p.category === cat); + const enabledCount = catProducts.filter((p) => p.enabled).length; + return ( + + ); + })} +
+
+ + {/* Product list */} +
+

Products & Services

+
+ {products.map((product, idx) => { + const meta = categoryMeta[product.category]; + return ( +
+ +
+

{product.name}

+
+ {meta.label} + + {product.type === 'one_time' ? 'One-time' : product.billingPeriod} + +
+
+
+ updateProduct(idx, 'quantity', Math.max(1, Number(e.target.value)))} + className="input-field w-12 text-xs text-center" + disabled={!product.enabled} + /> + updateProduct(idx, 'price', e.target.value)} + className="input-field w-24 text-xs text-right" + disabled={!product.enabled} + /> +
+
+ ); + })} +
+
+ +
+ + +
+
+ +
+ {/* Category cost breakdown */} +
+

Cost by Category

+ + {categoryTotals.length === 0 ? ( +

No products selected

+ ) : ( + <> + {/* Stacked bar for one-time costs */} + {oneTimeCosts > 0 && ( +
+

One-time costs

+
+ {categoryTotals + .filter((c) => c.oneTime > 0) + .map((cat) => ( +
+ {(cat.oneTime / oneTimeCosts) * 100 > 15 ? cat.icon : ''} +
+ ))} +
+
+ )} + +
+ {categoryTotals.map((cat) => ( +
+
+
+

{cat.icon} {cat.label}

+

{cat.count} item(s)

+
+
+ {cat.oneTime > 0 && ( +

EUR {cat.oneTime.toFixed(2)}

+ )} + {cat.recurring > 0 && ( +

+ EUR {cat.recurring.toFixed(2)}/mo

+ )} +
+
+ ))} + +
+
+ One-time total (net) + EUR {oneTimeCosts.toFixed(2)} +
+ {monthlyCosts > 0 && ( +
+ Monthly total (net) + EUR {monthlyCosts.toFixed(2)}/mo +
+ )} +
+
+ + )} +
+ + {/* Bundle summary */} +
+

Selected Bundle

+
+ {enabledProducts.map((p, idx) => ( +
+ + {categoryMeta[p.category].icon} {p.name} + {p.quantity > 1 && x{p.quantity}} + + + EUR {(parseFloat(p.price) * p.quantity).toFixed(2)} + {p.type === 'recurring' && /mo} + +
+ ))} + {enabledProducts.length === 0 && ( +

No products selected

+ )} +
+
+ + {/* Computed results */} +
+

Computed via Library

+
+ + + + +
+ + {(result.total_details?.breakdown?.recurrences?.length ?? 0) > 0 && ( +
+

By Recurrence:

+ {result.total_details?.breakdown?.recurrences?.map((r: any, i: number) => ( +
+ + {r.type === 'one_time' ? 'One-time' : r.billing_period} + + {fmtCents(r.amount_total)} +
+ ))} +
+ )} +
+
+
+ + {/* Usage */} +
+ ` { + quantity: ${p.quantity}, + _price: { + unit_amount_decimal: '${p.price}', + unit_amount_currency: 'EUR', + pricing_model: 'per_unit', + is_tax_inclusive: false, + type: '${p.type}',${p.billingPeriod ? `\n billing_period: '${p.billingPeriod}',` : ''} + tax: [{ rate: ${taxRate}, type: 'VAT' }], + description: '${p.name}', + }, + taxes: [{ tax: { rate: ${taxRate} } }], + },`).join('\n')}${enabledProducts.length > 4 ? `\n // ... ${enabledProducts.length - 4} more items` : ''} +]; + +const result = computeAggregatedAndPriceTotals(items); +// result.amount_total = ${fmtCents(result.amount_total)} +// Recurrence breakdown available via result.total_details.breakdown.recurrences`} + /> +
+
+ ); +} diff --git a/demo/src/sections/OverviewDemo.tsx b/demo/src/sections/OverviewDemo.tsx new file mode 100644 index 0000000..7846cfd --- /dev/null +++ b/demo/src/sections/OverviewDemo.tsx @@ -0,0 +1,243 @@ +import { CodeBlock } from '../components/CodeBlock'; + +interface OverviewDemoProps { + onNavigate: (id: string) => void; +} + +const features = [ + { + id: 'per-unit', + title: 'Per Unit Pricing', + desc: 'Simple price x quantity calculations with tax support', + icon: '๐Ÿ“ฆ', + color: 'bg-blue-50 border-blue-200', + }, + { + id: 'tiered-volume', + title: 'Tiered Volume', + desc: 'Single tier selected based on total quantity', + icon: '๐Ÿ“Š', + color: 'bg-indigo-50 border-indigo-200', + }, + { + id: 'tiered-graduated', + title: 'Tiered Graduated', + desc: 'Different rates apply to different quantity ranges', + icon: '๐Ÿ“ˆ', + color: 'bg-purple-50 border-purple-200', + }, + { + id: 'tiered-flatfee', + title: 'Tiered Flat Fee', + desc: 'Fixed fee based on quantity range', + icon: '๐Ÿท๏ธ', + color: 'bg-pink-50 border-pink-200', + }, + { + id: 'tax', + title: 'Tax Handling', + desc: 'Inclusive/exclusive tax with multi-rate breakdown', + icon: '๐Ÿงพ', + color: 'bg-green-50 border-green-200', + }, + { + id: 'discounts', + title: 'Discounts & Coupons', + desc: 'Fixed, percentage, and cashback coupon types', + icon: '๐ŸŽŸ๏ธ', + color: 'bg-amber-50 border-amber-200', + }, + { + id: 'composite', + title: 'Composite Pricing', + desc: 'Multi-component bundled price items', + icon: '๐Ÿงฉ', + color: 'bg-cyan-50 border-cyan-200', + }, + { + id: 'recurring', + title: 'Recurring Billing', + desc: 'Billing periods and frequency normalization', + icon: '๐Ÿ”„', + color: 'bg-teal-50 border-teal-200', + }, + { + id: 'currency', + title: 'Currency & Formatting', + desc: 'Multi-currency support with locale-aware formatting', + icon: '๐Ÿ’ฑ', + color: 'bg-emerald-50 border-emerald-200', + }, + { + id: 'dynamic-tariff', + title: 'Dynamic Tariff', + desc: 'Market-based pricing with configurable markup', + icon: 'โšก', + color: 'bg-yellow-50 border-yellow-200', + }, + { + id: 'getag', + title: 'GetAG Energy Pricing', + desc: 'German energy operator integration with tiered markups', + icon: '๐Ÿ”Œ', + color: 'bg-orange-50 border-orange-200', + }, +]; + +const useCases = [ + { + id: 'electricity', + title: 'Electricity', + desc: 'Single & dual-tariff (HT/NT) electricity pricing with Grundpreis and Arbeitspreis', + icon: 'โšก', + color: 'bg-yellow-50 border-yellow-200', + }, + { + id: 'gas', + title: 'Gas', + desc: 'Gas supply tariffs with CO2 levy, gas storage levy, and per-kWh work price', + icon: '๐Ÿ”ฅ', + color: 'bg-orange-50 border-orange-200', + }, + { + id: 'house-connection', + title: 'House Connection', + desc: 'Hausanschluss fees with distance-based trench work and connection services', + icon: '๐Ÿก', + color: 'bg-emerald-50 border-emerald-200', + }, + { + id: 'non-commodity', + title: 'Non-Commodity', + desc: 'Solar panels, wallboxes, heat pumps, and smart home products with service contracts', + icon: '๐Ÿ“‹', + color: 'bg-purple-50 border-purple-200', + }, +]; + +export function OverviewDemo({ onNavigate }: OverviewDemoProps) { + return ( +
+ {/* Hero */} +
+

+ epilot Pricing Playground +

+

+ Interactive playground for @epilot/pricing โ€” a comprehensive + pricing calculation engine supporting 6 pricing models, tax handling, + discounts, composite pricing, recurring billing, multi-currency formatting, and + energy-market integrations. Explore each capability below. +

+
+ + {/* Stats */} +
+ {[ + { label: 'Pricing Models', value: '6' }, + { label: 'Exported Functions', value: '40+' }, + { label: 'Billing Periods', value: '6' }, + { label: 'Decimal Precision', value: '12 digits' }, + ].map((stat) => ( +
+

{stat.value}

+

{stat.label}

+
+ ))} +
+ + {/* Use Cases */} +

Energy & Utility Use Cases

+
+ {useCases.map((f) => ( + + ))} +
+ + {/* Capabilities */} +

Capabilities

+
+ {features.map((f) => ( + + ))} +
+ + {/* Quick Start */} +
+ + +
+ + {/* Architecture */} +
+

How It Works

+
+ {[ + { step: '1', label: 'Price Items', desc: 'Define price, quantity, tax, coupons' }, + { step: '2', label: 'Compute', desc: 'computeAggregatedAndPriceTotals()' }, + { step: '3', label: 'Results', desc: 'Subtotals, tax, discounts, recurrences' }, + ].map((s, i) => ( +
+ {i > 0 && ( + + + + )} +
+
+ {s.step} +
+

{s.label}

+

{s.desc}

+
+
+ ))} +
+
+
+ ); +} diff --git a/demo/src/sections/PerUnitDemo.tsx b/demo/src/sections/PerUnitDemo.tsx new file mode 100644 index 0000000..d4ee7a0 --- /dev/null +++ b/demo/src/sections/PerUnitDemo.tsx @@ -0,0 +1,195 @@ +import { useState, useMemo } from 'react'; +import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { CodeBlock } from '../components/CodeBlock'; +import { buildPriceItemDto, fmtCents } from '../helpers'; + +export function PerUnitDemo() { + const [unitPrice, setUnitPrice] = useState('49.99'); + const [quantity, setQuantity] = useState(5); + const [taxRate, setTaxRate] = useState(19); + const [isTaxInclusive, setIsTaxInclusive] = useState(true); + const [currency, setCurrency] = useState('EUR'); + + const result = useMemo(() => { + const item = buildPriceItemDto({ + unitAmountDecimal: unitPrice, + quantity, + currency, + taxRate, + isTaxInclusive, + description: 'Product', + }); + return computeAggregatedAndPriceTotals([item]); + }, [unitPrice, quantity, taxRate, isTaxInclusive, currency]); + + const lineItem = result.items?.[0]; + + return ( +
+

Per Unit Pricing

+

+ The simplest model: unit price multiplied by quantity. Supports tax-inclusive and tax-exclusive modes. +

+ +
+ {/* Controls */} +
+

Configure

+ +
+ + setUnitPrice(e.target.value)} + className="input-field mt-1" + /> +
+ +
+ + setQuantity(Number(e.target.value))} + className="w-full mt-1 accent-primary-600" + /> +
+ 1 + 100 +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + + +
+
+ + {/* Results */} +
+
+

Computed Result

+
+ + + + +
+
+ +
+

Aggregated Totals

+
+ + + +
+
+
+
+ + {/* Code */} +
+ +
+
+ ); +} diff --git a/demo/src/sections/RecurringBillingDemo.tsx b/demo/src/sections/RecurringBillingDemo.tsx new file mode 100644 index 0000000..929041f --- /dev/null +++ b/demo/src/sections/RecurringBillingDemo.tsx @@ -0,0 +1,293 @@ +import { useState, useMemo } from 'react'; +import { computeAggregatedAndPriceTotals, normalizeValueToFrequencyUnit } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { CodeBlock } from '../components/CodeBlock'; +import { buildPriceItemDto, fmtCents } from '../helpers'; + +const periods = [ + { value: 'weekly', label: 'Weekly' }, + { value: 'monthly', label: 'Monthly' }, + { value: 'every_quarter', label: 'Quarterly' }, + { value: 'every_6_months', label: 'Semi-Annual' }, + { value: 'yearly', label: 'Yearly' }, +]; + +const normFactors: Record> = { + weekly: { weekly: 1, monthly: 4, every_quarter: 13, every_6_months: 26, yearly: 52 }, + monthly: { weekly: 1 / 4, monthly: 1, every_quarter: 3, every_6_months: 6, yearly: 12 }, + every_quarter: { weekly: 1 / 13, monthly: 1 / 3, every_quarter: 1, every_6_months: 2, yearly: 4 }, + every_6_months: { weekly: 1 / 26, monthly: 1 / 6, every_quarter: 1 / 2, every_6_months: 1, yearly: 2 }, + yearly: { weekly: 1 / 52, monthly: 1 / 12, every_quarter: 1 / 4, every_6_months: 1 / 2, yearly: 1 }, +}; + +export function RecurringBillingDemo() { + const [unitPrice, setUnitPrice] = useState('29.99'); + const [basePeriod, setBasePeriod] = useState('monthly'); + const [quantity, setQuantity] = useState(1); + const [taxRate, setTaxRate] = useState(19); + const [isTaxInclusive, setIsTaxInclusive] = useState(true); + + const result = useMemo(() => { + const item = buildPriceItemDto({ + unitAmountDecimal: unitPrice, + quantity, + pricingModel: 'per_unit', + type: 'recurring', + billingPeriod: basePeriod, + taxRate, + isTaxInclusive, + description: 'Subscription', + }); + return computeAggregatedAndPriceTotals([item]); + }, [unitPrice, quantity, basePeriod, taxRate, isTaxInclusive]); + + // Frequency normalization + const normalizedPrices = useMemo(() => { + const base = parseFloat(unitPrice); + if (isNaN(base)) return []; + + return periods.map((p) => { + try { + const normalized = normalizeValueToFrequencyUnit( + unitPrice, + basePeriod as any, + p.value as any, + ); + return { + period: p.label, + periodValue: p.value, + amount: typeof normalized === 'string' ? parseFloat(normalized) : normalized, + isBase: p.value === basePeriod, + }; + } catch { + const factor = normFactors[basePeriod]?.[p.value] ?? 1; + return { + period: p.label, + periodValue: p.value, + amount: base * factor, + isBase: p.value === basePeriod, + }; + } + }); + }, [unitPrice, basePeriod]); + + // Mixed recurrence demo + const [showMixed, setShowMixed] = useState(false); + const mixedResult = useMemo(() => { + const items = [ + buildPriceItemDto({ + unitAmountDecimal: '499.00', + quantity: 1, + type: 'one_time', + taxRate: 19, + isTaxInclusive: true, + description: 'Setup Fee', + }), + buildPriceItemDto({ + unitAmountDecimal: '29.99', + quantity: 1, + type: 'recurring', + billingPeriod: 'monthly', + taxRate: 19, + isTaxInclusive: true, + description: 'Monthly Plan', + }), + buildPriceItemDto({ + unitAmountDecimal: '199.00', + quantity: 1, + type: 'recurring', + billingPeriod: 'yearly', + taxRate: 19, + isTaxInclusive: true, + description: 'Annual License', + }), + ]; + return computeAggregatedAndPriceTotals(items); + }, []); + + return ( +
+

Recurring Billing

+

+ Support for one-time and recurring prices with frequency normalization. + Convert prices between billing periods automatically. +

+ +
+
+
+

Configure

+
+
+ + setUnitPrice(e.target.value)} + className="input-field mt-1" + /> +
+
+ + +
+
+
+ Qty: {quantity} + | + +
+
+ + {/* Computed Result */} +
+

Computed Result

+
+ + + + p.value === basePeriod)?.label ?? basePeriod} + /> +
+
+
+ +
+ {/* Frequency Normalization */} +
+

Frequency Normalization

+

+ Same price expressed in different billing periods using normalizeValueToFrequencyUnit() +

+
+ {normalizedPrices.map((np) => ( +
+ + {np.period} + {np.isBase && (base)} + + + โ‚ฌ{typeof np.amount === 'number' ? np.amount.toFixed(2) : np.amount} + +
+ ))} +
+
+
+
+ + {/* Mixed Recurrence */} +
+
+

Mixed Recurrence Example

+ +
+ {showMixed && ( +
+

+ Three items: a one-time setup fee, a monthly subscription, and an annual license. + The library groups totals by recurrence type. +

+ +
+ {['Setup Fee (โ‚ฌ499, one-time)', 'Monthly Plan (โ‚ฌ29.99/mo)', 'Annual License (โ‚ฌ199/yr)'].map( + (label, i) => ( +
+

{label}

+

+ {fmtCents(mixedResult.items?.[i]?.amount_total)} +

+
+ ), + )} +
+ +
+ + + +
+ + {(mixedResult.total_details?.breakdown?.recurrences?.length ?? 0) > 0 && ( +
+

Grouped by Recurrence:

+ {mixedResult.total_details?.breakdown?.recurrences?.map((r: any, i: number) => ( +
+ + {r.type === 'one_time' ? 'One-time' : r.billing_period} + +
+ {fmtCents(r.amount_total)} + (net: {fmtCents(r.amount_subtotal)}) +
+
+ ))} +
+ )} +
+ )} +
+ + {/* Usage */} +
+ p.periodValue === 'yearly')?.amount.toFixed(2) ?? 'N/A'}`} + /> +
+
+ ); +} diff --git a/demo/src/sections/TaxDemo.tsx b/demo/src/sections/TaxDemo.tsx new file mode 100644 index 0000000..afc65cc --- /dev/null +++ b/demo/src/sections/TaxDemo.tsx @@ -0,0 +1,223 @@ +import { useState, useMemo } from 'react'; +import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { CodeBlock } from '../components/CodeBlock'; +import { buildPriceItemDto, fmtCents } from '../helpers'; + +export function TaxDemo() { + const [unitPrice, setUnitPrice] = useState('100.00'); + const [quantity, setQuantity] = useState(3); + const [taxRate, setTaxRate] = useState(19); + + // Compute both modes side by side + const inclusiveResult = useMemo(() => { + const item = buildPriceItemDto({ + unitAmountDecimal: unitPrice, + quantity, + taxRate, + isTaxInclusive: true, + }); + return computeAggregatedAndPriceTotals([item]); + }, [unitPrice, quantity, taxRate]); + + const exclusiveResult = useMemo(() => { + const item = buildPriceItemDto({ + unitAmountDecimal: unitPrice, + quantity, + taxRate, + isTaxInclusive: false, + }); + return computeAggregatedAndPriceTotals([item]); + }, [unitPrice, quantity, taxRate]); + + // Multi-tax demo + const [showMultiTax, setShowMultiTax] = useState(false); + const multiTaxResult = useMemo(() => { + const item1 = buildPriceItemDto({ + unitAmountDecimal: '50.00', + quantity: 2, + taxRate: 19, + isTaxInclusive: true, + description: 'Standard Rate Item', + }); + const item2 = buildPriceItemDto({ + unitAmountDecimal: '30.00', + quantity: 3, + taxRate: 7, + isTaxInclusive: true, + description: 'Reduced Rate Item', + }); + return computeAggregatedAndPriceTotals([item1, item2]); + }, []); + + const incItem = inclusiveResult.items?.[0]; + const excItem = exclusiveResult.items?.[0]; + + return ( +
+

Tax Handling

+

+ Compare tax-inclusive vs tax-exclusive pricing side by side. The library handles tax calculations + precisely using Dinero.js for decimal arithmetic. +

+ + {/* Controls */} +
+
+
+ + setUnitPrice(e.target.value)} + className="input-field mt-1" + /> +
+
+ + setQuantity(Number(e.target.value))} + className="w-full mt-3 accent-primary-600" + /> +
+
+ + +
+
+
+ + {/* Side by Side */} +
+ {/* Inclusive */} +
+
+ Tax Inclusive + Price includes tax +
+
+ + + + + +
+
+ The โ‚ฌ{unitPrice} price already contains {taxRate}% tax. + Net = โ‚ฌ{unitPrice} / 1.{String(taxRate).padStart(2, '0')} per unit. +
+
+ + {/* Exclusive */} +
+
+ Tax Exclusive + Tax added on top +
+
+ + + + + +
+
+ โ‚ฌ{unitPrice} is the net price. {taxRate}% tax is added on top. + Gross = โ‚ฌ{unitPrice} * 1.{String(taxRate).padStart(2, '0')} per unit. +
+
+
+ + {/* Multi-Tax */} +
+
+

Multi-Tax Breakdown

+ +
+ {showMultiTax && ( +
+

+ Two items with different tax rates (19% standard, 7% reduced). The library tracks + tax breakdown by rate. +

+
+ + + + +
+
+

Tax Breakdown:

+ {multiTaxResult.total_details?.breakdown?.taxes?.map((t: any, i: number) => { + const rate = t.tax?.rate ?? t.rateValue ?? 0; + const type = t.tax?.type || 'VAT'; + return ( +
+ {rate}% + {type} {rate}% + {fmtCents(t.amount)} +
+ ); + })} +
+
+ )} +
+ + {/* Usage */} +
+ +
+
+ ); +} diff --git a/demo/src/sections/TieredFlatFeeDemo.tsx b/demo/src/sections/TieredFlatFeeDemo.tsx new file mode 100644 index 0000000..685b499 --- /dev/null +++ b/demo/src/sections/TieredFlatFeeDemo.tsx @@ -0,0 +1,197 @@ +import { useState, useMemo } from 'react'; +import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { CodeBlock } from '../components/CodeBlock'; +import { buildPriceItemDto, fmtCents } from '../helpers'; + +const defaultTiers = [ + { up_to: 10, unit_amount: 0, unit_amount_decimal: '0', flat_fee_amount: 9900, flat_fee_amount_decimal: '99.00' }, + { up_to: 50, unit_amount: 0, unit_amount_decimal: '0', flat_fee_amount: 19900, flat_fee_amount_decimal: '199.00' }, + { up_to: 100, unit_amount: 0, unit_amount_decimal: '0', flat_fee_amount: 34900, flat_fee_amount_decimal: '349.00' }, + { up_to: null, unit_amount: 0, unit_amount_decimal: '0', flat_fee_amount: 49900, flat_fee_amount_decimal: '499.00' }, +]; + +export function TieredFlatFeeDemo() { + const [quantity, setQuantity] = useState(30); + const [tiers, setTiers] = useState(defaultTiers); + const [taxRate, setTaxRate] = useState(19); + const [isTaxInclusive, setIsTaxInclusive] = useState(true); + + const result = useMemo(() => { + const item = buildPriceItemDto({ + unitAmountDecimal: '0', + quantity, + pricingModel: 'tiered_flatfee', + taxRate, + isTaxInclusive, + tiers, + description: 'Flat Fee Product', + }); + return computeAggregatedAndPriceTotals([item]); + }, [quantity, tiers, taxRate, isTaxInclusive]); + + const activeTierIdx = tiers.findIndex((t) => t.up_to === null || quantity <= t.up_to); + + const updateTier = (idx: number, field: string, value: string) => { + setTiers((prev) => + prev.map((t, i) => { + if (i !== idx) return t; + if (field === 'up_to') return { ...t, up_to: value === '' ? null : (Number(value) as any) }; + return { ...t, flat_fee_amount_decimal: value, flat_fee_amount: Math.round(Number(value) * 100) }; + }), + ); + }; + + return ( +
+

Tiered Flat Fee

+

+ A single fixed fee is charged based on the quantity range. Unlike volume pricing, + the fee does not multiply by quantity. +

+ +
+
+
+

Tier Configuration

+ + + + + + + + + + + {tiers.map((tier, idx) => ( + + + + + + + ))} + +
TierUp ToFlat FeeStatus
Tier {idx + 1} + updateTier(idx, 'up_to', e.target.value)} + className="input-field w-20" + /> + + updateTier(idx, 'flat_fee_amount_decimal', e.target.value)} + className="input-field w-28" + /> + + {idx === activeTierIdx && Selected} +
+
+ +
+ + setQuantity(Number(e.target.value))} + className="w-full mt-2 accent-primary-600" + /> +
+ +
+
+
+ +
+ {/* Visual */} +
+

Fee by Range

+
+ {tiers.map((tier, idx) => { + const prevLimit = idx === 0 ? 0 : (tiers[idx - 1].up_to ?? 0); + const isActive = idx === activeTierIdx; + return ( +
+
+ + {prevLimit + 1} - {tier.up_to ?? 'โˆž'} units + +
+ + โ‚ฌ{tier.flat_fee_amount_decimal} + + {isActive && ( + โ† Your plan + )} +
+ ); + })} +
+
+ +
+

Computed Result

+
+ + + + 0 ? fmtCents(Math.round((result.amount_total ?? 0) / quantity)) : '-'} + color="blue" + /> +
+
+
+
+ + {/* Usage */} +
+ ` { up_to: ${t.up_to === null ? 'null' : t.up_to}, unit_amount_decimal: '0', flat_fee_amount_decimal: '${t.flat_fee_amount_decimal}' },`).join('\n')} + ], + }, + taxes: [{ tax: { rate: ${taxRate} } }], +}; + +const result = computeAggregatedAndPriceTotals([priceItem]); +// result.amount_total = ${result.amount_total} (${fmtCents(result.amount_total)})`} + /> +
+
+ ); +} diff --git a/demo/src/sections/TieredGraduatedDemo.tsx b/demo/src/sections/TieredGraduatedDemo.tsx new file mode 100644 index 0000000..c6c26b9 --- /dev/null +++ b/demo/src/sections/TieredGraduatedDemo.tsx @@ -0,0 +1,236 @@ +import { useState, useMemo } from 'react'; +import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { CodeBlock } from '../components/CodeBlock'; +import { buildPriceItemDto, fmtCents } from '../helpers'; + +const defaultTiers = [ + { up_to: 10, unit_amount: 5000, unit_amount_decimal: '50.00', flat_fee_amount: 0, flat_fee_amount_decimal: '0' }, + { up_to: 50, unit_amount: 3000, unit_amount_decimal: '30.00', flat_fee_amount: 0, flat_fee_amount_decimal: '0' }, + { up_to: 100, unit_amount: 2000, unit_amount_decimal: '20.00', flat_fee_amount: 0, flat_fee_amount_decimal: '0' }, + { up_to: null, unit_amount: 1000, unit_amount_decimal: '10.00', flat_fee_amount: 0, flat_fee_amount_decimal: '0' }, +]; + +function getTierBreakdown(quantity: number, tiers: typeof defaultTiers) { + const breakdown: { tier: number; from: number; to: number; qty: number; rate: string; subtotal: number }[] = []; + let remaining = quantity; + let prevLimit = 0; + + for (let i = 0; i < tiers.length && remaining > 0; i++) { + const tierLimit = tiers[i].up_to ?? Infinity; + const tierCapacity = tierLimit - prevLimit; + const qtyInTier = Math.min(remaining, tierCapacity); + const rate = parseFloat(tiers[i].unit_amount_decimal); + + breakdown.push({ + tier: i + 1, + from: prevLimit + 1, + to: prevLimit + qtyInTier, + qty: qtyInTier, + rate: tiers[i].unit_amount_decimal, + subtotal: qtyInTier * rate * 100, + }); + + remaining -= qtyInTier; + prevLimit = tierLimit === Infinity ? prevLimit + qtyInTier : tierLimit; + } + return breakdown; +} + +export function TieredGraduatedDemo() { + const [quantity, setQuantity] = useState(75); + const [tiers, setTiers] = useState(defaultTiers); + const [isTaxInclusive, setIsTaxInclusive] = useState(true); + const [taxRate, setTaxRate] = useState(19); + + const result = useMemo(() => { + const item = buildPriceItemDto({ + unitAmountDecimal: '0', + quantity, + pricingModel: 'tiered_graduated', + taxRate, + isTaxInclusive, + tiers, + description: 'Graduated Product', + }); + return computeAggregatedAndPriceTotals([item]); + }, [quantity, tiers, taxRate, isTaxInclusive]); + + const breakdown = getTierBreakdown(quantity, tiers); + + const updateTier = (idx: number, field: string, value: string) => { + setTiers((prev) => + prev.map((t, i) => { + if (i !== idx) return t; + if (field === 'up_to') return { ...t, up_to: value === '' ? null : (Number(value) as any) }; + return { ...t, unit_amount_decimal: value, unit_amount: Math.round(Number(value) * 100) }; + }), + ); + }; + + // Stacked bar visualization + const totalUnits = breakdown.reduce((s, b) => s + b.qty, 0); + const tierColors = ['bg-blue-400', 'bg-indigo-400', 'bg-purple-400', 'bg-pink-400']; + + return ( +
+

Tiered Graduated Pricing

+

+ Units are spread across tiers. Each tier charges its own rate for the units within its range. + This is the "graduated" model used by many SaaS platforms. +

+ +
+
+ {/* Tier Editor */} +
+

Tier Configuration

+ + + + + + + + + + {tiers.map((tier, idx) => ( + + + + + + ))} + +
TierUp ToUnit Price
+ + Tier {idx + 1} + + updateTier(idx, 'up_to', e.target.value)} + className="input-field w-20" + /> + + updateTier(idx, 'unit_amount_decimal', e.target.value)} + className="input-field w-24" + /> +
+
+ + {/* Quantity */} +
+ + setQuantity(Number(e.target.value))} + className="w-full mt-2 accent-primary-600" + /> +
+ +
+
+
+ +
+ {/* Stacked Bar */} +
+

Tier Breakdown Visualization

+
+ {breakdown.map((b, i) => ( +
+ {b.qty > 5 && `${b.qty}u`} +
+ ))} +
+
+ {breakdown.map((b, i) => ( +
+ + + Tier {b.tier}: units {b.from}-{b.to} ({b.qty} units) + + @ โ‚ฌ{b.rate}/unit + {fmtCents(b.subtotal)} +
+ ))} +
+
+ + {/* Results */} +
+

Computed Result

+
+ + + + 0 + ? fmtCents(Math.round((result.amount_total ?? 0) / quantity)) + : '-' + } + color="blue" + /> +
+
+
+
+ + {/* Usage */} +
+ ` { up_to: ${t.up_to === null ? 'null' : t.up_to}, unit_amount_decimal: '${t.unit_amount_decimal}', flat_fee_amount_decimal: '0' },`).join('\n')} + ], + }, + taxes: [{ tax: { rate: ${taxRate} } }], +}; + +const result = computeAggregatedAndPriceTotals([priceItem]); +// result.amount_total = ${result.amount_total} (${fmtCents(result.amount_total)})`} + /> +
+
+ ); +} diff --git a/demo/src/sections/TieredVolumeDemo.tsx b/demo/src/sections/TieredVolumeDemo.tsx new file mode 100644 index 0000000..7605d64 --- /dev/null +++ b/demo/src/sections/TieredVolumeDemo.tsx @@ -0,0 +1,206 @@ +import { useState, useMemo } from 'react'; +import { computeAggregatedAndPriceTotals } from '@epilot/pricing'; +import { ResultCard } from '../components/ResultCard'; +import { TierChart } from '../components/TierChart'; +import { CodeBlock } from '../components/CodeBlock'; +import { buildPriceItemDto, fmtCents } from '../helpers'; + +const defaultTiers = [ + { up_to: 10, unit_amount: 5000, unit_amount_decimal: '50.00', flat_fee_amount: 0, flat_fee_amount_decimal: '0' }, + { up_to: 50, unit_amount: 4000, unit_amount_decimal: '40.00', flat_fee_amount: 0, flat_fee_amount_decimal: '0' }, + { up_to: 100, unit_amount: 3000, unit_amount_decimal: '30.00', flat_fee_amount: 0, flat_fee_amount_decimal: '0' }, + { up_to: null, unit_amount: 2000, unit_amount_decimal: '20.00', flat_fee_amount: 0, flat_fee_amount_decimal: '0' }, +]; + +export function TieredVolumeDemo() { + const [quantity, setQuantity] = useState(25); + const [tiers, setTiers] = useState(defaultTiers); + const [taxRate, setTaxRate] = useState(19); + const [isTaxInclusive, setIsTaxInclusive] = useState(true); + + const result = useMemo(() => { + const item = buildPriceItemDto({ + unitAmountDecimal: '0', + quantity, + pricingModel: 'tiered_volume', + taxRate, + isTaxInclusive, + tiers, + description: 'Volume Product', + }); + return computeAggregatedAndPriceTotals([item]); + }, [quantity, tiers, taxRate, isTaxInclusive]); + + const lineItem = result.items?.[0]; + + // Determine active tier + const activeTierIdx = tiers.findIndex((t) => t.up_to === null || quantity <= t.up_to); + + const updateTier = (idx: number, field: string, value: string) => { + setTiers((prev) => + prev.map((t, i) => { + if (i !== idx) return t; + if (field === 'up_to') { + const v = value === '' ? null : Number(value); + return { ...t, up_to: v as any }; + } + const numVal = Number(value); + return { + ...t, + unit_amount_decimal: value, + unit_amount: Math.round(numVal * 100), + }; + }), + ); + }; + + const chartBars = tiers.map((t, i) => ({ + label: t.up_to ? `โ‰ค ${t.up_to}` : 'โˆž', + value: parseFloat(t.unit_amount_decimal), + active: i === activeTierIdx, + sublabel: `${t.unit_amount_decimal}/unit`, + })); + + return ( +
+

Tiered Volume Pricing

+

+ A single tier is selected based on total quantity. The selected tier's unit price applies to all units. +

+ +
+
+ {/* Tier Editor */} +
+

Tier Configuration

+ + + + + + + + + + + {tiers.map((tier, idx) => ( + + + + + + + ))} + +
TierUp ToUnit PriceActive
Tier {idx + 1} + updateTier(idx, 'up_to', e.target.value)} + className="input-field w-20" + /> + + updateTier(idx, 'unit_amount_decimal', e.target.value)} + className="input-field w-24" + /> + + {idx === activeTierIdx && Active} +
+
+ + {/* Quantity */} +
+ + setQuantity(Number(e.target.value))} + className="w-full mt-2 accent-primary-600" + /> +
+ 1 + 200 +
+ +
+ Tax: {taxRate}% + | + +
+
+
+ +
+ {/* Chart */} +
+ `โ‚ฌ${v.toFixed(0)}`} + /> +
+ + {/* Results */} +
+

Computed Result

+
+ + + + +
+
+ How it works: With quantity {quantity}, tier {activeTierIdx + 1} is selected + ({tiers[activeTierIdx]?.unit_amount_decimal}/unit). All {quantity} units use this price. +
+
+
+
+ + {/* Usage */} +
+ ` { up_to: ${t.up_to === null ? 'null' : t.up_to}, unit_amount_decimal: '${t.unit_amount_decimal}', flat_fee_amount_decimal: '0' },`).join('\n')} + ], + }, + taxes: [{ tax: { rate: ${taxRate} } }], +}; + +const result = computeAggregatedAndPriceTotals([priceItem]); +// result.amount_total = ${result.amount_total} (${fmtCents(result.amount_total)})`} + /> +
+
+ ); +} diff --git a/demo/tailwind.config.js b/demo/tailwind.config.js new file mode 100644 index 0000000..d4e9939 --- /dev/null +++ b/demo/tailwind.config.js @@ -0,0 +1,23 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + colors: { + primary: { + 50: '#eff6ff', + 100: '#dbeafe', + 200: '#bfdbfe', + 300: '#93c5fd', + 400: '#60a5fa', + 500: '#3b82f6', + 600: '#2563eb', + 700: '#1d4ed8', + 800: '#1e40af', + 900: '#1e3a8a', + }, + }, + }, + }, + plugins: [], +}; diff --git a/demo/tsconfig.json b/demo/tsconfig.json new file mode 100644 index 0000000..0fe38ff --- /dev/null +++ b/demo/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "paths": { + "@epilot/pricing": ["../src/index.ts"], + "@epilot/pricing/*": ["../src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/demo/vite.config.ts b/demo/vite.config.ts new file mode 100644 index 0000000..a7feb00 --- /dev/null +++ b/demo/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@epilot/pricing': path.resolve(__dirname, '../src/index.ts'), + '@epilot/pricing-client': path.resolve(__dirname, '../node_modules/@epilot/pricing-client'), + 'dinero.js': path.resolve(__dirname, '../node_modules/dinero.js'), + 'i18next': path.resolve(__dirname, '../node_modules/i18next'), + }, + }, + optimizeDeps: { + include: ['dinero.js', 'react', 'react-dom'], + }, +});