diff --git a/.gitignore b/.gitignore index 7c117c5..ef6a85c 100755 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,7 @@ dist/ .playwright-mcp mobile-ui-references/ .fuse_* + +# directories tmp/ +references/ diff --git a/bun.lock b/bun.lock index bb8ae1a..5b1c2d6 100755 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,8 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.2.2", + "@json-render/core": "^0.4.0", + "@json-render/react": "^0.4.0", "@opennextjs/cloudflare": "^1.14.4", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", @@ -41,23 +43,31 @@ "@tanstack/react-table": "^8.21.3", "@workos-inc/authkit-nextjs": "^2.13.0", "@workos-inc/node": "^8.1.0", + "ai": "^6.0.72", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "drizzle-orm": "^0.45.1", "embla-carousel-react": "^8.6.0", + "framer-motion": "11", "frappe-gantt": "^1.0.4", "input-otp": "^1.4.2", - "lucide-react": "^0.562.0", + "lucide-react": "^0.563.0", + "nanoid": "^5.1.6", "next": "15.5.9", "next-themes": "^0.4.6", + "radix-ui": "^1.4.3", "react": "19.1.4", "react-day-picker": "^9.13.0", "react-dom": "19.1.4", "react-hook-form": "^7.71.1", + "react-markdown": "10", "react-resizable-panels": "^4.4.1", "recharts": "2.15.4", + "remark-gfm": "4", + "remeda": "2", + "shiki": "1", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.4.0", @@ -81,6 +91,12 @@ }, }, "packages": { + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.7", "@ai-sdk/provider-utils": "4.0.13", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9aRTVM1P1u4yUIjBpco/WCF1WXr/DgWKuDYgLLHdENS8kiEuxDOPJuGbc/6+7EwQ6ZqSh0UOgeqvHfGJfU23Qg=="], + + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-VkPLrutM6VdA924/mG8OS+5frbVTcu6e046D2bgDo00tehBANR1QBJ/mPcZ9tXMFOsVcm6SQArOregxePzTFPw=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.13", "", { "dependencies": { "@ai-sdk/provider": "3.0.7", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HHG72BN4d+OWTcq2NwTxOm/2qvk1duYsnhCDtsbYwn/h/4zeqURu1S0+Cn0nY2Ysq9a9HGKvrYuMn9bgFhR2Og=="], + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@ast-grep/napi": ["@ast-grep/napi@0.40.0", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.0", "@ast-grep/napi-darwin-x64": "0.40.0", "@ast-grep/napi-linux-arm64-gnu": "0.40.0", "@ast-grep/napi-linux-arm64-musl": "0.40.0", "@ast-grep/napi-linux-x64-gnu": "0.40.0", "@ast-grep/napi-linux-x64-musl": "0.40.0", "@ast-grep/napi-win32-arm64-msvc": "0.40.0", "@ast-grep/napi-win32-ia32-msvc": "0.40.0", "@ast-grep/napi-win32-x64-msvc": "0.40.0" } }, "sha512-tq6nO/8KwUF/mHuk1ECaAOSOlz2OB/PmygnvprJzyAHGRVzdcffblaOOWe90M9sGz5MAasXoF+PTcayQj9TKKA=="], @@ -413,6 +429,10 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@json-render/core": ["@json-render/core@0.4.0", "", { "dependencies": { "zod": "^4.0.0" } }, "sha512-zcmNNetyXoqShG9qfXnipnkaGLn3XC7Kec/t3+C39mXAStg2RX29ciZwsZT+Fzo900LeRHRIGel7L/IHCdktrA=="], + + "@json-render/react": ["@json-render/react@0.4.0", "", { "dependencies": { "@json-render/core": "0.4.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-OAXdWdOrAXHEFzpEF7xV+84D00JEmLMKlt5u0wc+C/P+q4q6TnpAWx2j28PBpDB2mpidkW9VnTUM+SCH9J8Lrw=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], "@next/env": ["@next/env@15.5.9", "", {}, "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg=="], @@ -459,6 +479,8 @@ "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.15.0", "", { "dependencies": { "@ast-grep/napi": "0.40.0", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.9.11", "cloudflare": "^4.4.1", "enquirer": "^2.4.1", "glob": "^12.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "next": "^14.2.35 || ~15.0.7 || ~15.1.11 || ~15.2.8 || ~15.3.8 || ~15.4.10 || ~15.5.9 || ^16.0.10", "wrangler": "^4.59.2" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-AZPaqk25XUBxtdkfjUZQBbY3ovifVLC4GgSRHuejqsIWfv8KjTRNFVdaCaaPmbLkrgymqxNhkbfJS5sD28AK/g=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.6.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg=="], "@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="], @@ -475,6 +497,8 @@ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="], + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="], "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="], @@ -509,6 +533,8 @@ "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + "@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="], + "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="], "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], @@ -521,6 +547,10 @@ "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="], + "@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="], + + "@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="], + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], @@ -551,10 +581,14 @@ "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="], + "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="], + "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], @@ -583,6 +617,20 @@ "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.15.0", "", {}, "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw=="], + "@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1" } }, "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA=="], + + "@shikijs/langs": ["@shikijs/langs@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ=="], + + "@shikijs/themes": ["@shikijs/themes@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g=="], + + "@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], "@smithy/abort-controller": ["@smithy/abort-controller@2.2.0", "", { "dependencies": { "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw=="], @@ -689,6 +737,8 @@ "@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], @@ -765,12 +815,18 @@ "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + "@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="], "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.8", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/http-assert": ["@types/http-assert@1.5.6", "", {}, "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw=="], "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], @@ -785,8 +841,12 @@ "@types/koa-compose": ["@types/koa-compose@3.2.9", "", { "dependencies": { "@types/koa": "*" } }, "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA=="], + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], @@ -803,6 +863,8 @@ "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/type-utils": "8.53.1", "@typescript-eslint/utils": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg=="], @@ -823,6 +885,8 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.2.0", "", {}, "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="], + "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], @@ -861,6 +925,8 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], + "@workos-inc/authkit-nextjs": ["@workos-inc/authkit-nextjs@2.13.0", "", { "dependencies": { "@workos-inc/node": "^7.72.0", "iron-session": "^8.0.1", "jose": "^5.2.3", "path-to-regexp": "^6.2.2" }, "peerDependencies": { "next": "^13.5.9 || ^14.2.26 || ^15.2.3 || ^16", "react": "^18.0 || ^19.0.0", "react-dom": "^18.0 || ^19.0.0" } }, "sha512-ppxzhfakPumHPPggYSROaAlgxfS7viFMPmWPG76Tp6Rh9G7YqkBSp7xtvMtM6gXOFFMvvEJRcKEta6YHeercTQ=="], "@workos-inc/node": ["@workos-inc/node@8.1.0", "", { "dependencies": { "iron-webcrypto": "^2.0.0", "jose": "~6.1.0" } }, "sha512-Ep2QSP43y4ZdJIOuL4Hjaq5f0u8Z0qZe7QWzrrBV6cHc/kcicDBcB0AanMP6eB9x3x6FaHfevLbkbjPF4+TCYQ=="], @@ -875,6 +941,8 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + "ai": ["ai@6.0.72", "", { "dependencies": { "@ai-sdk/gateway": "3.0.35", "@ai-sdk/provider": "3.0.7", "@ai-sdk/provider-utils": "4.0.13", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-D3TzDX6LzYL8qwi1A0rLnmuUexqDcCu4LSg77hcDHsqNRkaGspGItkz1U3RnN3ojv31XQYI9VmoWpkj44uvIUA=="], + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], @@ -921,6 +989,8 @@ "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], @@ -951,8 +1021,18 @@ "caniuse-lite": ["caniuse-lite@1.0.30001765", "", {}, "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], @@ -971,6 +1051,8 @@ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], @@ -1025,6 +1107,8 @@ "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], @@ -1035,10 +1119,14 @@ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], @@ -1067,6 +1155,8 @@ "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], @@ -1131,6 +1221,8 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], @@ -1139,10 +1231,14 @@ "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="], @@ -1183,6 +1279,8 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "framer-motion": ["framer-motion@11.18.2", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="], + "frappe-gantt": ["frappe-gantt@1.0.4", "", {}, "sha512-N94OP9ZiapaG5nzgCeZdxsKP8HD5aLVlH5sEHxSNZQnNKQ4BOn2l46HUD+KIE0LpYIterP7gIrFfkLNRuK0npQ=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -1243,6 +1341,16 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], @@ -1263,6 +1371,8 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + "input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="], "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], @@ -1275,6 +1385,10 @@ "iron-webcrypto": ["iron-webcrypto@2.0.0", "", { "dependencies": { "uint8array-extras": "^1.5.0" } }, "sha512-rtffZKDUHciZElM8mjFCufBC7nVhCxHYyWHESqs89OioEDz4parOofd8/uhrejh/INhQFfYQfByS22LlezR9sQ=="], + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], @@ -1293,6 +1407,8 @@ "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], @@ -1303,6 +1419,8 @@ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], @@ -1311,6 +1429,8 @@ "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], @@ -1351,6 +1471,8 @@ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], @@ -1401,16 +1523,50 @@ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], - "lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="], + "lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], @@ -1419,6 +1575,62 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -1439,9 +1651,13 @@ "mnemonist": ["mnemonist@0.38.3", "", { "dependencies": { "obliterator": "^1.6.1" } }, "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw=="], + "motion-dom": ["motion-dom@11.18.1", "", { "dependencies": { "motion-utils": "^11.18.1" } }, "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw=="], + + "motion-utils": ["motion-utils@11.18.1", "", {}, "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="], @@ -1485,6 +1701,8 @@ "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + "oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], @@ -1497,6 +1715,8 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -1523,6 +1743,8 @@ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -1535,6 +1757,8 @@ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], @@ -1549,6 +1773,8 @@ "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], @@ -1567,8 +1793,24 @@ "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + "regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="], + + "regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "remeda": ["remeda@2.33.5", "", {}, "sha512-FqmpPA9i9T5EGcqgyHf9kHjefnyCZM1M3kSdZjPk1j2StGNoJyoYp0807RYcjNkQ1UpsEQa5qzgsjLY4vYtT8g=="], + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -1611,6 +1853,8 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], @@ -1629,6 +1873,8 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], @@ -1651,6 +1897,8 @@ "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1663,6 +1911,10 @@ "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -1687,6 +1939,10 @@ "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], "ts-tqdm": ["ts-tqdm@0.8.6", "", {}, "sha512-3X3M1PZcHtgQbnwizL+xU8CAgbYbeLHrrDwL9xxcZZrV5J+e7loJm1XrXozHjSkl44J0Zg0SgA8rXbh83kCkcQ=="], @@ -1723,6 +1979,18 @@ "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], @@ -1743,6 +2011,10 @@ "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], @@ -1793,6 +2065,8 @@ "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug=="], @@ -2333,6 +2607,8 @@ "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-form/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], + "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -2349,6 +2625,8 @@ "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@radix-ui/react-toolbar/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@smithy/chunked-blob-reader-native/@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], @@ -2471,14 +2749,32 @@ "is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "miniflare/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "radix-ui/@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="], + + "radix-ui/@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], + + "radix-ui/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], + + "radix-ui/@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="], + + "radix-ui/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], + + "radix-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -3005,6 +3301,8 @@ "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], diff --git a/docs/spec.json b/docs/spec.json new file mode 100755 index 0000000..f6207ee --- /dev/null +++ b/docs/spec.json @@ -0,0 +1,2725 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "meta": { + "title": "COMPASS Phase One Implementation Specification", + "version": "1.0.0", + "created": "2026-02-05", + "lastUpdated": "2026-02-05", + "status": "approved", + "client": "High-Performance Structures (HPS)", + "projectGoal": "Replace Buildertrend with COMPASS - Phase One delivers critical features to enable migration", + "totalEstimatedWeeks": 10, + "techStack": { + "framework": "Next.js 15.5 with App Router", + "runtime": "Cloudflare Workers via @opennextjs/cloudflare", + "database": "Cloudflare D1 (SQLite) with Drizzle ORM", + "auth": "WorkOS AuthKit", + "email": "Resend", + "storage": "Google Drive API v3 (service account)", + "offline": "Workbox + Dexie.js (IndexedDB)", + "ui": "shadcn/ui + Tailwind CSS v4 + React 19", + "validation": "Zod 4.x", + "forms": "React Hook Form 7.x" + } + }, + "features": [ + { + "id": "F001", + "name": "Three-Tier User System", + "priority": "P0", + "status": "planned", + "estimatedDays": 8, + "sprint": 1, + "dependencies": [], + "blocksFeatures": ["F004", "F005", "F006"], + "description": "Extend the user management system to support three distinct user tiers: Internal Users, Subcontractors/Suppliers, and Clients. Each tier has different access levels, capabilities, and organizational structures.", + "businessValue": "Enables proper access control and role-based routing for notifications, bid packages, and schedule items. Critical for multi-stakeholder project management.", + "userStories": [ + { + "id": "US001-1", + "role": "Admin", + "action": "create and manage internal users with specific roles", + "benefit": "I can assign appropriate permissions to office staff, field workers, and other internal team members", + "acceptanceCriteria": [ + "Admin can create new internal users with roles: super_admin, office_admin, field_admin, field", + "Admin can edit user roles and deactivate users", + "Role changes take effect immediately", + "Audit log captures all user management actions" + ] + }, + { + "id": "US001-2", + "role": "Admin", + "action": "manage subcontractor companies with multiple contacts", + "benefit": "I can organize vendor contacts by their function and route communications appropriately", + "acceptanceCriteria": [ + "Admin can add multiple contacts per vendor company", + "Each contact has a functional role: estimator, scheduler, billing, or sales", + "Contacts can optionally be given login accounts", + "Contact list is searchable and filterable by role" + ] + }, + { + "id": "US001-3", + "role": "Admin", + "action": "add multiple client accounts to a single project", + "benefit": "all stakeholders on the client side can access project information with appropriate permissions", + "acceptanceCriteria": [ + "Multiple client users can be assigned to one project", + "Each client user has their own login credentials", + "Client users only see projects they are assigned to", + "Client permissions are read-only by default" + ] + }, + { + "id": "US001-4", + "role": "Subcontractor", + "action": "log in and view my assigned tasks and bid packages", + "benefit": "I can stay informed about upcoming work and respond to bid requests", + "acceptanceCriteria": [ + "Subcontractor users see only projects they are assigned to", + "Estimators see bid packages, schedulers see schedule items", + "Subcontractors can update task status for their assigned work", + "Subcontractors can submit daily logs" + ] + } + ], + "technicalApproach": { + "overview": "Extend existing users table with userType discriminator. Create new vendor_contacts table for multi-contact vendor management. Leverage existing projectMembers table for client-project assignments.", + "schemaChanges": [ + { + "table": "users", + "operation": "ALTER", + "changes": [ + { + "column": "user_type", + "type": "TEXT", + "default": "internal", + "values": ["internal", "subcontractor", "client"], + "description": "Discriminator for three-tier user system" + } + ] + }, + { + "table": "vendor_contacts", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "vendor_id", "type": "TEXT", "references": "vendors.id", "onDelete": "CASCADE" }, + { "name": "name", "type": "TEXT", "notNull": true }, + { "name": "email", "type": "TEXT" }, + { "name": "phone", "type": "TEXT" }, + { "name": "functional_role", "type": "TEXT", "notNull": true, "values": ["estimator", "scheduler", "billing", "sales"] }, + { "name": "is_primary", "type": "BOOLEAN", "default": false }, + { "name": "has_login_account", "type": "BOOLEAN", "default": false }, + { "name": "user_id", "type": "TEXT", "references": "users.id", "nullable": true }, + { "name": "notes", "type": "TEXT" }, + { "name": "created_at", "type": "TEXT", "notNull": true }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_vendor_contacts_vendor", "columns": ["vendor_id"] }, + { "name": "idx_vendor_contacts_role", "columns": ["functional_role"] }, + { "name": "idx_vendor_contacts_user", "columns": ["user_id"] } + ] + }, + { + "table": "user_invitations", + "operation": "ALTER", + "changes": [ + { + "column": "user_type", + "type": "TEXT", + "default": "internal", + "description": "Specifies what type of user is being invited" + }, + { + "column": "vendor_contact_id", + "type": "TEXT", + "references": "vendor_contacts.id", + "nullable": true, + "description": "Links invitation to vendor contact if inviting a subcontractor" + } + ] + } + ], + "permissionsChanges": { + "file": "src/lib/permissions.ts", + "changes": [ + "Add userType-aware permission checks", + "Subcontractor role with limited project access", + "Client role inherits from existing client permissions", + "Role-based resource filtering (estimators see bid packages, schedulers see schedule)" + ] + }, + "apiEndpoints": [ + { + "method": "GET", + "path": "/api/vendors/[id]/contacts", + "description": "List all contacts for a vendor", + "auth": "office, admin" + }, + { + "method": "POST", + "path": "/api/vendors/[id]/contacts", + "description": "Create a new vendor contact", + "auth": "office, admin" + }, + { + "method": "PUT", + "path": "/api/vendor-contacts/[id]", + "description": "Update a vendor contact", + "auth": "office, admin" + }, + { + "method": "DELETE", + "path": "/api/vendor-contacts/[id]", + "description": "Delete a vendor contact", + "auth": "admin" + }, + { + "method": "POST", + "path": "/api/vendor-contacts/[id]/invite", + "description": "Send login invitation to vendor contact", + "auth": "admin" + } + ], + "components": [ + { + "name": "VendorContactsTable", + "path": "src/components/vendors/vendor-contacts-table.tsx", + "description": "Data table showing all contacts for a vendor with role badges" + }, + { + "name": "VendorContactDialog", + "path": "src/components/vendors/vendor-contact-dialog.tsx", + "description": "Modal for creating/editing vendor contacts" + }, + { + "name": "InviteSubcontractorDialog", + "path": "src/components/vendors/invite-subcontractor-dialog.tsx", + "description": "Modal for inviting vendor contact as a user" + }, + { + "name": "UserTypeFilter", + "path": "src/components/people/user-type-filter.tsx", + "description": "Filter component for people list by user type" + } + ] + }, + "testCases": [ + { + "id": "TC001-1", + "type": "unit", + "description": "Permission check correctly identifies user type", + "steps": ["Create users of each type", "Verify permission checks return correct values"] + }, + { + "id": "TC001-2", + "type": "integration", + "description": "Vendor contact CRUD operations work correctly", + "steps": ["Create vendor contact", "Update contact role", "Delete contact", "Verify cascade behavior"] + }, + { + "id": "TC001-3", + "type": "e2e", + "description": "Subcontractor invitation flow", + "steps": ["Admin creates vendor contact", "Admin sends invitation", "Contact receives email", "Contact creates account", "Contact logs in and sees limited view"] + } + ] + }, + { + "id": "F002", + "name": "PWA Infrastructure and Offline Foundation", + "priority": "P0", + "status": "planned", + "estimatedDays": 10, + "sprint": 1, + "dependencies": [], + "blocksFeatures": ["F003"], + "description": "Establish Progressive Web App infrastructure including service worker, IndexedDB schema, and offline detection. This foundation enables full offline CRUD capabilities for the application.", + "businessValue": "Field workers need to access and update project information from job sites with poor or no connectivity. Offline support is critical for daily operations and was cited as a major Buildertrend limitation.", + "userStories": [ + { + "id": "US002-1", + "role": "Field Worker", + "action": "install COMPASS as an app on my phone", + "benefit": "I can access it quickly without opening a browser", + "acceptanceCriteria": [ + "Web app manifest enables 'Add to Home Screen' prompt", + "App icon and splash screen display correctly", + "App launches in standalone mode without browser chrome", + "App works on iOS Safari and Android Chrome" + ] + }, + { + "id": "US002-2", + "role": "Field Worker", + "action": "see a clear indicator when I'm offline", + "benefit": "I know whether my changes are being saved locally or synced to the server", + "acceptanceCriteria": [ + "Offline indicator appears in header when connection lost", + "Indicator shows number of pending changes", + "Toast notification when connection restored", + "Sync progress indicator during background sync" + ] + }, + { + "id": "US002-3", + "role": "Field Worker", + "action": "continue working when I lose internet connection", + "benefit": "my work isn't interrupted by poor cell coverage on job sites", + "acceptanceCriteria": [ + "All previously loaded data available offline", + "Can create new daily logs offline", + "Can update task status offline", + "Changes sync automatically when online" + ] + }, + { + "id": "US002-4", + "role": "System", + "action": "resolve conflicts when the same record is edited online and offline", + "benefit": "data integrity is maintained without user intervention", + "acceptanceCriteria": [ + "Last-write-wins strategy for simple conflicts", + "User prompted for manual resolution on complex conflicts", + "Conflict history logged for audit", + "No data loss during conflict resolution" + ] + } + ], + "technicalApproach": { + "overview": "Implement PWA using Workbox for service worker management and Dexie.js for IndexedDB abstraction. Create a sync queue system that tracks pending changes and reconciles with server on reconnection.", + "serviceWorker": { + "tool": "Workbox 7.x via next-pwa or manual configuration", + "strategies": [ + { + "route": "/api/*", + "strategy": "NetworkFirst", + "fallback": "cached response or offline indicator" + }, + { + "route": "/_next/static/*", + "strategy": "CacheFirst", + "expiration": "30 days" + }, + { + "route": "/dashboard/*", + "strategy": "StaleWhileRevalidate", + "description": "Serve cached page immediately, update in background" + } + ], + "backgroundSync": { + "queueName": "compass-sync-queue", + "maxRetries": 3, + "retryDelay": "exponential backoff starting at 5 minutes" + } + }, + "indexedDBSchema": { + "library": "Dexie.js 4.x", + "databaseName": "compass-offline", + "version": 1, + "stores": [ + { + "name": "projects", + "keyPath": "id", + "indexes": ["status", "updatedAt", "syncStatus"], + "description": "Cached project records" + }, + { + "name": "scheduleTasks", + "keyPath": "id", + "indexes": ["projectId", "status", "syncStatus"], + "description": "Cached schedule tasks" + }, + { + "name": "dailyLogs", + "keyPath": "id", + "indexes": ["projectId", "date", "syncStatus"], + "description": "Daily logs including offline-created ones" + }, + { + "name": "syncQueue", + "keyPath": "id", + "indexes": ["entityType", "entityId", "createdAt", "status"], + "description": "Queue of pending sync operations" + }, + { + "name": "syncConflicts", + "keyPath": "id", + "indexes": ["entityType", "entityId", "resolvedAt"], + "description": "Conflicts requiring manual resolution" + }, + { + "name": "offlinePhotos", + "keyPath": "id", + "indexes": ["dailyLogId", "uploadStatus"], + "description": "Photos captured offline pending upload" + } + ] + }, + "syncQueue": { + "operations": ["CREATE", "UPDATE", "DELETE"], + "queueItem": { + "id": "uuid", + "entityType": "dailyLog | scheduleTask | ...", + "entityId": "string", + "operation": "CREATE | UPDATE | DELETE", + "payload": "JSON serialized entity", + "createdAt": "ISO timestamp", + "attemptCount": "number", + "lastAttemptAt": "ISO timestamp | null", + "status": "pending | syncing | synced | failed | conflict", + "errorMessage": "string | null" + }, + "syncProcess": [ + "1. Detect online status change", + "2. Fetch pending queue items ordered by createdAt", + "3. For each item, attempt server sync", + "4. On success: mark synced, update local record with server response", + "5. On 409 Conflict: move to conflicts table for resolution", + "6. On network error: increment attemptCount, retry with backoff", + "7. Emit sync completion event for UI update" + ] + }, + "conflictResolution": { + "strategy": "last-write-wins with manual override option", + "autoResolve": [ + "Server version newer and local version unchanged since fetch", + "Local version is CREATE and server has no record" + ], + "manualResolve": [ + "Both local and server modified since last sync", + "DELETE conflicts (local deleted, server modified)" + ], + "conflictUI": { + "component": "SyncConflictDialog", + "showDiff": true, + "options": ["Keep mine", "Keep server", "Merge (manual edit)"] + } + }, + "files": [ + { + "path": "src/lib/offline/db.ts", + "description": "Dexie database instance and schema definition" + }, + { + "path": "src/lib/offline/sync-queue.ts", + "description": "Sync queue management functions" + }, + { + "path": "src/lib/offline/sync-service.ts", + "description": "Background sync orchestration" + }, + { + "path": "src/lib/offline/conflict-resolver.ts", + "description": "Conflict detection and resolution logic" + }, + { + "path": "src/hooks/use-online-status.ts", + "description": "React hook for online/offline detection" + }, + { + "path": "src/hooks/use-sync-status.ts", + "description": "React hook for sync queue status" + }, + { + "path": "src/components/offline/offline-indicator.tsx", + "description": "Header component showing offline status" + }, + { + "path": "src/components/offline/sync-status-badge.tsx", + "description": "Badge showing sync status for individual records" + }, + { + "path": "src/components/offline/sync-conflict-dialog.tsx", + "description": "Modal for manual conflict resolution" + }, + { + "path": "public/manifest.json", + "description": "PWA manifest file" + }, + { + "path": "src/app/sw.ts", + "description": "Service worker entry point" + } + ], + "manifest": { + "name": "COMPASS", + "short_name": "COMPASS", + "description": "Construction Project Management System", + "start_url": "/dashboard", + "display": "standalone", + "background_color": "#0a0a0a", + "theme_color": "#0a0a0a", + "icons": [ + { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, + { "src": "/icons/icon-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] + } + }, + "testCases": [ + { + "id": "TC002-1", + "type": "unit", + "description": "Sync queue correctly orders operations", + "steps": ["Add multiple operations", "Verify FIFO ordering", "Verify operation deduplication"] + }, + { + "id": "TC002-2", + "type": "unit", + "description": "Conflict detection identifies all conflict types", + "steps": ["Create conflicting local/server states", "Verify conflict detection", "Verify conflict categorization"] + }, + { + "id": "TC002-3", + "type": "integration", + "description": "Background sync processes queue on reconnection", + "steps": ["Queue operations while offline", "Simulate reconnection", "Verify all operations synced"] + }, + { + "id": "TC002-4", + "type": "e2e", + "description": "Full offline workflow", + "steps": ["Load app online", "Go offline", "Create/edit records", "Go online", "Verify sync completes"] + } + ] + }, + { + "id": "F003", + "name": "Daily Logging System", + "priority": "P0", + "status": "planned", + "estimatedDays": 8, + "sprint": 2, + "dependencies": ["F002"], + "blocksFeatures": [], + "description": "Complete daily logging system allowing field workers, office staff, and subcontractors to create detailed daily logs from any device. Supports offline creation with photo attachments and automatic weather data.", + "businessValue": "Daily logs are essential for project documentation, liability protection, and client communication. Field workers need to log from job sites with unreliable connectivity. This directly replaces Buildertrend's daily log feature.", + "userStories": [ + { + "id": "US003-1", + "role": "Field Worker", + "action": "create a daily log from my phone while on site", + "benefit": "I can document work completed, issues, and conditions while the details are fresh", + "acceptanceCriteria": [ + "Mobile-optimized form with large touch targets", + "Can create log while offline", + "Log saved locally and synced when online", + "Confirmation shown when log saved" + ] + }, + { + "id": "US003-2", + "role": "Field Worker", + "action": "attach photos to my daily log", + "benefit": "I can provide visual documentation of work progress and issues", + "acceptanceCriteria": [ + "Can capture photos directly from camera", + "Can select existing photos from gallery", + "Multiple photos per log supported", + "Photos compressed for upload", + "Photos queue for upload when offline" + ] + }, + { + "id": "US003-3", + "role": "Field Worker", + "action": "have weather automatically filled in", + "benefit": "I save time and ensure accurate weather documentation", + "acceptanceCriteria": [ + "Weather fetched based on project location when online", + "Temperature, conditions, and precipitation displayed", + "Weather can be manually overridden", + "Weather data cached for offline use" + ] + }, + { + "id": "US003-4", + "role": "PM", + "action": "view all daily logs for a project in chronological order", + "benefit": "I can review project progress and identify issues", + "acceptanceCriteria": [ + "Timeline view showing all logs", + "Filter by date range, author, or tags", + "Photos displayed inline", + "Export to PDF option" + ] + }, + { + "id": "US003-5", + "role": "Subcontractor", + "action": "submit a daily log for work my crew performed", + "benefit": "I can document our work and hours for billing and coordination", + "acceptanceCriteria": [ + "Subcontractor logs tagged with company name", + "Can log crew members present and hours", + "Logs visible to office staff and admins", + "Option to share specific logs with client" + ] + } + ], + "technicalApproach": { + "overview": "Build on PWA infrastructure to provide full offline daily log creation. Use IndexedDB for local storage, queue photos for background upload, and fetch weather from OpenWeather API.", + "schemaChanges": [ + { + "table": "daily_logs", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "project_id", "type": "TEXT", "references": "projects.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "author_id", "type": "TEXT", "references": "users.id", "notNull": true }, + { "name": "log_date", "type": "TEXT", "notNull": true, "description": "Date of the log (YYYY-MM-DD)" }, + { "name": "weather_temp_f", "type": "INTEGER", "description": "Temperature in Fahrenheit" }, + { "name": "weather_conditions", "type": "TEXT", "description": "e.g., Sunny, Cloudy, Rain" }, + { "name": "weather_precipitation", "type": "TEXT", "description": "e.g., None, Light Rain, Heavy Rain" }, + { "name": "weather_source", "type": "TEXT", "default": "auto", "values": ["auto", "manual"] }, + { "name": "work_completed", "type": "TEXT", "notNull": true, "description": "Description of work done" }, + { "name": "issues", "type": "TEXT", "description": "Any issues or problems encountered" }, + { "name": "materials_used", "type": "TEXT", "description": "JSON array of materials" }, + { "name": "crew_present", "type": "TEXT", "description": "JSON array of crew member names/counts" }, + { "name": "hours_worked", "type": "REAL", "description": "Total crew hours" }, + { "name": "safety_incidents", "type": "TEXT", "description": "Any safety incidents to report" }, + { "name": "visitor_log", "type": "TEXT", "description": "Visitors to job site" }, + { "name": "notes", "type": "TEXT", "description": "Additional notes" }, + { "name": "is_client_visible", "type": "BOOLEAN", "default": false }, + { "name": "tags", "type": "TEXT", "description": "JSON array of tags" }, + { "name": "sync_status", "type": "TEXT", "default": "synced", "values": ["synced", "pending", "conflict"] }, + { "name": "created_at", "type": "TEXT", "notNull": true }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_daily_logs_project_date", "columns": ["project_id", "log_date"] }, + { "name": "idx_daily_logs_author", "columns": ["author_id"] }, + { "name": "idx_daily_logs_sync", "columns": ["sync_status"] } + ] + }, + { + "table": "daily_log_photos", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "daily_log_id", "type": "TEXT", "references": "daily_logs.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "file_name", "type": "TEXT", "notNull": true }, + { "name": "file_size", "type": "INTEGER" }, + { "name": "mime_type", "type": "TEXT" }, + { "name": "drive_file_id", "type": "TEXT", "description": "Google Drive file ID once uploaded" }, + { "name": "drive_url", "type": "TEXT", "description": "Google Drive view URL" }, + { "name": "thumbnail_url", "type": "TEXT" }, + { "name": "caption", "type": "TEXT" }, + { "name": "upload_status", "type": "TEXT", "default": "pending", "values": ["pending", "uploading", "uploaded", "failed"] }, + { "name": "sort_order", "type": "INTEGER", "default": 0 }, + { "name": "created_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_daily_log_photos_log", "columns": ["daily_log_id"] }, + { "name": "idx_daily_log_photos_upload", "columns": ["upload_status"] } + ] + }, + { + "table": "daily_log_task_links", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "daily_log_id", "type": "TEXT", "references": "daily_logs.id", "onDelete": "CASCADE" }, + { "name": "schedule_task_id", "type": "TEXT", "references": "schedule_tasks.id", "onDelete": "CASCADE" }, + { "name": "notes", "type": "TEXT" } + ], + "description": "Links daily logs to specific schedule tasks worked on" + } + ], + "weatherIntegration": { + "provider": "OpenWeather API", + "endpoint": "https://api.openweathermap.org/data/2.5/weather", + "parameters": ["lat", "lon", "units=imperial", "appid"], + "caching": "Cache weather by project location for 1 hour", + "fallback": "Manual entry when offline or API unavailable" + }, + "photoHandling": { + "capture": "Use native file input with capture=camera on mobile", + "compression": "Compress to max 1920px width, 80% JPEG quality", + "localStorage": "Store blob in IndexedDB while offline", + "upload": "Background upload to Google Drive Daily Logs folder", + "thumbnail": "Generate 200px thumbnail for list views" + }, + "apiEndpoints": [ + { + "method": "GET", + "path": "/api/projects/[id]/daily-logs", + "description": "List daily logs for a project", + "params": ["startDate", "endDate", "authorId"], + "auth": "field, office, admin, client (if visible)" + }, + { + "method": "GET", + "path": "/api/daily-logs/[id]", + "description": "Get single daily log with photos", + "auth": "field, office, admin, client (if visible)" + }, + { + "method": "POST", + "path": "/api/projects/[id]/daily-logs", + "description": "Create a new daily log", + "auth": "field, office, admin, subcontractor" + }, + { + "method": "PUT", + "path": "/api/daily-logs/[id]", + "description": "Update a daily log", + "auth": "author, office, admin" + }, + { + "method": "DELETE", + "path": "/api/daily-logs/[id]", + "description": "Delete a daily log", + "auth": "admin" + }, + { + "method": "POST", + "path": "/api/daily-logs/[id]/photos", + "description": "Upload photo to daily log", + "auth": "field, office, admin, subcontractor" + }, + { + "method": "GET", + "path": "/api/weather", + "description": "Get current weather for location", + "params": ["lat", "lon"], + "auth": "any authenticated" + } + ], + "components": [ + { + "name": "DailyLogForm", + "path": "src/components/daily-logs/daily-log-form.tsx", + "description": "Mobile-optimized form for creating/editing daily logs" + }, + { + "name": "DailyLogTimeline", + "path": "src/components/daily-logs/daily-log-timeline.tsx", + "description": "Chronological list of daily logs for a project" + }, + { + "name": "DailyLogCard", + "path": "src/components/daily-logs/daily-log-card.tsx", + "description": "Card component displaying log summary" + }, + { + "name": "DailyLogDetail", + "path": "src/components/daily-logs/daily-log-detail.tsx", + "description": "Full detail view of a daily log" + }, + { + "name": "PhotoCapture", + "path": "src/components/daily-logs/photo-capture.tsx", + "description": "Camera/gallery photo input with preview" + }, + { + "name": "PhotoGallery", + "path": "src/components/daily-logs/photo-gallery.tsx", + "description": "Grid of photos with lightbox viewer" + }, + { + "name": "WeatherWidget", + "path": "src/components/daily-logs/weather-widget.tsx", + "description": "Displays current weather with auto-refresh" + }, + { + "name": "TaskLinker", + "path": "src/components/daily-logs/task-linker.tsx", + "description": "Select schedule tasks worked on" + } + ], + "routes": [ + { + "path": "/dashboard/projects/[id]/daily-logs", + "description": "Daily log timeline for a project" + }, + { + "path": "/dashboard/projects/[id]/daily-logs/new", + "description": "Create new daily log" + }, + { + "path": "/dashboard/projects/[id]/daily-logs/[logId]", + "description": "View/edit single daily log" + } + ] + }, + "testCases": [ + { + "id": "TC003-1", + "type": "unit", + "description": "Daily log validation accepts valid data", + "steps": ["Submit valid log data", "Verify validation passes", "Verify log created"] + }, + { + "id": "TC003-2", + "type": "integration", + "description": "Photo upload to Google Drive works", + "steps": ["Create log with photo", "Verify photo uploaded to Drive", "Verify URL stored in database"] + }, + { + "id": "TC003-3", + "type": "integration", + "description": "Weather auto-fill from location", + "steps": ["Create log with project location", "Verify weather fetched", "Verify weather saved to log"] + }, + { + "id": "TC003-4", + "type": "e2e", + "description": "Offline daily log creation and sync", + "steps": ["Go offline", "Create daily log with photos", "Go online", "Verify log synced", "Verify photos uploaded"] + }, + { + "id": "TC003-5", + "type": "e2e", + "description": "Subcontractor can create daily log", + "steps": ["Login as subcontractor", "Navigate to assigned project", "Create daily log", "Verify log visible to PM"] + } + ] + }, + { + "id": "F004", + "name": "Google Drive Integration", + "priority": "P0", + "status": "planned", + "estimatedDays": 10, + "sprint": 1, + "dependencies": [], + "blocksFeatures": ["F003", "F005"], + "description": "Replace planned S3 storage with Google Drive integration using a service account. All project files stored in company Google Drive with automatic CSI folder structure creation.", + "businessValue": "HPS uses Google Workspace for all office apps. Integrating with their existing Google Drive eliminates the need for separate file storage and keeps all documents in familiar tools.", + "userStories": [ + { + "id": "US004-1", + "role": "PM", + "action": "browse project files within COMPASS", + "benefit": "I don't have to switch to Google Drive to find project documents", + "acceptanceCriteria": [ + "File browser shows real Drive contents", + "Can navigate folder hierarchy", + "File previews available for common types", + "Click file to open in Drive" + ] + }, + { + "id": "US004-2", + "role": "PM", + "action": "upload documents to the correct project folder", + "benefit": "files are automatically organized in the right location", + "acceptanceCriteria": [ + "Drag-and-drop upload supported", + "Can select destination folder", + "Upload progress shown", + "File appears in browser after upload" + ] + }, + { + "id": "US004-3", + "role": "System", + "action": "create CSI folder structure when a project is created", + "benefit": "all projects have consistent folder organization", + "acceptanceCriteria": [ + "50+ CSI folders created automatically", + "Folder structure matches HPS standard", + "Project folder named with project code", + "Provisioning status tracked" + ] + }, + { + "id": "US004-4", + "role": "Admin", + "action": "configure the root folder for COMPASS projects", + "benefit": "I can control where project folders are created", + "acceptanceCriteria": [ + "Settings page for Drive configuration", + "Can select root folder from Drive", + "Validation that service account has access", + "Folder ID stored in system settings" + ] + } + ], + "technicalApproach": { + "overview": "Use Google Drive API v3 with service account authentication. Service account credentials stored securely in environment variables. Existing file browser UI connected to Drive backend.", + "authentication": { + "type": "Service Account", + "setup": [ + "1. Create Google Cloud project", + "2. Enable Google Drive API", + "3. Create service account", + "4. Download JSON credentials", + "5. Share root folder with service account email", + "6. Store credentials in GOOGLE_SERVICE_ACCOUNT_KEY env var" + ], + "library": "googleapis npm package", + "scopes": ["https://www.googleapis.com/auth/drive"] + }, + "schemaChanges": [ + { + "table": "projects", + "operation": "ALTER", + "changes": [ + { + "column": "drive_folder_id", + "type": "TEXT", + "nullable": true, + "description": "Google Drive folder ID for this project" + }, + { + "column": "drive_folder_url", + "type": "TEXT", + "nullable": true, + "description": "Direct URL to project folder in Drive" + } + ] + }, + { + "table": "system_settings", + "operation": "CREATE", + "columns": [ + { "name": "key", "type": "TEXT", "primaryKey": true }, + { "name": "value", "type": "TEXT", "notNull": true }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "initialData": [ + { "key": "drive_root_folder_id", "value": "" }, + { "key": "drive_configured", "value": "false" } + ] + }, + { + "table": "documents", + "operation": "ALTER", + "changes": [ + { + "column": "drive_file_id", + "type": "TEXT", + "nullable": true, + "description": "Google Drive file ID" + }, + { + "column": "drive_url", + "type": "TEXT", + "nullable": true, + "description": "Direct link to file in Drive" + }, + { + "column": "drive_thumbnail_url", + "type": "TEXT", + "nullable": true, + "description": "Thumbnail URL for preview" + } + ] + } + ], + "driveService": { + "file": "src/lib/drive/drive-service.ts", + "methods": [ + { + "name": "listFiles", + "params": ["folderId: string", "pageToken?: string"], + "returns": "{ files: DriveFile[], nextPageToken?: string }", + "description": "List files and folders in a Drive folder" + }, + { + "name": "getFile", + "params": ["fileId: string"], + "returns": "DriveFile", + "description": "Get file metadata" + }, + { + "name": "createFolder", + "params": ["name: string", "parentId: string"], + "returns": "DriveFile", + "description": "Create a new folder" + }, + { + "name": "uploadFile", + "params": ["file: Buffer", "name: string", "mimeType: string", "parentId: string"], + "returns": "DriveFile", + "description": "Upload a file to Drive" + }, + { + "name": "deleteFile", + "params": ["fileId: string"], + "returns": "void", + "description": "Move file to trash" + }, + { + "name": "moveFile", + "params": ["fileId: string", "newParentId: string"], + "returns": "DriveFile", + "description": "Move file to different folder" + }, + { + "name": "renameFile", + "params": ["fileId: string", "newName: string"], + "returns": "DriveFile", + "description": "Rename a file" + }, + { + "name": "getDownloadUrl", + "params": ["fileId: string"], + "returns": "string", + "description": "Get direct download URL" + }, + { + "name": "createCSIFolderStructure", + "params": ["projectFolderId: string"], + "returns": "void", + "description": "Create 50+ CSI division folders" + } + ] + }, + "folderStructure": { + "source": "hps-structures/directories sample project", + "template": [ + "Architectural", + "Architectural/3D Views", + "Architectural/Archive", + "Communications", + "Contracts", + "Estimates", + "Estimates/Preconstruction Estimates", + "Meeting Notes", + "Property Information", + "Spec and Finishes", + "Spec and Finishes/Cabinetry", + "Spec and Finishes/Electrical Needs", + "Spec and Finishes/Plumbing Fixtures", + "Testing", + "Utilities", + "___BID SET", + "___BID SET/BidPackages" + ], + "note": "Full CSI structure to be defined from HPS requirements" + }, + "apiEndpoints": [ + { + "method": "GET", + "path": "/api/drive/folders/[folderId]", + "description": "List contents of a folder", + "params": ["pageToken"], + "auth": "field, office, admin" + }, + { + "method": "POST", + "path": "/api/drive/folders", + "description": "Create a new folder", + "body": ["name", "parentId"], + "auth": "office, admin" + }, + { + "method": "POST", + "path": "/api/drive/upload", + "description": "Upload a file", + "body": ["file (multipart)", "folderId"], + "auth": "field, office, admin" + }, + { + "method": "DELETE", + "path": "/api/drive/files/[fileId]", + "description": "Delete a file", + "auth": "office, admin" + }, + { + "method": "PUT", + "path": "/api/drive/files/[fileId]/move", + "description": "Move a file", + "body": ["newParentId"], + "auth": "office, admin" + }, + { + "method": "PUT", + "path": "/api/drive/files/[fileId]/rename", + "description": "Rename a file", + "body": ["newName"], + "auth": "office, admin" + }, + { + "method": "GET", + "path": "/api/settings/drive", + "description": "Get Drive configuration", + "auth": "admin" + }, + { + "method": "PUT", + "path": "/api/settings/drive", + "description": "Update Drive configuration", + "body": ["rootFolderId"], + "auth": "admin" + } + ], + "componentChanges": [ + { + "component": "FileBrowser", + "path": "src/components/files/file-browser.tsx", + "changes": [ + "Replace mock data with Drive API calls", + "Add loading states", + "Handle pagination with pageToken" + ] + }, + { + "component": "FileDropZone", + "path": "src/components/files/file-drop-zone.tsx", + "changes": [ + "Connect to Drive upload endpoint", + "Show upload progress", + "Handle upload errors" + ] + }, + { + "component": "DriveSettingsForm", + "path": "src/components/settings/drive-settings-form.tsx", + "description": "New component for configuring Drive root folder" + } + ], + "provisioningJob": { + "trigger": "Project creation", + "queue": "Cloudflare Queues", + "steps": [ + "1. Create project folder in root: {PROJECT_CODE} - {PROJECT_NAME}", + "2. Create CSI folder structure from template", + "3. Update project record with drive_folder_id", + "4. Update provisioning status to complete", + "5. Send notification email" + ], + "errorHandling": "Retry up to 3 times, then mark as failed and notify admin" + } + }, + "testCases": [ + { + "id": "TC004-1", + "type": "unit", + "description": "Drive service authenticates with service account", + "steps": ["Initialize drive service", "Verify authentication succeeds", "Verify can list root folder"] + }, + { + "id": "TC004-2", + "type": "integration", + "description": "File upload creates file in Drive", + "steps": ["Upload test file", "Verify file exists in Drive", "Verify metadata correct"] + }, + { + "id": "TC004-3", + "type": "integration", + "description": "CSI folder creation completes", + "steps": ["Create project", "Trigger provisioning", "Verify all folders created", "Verify project updated"] + }, + { + "id": "TC004-4", + "type": "e2e", + "description": "File browser shows Drive contents", + "steps": ["Navigate to project files", "Verify folders displayed", "Upload new file", "Verify file appears"] + } + ] + }, + { + "id": "F005", + "name": "Bid Package System", + "priority": "P0", + "status": "planned", + "estimatedDays": 13, + "sprint": 3, + "dependencies": ["F001", "F004"], + "blocksFeatures": [], + "description": "Complete bid package system for creating and sending requests for bid to subcontractors. Includes customizable templates, auto-fill from project data, and role-based delivery ensuring estimators receive bid packages while schedulers receive only schedule items.", + "businessValue": "Streamlines the bidding process which is currently manual and time-consuming. Role-based routing ensures the right people get the right information, reducing confusion and improving response rates.", + "userStories": [ + { + "id": "US005-1", + "role": "PM", + "action": "create a bid package for a project", + "benefit": "I can solicit bids from subcontractors in a standardized format", + "acceptanceCriteria": [ + "Can select from bid package templates", + "Project data auto-fills (name, address, dates)", + "Can add scope items from schedule or manual entry", + "Can attach drawings from Google Drive", + "Can set bid due date" + ] + }, + { + "id": "US005-2", + "role": "PM", + "action": "send a bid package to selected vendors", + "benefit": "I can reach multiple potential subcontractors with one action", + "acceptanceCriteria": [ + "Can select vendors by trade/category", + "Only estimator contacts shown for selection", + "Email sent to all selected contacts", + "Delivery tracked per recipient" + ] + }, + { + "id": "US005-3", + "role": "PM", + "action": "track bid package responses", + "benefit": "I know which subs have responded and can follow up with non-responders", + "acceptanceCriteria": [ + "Response status per vendor: sent, viewed, responded, declined", + "Can record bid amounts received", + "Can add notes per vendor", + "Dashboard shows pending bids" + ] + }, + { + "id": "US005-4", + "role": "Admin", + "action": "create and manage bid package templates", + "benefit": "I can standardize our bid request format across projects", + "acceptanceCriteria": [ + "WYSIWYG template editor", + "Variable placeholders: {{project.name}}, {{project.address}}, etc.", + "Can create trade-specific templates (electrical, plumbing, etc.)", + "Templates versioned" + ] + }, + { + "id": "US005-5", + "role": "Subcontractor (Estimator)", + "action": "view bid packages sent to me", + "benefit": "I can review the scope and prepare my bid", + "acceptanceCriteria": [ + "See only bid packages sent to me", + "Can download attached drawings", + "Can mark as 'viewed'", + "Can submit bid response through portal" + ] + } + ], + "technicalApproach": { + "overview": "Build complete bid package management including templates, auto-fill, email delivery, and response tracking. Integration with vendor contacts for role-based routing. Store bid package documents in Google Drive.", + "schemaChanges": [ + { + "table": "bid_package_templates", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "name", "type": "TEXT", "notNull": true }, + { "name": "description", "type": "TEXT" }, + { "name": "trade_category", "type": "TEXT", "description": "e.g., Electrical, Plumbing, Roofing" }, + { "name": "content", "type": "TEXT", "notNull": true, "description": "HTML template with variable placeholders" }, + { "name": "default_scope_items", "type": "TEXT", "description": "JSON array of default scope items" }, + { "name": "is_active", "type": "BOOLEAN", "default": true }, + { "name": "version", "type": "INTEGER", "default": 1 }, + { "name": "created_by", "type": "TEXT", "references": "users.id" }, + { "name": "created_at", "type": "TEXT", "notNull": true }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_bid_templates_trade", "columns": ["trade_category"] } + ] + }, + { + "table": "bid_packages", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "project_id", "type": "TEXT", "references": "projects.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "template_id", "type": "TEXT", "references": "bid_package_templates.id" }, + { "name": "title", "type": "TEXT", "notNull": true }, + { "name": "description", "type": "TEXT" }, + { "name": "trade_category", "type": "TEXT" }, + { "name": "bid_due_date", "type": "TEXT", "notNull": true }, + { "name": "status", "type": "TEXT", "default": "draft", "values": ["draft", "sent", "closed", "awarded"] }, + { "name": "rendered_content", "type": "TEXT", "description": "HTML with variables replaced" }, + { "name": "drive_folder_id", "type": "TEXT", "description": "Folder for bid package attachments" }, + { "name": "notes", "type": "TEXT" }, + { "name": "created_by", "type": "TEXT", "references": "users.id" }, + { "name": "sent_at", "type": "TEXT" }, + { "name": "closed_at", "type": "TEXT" }, + { "name": "created_at", "type": "TEXT", "notNull": true }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_bid_packages_project", "columns": ["project_id"] }, + { "name": "idx_bid_packages_status", "columns": ["status"] }, + { "name": "idx_bid_packages_due", "columns": ["bid_due_date"] } + ] + }, + { + "table": "bid_package_scope_items", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "bid_package_id", "type": "TEXT", "references": "bid_packages.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "description", "type": "TEXT", "notNull": true }, + { "name": "quantity", "type": "TEXT" }, + { "name": "unit", "type": "TEXT" }, + { "name": "csi_code", "type": "TEXT" }, + { "name": "notes", "type": "TEXT" }, + { "name": "sort_order", "type": "INTEGER", "default": 0 } + ], + "indexes": [ + { "name": "idx_bid_scope_items_package", "columns": ["bid_package_id"] } + ] + }, + { + "table": "bid_package_attachments", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "bid_package_id", "type": "TEXT", "references": "bid_packages.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "file_name", "type": "TEXT", "notNull": true }, + { "name": "drive_file_id", "type": "TEXT", "notNull": true }, + { "name": "drive_url", "type": "TEXT" }, + { "name": "file_size", "type": "INTEGER" }, + { "name": "mime_type", "type": "TEXT" }, + { "name": "sort_order", "type": "INTEGER", "default": 0 }, + { "name": "created_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_bid_attachments_package", "columns": ["bid_package_id"] } + ] + }, + { + "table": "bid_package_recipients", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "bid_package_id", "type": "TEXT", "references": "bid_packages.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "vendor_id", "type": "TEXT", "references": "vendors.id", "notNull": true }, + { "name": "vendor_contact_id", "type": "TEXT", "references": "vendor_contacts.id", "notNull": true }, + { "name": "email", "type": "TEXT", "notNull": true }, + { "name": "sent_at", "type": "TEXT" }, + { "name": "viewed_at", "type": "TEXT" }, + { "name": "response_status", "type": "TEXT", "default": "pending", "values": ["pending", "sent", "viewed", "responded", "declined", "no_response"] }, + { "name": "bid_amount", "type": "REAL" }, + { "name": "response_notes", "type": "TEXT" }, + { "name": "response_date", "type": "TEXT" }, + { "name": "is_awarded", "type": "BOOLEAN", "default": false }, + { "name": "created_at", "type": "TEXT", "notNull": true }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_bid_recipients_package", "columns": ["bid_package_id"] }, + { "name": "idx_bid_recipients_vendor", "columns": ["vendor_id"] }, + { "name": "idx_bid_recipients_status", "columns": ["response_status"] } + ] + } + ], + "templateVariables": { + "description": "Variables that can be used in bid package templates, replaced at render time", + "variables": [ + { "key": "{{project.name}}", "source": "projects.name" }, + { "key": "{{project.code}}", "source": "projects.id or generated code" }, + { "key": "{{project.address}}", "source": "projects.address" }, + { "key": "{{project.clientName}}", "source": "projects.clientName" }, + { "key": "{{project.manager}}", "source": "projects.projectManager" }, + { "key": "{{bidPackage.title}}", "source": "bid_packages.title" }, + { "key": "{{bidPackage.dueDate}}", "source": "bid_packages.bid_due_date, formatted" }, + { "key": "{{bidPackage.scopeItems}}", "source": "Rendered list of scope items" }, + { "key": "{{company.name}}", "source": "System setting" }, + { "key": "{{company.address}}", "source": "System setting" }, + { "key": "{{company.phone}}", "source": "System setting" }, + { "key": "{{company.email}}", "source": "System setting" }, + { "key": "{{today}}", "source": "Current date, formatted" } + ] + }, + "autoFillSources": { + "description": "Data sources for auto-populating bid packages", + "sources": [ + { + "source": "Project record", + "fields": ["name", "address", "clientName", "projectManager"] + }, + { + "source": "Schedule tasks", + "fields": ["Tasks can be selected and converted to scope items"] + }, + { + "source": "Budget line items", + "fields": ["CSI items can be selected as scope"] + }, + { + "source": "Google Drive", + "fields": ["Drawings and specs from ___BID SET folder"] + } + ] + }, + "roleBasedRouting": { + "description": "Logic for ensuring correct contacts receive appropriate communications", + "rules": [ + { + "content": "Bid Package", + "targetRole": "estimator", + "logic": "Only vendor_contacts with functional_role='estimator' shown in recipient selection" + }, + { + "content": "Schedule Items", + "targetRole": "scheduler", + "logic": "Only vendor_contacts with functional_role='scheduler' receive schedule notifications" + }, + { + "content": "Invoices/Bills", + "targetRole": "billing", + "logic": "Only vendor_contacts with functional_role='billing' receive payment communications" + } + ] + }, + "emailDelivery": { + "provider": "Resend", + "template": "bid-package-invitation", + "content": { + "subject": "Request for Bid: {{project.name}} - {{bidPackage.title}}", + "body": [ + "Greeting with vendor name", + "Project overview", + "Scope summary", + "Due date", + "Link to view full bid package", + "List of attachments", + "Contact information" + ] + }, + "tracking": { + "sentAt": "Recorded when email sent", + "viewedAt": "Tracked via unique link click", + "viewLink": "/bid-packages/[id]/view?token=[unique_token]" + } + }, + "apiEndpoints": [ + { + "method": "GET", + "path": "/api/projects/[id]/bid-packages", + "description": "List bid packages for a project", + "auth": "office, admin" + }, + { + "method": "GET", + "path": "/api/bid-packages/[id]", + "description": "Get bid package details", + "auth": "office, admin, subcontractor (if recipient)" + }, + { + "method": "POST", + "path": "/api/projects/[id]/bid-packages", + "description": "Create new bid package", + "auth": "office, admin" + }, + { + "method": "PUT", + "path": "/api/bid-packages/[id]", + "description": "Update bid package", + "auth": "office, admin" + }, + { + "method": "DELETE", + "path": "/api/bid-packages/[id]", + "description": "Delete bid package (draft only)", + "auth": "admin" + }, + { + "method": "POST", + "path": "/api/bid-packages/[id]/send", + "description": "Send bid package to recipients", + "auth": "office, admin" + }, + { + "method": "POST", + "path": "/api/bid-packages/[id]/recipients", + "description": "Add recipients to bid package", + "body": ["vendorContactIds"], + "auth": "office, admin" + }, + { + "method": "PUT", + "path": "/api/bid-package-recipients/[id]", + "description": "Update recipient response", + "body": ["bidAmount", "responseNotes", "responseStatus"], + "auth": "office, admin" + }, + { + "method": "GET", + "path": "/api/bid-package-templates", + "description": "List bid package templates", + "auth": "office, admin" + }, + { + "method": "POST", + "path": "/api/bid-package-templates", + "description": "Create bid package template", + "auth": "admin" + }, + { + "method": "PUT", + "path": "/api/bid-package-templates/[id]", + "description": "Update bid package template", + "auth": "admin" + }, + { + "method": "GET", + "path": "/api/vendors/by-trade/[trade]", + "description": "Get vendors by trade with estimator contacts", + "auth": "office, admin" + }, + { + "method": "GET", + "path": "/bid-packages/[id]/view", + "description": "Public view page for recipients (with token)", + "auth": "token-based" + } + ], + "components": [ + { + "name": "BidPackageList", + "path": "src/components/bid-packages/bid-package-list.tsx", + "description": "Table of bid packages for a project" + }, + { + "name": "BidPackageForm", + "path": "src/components/bid-packages/bid-package-form.tsx", + "description": "Form for creating/editing bid packages" + }, + { + "name": "BidPackageDetail", + "path": "src/components/bid-packages/bid-package-detail.tsx", + "description": "Full bid package view with recipients" + }, + { + "name": "ScopeItemsEditor", + "path": "src/components/bid-packages/scope-items-editor.tsx", + "description": "Editable list of scope items" + }, + { + "name": "RecipientSelector", + "path": "src/components/bid-packages/recipient-selector.tsx", + "description": "Multi-select for vendors/contacts filtered by estimator role" + }, + { + "name": "RecipientStatusTable", + "path": "src/components/bid-packages/recipient-status-table.tsx", + "description": "Table showing delivery and response status" + }, + { + "name": "BidResponseForm", + "path": "src/components/bid-packages/bid-response-form.tsx", + "description": "Form for recording bid responses" + }, + { + "name": "TemplateEditor", + "path": "src/components/bid-packages/template-editor.tsx", + "description": "WYSIWYG editor for bid package templates" + }, + { + "name": "AttachmentPicker", + "path": "src/components/bid-packages/attachment-picker.tsx", + "description": "File picker for Drive attachments" + }, + { + "name": "BidPackagePublicView", + "path": "src/components/bid-packages/bid-package-public-view.tsx", + "description": "Public-facing view for subcontractors" + } + ], + "routes": [ + { + "path": "/dashboard/projects/[id]/bid-packages", + "description": "Bid packages list for a project" + }, + { + "path": "/dashboard/projects/[id]/bid-packages/new", + "description": "Create new bid package" + }, + { + "path": "/dashboard/projects/[id]/bid-packages/[bidId]", + "description": "View/edit bid package" + }, + { + "path": "/dashboard/settings/bid-templates", + "description": "Manage bid package templates" + }, + { + "path": "/bid-packages/[id]/view", + "description": "Public view for recipients (token auth)" + } + ] + }, + "testCases": [ + { + "id": "TC005-1", + "type": "unit", + "description": "Template variable replacement works correctly", + "steps": ["Create template with variables", "Render with project data", "Verify all variables replaced"] + }, + { + "id": "TC005-2", + "type": "integration", + "description": "Bid package email delivery", + "steps": ["Create bid package", "Add recipients", "Send package", "Verify emails sent via Resend"] + }, + { + "id": "TC005-3", + "type": "integration", + "description": "Role-based filtering shows only estimators", + "steps": ["Create vendor with multiple contacts", "Open recipient selector", "Verify only estimators shown"] + }, + { + "id": "TC005-4", + "type": "e2e", + "description": "Full bid package workflow", + "steps": ["Create package from template", "Add scope items", "Attach drawings", "Select recipients", "Send", "Record responses"] + }, + { + "id": "TC005-5", + "type": "e2e", + "description": "Subcontractor views bid package", + "steps": ["Send bid package to sub", "Sub clicks email link", "Verify view tracked", "Sub downloads attachments"] + } + ] + }, + { + "id": "F006", + "name": "Notifications System", + "priority": "P0", + "status": "planned", + "estimatedDays": 10, + "sprint": 3, + "dependencies": ["F001"], + "blocksFeatures": [], + "description": "Complete notification system with in-app notifications, email delivery, and time-driven scheduled notifications. Supports schedule assignment notifications, payment events, and bid package reminders.", + "businessValue": "Proactive notifications keep team members informed without requiring them to constantly check the app. Time-driven notifications for upcoming tasks and overdue items prevent things from falling through the cracks.", + "userStories": [ + { + "id": "US006-1", + "role": "User", + "action": "see my notifications in the app", + "benefit": "I stay informed about important events without checking email", + "acceptanceCriteria": [ + "Notification bell icon in header shows unread count", + "Dropdown shows recent notifications", + "Click notification to navigate to related item", + "Can mark as read or mark all as read" + ] + }, + { + "id": "US006-2", + "role": "User", + "action": "receive email notifications for important events", + "benefit": "I'm alerted even when not in the app", + "acceptanceCriteria": [ + "Email sent for configured event types", + "Email includes direct link to item", + "Can configure which events trigger email", + "Unsubscribe link in email" + ] + }, + { + "id": "US006-3", + "role": "Field Worker", + "action": "get notified when I'm assigned to a schedule item", + "benefit": "I know immediately when I have new work assigned", + "acceptanceCriteria": [ + "Notification created when task assignedTo changes to me", + "Email sent if preference enabled", + "Notification includes task details and dates" + ] + }, + { + "id": "US006-4", + "role": "PM", + "action": "receive automatic reminders for tasks due tomorrow", + "benefit": "I can proactively address upcoming deadlines", + "acceptanceCriteria": [ + "Daily job runs at configured time (e.g., 7am)", + "Finds all tasks due tomorrow", + "Sends notification to assignees and PM", + "Summary email with all upcoming tasks" + ] + }, + { + "id": "US006-5", + "role": "Subcontractor (Scheduler)", + "action": "get notified when schedule items are assigned to my company", + "benefit": "I can plan crew availability for upcoming work", + "acceptanceCriteria": [ + "Only scheduler contacts receive schedule notifications", + "Notification includes start date, duration, scope", + "Email with link to schedule view" + ] + }, + { + "id": "US006-6", + "role": "Admin", + "action": "get notified when payment events occur", + "benefit": "I can track financial activity without constant checking", + "acceptanceCriteria": [ + "Notification for: invoice paid, payment received, bill due", + "Includes amount and related entities", + "Links to financial detail view" + ] + } + ], + "technicalApproach": { + "overview": "Build notification service that creates notifications, delivers via email (Resend), and provides in-app notification center. Scheduled jobs via Cloudflare Queues for time-based notifications.", + "schemaChanges": [ + { + "table": "notifications", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "user_id", "type": "TEXT", "references": "users.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "type", "type": "TEXT", "notNull": true, "values": ["task_assigned", "task_due", "task_overdue", "bid_package_sent", "bid_response_received", "payment_received", "invoice_due", "daily_log_submitted", "project_status_changed", "mention", "system"] }, + { "name": "title", "type": "TEXT", "notNull": true }, + { "name": "message", "type": "TEXT", "notNull": true }, + { "name": "link", "type": "TEXT", "description": "URL to navigate to on click" }, + { "name": "entity_type", "type": "TEXT", "description": "Type of related entity" }, + { "name": "entity_id", "type": "TEXT", "description": "ID of related entity" }, + { "name": "is_read", "type": "BOOLEAN", "default": false }, + { "name": "read_at", "type": "TEXT" }, + { "name": "email_sent", "type": "BOOLEAN", "default": false }, + { "name": "email_sent_at", "type": "TEXT" }, + { "name": "created_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_notifications_user", "columns": ["user_id"] }, + { "name": "idx_notifications_unread", "columns": ["user_id", "is_read"] }, + { "name": "idx_notifications_type", "columns": ["type"] }, + { "name": "idx_notifications_entity", "columns": ["entity_type", "entity_id"] } + ] + }, + { + "table": "notification_preferences", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "user_id", "type": "TEXT", "references": "users.id", "onDelete": "CASCADE", "notNull": true, "unique": true }, + { "name": "email_enabled", "type": "BOOLEAN", "default": true }, + { "name": "email_task_assigned", "type": "BOOLEAN", "default": true }, + { "name": "email_task_due", "type": "BOOLEAN", "default": true }, + { "name": "email_task_overdue", "type": "BOOLEAN", "default": true }, + { "name": "email_bid_package", "type": "BOOLEAN", "default": true }, + { "name": "email_payment", "type": "BOOLEAN", "default": true }, + { "name": "email_daily_digest", "type": "BOOLEAN", "default": false }, + { "name": "digest_time", "type": "TEXT", "default": "07:00" }, + { "name": "quiet_hours_start", "type": "TEXT" }, + { "name": "quiet_hours_end", "type": "TEXT" }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_notification_prefs_user", "columns": ["user_id"] } + ] + }, + { + "table": "scheduled_notifications", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "type", "type": "TEXT", "notNull": true, "values": ["task_reminder", "bid_due_reminder", "invoice_due_reminder", "daily_digest"] }, + { "name": "scheduled_for", "type": "TEXT", "notNull": true }, + { "name": "entity_type", "type": "TEXT" }, + { "name": "entity_id", "type": "TEXT" }, + { "name": "target_user_id", "type": "TEXT", "references": "users.id" }, + { "name": "status", "type": "TEXT", "default": "pending", "values": ["pending", "processing", "sent", "failed", "cancelled"] }, + { "name": "processed_at", "type": "TEXT" }, + { "name": "error_message", "type": "TEXT" }, + { "name": "created_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_scheduled_notifications_time", "columns": ["scheduled_for", "status"] }, + { "name": "idx_scheduled_notifications_entity", "columns": ["entity_type", "entity_id"] } + ] + } + ], + "notificationService": { + "file": "src/lib/notifications/notification-service.ts", + "methods": [ + { + "name": "createNotification", + "params": ["userId", "type", "title", "message", "options?"], + "description": "Create notification and optionally send email based on preferences" + }, + { + "name": "createBulkNotifications", + "params": ["userIds[]", "type", "title", "message", "options?"], + "description": "Create notifications for multiple users" + }, + { + "name": "markAsRead", + "params": ["notificationId"], + "description": "Mark single notification as read" + }, + { + "name": "markAllAsRead", + "params": ["userId"], + "description": "Mark all notifications as read for user" + }, + { + "name": "getUnreadCount", + "params": ["userId"], + "returns": "number", + "description": "Get count of unread notifications" + }, + { + "name": "getUserNotifications", + "params": ["userId", "options?"], + "returns": "Notification[]", + "description": "Get paginated notifications for user" + }, + { + "name": "scheduleNotification", + "params": ["type", "scheduledFor", "entityType?", "entityId?", "targetUserId?"], + "description": "Schedule a future notification" + }, + { + "name": "cancelScheduledNotification", + "params": ["entityType", "entityId"], + "description": "Cancel scheduled notifications for an entity" + } + ] + }, + "emailTemplates": { + "provider": "Resend", + "templates": [ + { + "name": "task-assigned", + "subject": "You've been assigned to: {{taskTitle}}", + "variables": ["taskTitle", "projectName", "startDate", "link"] + }, + { + "name": "task-due-reminder", + "subject": "Reminder: {{taskTitle}} is due tomorrow", + "variables": ["taskTitle", "projectName", "dueDate", "link"] + }, + { + "name": "task-overdue", + "subject": "Overdue: {{taskTitle}} was due {{daysOverdue}} days ago", + "variables": ["taskTitle", "projectName", "dueDate", "daysOverdue", "link"] + }, + { + "name": "bid-package-invitation", + "subject": "Request for Bid: {{projectName}} - {{bidPackageTitle}}", + "variables": ["vendorName", "projectName", "bidPackageTitle", "dueDate", "link"] + }, + { + "name": "bid-response-received", + "subject": "Bid received from {{vendorName}} for {{projectName}}", + "variables": ["vendorName", "projectName", "bidAmount", "link"] + }, + { + "name": "payment-received", + "subject": "Payment received: ${{amount}} for {{projectName}}", + "variables": ["amount", "projectName", "payerName", "link"] + }, + { + "name": "daily-digest", + "subject": "COMPASS Daily Summary - {{date}}", + "variables": ["date", "tasksDueToday", "tasksOverdue", "pendingBids", "recentActivity"] + } + ] + }, + "scheduledJobs": { + "infrastructure": "Cloudflare Queues with cron triggers", + "jobs": [ + { + "name": "task-due-reminders", + "schedule": "0 7 * * *", + "description": "Daily at 7am: Find tasks due tomorrow, create notifications" + }, + { + "name": "task-overdue-alerts", + "schedule": "0 8 * * *", + "description": "Daily at 8am: Find overdue tasks, alert PMs" + }, + { + "name": "bid-due-reminders", + "schedule": "0 9 * * *", + "description": "Daily at 9am: Remind about bids due in 2 days" + }, + { + "name": "daily-digest", + "schedule": "0 6 * * *", + "description": "Daily at 6am: Send digest emails to opted-in users" + }, + { + "name": "process-scheduled-notifications", + "schedule": "*/15 * * * *", + "description": "Every 15 mins: Process scheduled_notifications table" + } + ] + }, + "eventTriggers": { + "description": "Application events that trigger notifications", + "triggers": [ + { + "event": "Task assigned", + "action": "scheduleTask UPDATE with new assignedTo", + "notification": "task_assigned to new assignee" + }, + { + "event": "Task status changed", + "action": "scheduleTask UPDATE status", + "notification": "task_status_changed to PM and assignee" + }, + { + "event": "Bid package sent", + "action": "bidPackage status → sent", + "notification": "bid_package_sent to creator" + }, + { + "event": "Bid response recorded", + "action": "bidPackageRecipient responseStatus updated", + "notification": "bid_response_received to PM" + }, + { + "event": "Daily log submitted", + "action": "dailyLog CREATE", + "notification": "daily_log_submitted to PM" + }, + { + "event": "Payment received", + "action": "payment CREATE", + "notification": "payment_received to admin" + } + ] + }, + "apiEndpoints": [ + { + "method": "GET", + "path": "/api/notifications", + "description": "Get current user's notifications", + "params": ["limit", "offset", "unreadOnly"], + "auth": "any authenticated" + }, + { + "method": "GET", + "path": "/api/notifications/unread-count", + "description": "Get unread notification count", + "auth": "any authenticated" + }, + { + "method": "PUT", + "path": "/api/notifications/[id]/read", + "description": "Mark notification as read", + "auth": "owner" + }, + { + "method": "PUT", + "path": "/api/notifications/mark-all-read", + "description": "Mark all notifications as read", + "auth": "any authenticated" + }, + { + "method": "GET", + "path": "/api/notification-preferences", + "description": "Get current user's notification preferences", + "auth": "any authenticated" + }, + { + "method": "PUT", + "path": "/api/notification-preferences", + "description": "Update notification preferences", + "auth": "any authenticated" + } + ], + "components": [ + { + "name": "NotificationBell", + "path": "src/components/notifications/notification-bell.tsx", + "description": "Header icon with unread count badge" + }, + { + "name": "NotificationDropdown", + "path": "src/components/notifications/notification-dropdown.tsx", + "description": "Dropdown list of recent notifications" + }, + { + "name": "NotificationItem", + "path": "src/components/notifications/notification-item.tsx", + "description": "Single notification display with icon and actions" + }, + { + "name": "NotificationCenter", + "path": "src/components/notifications/notification-center.tsx", + "description": "Full page view of all notifications" + }, + { + "name": "NotificationPreferencesForm", + "path": "src/components/notifications/notification-preferences-form.tsx", + "description": "Settings form for notification preferences" + } + ], + "routes": [ + { + "path": "/dashboard/notifications", + "description": "Full notification center" + }, + { + "path": "/dashboard/settings/notifications", + "description": "Notification preferences" + } + ] + }, + "testCases": [ + { + "id": "TC006-1", + "type": "unit", + "description": "Notification service creates notification correctly", + "steps": ["Call createNotification", "Verify notification in database", "Verify email sent if preference enabled"] + }, + { + "id": "TC006-2", + "type": "integration", + "description": "Task assignment triggers notification", + "steps": ["Assign task to user", "Verify notification created", "Verify email sent"] + }, + { + "id": "TC006-3", + "type": "integration", + "description": "Scheduled job processes due reminders", + "steps": ["Create task due tomorrow", "Run scheduled job", "Verify notification created"] + }, + { + "id": "TC006-4", + "type": "e2e", + "description": "User receives and interacts with notifications", + "steps": ["Trigger notification event", "See unread count update", "Open dropdown", "Click notification", "Verify navigation and marked read"] + }, + { + "id": "TC006-5", + "type": "e2e", + "description": "Preference changes affect email delivery", + "steps": ["Disable email for task_assigned", "Assign task", "Verify notification created", "Verify no email sent"] + } + ] + }, + { + "id": "F007", + "name": "Client Communications Suite", + "priority": "P1", + "status": "planned", + "estimatedDays": 13, + "sprint": 4, + "dependencies": ["F001", "F006"], + "blocksFeatures": [], + "description": "Complete client communication tools including in-app messaging, email logging, and client portal updates. Provides a single source of truth for all client interactions.", + "businessValue": "Centralizes all client communication history. Prevents the 'you never told me that' scenario by maintaining complete records. Improves client experience with proactive updates.", + "userStories": [ + { + "id": "US007-1", + "role": "PM", + "action": "send a message to a client through COMPASS", + "benefit": "all communication is logged and visible to the team", + "acceptanceCriteria": [ + "Can compose message to client users", + "Message stored in COMPASS", + "Email notification sent to client", + "Reply captured and logged" + ] + }, + { + "id": "US007-2", + "role": "Client", + "action": "view and respond to messages from the builder", + "benefit": "I can communicate without hunting through email", + "acceptanceCriteria": [ + "Client sees messages in their portal", + "Can reply to messages", + "Reply notifications sent to PM", + "Message history preserved" + ] + }, + { + "id": "US007-3", + "role": "PM", + "action": "log an email conversation with a client", + "benefit": "I can keep records of external email threads", + "acceptanceCriteria": [ + "Can manually create email log entry", + "Fields: date, subject, participants, summary", + "Can attach email as file", + "Appears in communication timeline" + ] + }, + { + "id": "US007-4", + "role": "PM", + "action": "post an update to the client portal", + "benefit": "clients are proactively informed of progress", + "acceptanceCriteria": [ + "Can create project update/announcement", + "Update visible to all project clients", + "Can include photos", + "Notification sent to clients" + ] + }, + { + "id": "US007-5", + "role": "Client", + "action": "see recent updates and activity on my project", + "benefit": "I stay informed without bothering the builder", + "acceptanceCriteria": [ + "Client portal shows recent updates", + "Can see schedule milestones", + "Can see client-visible daily logs", + "Activity feed of relevant events" + ] + } + ], + "technicalApproach": { + "overview": "Build messaging system with threads, email logging capability, and client-facing update posts. Integrate with notification system for delivery. Client portal surfaces relevant project information.", + "schemaChanges": [ + { + "table": "message_threads", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "project_id", "type": "TEXT", "references": "projects.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "subject", "type": "TEXT", "notNull": true }, + { "name": "started_by", "type": "TEXT", "references": "users.id", "notNull": true }, + { "name": "last_message_at", "type": "TEXT" }, + { "name": "is_archived", "type": "BOOLEAN", "default": false }, + { "name": "created_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_message_threads_project", "columns": ["project_id"] }, + { "name": "idx_message_threads_last", "columns": ["last_message_at"] } + ] + }, + { + "table": "message_thread_participants", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "thread_id", "type": "TEXT", "references": "message_threads.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "user_id", "type": "TEXT", "references": "users.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "last_read_at", "type": "TEXT" }, + { "name": "is_muted", "type": "BOOLEAN", "default": false } + ], + "indexes": [ + { "name": "idx_thread_participants_thread", "columns": ["thread_id"] }, + { "name": "idx_thread_participants_user", "columns": ["user_id"] } + ], + "constraints": [ + { "type": "unique", "columns": ["thread_id", "user_id"] } + ] + }, + { + "table": "messages", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "thread_id", "type": "TEXT", "references": "message_threads.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "sender_id", "type": "TEXT", "references": "users.id", "notNull": true }, + { "name": "content", "type": "TEXT", "notNull": true }, + { "name": "is_system_message", "type": "BOOLEAN", "default": false }, + { "name": "created_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_messages_thread", "columns": ["thread_id"] }, + { "name": "idx_messages_created", "columns": ["created_at"] } + ] + }, + { + "table": "message_attachments", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "message_id", "type": "TEXT", "references": "messages.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "file_name", "type": "TEXT", "notNull": true }, + { "name": "drive_file_id", "type": "TEXT" }, + { "name": "drive_url", "type": "TEXT" }, + { "name": "file_size", "type": "INTEGER" }, + { "name": "mime_type", "type": "TEXT" } + ] + }, + { + "table": "email_logs", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "project_id", "type": "TEXT", "references": "projects.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "logged_by", "type": "TEXT", "references": "users.id", "notNull": true }, + { "name": "email_date", "type": "TEXT", "notNull": true }, + { "name": "subject", "type": "TEXT", "notNull": true }, + { "name": "from_address", "type": "TEXT" }, + { "name": "to_addresses", "type": "TEXT", "description": "JSON array" }, + { "name": "summary", "type": "TEXT" }, + { "name": "full_content", "type": "TEXT" }, + { "name": "attachment_drive_ids", "type": "TEXT", "description": "JSON array of Drive file IDs" }, + { "name": "is_client_visible", "type": "BOOLEAN", "default": true }, + { "name": "created_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_email_logs_project", "columns": ["project_id"] }, + { "name": "idx_email_logs_date", "columns": ["email_date"] } + ] + }, + { + "table": "project_updates", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "project_id", "type": "TEXT", "references": "projects.id", "onDelete": "CASCADE", "notNull": true }, + { "name": "author_id", "type": "TEXT", "references": "users.id", "notNull": true }, + { "name": "title", "type": "TEXT", "notNull": true }, + { "name": "content", "type": "TEXT", "notNull": true }, + { "name": "is_pinned", "type": "BOOLEAN", "default": false }, + { "name": "photo_drive_ids", "type": "TEXT", "description": "JSON array of Drive file IDs" }, + { "name": "notify_clients", "type": "BOOLEAN", "default": true }, + { "name": "published_at", "type": "TEXT" }, + { "name": "created_at", "type": "TEXT", "notNull": true }, + { "name": "updated_at", "type": "TEXT", "notNull": true } + ], + "indexes": [ + { "name": "idx_project_updates_project", "columns": ["project_id"] }, + { "name": "idx_project_updates_published", "columns": ["published_at"] } + ] + } + ], + "messagingFlow": { + "createThread": [ + "1. PM creates new thread with subject and initial message", + "2. Selects client participants", + "3. System creates thread, participants, first message", + "4. Email notification sent to client participants", + "5. Thread appears in both PM's and client's message list" + ], + "replyToThread": [ + "1. User (PM or client) opens thread", + "2. Types reply message", + "3. System creates message record", + "4. Updates thread.last_message_at", + "5. Email notification to other participants", + "6. In-app notification to other participants" + ] + }, + "clientPortal": { + "description": "Client-facing views showing relevant project information", + "views": [ + { + "name": "Dashboard", + "content": ["Recent updates", "Upcoming milestones", "Unread messages count"] + }, + { + "name": "Updates", + "content": ["Project updates/announcements", "Pinned updates at top"] + }, + { + "name": "Messages", + "content": ["Message threads client is participant in"] + }, + { + "name": "Schedule", + "content": ["Milestones only (not full Gantt)", "Client-visible tasks"] + }, + { + "name": "Documents", + "content": ["Documents marked client-visible"] + }, + { + "name": "Daily Logs", + "content": ["Logs marked is_client_visible"] + } + ] + }, + "apiEndpoints": [ + { + "method": "GET", + "path": "/api/projects/[id]/messages", + "description": "List message threads for project", + "auth": "office, admin, client (own threads)" + }, + { + "method": "POST", + "path": "/api/projects/[id]/messages", + "description": "Create new message thread", + "body": ["subject", "content", "participantIds"], + "auth": "office, admin" + }, + { + "method": "GET", + "path": "/api/message-threads/[id]", + "description": "Get thread with messages", + "auth": "participant" + }, + { + "method": "POST", + "path": "/api/message-threads/[id]/messages", + "description": "Reply to thread", + "body": ["content", "attachments?"], + "auth": "participant" + }, + { + "method": "PUT", + "path": "/api/message-threads/[id]/read", + "description": "Mark thread as read", + "auth": "participant" + }, + { + "method": "GET", + "path": "/api/projects/[id]/email-logs", + "description": "List email logs for project", + "auth": "office, admin" + }, + { + "method": "POST", + "path": "/api/projects/[id]/email-logs", + "description": "Create email log entry", + "auth": "office, admin" + }, + { + "method": "GET", + "path": "/api/projects/[id]/updates", + "description": "List project updates", + "auth": "office, admin, client" + }, + { + "method": "POST", + "path": "/api/projects/[id]/updates", + "description": "Create project update", + "auth": "office, admin" + }, + { + "method": "PUT", + "path": "/api/project-updates/[id]", + "description": "Edit project update", + "auth": "author, admin" + }, + { + "method": "GET", + "path": "/api/client/dashboard", + "description": "Client portal dashboard data", + "auth": "client" + } + ], + "components": [ + { + "name": "MessageThreadList", + "path": "src/components/messages/message-thread-list.tsx", + "description": "List of message threads" + }, + { + "name": "MessageThread", + "path": "src/components/messages/message-thread.tsx", + "description": "Full thread view with all messages" + }, + { + "name": "MessageComposer", + "path": "src/components/messages/message-composer.tsx", + "description": "New message/reply input" + }, + { + "name": "NewThreadDialog", + "path": "src/components/messages/new-thread-dialog.tsx", + "description": "Modal for starting new thread" + }, + { + "name": "EmailLogForm", + "path": "src/components/communications/email-log-form.tsx", + "description": "Form for logging email conversations" + }, + { + "name": "EmailLogTimeline", + "path": "src/components/communications/email-log-timeline.tsx", + "description": "Timeline of logged emails" + }, + { + "name": "ProjectUpdateForm", + "path": "src/components/communications/project-update-form.tsx", + "description": "Form for creating project updates" + }, + { + "name": "ProjectUpdateCard", + "path": "src/components/communications/project-update-card.tsx", + "description": "Display card for project update" + }, + { + "name": "ProjectUpdateFeed", + "path": "src/components/communications/project-update-feed.tsx", + "description": "Feed of project updates" + }, + { + "name": "ClientPortalDashboard", + "path": "src/components/client-portal/client-portal-dashboard.tsx", + "description": "Client-facing dashboard" + }, + { + "name": "ClientPortalSidebar", + "path": "src/components/client-portal/client-portal-sidebar.tsx", + "description": "Navigation for client portal" + } + ], + "routes": [ + { + "path": "/dashboard/projects/[id]/messages", + "description": "Project message threads" + }, + { + "path": "/dashboard/projects/[id]/messages/[threadId]", + "description": "Single message thread" + }, + { + "path": "/dashboard/projects/[id]/communications", + "description": "Email logs and communication timeline" + }, + { + "path": "/dashboard/projects/[id]/updates", + "description": "Project updates/announcements" + }, + { + "path": "/portal", + "description": "Client portal root (redirects to first project)" + }, + { + "path": "/portal/projects/[id]", + "description": "Client portal project dashboard" + }, + { + "path": "/portal/projects/[id]/messages", + "description": "Client portal messages" + }, + { + "path": "/portal/projects/[id]/updates", + "description": "Client portal updates" + }, + { + "path": "/portal/projects/[id]/schedule", + "description": "Client portal schedule (milestones)" + }, + { + "path": "/portal/projects/[id]/documents", + "description": "Client portal documents" + } + ] + }, + "testCases": [ + { + "id": "TC007-1", + "type": "integration", + "description": "Message thread creation and notification", + "steps": ["Create thread with client", "Verify email sent", "Verify client sees thread"] + }, + { + "id": "TC007-2", + "type": "integration", + "description": "Client reply notification", + "steps": ["Client replies to thread", "Verify PM notified", "Verify message appears"] + }, + { + "id": "TC007-3", + "type": "e2e", + "description": "Email log workflow", + "steps": ["Create email log", "Attach file", "Verify appears in timeline"] + }, + { + "id": "TC007-4", + "type": "e2e", + "description": "Project update notification", + "steps": ["Create update with notify_clients", "Verify clients notified", "Verify update visible in portal"] + }, + { + "id": "TC007-5", + "type": "e2e", + "description": "Client portal access", + "steps": ["Login as client", "Navigate portal", "Verify only appropriate data visible"] + } + ] + }, + { + "id": "F008", + "name": "Schedule Enhancements for Notifications", + "priority": "P1", + "status": "planned", + "estimatedDays": 3, + "sprint": 3, + "dependencies": ["F001", "F006"], + "blocksFeatures": [], + "description": "Enhance existing schedule system to support notifications for subcontractors and integrate with the three-tier user system. Enables assignment of vendors to schedule items and role-based notification routing.", + "businessValue": "Ensures subcontractors are automatically notified when work is scheduled for them. Reduces manual coordination overhead and prevents missed handoffs.", + "userStories": [ + { + "id": "US008-1", + "role": "PM", + "action": "assign a vendor to a schedule task", + "benefit": "the subcontractor is automatically notified of upcoming work", + "acceptanceCriteria": [ + "Can select vendor from dropdown on task", + "Scheduler contact at vendor receives notification", + "Task shows vendor assignment", + "Vendor can view assigned tasks when logged in" + ] + }, + { + "id": "US008-2", + "role": "Subcontractor (Scheduler)", + "action": "see all tasks my company is assigned to", + "benefit": "I can plan crew schedules and resource allocation", + "acceptanceCriteria": [ + "Filtered view shows only vendor's tasks", + "Shows across all projects", + "Includes dates, status, project info", + "Can export to CSV" + ] + }, + { + "id": "US008-3", + "role": "PM", + "action": "have notifications automatically sent when schedule changes affect a vendor", + "benefit": "subcontractors stay informed without manual communication", + "acceptanceCriteria": [ + "Notification on task date change", + "Notification on task status change", + "Notification on task deletion", + "Email includes changed details" + ] + } + ], + "technicalApproach": { + "overview": "Add vendor assignment to schedule tasks. Integrate with notification system for automatic alerts. Create subcontractor-specific schedule views.", + "schemaChanges": [ + { + "table": "schedule_tasks", + "operation": "ALTER", + "changes": [ + { + "column": "assigned_vendor_id", + "type": "TEXT", + "references": "vendors.id", + "nullable": true, + "description": "Vendor company assigned to this task" + } + ] + }, + { + "table": "schedule_task_notifications", + "operation": "CREATE", + "columns": [ + { "name": "id", "type": "TEXT", "primaryKey": true }, + { "name": "task_id", "type": "TEXT", "references": "schedule_tasks.id", "onDelete": "CASCADE" }, + { "name": "vendor_contact_id", "type": "TEXT", "references": "vendor_contacts.id" }, + { "name": "notification_type", "type": "TEXT", "values": ["assigned", "date_changed", "status_changed", "deleted"] }, + { "name": "sent_at", "type": "TEXT" }, + { "name": "email_sent", "type": "BOOLEAN", "default": false } + ], + "description": "Tracks notifications sent to vendors about schedule changes" + } + ], + "notificationTriggers": [ + { + "trigger": "Task vendor assigned", + "recipients": "Scheduler contacts at vendor", + "content": "You have been scheduled for {{taskTitle}} starting {{startDate}}" + }, + { + "trigger": "Task dates changed", + "recipients": "Scheduler contacts at assigned vendor", + "content": "Schedule change: {{taskTitle}} now {{startDate}} - {{endDate}}" + }, + { + "trigger": "Task status changed", + "recipients": "Scheduler contacts at assigned vendor", + "content": "Status update: {{taskTitle}} is now {{status}}" + } + ], + "apiEndpoints": [ + { + "method": "GET", + "path": "/api/vendors/[id]/schedule", + "description": "Get all tasks assigned to a vendor", + "auth": "office, admin, subcontractor (own vendor)" + }, + { + "method": "GET", + "path": "/api/my-schedule", + "description": "Get tasks for current user's vendor", + "auth": "subcontractor" + } + ], + "componentChanges": [ + { + "component": "TaskFormDialog", + "path": "src/components/schedule/task-form-dialog.tsx", + "changes": ["Add vendor selector dropdown", "Show scheduler contacts for selected vendor"] + }, + { + "component": "SubcontractorScheduleView", + "path": "src/components/schedule/subcontractor-schedule-view.tsx", + "description": "New component for vendor's assigned tasks across projects" + } + ], + "routes": [ + { + "path": "/dashboard/my-schedule", + "description": "Subcontractor's view of their assigned tasks" + } + ] + }, + "testCases": [ + { + "id": "TC008-1", + "type": "integration", + "description": "Vendor assignment triggers notification", + "steps": ["Assign vendor to task", "Verify notification created", "Verify email to scheduler contact"] + }, + { + "id": "TC008-2", + "type": "integration", + "description": "Date change notification to vendor", + "steps": ["Change task dates", "Verify notification to vendor", "Verify includes old and new dates"] + }, + { + "id": "TC008-3", + "type": "e2e", + "description": "Subcontractor views assigned schedule", + "steps": ["Login as subcontractor", "View my-schedule", "Verify only own vendor's tasks shown"] + } + ] + } + ], + "phaseTwo": { + "description": "Features deferred to Phase Two based on client feedback", + "features": [ + { + "name": "Payment Processing", + "reason": "Client prefers to use NetSuite for payment workflows", + "originalEstimate": "2 weeks", + "dependsOn": "NetSuite Integration" + }, + { + "name": "Purchase Orders", + "reason": "Nice-to-have per client feedback", + "scope": "PO system coordinated with subs, tied to schedule" + }, + { + "name": "Project Reporting", + "reason": "Can use NetSuite for these reports initially", + "scope": "Sub usage reports, financial reports" + }, + { + "name": "Subcontractor Insurance Tracking", + "reason": "Part of enhanced subcontractor management", + "scope": "Single source of truth for insurance documentation" + }, + { + "name": "NetSuite Integration", + "reason": "Complex integration requiring Phase One stability first", + "scope": "Bidirectional sync, notifications/flags from NetSuite" + }, + { + "name": "Lead/Opportunity Management", + "reason": "Nice-to-have per client feedback", + "scope": "Lead tracking similar to Buildertrend, PlanSwift integration" + } + ] + }, + "milestones": [ + { + "name": "Sprint 1 Complete", + "targetDate": "Week 2", + "deliverables": [ + "Three-tier user schema deployed", + "PWA infrastructure functional", + "Google Drive service account connected", + "Basic file browser showing Drive contents" + ] + }, + { + "name": "Sprint 2 Complete", + "targetDate": "Week 4", + "deliverables": [ + "Vendor contact management UI complete", + "Offline sync layer functional", + "Daily log creation working (online)", + "Full Drive integration with CSI folders" + ] + }, + { + "name": "Sprint 3 Complete", + "targetDate": "Week 6", + "deliverables": [ + "Daily logs with offline support complete", + "Notification system operational", + "Bid package creation and sending functional", + "Schedule notifications to vendors working" + ] + }, + { + "name": "Sprint 4 Complete", + "targetDate": "Week 8", + "deliverables": [ + "Client messaging complete", + "Email logging functional", + "Project updates/portal working", + "Full offline CRUD operational" + ] + }, + { + "name": "Phase One Complete", + "targetDate": "Week 10", + "deliverables": [ + "All Phase One features integrated", + "End-to-end testing complete", + "Performance validated", + "Ready for production deployment" + ] + } + ], + "risks": [ + { + "risk": "Google Drive API rate limits", + "probability": "Medium", + "impact": "Medium", + "mitigation": "Implement request queuing and caching, batch operations where possible" + }, + { + "risk": "Offline sync conflicts more complex than expected", + "probability": "Medium", + "impact": "High", + "mitigation": "Start with simpler last-write-wins, add manual resolution UI early" + }, + { + "risk": "Service worker caching issues on Cloudflare Workers", + "probability": "Low", + "impact": "Medium", + "mitigation": "Test early on production environment, have fallback to online-only" + }, + { + "risk": "WorkOS service account auth complexity", + "probability": "Low", + "impact": "Medium", + "mitigation": "Verify auth flow works for subcontractor invitations in Sprint 1" + }, + { + "risk": "Scope creep from client communications feature", + "probability": "Medium", + "impact": "Medium", + "mitigation": "Define clear boundaries, defer email auto-capture to Phase Two" + } + ], + "openQuestions": [ + { + "question": "Exact CSI folder structure to use?", + "owner": "Client", + "status": "pending", + "notes": "Sample structure visible in hps-structures/directories, need confirmation" + }, + { + "question": "OpenWeather API key or alternative weather provider?", + "owner": "Dev Team", + "status": "pending", + "notes": "Need API key for weather auto-fill in daily logs" + }, + { + "question": "Email domain for Resend (transactional emails)?", + "owner": "Client", + "status": "pending", + "notes": "Need verified domain for bid packages, notifications" + }, + { + "question": "Conflict resolution preference - auto vs manual?", + "owner": "Client", + "status": "decided", + "decision": "Last-write-wins with manual override option" + }, + { + "question": "Client portal branding requirements?", + "owner": "Client", + "status": "pending", + "notes": "Need logo, colors, any custom branding for client-facing views" + } + ] +} diff --git a/drizzle/0008_superb_lifeguard.sql b/drizzle/0008_superb_lifeguard.sql new file mode 100755 index 0000000..72ede6c --- /dev/null +++ b/drizzle/0008_superb_lifeguard.sql @@ -0,0 +1,21 @@ +CREATE TABLE `agent_conversations` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `title` text, + `last_message_at` text NOT NULL, + `created_at` text NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `agent_memories` ( + `id` text PRIMARY KEY NOT NULL, + `conversation_id` text NOT NULL, + `user_id` text NOT NULL, + `role` text NOT NULL, + `content` text NOT NULL, + `embedding` text, + `metadata` text, + `created_at` text NOT NULL, + FOREIGN KEY (`conversation_id`) REFERENCES `agent_conversations`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); \ No newline at end of file diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json new file mode 100755 index 0000000..b6037ae --- /dev/null +++ b/drizzle/meta/0008_snapshot.json @@ -0,0 +1,2324 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "353cccf6-b609-4381-bbb4-a8d5456cfae0", + "prevId": "79017edb-88ed-4147-b649-3e12d16a60f5", + "tables": { + "agent_conversations": { + "name": "agent_conversations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_conversations_user_id_users_id_fk": { + "name": "agent_conversations_user_id_users_id_fk", + "tableFrom": "agent_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_memories": { + "name": "agent_memories", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "embedding": { + "name": "embedding", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_memories_conversation_id_agent_conversations_id_fk": { + "name": "agent_memories_conversation_id_agent_conversations_id_fk", + "tableFrom": "agent_memories", + "tableTo": "agent_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_memories_user_id_users_id_fk": { + "name": "agent_memories_user_id_users_id_fk", + "tableFrom": "agent_memories", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "customers": { + "name": "customers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company": { + "name": "company", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feedback": { + "name": "feedback", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_url": { + "name": "page_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "viewport_width": { + "name": "viewport_width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "viewport_height": { + "name": "viewport_height", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_hash": { + "name": "ip_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_issue_url": { + "name": "github_issue_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "group_members": { + "name": "group_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "group_members_group_id_groups_id_fk": { + "name": "group_members_group_id_groups_id_fk", + "tableFrom": "group_members", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_members_user_id_users_id_fk": { + "name": "group_members_user_id_users_id_fk", + "tableFrom": "group_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "groups": { + "name": "groups", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groups_organization_id_organizations_id_fk": { + "name": "groups_organization_id_organizations_id_fk", + "tableFrom": "groups", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization_members": { + "name": "organization_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_members_organization_id_organizations_id_fk": { + "name": "organization_members_organization_id_organizations_id_fk", + "tableFrom": "organization_members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_members_user_id_users_id_fk": { + "name": "organization_members_user_id_users_id_fk", + "tableFrom": "organization_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organizations": { + "name": "organizations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_members": { + "name": "project_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_members_project_id_projects_id_fk": { + "name": "project_members_project_id_projects_id_fk", + "tableFrom": "project_members", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_members_user_id_users_id_fk": { + "name": "project_members_user_id_users_id_fk", + "tableFrom": "project_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'OPEN'" + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_manager": { + "name": "project_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "netsuite_job_id": { + "name": "netsuite_job_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_organization_id_organizations_id_fk": { + "name": "projects_organization_id_organizations_id_fk", + "tableFrom": "projects", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_baselines": { + "name": "schedule_baselines", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_data": { + "name": "snapshot_data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_baselines_project_id_projects_id_fk": { + "name": "schedule_baselines_project_id_projects_id_fk", + "tableFrom": "schedule_baselines", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_tasks": { + "name": "schedule_tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_date": { + "name": "start_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workdays": { + "name": "workdays", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_date_calculated": { + "name": "end_date_calculated", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'PENDING'" + }, + "is_critical_path": { + "name": "is_critical_path", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_milestone": { + "name": "is_milestone", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "percent_complete": { + "name": "percent_complete", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "assigned_to": { + "name": "assigned_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_tasks_project_id_projects_id_fk": { + "name": "schedule_tasks_project_id_projects_id_fk", + "tableFrom": "schedule_tasks", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "task_dependencies": { + "name": "task_dependencies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "predecessor_id": { + "name": "predecessor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "successor_id": { + "name": "successor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'FS'" + }, + "lag_days": { + "name": "lag_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "task_dependencies_predecessor_id_schedule_tasks_id_fk": { + "name": "task_dependencies_predecessor_id_schedule_tasks_id_fk", + "tableFrom": "task_dependencies", + "tableTo": "schedule_tasks", + "columnsFrom": [ + "predecessor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_dependencies_successor_id_schedule_tasks_id_fk": { + "name": "task_dependencies_successor_id_schedule_tasks_id_fk", + "tableFrom": "task_dependencies", + "tableTo": "schedule_tasks", + "columnsFrom": [ + "successor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "team_members": { + "name": "team_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "teams_organization_id_organizations_id_fk": { + "name": "teams_organization_id_organizations_id_fk", + "tableFrom": "teams", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'office'" + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_login_at": { + "name": "last_login_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vendors": { + "name": "vendors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Subcontractor'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workday_exceptions": { + "name": "workday_exceptions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_date": { + "name": "start_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_date": { + "name": "end_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'non_working'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'company_holiday'" + }, + "recurrence": { + "name": "recurrence", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'one_time'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "workday_exceptions_project_id_projects_id_fk": { + "name": "workday_exceptions_project_id_projects_id_fk", + "tableFrom": "workday_exceptions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credit_memos": { + "name": "credit_memos", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memo_number": { + "name": "memo_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "issue_date": { + "name": "issue_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total": { + "name": "total", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_applied": { + "name": "amount_applied", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_remaining": { + "name": "amount_remaining", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "line_items": { + "name": "line_items", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "credit_memos_customer_id_customers_id_fk": { + "name": "credit_memos_customer_id_customers_id_fk", + "tableFrom": "credit_memos", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credit_memos_project_id_projects_id_fk": { + "name": "credit_memos_project_id_projects_id_fk", + "tableFrom": "credit_memos", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invoices": { + "name": "invoices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_number": { + "name": "invoice_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "issue_date": { + "name": "issue_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subtotal": { + "name": "subtotal", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "tax": { + "name": "tax", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total": { + "name": "total", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_paid": { + "name": "amount_paid", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_due": { + "name": "amount_due", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "line_items": { + "name": "line_items", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "invoices_customer_id_customers_id_fk": { + "name": "invoices_customer_id_customers_id_fk", + "tableFrom": "invoices", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invoices_project_id_projects_id_fk": { + "name": "invoices_project_id_projects_id_fk", + "tableFrom": "invoices", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "netsuite_auth": { + "name": "netsuite_auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_in": { + "name": "expires_in", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "netsuite_sync_log": { + "name": "netsuite_sync_log", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "sync_type": { + "name": "sync_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "record_type": { + "name": "record_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "records_processed": { + "name": "records_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "records_failed": { + "name": "records_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "error_summary": { + "name": "error_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "netsuite_sync_metadata": { + "name": "netsuite_sync_metadata", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "local_table": { + "name": "local_table", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "local_record_id": { + "name": "local_record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "netsuite_record_type": { + "name": "netsuite_record_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "netsuite_internal_id": { + "name": "netsuite_internal_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_modified_local": { + "name": "last_modified_local", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_modified_remote": { + "name": "last_modified_remote", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'synced'" + }, + "conflict_data": { + "name": "conflict_data", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "payments": { + "name": "payments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_type": { + "name": "payment_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payment_date": { + "name": "payment_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payment_method": { + "name": "payment_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_number": { + "name": "reference_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "payments_customer_id_customers_id_fk": { + "name": "payments_customer_id_customers_id_fk", + "tableFrom": "payments", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payments_vendor_id_vendors_id_fk": { + "name": "payments_vendor_id_vendors_id_fk", + "tableFrom": "payments", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payments_project_id_projects_id_fk": { + "name": "payments_project_id_projects_id_fk", + "tableFrom": "payments", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vendor_bills": { + "name": "vendor_bills", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bill_number": { + "name": "bill_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "bill_date": { + "name": "bill_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subtotal": { + "name": "subtotal", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "tax": { + "name": "tax", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total": { + "name": "total", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_paid": { + "name": "amount_paid", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_due": { + "name": "amount_due", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "line_items": { + "name": "line_items", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_bills_vendor_id_vendors_id_fk": { + "name": "vendor_bills_vendor_id_vendors_id_fk", + "tableFrom": "vendor_bills", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vendor_bills_project_id_projects_id_fk": { + "name": "vendor_bills_project_id_projects_id_fk", + "tableFrom": "vendor_bills", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 27bb259..ecf47bf 100755 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1770321600000, "tag": "0007_add_customer_fields", "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1770320934942, + "tag": "0008_superb_lifeguard", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/seed-users.sql b/drizzle/seed-users.sql index 812bf1b..e87142f 100755 --- a/drizzle/seed-users.sql +++ b/drizzle/seed-users.sql @@ -47,11 +47,4 @@ VALUES ('gm-1', 'group-1', 'user-2', '2026-01-15T00:00:00Z'), ('gm-2', 'group-2', 'user-4', '2026-01-25T00:00:00Z'); --- seed project members (using existing project) -INSERT INTO project_members (id, project_id, user_id, role, assigned_at) -VALUES - ('pm-1', 'proj-o-001', 'user-1', 'admin', '2026-01-01T00:00:00Z'), - ('pm-2', 'proj-o-001', 'user-2', 'manager', '2026-01-15T00:00:00Z'), - ('pm-3', 'proj-o-001', 'user-4', 'crew', '2026-01-25T00:00:00Z'), - ('pm-4', 'proj-o-002', 'user-2', 'manager', '2026-01-16T00:00:00Z'), - ('pm-5', 'proj-o-003', 'user-3', 'manager', '2026-01-21T00:00:00Z'); +-- project_members are seeded in seed.sql (after projects exist) diff --git a/drizzle/seed.sql b/drizzle/seed.sql index f47c17c..9050276 100755 --- a/drizzle/seed.sql +++ b/drizzle/seed.sql @@ -152,6 +152,16 @@ INSERT OR IGNORE INTO schedule_tasks (id, project_id, title, start_date, workday ('task-n001-040', 'proj-n-001', 'Punch List', '2026-03-19', 5, '2026-03-25', 'closeout', 'PENDING', 0, 0, 0, NULL, 40, '2025-08-15T09:00:00Z', '2025-08-15T09:00:00Z'), ('task-n001-041', 'proj-n-001', 'Certificate of Occupancy', '2026-03-26', 2, '2026-03-27', 'closeout', 'PENDING', 1, 1, 0, 'Daniel M Vogel', 41, '2025-08-15T09:00:00Z', '2025-08-15T09:00:00Z'); +-- ─── Project Members (must come after projects) ─── + +INSERT OR IGNORE INTO project_members (id, project_id, user_id, role, assigned_at) +VALUES + ('pm-1', 'proj-o-001', 'user-1', 'admin', '2026-01-01T00:00:00Z'), + ('pm-2', 'proj-o-001', 'user-2', 'manager', '2026-01-15T00:00:00Z'), + ('pm-3', 'proj-o-001', 'user-4', 'crew', '2026-01-25T00:00:00Z'), + ('pm-4', 'proj-o-002', 'user-2', 'manager', '2026-01-16T00:00:00Z'), + ('pm-5', 'proj-o-003', 'user-3', 'manager', '2026-01-21T00:00:00Z'); + -- ─── Dependencies for N-001 (finish-to-start) ─── INSERT OR IGNORE INTO task_dependencies (id, predecessor_id, successor_id, type, lag_days) VALUES diff --git a/next.config.ts b/next.config.ts index 9352f4e..991ce9d 100755 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,21 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + eslint: { + ignoreDuringBuilds: true, + }, + experimental: { + optimizePackageImports: [ + "@tabler/icons-react", + "lucide-react", + "@radix-ui/react-icons", + "recharts", + "@workos-inc/node", + "date-fns", + "remeda", + "framer-motion", + ], + }, }; export default nextConfig; diff --git a/package.json b/package.json index 8aee75e..47e2fab 100755 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.2.2", + "@json-render/core": "^0.4.0", + "@json-render/react": "^0.4.0", "@opennextjs/cloudflare": "^1.14.4", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", @@ -53,23 +55,31 @@ "@tanstack/react-table": "^8.21.3", "@workos-inc/authkit-nextjs": "^2.13.0", "@workos-inc/node": "^8.1.0", + "ai": "^6.0.72", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "drizzle-orm": "^0.45.1", "embla-carousel-react": "^8.6.0", + "framer-motion": "11", "frappe-gantt": "^1.0.4", "input-otp": "^1.4.2", - "lucide-react": "^0.562.0", + "lucide-react": "^0.563.0", + "nanoid": "^5.1.6", "next": "15.5.9", "next-themes": "^0.4.6", + "radix-ui": "^1.4.3", "react": "19.1.4", "react-day-picker": "^9.13.0", "react-dom": "19.1.4", "react-hook-form": "^7.71.1", + "react-markdown": "10", "react-resizable-panels": "^4.4.1", "recharts": "2.15.4", + "remark-gfm": "4", + "remeda": "2", + "shiki": "1", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.4.0", diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index 13eb06b..9bc384a 100755 --- a/src/app/(auth)/layout.tsx +++ b/src/app/(auth)/layout.tsx @@ -1,9 +1,10 @@ -import { Card, CardContent } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card" +import { Toaster } from "@/components/ui/sonner" export default function AuthLayout({ children, }: { - children: React.ReactNode; + children: React.ReactNode }) { return (
@@ -26,6 +27,7 @@ export default function AuthLayout({ High Performance Structures

+ - ); + ) } diff --git a/src/app/api/agent/route.ts b/src/app/api/agent/route.ts new file mode 100755 index 0000000..9137da6 --- /dev/null +++ b/src/app/api/agent/route.ts @@ -0,0 +1,196 @@ +/** + * Agent API Route - Proxy to ElizaOS Server + * + * POST /api/agent - Send message to the Compass agent + * GET /api/agent - Get conversation history + * + * This route proxies requests to the ElizaOS sidecar server, + * handling auth on the Next.js side and forwarding messages + * to the agent's sessions API. + */ + +import { NextResponse } from "next/server" +import { getCurrentUser } from "@/lib/auth" + +const ELIZAOS_URL = + process.env.ELIZAOS_API_URL ?? "http://localhost:3001" + +interface RequestBody { + message: string + conversationId?: string + context?: { + view?: string + projectId?: string + } +} + +interface ElizaSessionResponse { + id: string + agentId?: string + userId?: string +} + +interface ElizaMessageResponse { + id: string + content: string + authorId?: string + createdAt?: string + metadata?: Record + sessionStatus?: Record +} + +async function getOrCreateSession( + userId: string, + conversationId?: string +): Promise { + if (conversationId) return conversationId + + const response = await fetch( + `${ELIZAOS_URL}/api/messaging/sessions`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userId }), + } + ) + + if (!response.ok) { + throw new Error( + `Failed to create session: ${response.status} ${response.statusText}` + ) + } + + const data: ElizaSessionResponse = await response.json() + return data.id +} + +export async function POST(request: Request): Promise { + try { + const user = await getCurrentUser() + if (!user) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ) + } + + const body: RequestBody = await request.json() + + if (!body.message || typeof body.message !== "string") { + return NextResponse.json( + { error: "Message is required" }, + { status: 400 } + ) + } + + const sessionId = await getOrCreateSession( + user.id, + body.conversationId + ) + + // Send message to ElizaOS sessions API + const response = await fetch( + `${ELIZAOS_URL}/api/messaging/sessions/${sessionId}/messages`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + content: body.message, + metadata: { + source: body.context?.view ?? "dashboard", + projectId: body.context?.projectId, + userId: user.id, + userRole: user.role, + userName: user.displayName ?? user.email, + }, + }), + } + ) + + if (!response.ok) { + const errorText = await response.text() + console.error("ElizaOS error:", errorText) + return NextResponse.json( + { error: "Agent unavailable" }, + { status: 502 } + ) + } + + const data: ElizaMessageResponse = await response.json() + + // Extract action data from metadata if present + const actionData = data.metadata?.action as + | { type: string; payload?: Record } + | undefined + const actions = actionData ? [actionData] : undefined + + return NextResponse.json({ + id: data.id ?? crypto.randomUUID(), + text: data.content ?? "", + actions, + ui: data.metadata?.ui, + conversationId: sessionId, + }) + } catch (error) { + console.error("Agent API error:", error) + return NextResponse.json( + { + error: + error instanceof Error + ? error.message + : "Internal server error", + }, + { status: 500 } + ) + } +} + +export async function GET(request: Request): Promise { + try { + const user = await getCurrentUser() + if (!user) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401 } + ) + } + + const { searchParams } = new URL(request.url) + const sessionId = searchParams.get("conversationId") + + if (!sessionId) { + // No session listing support via proxy yet + return NextResponse.json({ conversations: [] }) + } + + // Get messages from ElizaOS session + const response = await fetch( + `${ELIZAOS_URL}/api/messaging/sessions/${sessionId}/messages?limit=100` + ) + + if (!response.ok) { + return NextResponse.json( + { error: "Session not found" }, + { status: 404 } + ) + } + + const messages = await response.json() + + return NextResponse.json({ + conversation: { id: sessionId }, + messages, + }) + } catch (error) { + console.error("Agent API error:", error) + return NextResponse.json( + { + error: + error instanceof Error + ? error.message + : "Internal server error", + }, + { status: 500 } + ) + } +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index d1d33c3..cec07d8 100755 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -151,9 +151,12 @@ export async function POST(request: NextRequest) { ) } catch (error) { console.error("Login error:", error) + const err = error as { code?: string } + const isAuthError = ["invalid_credentials", "user_not_found", + "expired_code", "invalid_code"].includes(err.code || "") return NextResponse.json( { success: false, error: mapWorkOSError(error) }, - { status: 500 } + { status: isAuthError ? 401 : 500 } ) } } diff --git a/src/app/api/auth/sso/route.ts b/src/app/api/auth/sso/route.ts index f9941ff..73bb8c3 100755 --- a/src/app/api/auth/sso/route.ts +++ b/src/app/api/auth/sso/route.ts @@ -32,10 +32,16 @@ export async function GET(request: NextRequest) { const workos = getWorkOS() + // derive origin from Host header (nextUrl.origin is wrong on CF Workers) + const host = request.headers.get("host") + const proto = request.headers.get("x-forwarded-proto") || "https" + const origin = host ? `${proto}://${host}` : request.nextUrl.origin + const redirectUri = `${origin}/api/auth/callback` + const authorizationUrl = workos.userManagement.getAuthorizationUrl({ provider: provider as Provider, clientId: process.env.WORKOS_CLIENT_ID!, - redirectUri: process.env.NEXT_PUBLIC_WORKOS_REDIRECT_URI!, + redirectUri, state: from || "/dashboard", }) diff --git a/src/app/dashboard/customers/page.tsx b/src/app/dashboard/customers/page.tsx index 65df670..4e5ca17 100755 --- a/src/app/dashboard/customers/page.tsx +++ b/src/app/dashboard/customers/page.tsx @@ -2,6 +2,7 @@ import * as React from "react" import { IconPlus } from "@tabler/icons-react" +import { Plus } from "lucide-react" import { toast } from "sonner" import { @@ -14,6 +15,7 @@ import type { Customer } from "@/db/schema" import { Button } from "@/components/ui/button" import { CustomersTable } from "@/components/financials/customers-table" import { CustomerDialog } from "@/components/financials/customer-dialog" +import { useRegisterPageActions } from "@/hooks/use-register-page-actions" export default function CustomersPage() { const [customers, setCustomers] = React.useState([]) @@ -21,6 +23,24 @@ export default function CustomersPage() { const [dialogOpen, setDialogOpen] = React.useState(false) const [editing, setEditing] = React.useState(null) + const openCreate = React.useCallback(() => { + setEditing(null) + setDialogOpen(true) + }, []) + + const pageActions = React.useMemo( + () => [ + { + id: "add-customer", + label: "Add Customer", + icon: Plus, + onSelect: openCreate, + }, + ], + [openCreate] + ) + useRegisterPageActions(pageActions) + const load = async () => { try { const data = await getCustomers() @@ -34,11 +54,6 @@ export default function CustomersPage() { React.useEffect(() => { load() }, []) - const handleCreate = () => { - setEditing(null) - setDialogOpen(true) - } - const handleEdit = (customer: Customer) => { setEditing(customer) setDialogOpen(true) @@ -115,7 +130,7 @@ export default function CustomersPage() { Manage customer accounts

