diff --git a/CLAUDE.md b/CLAUDE.md index 7eb90ff..ea1c5c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,6 +48,11 @@ bun run test:e2e # Coverage report bun run test:coverage +# Library build (tsup) — exports @libredb/studio package for platform consumption +# IMPORTANT: After changing any component used by platform (workspace, providers, etc.), +# you MUST run this command. `bun run build` (Next.js) does NOT update the package dist. +bun run build:lib + # Docker development docker-compose up -d diff --git a/bun.lock b/bun.lock index 54a7e8f..c6a0611 100644 --- a/bun.lock +++ b/bun.lock @@ -91,9 +91,14 @@ "eslint-config-next": "^16.1.6", "happy-dom": "^20.6.1", "tailwindcss": "^4", + "tsup": "^8.5.1", "tw-animate-css": "^1.4.0", "typescript": "^5", }, + "peerDependencies": { + "react": "^19", + "react-dom": "^19", + }, }, }, "packages": { @@ -177,6 +182,58 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], @@ -425,6 +482,56 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.0", "", { "os": "none", "cpu": "arm64" }, "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w=="], + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], @@ -619,6 +726,8 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], @@ -689,6 +798,10 @@ "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -701,6 +814,8 @@ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], @@ -723,6 +838,10 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], @@ -855,6 +974,8 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -921,6 +1042,8 @@ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], @@ -1087,6 +1210,8 @@ "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -1143,6 +1268,12 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], @@ -1201,6 +1332,8 @@ "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], "mongodb": ["mongodb@7.0.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^7.0.0", "mongodb-connection-string-url": "^7.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.806.0", "@mongodb-js/zstd": "^7.0.0", "gcp-metadata": "^7.0.1", "kerberos": "^7.0.0", "mongodb-client-encryption": ">=7.0.0 <7.1.0", "snappy": "^7.3.2", "socks": "^2.8.6" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg=="], @@ -1219,6 +1352,8 @@ "mysql2": ["mysql2@3.16.0", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-AEGW7QLLSuSnjCS4pk3EIqOmogegmze9h8EyrndavUQnIUcfkVal/sK7QznE+a3bc6rzPbAiui9Jcb+96tPwYA=="], + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], "nan": ["nan@2.25.0", "", {}, "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g=="], @@ -1285,6 +1420,8 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="], "pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="], @@ -1305,6 +1442,10 @@ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], @@ -1313,6 +1454,8 @@ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], @@ -1367,6 +1510,8 @@ "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "recharts": ["recharts@2.15.4", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="], "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="], @@ -1383,7 +1528,7 @@ "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=="], + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], @@ -1391,6 +1536,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.0", "@rollup/rollup-android-arm64": "4.60.0", "@rollup/rollup-darwin-arm64": "4.60.0", "@rollup/rollup-darwin-x64": "4.60.0", "@rollup/rollup-freebsd-arm64": "4.60.0", "@rollup/rollup-freebsd-x64": "4.60.0", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", "@rollup/rollup-linux-arm-musleabihf": "4.60.0", "@rollup/rollup-linux-arm64-gnu": "4.60.0", "@rollup/rollup-linux-arm64-musl": "4.60.0", "@rollup/rollup-linux-loong64-gnu": "4.60.0", "@rollup/rollup-linux-loong64-musl": "4.60.0", "@rollup/rollup-linux-ppc64-gnu": "4.60.0", "@rollup/rollup-linux-ppc64-musl": "4.60.0", "@rollup/rollup-linux-riscv64-gnu": "4.60.0", "@rollup/rollup-linux-riscv64-musl": "4.60.0", "@rollup/rollup-linux-s390x-gnu": "4.60.0", "@rollup/rollup-linux-x64-gnu": "4.60.0", "@rollup/rollup-linux-x64-musl": "4.60.0", "@rollup/rollup-openbsd-x64": "4.60.0", "@rollup/rollup-openharmony-arm64": "4.60.0", "@rollup/rollup-win32-arm64-msvc": "4.60.0", "@rollup/rollup-win32-ia32-msvc": "4.60.0", "@rollup/rollup-win32-x64-gnu": "4.60.0", "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ=="], + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -1437,6 +1584,8 @@ "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], @@ -1481,6 +1630,8 @@ "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=="], + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], @@ -1501,20 +1652,32 @@ "text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="], + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], @@ -1535,6 +1698,8 @@ "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="], + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -1675,12 +1840,16 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "jsonwebtoken/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "mlly/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "nearley/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "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=="], @@ -1697,6 +1866,8 @@ "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "tar-stream/bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], diff --git a/docs/superpowers/plans/2026-03-26-studio-workspace-export.md b/docs/superpowers/plans/2026-03-26-studio-workspace-export.md new file mode 100644 index 0000000..0d116f5 --- /dev/null +++ b/docs/superpowers/plans/2026-03-26-studio-workspace-export.md @@ -0,0 +1,1482 @@ +# StudioWorkspace Composite Export — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Export the entire Studio workspace as a single `` composite component that platform can import with callback props, giving it the full IDE experience without rebuilding the orchestration layer. + +**Architecture:** Adapter pattern — create new hooks (`useConnectionAdapter`, `useQueryAdapter`) that have the same return shape as the original hooks but delegate data operations to callback props instead of internal API routes. StudioWorkspace.tsx composes these adapters with existing UI components (QueryEditor, ResultsGrid, BottomPanel, etc.) to provide the full workspace. + +**Tech Stack:** React 19, TypeScript (strict), tsup (build), bun:test + @testing-library/react (tests) + +**Spec:** `docs/superpowers/specs/2026-03-26-studio-workspace-export-design.md` + +--- + +### Task 1: Workspace Types + +**Files:** +- Create: `src/workspace/types.ts` + +- [ ] **Step 1: Create workspace types file** + +```typescript +// src/workspace/types.ts +import type { DatabaseType, TableSchema, QueryResult, SavedQuery } from '@/lib/types'; + +// === Connection (platform → studio) === + +export interface WorkspaceConnection { + id: string; + name: string; + type: DatabaseType; +} + +// === User (platform → studio) === + +export interface WorkspaceUser { + id: string; + name?: string; + role?: string; +} + +// === Query result (studio ← platform) === + +export interface WorkspaceQueryResult { + rows: Record[]; + fields: string[]; + columns?: { name: string; type?: string }[]; + rowCount: number; + executionTime: number; + pagination?: { + limit: number; + offset: number; + hasMore: boolean; + totalReturned: number; + wasLimited: boolean; + }; +} + +// === Feature flags === + +export interface WorkspaceFeatures { + ai?: boolean; + charts?: boolean; + codeGenerator?: boolean; + testDataGenerator?: boolean; + schemaDiagram?: boolean; + dataImport?: boolean; + inlineEditing?: boolean; + transactions?: boolean; + connectionManagement?: boolean; + dataMasking?: boolean; +} + +export const DEFAULT_WORKSPACE_FEATURES: Required = { + ai: false, + charts: true, + codeGenerator: true, + testDataGenerator: true, + schemaDiagram: true, + dataImport: true, + inlineEditing: false, + transactions: false, + connectionManagement: false, + dataMasking: false, +}; + +// === Saved query input === + +export interface SavedQueryInput { + name: string; + query: string; + description?: string; + connectionType?: string; + tags?: string[]; +} + +// === Main props === + +export interface StudioWorkspaceProps { + connections: WorkspaceConnection[]; + currentUser?: WorkspaceUser; + + onQueryExecute: (connectionId: string, sql: string, options?: { + limit?: number; + offset?: number; + unlimited?: boolean; + }) => Promise; + onSchemaFetch: (connectionId: string) => Promise; + + onTestConnection?: (config: { + type: DatabaseType; + host: string; + port: number; + database: string; + username: string; + password: string; + sslEnabled?: boolean; + }) => Promise<{ success: boolean; message: string }>; + onSaveQuery?: (query: SavedQueryInput) => Promise; + onLoadSavedQueries?: () => Promise; + + features?: WorkspaceFeatures; + className?: string; +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/workspace/types.ts +git commit -m "feat(workspace): add StudioWorkspace types and props interface" +``` + +--- + +### Task 2: Connection Adapter Hook + +**Files:** +- Create: `src/workspace/hooks/use-connection-adapter.ts` +- Test: `tests/hooks/use-connection-adapter.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// tests/hooks/use-connection-adapter.test.ts +import '../setup-dom'; + +import { describe, test, expect, beforeEach } from 'bun:test'; +import { renderHook, act, waitFor } from '@testing-library/react'; +import { useConnectionAdapter } from '@/workspace/hooks/use-connection-adapter'; +import type { WorkspaceConnection } from '@/workspace/types'; +import type { TableSchema } from '@/lib/types'; + +const makeConnections = (): WorkspaceConnection[] => [ + { id: 'conn-1', name: 'Production DB', type: 'postgres' }, + { id: 'conn-2', name: 'Analytics DB', type: 'mysql' }, +]; + +const makeSchema = (): TableSchema[] => [ + { + name: 'users', + columns: [ + { name: 'id', type: 'integer', nullable: false, isPrimary: true }, + { name: 'email', type: 'varchar', nullable: false, isPrimary: false }, + ], + indexes: [{ name: 'users_pkey', columns: ['id'], unique: true }], + rowCount: 100, + }, +]; + +describe('useConnectionAdapter', () => { + test('initializes with first connection as active', () => { + const connections = makeConnections(); + const { result } = renderHook(() => + useConnectionAdapter({ + connections, + onSchemaFetch: async () => [], + }) + ); + + expect(result.current.connections).toEqual(connections); + expect(result.current.activeConnection?.id).toBe('conn-1'); + }); + + test('returns null activeConnection when connections array is empty', () => { + const { result } = renderHook(() => + useConnectionAdapter({ + connections: [], + onSchemaFetch: async () => [], + }) + ); + + expect(result.current.activeConnection).toBeNull(); + }); + + test('setActiveConnection updates active connection', () => { + const connections = makeConnections(); + const { result } = renderHook(() => + useConnectionAdapter({ + connections, + onSchemaFetch: async () => [], + }) + ); + + act(() => { + result.current.setActiveConnection(connections[1]); + }); + + expect(result.current.activeConnection?.id).toBe('conn-2'); + }); + + test('fetchSchema calls onSchemaFetch and updates schema state', async () => { + const schemaData = makeSchema(); + let fetchedConnectionId: string | null = null; + + const { result } = renderHook(() => + useConnectionAdapter({ + connections: makeConnections(), + onSchemaFetch: async (connId) => { + fetchedConnectionId = connId; + return schemaData; + }, + }) + ); + + await act(async () => { + await result.current.fetchSchema(result.current.activeConnection!); + }); + + expect(fetchedConnectionId).toBe('conn-1'); + expect(result.current.schema).toEqual(schemaData); + expect(result.current.tableNames).toEqual(['users']); + expect(result.current.isLoadingSchema).toBe(false); + }); + + test('fetchSchema sets isLoadingSchema during fetch', async () => { + let resolveSchema: ((val: TableSchema[]) => void) | null = null; + const schemaPromise = new Promise((resolve) => { + resolveSchema = resolve; + }); + + const { result } = renderHook(() => + useConnectionAdapter({ + connections: makeConnections(), + onSchemaFetch: async () => schemaPromise, + }) + ); + + // Start fetch (don't await) + let fetchPromise: Promise; + act(() => { + fetchPromise = result.current.fetchSchema(result.current.activeConnection!); + }); + + expect(result.current.isLoadingSchema).toBe(true); + + // Resolve + await act(async () => { + resolveSchema!(makeSchema()); + await fetchPromise!; + }); + + expect(result.current.isLoadingSchema).toBe(false); + }); + + test('updates connections when props change', () => { + const initial = makeConnections(); + const { result, rerender } = renderHook( + ({ connections }) => + useConnectionAdapter({ + connections, + onSchemaFetch: async () => [], + }), + { initialProps: { connections: initial } } + ); + + expect(result.current.connections).toHaveLength(2); + + const updated = [...initial, { id: 'conn-3', name: 'New DB', type: 'sqlite' as const }]; + rerender({ connections: updated }); + + expect(result.current.connections).toHaveLength(3); + }); + + test('resets activeConnection when it is removed from connections', () => { + const initial = makeConnections(); + const { result, rerender } = renderHook( + ({ connections }) => + useConnectionAdapter({ + connections, + onSchemaFetch: async () => [], + }), + { initialProps: { connections: initial } } + ); + + // Set active to conn-2 + act(() => { + result.current.setActiveConnection(initial[1]); + }); + expect(result.current.activeConnection?.id).toBe('conn-2'); + + // Remove conn-2 + rerender({ connections: [initial[0]] }); + + expect(result.current.activeConnection?.id).toBe('conn-1'); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd /home/cevheri/projects/libredb/libredb-studio && bun run test tests/hooks/use-connection-adapter.test.ts` +Expected: FAIL — module `@/workspace/hooks/use-connection-adapter` not found + +- [ ] **Step 3: Write the implementation** + +```typescript +// src/workspace/hooks/use-connection-adapter.ts +'use client'; + +import { useState, useEffect, useCallback, useMemo } from 'react'; +import type { DatabaseConnection, TableSchema } from '@/lib/types'; +import type { WorkspaceConnection } from '@/workspace/types'; + +interface UseConnectionAdapterParams { + connections: WorkspaceConnection[]; + onSchemaFetch: (connectionId: string) => Promise; +} + +/** + * Adapter hook that provides the same interface as useConnectionManager + * but sources data from props and callbacks instead of internal API routes. + */ +export function useConnectionAdapter({ + connections: externalConnections, + onSchemaFetch, +}: UseConnectionAdapterParams) { + // Convert WorkspaceConnection[] to DatabaseConnection[] (add required fields) + const connections: DatabaseConnection[] = useMemo( + () => + externalConnections.map((c) => ({ + id: c.id, + name: c.name, + type: c.type, + createdAt: new Date(), + managed: true, // platform-managed connections + })), + [externalConnections] + ); + + const [activeConnection, setActiveConnection] = useState( + connections[0] ?? null + ); + const [schema, setSchema] = useState([]); + const [isLoadingSchema, setIsLoadingSchema] = useState(false); + + // Sync activeConnection when connections prop changes + useEffect(() => { + if (connections.length === 0) { + setActiveConnection(null); + return; + } + + // If current active is still in the list, keep it + if (activeConnection && connections.some((c) => c.id === activeConnection.id)) { + return; + } + + // Otherwise, select first + setActiveConnection(connections[0]); + }, [connections]); // eslint-disable-line react-hooks/exhaustive-deps + + const fetchSchema = useCallback( + async (conn: DatabaseConnection) => { + setIsLoadingSchema(true); + try { + const result = await onSchemaFetch(conn.id); + setSchema(result); + } catch { + setSchema([]); + } finally { + setIsLoadingSchema(false); + } + }, + [onSchemaFetch] + ); + + const tableNames = useMemo(() => schema.map((s) => s.name), [schema]); + const schemaContext = useMemo(() => JSON.stringify(schema), [schema]); + + return { + connections, + setConnections: () => {}, // no-op — platform controls connections + activeConnection, + setActiveConnection: setActiveConnection as (conn: DatabaseConnection | null) => void, + schema, + setSchema, + isLoadingSchema, + connectionPulse: null as 'healthy' | 'degraded' | 'error' | null, + fetchSchema, + tableNames, + schemaContext, + }; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd /home/cevheri/projects/libredb/libredb-studio && bun run test tests/hooks/use-connection-adapter.test.ts` +Expected: All 7 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/workspace/hooks/use-connection-adapter.ts tests/hooks/use-connection-adapter.test.ts +git commit -m "feat(workspace): add useConnectionAdapter hook with tests" +``` + +--- + +### Task 3: Query Adapter Hook + +**Files:** +- Create: `src/workspace/hooks/use-query-adapter.ts` +- Test: `tests/hooks/use-query-adapter.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// tests/hooks/use-query-adapter.test.ts +import '../setup-dom'; + +import { describe, test, expect, beforeEach } from 'bun:test'; +import { renderHook, act, waitFor } from '@testing-library/react'; +import { useQueryAdapter } from '@/workspace/hooks/use-query-adapter'; +import type { QueryTab } from '@/lib/types'; +import type { WorkspaceQueryResult, WorkspaceConnection } from '@/workspace/types'; + +const mockConnection: WorkspaceConnection & { createdAt: Date; managed: boolean } = { + id: 'conn-1', + name: 'Test DB', + type: 'postgres', + createdAt: new Date(), + managed: true, +}; + +const mockQueryResult: WorkspaceQueryResult = { + rows: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }], + fields: ['id', 'name'], + rowCount: 2, + executionTime: 42, +}; + +const defaultTab: QueryTab = { + id: 'tab-1', + name: 'Query 1', + query: 'SELECT * FROM users', + result: null, + isExecuting: false, + type: 'sql', +}; + +describe('useQueryAdapter', () => { + test('executeQuery calls onQueryExecute and updates tab with result', async () => { + let executedSql: string | null = null; + let executedConnId: string | null = null; + + const tabs = [defaultTab]; + const setTabs = (fn: (prev: QueryTab[]) => QueryTab[]) => { + const updated = fn(tabs); + tabs.splice(0, tabs.length, ...updated); + }; + + const { result } = renderHook(() => + useQueryAdapter({ + activeConnection: mockConnection, + onQueryExecute: async (connId, sql) => { + executedConnId = connId; + executedSql = sql; + return mockQueryResult; + }, + tabs, + activeTabId: 'tab-1', + currentTab: defaultTab, + setTabs: setTabs as any, + fetchSchema: async () => {}, + features: {}, + }) + ); + + await act(async () => { + await result.current.executeQuery('SELECT 1'); + }); + + expect(executedConnId).toBe('conn-1'); + expect(executedSql).toBe('SELECT 1'); + }); + + test('executeQuery uses tab query when no override provided', async () => { + let executedSql: string | null = null; + const tabs = [defaultTab]; + const setTabs = (fn: (prev: QueryTab[]) => QueryTab[]) => { + const updated = fn(tabs); + tabs.splice(0, tabs.length, ...updated); + }; + + const { result } = renderHook(() => + useQueryAdapter({ + activeConnection: mockConnection, + onQueryExecute: async (_connId, sql) => { + executedSql = sql; + return mockQueryResult; + }, + tabs, + activeTabId: 'tab-1', + currentTab: defaultTab, + setTabs: setTabs as any, + fetchSchema: async () => {}, + features: {}, + }) + ); + + await act(async () => { + await result.current.executeQuery(); + }); + + expect(executedSql).toBe('SELECT * FROM users'); + }); + + test('returns error state when onQueryExecute throws', async () => { + const tabs = [defaultTab]; + const setTabs = (fn: (prev: QueryTab[]) => QueryTab[]) => { + const updated = fn(tabs); + tabs.splice(0, tabs.length, ...updated); + }; + + const { result } = renderHook(() => + useQueryAdapter({ + activeConnection: mockConnection, + onQueryExecute: async () => { + throw new Error('Connection refused'); + }, + tabs, + activeTabId: 'tab-1', + currentTab: defaultTab, + setTabs: setTabs as any, + fetchSchema: async () => {}, + features: {}, + }) + ); + + await act(async () => { + await result.current.executeQuery('SELECT 1'); + }); + + // Tab should not be stuck in executing state + expect(tabs[0].isExecuting).toBe(false); + }); + + test('cancelQuery aborts in-flight request', async () => { + let resolveFetch: ((val: WorkspaceQueryResult) => void) | null = null; + + const tabs = [defaultTab]; + const setTabs = (fn: (prev: QueryTab[]) => QueryTab[]) => { + const updated = fn(tabs); + tabs.splice(0, tabs.length, ...updated); + }; + + const { result } = renderHook(() => + useQueryAdapter({ + activeConnection: mockConnection, + onQueryExecute: async () => { + return new Promise((resolve) => { + resolveFetch = resolve; + }); + }, + tabs, + activeTabId: 'tab-1', + currentTab: defaultTab, + setTabs: setTabs as any, + fetchSchema: async () => {}, + features: {}, + }) + ); + + // Start query (don't await) + act(() => { + result.current.executeQuery('SELECT 1'); + }); + + // Cancel + act(() => { + result.current.cancelQuery(); + }); + + // Resolve the pending promise (shouldn't crash) + resolveFetch?.(mockQueryResult); + + expect(tabs[0].isExecuting).toBe(false); + }); + + test('bottomPanelMode defaults to results', () => { + const { result } = renderHook(() => + useQueryAdapter({ + activeConnection: mockConnection, + onQueryExecute: async () => mockQueryResult, + tabs: [defaultTab], + activeTabId: 'tab-1', + currentTab: defaultTab, + setTabs: () => {}, + fetchSchema: async () => {}, + features: {}, + }) + ); + + expect(result.current.bottomPanelMode).toBe('results'); + }); + + test('historyKey increments after successful query', async () => { + const tabs = [defaultTab]; + const setTabs = (fn: (prev: QueryTab[]) => QueryTab[]) => { + const updated = fn(tabs); + tabs.splice(0, tabs.length, ...updated); + }; + + const { result } = renderHook(() => + useQueryAdapter({ + activeConnection: mockConnection, + onQueryExecute: async () => mockQueryResult, + tabs, + activeTabId: 'tab-1', + currentTab: defaultTab, + setTabs: setTabs as any, + fetchSchema: async () => {}, + features: {}, + }) + ); + + const initialKey = result.current.historyKey; + + await act(async () => { + await result.current.executeQuery('SELECT 1'); + }); + + expect(result.current.historyKey).toBe(initialKey + 1); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd /home/cevheri/projects/libredb/libredb-studio && bun run test tests/hooks/use-query-adapter.test.ts` +Expected: FAIL — module not found + +- [ ] **Step 3: Write the implementation** + +```typescript +// src/workspace/hooks/use-query-adapter.ts +'use client'; + +import { useState, useCallback, useRef, type Dispatch, type SetStateAction } from 'react'; +import type { DatabaseConnection, QueryTab } from '@/lib/types'; +import type { WorkspaceQueryResult, WorkspaceFeatures } from '@/workspace/types'; +import type { BottomPanelMode } from '@/components/studio/BottomPanel'; +import { useToast } from '@/hooks/use-toast'; + +interface UseQueryAdapterParams { + activeConnection: DatabaseConnection | null; + onQueryExecute: (connectionId: string, sql: string, options?: { + limit?: number; + offset?: number; + unlimited?: boolean; + }) => Promise; + tabs: QueryTab[]; + activeTabId: string; + currentTab: QueryTab; + setTabs: Dispatch>; + fetchSchema: (conn: DatabaseConnection) => Promise; + features: Partial; +} + +export function useQueryAdapter({ + activeConnection, + onQueryExecute, + tabs, + activeTabId, + currentTab, + setTabs, + fetchSchema, + features, +}: UseQueryAdapterParams) { + const [historyKey, setHistoryKey] = useState(0); + const [bottomPanelMode, setBottomPanelMode] = useState('results'); + const [safetyCheckQuery, setSafetyCheckQuery] = useState(null); + const [unlimitedWarningOpen, setUnlimitedWarningOpen] = useState(false); + const [pendingUnlimitedQuery, setPendingUnlimitedQuery] = useState<{ + query: string; + tabId: string; + } | null>(null); + + const cancelledRef = useRef(false); + const { toast } = useToast(); + + // In-memory history for embedded mode + const historyRef = useRef>([]); + + const executeQuery = useCallback(async ( + overrideQuery?: string, + tabId?: string, + isExplain: boolean = false, + ) => { + const targetTabId = tabId || activeTabId; + const tabToExec = tabs.find(t => t.id === targetTabId) || currentTab; + const queryToExecute = overrideQuery || tabToExec.query; + + if (!activeConnection) { + toast({ title: 'No Connection', description: 'Select a connection first.', variant: 'destructive' }); + return; + } + + if (!queryToExecute.trim()) { + toast({ title: 'Empty Query', description: 'Enter a query to execute.', variant: 'destructive' }); + return; + } + + cancelledRef.current = false; + + // Set executing state + setTabs(prev => prev.map(t => t.id === targetTabId + ? { ...t, isExecuting: true } + : t + )); + setBottomPanelMode('results'); + + const startTime = Date.now(); + + try { + const result = await onQueryExecute(activeConnection.id, queryToExecute); + + if (cancelledRef.current) return; + + const executionTime = result.executionTime || (Date.now() - startTime); + + // Add to in-memory history + historyRef.current.unshift({ + id: Math.random().toString(36).substring(7), + connectionId: activeConnection.id, + connectionName: activeConnection.name, + tabName: tabToExec.name, + query: queryToExecute, + executionTime, + status: 'success', + executedAt: new Date(), + rowCount: result.rowCount, + }); + + // Update tab with result + setTabs(prev => prev.map(t => t.id === targetTabId + ? { + ...t, + result: { + rows: result.rows, + fields: result.fields || result.columns?.map(c => c.name) || Object.keys(result.rows[0] || {}), + rowCount: result.rowCount, + executionTime, + pagination: result.pagination, + }, + allRows: result.rows, + currentOffset: result.rows.length, + isExecuting: false, + isLoadingMore: false, + } + : t + )); + + setHistoryKey(prev => prev + 1); + } catch (error) { + if (cancelledRef.current) return; + + const errorMessage = error instanceof Error ? error.message : 'Query failed'; + + // Add error to history + historyRef.current.unshift({ + id: Math.random().toString(36).substring(7), + connectionId: activeConnection.id, + connectionName: activeConnection.name, + tabName: tabToExec.name, + query: queryToExecute, + executionTime: Date.now() - startTime, + status: 'error', + executedAt: new Date(), + errorMessage, + }); + + setTabs(prev => prev.map(t => t.id === targetTabId + ? { ...t, isExecuting: false, isLoadingMore: false } + : t + )); + + toast({ title: 'Query Error', description: errorMessage, variant: 'destructive' }); + } + }, [activeConnection, tabs, activeTabId, currentTab, onQueryExecute, setTabs, toast]); + + const forceExecuteQuery = useCallback((query: string) => { + setSafetyCheckQuery(null); + executeQuery(query); + }, [executeQuery]); + + const cancelQuery = useCallback(() => { + cancelledRef.current = true; + setTabs(prev => prev.map(t => t.isExecuting + ? { ...t, isExecuting: false, isLoadingMore: false } + : t + )); + toast({ title: 'Query Cancelled', description: 'Query execution was cancelled.' }); + }, [setTabs, toast]); + + const handleLoadMore = useCallback(() => { + if (!currentTab.result?.pagination?.hasMore || !activeConnection) return; + + const currentOffset = currentTab.currentOffset || currentTab.result.rows.length; + + setTabs(prev => prev.map(t => t.id === currentTab.id + ? { ...t, isLoadingMore: true } + : t + )); + + onQueryExecute(activeConnection.id, currentTab.query, { + limit: 500, + offset: currentOffset, + }).then(result => { + if (cancelledRef.current) return; + + setTabs(prev => prev.map(t => { + if (t.id !== currentTab.id || !t.result) return t; + const existingRows = t.allRows || t.result.rows; + const newAllRows = [...existingRows, ...result.rows]; + return { + ...t, + result: { + ...t.result, + rows: newAllRows, + rowCount: newAllRows.length, + pagination: result.pagination, + }, + allRows: newAllRows, + currentOffset: currentOffset + result.rows.length, + isLoadingMore: false, + }; + })); + }).catch(() => { + setTabs(prev => prev.map(t => t.id === currentTab.id + ? { ...t, isLoadingMore: false } + : t + )); + }); + }, [currentTab, activeConnection, onQueryExecute, setTabs]); + + const handleUnlimitedQuery = useCallback(() => { + if (!pendingUnlimitedQuery || !activeConnection) return; + // For embedded mode, just re-execute with unlimited flag + // The consumer's onQueryExecute handles the actual execution + executeQuery(pendingUnlimitedQuery.query, pendingUnlimitedQuery.tabId); + setUnlimitedWarningOpen(false); + setPendingUnlimitedQuery(null); + }, [pendingUnlimitedQuery, activeConnection, executeQuery]); + + return { + executeQuery, + forceExecuteQuery, + cancelQuery, + handleLoadMore, + handleUnlimitedQuery, + safetyCheckQuery, + setSafetyCheckQuery, + unlimitedWarningOpen, + setUnlimitedWarningOpen, + pendingUnlimitedQuery, + setPendingUnlimitedQuery, + historyKey, + bottomPanelMode, + setBottomPanelMode, + }; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd /home/cevheri/projects/libredb/libredb-studio && bun run test tests/hooks/use-query-adapter.test.ts` +Expected: All 6 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/workspace/hooks/use-query-adapter.ts tests/hooks/use-query-adapter.test.ts +git commit -m "feat(workspace): add useQueryAdapter hook with tests" +``` + +--- + +### Task 4: StudioWorkspace Component + +**Files:** +- Create: `src/workspace/StudioWorkspace.tsx` + +This is the core composite component. It composes the adapter hooks with existing UI components. + +- [ ] **Step 1: Create StudioWorkspace.tsx** + +```typescript +// src/workspace/StudioWorkspace.tsx +'use client'; + +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import { Sidebar, ConnectionsList } from '@/components/sidebar'; +import { MobileNav } from '@/components/MobileNav'; +import { SchemaExplorer } from '@/components/schema-explorer'; +import { QueryEditor, QueryEditorRef } from '@/components/QueryEditor'; +import { DataProfiler } from '@/components/DataProfiler'; +import { CodeGenerator } from '@/components/CodeGenerator'; +import { TestDataGenerator } from '@/components/TestDataGenerator'; +import { SchemaDiagram } from '@/components/SchemaDiagram'; +import { DataImportModal } from '@/components/DataImportModal'; +import { SaveQueryModal } from '@/components/SaveQueryModal'; +import { + StudioTabBar, + QueryToolbar, + BottomPanel, +} from '@/components/studio/index'; +import type { SavedQuery } from '@/lib/types'; +import { useToast } from '@/hooks/use-toast'; +import { useTabManager } from '@/hooks/use-tab-manager'; +import { useConnectionAdapter } from '@/workspace/hooks/use-connection-adapter'; +import { useQueryAdapter } from '@/workspace/hooks/use-query-adapter'; +import type { StudioWorkspaceProps } from '@/workspace/types'; +import { DEFAULT_WORKSPACE_FEATURES } from '@/workspace/types'; +import { cn } from '@/lib/utils'; +import { Database, Plus } from 'lucide-react'; +import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'; +import { AnimatePresence } from 'framer-motion'; +import { Button } from '@/components/ui/button'; + +export function StudioWorkspace({ + connections: externalConnections, + currentUser, + onQueryExecute, + onSchemaFetch, + onSaveQuery, + onLoadSavedQueries, + features: featuresProp, + className, +}: StudioWorkspaceProps) { + const queryEditorRef = useRef(null); + const { toast } = useToast(); + + const features = useMemo( + () => ({ ...DEFAULT_WORKSPACE_FEATURES, ...featuresProp }), + [featuresProp] + ); + + // 1. Connection Adapter + const conn = useConnectionAdapter({ + connections: externalConnections, + onSchemaFetch, + }); + + // 2. Tab Manager (reused as-is — pure UI state) + const tabMgr = useTabManager({ + activeConnection: conn.activeConnection, + metadata: null, // no direct DB access in embedded mode + schema: conn.schema, + }); + + // 3. Query Adapter + const queryExec = useQueryAdapter({ + activeConnection: conn.activeConnection, + onQueryExecute, + tabs: tabMgr.tabs, + activeTabId: tabMgr.activeTabId, + currentTab: tabMgr.currentTab, + setTabs: tabMgr.setTabs, + fetchSchema: conn.fetchSchema, + features, + }); + + // Fetch schema on connection change + useEffect(() => { + if (conn.activeConnection) { + conn.fetchSchema(conn.activeConnection); + } else { + conn.setSchema([]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [conn.activeConnection]); + + // Modal state + const [showDiagram, setShowDiagram] = useState(false); + const [isSaveQueryModalOpen, setIsSaveQueryModalOpen] = useState(false); + const [savedKey, setSavedKey] = useState(0); + const [activeMobileTab, setActiveMobileTab] = useState<'database' | 'schema' | 'editor'>('editor'); + const [isImportModalOpen, setIsImportModalOpen] = useState(false); + const [isNL2SQLOpen, setIsNL2SQLOpen] = useState(false); + const [profilerTable, setProfilerTable] = useState(null); + const [codeGenTable, setCodeGenTable] = useState(null); + const [testDataTable, setTestDataTable] = useState(null); + + const handleSaveQuery = async (name: string, description: string, tags: string[]) => { + if (!conn.activeConnection) return; + if (onSaveQuery) { + await onSaveQuery({ + name, + query: tabMgr.currentTab.query, + description, + connectionType: conn.activeConnection.type, + tags, + }); + setSavedKey(prev => prev + 1); + toast({ title: 'Query Saved', description: `"${name}" has been saved.` }); + } + }; + + const exportResults = (format: 'csv' | 'json' | 'sql-insert' | 'sql-ddl') => { + if (!tabMgr.currentTab.result) return; + const data = tabMgr.currentTab.result.rows; + let content = ''; + let mimeType = 'text/plain'; + let ext: string = format; + + if (format === 'csv') { + const headers = Object.keys(data[0] || {}).join(','); + const rows = data.map(row => Object.values(row).map(val => `"${val}"`).join(',')).join('\n'); + content = `${headers}\n${rows}`; + mimeType = 'text/csv'; + ext = 'csv'; + } else if (format === 'json') { + content = JSON.stringify(data, null, 2); + mimeType = 'application/json'; + ext = 'json'; + } else if (format === 'sql-insert') { + const tableName = tabMgr.currentTab.name.replace(/^Query[: ]*/, '') || 'table_name'; + const columns = Object.keys(data[0] || {}); + const lines = data.map(row => { + const values = columns.map(col => { + const val = row[col]; + if (val === null || val === undefined) return 'NULL'; + if (typeof val === 'number' || typeof val === 'boolean') return String(val); + return `'${String(val).replace(/'/g, "''")}'`; + }); + return `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${values.join(', ')});`; + }); + content = lines.join('\n'); + mimeType = 'text/sql'; + ext = 'sql'; + } else if (format === 'sql-ddl') { + const tableName = tabMgr.currentTab.name.replace(/^Query[: ]*/, '') || 'table_name'; + const columns = Object.keys(data[0] || {}); + const colDefs = columns.map(col => { + const sampleVal = data[0]?.[col]; + let sqlType = 'TEXT'; + if (typeof sampleVal === 'number') sqlType = Number.isInteger(sampleVal) ? 'INTEGER' : 'NUMERIC'; + else if (typeof sampleVal === 'boolean') sqlType = 'BOOLEAN'; + else if (sampleVal instanceof Date) sqlType = 'TIMESTAMP'; + return ` ${col} ${sqlType}`; + }); + content = `CREATE TABLE ${tableName} (\n${colDefs.join(',\n')}\n);`; + mimeType = 'text/sql'; + ext = 'sql'; + } + + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `query_result_export.${ext}`; + link.click(); + URL.revokeObjectURL(url); + }; + + const onTableClick = (tableName: string) => { + tabMgr.handleTableClick(tableName, queryExec.executeQuery); + }; + + return ( +
+ + {/* Sidebar */} + + {}} // no-op in embedded mode + onEditConnection={() => {}} // no-op in embedded mode + onAddConnection={() => {}} // no-op in embedded mode + onTableClick={onTableClick} + onGenerateSelect={tabMgr.handleGenerateSelect} + onCreateTableClick={() => {}} // no-op in embedded mode + onShowDiagram={features.schemaDiagram ? () => setShowDiagram(true) : undefined} + isAdmin={false} + onOpenMaintenance={() => {}} + databaseType={conn.activeConnection?.type} + metadata={null} + onProfileTable={features.codeGenerator ? (name) => setProfilerTable(name) : undefined} + onGenerateCode={features.codeGenerator ? (name) => setCodeGenTable(name) : undefined} + onGenerateTestData={features.testDataGenerator ? (name) => setTestDataTable(name) : undefined} + /> + + + + {/* Main Editor Area */} + +
+ {/* Tab Bar */} + + +
+ {/* Schema Diagram Overlay */} + + {features.schemaDiagram && showDiagram && ( + setShowDiagram(false)} /> + )} + + + {/* Mobile: Database Tab */} + {activeMobileTab === 'database' && ( +
+
+

Connections

+
+ { + conn.setActiveConnection(c); + setActiveMobileTab('editor'); + }} + onDeleteConnection={() => {}} + onAddConnection={() => {}} + /> +
+ )} + + {/* Mobile: Schema Tab */} + {activeMobileTab === 'schema' && ( +
+ {conn.activeConnection ? ( + { + onTableClick(tableName); + setActiveMobileTab('editor'); + }} + onGenerateSelect={(tableName) => { + tabMgr.handleGenerateSelect(tableName); + setActiveMobileTab('editor'); + }} + onCreateTableClick={() => {}} + isAdmin={false} + onOpenMaintenance={() => {}} + databaseType={conn.activeConnection?.type} + metadata={null} + onProfileTable={features.codeGenerator ? (name) => setProfilerTable(name) : undefined} + onGenerateCode={features.codeGenerator ? (name) => setCodeGenTable(name) : undefined} + onGenerateTestData={features.testDataGenerator ? (name) => setTestDataTable(name) : undefined} + /> + ) : ( +
+ +

Select a connection first

+
+ )} +
+ )} + + {/* Editor + Results */} +
+ + +
+ setIsSaveQueryModalOpen(true) : undefined} + onExecuteQuery={() => queryExec.executeQuery()} + onCancelQuery={queryExec.cancelQuery} + onBeginTransaction={() => {}} + onCommitTransaction={() => {}} + onRollbackTransaction={() => {}} + onTogglePlayground={() => {}} + onToggleEditing={() => {}} + onImport={features.dataImport ? () => setIsImportModalOpen(true) : undefined} + /> +
+ tabMgr.updateTabById(tabMgr.currentTab.id, { query: val })} + language={tabMgr.currentTab.type === 'mongodb' ? 'json' : 'sql'} + tables={conn.tableNames} + databaseType={conn.activeConnection?.type} + schemaContext={conn.schemaContext} + capabilities={undefined} + /> +
+
+
+ + + {}} + maskingEnabled={false} + onToggleMasking={undefined} + userRole={currentUser?.role} + maskingConfig={{ enabled: false, mode: 'partial', patterns: [] }} + editingEnabled={false} + pendingChanges={[]} + onCellChange={() => {}} + onApplyChanges={() => {}} + onDiscardChanges={() => {}} + onExecuteQuery={(q) => queryExec.executeQuery(q)} + onLoadQuery={(q) => tabMgr.updateCurrentTab({ query: q })} + onLoadMore={ + tabMgr.currentTab.result?.pagination?.hasMore + ? queryExec.handleLoadMore + : undefined + } + isLoadingMore={tabMgr.currentTab.isLoadingMore} + onExportResults={exportResults} + /> + +
+
+
+
+
+
+ + {/* Modals — only features that are enabled */} + {onSaveQuery && ( + setIsSaveQueryModalOpen(false)} + onSave={handleSaveQuery} + defaultQuery={tabMgr.currentTab.query} + /> + )} + + {features.dataImport && ( + setIsImportModalOpen(false)} + onImport={(sql) => queryExec.executeQuery(sql)} + tables={conn.schema} + databaseType={conn.activeConnection?.type} + /> + )} + + {features.codeGenerator && ( + <> + setProfilerTable(null)} + tableName={profilerTable || ''} + tableSchema={conn.schema.find(t => t.name === profilerTable) || null} + connection={conn.activeConnection} + schemaContext={conn.schemaContext} + databaseType={conn.activeConnection?.type} + /> + setCodeGenTable(null)} + tableName={codeGenTable || ''} + tableSchema={conn.schema.find(t => t.name === codeGenTable) || null} + databaseType={conn.activeConnection?.type} + /> + + )} + + {features.testDataGenerator && ( + setTestDataTable(null)} + tableName={testDataTable || ''} + tableSchema={conn.schema.find(t => t.name === testDataTable) || null} + databaseType={conn.activeConnection?.type} + queryLanguage={undefined} + onExecuteQuery={(q) => queryExec.executeQuery(q)} + /> + )} + + +
+ ); +} +``` + +- [ ] **Step 2: Verify no TypeScript errors** + +Run: `cd /home/cevheri/projects/libredb/libredb-studio && npx tsc --noEmit --pretty src/workspace/StudioWorkspace.tsx 2>&1 | head -30` + +Fix any type errors that come up. Common issues: +- Props mismatches on Sidebar, BottomPanel, QueryToolbar — adjust `undefined` vs `() => {}` as needed +- `MaskingConfig` import may be needed from `@/lib/data-masking` + +- [ ] **Step 3: Commit** + +```bash +git add src/workspace/StudioWorkspace.tsx +git commit -m "feat(workspace): add StudioWorkspace composite component" +``` + +--- + +### Task 5: Export Entry Point + +**Files:** +- Create: `src/exports/workspace.ts` +- Modify: `tsup.config.ts:4-9` (add workspace entry) +- Modify: `package.json` (add ./workspace export) + +- [ ] **Step 1: Create the export file** + +```typescript +// src/exports/workspace.ts +export { StudioWorkspace } from '../workspace/StudioWorkspace' +export type { + StudioWorkspaceProps, + WorkspaceConnection, + WorkspaceUser, + WorkspaceQueryResult, + WorkspaceFeatures, + SavedQueryInput, +} from '../workspace/types' +export { DEFAULT_WORKSPACE_FEATURES } from '../workspace/types' +``` + +- [ ] **Step 2: Add workspace entry to tsup.config.ts** + +In `tsup.config.ts`, add `workspace` to the `entry` object: + +```typescript +entry: { + index: 'src/exports/index.ts', + providers: 'src/exports/providers.ts', + types: 'src/exports/types.ts', + components: 'src/exports/components.ts', + workspace: 'src/exports/workspace.ts', // NEW +}, +``` + +- [ ] **Step 3: Add ./workspace export to package.json** + +Add after the `"./components"` export block: + +```json +"./workspace": { + "import": { + "types": "./dist/workspace.d.mts", + "default": "./dist/workspace.mjs" + }, + "require": { + "types": "./dist/workspace.d.ts", + "default": "./dist/workspace.js" + } +} +``` + +- [ ] **Step 4: Verify build succeeds** + +Run: `cd /home/cevheri/projects/libredb/libredb-studio && bun run build:lib 2>&1 | tail -20` + +Expected: Build completes, `dist/workspace.mjs` and `dist/workspace.d.mts` are generated. + +- [ ] **Step 5: Commit** + +```bash +git add src/exports/workspace.ts tsup.config.ts package.json +git commit -m "feat(workspace): add workspace export entry point and build config" +``` + +--- + +### Task 6: Run Full Test Suite + +**Files:** None (verification only) + +- [ ] **Step 1: Run all existing tests to confirm nothing is broken** + +Run: `cd /home/cevheri/projects/libredb/libredb-studio && bun run test 2>&1 | tail -30` + +Expected: All existing tests pass. The new adapter hook tests also pass. + +- [ ] **Step 2: Run lint** + +Run: `cd /home/cevheri/projects/libredb/libredb-studio && bun run lint 2>&1 | tail -20` + +Expected: No new lint errors introduced. + +- [ ] **Step 3: Run typecheck** + +Run: `cd /home/cevheri/projects/libredb/libredb-studio && bun run typecheck 2>&1 | tail -20` + +Expected: No type errors. + +- [ ] **Step 4: Run build** + +Run: `cd /home/cevheri/projects/libredb/libredb-studio && bun run build 2>&1 | tail -20` + +Expected: Next.js production build succeeds (standalone studio app still works). + +- [ ] **Step 5: Commit any fixes if needed** + +If any test/lint/type/build issues were found and fixed: + +```bash +git add -A +git commit -m "fix(workspace): resolve test/lint/type issues" +``` + +--- + +### Summary: File Map + +| Action | File | +|--------|------| +| Create | `src/workspace/types.ts` | +| Create | `src/workspace/hooks/use-connection-adapter.ts` | +| Create | `src/workspace/hooks/use-query-adapter.ts` | +| Create | `src/workspace/StudioWorkspace.tsx` | +| Create | `src/exports/workspace.ts` | +| Create | `tests/hooks/use-connection-adapter.test.ts` | +| Create | `tests/hooks/use-query-adapter.test.ts` | +| Modify | `tsup.config.ts` (add workspace entry) | +| Modify | `package.json` (add ./workspace export) | + +**Total: 7 new files, 2 modified files. Zero existing files changed.** diff --git a/docs/superpowers/specs/2026-03-26-studio-workspace-export-design.md b/docs/superpowers/specs/2026-03-26-studio-workspace-export-design.md new file mode 100644 index 0000000..131ec36 --- /dev/null +++ b/docs/superpowers/specs/2026-03-26-studio-workspace-export-design.md @@ -0,0 +1,299 @@ +# StudioWorkspace Composite Export — Design Specification + +**Date:** 2026-03-26 +**Status:** Approved +**Context:** libredb-platform's workspace is a basic textarea + HTML table query executor. Studio standalone has 28+ components, Monaco editor, AI copilot, charts, ER diagrams — a full database IDE. The gap exists because the npm package exports individual components, but the orchestration layer (Studio.tsx + 16 hooks + resizable layout + tab manager) is not exported. Platform would need to rebuild the entire workspace wrapper around individual components — essentially rewriting studio inside platform. + +**Solution:** Export the entire workspace as a single `` composite component with callback props. Platform provides data + callbacks, studio provides the full IDE experience. + +**Principle:** Additive only — Studio.tsx and all existing code remain unchanged. + +--- + +## 1. Props Interface + +```typescript +interface StudioWorkspaceProps { + // Data — platform provides + connections: WorkspaceConnection[]; + currentUser?: WorkspaceUser; + + // Core callbacks — required + onQueryExecute: (connectionId: string, sql: string) => Promise; + onSchemaFetch: (connectionId: string) => Promise; + + // Optional callbacks + onTestConnection?: (config: TestConnectionConfig) => Promise<{ success: boolean; message: string }>; + onSaveQuery?: (query: SavedQueryInput) => Promise; + onLoadSavedQueries?: () => Promise; + + // Feature flags — disabled features hidden from UI + features?: WorkspaceFeatures; + + // UI customization + className?: string; +} + +interface WorkspaceConnection { + id: string; + name: string; + type: DatabaseType; // 'postgres' | 'mysql' | 'sqlite' | 'oracle' | 'mssql' | 'mongodb' | 'redis' +} + +interface WorkspaceUser { + id: string; + name?: string; + role?: string; +} + +interface WorkspaceQueryResult { + rows: Record[]; + columns: { name: string; type?: string }[]; + rowCount: number; + executionTime: number; +} + +interface TestConnectionConfig { + type: DatabaseType; + host: string; + port: number; + database: string; + username: string; + password: string; + sslEnabled?: boolean; +} + +interface SavedQueryInput { + name: string; + query: string; + description?: string; + connectionType?: string; + tags?: string[]; +} + +interface WorkspaceFeatures { + ai?: boolean; // default: false — NL2SQL, QuerySafety AI, DataProfiler AI narrative + charts?: boolean; // default: true + codeGenerator?: boolean; // default: true + testDataGenerator?: boolean; // default: true (uses onQueryExecute) + schemaDiagram?: boolean; // default: true + dataImport?: boolean; // default: true + inlineEditing?: boolean; // default: false (needs special mutation API) + transactions?: boolean; // default: false (needs BEGIN/COMMIT/ROLLBACK API) + connectionManagement?: boolean; // default: false (platform manages connections) + dataMasking?: boolean; // default: false +} +``` + +--- + +## 2. Architecture + +### Dependency Inversion via Adapter Hooks + +Studio.tsx uses internal hooks that call API routes and localStorage. StudioWorkspace uses adapter hooks with the same return shape but delegating to callback props. + +``` +Studio.tsx (standalone) StudioWorkspace.tsx (embedded) +──────────────────────── ──────────────────────────── +useAuth() → internal JWT currentUser prop +useConnectionManager() → storage useConnectionAdapter(props) +useQueryExecution() → /api/db useQueryAdapter(props) +useStorageSync() → localStorage not needed +useTabManager() → pure UI state useTabManager() (reused as-is) +useTransactionControl() → /api/db disabled in v1 +useInlineEditing() → /api/db disabled in v1 +useProviderMetadata() → /api/db disabled (no direct DB access) +``` + +### Adapter Hook Contracts + +**useConnectionAdapter(props)** — same shape as useConnectionManager: +- `connections`: from props (reactive to prop changes) +- `activeConnection`: internal state, initialized to first connection +- `setActiveConnection`: setter +- `schema` / `isLoadingSchema`: fetched via `props.onSchemaFetch(connectionId)` +- `fetchSchema`: triggers `onSchemaFetch` +- `tableNames` / `schemaContext`: derived from schema +- No storage operations, no connection CRUD + +**useQueryAdapter(props, tabMgr)** — same shape as useQueryExecution: +- `executeQuery(sql?)`: calls `props.onQueryExecute(activeConnectionId, sql)`, stores result in tab +- `cancelQuery`: sets abort flag (best-effort) +- `bottomPanelMode` / `setBottomPanelMode`: internal state +- `historyKey`: increments on each execution (triggers history refresh) +- `safetyCheckQuery` / `forceExecuteQuery`: disabled when `features.ai` is false +- No internal API calls + +### Shared UI — No Duplication + +Both Studio.tsx and StudioWorkspace.tsx render the same components: +- QueryEditor, ResultsGrid, SchemaExplorer, SchemaDiagram, DataCharts +- Sidebar, StudioTabBar, QueryToolbar, BottomPanel +- ResizablePanelGroup layout, mobile layout + +The difference is only in how hooks provide data to these components. + +--- + +## 3. File Structure + +All new files — nothing existing is modified. + +``` +libredb-studio/src/ +├── workspace/ # NEW directory +│ ├── types.ts # WorkspaceProps, WorkspaceConnection, etc. +│ ├── StudioWorkspace.tsx # Composite component +│ ├── defaults.ts # Default feature flags +│ └── hooks/ +│ ├── use-connection-adapter.ts # Connection management via props+callbacks +│ └── use-query-adapter.ts # Query execution via callbacks +├── exports/ +│ └── workspace.ts # NEW export entry point +``` + +Modified files (minimal): +``` +package.json → add "./workspace" to exports field +tsup.config.ts → add workspace entry point +``` + +--- + +## 4. v1 Feature Scope + +### Included (works via props + callbacks) + +| Feature | Component | Why It Works | +|---------|-----------|-------------| +| Monaco SQL editor | QueryEditor | Pure client-side, needs only schema context | +| Virtualized results grid | ResultsGrid | Renders data from onQueryExecute result | +| Schema explorer (sidebar) | SchemaExplorer | Data from onSchemaFetch | +| ER diagram | SchemaDiagram | Pure client-side, reads schema state | +| 8 chart types | DataCharts | Pure client-side, reads query results | +| Code generation | CodeGenerator | Pure client-side, reads schema | +| Test data generation | TestDataGenerator | Generates SQL, uses onQueryExecute | +| Multi-tab workspace | StudioTabBar + useTabManager | Pure UI state, no external deps | +| Resizable panels | ResizablePanelGroup | Pure UI | +| Query history | BottomPanel history tab | In-memory history array | +| Mobile responsive | MobileNav + responsive layout | Pure UI | +| Data import | DataImportModal | Generates SQL, uses onQueryExecute | + +### Excluded from v1 (disabled via feature flags) + +| Feature | Reason | v2 Path | +|---------|--------|---------| +| NL2SQL | Calls /api/ai/nl2sql | Add `onNL2SQL` callback | +| AI DataProfiler | Calls /api/ai/describe-schema | Add `onAIDescribe` callback | +| QuerySafety AI | Calls /api/ai/query-safety | Add `onQuerySafety` callback | +| AI Autopilot | Calls /api/ai/autopilot | Add `onAIAutopilot` callback | +| Inline editing | Needs mutation API per cell | Add `onCellUpdate` callback | +| Transaction control | Needs BEGIN/COMMIT/ROLLBACK | Add `onTransaction` callback | +| Connection CRUD | Platform manages connections | Add `onConnectionSave/Delete` | +| Data masking | Needs RBAC config | Add `maskingConfig` prop | +| Command palette | Navigation targets don't exist in embedded | Adapt or exclude | +| Storage sync | Platform handles persistence | Not needed | + +--- + +## 5. Platform Integration + +### Platform workspace page usage: + +```tsx +import { StudioWorkspace } from '@libredb/studio/workspace'; + +export default async function WorkspacePage() { + const user = await requireAuth(); + const connections = await ConnectionRepository.findByTenant(user.currentTenantId); + + const serialized = connections.map(c => ({ + id: c.id, + name: c.name, + type: c.type.toLowerCase(), + })); + + return ( + { + const res = await fetch('/api/db/query', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ connectionId, sql }), + }); + if (!res.ok) throw new Error(await res.text()); + return res.json(); + }} + onSchemaFetch={async (connectionId) => { + const res = await fetch('/api/db/schema', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ connectionId }), + }); + const data = await res.json(); + return data.schema; + }} + features={{ + charts: true, + schemaDiagram: true, + codeGenerator: true, + testDataGenerator: true, + dataImport: true, + }} + /> + ); +} +``` + +Platform's existing `/api/db/query` and `/api/db/schema` routes handle auth, RBAC, audit logging — StudioWorkspace doesn't need to know about any of that. + +--- + +## 6. Build & Export + +### package.json exports addition: +```json +{ + "exports": { + ".": { "import": "./dist/index.mjs", "require": "./dist/index.js" }, + "./providers": { "import": "./dist/providers.mjs", "require": "./dist/providers.js" }, + "./types": { "import": "./dist/types.mjs", "require": "./dist/types.js" }, + "./components": { "import": "./dist/components.mjs", "require": "./dist/components.js" }, + "./workspace": { "import": "./dist/workspace.mjs", "require": "./dist/workspace.js" } + } +} +``` + +### tsup entry point addition: +```typescript +entry: [ + 'src/exports/index.ts', + 'src/exports/providers.ts', + 'src/exports/types.ts', + 'src/exports/components.ts', + 'src/exports/workspace.ts', // NEW +] +``` + +--- + +## 7. Testing Strategy + +- Unit tests for adapter hooks (mock callbacks, verify they're called correctly) +- Unit tests for feature flag logic (disabled features don't render) +- Component test for StudioWorkspace (renders with minimal props) +- No E2E needed in v1 (platform E2E will cover integration) + +--- + +## 8. What Does NOT Change + +- `src/components/Studio.tsx` — untouched +- All 16 existing hooks — untouched +- All 28+ existing components — untouched +- All 33 API routes — untouched +- Existing exports (components, providers, types) — untouched +- Standalone app behavior — identical diff --git a/eslint.config.mjs b/eslint.config.mjs index e600c52..6412e12 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,6 +9,7 @@ const eslintConfig = defineConfig([ ".next/**", "out/**", "build/**", + "dist/**", "next-env.d.ts", ]), { diff --git a/package.json b/package.json index 54c1f7f..d6741f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@libredb/studio", - "version": "0.9.7", + "version": "0.9.12", "private": false, "publishConfig": { "access": "public" @@ -9,12 +9,64 @@ "type": "git", "url": "https://github.com/libredb/libredb-studio" }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", "exports": { - ".": "./src/exports/index.ts", - "./components": "./src/exports/components.ts", - "./providers": "./src/exports/providers.ts", - "./types": "./src/exports/types.ts" + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./providers": { + "import": { + "types": "./dist/providers.d.mts", + "default": "./dist/providers.mjs" + }, + "require": { + "types": "./dist/providers.d.ts", + "default": "./dist/providers.js" + } + }, + "./types": { + "import": { + "types": "./dist/types.d.mts", + "default": "./dist/types.mjs" + }, + "require": { + "types": "./dist/types.d.ts", + "default": "./dist/types.js" + } + }, + "./components": { + "import": { + "types": "./dist/components.d.mts", + "default": "./dist/components.mjs" + }, + "require": { + "types": "./dist/components.d.ts", + "default": "./dist/components.js" + } + }, + "./workspace": { + "import": { + "types": "./dist/workspace.d.mts", + "default": "./dist/workspace.mjs" + }, + "require": { + "types": "./dist/workspace.d.ts", + "default": "./dist/workspace.js" + } + } }, + "files": [ + "dist" + ], "peerDependencies": { "react": "^19", "react-dom": "^19" @@ -22,6 +74,8 @@ "scripts": { "dev": "next dev", "build": "next build", + "build:lib": "tsup", + "prepublishOnly": "tsup", "start": "next start", "lint": "eslint .", "typecheck": "tsc --noEmit", @@ -124,6 +178,7 @@ "eslint-config-next": "^16.1.6", "happy-dom": "^20.6.1", "tailwindcss": "^4", + "tsup": "^8.5.1", "tw-animate-css": "^1.4.0", "typescript": "^5" } diff --git a/src/app/globals.css b/src/app/globals.css index 683353e..0f6b69e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,6 +3,20 @@ @theme { --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + + /* Studio IDE font scale — compact sizes for data-dense UIs */ + --text-micro: 0.5rem; /* 8px — tiny badges */ + --text-micro--line-height: 0.75rem; + --text-caption: 0.625rem; /* 10px — status dots, minor labels */ + --text-caption--line-height: 1rem; + --text-label: 0.6875rem; /* 11px — toolbar buttons, sidebar counts */ + --text-label--line-height: 1rem; + --text-body: 0.8125rem; /* 13px — connection names, explorer items */ + --text-body--line-height: 1.25rem; + --text-data: 0.75rem; /* 12px — table cells, monospace data */ + --text-data--line-height: 1rem; + --text-title: 0.9375rem; /* 15px — section headings */ + --text-title--line-height: 1.375rem; } /* diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b499b2c..6091aae 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,9 +1,10 @@ import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { Toaster } from "@/components/ui/sonner"; -const inter = Inter({ subsets: ["latin"] }); +const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }); +const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] }); export const metadata: Metadata = { title: "LibreDB Studio | Universal Database Editor", @@ -25,7 +26,7 @@ export default function RootLayout({ }>) { return ( - + {children} diff --git a/src/components/AIAutopilotPanel.tsx b/src/components/AIAutopilotPanel.tsx index 381f189..069ae26 100644 --- a/src/components/AIAutopilotPanel.tsx +++ b/src/components/AIAutopilotPanel.tsx @@ -121,7 +121,7 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }: const sql = codeContent.trim(); elements.push(
-
+              
                 {sql}
               
@@ -187,7 +187,7 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }:
- + AI Performance Autopilot
@@ -195,7 +195,7 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }: onClick={runAutopilot} disabled={isLoading || !connection} className={cn( - "flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[10px] font-bold transition-colors", + "flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-colors", isLoading ? "bg-cyan-600/20 text-cyan-400 cursor-wait" : "bg-cyan-600 hover:bg-cyan-500 text-white" @@ -215,7 +215,7 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }:

AI Performance Autopilot

-

+

Click "Run Analysis" to get AI-powered optimization recommendations

diff --git a/src/components/CodeGenerator.tsx b/src/components/CodeGenerator.tsx index 5d5f4a7..ec0bcae 100644 --- a/src/components/CodeGenerator.tsx +++ b/src/components/CodeGenerator.tsx @@ -208,7 +208,7 @@ export function CodeGenerator({ Code Generator {tableName} {databaseType && ( - {databaseType} + {databaseType} )}
-

+

Supports: postgres://, mysql://, mongodb://, redis://, oracle://, mssql://

@@ -156,7 +156,7 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection, on
- +
- +
{(Object.keys(ENVIRONMENT_COLORS) as ConnectionEnvironment[]).map((env) => (