- diff --git a/src/app/dashboard/financials/page.tsx b/src/app/dashboard/financials/page.tsx index 212eac3..fc22080 100755 --- a/src/app/dashboard/financials/page.tsx +++ b/src/app/dashboard/financials/page.tsx @@ -2,8 +2,10 @@ import * as React from "react" import { IconPlus } from "@tabler/icons-react" +import { Plus } from "lucide-react" import { useSearchParams, useRouter } from "next/navigation" import { toast } from "sonner" +import { useRegisterPageActions } from "@/hooks/use-register-page-actions" import { getCustomers } from "@/app/actions/customers" import { getVendors } from "@/app/actions/vendors" @@ -137,6 +139,62 @@ function FinancialsContent() { React.useEffect(() => { loadAll() }, []) + const openInvoice = React.useCallback(() => { + setEditingInvoice(null) + setInvoiceDialogOpen(true) + }, []) + + const openBill = React.useCallback(() => { + setEditingBill(null) + setBillDialogOpen(true) + }, []) + + const openPayment = React.useCallback(() => { + setEditingPayment(null) + setPaymentDialogOpen(true) + }, []) + + const openMemo = React.useCallback(() => { + setEditingMemo(null) + setMemoDialogOpen(true) + }, []) + + const TAB_ACTIONS: Record< + Tab, + { id: string; label: string; onSelect: () => void } + > = React.useMemo( + () => ({ + invoices: { + id: "new-invoice", + label: "New Invoice", + onSelect: openInvoice, + }, + bills: { + id: "new-bill", + label: "New Bill", + onSelect: openBill, + }, + payments: { + id: "new-payment", + label: "New Payment", + onSelect: openPayment, + }, + "credit-memos": { + id: "new-credit-memo", + label: "New Credit Memo", + onSelect: openMemo, + }, + }), + [openInvoice, openBill, openPayment, openMemo] + ) + + const pageActions = React.useMemo(() => { + const action = TAB_ACTIONS[tab] + return [{ ...action, icon: Plus }] + }, [tab, TAB_ACTIONS]) + + useRegisterPageActions(pageActions) + const handleTabChange = (value: string) => { setTab(value as Tab) router.replace(`/dashboard/financials?tab=${value}`, { scroll: false }) @@ -293,53 +351,25 @@ function FinancialsContent() { {tab === "invoices" && ( - )} {tab === "bills" && ( - )} {tab === "payments" && ( - )} {tab === "credit-memos" && ( - diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 185a491..1ed8854 100755 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -4,7 +4,11 @@ import { MobileBottomNav } from "@/components/mobile-bottom-nav" import { CommandMenuProvider } from "@/components/command-menu-provider" import { SettingsProvider } from "@/components/settings-provider" import { FeedbackWidget } from "@/components/feedback-widget" +import { PageActionsProvider } from "@/components/page-actions-provider" +import { DashboardContextMenu } from "@/components/dashboard-context-menu" import { Toaster } from "@/components/ui/sonner" +import { ChatPanel } from "@/components/agent/chat-panel" +import { AgentProvider } from "@/components/agent/agent-provider" import { SidebarInset, SidebarProvider, @@ -26,7 +30,9 @@ export default async function DashboardLayout({ return ( + + -
-
- {children} +
+ +
+
+ {children} +
+
+
@@ -55,7 +66,9 @@ export default async function DashboardLayout({ + + ) } diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 170ef8d..b82489e 100755 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,18 +1,6 @@ export const dynamic = "force-dynamic" -import { FeedbackCallout } from "@/components/feedback-widget" -import { - IconBrandGithub, - IconExternalLink, - IconGitCommit, - IconGitFork, - IconStar, - IconAlertCircle, - IconEye, -} from "@tabler/icons-react" - -const REPO = "High-Performance-Structures/compass" -const GITHUB_URL = `https://github.com/${REPO}` +import { DashboardChat } from "@/components/dashboard-chat" type RepoStats = { stargazers_count: number @@ -21,20 +9,16 @@ type RepoStats = { subscribers_count: number } -type Commit = { - sha: string - commit: { - message: string - author: { name: string; date: string } - } - html_url: string -} +const REPO = "High-Performance-Structures/compass" -async function getRepoData() { +async function getRepoStats(): Promise { try { - const { getCloudflareContext } = await import("@opennextjs/cloudflare") + const { getCloudflareContext } = await import( + "@opennextjs/cloudflare" + ) const { env } = await getCloudflareContext() - const token = (env as unknown as Record).GITHUB_TOKEN as string | undefined + const token = (env as unknown as Record) + .GITHUB_TOKEN as string | undefined const headers: Record = { Accept: "application/vnd.github+json", @@ -42,234 +26,19 @@ async function getRepoData() { } if (token) headers.Authorization = `Bearer ${token}` - const [repoRes, commitsRes] = await Promise.all([ - fetch(`https://api.github.com/repos/${REPO}`, { - next: { revalidate: 300 }, - headers, - }), - fetch(`https://api.github.com/repos/${REPO}/commits?per_page=8`, { - next: { revalidate: 300 }, - headers, - }), - ]) - - if (!repoRes.ok || !commitsRes.ok) return null - - const repo: RepoStats = await repoRes.json() - const commits: Commit[] = await commitsRes.json() - return { repo, commits } + const res = await fetch( + `https://api.github.com/repos/${REPO}`, + { next: { revalidate: 300 }, headers } + ) + if (!res.ok) return null + return (await res.json()) as RepoStats } catch { return null } } -function timeAgo(date: string) { - const seconds = Math.floor( - (Date.now() - new Date(date).getTime()) / 1000 - ) - if (seconds < 60) return "just now" - const minutes = Math.floor(seconds / 60) - if (minutes < 60) return `${minutes}m ago` - const hours = Math.floor(minutes / 60) - if (hours < 24) return `${hours}h ago` - const days = Math.floor(hours / 24) - if (days < 30) return `${days}d ago` - return new Date(date).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }) -} - export default async function Page() { - const data = await getRepoData() + const stats = await getRepoStats() - return ( -
-
-
- -

- Compass -

-

- Development preview — features may be incomplete - or change without notice. -

-
- -
-
- -
-
-
-

- - Working -

-
    -
  • Projects — create and manage projects with D1 database
  • -
  • Schedule — Gantt chart with phases, tasks, dependencies, and critical path
  • -
  • File browser — drive-style UI with folder navigation
  • -
  • Settings — app preferences with theme and notifications
  • -
  • Sidebar navigation with contextual project/file views
  • -
  • Command palette search (Cmd+K)
  • -
-
- -
-

- - In Progress -

-
    -
  • Project auto-provisioning (code generation, CSI folder structure)
  • -
  • Budget tracking (CSI divisions, estimated vs actual, change orders)
  • -
  • Document management (S3/R2 storage, metadata, versioning)
  • -
  • Communication logging (manual entries, timeline view)
  • -
  • Dashboard — three-column layout (past due, due today, action items)
  • -
  • User authentication and roles (WorkOS)
  • -
  • Email notifications (Resend)
  • -
  • Basic reports (budget variance, overdue tasks, monthly actuals)
  • -
-
- -
-

- - Planned -

-
    -
  • Client portal with read-only views
  • -
  • BuilderTrend import wizard (CSV-based)
  • -
  • Daily logs
  • -
  • Time tracking
  • -
  • Report builder (custom fields and filters)
  • -
  • Bid package management
  • -
-
- -
-

- - Future -

-
    -
  • Netsuite/QuickBooks API sync
  • -
  • Payment integration
  • -
  • RFI/Submittal tracking
  • -
  • Native mobile apps (iOS/Android)
  • -
  • Advanced scheduling (resource leveling, baseline comparison)
  • -
-
- -
- - {data && ( -
- - -
-

View on GitHub

-

{REPO}

-
- -
-
- } - label="Stars" - value={data.repo.stargazers_count} - /> - } - label="Forks" - value={data.repo.forks_count} - /> - } - label="Issues" - value={data.repo.open_issues_count} - /> - } - label="Watchers" - value={data.repo.subscribers_count} - /> -
- - -
- )} -
-
-
- ) -} - -function StatCard({ - icon, - label, - value, -}: { - icon: React.ReactNode - label: string - value: number -}) { - return ( -
-
- {icon} - {label} -
-

- {value.toLocaleString()} -

-
- ) + return } diff --git a/src/app/dashboard/people/page.tsx b/src/app/dashboard/people/page.tsx index 2e3f35f..0deb0bf 100755 --- a/src/app/dashboard/people/page.tsx +++ b/src/app/dashboard/people/page.tsx @@ -2,6 +2,7 @@ import * as React from "react" import { IconUserPlus } from "@tabler/icons-react" +import { UserPlus } from "lucide-react" import { toast } from "sonner" import { getUsers, deactivateUser, type UserWithRelations } from "@/app/actions/users" @@ -9,6 +10,7 @@ import { Button } from "@/components/ui/button" import { PeopleTable } from "@/components/people-table" import { UserDrawer } from "@/components/people/user-drawer" import { InviteDialog } from "@/components/people/invite-dialog" +import { useRegisterPageActions } from "@/hooks/use-register-page-actions" export default function PeoplePage() { @@ -18,6 +20,19 @@ export default function PeoplePage() { const [drawerOpen, setDrawerOpen] = React.useState(false) const [inviteDialogOpen, setInviteDialogOpen] = React.useState(false) + const pageActions = React.useMemo( + () => [ + { + id: "invite-user", + label: "Invite User", + icon: UserPlus, + onSelect: () => setInviteDialogOpen(true), + }, + ], + [] + ) + useRegisterPageActions(pageActions) + React.useEffect(() => { loadUsers() }, []) diff --git a/src/app/dashboard/vendors/page.tsx b/src/app/dashboard/vendors/page.tsx index 7717c38..344ceb1 100755 --- a/src/app/dashboard/vendors/page.tsx +++ b/src/app/dashboard/vendors/page.tsx @@ -2,6 +2,7 @@ import * as React from "react" import { IconPlus } from "@tabler/icons-react" +import { Plus } from "lucide-react" import { toast } from "sonner" import { @@ -14,6 +15,7 @@ import type { Vendor } from "@/db/schema" import { Button } from "@/components/ui/button" import { VendorsTable } from "@/components/financials/vendors-table" import { VendorDialog } from "@/components/financials/vendor-dialog" +import { useRegisterPageActions } from "@/hooks/use-register-page-actions" export default function VendorsPage() { const [vendors, setVendors] = React.useState([]) @@ -21,6 +23,24 @@ export default function VendorsPage() { const [dialogOpen, setDialogOpen] = React.useState(false) const [editing, setEditing] = React.useState(null) + const openCreate = React.useCallback(() => { + setEditing(null) + setDialogOpen(true) + }, []) + + const pageActions = React.useMemo( + () => [ + { + id: "add-vendor", + label: "Add Vendor", + icon: Plus, + onSelect: openCreate, + }, + ], + [openCreate] + ) + useRegisterPageActions(pageActions) + const load = async () => { try { const data = await getVendors() @@ -34,11 +54,6 @@ export default function VendorsPage() { React.useEffect(() => { load() }, []) - const handleCreate = () => { - setEditing(null) - setDialogOpen(true) - } - const handleEdit = (vendor: Vendor) => { setEditing(vendor) setDialogOpen(true) @@ -114,7 +129,7 @@ export default function VendorsPage() { Manage vendor relationships

- diff --git a/src/components/agent/agent-provider.tsx b/src/components/agent/agent-provider.tsx new file mode 100755 index 0000000..eaeec5a --- /dev/null +++ b/src/components/agent/agent-provider.tsx @@ -0,0 +1,50 @@ +/** + * Agent Provider + * + * Provides context for controlling the chat panel from anywhere in the app. + */ + +"use client" + +import * as React from "react" + +interface AgentContextValue { + isOpen: boolean + open: () => void + close: () => void + toggle: () => void +} + +const AgentContext = React.createContext(null) + +export function AgentProvider({ children }: { children: React.ReactNode }) { + const [isOpen, setIsOpen] = React.useState(false) + + const contextValue = React.useMemo( + () => ({ + isOpen, + open: () => setIsOpen(true), + close: () => setIsOpen(false), + toggle: () => setIsOpen((prev) => !prev), + }), + [isOpen] + ) + + return ( + + {children} + + ) +} + +export function useAgent() { + const context = React.useContext(AgentContext) + if (!context) { + throw new Error("useAgent must be used within an AgentProvider") + } + return context +} + +export function useAgentOptional() { + return React.useContext(AgentContext) +} diff --git a/src/components/agent/chat-panel.tsx b/src/components/agent/chat-panel.tsx new file mode 100755 index 0000000..2fcf829 --- /dev/null +++ b/src/components/agent/chat-panel.tsx @@ -0,0 +1,303 @@ +"use client" + +import { useState, useEffect, useCallback, useRef } from "react" +import { useRouter, usePathname } from "next/navigation" +import { MessageSquare } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Chat } from "@/components/ui/chat" +import { cn } from "@/lib/utils" +import { + useElizaChat, + initializeActionHandlers, + executeAction, + unregisterActionHandler, + ALL_HANDLER_TYPES, + type AgentAction, +} from "@/lib/eliza/chat-adapter" +import { DynamicUI } from "./dynamic-ui" +import { useAgentOptional } from "./agent-provider" +import { toast } from "sonner" + +interface ChatPanelProps { + className?: string +} + +export function ChatPanel({ className }: ChatPanelProps) { + const agentContext = useAgentOptional() + const isOpen = agentContext?.isOpen ?? false + const setIsOpen = agentContext + ? (open: boolean) => + open ? agentContext.open() : agentContext.close() + : () => {} + + const router = useRouter() + const pathname = usePathname() + + const routerRef = useRef(router) + routerRef.current = router + + const onAction = useCallback((action: AgentAction) => { + executeAction(action) + }, []) + + const onError = useCallback((error: Error) => { + toast.error(error.message) + }, []) + + useEffect(() => { + initializeActionHandlers(() => routerRef.current) + + const handleToast = (event: CustomEvent) => { + const { message, type = "default" } = event.detail ?? {} + if (message) { + if (type === "success") toast.success(message) + else if (type === "error") toast.error(message) + else toast(message) + } + } + + window.addEventListener( + "agent-toast", + handleToast as EventListener + ) + + return () => { + window.removeEventListener( + "agent-toast", + handleToast as EventListener + ) + for (const type of ALL_HANDLER_TYPES) { + unregisterActionHandler(type) + } + } + }, []) + + const { + messages, + isGenerating, + stop, + append, + setMessages, + } = useElizaChat({ + context: { view: pathname }, + onAction, + onError, + }) + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === ".") { + e.preventDefault() + agentContext?.toggle() + } + if (e.key === "Escape" && isOpen) { + setIsOpen(false) + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [isOpen, setIsOpen, agentContext]) + + const suggestions = getSuggestionsForPath(pathname) + + const chatMessages = messages.map((msg) => ({ + id: msg.id, + role: msg.role, + content: msg.content, + createdAt: msg.createdAt, + })) + + const handleRateResponse = useCallback( + ( + messageId: string, + rating: "thumbs-up" | "thumbs-down" + ) => { + console.log("Rating:", messageId, rating) + }, + [] + ) + + const [panelWidth, setPanelWidth] = useState(420) + const [isResizing, setIsResizing] = useState(false) + const dragStartX = useRef(0) + const dragStartWidth = useRef(0) + + useEffect(() => { + const onMouseMove = (e: MouseEvent) => { + if (!dragStartWidth.current) return + const delta = dragStartX.current - e.clientX + const next = Math.min(720, Math.max(320, dragStartWidth.current + delta)) + setPanelWidth(next) + } + const onMouseUp = () => { + if (!dragStartWidth.current) return + dragStartWidth.current = 0 + setIsResizing(false) + document.body.style.cursor = "" + document.body.style.userSelect = "" + } + window.addEventListener("mousemove", onMouseMove) + window.addEventListener("mouseup", onMouseUp) + return () => { + window.removeEventListener("mousemove", onMouseMove) + window.removeEventListener("mouseup", onMouseUp) + } + }, []) + + const handleResizeStart = useCallback( + (e: React.MouseEvent) => { + e.preventDefault() + setIsResizing(true) + dragStartX.current = e.clientX + dragStartWidth.current = panelWidth + document.body.style.cursor = "col-resize" + document.body.style.userSelect = "none" + }, + [panelWidth] + ) + + // Dashboard has its own inline chat — skip the side panel + if (pathname === "/dashboard") return null + + return ( + <> + {/* Panel — mobile: full-screen overlay, desktop: integrated flex child */} +
+ {/* Desktop resize handle */} +
+ +
+ {/* Chat */} +
+ +
+ + {/* Dynamic UI for agent-generated components */} + {messages.some((m) => m.actions) && ( +
+ {messages + .filter((m) => m.actions) + .slice(-1) + .map((m) => { + const uiAction = m.actions?.find( + (a) => a.type === "RENDER_UI" + ) + if (!uiAction?.payload?.spec) return null + return ( + + ) + })} +
+ )} +
+
+ + {/* Mobile backdrop */} + {isOpen && ( +
setIsOpen(false)} + aria-hidden="true" + /> + )} + + {/* Mobile FAB trigger (desktop uses header button) */} + {!isOpen && ( + + )} + + ) +} + +function getSuggestionsForPath(pathname: string): string[] { + if (pathname.includes("/customers")) { + return [ + "Show me all customers", + "Create a new customer", + "Find customers without email", + ] + } + if (pathname.includes("/vendors")) { + return [ + "List all vendors", + "Add a new subcontractor", + "Show vendors by category", + ] + } + if (pathname.includes("/schedule")) { + return [ + "What tasks are on the critical path?", + "Show overdue tasks", + "Add a new task", + ] + } + if (pathname.includes("/finances")) { + return [ + "Show overdue invoices", + "What payments are pending?", + "Create a new invoice", + ] + } + if (pathname.includes("/projects")) { + return [ + "List all active projects", + "Create a new project", + "Which projects are behind schedule?", + ] + } + if (pathname.includes("/netsuite")) { + return [ + "Sync customers from NetSuite", + "Check for sync conflicts", + "When was the last sync?", + ] + } + + return [ + "What can you help me with?", + "Show me today's tasks", + "Navigate to customers", + ] +} + +export default ChatPanel diff --git a/src/components/agent/dynamic-ui.tsx b/src/components/agent/dynamic-ui.tsx new file mode 100755 index 0000000..72ffaff --- /dev/null +++ b/src/components/agent/dynamic-ui.tsx @@ -0,0 +1,622 @@ +/** + * Dynamic UI Renderer + * + * Renders agent-generated UI specs using shadcn/ui components. + * Handles action callbacks for interactive elements. + */ + +"use client" + +import { useCallback } from "react" +import { executeAction } from "@/lib/eliza/chat-adapter" +import type { ComponentSpec } from "@/lib/eliza/json-render/catalog" + +// Import shadcn components +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Progress } from "@/components/ui/progress" +import { cn } from "@/lib/utils" + +interface DynamicUIProps { + spec: ComponentSpec + className?: string +} + +export function DynamicUI({ spec, className }: DynamicUIProps) { + const handleAction = useCallback( + async (action: { type: string; payload?: Record }) => { + await executeAction(action) + }, + [] + ) + + return ( +
+ +
+ ) +} + +interface RendererProps { + spec: ComponentSpec + onAction: (action: { type: string; payload?: Record }) => void +} + +function ComponentRenderer({ spec, onAction }: RendererProps) { + switch (spec.type) { + case "DataTable": + return + + case "Card": + return + + case "Badge": + return {spec.props.label} + + case "StatCard": + return + + case "Button": + return ( + + ) + + case "ButtonGroup": + return ( +
+ {spec.props.buttons.map((btn, i) => ( + + ))} +
+ ) + + case "InvoiceTable": + return + + case "CustomerCard": + return + + case "VendorCard": + return + + case "SchedulePreview": + return + + case "ProjectSummary": + return + + case "Grid": + return ( +
+ {(spec.props.children as ComponentSpec[])?.map((child, i) => ( + + ))} +
+ ) + + case "Stack": + return ( +
+ {(spec.props.children as ComponentSpec[])?.map((child, i) => ( + + ))} +
+ ) + + default: + return ( +
+ Unknown component type: {(spec as { type: string }).type} +
+ ) + } +} + +// DataTable renderer +function DataTableRenderer({ + columns, + data, + onRowClick, + onAction, +}: { + columns: Array<{ key: string; header: string; format?: string }> + data: Array> + onRowClick?: { type: string; payload?: Record } + onAction: RendererProps["onAction"] +}) { + return ( +
+ + + + {columns.map((col) => ( + {col.header} + ))} + + + + {data.map((row, i) => ( + + onRowClick && + onAction({ + ...onRowClick, + payload: { ...onRowClick.payload, rowData: row }, + }) + } + > + {columns.map((col) => ( + + {formatValue(row[col.key], col.format)} + + ))} + + ))} + +
+
+ ) +} + +// Card renderer +function CardRenderer({ + title, + description, + children, + footer, + onAction, +}: { + title: string + description?: string + children?: unknown[] + footer?: string + onAction: RendererProps["onAction"] +}) { + return ( + + + {title} + {description && {description}} + + {children && children.length > 0 && ( + + {(children as ComponentSpec[]).map((child, i) => ( + + ))} + + )} + {footer && ( + +

{footer}

+
+ )} +
+ ) +} + +// StatCard renderer +function StatCardRenderer({ + title, + value, + change, + changeLabel, +}: { + title: string + value: string | number + change?: number + changeLabel?: string +}) { + return ( + + + {title} + {value} + + {change !== undefined && ( + +
+ = 0 ? "text-green-600" : "text-red-600"}> + {change >= 0 ? "+" : ""} + {change}% + + {changeLabel && ` ${changeLabel}`} +
+
+ )} +
+ ) +} + +// Invoice table renderer +function InvoiceTableRenderer({ + invoices, + onRowClick, + onAction, +}: { + invoices: Array<{ + id: string + number: string + customer: string + amount: number + dueDate: string + status: string + }> + onRowClick?: { type: string; payload?: Record } + onAction: RendererProps["onAction"] +}) { + const statusVariant = (status: string) => { + switch (status) { + case "paid": + return "default" + case "overdue": + return "destructive" + default: + return "secondary" + } + } + + return ( +
+ + + + Invoice # + Customer + Amount + Due Date + Status + + + + {invoices.map((invoice) => ( + + onRowClick && + onAction({ + ...onRowClick, + payload: { ...onRowClick.payload, invoiceId: invoice.id }, + }) + } + > + {invoice.number} + {invoice.customer} + + {formatCurrency(invoice.amount)} + + {formatDate(invoice.dueDate)} + + + {invoice.status} + + + + ))} + +
+
+ ) +} + +// Customer card renderer +function CustomerCardRenderer({ + customer, + actions, + onAction, +}: { + customer: { + id: string + name: string + company?: string + email?: string + phone?: string + } + actions?: Array<{ type: string; payload?: Record }> + onAction: RendererProps["onAction"] +}) { + return ( + + + {customer.name} + {customer.company && ( + {customer.company} + )} + + + {customer.email &&

Email: {customer.email}

} + {customer.phone &&

Phone: {customer.phone}

} +
+ {actions && actions.length > 0 && ( + + {actions.map((action, i) => ( + + ))} + + )} +
+ ) +} + +// Vendor card renderer +function VendorCardRenderer({ + vendor, + actions, + onAction, +}: { + vendor: { + id: string + name: string + category: string + email?: string + phone?: string + } + actions?: Array<{ type: string; payload?: Record }> + onAction: RendererProps["onAction"] +}) { + return ( + + +
+ {vendor.name} + {vendor.category} +
+
+ + {vendor.email &&

Email: {vendor.email}

} + {vendor.phone &&

Phone: {vendor.phone}

} +
+ {actions && actions.length > 0 && ( + + {actions.map((action, i) => ( + + ))} + + )} +
+ ) +} + +// Schedule preview renderer +function SchedulePreviewRenderer({ + projectName, + tasks, + onTaskClick, + onAction, +}: { + projectId: string + projectName: string + tasks: Array<{ + id: string + title: string + startDate: string + endDate: string + phase: string + status: string + percentComplete: number + isCriticalPath?: boolean + }> + onTaskClick?: { type: string; payload?: Record } + onAction: RendererProps["onAction"] +}) { + return ( + + + {projectName} Schedule + {tasks.length} tasks + + + {tasks.slice(0, 5).map((task) => ( +
+ onTaskClick && + onAction({ + ...onTaskClick, + payload: { ...onTaskClick.payload, taskId: task.id }, + }) + } + > +
+ {task.title} + {task.phase} +
+
+ +
+ {task.percentComplete}% complete + + {formatDate(task.startDate)} - {formatDate(task.endDate)} + +
+
+
+ ))} + {tasks.length > 5 && ( +

+ +{tasks.length - 5} more tasks +

+ )} +
+
+ ) +} + +// Project summary renderer +function ProjectSummaryRenderer({ + project, + stats, + actions, + onAction, +}: { + project: { + id: string + name: string + status: string + address?: string + clientName?: string + projectManager?: string + } + stats?: { + tasksTotal: number + tasksComplete: number + daysRemaining?: number + budgetUsed?: number + } + actions?: Array<{ type: string; payload?: Record }> + onAction: RendererProps["onAction"] +}) { + const completion = stats + ? Math.round((stats.tasksComplete / stats.tasksTotal) * 100) + : 0 + + return ( + + +
+ {project.name} + {project.status} +
+ {project.address && ( + {project.address} + )} +
+ + {project.clientName && ( +

Client: {project.clientName}

+ )} + {project.projectManager && ( +

PM: {project.projectManager}

+ )} + {stats && ( +
+
+ Progress + + {stats.tasksComplete}/{stats.tasksTotal} tasks ({completion}%) + +
+ +
+ )} +
+ {actions && actions.length > 0 && ( + + {actions.map((action, i) => ( + + ))} + + )} +
+ ) +} + +// Utility functions +function formatValue(value: unknown, format?: string): React.ReactNode { + if (value === null || value === undefined) return "-" + + switch (format) { + case "currency": + return formatCurrency(Number(value)) + case "date": + return formatDate(String(value)) + case "badge": + return {String(value)} + default: + return String(value) + } +} + +function formatCurrency(amount: number): string { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(amount) +} + +function formatDate(dateStr: string): string { + try { + return new Date(dateStr).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) + } catch { + return dateStr + } +} + +export default DynamicUI diff --git a/src/components/ai/prompt-input.tsx b/src/components/ai/prompt-input.tsx new file mode 100755 index 0000000..5864edf --- /dev/null +++ b/src/components/ai/prompt-input.tsx @@ -0,0 +1,1297 @@ +"use client" + +import type { ChatStatus, FileUIPart } from "ai" +import { + CornerDownLeftIcon, + ImageIcon, + Loader2Icon, + MicIcon, + PaperclipIcon, + PlusIcon, + SquareIcon, + XIcon, +} from "lucide-react" +import { nanoid } from "nanoid" +import { + type ChangeEvent, + type ChangeEventHandler, + Children, + type ClipboardEventHandler, + type ComponentProps, + createContext, + type FormEvent, + type FormEventHandler, + Fragment, + type HTMLAttributes, + type KeyboardEventHandler, + type PropsWithChildren, + type ReactNode, + type RefObject, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react" +import { Button } from "@/components/ui/button" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card" +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupTextarea, +} from "@/components/ui/input-group" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { cn } from "@/lib/utils" + +// ============================================================================ +// Provider Context & Types +// ============================================================================ + +export interface AttachmentsContext { + files: (FileUIPart & { id: string })[] + add: (files: File[] | FileList) => void + remove: (id: string) => void + clear: () => void + openFileDialog: () => void + fileInputRef: RefObject +} + +export interface TextInputContext { + value: string + setInput: (v: string) => void + clear: () => void +} + +export interface PromptInputControllerProps { + textInput: TextInputContext + attachments: AttachmentsContext + /** INTERNAL: Allows PromptInput to register its file textInput + "open" callback */ + __registerFileInput: (ref: RefObject, open: () => void) => void +} + +const PromptInputController = createContext(null) +const ProviderAttachmentsContext = createContext(null) + +export const usePromptInputController = () => { + const ctx = useContext(PromptInputController) + if (!ctx) { + throw new Error( + "Wrap your component inside to use usePromptInputController().", + ) + } + return ctx +} + +// Optional variants (do NOT throw). Useful for dual-mode components. +const useOptionalPromptInputController = () => useContext(PromptInputController) + +export const useProviderAttachments = () => { + const ctx = useContext(ProviderAttachmentsContext) + if (!ctx) { + throw new Error( + "Wrap your component inside to use useProviderAttachments().", + ) + } + return ctx +} + +const useOptionalProviderAttachments = () => useContext(ProviderAttachmentsContext) + +export type PromptInputProviderProps = PropsWithChildren<{ + initialInput?: string +}> + +/** + * Optional global provider that lifts PromptInput state outside of PromptInput. + * If you don't use it, PromptInput stays fully self-managed. + */ +export function PromptInputProvider({ + initialInput: initialTextInput = "", + children, +}: PromptInputProviderProps) { + // ----- textInput state + const [textInput, setTextInput] = useState(initialTextInput) + const clearInput = useCallback(() => setTextInput(""), []) + + // ----- attachments state (global when wrapped) + const [attachmentFiles, setAttachmentFiles] = useState<(FileUIPart & { id: string })[]>([]) + const fileInputRef = useRef(null) + const openRef = useRef<() => void>(() => {}) + + const add = useCallback((files: File[] | FileList) => { + const incoming = Array.from(files) + if (incoming.length === 0) { + return + } + + setAttachmentFiles(prev => + prev.concat( + incoming.map(file => ({ + id: nanoid(), + type: "file" as const, + url: URL.createObjectURL(file), + mediaType: file.type, + filename: file.name, + })), + ), + ) + }, []) + + const remove = useCallback((id: string) => { + setAttachmentFiles(prev => { + const found = prev.find(f => f.id === id) + if (found?.url) { + URL.revokeObjectURL(found.url) + } + return prev.filter(f => f.id !== id) + }) + }, []) + + const clear = useCallback(() => { + setAttachmentFiles(prev => { + for (const f of prev) { + if (f.url) { + URL.revokeObjectURL(f.url) + } + } + return [] + }) + }, []) + + // Keep a ref to attachments for cleanup on unmount (avoids stale closure) + const attachmentsRef = useRef(attachmentFiles) + attachmentsRef.current = attachmentFiles + + // Cleanup blob URLs on unmount to prevent memory leaks + useEffect(() => { + return () => { + for (const f of attachmentsRef.current) { + if (f.url) { + URL.revokeObjectURL(f.url) + } + } + } + }, []) + + const openFileDialog = useCallback(() => { + openRef.current?.() + }, []) + + const attachments = useMemo( + () => ({ + files: attachmentFiles, + add, + remove, + clear, + openFileDialog, + fileInputRef, + }), + [attachmentFiles, add, remove, clear, openFileDialog], + ) + + const __registerFileInput = useCallback( + (ref: RefObject, open: () => void) => { + fileInputRef.current = ref.current + openRef.current = open + }, + [], + ) + + const controller = useMemo( + () => ({ + textInput: { + value: textInput, + setInput: setTextInput, + clear: clearInput, + }, + attachments, + __registerFileInput, + }), + [textInput, clearInput, attachments, __registerFileInput], + ) + + return ( + + + {children} + + + ) +} + +// ============================================================================ +// Component Context & Hooks +// ============================================================================ + +const LocalAttachmentsContext = createContext(null) + +export const usePromptInputAttachments = () => { + // Dual-mode: prefer provider if present, otherwise use local + const provider = useOptionalProviderAttachments() + const local = useContext(LocalAttachmentsContext) + const context = provider ?? local + if (!context) { + throw new Error( + "usePromptInputAttachments must be used within a PromptInput or PromptInputProvider", + ) + } + return context +} + +export type PromptInputAttachmentProps = HTMLAttributes & { + data: FileUIPart & { id: string } + className?: string +} + +export function PromptInputAttachment({ data, className, ...props }: PromptInputAttachmentProps) { + const attachments = usePromptInputAttachments() + + const filename = data.filename || "" + + const mediaType = data.mediaType?.startsWith("image/") && data.url ? "image" : "file" + const isImage = mediaType === "image" + + const attachmentLabel = filename || (isImage ? "Image" : "Attachment") + + return ( + + +
+
+
+ {isImage ? ( + {filename + ) : ( +
+ +
+ )} +
+ +
+ + {attachmentLabel} +
+
+ +
+ {isImage && ( +
+ {filename +
+ )} +
+
+

+ {filename || (isImage ? "Image" : "Attachment")} +

+ {data.mediaType && ( +

{data.mediaType}

+ )} +
+
+
+
+
+ ) +} + +export type PromptInputAttachmentsProps = Omit, "children"> & { + children: (attachment: FileUIPart & { id: string }) => ReactNode +} + +export function PromptInputAttachments({ + children, + className, + ...props +}: PromptInputAttachmentsProps) { + const attachments = usePromptInputAttachments() + + if (!attachments.files.length) { + return null + } + + return ( +
+ {attachments.files.map(file => ( + {children(file)} + ))} +
+ ) +} + +export type PromptInputActionAddAttachmentsProps = ComponentProps & { + label?: string +} + +export const PromptInputActionAddAttachments = ({ + label = "Add photos or files", + ...props +}: PromptInputActionAddAttachmentsProps) => { + const attachments = usePromptInputAttachments() + + return ( + { + e.preventDefault() + attachments.openFileDialog() + }} + > + {label} + + ) +} + +export interface PromptInputMessage { + text: string + files: FileUIPart[] +} + +export type PromptInputProps = Omit, "onSubmit" | "onError"> & { + accept?: string // e.g., "image/*" or leave undefined for any + multiple?: boolean + // When true, accepts drops anywhere on document. Default false (opt-in). + globalDrop?: boolean + // Render a hidden input with given name and keep it in sync for native form posts. Default false. + syncHiddenInput?: boolean + // Minimal constraints + maxFiles?: number + maxFileSize?: number // bytes + onError?: (err: { code: "max_files" | "max_file_size" | "accept"; message: string }) => void + onSubmit: (message: PromptInputMessage, event: FormEvent) => void | Promise +} + +export const PromptInput = ({ + className, + accept, + multiple, + globalDrop, + syncHiddenInput, + maxFiles, + maxFileSize, + onError, + onSubmit, + children, + ...props +}: PromptInputProps) => { + // Try to use a provider controller if present + const controller = useOptionalPromptInputController() + const usingProvider = !!controller + + // Refs + const inputRef = useRef(null) + const formRef = useRef(null) + + // ----- Local attachments (only used when no provider) + const [items, setItems] = useState<(FileUIPart & { id: string })[]>([]) + const files = usingProvider ? controller.attachments.files : items + + // Keep a ref to files for cleanup on unmount (avoids stale closure) + const filesRef = useRef(files) + filesRef.current = files + + const openFileDialogLocal = useCallback(() => { + inputRef.current?.click() + }, []) + + const matchesAccept = useCallback( + (f: File) => { + if (!accept || accept.trim() === "") { + return true + } + + const patterns = accept + .split(",") + .map(s => s.trim()) + .filter(Boolean) + + return patterns.some(pattern => { + if (pattern.endsWith("/*")) { + const prefix = pattern.slice(0, -1) // e.g: image/* -> image/ + return f.type.startsWith(prefix) + } + return f.type === pattern + }) + }, + [accept], + ) + + const addLocal = useCallback( + (fileList: File[] | FileList) => { + const incoming = Array.from(fileList) + const accepted = incoming.filter(f => matchesAccept(f)) + if (incoming.length && accepted.length === 0) { + onError?.({ + code: "accept", + message: "No files match the accepted types.", + }) + return + } + const withinSize = (f: File) => (maxFileSize ? f.size <= maxFileSize : true) + const sized = accepted.filter(withinSize) + if (accepted.length > 0 && sized.length === 0) { + onError?.({ + code: "max_file_size", + message: "All files exceed the maximum size.", + }) + return + } + + setItems(prev => { + const capacity = + typeof maxFiles === "number" ? Math.max(0, maxFiles - prev.length) : undefined + const capped = typeof capacity === "number" ? sized.slice(0, capacity) : sized + if (typeof capacity === "number" && sized.length > capacity) { + onError?.({ + code: "max_files", + message: "Too many files. Some were not added.", + }) + } + const next: (FileUIPart & { id: string })[] = [] + for (const file of capped) { + next.push({ + id: nanoid(), + type: "file", + url: URL.createObjectURL(file), + mediaType: file.type, + filename: file.name, + }) + } + return prev.concat(next) + }) + }, + [matchesAccept, maxFiles, maxFileSize, onError], + ) + + const removeLocal = useCallback( + (id: string) => + setItems(prev => { + const found = prev.find(file => file.id === id) + if (found?.url) { + URL.revokeObjectURL(found.url) + } + return prev.filter(file => file.id !== id) + }), + [], + ) + + const clearLocal = useCallback( + () => + setItems(prev => { + for (const file of prev) { + if (file.url) { + URL.revokeObjectURL(file.url) + } + } + return [] + }), + [], + ) + + const add = usingProvider ? controller.attachments.add : addLocal + const remove = usingProvider ? controller.attachments.remove : removeLocal + const clear = usingProvider ? controller.attachments.clear : clearLocal + const openFileDialog = usingProvider ? controller.attachments.openFileDialog : openFileDialogLocal + + // Let provider know about our hidden file input so external menus can call openFileDialog() + useEffect(() => { + if (!usingProvider) { + return + } + controller.__registerFileInput(inputRef, () => inputRef.current?.click()) + }, [usingProvider, controller]) + + // Note: File input cannot be programmatically set for security reasons + // The syncHiddenInput prop is no longer functional + useEffect(() => { + if (syncHiddenInput && inputRef.current && files.length === 0) { + inputRef.current.value = "" + } + }, [files, syncHiddenInput]) + + // Attach drop handlers on nearest form and document (opt-in) + useEffect(() => { + const form = formRef.current + if (!form) { + return + } + if (globalDrop) { + return // when global drop is on, let the document-level handler own drops + } + + const onDragOver = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault() + } + } + const onDrop = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault() + } + if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { + add(e.dataTransfer.files) + } + } + form.addEventListener("dragover", onDragOver) + form.addEventListener("drop", onDrop) + return () => { + form.removeEventListener("dragover", onDragOver) + form.removeEventListener("drop", onDrop) + } + }, [add, globalDrop]) + + useEffect(() => { + if (!globalDrop) { + return + } + + const onDragOver = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault() + } + } + const onDrop = (e: DragEvent) => { + if (e.dataTransfer?.types?.includes("Files")) { + e.preventDefault() + } + if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { + add(e.dataTransfer.files) + } + } + document.addEventListener("dragover", onDragOver) + document.addEventListener("drop", onDrop) + return () => { + document.removeEventListener("dragover", onDragOver) + document.removeEventListener("drop", onDrop) + } + }, [add, globalDrop]) + + useEffect( + () => () => { + if (!usingProvider) { + for (const f of filesRef.current) { + if (f.url) { + URL.revokeObjectURL(f.url) + } + } + } + }, + [usingProvider], + ) + + const handleChange: ChangeEventHandler = event => { + if (event.currentTarget.files) { + add(event.currentTarget.files) + } + // Reset input value to allow selecting files that were previously removed + event.currentTarget.value = "" + } + + const convertBlobUrlToDataUrl = async (url: string): Promise => { + try { + const response = await fetch(url) + const blob = await response.blob() + return new Promise(resolve => { + const reader = new FileReader() + reader.onloadend = () => resolve(reader.result as string) + reader.onerror = () => resolve(null) + reader.readAsDataURL(blob) + }) + } catch { + return null + } + } + + const ctx = useMemo( + () => ({ + files: files.map(item => ({ ...item, id: item.id })), + add, + remove, + clear, + openFileDialog, + fileInputRef: inputRef, + }), + [files, add, remove, clear, openFileDialog], + ) + + const handleSubmit: FormEventHandler = event => { + event.preventDefault() + + const form = event.currentTarget + const text = usingProvider + ? controller.textInput.value + : (() => { + const formData = new FormData(form) + return (formData.get("message") as string) || "" + })() + + // Reset form immediately after capturing text to avoid race condition + // where user input during async blob conversion would be lost + if (!usingProvider) { + form.reset() + } + + // Convert blob URLs to data URLs asynchronously + Promise.all( + files.map(async ({ id: _, ...item }) => { + if (item.url?.startsWith("blob:")) { + const dataUrl = await convertBlobUrlToDataUrl(item.url) + // If conversion failed, keep the original blob URL + return { + ...item, + url: dataUrl ?? item.url, + } + } + return item + }), + ) + .then((convertedFiles: FileUIPart[]) => { + try { + const result = onSubmit({ text, files: convertedFiles }, event) + + // Handle both sync and async onSubmit + if (result instanceof Promise) { + result + .then(() => { + clear() + if (usingProvider) { + controller.textInput.clear() + } + }) + .catch(() => { + // Don't clear on error - user may want to retry + }) + } else { + // Sync function completed without throwing, clear attachments + clear() + if (usingProvider) { + controller.textInput.clear() + } + } + } catch { + // Don't clear on error - user may want to retry + } + }) + .catch(() => { + // Don't clear on error - user may want to retry + }) + } + + // Render with or without local provider + const inner = ( + <> + +
+ {children} +
+ + ) + + return usingProvider ? ( + inner + ) : ( + {inner} + ) +} + +export type PromptInputBodyProps = HTMLAttributes + +export const PromptInputBody = ({ className, ...props }: PromptInputBodyProps) => ( +
+) + +export type PromptInputTextareaProps = ComponentProps + +export const PromptInputTextarea = ({ + onChange, + className, + placeholder = "What would you like to know?", + ...props +}: PromptInputTextareaProps) => { + const controller = useOptionalPromptInputController() + const attachments = usePromptInputAttachments() + const [isComposing, setIsComposing] = useState(false) + + const handleKeyDown: KeyboardEventHandler = e => { + if (e.key === "Enter") { + if (isComposing || e.nativeEvent.isComposing) { + return + } + if (e.shiftKey) { + return + } + e.preventDefault() + + // Check if the submit button is disabled before submitting + const form = e.currentTarget.form + const submitButton = form?.querySelector('button[type="submit"]') as HTMLButtonElement | null + if (submitButton?.disabled) { + return + } + + form?.requestSubmit() + } + + // Remove last attachment when Backspace is pressed and textarea is empty + if (e.key === "Backspace" && e.currentTarget.value === "" && attachments.files.length > 0) { + e.preventDefault() + const lastAttachment = attachments.files.at(-1) + if (lastAttachment) { + attachments.remove(lastAttachment.id) + } + } + } + + const handlePaste: ClipboardEventHandler = event => { + const items = event.clipboardData?.items + + if (!items) { + return + } + + const files: File[] = [] + + for (const item of items) { + if (item.kind === "file") { + const file = item.getAsFile() + if (file) { + files.push(file) + } + } + } + + if (files.length > 0) { + event.preventDefault() + attachments.add(files) + } + } + + const controlledProps = controller + ? { + value: controller.textInput.value, + onChange: (e: ChangeEvent) => { + controller.textInput.setInput(e.currentTarget.value) + onChange?.(e) + }, + } + : { + onChange, + } + + return ( + setIsComposing(false)} + onCompositionStart={() => setIsComposing(true)} + onKeyDown={handleKeyDown} + onPaste={handlePaste} + placeholder={placeholder} + {...props} + {...controlledProps} + /> + ) +} + +export type PromptInputHeaderProps = Omit, "align"> + +export const PromptInputHeader = ({ className, ...props }: PromptInputHeaderProps) => ( + +) + +export type PromptInputFooterProps = Omit, "align"> + +export const PromptInputFooter = ({ className, ...props }: PromptInputFooterProps) => ( + +) + +export type PromptInputToolsProps = HTMLAttributes + +export const PromptInputTools = ({ className, ...props }: PromptInputToolsProps) => ( +
+) + +export type PromptInputButtonProps = ComponentProps + +export const PromptInputButton = ({ + variant = "ghost", + className, + size, + ...props +}: PromptInputButtonProps) => { + const newSize = size ?? (Children.count(props.children) > 1 ? "sm" : "icon-sm") + + return ( + + ) +} + +export type PromptInputActionMenuProps = ComponentProps +export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => ( + +) + +export type PromptInputActionMenuTriggerProps = PromptInputButtonProps + +export const PromptInputActionMenuTrigger = ({ + className, + children, + ...props +}: PromptInputActionMenuTriggerProps) => ( + + + {children ?? } + + +) + +export type PromptInputActionMenuContentProps = ComponentProps +export const PromptInputActionMenuContent = ({ + className, + ...props +}: PromptInputActionMenuContentProps) => ( + +) + +export type PromptInputActionMenuItemProps = ComponentProps +export const PromptInputActionMenuItem = ({ + className, + ...props +}: PromptInputActionMenuItemProps) => + +// Note: Actions that perform side-effects (like opening a file dialog) +// are provided in opt-in modules (e.g., prompt-input-attachments). + +export type PromptInputSubmitProps = ComponentProps & { + status?: ChatStatus +} + +export const PromptInputSubmit = ({ + className, + variant = "default", + size = "icon-sm", + status, + children, + ...props +}: PromptInputSubmitProps) => { + let Icon = + + if (status === "submitted") { + Icon = + } else if (status === "streaming") { + Icon = + } else if (status === "error") { + Icon = + } + + return ( + + {children ?? Icon} + + ) +} + +interface SpeechRecognition extends EventTarget { + continuous: boolean + interimResults: boolean + lang: string + start(): void + stop(): void + onstart: ((this: SpeechRecognition, ev: Event) => void) | null + onend: ((this: SpeechRecognition, ev: Event) => void) | null + onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null + onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void) | null +} + +interface SpeechRecognitionEvent extends Event { + results: SpeechRecognitionResultList + resultIndex: number +} + +interface SpeechRecognitionResultList { + readonly length: number + item(index: number): SpeechRecognitionResult + [index: number]: SpeechRecognitionResult +} + +interface SpeechRecognitionResult { + readonly length: number + item(index: number): SpeechRecognitionAlternative + [index: number]: SpeechRecognitionAlternative + isFinal: boolean +} + +interface SpeechRecognitionAlternative { + transcript: string + confidence: number +} + +interface SpeechRecognitionErrorEvent extends Event { + error: string +} + +declare global { + interface Window { + SpeechRecognition: { + new (): SpeechRecognition + } + webkitSpeechRecognition: { + new (): SpeechRecognition + } + } +} + +export type PromptInputSpeechButtonProps = ComponentProps & { + textareaRef?: RefObject + onTranscriptionChange?: (text: string) => void +} + +export const PromptInputSpeechButton = ({ + className, + textareaRef, + onTranscriptionChange, + ...props +}: PromptInputSpeechButtonProps) => { + const [isListening, setIsListening] = useState(false) + const [recognition, setRecognition] = useState(null) + const recognitionRef = useRef(null) + + useEffect(() => { + if ( + typeof window !== "undefined" && + ("SpeechRecognition" in window || "webkitSpeechRecognition" in window) + ) { + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition + const speechRecognition = new SpeechRecognition() + + speechRecognition.continuous = true + speechRecognition.interimResults = true + speechRecognition.lang = "en-US" + + speechRecognition.onstart = () => { + setIsListening(true) + } + + speechRecognition.onend = () => { + setIsListening(false) + } + + speechRecognition.onresult = event => { + let finalTranscript = "" + + for (let i = event.resultIndex; i < event.results.length; i++) { + const result = event.results[i] + if (result.isFinal) { + finalTranscript += result[0]?.transcript ?? "" + } + } + + if (finalTranscript && textareaRef?.current) { + const textarea = textareaRef.current + const currentValue = textarea.value + const newValue = currentValue + (currentValue ? " " : "") + finalTranscript + + textarea.value = newValue + textarea.dispatchEvent(new Event("input", { bubbles: true })) + onTranscriptionChange?.(newValue) + } + } + + speechRecognition.onerror = event => { + console.error("Speech recognition error:", event.error) + setIsListening(false) + } + + recognitionRef.current = speechRecognition + setRecognition(speechRecognition) + } + + return () => { + if (recognitionRef.current) { + recognitionRef.current.stop() + } + } + }, [textareaRef, onTranscriptionChange]) + + const toggleListening = useCallback(() => { + if (!recognition) { + return + } + + if (isListening) { + recognition.stop() + } else { + recognition.start() + } + }, [recognition, isListening]) + + return ( + + + + ) +} + +export type PromptInputSelectProps = ComponentProps + +export const PromptInputSelect = (props: PromptInputSelectProps) => + setIdleInput(e.target.value) + } + onFocus={() => setIsIdleFocused(true)} + onBlur={() => setIsIdleFocused(false)} + placeholder={ + animatedPlaceholder || + "Ask anything..." + } + className={cn( + "flex-1 bg-transparent text-foreground outline-none", + "placeholder:text-muted-foreground placeholder:transition-opacity placeholder:duration-300", + animFading + ? "placeholder:opacity-0" + : "placeholder:opacity-100" + )} + /> + + + + + {stats && ( +
+ + + View on GitHub + + + + | + + + {REPO} + +
+ + + {stats.stargazers_count} + + + + {stats.forks_count} + + + + {stats.open_issues_count} + + + + {stats.subscribers_count} + +
+
+ )} +
+
+ + {/* Active: messages or suggestions */} +
+ {messages.length > 0 ? ( +
+
+ {messages.map((msg) => { + if (msg.role === "user") { + return ( +
+
+ {msg.content} +
+
+ ) + } + return ( +
+ {msg.content ? ( + <> +
+ + {msg.content} + +
+
+ + + + +
+ + ) : ( + + )} +
+ ) + })} +
+
+ ) : ( +
+
+ +
+
+ )} +
+
+ + {/* Bottom input - active only */} +
+
{ + e.preventDefault() + const trimmed = chatInput.trim() + if (!trimmed || isGenerating) return + append({ role: "user", content: trimmed }) + setChatInput("") + }} + > +
+