From 9d40bf40f97dfafb88e531824dd56b17963a30dc Mon Sep 17 00:00:00 2001 From: Nicolas Jesenberger Date: Sun, 29 Mar 2026 19:39:01 +0700 Subject: [PATCH 1/4] update: use shadow dom --- package/package.json | 1 + package/pnpm-lock.yaml | 281 +- .../components/design-mode/styles.module.scss | 546 ++-- .../src/components/page-toolbar-css/index.tsx | 2339 +++++++++-------- .../checkbox-field/styles.module.scss | 1 + .../page-toolbar-css/styles.module.scss | 117 - package/src/components/reset.scss | 489 ++++ package/src/components/shadow-root/index.tsx | 50 + package/src/scss.d.ts | 2 + package/tsconfig.json | 2 +- package/tsup.config.ts | 90 +- 11 files changed, 2351 insertions(+), 1567 deletions(-) create mode 100644 package/src/components/reset.scss create mode 100644 package/src/components/shadow-root/index.tsx diff --git a/package/package.json b/package/package.json index d53a43f5..d769860e 100644 --- a/package/package.json +++ b/package/package.json @@ -49,6 +49,7 @@ "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.3.0", + "esbuild": "^0.27.4", "esbuild-sass-plugin": "^3.6.0", "jsdom": "^25.0.0", "postcss": "^8.5.6", diff --git a/package/pnpm-lock.yaml b/package/pnpm-lock.yaml index f1de80dc..3cf53c88 100644 --- a/package/pnpm-lock.yaml +++ b/package/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.3.0 version: 4.7.0(vite@5.4.21(@types/node@20.19.30)(sass-embedded@1.97.2)(sass@1.97.2)) + esbuild: + specifier: ^0.27.4 + version: 0.27.4 esbuild-sass-plugin: specifier: ^3.6.0 version: 3.6.0(esbuild@0.27.2)(sass-embedded@1.97.2) @@ -67,7 +70,7 @@ importers: version: 12.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next: specifier: ^14.0.0 - version: 14.2.35(@babel/core@7.28.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.97.2) + version: 14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.97.2) prism-react-renderer: specifier: ^2.4.1 version: 2.4.1(react@18.3.1) @@ -236,6 +239,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -248,6 +257,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -260,6 +275,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -272,6 +293,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -284,6 +311,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -296,6 +329,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -308,6 +347,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -320,6 +365,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -332,6 +383,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -344,6 +401,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -356,6 +419,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -368,6 +437,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -380,6 +455,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -392,6 +473,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -404,6 +491,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -416,6 +509,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -428,12 +527,24 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.27.2': resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -446,12 +557,24 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.27.2': resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -464,12 +587,24 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.27.2': resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -482,6 +617,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -494,6 +635,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -506,6 +653,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -518,6 +671,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1095,6 +1254,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2049,147 +2213,225 @@ snapshots: '@esbuild/aix-ppc64@0.27.2': optional: true + '@esbuild/aix-ppc64@0.27.4': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true '@esbuild/android-arm64@0.27.2': optional: true + '@esbuild/android-arm64@0.27.4': + optional: true + '@esbuild/android-arm@0.21.5': optional: true '@esbuild/android-arm@0.27.2': optional: true + '@esbuild/android-arm@0.27.4': + optional: true + '@esbuild/android-x64@0.21.5': optional: true '@esbuild/android-x64@0.27.2': optional: true + '@esbuild/android-x64@0.27.4': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true '@esbuild/darwin-arm64@0.27.2': optional: true + '@esbuild/darwin-arm64@0.27.4': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true '@esbuild/darwin-x64@0.27.2': optional: true + '@esbuild/darwin-x64@0.27.4': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true '@esbuild/freebsd-arm64@0.27.2': optional: true + '@esbuild/freebsd-arm64@0.27.4': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true '@esbuild/freebsd-x64@0.27.2': optional: true + '@esbuild/freebsd-x64@0.27.4': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true '@esbuild/linux-arm64@0.27.2': optional: true + '@esbuild/linux-arm64@0.27.4': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true '@esbuild/linux-arm@0.27.2': optional: true + '@esbuild/linux-arm@0.27.4': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true '@esbuild/linux-ia32@0.27.2': optional: true + '@esbuild/linux-ia32@0.27.4': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true '@esbuild/linux-loong64@0.27.2': optional: true + '@esbuild/linux-loong64@0.27.4': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true '@esbuild/linux-mips64el@0.27.2': optional: true + '@esbuild/linux-mips64el@0.27.4': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true '@esbuild/linux-ppc64@0.27.2': optional: true + '@esbuild/linux-ppc64@0.27.4': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true '@esbuild/linux-riscv64@0.27.2': optional: true + '@esbuild/linux-riscv64@0.27.4': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true '@esbuild/linux-s390x@0.27.2': optional: true + '@esbuild/linux-s390x@0.27.4': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true '@esbuild/linux-x64@0.27.2': optional: true + '@esbuild/linux-x64@0.27.4': + optional: true + '@esbuild/netbsd-arm64@0.27.2': optional: true + '@esbuild/netbsd-arm64@0.27.4': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true '@esbuild/netbsd-x64@0.27.2': optional: true + '@esbuild/netbsd-x64@0.27.4': + optional: true + '@esbuild/openbsd-arm64@0.27.2': optional: true + '@esbuild/openbsd-arm64@0.27.4': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true '@esbuild/openbsd-x64@0.27.2': optional: true + '@esbuild/openbsd-x64@0.27.4': + optional: true + '@esbuild/openharmony-arm64@0.27.2': optional: true + '@esbuild/openharmony-arm64@0.27.4': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true '@esbuild/sunos-x64@0.27.2': optional: true + '@esbuild/sunos-x64@0.27.4': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true '@esbuild/win32-arm64@0.27.2': optional: true + '@esbuild/win32-arm64@0.27.4': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true '@esbuild/win32-ia32@0.27.2': optional: true + '@esbuild/win32-ia32@0.27.4': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true '@esbuild/win32-x64@0.27.2': optional: true + '@esbuild/win32-x64@0.27.4': + optional: true + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2702,6 +2944,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.2 '@esbuild/win32-x64': 0.27.2 + 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 + escalade@3.2.0: {} estree-walker@3.0.3: @@ -2919,7 +3190,7 @@ snapshots: nanoid@3.3.11: {} - next@14.2.35(@babel/core@7.28.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.97.2): + next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.97.2): dependencies: '@next/env': 14.2.35 '@swc/helpers': 0.5.5 @@ -2929,7 +3200,7 @@ snapshots: postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(@babel/core@7.28.6)(react@18.3.1) + styled-jsx: 5.1.1(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 14.2.33 '@next/swc-darwin-x64': 14.2.33 @@ -3235,12 +3506,10 @@ snapshots: string-hash@1.1.3: {} - styled-jsx@5.1.1(@babel/core@7.28.6)(react@18.3.1): + styled-jsx@5.1.1(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 - optionalDependencies: - '@babel/core': 7.28.6 sucrase@3.35.1: dependencies: diff --git a/package/src/components/design-mode/styles.module.scss b/package/src/components/design-mode/styles.module.scss index ceaf9057..b55292cf 100644 --- a/package/src/components/design-mode/styles.module.scss +++ b/package/src/components/design-mode/styles.module.scss @@ -1,3 +1,5 @@ +@use "sass:color"; + // ============================================================================= // Layout Mode Styles // ============================================================================= @@ -23,7 +25,6 @@ $red: #ef4444; pointer-events: none !important; } - .overlay { position: fixed; inset: 0; @@ -86,8 +87,11 @@ $red: #ef4444; content: ""; position: absolute; inset: 0; - background-image: - radial-gradient(circle, rgba(0, 0, 0, 0.08) 1px, transparent 1px); + background-image: radial-gradient( + circle, + rgba(0, 0, 0, 0.08) 1px, + transparent 1px + ); background-size: 24px 24px; background-position: 12px 12px; pointer-events: none; @@ -96,8 +100,11 @@ $red: #ef4444; &.gridActive::after { opacity: 1; - background-image: - radial-gradient(circle, rgba(0, 0, 0, 0.22) 1px, transparent 1px); + background-image: radial-gradient( + circle, + rgba(0, 0, 0, 0.22) 1px, + transparent 1px + ); } } @@ -160,7 +167,9 @@ $red: #ef4444; .wireframePurposeWrap { display: grid; grid-template-rows: 1fr; - transition: grid-template-rows 0.2s ease, opacity 0.15s ease; + transition: + grid-template-rows 0.2s ease, + opacity 0.15s ease; opacity: 1; &.collapsed { @@ -229,7 +238,9 @@ $red: #ef4444; cursor: pointer; border: 1px dashed rgba(255, 255, 255, 0.1); background: transparent; - transition: background 0.15s ease, border-color 0.15s ease; + transition: + background 0.15s ease, + border-color 0.15s ease; &:hover { background: rgba(255, 255, 255, 0.04); @@ -301,121 +312,6 @@ $red: #ef4444; } } -.canvasPurposeWrap { - display: grid; - grid-template-rows: 1fr; - transition: grid-template-rows 0.2s ease, opacity 0.15s ease; - opacity: 1; - - &.collapsed { - grid-template-rows: 0fr; - opacity: 0; - } -} - -.canvasPurposeInner { - overflow: hidden; -} - -// Matches .settingsToggle from settings panel -.canvasPurposeToggle { - display: flex; - align-items: center; - gap: 0.5rem; - cursor: pointer; - margin: 0.375rem 1rem 0.375rem 1.1875rem; - - input[type="checkbox"] { - position: absolute; - opacity: 0; - width: 0; - height: 0; - } -} - -// Matches .customCheckbox from settings panel -.canvasPurposeCheck { - position: relative; - width: 14px; - height: 14px; - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 4px; - background: rgba(255, 255, 255, 0.05); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - transition: background 0.25s ease, border-color 0.25s ease; - - svg { - color: #1a1a1a; - opacity: 1; - transition: opacity 0.15s ease; - } - - &.checked { - border-color: rgba(255, 255, 255, 0.3); - background: rgba(255, 255, 255, 1); - } - - .light & { - border: 1px solid rgba(0, 0, 0, 0.15); - background: #fff; - - &.checked { - border-color: #1a1a1a; - background: #1a1a1a; - - svg { - color: #fff; - } - } - } -} - -// Matches .toggleLabel from settings panel -.canvasPurposeLabel { - font-size: 0.8125rem; - font-weight: 400; - color: rgba(255, 255, 255, 0.5); - letter-spacing: -0.0094em; - display: flex; - align-items: center; - gap: 0.25rem; - - .light & { - color: rgba(0, 0, 0, 0.5); - } -} - -// Matches .helpIcon from settings panel -.canvasPurposeHelp { - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - cursor: help; - - svg { - color: rgba(255, 255, 255, 0.2); - transform: translateY(2px); - transition: color 0.15s ease; - } - - &:hover svg { - color: rgba(255, 255, 255, 0.5); - } - - .light & svg { - color: rgba(0, 0, 0, 0.2); - } - - .light &:hover svg { - color: rgba(0, 0, 0, 0.5); - } - -} - // ============================================================================= // Placed Component // ============================================================================= @@ -426,7 +322,11 @@ $red: #ef4444; border-radius: 6px; background: rgba(59, 130, 246, 0.08); cursor: grab; - transition: box-shadow 0.15s, border-color 0.15s, opacity 0.15s ease, transform 0.15s ease; + transition: + box-shadow 0.15s, + border-color 0.15s, + opacity 0.15s ease, + transform 0.15s ease; user-select: none; pointer-events: auto; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); @@ -446,10 +346,14 @@ $red: #ef4444; border-color: $blue; border-style: solid; background: rgba(59, 130, 246, 0.1); - box-shadow: 0 0 0 2px $blue-dim, 0 2px 8px rgba(59, 130, 246, 0.15); + box-shadow: + 0 0 0 2px $blue-dim, + 0 2px 8px rgba(59, 130, 246, 0.15); &:hover { - box-shadow: 0 0 0 2px $blue-dim, 0 2px 8px rgba(59, 130, 246, 0.15); + box-shadow: + 0 0 0 2px $blue-dim, + 0 2px 8px rgba(59, 130, 246, 0.15); } } @@ -466,10 +370,14 @@ $red: #ef4444; &.selected { border-color: $orange; background: rgba(249, 115, 22, 0.1); - box-shadow: 0 0 0 2px $orange-dim, 0 2px 8px rgba(249, 115, 22, 0.15); + box-shadow: + 0 0 0 2px $orange-dim, + 0 2px 8px rgba(249, 115, 22, 0.15); &:hover { - box-shadow: 0 0 0 2px $orange-dim, 0 2px 8px rgba(249, 115, 22, 0.15); + box-shadow: + 0 0 0 2px $orange-dim, + 0 2px 8px rgba(249, 115, 22, 0.15); } } } @@ -484,7 +392,9 @@ $red: #ef4444; transform: scale(0.97); pointer-events: none; animation: none; - transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.32, 0.72, 0, 1); + transition: + opacity 0.2s ease, + transform 0.2s cubic-bezier(0.32, 0.72, 0, 1); } } @@ -504,8 +414,11 @@ $red: #ef4444; color: rgba(59, 130, 246, 0.7); white-space: nowrap; pointer-events: none; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - text-shadow: 0 0 4px rgba(255, 255, 255, 0.8), 0 0 8px rgba(255, 255, 255, 0.5); + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + text-shadow: + 0 0 4px rgba(255, 255, 255, 0.8), + 0 0 8px rgba(255, 255, 255, 0.5); .selected & { color: $blue; @@ -532,11 +445,16 @@ $red: #ef4444; overflow: hidden; text-overflow: ellipsis; pointer-events: none; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - text-shadow: 0 0 4px rgba(255, 255, 255, 0.9), 0 0 8px rgba(255, 255, 255, 0.6); + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + text-shadow: + 0 0 4px rgba(255, 255, 255, 0.9), + 0 0 8px rgba(255, 255, 255, 0.6); opacity: 0; transform: translateY(-2px); - transition: opacity 0.2s ease, transform 0.2s ease; + transition: + opacity 0.2s ease, + transform 0.2s ease; &.annotationVisible { opacity: 1; @@ -556,11 +474,16 @@ $red: #ef4444; overflow: hidden; text-overflow: ellipsis; pointer-events: none; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - text-shadow: 0 0 4px rgba(255, 255, 255, 0.9), 0 0 8px rgba(255, 255, 255, 0.6); + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + text-shadow: + 0 0 4px rgba(255, 255, 255, 0.9), + 0 0 8px rgba(255, 255, 255, 0.6); opacity: 0; transform: translateY(-2px); - transition: opacity 0.2s ease, transform 0.2s ease; + transition: + opacity 0.2s ease, + transform 0.2s ease; &.annotationVisible { opacity: 1; @@ -580,12 +503,16 @@ $red: #ef4444; border: 1.5px solid $blue; border-radius: 2px; z-index: 12; - box-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.12); + box-shadow: + 0 0 0 0.5px rgba(0, 0, 0, 0.1), + 0 1px 2px rgba(0, 0, 0, 0.12); opacity: 0; transform: scale(0.3); pointer-events: none; will-change: opacity, transform; - transition: opacity 0.2s ease-out, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); + transition: + opacity 0.2s ease-out, + transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); .placement:hover &, .sectionOutline:hover &, @@ -608,13 +535,35 @@ $red: #ef4444; } } -.handleNw { top: -4px; left: -4px; cursor: nw-resize; } -.handleNe { top: -4px; right: -4px; cursor: ne-resize; } -.handleSe { bottom: -4px; right: -4px; cursor: se-resize; } -.handleSw { bottom: -4px; left: -4px; cursor: sw-resize; } +.handleNw { + top: -4px; + left: -4px; + cursor: nw-resize; +} +.handleNe { + top: -4px; + right: -4px; + cursor: ne-resize; +} +.handleSe { + bottom: -4px; + right: -4px; + cursor: se-resize; +} +.handleSw { + bottom: -4px; + left: -4px; + cursor: sw-resize; +} // Hide midpoint dot handles (replaced by edge bars) -.handleN, .handleE, .handleS, .handleW { opacity: 0 !important; pointer-events: none !important; } +.handleN, +.handleE, +.handleS, +.handleW { + opacity: 0 !important; + pointer-events: none !important; +} // ============================================================================= // Edge Resize Bars (wide hit areas + arrow indicators) @@ -638,7 +587,9 @@ $red: #ef4444; background: $orange; } opacity: 0; - transition: opacity 0.1s ease, transform 0.1s ease; + transition: + opacity 0.1s ease, + transform 0.1s ease; transform: scale(0.8); } @@ -662,7 +613,8 @@ $red: #ef4444; } // Horizontal edges (top/bottom) — full width, 12px tall -.edgeN, .edgeS { +.edgeN, +.edgeS { left: 12px; right: 12px; height: 12px; @@ -674,11 +626,17 @@ $red: #ef4444; } } -.edgeN { top: -6px; } -.edgeS { bottom: -6px; cursor: s-resize; } +.edgeN { + top: -6px; +} +.edgeS { + bottom: -6px; + cursor: s-resize; +} // Vertical edges (left/right) — full height, 12px wide -.edgeE, .edgeW { +.edgeE, +.edgeW { top: 12px; bottom: 12px; width: 12px; @@ -690,8 +648,13 @@ $red: #ef4444; } } -.edgeE { right: -6px; } -.edgeW { left: -6px; cursor: w-resize; } +.edgeE { + right: -6px; +} +.edgeW { + left: -6px; + cursor: w-resize; +} // ============================================================================= // Delete Button @@ -720,7 +683,13 @@ $red: #ef4444; opacity: 0; transform: scale(0.8); will-change: opacity, transform; - transition: opacity 0.2s ease-out, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.12s ease, color 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease; + transition: + opacity 0.2s ease-out, + transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), + background 0.12s ease, + color 0.12s ease, + border-color 0.12s ease, + box-shadow 0.12s ease; .placement:hover &, .selected &, @@ -797,7 +766,8 @@ $red: #ef4444; border-radius: 4px; white-space: nowrap; font-weight: 500; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); } @@ -830,11 +800,15 @@ $red: #ef4444; font-size: 9px; font-weight: 600; color: $blue; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; text-transform: uppercase; letter-spacing: 0.04em; box-shadow: 0 4px 16px rgba(59, 130, 246, 0.15); - transition: width 0.08s ease, height 0.08s ease, opacity 0.08s ease; + transition: + width 0.08s ease, + height 0.08s ease, + opacity 0.08s ease; } .dragPreviewWireframe { @@ -862,14 +836,14 @@ $red: #ef4444; 0 1px 8px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(0, 0, 0, 0.04); z-index: 100001; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; cursor: default; // Initial state (before enter transition triggers) opacity: 0; filter: blur(5px); - // Child element transitions (match settings panel) .paletteItem, .paletteItemLabel, @@ -1058,12 +1032,32 @@ $red: #ef4444; mask-image: linear-gradient(to bottom, transparent 0, black 32px); } &.fadeBottom { - -webkit-mask-image: linear-gradient(to bottom, black calc(100% - 32px), transparent 100%); - mask-image: linear-gradient(to bottom, black calc(100% - 32px), transparent 100%); + -webkit-mask-image: linear-gradient( + to bottom, + black calc(100% - 32px), + transparent 100% + ); + mask-image: linear-gradient( + to bottom, + black calc(100% - 32px), + transparent 100% + ); } &.fadeTop.fadeBottom { - -webkit-mask-image: linear-gradient(to bottom, transparent 0, black 32px, black calc(100% - 32px), transparent 100%); - mask-image: linear-gradient(to bottom, transparent 0, black 32px, black calc(100% - 32px), transparent 100%); + -webkit-mask-image: linear-gradient( + to bottom, + transparent 0, + black 32px, + black calc(100% - 32px), + transparent 100% + ); + mask-image: linear-gradient( + to bottom, + transparent 0, + black 32px, + black calc(100% - 32px), + transparent 100% + ); } &::-webkit-scrollbar { @@ -1098,7 +1092,9 @@ $red: #ef4444; .paletteFooterInnerContent { opacity: 1; transform: translateY(0); - transition: opacity 0.15s ease, transform 0.15s ease; + transition: + opacity 0.15s ease, + transform 0.15s ease; .footerHidden & { opacity: 0; @@ -1167,7 +1163,6 @@ $red: #ef4444; gap: 0.75rem; } - // Rolling count number .rollingWrap { display: inline-block; @@ -1183,29 +1178,60 @@ $red: #ef4444; top: 0; } -.exitUp { animation: numExitUp 0.25s cubic-bezier(0.32, 0.72, 0, 1) forwards; } -.enterUp { animation: numEnterUp 0.25s cubic-bezier(0.32, 0.72, 0, 1) forwards; } -.exitDown { animation: numExitDown 0.25s cubic-bezier(0.32, 0.72, 0, 1) forwards; } -.enterDown { animation: numEnterDown 0.25s cubic-bezier(0.32, 0.72, 0, 1) forwards; } +.exitUp { + animation: numExitUp 0.25s cubic-bezier(0.32, 0.72, 0, 1) forwards; +} +.enterUp { + animation: numEnterUp 0.25s cubic-bezier(0.32, 0.72, 0, 1) forwards; +} +.exitDown { + animation: numExitDown 0.25s cubic-bezier(0.32, 0.72, 0, 1) forwards; +} +.enterDown { + animation: numEnterDown 0.25s cubic-bezier(0.32, 0.72, 0, 1) forwards; +} @keyframes numExitUp { - from { transform: translateY(0); opacity: 1; } - to { transform: translateY(-110%); opacity: 0; } + from { + transform: translateY(0); + opacity: 1; + } + to { + transform: translateY(-110%); + opacity: 0; + } } @keyframes numEnterUp { - from { transform: translateY(110%); opacity: 0; } - to { transform: translateY(0); opacity: 1; } + from { + transform: translateY(110%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } } @keyframes numExitDown { - from { transform: translateY(0); opacity: 1; } - to { transform: translateY(110%); opacity: 0; } + from { + transform: translateY(0); + opacity: 1; + } + to { + transform: translateY(110%); + opacity: 0; + } } @keyframes numEnterDown { - from { transform: translateY(-110%); opacity: 0; } - to { transform: translateY(0); opacity: 1; } + from { + transform: translateY(-110%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } } - // ============================================================================= // Rearrange Overlay // ============================================================================= @@ -1247,21 +1273,31 @@ $red: #ef4444; &:active { cursor: grabbing; } - transition: box-shadow 0.15s, border-color 0.3s, background-color 0.3s, border-style 0s; + transition: + box-shadow 0.15s, + border-color 0.3s, + background-color 0.3s, + border-style 0s; user-select: none; pointer-events: auto; animation: sectionEnter 0.2s ease; &:hover { - box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1), 0 4px 12px rgba(0, 0, 0, 0.15); + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 0.1), + 0 4px 12px rgba(0, 0, 0, 0.15); } &.selected { border-style: solid; - box-shadow: 0 0 0 2px $blue-dim, 0 2px 8px rgba(59, 130, 246, 0.15); + box-shadow: + 0 0 0 2px $blue-dim, + 0 2px 8px rgba(59, 130, 246, 0.15); &:hover { - box-shadow: 0 0 0 2px $blue-dim, 0 2px 8px rgba(59, 130, 246, 0.15); + box-shadow: + 0 0 0 2px $blue-dim, + 0 2px 8px rgba(59, 130, 246, 0.15); } } @@ -1301,7 +1337,9 @@ $red: #ef4444; transform: scale(0.97); pointer-events: none; animation: none; - transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.32, 0.72, 0, 1); + transition: + opacity 0.2s ease, + transform 0.2s cubic-bezier(0.32, 0.72, 0, 1); } } @@ -1316,7 +1354,8 @@ $red: #ef4444; border-radius: 4px; white-space: nowrap; pointer-events: none; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); max-width: calc(100% - 8px); overflow: hidden; @@ -1335,18 +1374,23 @@ $red: #ef4444; border-radius: 4px; white-space: nowrap; pointer-events: none; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; text-transform: uppercase; letter-spacing: 0.04em; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); opacity: 0; transform: scale(0.8); - transition: opacity 0.15s ease, transform 0.15s ease; + transition: + opacity 0.15s ease, + transform 0.15s ease; &.badgeVisible { opacity: 1; transform: scale(1); - transition: opacity 0.2s cubic-bezier(0.34, 1.2, 0.64, 1), transform 0.2s cubic-bezier(0.34, 1.2, 0.64, 1); + transition: + opacity 0.2s cubic-bezier(0.34, 1.2, 0.64, 1), + transform 0.2s cubic-bezier(0.34, 1.2, 0.64, 1); } } @@ -1367,7 +1411,8 @@ $red: #ef4444; border-radius: 3px; white-space: nowrap; pointer-events: none; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; .light & { color: rgba(0, 0, 0, 0.5); @@ -1387,7 +1432,8 @@ $red: #ef4444; font-size: 9.5px; font-weight: 400; color: rgba(0, 0, 0, 0.4); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; pointer-events: auto; animation: overlayFadeIn 0.3s ease; line-height: 1.5; @@ -1438,7 +1484,7 @@ $red: #ef4444; } &::-webkit-slider-thumb:hover { - background: darken(#f97316, 8%); + background: color.adjust(#f97316, $lightness: -8%); } &::-moz-range-thumb { @@ -1495,7 +1541,6 @@ $red: #ef4444; } } - // ============================================================================= // Ghost Outlines (suggested placement) // ============================================================================= @@ -1510,7 +1555,10 @@ $red: #ef4444; user-select: none; pointer-events: auto; animation: ghostEnter 0.25s ease; - transition: box-shadow 0.15s, border-color 0.3s, opacity 0.25s; + transition: + box-shadow 0.15s, + border-color 0.3s, + opacity 0.25s; &:active { cursor: grabbing; @@ -1518,7 +1566,9 @@ $red: #ef4444; &:hover { opacity: 0.7; - box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.1), 0 4px 12px rgba(0, 0, 0, 0.08); + box-shadow: + 0 0 0 1px rgba(59, 130, 246, 0.1), + 0 4px 12px rgba(0, 0, 0, 0.08); } &.selected { @@ -1527,7 +1577,9 @@ $red: #ef4444; border-width: 2px; border-color: $blue; background: rgba(59, 130, 246, 0.08); - box-shadow: 0 0 0 2px $blue-dim, 0 2px 8px rgba(59, 130, 246, 0.15); + box-shadow: + 0 0 0 2px $blue-dim, + 0 2px 8px rgba(59, 130, 246, 0.15); } &.exiting { @@ -1535,7 +1587,9 @@ $red: #ef4444; transform: scale(0.97); pointer-events: none; animation: none; - transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.32, 0.72, 0, 1); + transition: + opacity 0.2s ease, + transform 0.2s cubic-bezier(0.32, 0.72, 0, 1); } } @@ -1552,15 +1606,22 @@ $red: #ef4444; border-radius: 3px; white-space: nowrap; pointer-events: none; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; letter-spacing: 0.02em; line-height: 1.2; animation: badgeSlideIn 0.2s ease both; } @keyframes badgeSlideIn { - from { opacity: 0; transform: translateY(4px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } } .ghostBadgeExtra { @@ -1569,8 +1630,12 @@ $red: #ef4444; } @keyframes badgeExtraIn { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + to { + opacity: 1; + } } // ============================================================================= @@ -1598,7 +1663,8 @@ $red: #ef4444; border-radius: 3px; white-space: nowrap; pointer-events: none; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: rgba(150, 150, 150, 0.08); } @@ -1627,13 +1693,23 @@ $red: #ef4444; } @keyframes connectorDraw { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + to { + opacity: 1; + } } @keyframes connectorDotIn { - from { transform: scale(0); opacity: 0; } - to { transform: scale(1); opacity: 1; } + from { + transform: scale(0); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } } .connectorExiting { @@ -1645,13 +1721,23 @@ $red: #ef4444; } @keyframes connectorOut { - from { opacity: 1; } - to { opacity: 0; } + from { + opacity: 1; + } + to { + opacity: 0; + } } @keyframes connectorDotOut { - from { transform: scale(1); opacity: 1; } - to { transform: scale(0); opacity: 0; } + from { + transform: scale(1); + opacity: 1; + } + to { + transform: scale(0); + opacity: 0; + } } // ============================================================================= @@ -1659,28 +1745,52 @@ $red: #ef4444; // ============================================================================= @keyframes placementEnter { - from { opacity: 0; transform: scale(0.85); } - to { opacity: 1; transform: scale(1); } + from { + opacity: 0; + transform: scale(0.85); + } + to { + opacity: 1; + transform: scale(1); + } } @keyframes sectionEnter { - from { opacity: 0; transform: scale(0.96); } - to { opacity: 1; transform: scale(1); } + from { + opacity: 0; + transform: scale(0.96); + } + to { + opacity: 1; + transform: scale(1); + } } @keyframes highlightFadeIn { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + to { + opacity: 1; + } } @keyframes overlayFadeIn { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + to { + opacity: 1; + } } @keyframes ghostEnter { - from { opacity: 0; transform: scale(0.96); } - to { opacity: 0.6; transform: scale(1); } + from { + opacity: 0; + transform: scale(0.96); + } + to { + opacity: 0.6; + transform: scale(1); + } } - - diff --git a/package/src/components/page-toolbar-css/index.tsx b/package/src/components/page-toolbar-css/index.tsx index 42df435a..358297d6 100644 --- a/package/src/components/page-toolbar-css/index.tsx +++ b/package/src/components/page-toolbar-css/index.tsx @@ -2,6 +2,7 @@ import { useState, useCallback, useEffect, useLayoutEffect, useRef } from "react"; import { createPortal } from "react-dom"; +import { ShadowRoot } from "../shadow-root"; import { AnnotationPopupCSS, @@ -16,15 +17,11 @@ import { IconEyeAnimated, IconPausePlayAnimated, IconXmarkLarge, - IconEdit, - IconChevronLeft, - IconChevronRight, IconLayout, } from "../icons"; import { HelpTooltip } from "../help-tooltip"; import { DesignMode } from "../design-mode"; import { DesignPalette } from "../design-mode/palette"; -import designStyles from "../design-mode/styles.module.scss"; import { RearrangeOverlay } from "../design-mode/rearrange"; import { generateDesignOutput, generateRearrangeOutput } from "../design-mode/output"; import { detectPageSections } from "../design-mode/section-detection"; @@ -84,11 +81,37 @@ import { } from "../../utils/freeze-animations"; import type { Annotation } from "../../types"; -import styles from "./styles.module.scss"; import { generateOutput } from "../../utils/generate-output"; import { AnnotationMarker, ExitingMarker, PendingMarker } from "./annotation-marker"; import { SettingsPanel } from "./settings-panel"; +import { css as resetCss } from "../reset.scss"; +import styles, { css as toolbarCss } from "./styles.module.scss"; +import designStyles, { + css as designModeCss, +} from "../design-mode/styles.module.scss"; +import { css as popupCss } from "../annotation-popup-css/styles.module.scss"; +import { css as checkboxCss } from "../checkbox/styles.module.scss"; +import { css as helpTooltipCss } from "../help-tooltip/styles.module.scss"; +import { css as iconTransitionsCss } from "../icon-transitions.module.scss"; +import { css as annotationMarkerCss } from "./annotation-marker/styles.module.scss"; +import { css as checkboxFieldCss } from "./settings-panel/checkbox-field/styles.module.scss"; +import { css as settingsPanelCss } from "./settings-panel/styles.module.scss"; +import { css as switchCss } from "../switch/styles.module.scss"; + +const shadowCss = [ + resetCss, + toolbarCss, + popupCss, + checkboxCss, + designModeCss, + helpTooltipCss, + iconTransitionsCss, + annotationMarkerCss, + checkboxFieldCss, + settingsPanelCss, + switchCss, +].join("\n"); /** * Composes element identification with React component detection. * This is the boundary where we combine framework-agnostic element ID @@ -139,7 +162,11 @@ type HoverInfo = { reactComponents?: string | null; }; -export type OutputDetailLevel = "compact" | "standard" | "detailed" | "forensic"; +export type OutputDetailLevel = + | "compact" + | "standard" + | "detailed" + | "forensic"; // ReactComponentMode is now derived from outputDetail when reactEnabled is true export type ReactComponentMode = "smart" | "filtered" | "all" | "off"; type MarkerClickBehavior = "edit" | "delete"; @@ -195,36 +222,28 @@ export const COLOR_OPTIONS = [ { id: "red", label: "Red", srgb: "#FF383C", p3: "color(display-p3 1.00 0.22 0.24)" }, ]; -const injectAgentationColorTokens = () => { - if (typeof document === "undefined") return; - if (document.getElementById("agentation-color-tokens")) return; - const style = document.createElement("style"); - style.id = "agentation-color-tokens"; - style.textContent = [ - ...COLOR_OPTIONS.map(c => ` +const agentationColorTokensCss = [ + ...COLOR_OPTIONS.map( + (c) => ` + [data-agentation-accent="${c.id}"] { + --agentation-color-accent: ${c.srgb}; + } + @supports (color: color(display-p3 0 0 0)) { [data-agentation-accent="${c.id}"] { - --agentation-color-accent: ${c.srgb}; - } - - @supports (color: color(display-p3 0 0 0)) { - [data-agentation-accent="${c.id}"] { - --agentation-color-accent: ${c.p3}; - } + --agentation-color-accent: ${c.p3}; } - `), - `:root { - ${COLOR_OPTIONS.map(c => `--agentation-color-${c.id}: ${c.srgb};`).join("\n")} - }`, - `@supports (color: color(display-p3 0 0 0)) { - :root { - ${COLOR_OPTIONS.map(c => `--agentation-color-${c.id}: ${c.p3};`).join("\n")} - } - }`, - ].join(""); - document.head.appendChild(style); -} - -injectAgentationColorTokens(); + } + `, + ), + `:host { + ${COLOR_OPTIONS.map((c) => `--agentation-color-${c.id}: ${c.srgb};`).join("\n")} + }`, + `@supports (color: color(display-p3 0 0 0)) { + :host { + ${COLOR_OPTIONS.map((c) => `--agentation-color-${c.id}: ${c.p3};`).join("\n")} + } + }`, +].join(""); // ============================================================================= // Utils @@ -548,20 +567,20 @@ export function PageFeedbackToolbarCSS({ }; }, []); -const [settings, setSettings] = useState(() => { - try { + const [settings, setSettings] = useState(() => { + try { const saved = JSON.parse(localStorage.getItem("feedback-toolbar-settings") ?? ""); - return { - ...DEFAULT_SETTINGS, - ...saved, + return { + ...DEFAULT_SETTINGS, + ...saved, annotationColorId: COLOR_OPTIONS.find(c => c.id === saved.annotationColorId) - ? saved.annotationColorId - : DEFAULT_SETTINGS.annotationColorId, - }; - } catch { - return DEFAULT_SETTINGS; - } -}); + ? saved.annotationColorId + : DEFAULT_SETTINGS.annotationColorId, + }; + } catch { + return DEFAULT_SETTINGS; + } + }); const [isDarkMode, setIsDarkMode] = useState(true); const [showEntranceAnimation, setShowEntranceAnimation] = useState(false); @@ -1803,20 +1822,13 @@ const [settings, setSettings] = useState(() => { "mark", "small", "sub", "sup", "[contenteditable]" ].join(", "); - const notAgentationSelector = `:not([data-agentation-root]):not([data-agentation-root] *)`; - const style = document.createElement("style"); style.id = "feedback-cursor-styles"; // Text elements get text cursor (higher specificity with body prefix) // Everything else gets crosshair style.textContent = ` - body ${notAgentationSelector} { - cursor: crosshair !important; - } - - body :is(${textElementsSelector})${notAgentationSelector} { - cursor: text !important; - } + body { cursor: crosshair !important; } + body :is(${textElementsSelector}) { cursor: text !important; } `; document.head.appendChild(style); @@ -2754,60 +2766,60 @@ const [settings, setSettings] = useState(() => { // Handle marker hover - finds element(s) for live position tracking const handleMarkerHover = useCallback( (annotation: Annotation | null) => { - if (!annotation) { - setHoveredMarkerId(null); - setHoveredTargetElement(null); - setHoveredTargetElements([]); - return; - } + if (!annotation) { + setHoveredMarkerId(null); + setHoveredTargetElement(null); + setHoveredTargetElements([]); + return; + } - setHoveredMarkerId(annotation.id); - - // Find elements at the annotation's position(s) for live tracking - if (annotation.elementBoundingBoxes?.length) { - // Cmd+shift+click: find element at each bounding box center - const elements: HTMLElement[] = []; - for (const bb of annotation.elementBoundingBoxes) { - const centerX = bb.x + bb.width / 2; - const centerY = bb.y + bb.height / 2 - window.scrollY; - // Use elementsFromPoint to look through the marker if it's covering - const allEls = document.elementsFromPoint(centerX, centerY); - const el = allEls.find( - (e) => !e.closest('[data-annotation-marker]') && !e.closest('[data-agentation-root]'), - ) as HTMLElement | undefined; - if (el) elements.push(el); - } - setHoveredTargetElements(elements); - setHoveredTargetElement(null); - } else if (annotation.boundingBox) { - // Single element - const bb = annotation.boundingBox; + setHoveredMarkerId(annotation.id); + + // Find elements at the annotation's position(s) for live tracking + if (annotation.elementBoundingBoxes?.length) { + // Cmd+shift+click: find element at each bounding box center + const elements: HTMLElement[] = []; + for (const bb of annotation.elementBoundingBoxes) { const centerX = bb.x + bb.width / 2; - const centerY = annotation.isFixed - ? bb.y + bb.height / 2 - : bb.y + bb.height / 2 - window.scrollY; - const el = deepElementFromPoint(centerX, centerY); + const centerY = bb.y + bb.height / 2 - window.scrollY; + // Use elementsFromPoint to look through the marker if it's covering + const allEls = document.elementsFromPoint(centerX, centerY); + const el = allEls.find( + (e) => !e.closest('[data-annotation-marker]') && !e.closest('[data-agentation-root]'), + ) as HTMLElement | undefined; + if (el) elements.push(el); + } + setHoveredTargetElements(elements); + setHoveredTargetElement(null); + } else if (annotation.boundingBox) { + // Single element + const bb = annotation.boundingBox; + const centerX = bb.x + bb.width / 2; + const centerY = annotation.isFixed + ? bb.y + bb.height / 2 + : bb.y + bb.height / 2 - window.scrollY; + const el = deepElementFromPoint(centerX, centerY); - // Validate found element's size roughly matches stored bounding box - // (prevents using wrong child element when clicking center of a container) - if (el) { - const elRect = el.getBoundingClientRect(); - const widthRatio = elRect.width / bb.width; - const heightRatio = elRect.height / bb.height; - // If found element is much smaller than stored, it's probably a child - don't use it - if (widthRatio < 0.5 || heightRatio < 0.5) { - setHoveredTargetElement(null); - } else { - setHoveredTargetElement(el); - } - } else { + // Validate found element's size roughly matches stored bounding box + // (prevents using wrong child element when clicking center of a container) + if (el) { + const elRect = el.getBoundingClientRect(); + const widthRatio = elRect.width / bb.width; + const heightRatio = elRect.height / bb.height; + // If found element is much smaller than stored, it's probably a child - don't use it + if (widthRatio < 0.5 || heightRatio < 0.5) { setHoveredTargetElement(null); + } else { + setHoveredTargetElement(el); } - setHoveredTargetElements([]); } else { setHoveredTargetElement(null); - setHoveredTargetElements([]); } + setHoveredTargetElements([]); + } else { + setHoveredTargetElement(null); + setHoveredTargetElements([]); + } }, [], ); @@ -3090,16 +3102,16 @@ const [settings, setSettings] = useState(() => { // Append design layout section if there are placements (or purpose in wireframe mode) if (designPlacements.length > 0 || (wireframeOnly && wireframePurpose)) { output += "\n" + generateDesignOutput(designPlacements, { - width: window.innerWidth, - height: window.innerHeight, + width: window.innerWidth, + height: window.innerHeight, }, { blankCanvas, wireframePurpose: wireframePurpose || undefined }, settings.outputDetail); } // Append rearrange section if sections were reordered if (rearrangeState) { const rearrangeOutput = generateRearrangeOutput(rearrangeState, settings.outputDetail, { - width: window.innerWidth, - height: window.innerHeight, + width: window.innerWidth, + height: window.innerHeight, }); if (rearrangeOutput) { output += "\n" + rearrangeOutput; @@ -3160,16 +3172,16 @@ const [settings, setSettings] = useState(() => { // Append design layout section if there are placements if (designPlacements.length > 0) { output += "\n" + generateDesignOutput(designPlacements, { - width: window.innerWidth, - height: window.innerHeight, + width: window.innerWidth, + height: window.innerHeight, }, { blankCanvas, wireframePurpose: wireframePurpose || undefined }, settings.outputDetail); } // Append rearrange section if sections were reordered if (rearrangeState) { const rearrangeOutput = generateRearrangeOutput(rearrangeState, settings.outputDetail, { - width: window.innerWidth, - height: window.innerHeight, + width: window.innerWidth, + height: window.innerHeight, }); if (rearrangeOutput) { output += "\n" + rearrangeOutput; @@ -3563,1018 +3575,793 @@ const [settings, setSettings] = useState(() => { }; return createPortal( -
- {/* Toolbar */} -
- {/* Morphing container */} -
{ - // Don't activate if we just finished dragging - if (justFinishedToolbarDragRef.current) { - justFinishedToolbarDragRef.current = false; - e.preventDefault(); - return; - } - setIsActive(true); - } - : undefined - } - onMouseDown={handleToolbarMouseDown} - role={!isActive ? "button" : undefined} - tabIndex={!isActive ? 0 : -1} - title={!isActive ? "Start feedback mode" : undefined} - > - {/* Toggle content - visible when collapsed */} + + +
+ {/* Toolbar */}
- - {hasVisibleAnnotations && ( - - {visibleAnnotations.length} - - )} -
- - {/* Controls content - visible when expanded */} -
+ {/* Morphing container */}
{ + // Don't activate if we just finished dragging + if (justFinishedToolbarDragRef.current) { + justFinishedToolbarDragRef.current = false; + e.preventDefault(); + return; + } + setIsActive(true); + } + : undefined + } + onMouseDown={handleToolbarMouseDown} + role={!isActive ? "button" : undefined} + tabIndex={!isActive ? 0 : -1} + title={!isActive ? "Start feedback mode" : undefined} > - - - {isFrozen ? "Resume animations" : "Pause animations"} - P - -
+ + {hasVisibleAnnotations && ( + + {visibleAnnotations.length} + + )} +
- {/* Draw mode disabled for now -
- - - {isDrawMode ? "Exit draw mode" : "Draw mode"} - D - -
- */} - -
- - - {isDesignMode ? "Exit layout mode" : "Layout mode"} - L - -
+
+ + + {isFrozen ? "Resume animations" : "Pause animations"} + P + +
-
- - - {showMarkers ? "Hide markers" : "Show markers"} - H - -
+ {/* Draw mode disabled for now +
+ + + {isDrawMode ? "Exit draw mode" : "Draw mode"} + D + +
+ */} + +
+ + + {isDesignMode ? "Exit layout mode" : "Layout mode"} + L + +
-
- - - {isDesignMode && blankCanvas ? "Copy layout" : "Copy feedback"} - C - -
+
+ + + {showMarkers ? "Hide markers" : "Show markers"} + H + +
- {/* Send button - only visible when webhook URL is available AND auto-send is off */} -
- + + {isDesignMode && blankCanvas ? "Copy layout" : "Copy feedback"} + C - )} - - - Send Annotations - S - -
+
-
- - - Clear all - X - -
+ {/* Send button - only visible when webhook URL is available AND auto-send is off */} +
+ + + Send Annotations + S + +
+ +
+ + + Clear all + X + +
-
- + {endpoint && connectionStatus !== "disconnected" && ( + + )} + Settings +
+ +
+ +
window.innerWidth - 120 + ? styles.buttonWrapperAlignRight + : "" + }`} + > + + + Exit + Esc + +
+
+ + {/* Layout Mode Palette */} + { + setActiveDesignComponent(activeDesignComponent === type ? null : type); }} - > - - - {endpoint && connectionStatus !== "disconnected" && ( - { + const sections = detectPageSections(); + const existing = rearrangeState?.sections ?? []; + const existingSelectors = new Set(existing.map(s => s.selector)); + const newSections = sections.filter(s => !existingSelectors.has(s.selector)); + const merged = [...existing, ...newSections]; + const mergedOrder = [...(rearrangeState?.originalOrder ?? []), ...newSections.map(s => s.id)]; + setRearrangeState({ + sections: merged, + originalOrder: mergedOrder, + detectedAt: Date.now(), + }); + }} + placementCount={designPlacements.length} + onClearPlacements={() => { + // Animate placements and rearrange sections out, then clear + setDesignClearSignal(n => n + 1); + setRearrangeClearSignal(n => n + 1); + originalSetTimeout(() => { + setRearrangeState({ + sections: [], + originalOrder: [], + detectedAt: Date.now(), + }); + }, 200); + }} + blankCanvas={blankCanvas} + onBlankCanvasChange={(on) => { + const emptyRearrange = { sections: [], originalOrder: [], detectedAt: Date.now() }; + if (on) { + // Entering wireframe: stash all explore state, restore wireframe state + exploreStashRef.current = { rearrange: rearrangeState, placements: designPlacements }; + setRearrangeState(wireframeStashRef.current.rearrange || emptyRearrange); + setDesignPlacements(wireframeStashRef.current.placements); + setActiveDesignComponent(null); + } else { + // Leaving wireframe: stash all wireframe state, restore explore state + wireframeStashRef.current = { rearrange: rearrangeState, placements: designPlacements }; + setRearrangeState(exploreStashRef.current.rearrange || emptyRearrange); + setDesignPlacements(exploreStashRef.current.placements); } - /> - )} - Settings + setBlankCanvas(on); + }} + wireframePurpose={wireframePurpose} + onWireframePurposeChange={setWireframePurpose} + Tooltip={HelpTooltip} + onDragStart={(type, e) => { + e.preventDefault(); + const def = DEFAULT_SIZES[type]; + let preview: HTMLDivElement | null = null; + let didDrag = false; + const startX = e.clientX; + const startY = e.clientY; + + // Find toolbar bottom for distance-based scaling + const toolbar = (e.target as HTMLElement).closest("[data-feedback-toolbar]"); + const toolbarTop = toolbar?.getBoundingClientRect().top ?? window.innerHeight; + + const onMove = (ev: MouseEvent) => { + const dx = ev.clientX - startX; + const dy = ev.clientY - startY; + + if (!didDrag && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) { + didDrag = true; + preview = document.createElement("div"); + preview.className = `${designStyles.dragPreview}${blankCanvas ? ` ${designStyles.dragPreviewWireframe}` : ""}`; + document.body.appendChild(preview); + } + + if (!preview) return; + + // Scale up as cursor moves away from toolbar + const dist = Math.max(0, toolbarTop - ev.clientY); + const progress = Math.min(1, dist / 180); + const eased = 1 - Math.pow(1 - progress, 2); // ease-out + + const minW = 28; + const minH = 20; + const maxW = Math.min(140, def.width * 0.18); + const maxH = Math.min(90, def.height * 0.18); + const w = minW + (maxW - minW) * eased; + const h = minH + (maxH - minH) * eased; + + preview.style.width = `${w}px`; + preview.style.height = `${h}px`; + preview.style.left = `${ev.clientX - w / 2}px`; + preview.style.top = `${ev.clientY - h / 2}px`; + preview.style.opacity = `${0.5 + 0.5 * eased}`; + preview.textContent = eased > 0.25 ? type : ""; + }; + + const onUp = (ev: MouseEvent) => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + if (preview) document.body.removeChild(preview); + + if (didDrag) { + const w = def.width; + const h = def.height; + const scrollY = window.scrollY; + const x = Math.max(0, ev.clientX - w / 2); + const y = Math.max(0, ev.clientY + scrollY - h / 2); + const placement: DesignPlacement = { + id: `dp-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, + type, + x, + y, + width: w, + height: h, + scrollY, + timestamp: Date.now(), + }; + setDesignPlacements((prev) => [...prev, placement]); + setActiveDesignComponent(null); + // Deselect any previously selected placements + designSelectedIdsRef.current = new Set(); + setDesignDeselectSignal(n => n + 1); + } + }; + + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + }} + /> + + setSettings((s) => ({ ...s, ...patch }))} + isDarkMode={isDarkMode} + onToggleTheme={toggleTheme} + isDevMode={isDevMode} + connectionStatus={connectionStatus} + endpoint={endpoint} + isVisible={showSettingsVisible} + toolbarNearBottom={!!toolbarPosition && toolbarPosition.y < 230} + settingsPage={settingsPage} + onSettingsPageChange={setSettingsPage} + onHideToolbar={hideToolbarTemporarily} + />
+
+ {/* Blank canvas backdrop — stays mounted so opacity transition works on open/close */} + {(isDesignMode || designOverlayExiting) && (
+ )} -
window.innerWidth - 120 - ? styles.buttonWrapperAlignRight - : "" - }`} - > - - - Exit - Esc - + {/* Wireframe hint — bottom-left notice */} + {isDesignMode && blankCanvas && canvasReady && ( +
+
+ Toggle Opacity + setCanvasOpacity(Number(e.target.value))} + /> +
+
+ Wireframe Mode + + +
+ Drag components onto the canvas.
Copied output will only include the wireframed layout.
-
+ )} - {/* Layout Mode Palette */} - { - setActiveDesignComponent(activeDesignComponent === type ? null : type); - }} + {/* Layout mode overlay — passthrough when no component selected */} + {(isDesignMode || designOverlayExiting) && ( + { - const sections = detectPageSections(); - const existing = rearrangeState?.sections ?? []; - const existingSelectors = new Set(existing.map(s => s.selector)); - const newSections = sections.filter(s => !existingSelectors.has(s.selector)); - const merged = [...existing, ...newSections]; - const mergedOrder = [...(rearrangeState?.originalOrder ?? []), ...newSections.map(s => s.id)]; - setRearrangeState({ - sections: merged, - originalOrder: mergedOrder, - detectedAt: Date.now(), - }); - }} - placementCount={designPlacements.length} - onClearPlacements={() => { - // Animate placements and rearrange sections out, then clear - setDesignClearSignal(n => n + 1); - setRearrangeClearSignal(n => n + 1); - originalSetTimeout(() => { - setRearrangeState({ - sections: [], - originalOrder: [], - detectedAt: Date.now(), - }); - }, 200); - }} - blankCanvas={blankCanvas} - onBlankCanvasChange={(on) => { - const emptyRearrange = { sections: [], originalOrder: [], detectedAt: Date.now() }; - if (on) { - // Entering wireframe: stash all explore state, restore wireframe state - exploreStashRef.current = { rearrange: rearrangeState, placements: designPlacements }; - setRearrangeState(wireframeStashRef.current.rearrange || emptyRearrange); - setDesignPlacements(wireframeStashRef.current.placements); - setActiveDesignComponent(null); - } else { - // Leaving wireframe: stash all wireframe state, restore explore state - wireframeStashRef.current = { rearrange: rearrangeState, placements: designPlacements }; - setRearrangeState(exploreStashRef.current.rearrange || emptyRearrange); - setDesignPlacements(exploreStashRef.current.placements); + exiting={designOverlayExiting} + onInteractionChange={setDesignInteracting} + passthrough={!activeDesignComponent} + extraSnapRects={rearrangeState?.sections.map((s) => s.currentRect)} + deselectSignal={designDeselectSignal} + clearSignal={designClearSignal} + wireframe={blankCanvas} + onSelectionChange={(ids, isShift) => { + designSelectedIdsRef.current = ids; + if (!isShift) { + rearrangeSelectedIdsRef.current = new Set(); + setRearrangeDeselectSignal(n => n + 1); } - setBlankCanvas(on); }} - wireframePurpose={wireframePurpose} - onWireframePurposeChange={setWireframePurpose} - Tooltip={HelpTooltip} - onDragStart={(type, e) => { - e.preventDefault(); - const def = DEFAULT_SIZES[type]; - let preview: HTMLDivElement | null = null; - let didDrag = false; - const startX = e.clientX; - const startY = e.clientY; - - // Find toolbar bottom for distance-based scaling - const toolbar = (e.target as HTMLElement).closest("[data-feedback-toolbar]"); - const toolbarTop = toolbar?.getBoundingClientRect().top ?? window.innerHeight; - - const onMove = (ev: MouseEvent) => { - const dx = ev.clientX - startX; - const dy = ev.clientY - startY; - - if (!didDrag && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) { - didDrag = true; - preview = document.createElement("div"); - preview.className = `${designStyles.dragPreview}${blankCanvas ? ` ${designStyles.dragPreviewWireframe}` : ""}`; - document.body.appendChild(preview); + onDragMove={(dx, dy) => { + // Move selected rearrange sections by same delta + const selIds = rearrangeSelectedIdsRef.current; + if (!selIds.size || !rearrangeState) return; + // Cache start positions on first move + if (!crossDragStartRef.current) { + crossDragStartRef.current = new Map(); + for (const s of rearrangeState.sections) { + if (selIds.has(s.id)) { + crossDragStartRef.current.set(s.id, { x: s.currentRect.x, y: s.currentRect.y }); + } } - - if (!preview) return; - - // Scale up as cursor moves away from toolbar - const dist = Math.max(0, toolbarTop - ev.clientY); - const progress = Math.min(1, dist / 180); - const eased = 1 - Math.pow(1 - progress, 2); // ease-out - - const minW = 28; - const minH = 20; - const maxW = Math.min(140, def.width * 0.18); - const maxH = Math.min(90, def.height * 0.18); - const w = minW + (maxW - minW) * eased; - const h = minH + (maxH - minH) * eased; - - preview.style.width = `${w}px`; - preview.style.height = `${h}px`; - preview.style.left = `${ev.clientX - w / 2}px`; - preview.style.top = `${ev.clientY - h / 2}px`; - preview.style.opacity = `${0.5 + 0.5 * eased}`; - preview.textContent = eased > 0.25 ? type : ""; - }; - - const onUp = (ev: MouseEvent) => { - window.removeEventListener("mousemove", onMove); - window.removeEventListener("mouseup", onUp); - if (preview) document.body.removeChild(preview); - - if (didDrag) { - const w = def.width; - const h = def.height; - const scrollY = window.scrollY; - const x = Math.max(0, ev.clientX - w / 2); - const y = Math.max(0, ev.clientY + scrollY - h / 2); - const placement: DesignPlacement = { - id: `dp-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, - type, - x, - y, - width: w, - height: h, - scrollY, - timestamp: Date.now(), + } + for (const s of rearrangeState.sections) { + if (!selIds.has(s.id)) continue; + const start = crossDragStartRef.current.get(s.id); + if (!start) continue; + const outlineEl = document.querySelector(`[data-rearrange-section="${s.id}"]`) as HTMLElement | null; + if (outlineEl) outlineEl.style.transform = `translate(${dx}px, ${dy}px)`; + } + }} + onDragEnd={(dx, dy, committed) => { + const selIds = rearrangeSelectedIdsRef.current; + const starts = crossDragStartRef.current; + crossDragStartRef.current = null; + if (!selIds.size || !rearrangeState || !starts) return; + // Clear outline transforms + for (const id of selIds) { + const el = document.querySelector(`[data-rearrange-section="${id}"]`) as HTMLElement | null; + if (el) el.style.transform = ""; + } + if (committed) { + setRearrangeState(prev => { + if (!prev) return prev; + return { + ...prev, + sections: prev.sections.map(s => { + const start = starts.get(s.id); + if (!start) return s; + return { ...s, currentRect: { ...s.currentRect, x: Math.max(0, start.x + dx), y: Math.max(0, start.y + dy) } }; + }), }; - setDesignPlacements((prev) => [...prev, placement]); - setActiveDesignComponent(null); - // Deselect any previously selected placements - designSelectedIdsRef.current = new Set(); - setDesignDeselectSignal(n => n + 1); - } - }; - - window.addEventListener("mousemove", onMove); - window.addEventListener("mouseup", onUp); + }); + } }} /> + )} - setSettings((s) => ({ ...s, ...patch }))} - isDarkMode={isDarkMode} - onToggleTheme={toggleTheme} - isDevMode={isDevMode} - connectionStatus={connectionStatus} - endpoint={endpoint} - isVisible={showSettingsVisible} - toolbarNearBottom={!!toolbarPosition && toolbarPosition.y < 230} - settingsPage={settingsPage} - onSettingsPageChange={setSettingsPage} - onHideToolbar={hideToolbarTemporarily} - /> -
-
- - {/* Blank canvas backdrop — stays mounted so opacity transition works on open/close */} - {(isDesignMode || designOverlayExiting) && ( -
- )} - - {/* Wireframe hint — bottom-left notice */} - {isDesignMode && blankCanvas && canvasReady && ( -
-
- Toggle Opacity - setCanvasOpacity(Number(e.target.value))} - /> -
-
- Wireframe Mode - - -
- Drag components onto the canvas.
Copied output will only include the wireframed layout. -
- )} - - {/* Layout mode overlay — passthrough when no component selected */} - {(isDesignMode || designOverlayExiting) && ( - s.currentRect)} - deselectSignal={designDeselectSignal} - clearSignal={designClearSignal} - wireframe={blankCanvas} - onSelectionChange={(ids, isShift) => { - designSelectedIdsRef.current = ids; - if (!isShift) { - rearrangeSelectedIdsRef.current = new Set(); - setRearrangeDeselectSignal(n => n + 1); - } - }} - onDragMove={(dx, dy) => { - // Move selected rearrange sections by same delta - const selIds = rearrangeSelectedIdsRef.current; - if (!selIds.size || !rearrangeState) return; - // Cache start positions on first move - if (!crossDragStartRef.current) { - crossDragStartRef.current = new Map(); - for (const s of rearrangeState.sections) { - if (selIds.has(s.id)) { - crossDragStartRef.current.set(s.id, { x: s.currentRect.x, y: s.currentRect.y }); - } - } - } - for (const s of rearrangeState.sections) { - if (!selIds.has(s.id)) continue; - const start = crossDragStartRef.current.get(s.id); - if (!start) continue; - const outlineEl = document.querySelector(`[data-rearrange-section="${s.id}"]`) as HTMLElement | null; - if (outlineEl) outlineEl.style.transform = `translate(${dx}px, ${dy}px)`; - } - }} - onDragEnd={(dx, dy, committed) => { - const selIds = rearrangeSelectedIdsRef.current; - const starts = crossDragStartRef.current; - crossDragStartRef.current = null; - if (!selIds.size || !rearrangeState || !starts) return; - // Clear outline transforms - for (const id of selIds) { - const el = document.querySelector(`[data-rearrange-section="${id}"]`) as HTMLElement | null; - if (el) el.style.transform = ""; - } - if (committed) { - setRearrangeState(prev => { - if (!prev) return prev; - return { - ...prev, - sections: prev.sections.map(s => { - const start = starts.get(s.id); - if (!start) return s; - return { ...s, currentRect: { ...s.currentRect, x: Math.max(0, start.x + dx), y: Math.max(0, start.y + dy) } }; - }), - }; - }); - } - }} - /> - )} - - {/* Rearrange overlay — always active alongside design overlay */} - {(isDesignMode || designOverlayExiting) && rearrangeState && ( - ({ x: p.x, y: p.y, width: p.width, height: p.height }))} - clearSignal={rearrangeClearSignal} - deselectSignal={rearrangeDeselectSignal} - onSelectionChange={(ids, isShift) => { - rearrangeSelectedIdsRef.current = ids; - if (!isShift) { - designSelectedIdsRef.current = new Set(); - setDesignDeselectSignal(n => n + 1); - } - }} - onDragMove={(dx, dy) => { - // Move selected design placements by same delta - const selIds = designSelectedIdsRef.current; - if (!selIds.size) return; - // Cache start positions on first move - if (!crossDragStartRef.current) { - crossDragStartRef.current = new Map(); - for (const p of designPlacements) { - if (selIds.has(p.id)) { - crossDragStartRef.current.set(p.id, { x: p.x, y: p.y }); + {/* Rearrange overlay — always active alongside design overlay */} + {(isDesignMode || designOverlayExiting) && rearrangeState && ( + ({ x: p.x, y: p.y, width: p.width, height: p.height }))} + clearSignal={rearrangeClearSignal} + deselectSignal={rearrangeDeselectSignal} + onSelectionChange={(ids, isShift) => { + rearrangeSelectedIdsRef.current = ids; + if (!isShift) { + designSelectedIdsRef.current = new Set(); + setDesignDeselectSignal(n => n + 1); } - } - } - // Imperatively move placement divs - for (const id of selIds) { - const el = document.querySelector(`[data-design-placement="${id}"]`) as HTMLElement | null; - if (el) el.style.transform = `translate(${dx}px, ${dy}px)`; - } - }} - onDragEnd={(dx, dy, committed) => { - const selIds = designSelectedIdsRef.current; - const starts = crossDragStartRef.current; - crossDragStartRef.current = null; - if (!selIds.size || !starts) return; - // Clear transforms - for (const id of selIds) { - const el = document.querySelector(`[data-design-placement="${id}"]`) as HTMLElement | null; - if (el) el.style.transform = ""; - } - if (committed) { - setDesignPlacements(prev => prev.map(p => { - const start = starts.get(p.id); - if (!start) return p; - return { ...p, x: Math.max(0, start.x + dx), y: Math.max(0, start.y + dy) }; - })); - } - }} - /> - )} - - {/* Draw canvas — outside overlay so it can fade on toolbar close */} - - - {/* Markers layer - normal scrolling markers */} -
- {markersVisible && - visibleAnnotations - .filter((a) => !a.isFixed) - .map((annotation, layerIndex, arr) => ( - a.id === annotation.id)} - layerIndex={layerIndex} - layerSize={arr.length} - isExiting={markersExiting} - isClearing={isClearing} - isAnimated={animatedMarkers.has(annotation.id)} - isHovered={!markersExiting && hoveredMarkerId === annotation.id} - isDeleting={deletingMarkerId === annotation.id} - isEditingAny={!!editingAnnotation} - renumberFrom={renumberFrom} - markerClickBehavior={settings.markerClickBehavior} - tooltipStyle={getTooltipPosition(annotation)} - onHoverEnter={(a) => - !markersExiting && - a.id !== recentlyAddedIdRef.current && - handleMarkerHover(a) + }} + onDragMove={(dx, dy) => { + // Move selected design placements by same delta + const selIds = designSelectedIdsRef.current; + if (!selIds.size) return; + // Cache start positions on first move + if (!crossDragStartRef.current) { + crossDragStartRef.current = new Map(); + for (const p of designPlacements) { + if (selIds.has(p.id)) { + crossDragStartRef.current.set(p.id, { x: p.x, y: p.y }); + } + } } - onHoverLeave={() => handleMarkerHover(null)} - onClick={(a) => - settings.markerClickBehavior === "delete" - ? deleteAnnotation(a.id) - : startEditAnnotation(a) + // Imperatively move placement divs + for (const id of selIds) { + const el = document.querySelector(`[data-design-placement="${id}"]`) as HTMLElement | null; + if (el) el.style.transform = `translate(${dx}px, ${dy}px)`; } - onContextMenu={startEditAnnotation} - /> - ))} - {markersVisible && - !markersExiting && - exitingAnnotationsList - .filter((a) => !a.isFixed) - .map((a) => )} -
- - {/* Fixed markers layer */} -
- {markersVisible && - visibleAnnotations - .filter((a) => a.isFixed) - .map((annotation, layerIndex, arr) => ( - a.id === annotation.id)} - layerIndex={layerIndex} - layerSize={arr.length} - isExiting={markersExiting} - isClearing={isClearing} - isAnimated={animatedMarkers.has(annotation.id)} - isHovered={!markersExiting && hoveredMarkerId === annotation.id} - isDeleting={deletingMarkerId === annotation.id} - isEditingAny={!!editingAnnotation} - renumberFrom={renumberFrom} - markerClickBehavior={settings.markerClickBehavior} - tooltipStyle={getTooltipPosition(annotation)} - onHoverEnter={(a) => - !markersExiting && - a.id !== recentlyAddedIdRef.current && - handleMarkerHover(a) + }} + onDragEnd={(dx, dy, committed) => { + const selIds = designSelectedIdsRef.current; + const starts = crossDragStartRef.current; + crossDragStartRef.current = null; + if (!selIds.size || !starts) return; + // Clear transforms + for (const id of selIds) { + const el = document.querySelector(`[data-design-placement="${id}"]`) as HTMLElement | null; + if (el) el.style.transform = ""; } - onHoverLeave={() => handleMarkerHover(null)} - onClick={(a) => - settings.markerClickBehavior === "delete" - ? deleteAnnotation(a.id) - : startEditAnnotation(a) + if (committed) { + setDesignPlacements(prev => prev.map(p => { + const start = starts.get(p.id); + if (!start) return p; + return { ...p, x: Math.max(0, start.x + dx), y: Math.max(0, start.y + dy) }; + })); } - onContextMenu={startEditAnnotation} - /> - ))} - {markersVisible && - !markersExiting && - exitingAnnotationsList - .filter((a) => a.isFixed) - .map((a) => )} -
+ }} + /> + )} + {/* Draw canvas — outside overlay so it can fade on toolbar close */} + - {/* Interactive overlay */} - {isActive && ( -
- {/* Hover highlight */} - {hoverInfo?.rect && - !pendingAnnotation && - !isScrolling && - !isDragging && ( -
- )} - - {/* Cmd+shift+click multi-select highlights (during selection, before releasing modifiers) */} - {pendingMultiSelectElements - .filter((item) => document.contains(item.element)) - .map((item, index) => { - const rect = item.element.getBoundingClientRect(); - // Only show green if 2+ elements selected, otherwise use default blue - const isMulti = pendingMultiSelectElements.length > 1; - return ( -
- ); - })} - - {/* Marker hover outline (shows bounding box of hovered annotation) */} - {hoveredMarkerId && - !pendingAnnotation && - (() => { - const hoveredAnnotation = annotations.find( - (a) => a.id === hoveredMarkerId, - ); - if (!hoveredAnnotation?.boundingBox) return null; - - // Render individual element boxes if available (cmd+shift+click multi-select) - if (hoveredAnnotation.elementBoundingBoxes?.length) { - // Use live positions from hoveredTargetElements when available - if (hoveredTargetElements.length > 0) { - return hoveredTargetElements - .filter((el) => document.contains(el)) - .map((el, index) => { - const rect = el.getBoundingClientRect(); - return ( -
- ); - }); - } - // Fallback to stored bounding boxes - return hoveredAnnotation.elementBoundingBoxes.map( - (bb, index) => ( -
- ), - ); - } + {/* Markers layer - normal scrolling markers */} +
+ {markersVisible && + visibleAnnotations + .filter((a) => !a.isFixed) + .map((annotation, layerIndex, arr) => ( + a.id === annotation.id)} + layerIndex={layerIndex} + layerSize={arr.length} + isExiting={markersExiting} + isClearing={isClearing} + isAnimated={animatedMarkers.has(annotation.id)} + isHovered={!markersExiting && hoveredMarkerId === annotation.id} + isDeleting={deletingMarkerId === annotation.id} + isEditingAny={!!editingAnnotation} + renumberFrom={renumberFrom} + markerClickBehavior={settings.markerClickBehavior} + tooltipStyle={getTooltipPosition(annotation)} + onHoverEnter={(a) => + !markersExiting && + a.id !== recentlyAddedIdRef.current && + handleMarkerHover(a) + } + onHoverLeave={() => handleMarkerHover(null)} + onClick={(a) => + settings.markerClickBehavior === "delete" + ? deleteAnnotation(a.id) + : startEditAnnotation(a) + } + onContextMenu={startEditAnnotation} + /> + ))} + {markersVisible && + !markersExiting && + exitingAnnotationsList + .filter((a) => !a.isFixed) + .map((a) => )} +
- // Single element: use live position from hoveredTargetElement when available - const rect = - hoveredTargetElement && document.contains(hoveredTargetElement) - ? hoveredTargetElement.getBoundingClientRect() - : null; - - const bb = rect - ? { x: rect.left, y: rect.top, width: rect.width, height: rect.height } - : { - x: hoveredAnnotation.boundingBox.x, - y: hoveredAnnotation.isFixed - ? hoveredAnnotation.boundingBox.y - : hoveredAnnotation.boundingBox.y - scrollY, - width: hoveredAnnotation.boundingBox.width, - height: hoveredAnnotation.boundingBox.height, - }; + {/* Fixed markers layer */} +
+ {markersVisible && + visibleAnnotations + .filter((a) => a.isFixed) + .map((annotation, layerIndex, arr) => ( + a.id === annotation.id)} + layerIndex={layerIndex} + layerSize={arr.length} + isExiting={markersExiting} + isClearing={isClearing} + isAnimated={animatedMarkers.has(annotation.id)} + isHovered={!markersExiting && hoveredMarkerId === annotation.id} + isDeleting={deletingMarkerId === annotation.id} + isEditingAny={!!editingAnnotation} + renumberFrom={renumberFrom} + markerClickBehavior={settings.markerClickBehavior} + tooltipStyle={getTooltipPosition(annotation)} + onHoverEnter={(a) => + !markersExiting && + a.id !== recentlyAddedIdRef.current && + handleMarkerHover(a) + } + onHoverLeave={() => handleMarkerHover(null)} + onClick={(a) => + settings.markerClickBehavior === "delete" + ? deleteAnnotation(a.id) + : startEditAnnotation(a) + } + onContextMenu={startEditAnnotation} + /> + ))} + {markersVisible && + !markersExiting && + exitingAnnotationsList + .filter((a) => a.isFixed) + .map((a) => )} +
- const isMulti = hoveredAnnotation.isMultiSelect; - return ( -
- ); - })()} - {/* Hover tooltip */} - {hoverInfo && !pendingAnnotation && !isScrolling && !isDragging && ( + {/* Interactive overlay */} + {isActive && (
- {hoverInfo.reactComponents && ( -
- {hoverInfo.reactComponents} -
- )} -
- {hoverInfo.elementName} -
-
- )} - - {/* Pending annotation marker + popup */} - {pendingAnnotation && ( - <> - {/* Show element/area outline while adding annotation */} - {pendingAnnotation.multiSelectElements?.length - ? // Cmd+shift+click multi-select: show individual boxes with live positions - pendingAnnotation.multiSelectElements - .filter((el) => document.contains(el)) - .map((el, index) => { - const rect = el.getBoundingClientRect(); - return ( -
- ); - }) - : // Single element or drag multi-select: show single box - pendingAnnotation.targetElement && - document.contains(pendingAnnotation.targetElement) - ? // Single-click: use live getBoundingClientRect for consistent positioning - (() => { - const rect = - pendingAnnotation.targetElement!.getBoundingClientRect(); - return ( -
- ); - })() - : // Drag selection or fallback: use stored boundingBox - pendingAnnotation.boundingBox && ( -
- )} - - {(() => { - // Use stored coordinates - they match what will be saved - const markerX = pendingAnnotation.x; - const markerY = pendingAnnotation.isFixed - ? pendingAnnotation.y - : pendingAnnotation.y - scrollY; - - return ( - <> - + {/* Hover highlight */} + {hoverInfo?.rect && + !pendingAnnotation && + !isScrolling && + !isDragging && ( +
+ )} - document.contains(item.element)) + .map((item, index) => { + const rect = item.element.getBoundingClientRect(); + // Only show green if 2+ elements selected, otherwise use default blue + const isMulti = pendingMultiSelectElements.length > 1; + return ( +
window.innerHeight - 290 - ? { bottom: window.innerHeight - markerY + 20 } - : { top: markerY + 20 }), + position: "fixed", + left: rect.left, + top: rect.top, + width: rect.width, + height: rect.height, + ...(isMulti + ? {} + : { + borderColor: "color-mix(in srgb, var(--agentation-color-accent) 60%, transparent)", + backgroundColor: "color-mix(in srgb, var(--agentation-color-accent) 5%, transparent)", + }), }} /> - - ); - })()} - - )} + ); + })} + + {/* Marker hover outline (shows bounding box of hovered annotation) */} + {hoveredMarkerId && + !pendingAnnotation && + (() => { + const hoveredAnnotation = annotations.find( + (a) => a.id === hoveredMarkerId, + ); + if (!hoveredAnnotation?.boundingBox) return null; - {/* Edit annotation popup */} - {editingAnnotation && ( - <> - {/* Show element/area outline while editing */} - {editingAnnotation.elementBoundingBoxes?.length - ? // Cmd+shift+click: show individual element boxes (use live rects when available) - (() => { - // Use live positions from editingTargetElements when available - if (editingTargetElements.length > 0) { - return editingTargetElements + // Render individual element boxes if available (cmd+shift+click multi-select) + if (hoveredAnnotation.elementBoundingBoxes?.length) { + // Use live positions from hoveredTargetElements when available + if (hoveredTargetElements.length > 0) { + return hoveredTargetElements .filter((el) => document.contains(el)) .map((el, index) => { const rect = el.getBoundingClientRect(); return (
(() => { }); } // Fallback to stored bounding boxes - return editingAnnotation.elementBoundingBoxes!.map( + return hoveredAnnotation.elementBoundingBoxes.map( (bb, index) => (
(() => { /> ), ); - })() - : // Single element or drag multi-select: show single box - (() => { - // Use live position from editingTargetElement when available - const rect = - editingTargetElement && - document.contains(editingTargetElement) - ? editingTargetElement.getBoundingClientRect() - : null; - - const bb = rect - ? { x: rect.left, y: rect.top, width: rect.width, height: rect.height } - : editingAnnotation.boundingBox - ? { - x: editingAnnotation.boundingBox.x, - y: editingAnnotation.isFixed - ? editingAnnotation.boundingBox.y - : editingAnnotation.boundingBox.y - scrollY, - width: editingAnnotation.boundingBox.width, - height: editingAnnotation.boundingBox.height, - } - : null; + } - if (!bb) return null; + // Single element: use live position from hoveredTargetElement when available + const rect = + hoveredTargetElement && document.contains(hoveredTargetElement) + ? hoveredTargetElement.getBoundingClientRect() + : null; + + const bb = rect + ? { x: rect.left, y: rect.top, width: rect.width, height: rect.height } + : { + x: hoveredAnnotation.boundingBox.x, + y: hoveredAnnotation.isFixed + ? hoveredAnnotation.boundingBox.y + : hoveredAnnotation.boundingBox.y - scrollY, + width: hoveredAnnotation.boundingBox.width, + height: hoveredAnnotation.boundingBox.height, + }; + + const isMulti = hoveredAnnotation.isMultiSelect; + return ( +
+ ); + })()} - return ( -
+ {hoverInfo.reactComponents && ( +
+ {hoverInfo.reactComponents} +
+ )} +
+ {hoverInfo.elementName} +
+
+ )} + + {/* Pending annotation marker + popup */} + {pendingAnnotation && ( + <> + {/* Show element/area outline while adding annotation */} + {pendingAnnotation.multiSelectElements?.length + ? // Cmd+shift+click multi-select: show individual boxes with live positions + pendingAnnotation.multiSelectElements + .filter((el) => document.contains(el)) + .map((el, index) => { + const rect = el.getBoundingClientRect(); + return ( +
+ ); + }) + : // Single element or drag multi-select: show single box + pendingAnnotation.targetElement && + document.contains(pendingAnnotation.targetElement) + ? // Single-click: use live getBoundingClientRect for consistent positioning + (() => { + const rect = + pendingAnnotation.targetElement!.getBoundingClientRect(); + return ( +
+ }} + /> + ); + })() + : // Drag selection or fallback: use stored boundingBox + pendingAnnotation.boundingBox && ( +
+ )} + + {(() => { + // Use stored coordinates - they match what will be saved + const markerX = pendingAnnotation.x; + const markerY = pendingAnnotation.isFixed + ? pendingAnnotation.y + : pendingAnnotation.y - scrollY; + + return ( + <> + + + window.innerHeight - 290 + ? { bottom: window.innerHeight - markerY + 20 } + : { top: markerY + 20 }), + }} + /> + ); })()} + + )} - deleteAnnotation(editingAnnotation.id)} - isExiting={editExiting} - lightMode={!isDarkMode} - accentColor={ - editingAnnotation.isMultiSelect - ? "var(--agentation-color-green)" - : "var(--agentation-color-accent)" - } - style={(() => { - const markerY = editingAnnotation.isFixed - ? editingAnnotation.y - : editingAnnotation.y - scrollY; - return { - // Popup is 280px wide, centered with translateX(-50%), so 140px each side - // Clamp so popup stays 20px from viewport edges - left: Math.max( - 160, - Math.min( - window.innerWidth - 160, - (editingAnnotation.x / 100) * window.innerWidth, - ), - ), - // Position popup above or below marker to keep marker visible - ...(markerY > window.innerHeight - 290 - ? { bottom: window.innerHeight - markerY + 20 } - : { top: markerY + 20 }), - }; - })()} - /> - - )} + {/* Edit annotation popup */} + {editingAnnotation && ( + <> + {/* Show element/area outline while editing */} + {editingAnnotation.elementBoundingBoxes?.length + ? // Cmd+shift+click: show individual element boxes (use live rects when available) + (() => { + // Use live positions from editingTargetElements when available + if (editingTargetElements.length > 0) { + return editingTargetElements + .filter((el) => document.contains(el)) + .map((el, index) => { + const rect = el.getBoundingClientRect(); + return ( +
+ ); + }); + } + // Fallback to stored bounding boxes + return editingAnnotation.elementBoundingBoxes!.map( + (bb, index) => ( +
+ ), + ); + })() + : // Single element or drag multi-select: show single box + (() => { + // Use live position from editingTargetElement when available + const rect = + editingTargetElement && + document.contains(editingTargetElement) + ? editingTargetElement.getBoundingClientRect() + : null; + + const bb = rect + ? { x: rect.left, y: rect.top, width: rect.width, height: rect.height } + : editingAnnotation.boundingBox + ? { + x: editingAnnotation.boundingBox.x, + y: editingAnnotation.isFixed + ? editingAnnotation.boundingBox.y + : editingAnnotation.boundingBox.y - scrollY, + width: editingAnnotation.boundingBox.width, + height: editingAnnotation.boundingBox.height, + } + : null; + + if (!bb) return null; - {/* Drag selection - all visuals use refs for smooth 60fps */} - {isDragging && ( - <> -
-
- + return ( +
+ ); + })()} + + deleteAnnotation(editingAnnotation.id)} + isExiting={editExiting} + lightMode={!isDarkMode} + accentColor={ + editingAnnotation.isMultiSelect + ? "var(--agentation-color-green)" + : "var(--agentation-color-accent)" + } + style={(() => { + const markerY = editingAnnotation.isFixed + ? editingAnnotation.y + : editingAnnotation.y - scrollY; + return { + // Popup is 280px wide, centered with translateX(-50%), so 140px each side + // Clamp so popup stays 20px from viewport edges + left: Math.max( + 160, + Math.min( + window.innerWidth - 160, + (editingAnnotation.x / 100) * window.innerWidth, + ), + ), + // Position popup above or below marker to keep marker visible + ...(markerY > window.innerHeight - 290 + ? { bottom: window.innerHeight - markerY + 20 } + : { top: markerY + 20 }), + }; + })()} + /> + + )} + + {/* Drag selection - all visuals use refs for smooth 60fps */} + {isDragging && ( + <> +
+
+ + )} +
)} -
- )} -
, +
+ , document.body, ); } diff --git a/package/src/components/page-toolbar-css/settings-panel/checkbox-field/styles.module.scss b/package/src/components/page-toolbar-css/settings-panel/checkbox-field/styles.module.scss index 1d776d29..f97bfd8f 100644 --- a/package/src/components/page-toolbar-css/settings-panel/checkbox-field/styles.module.scss +++ b/package/src/components/page-toolbar-css/settings-panel/checkbox-field/styles.module.scss @@ -10,6 +10,7 @@ font-size: 13px; letter-spacing: -0.15px; color: rgb(26 26 26 / 0.5); + user-select: none; cursor: pointer; [data-agentation-theme="dark"] & { diff --git a/package/src/components/page-toolbar-css/styles.module.scss b/package/src/components/page-toolbar-css/styles.module.scss index 31917bb8..03546fef 100644 --- a/package/src/components/page-toolbar-css/styles.module.scss +++ b/package/src/components/page-toolbar-css/styles.module.scss @@ -387,7 +387,6 @@ ); } - &[data-error="true"] { color: var(--agentation-color-red); background-color: color-mix( @@ -1877,122 +1876,6 @@ } } -.customCheckbox { - position: relative; - width: 14px; - height: 14px; - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 4px; - background: rgba(255, 255, 255, 0.05); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - transition: - background-color 0.25s ease, - border-color 0.25s ease; - - svg { - color: #1a1a1a; - opacity: 1; - transition: opacity 0.15s ease; - } - - input[type="checkbox"]:checked + & { - border-color: rgba(255, 255, 255, 0.3); - background: rgba(255, 255, 255, 1); - } - - // Light mode variant (unchecked state) - [data-agentation-theme="light"] & { - border: 1px solid rgba(0, 0, 0, 0.15); - background: #fff; - - // Checked state in light mode - &.checked { - border-color: #1a1a1a; - background: #1a1a1a; - - svg { - color: #fff; - } - } - } -} - -.toggleLabel { - font-size: 0.8125rem; - font-weight: 400; - color: rgba(255, 255, 255, 0.5); - letter-spacing: -0.0094em; - display: flex; - align-items: center; - gap: 0.25rem; - - [data-agentation-theme="light"] & { - color: rgba(0, 0, 0, 0.5); - } -} - -// iOS-style toggle switch -.toggleSwitch { - position: relative; - display: inline-block; - width: 24px; - height: 16px; - flex-shrink: 0; - cursor: pointer; - transition: - background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1), - opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1); - - input { - opacity: 0; - width: 0; - height: 0; - } - - input:checked + .toggleSlider { - background-color: var(--agentation-color-blue); - } - - input:checked + .toggleSlider::before { - transform: translateX(8px); - } - - &.disabled { - opacity: 0.4; - - .toggleSlider { - cursor: not-allowed; - } - } -} - -.toggleSlider { - position: absolute; - cursor: pointer; - inset: 0; - border-radius: 16px; - background: #484848; // Dark mode unchecked - - [data-agentation-theme="light"] & { - background: #dddddd; // Light mode unchecked - } - - &::before { - content: ""; - position: absolute; - height: 12px; - width: 12px; - left: 2px; - bottom: 2px; - background: white; - border-radius: 50%; - transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); - } -} - // MCP Status Dot @keyframes mcpPulse { 0% { diff --git a/package/src/components/reset.scss b/package/src/components/reset.scss new file mode 100644 index 00000000..bab85871 --- /dev/null +++ b/package/src/components/reset.scss @@ -0,0 +1,489 @@ +/* Reset box-model and set borders */ +/* ============================================ */ + +*, +::before, +::after { + border-width: 0; + border-style: solid; + box-sizing: border-box; +} + +/* Document */ +/* ============================================ */ + +/** + * 1. Correct line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + * 3. Remove gray overlay on links for iOS. + * 4. Render kerning consistently in all browsers. + * 5. Correct font smoothing for macOS. + */ + +:host { + line-height: 1.5; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + -webkit-tap-highlight-color: transparent; /* 3 */ + font-feature-settings: "kern"; /* 4 */ + -webkit-font-feature-settings: "kern"; /* 5 */ + -moz-font-feature-settings: "kern"; /* 5 */ + -webkit-font-smoothing: antialiased; /* 5 */ + -moz-osx-font-smoothing: grayscale; /* 5 */ +} + +/* Vertical rhythm */ +/* ============================================ */ + +p, +table, +blockquote, +address, +pre, +iframe, +form, +figure, +dl { + margin: 0; +} + +/* Headings */ +/* ============================================ */ + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + font-size: inherit; + font-weight: inherit; +} + +/* Lists (enumeration) */ +/* ============================================ */ + +ul, +ol, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* Lists (definition) */ +/* ============================================ */ + +dd { + margin-left: 0; +} + +/* Grouping content */ +/* ============================================ */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + clear: both; + margin: 0; + border-top-width: 1px; + height: 0; /* 1 */ + box-sizing: content-box; /* 1 */ + overflow: visible; /* 2 */ + color: inherit; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + * 3. Wrap lines by default instead of overflow. + */ + +pre { + font-family: inherit; /* 1 */ + font-size: inherit; /* 2 */ + white-space: pre-line; /* 3 */ +} + +address { + font-style: inherit; +} + +/* Text-level semantics */ +/* ============================================ */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; + text-decoration: none; + color: inherit; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: none; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: "Menlo", "Monaco", "Consolas", "Courier New", monospace; /* 1 */ + font-size: inherit; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in all browsers. + */ + +sub, +sup { + position: relative; + vertical-align: baseline; + line-height: 0; + font-size: 75%; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Replaced content */ +/* ============================================ */ + +/** + * Prevent vertical alignment issues. + */ + +svg, +img, +embed, +object, +iframe { + vertical-align: bottom; +} + +/* + * 1. Remove image default bottom space. + * 2. Prevent image from overflowing the container. + */ + +img { + display: block; + max-width: 100%; +} + +/** + * Prevent alignment issues on Safari. + */ + +@supports (background: -webkit-named-image(i)) { + svg { + will-change: transform; + } +} + +/* Forms */ +/* ============================================ */ + +/** + * Reset form fields to make them styleable. + * 1. Make form elements stylable across systems iOS especially. + * 2. Inherit text-transform from parent. + */ + +button, +input, +optgroup, +select, +textarea { + -webkit-appearance: none; /* 1 */ + appearance: none; + border-radius: 0; + margin: 0; + padding: 0; + background: transparent; + vertical-align: middle; + text-align: inherit; + text-transform: inherit; /* 2 */ + font: inherit; + color: inherit; +} + +/** + * Correct cursors for clickable elements. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + cursor: pointer; +} + +button:disabled, +[type="button"]:disabled, +[type="reset"]:disabled, +[type="submit"]:disabled { + cursor: default; +} + +/** + * Clickable labels and selects. + */ + +select, +label { + cursor: pointer; +} + +/** + * Improve outlines for Firefox and unify style with input elements & buttons. + */ + +:-moz-focusring { + outline: auto; +} + +select:disabled { + opacity: inherit; +} + +/** + * 1. Remove padding. + */ + +option { + padding: 0; /* 1 */ +} + +/** + * Reset to invisible + */ + +fieldset { + margin: 0; + padding: 0; + min-width: 0; +} + +legend { + display: contents; + padding: 0; +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * Remove increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; +} + +/** + * Correct the outline style in Safari. + */ + +[type="search"] { + outline-offset: -2px; +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* + * Remove the ‘X’ from Chrome and Safari. + */ + +[type="search"]::-webkit-search-decoration, +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-results-button, +[type="search"]::-webkit-search-results-decoration { + display: none; +} + +/** + * 1. Hide file input completely. + * 2. Remove selected file text. + * 3. Set cursor to pointer for all browsers. + */ + +[type="file"] { + opacity: 0; /* 1 */ + font-size: 0; /* 2 */ + cursor: pointer; /* 3 */ +} + +/** + * Fix appearance for Firefox + */ + +[type="number"] { + -moz-appearance: textfield; +} + +/** + * Set cursor to pointer for all browsers. + */ + +[type="range"] { + cursor: pointer; +} + +/** + * Reset slider thumbs to make them styleable. + */ + +[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; +} + +[type="range"]::-moz-range-thumb { + -moz-appearance: none; + appearance: none; + border-width: 0; + border-radius: 0; + background-color: transparent; +} + +/* Interactive */ +/* ============================================ */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* + * Remove outline for editable content. + */ + +[contenteditable]:focus { + outline: auto; +} + +/* Tables */ +/* ============================================ */ + +/** +1. Correct table border color inheritance in all Chrome and Safari. +*/ + +table { + border-color: inherit; /* 1 */ + border-collapse: collapse; +} + +caption { + text-align: left; +} + +td, +th { + vertical-align: top; + padding: 0; +} + +th { + text-align: left; + font-weight: inherit; +} + +/* Misc */ +/* ============================================ */ + +/* + * Make placeholder style consistent across all browsers. + */ + +::placeholder { + color: #999; + opacity: 1; +} + +/* + * Hide focus outline but keep it visible for Windows High Contrast Mode. + */ + +:focus { + outline-style: solid; + outline-color: transparent; +} + +/* + * Hide input arrow when used with datalist. + */ + +::-webkit-calendar-picker-indicator { + display: none !important; +} \ No newline at end of file diff --git a/package/src/components/shadow-root/index.tsx b/package/src/components/shadow-root/index.tsx new file mode 100644 index 00000000..78fff427 --- /dev/null +++ b/package/src/components/shadow-root/index.tsx @@ -0,0 +1,50 @@ +import { + useRef, + useState, + useLayoutEffect, + type ReactNode, + type ElementType, + type ComponentPropsWithRef, +} from "react"; +import { createPortal } from "react-dom"; + +export interface ShadowRootProps extends ComponentPropsWithRef<"div"> { + mode?: ShadowRootMode; + delegatesFocus?: boolean; + slotAssignment?: SlotAssignmentMode; + children?: ReactNode; + host?: keyof HTMLElementTagNameMap | (string & {}); +} + +export const ShadowRoot = ({ + mode = "open", + delegatesFocus, + slotAssignment, + host = "div", + children, + ...hostProps +}: ShadowRootProps) => { + const hostRef = useRef(null); + const [shadowContainer, setShadowContainer] = useState( + null, + ); + + useLayoutEffect(() => { + const hostElement = hostRef.current; + if (!hostElement || hostElement.shadowRoot) return; + const shadow = hostElement.attachShadow({ + mode, + delegatesFocus, + slotAssignment, + }); + setShadowContainer(shadow as unknown as HTMLDivElement); + }, []); + + const Host = host as ElementType; + + return ( + + {shadowContainer && createPortal(children, shadowContainer)} + + ); +}; diff --git a/package/src/scss.d.ts b/package/src/scss.d.ts index 1de7b14c..21276d5e 100644 --- a/package/src/scss.d.ts +++ b/package/src/scss.d.ts @@ -1,10 +1,12 @@ // Type declarations for SCSS modules declare module "*.module.scss" { const classes: { [key: string]: string }; + export const css: string; export default classes; } declare module "*.scss" { const content: { [key: string]: string }; + export const css: string; export default content; } diff --git a/package/tsconfig.json b/package/tsconfig.json index c593e92d..b0203323 100644 --- a/package/tsconfig.json +++ b/package/tsconfig.json @@ -17,6 +17,6 @@ "isolatedModules": true, "noEmit": true }, - "include": ["src"], + "include": ["src", "tsup.config.ts"], "exclude": ["node_modules", "dist"] } diff --git a/package/tsup.config.ts b/package/tsup.config.ts index 8632acb0..7d5e659d 100644 --- a/package/tsup.config.ts +++ b/package/tsup.config.ts @@ -1,89 +1,51 @@ -import { defineConfig, type Options } from "tsup"; +import { defineConfig } from "tsup"; import * as sass from "sass"; import postcss from "postcss"; import postcssModules from "postcss-modules"; -import * as path from "path"; import * as fs from "fs"; import type { Plugin } from "esbuild"; -// Read version from package.json at build time const pkg = JSON.parse(fs.readFileSync("./package.json", "utf-8")); const VERSION = pkg.version; -// Custom SCSS CSS Modules plugin with SSR-safe style injection -function scssModulesPlugin(): Plugin { +const scssModulesPlugin = (): Plugin => { return { name: "scss-modules", setup(build) { - // Handle all .scss files build.onLoad({ filter: /\.scss$/ }, async (args) => { const isModule = args.path.includes(".module."); - // Use parent directory + filename for unique style IDs - const parentDir = path.basename(path.dirname(args.path)); - const baseName = path.basename(args.path, isModule ? ".module.scss" : ".scss"); - const styleId = `${parentDir}-${baseName}`; + const { css: sassOutput } = sass.compile(args.path); - // Compile SCSS to CSS - const result = sass.compile(args.path); - let css = result.css; - - if (isModule) { - // Process with postcss-modules to get class name mappings - let classNames: Record = {}; - const postcssResult = await postcss([ - postcssModules({ - getJSON(cssFileName, json) { - classNames = json; - }, - generateScopedName: "[name]__[local]___[hash:base64:5]", - }), - ]).process(css, { from: args.path }); - - css = postcssResult.css; - - // Generate JS that exports class names and injects styles (SSR-safe) - const contents = ` -const css = ${JSON.stringify(css)}; -const classNames = ${JSON.stringify(classNames)}; + if (!isModule) { + return { + contents: `export const css = ${JSON.stringify(sassOutput)};`, + loader: "js", + }; + } -// SSR-safe style injection (always update for HMR) -if (typeof document !== 'undefined') { - let style = document.getElementById('feedback-tool-styles-${styleId}'); - if (!style) { - style = document.createElement('style'); - style.id = 'feedback-tool-styles-${styleId}'; - document.head.appendChild(style); - } - style.textContent = css; -} + let classNames: Record = {}; + const { css } = await postcss([ + postcssModules({ + getJSON(_, json) { + classNames = json; + }, + generateScopedName: "[name]__[local]___[hash:base64:5]", + }), + ]).process(sassOutput, { from: args.path }); -export default classNames; -`; - return { contents, loader: "js" }; - } else { - // Regular SCSS - no CSS modules processing - const contents = ` -const css = ${JSON.stringify(css)}; -if (typeof document !== 'undefined') { - let style = document.getElementById('feedback-tool-styles-${styleId}'); - if (!style) { - style = document.createElement('style'); - style.id = 'feedback-tool-styles-${styleId}'; - document.head.appendChild(style); - } - style.textContent = css; -} -export default {}; -`; - return { contents, loader: "js" }; - } + return { + contents: ` + export const css = ${JSON.stringify(css)}; + export default ${JSON.stringify(classNames)}; + `, + loader: "js", + }; }); }, }; -} +}; export default defineConfig((options) => [ - // React component { entry: ["src/index.ts"], format: ["cjs", "esm"], From 636a85b0d1330cceef03321ac7154c50a7f58a2e Mon Sep 17 00:00:00 2001 From: Nicolas Jesenberger Date: Sun, 29 Mar 2026 20:35:56 +0700 Subject: [PATCH 2/4] fix: safari shadow dom transitions --- package/src/components/checkbox/index.tsx | 20 ++++++++++++++--- .../components/checkbox/styles.module.scss | 2 +- .../src/components/page-toolbar-css/index.tsx | 4 ++-- package/src/components/switch/index.tsx | 22 +++++++++++++++---- .../src/components/switch/styles.module.scss | 2 +- 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/package/src/components/checkbox/index.tsx b/package/src/components/checkbox/index.tsx index 7ff334bc..3ce7d025 100644 --- a/package/src/components/checkbox/index.tsx +++ b/package/src/components/checkbox/index.tsx @@ -2,10 +2,24 @@ import styles from "./styles.module.scss"; interface CheckboxProps extends React.InputHTMLAttributes {} -export const Checkbox = ({ className = "", ...props }: CheckboxProps) => { +export const Checkbox = ({ + className = "", + checked, + onChange, + ...props +}: CheckboxProps) => { return ( -
- +
+ + -
+
{/* Toolbar */}
{} -export const Switch = ({ className = "", ...props }: SwitchProps) => { +export const Switch = ({ + className = "", + checked, + onChange, + ...props +}: SwitchProps) => { return ( -
- -
+
+ +
); }; diff --git a/package/src/components/switch/styles.module.scss b/package/src/components/switch/styles.module.scss index d391f19f..e9f5569f 100644 --- a/package/src/components/switch/styles.module.scss +++ b/package/src/components/switch/styles.module.scss @@ -44,7 +44,7 @@ background-color: #fff; transition: transform 0.15s; - .switchContainer:has(.switchInput:checked) & { + .switchContainer[data-checked] & { transform: translateX(8px); } } From 2747d402467023cc6f9724ba63bfa709a3b98ccb Mon Sep 17 00:00:00 2001 From: Nicolas Jesenberger Date: Mon, 30 Mar 2026 12:46:35 +0700 Subject: [PATCH 3/4] fix: shadow dom selectors --- .../src/components/design-mode/rearrange.tsx | 14 ++++++++++---- .../src/components/page-toolbar-css/index.tsx | 19 +++++++++++-------- .../checkbox-field/styles.module.scss | 1 + package/src/utils/use-shadow-root.ts | 15 +++++++++++++++ 4 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 package/src/utils/use-shadow-root.ts diff --git a/package/src/components/design-mode/rearrange.tsx b/package/src/components/design-mode/rearrange.tsx index 1de37b10..54358d5e 100644 --- a/package/src/components/design-mode/rearrange.tsx +++ b/package/src/components/design-mode/rearrange.tsx @@ -6,6 +6,7 @@ import { AnnotationPopupCSS } from "../annotation-popup-css"; import type { DetectedSection, RearrangeState } from "./types"; import styles from "./styles.module.scss"; import { originalSetTimeout } from "../../utils/freeze-animations"; +import { useShadowRoot } from "../../utils/use-shadow-root"; // ============================================================================= // Rearrange Overlay — Click-to-capture, free drag, resize @@ -360,7 +361,7 @@ export function RearrangeOverlay({ rearrangeState, onChange, isDarkMode, exiting lastDy = snappedDy; // Ghost mode: only move outline (ghost preview), not the page element - const outlineEl = document.querySelector(`[data-rearrange-section="${section.id}"]`) as HTMLElement | null; + const outlineEl = shadowRoot.querySelector(`[data-rearrange-section="${section.id}"]`) as HTMLElement | null; if (outlineEl) outlineEl.style.transform = `translate(${snappedDx}px, ${snappedDy}px)`; // Update live drag position for connector lines setDragPositions(new Map([[section.id, { x: startPos.x + snappedDx, y: startPos.y + snappedDy, width: section.currentRect.width, height: section.currentRect.height }]])); @@ -373,7 +374,7 @@ export function RearrangeOverlay({ rearrangeState, onChange, isDarkMode, exiting interactionRef.current = null; setSnapGuides([]); setDragPositions(new Map()); - const outlineEl = document.querySelector(`[data-rearrange-section="${section.id}"]`) as HTMLElement | null; + const outlineEl = shadowRoot.querySelector(`[data-rearrange-section="${section.id}"]`) as HTMLElement | null; if (outlineEl) outlineEl.style.transform = ""; if (moved) { @@ -515,7 +516,7 @@ export function RearrangeOverlay({ rearrangeState, onChange, isDarkMode, exiting }>(); for (const s of sections) { if (newSelected.has(s.id)) { - const outlineEl = document.querySelector(`[data-rearrange-section="${s.id}"]`) as HTMLElement | null; + const outlineEl = shadowRoot.querySelector(`[data-rearrange-section="${s.id}"]`) as HTMLElement | null; dragEls.set(s.id, { outlineEl, curW: s.currentRect.width, curH: s.currentRect.height, @@ -637,7 +638,7 @@ export function RearrangeOverlay({ rearrangeState, onChange, isDarkMode, exiting let lastRect = { ...startRect }; // Cache outline for direct updates — ghost mode, no page element transforms - const resizeOutlineEl = document.querySelector(`[data-rearrange-section="${id}"]`) as HTMLElement | null; + const resizeOutlineEl = shadowRoot.querySelector(`[data-rearrange-section="${id}"]`) as HTMLElement | null; const onMove = (ev: MouseEvent) => { const dx = ev.clientX - startX; @@ -811,9 +812,14 @@ export function RearrangeOverlay({ rearrangeState, onChange, isDarkMode, exiting } }, [changedKey, sections]); + const overlayRef = useRef(null); + + const shadowRoot = useShadowRoot(overlayRef); + return ( <>
diff --git a/package/src/components/page-toolbar-css/index.tsx b/package/src/components/page-toolbar-css/index.tsx index 11a89b77..a34ba064 100644 --- a/package/src/components/page-toolbar-css/index.tsx +++ b/package/src/components/page-toolbar-css/index.tsx @@ -98,6 +98,7 @@ import { css as annotationMarkerCss } from "./annotation-marker/styles.module.sc import { css as checkboxFieldCss } from "./settings-panel/checkbox-field/styles.module.scss"; import { css as settingsPanelCss } from "./settings-panel/styles.module.scss"; import { css as switchCss } from "../switch/styles.module.scss"; +import { useShadowRoot } from "../../utils/use-shadow-root"; const shadowCss = [ resetCss, @@ -1823,7 +1824,7 @@ export function PageFeedbackToolbarCSS({ ].join(", "); const style = document.createElement("style"); - style.id = "feedback-cursor-styles"; + style.id = "agentation-cursor"; // Text elements get text cursor (higher specificity with body prefix) // Everything else gets crosshair style.textContent = ` @@ -1833,7 +1834,7 @@ export function PageFeedbackToolbarCSS({ document.head.appendChild(style); return () => { - const existingStyle = document.getElementById("feedback-cursor-styles"); + const existingStyle = document.getElementById("agentation-cursor"); if (existingStyle) existingStyle.remove(); }; }, [isActive]); @@ -3514,9 +3515,6 @@ export function PageFeedbackToolbarCSS({ pendingMultiSelectElements, ]); - if (!mounted) return null; - if (isToolbarHidden) return null; - const hasAnnotations = annotations.length > 0; // Filter annotations for rendering (exclude exiting ones from normal flow) @@ -3574,6 +3572,11 @@ export function PageFeedbackToolbarCSS({ return styles; }; + const shadowRoot = useShadowRoot(portalWrapperRef); + + if (!mounted) return null; + if (isToolbarHidden) return null; + return createPortal( @@ -3938,7 +3941,7 @@ export function PageFeedbackToolbarCSS({ didDrag = true; preview = document.createElement("div"); preview.className = `${designStyles.dragPreview}${blankCanvas ? ` ${designStyles.dragPreviewWireframe}` : ""}`; - document.body.appendChild(preview); + portalWrapperRef.current?.appendChild(preview); } if (!preview) return; @@ -3966,7 +3969,7 @@ export function PageFeedbackToolbarCSS({ const onUp = (ev: MouseEvent) => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); - if (preview) document.body.removeChild(preview); + if (preview) preview.remove(); if (didDrag) { const w = def.width; @@ -4138,7 +4141,7 @@ export function PageFeedbackToolbarCSS({ isDarkMode={isDarkMode} exiting={designOverlayExiting} blankCanvas={blankCanvas} - extraSnapRects={designPlacements.map(p => ({ x: p.x, y: p.y, width: p.width, height: p.height }))} + extraSnapRects={designPlacements.map(p => ({ x: p.x, y: p.y, width: p.width, height: p.height }))} clearSignal={rearrangeClearSignal} deselectSignal={rearrangeDeselectSignal} onSelectionChange={(ids, isShift) => { diff --git a/package/src/components/page-toolbar-css/settings-panel/checkbox-field/styles.module.scss b/package/src/components/page-toolbar-css/settings-panel/checkbox-field/styles.module.scss index f97bfd8f..407cedbe 100644 --- a/package/src/components/page-toolbar-css/settings-panel/checkbox-field/styles.module.scss +++ b/package/src/components/page-toolbar-css/settings-panel/checkbox-field/styles.module.scss @@ -10,6 +10,7 @@ font-size: 13px; letter-spacing: -0.15px; color: rgb(26 26 26 / 0.5); + -webkit-user-select: none; user-select: none; cursor: pointer; diff --git a/package/src/utils/use-shadow-root.ts b/package/src/utils/use-shadow-root.ts new file mode 100644 index 00000000..c96ba2d7 --- /dev/null +++ b/package/src/utils/use-shadow-root.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from "react"; + +export function useShadowRoot(ref: React.RefObject) { + const [shadowRoot, setShadowRoot] = useState( + () => (typeof document !== "undefined" ? document : null!) + ); + + useEffect(() => { + if (ref.current) { + setShadowRoot((ref.current.getRootNode() as ShadowRoot) ?? document); + } + }, []); + + return shadowRoot; +} \ No newline at end of file From 25b390c3f8709fe210cb533cc38b2664a96bb852 Mon Sep 17 00:00:00 2001 From: Nicolas Jesenberger Date: Mon, 30 Mar 2026 12:49:32 +0700 Subject: [PATCH 4/4] fix: safari user select none --- package/example/public/design-mode.html | 2 +- package/src/components/design-mode/rearrange.tsx | 6 +++++- package/src/components/design-mode/styles.module.scss | 7 +++++++ .../page-toolbar-css/annotation-marker/styles.module.scss | 1 + package/src/components/page-toolbar-css/styles.module.scss | 3 +++ 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/package/example/public/design-mode.html b/package/example/public/design-mode.html index e1d89031..31263550 100644 --- a/package/example/public/design-mode.html +++ b/package/example/public/design-mode.html @@ -68,7 +68,7 @@ .palette-section-title { font-size: 10px; font-weight: 600; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.08em; padding: 0 6px; margin-bottom: 8px; } .palette-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px; - cursor: grab; transition: all 0.15s; border: 1px solid transparent; user-select: none; + cursor: grab; transition: all 0.15s; border: 1px solid transparent; -webkit-user-select: none; user-select: none; } .palette-item:hover { background: var(--surface-2); border-color: var(--border); } .palette-item.selected { background: var(--accent-dim); border-color: var(--accent); } diff --git a/package/src/components/design-mode/rearrange.tsx b/package/src/components/design-mode/rearrange.tsx index 54358d5e..a9be0974 100644 --- a/package/src/components/design-mode/rearrange.tsx +++ b/package/src/components/design-mode/rearrange.tsx @@ -292,8 +292,12 @@ export function RearrangeOverlay({ rearrangeState, onChange, isDarkMode, exiting // --- Prevent text selection while rearrange mode is active --- useEffect(() => { const prev = document.body.style.userSelect; + document.body.style.webkitUserSelect = "none"; document.body.style.userSelect = "none"; - return () => { document.body.style.userSelect = prev; }; + return () => { + document.body.style.webkitUserSelect = prev; + document.body.style.userSelect = prev; + }; }, []); // --- Mousedown to capture new elements (+ immediate drag) --- diff --git a/package/src/components/design-mode/styles.module.scss b/package/src/components/design-mode/styles.module.scss index b55292cf..62a0ec86 100644 --- a/package/src/components/design-mode/styles.module.scss +++ b/package/src/components/design-mode/styles.module.scss @@ -327,6 +327,7 @@ $red: #ef4444; border-color 0.15s, opacity 0.15s ease, transform 0.15s ease; + -webkit-user-select: none; user-select: none; pointer-events: auto; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); @@ -924,6 +925,7 @@ $red: #ef4444; background-color 0.15s ease, border-color 0.15s ease; border: 1px solid transparent; + -webkit-user-select: none; user-select: none; min-height: 24px; @@ -1242,6 +1244,7 @@ $red: #ef4444; z-index: 99995; pointer-events: none; cursor: default; + -webkit-user-select: none; user-select: none; animation: overlayFadeIn 0.15s ease; } @@ -1278,6 +1281,7 @@ $red: #ef4444; border-color 0.3s, background-color 0.3s, border-style 0s; + -webkit-user-select: none; user-select: none; pointer-events: auto; animation: sectionEnter 0.2s ease; @@ -1453,6 +1457,7 @@ $red: #ef4444; color: rgba(0, 0, 0, 0.32); letter-spacing: 0.02em; white-space: nowrap; + -webkit-user-select: none; user-select: none; } @@ -1552,6 +1557,7 @@ $red: #ef4444; background: rgba(59, 130, 246, 0.04); cursor: grab; opacity: 0.5; + -webkit-user-select: none; user-select: none; pointer-events: auto; animation: ghostEnter 0.25s ease; @@ -1648,6 +1654,7 @@ $red: #ef4444; border-radius: 4px; background: transparent; pointer-events: none; + -webkit-user-select: none; user-select: none; animation: sectionEnter 0.2s ease; } diff --git a/package/src/components/page-toolbar-css/annotation-marker/styles.module.scss b/package/src/components/page-toolbar-css/annotation-marker/styles.module.scss index ebdd0f7f..8a49ce4e 100644 --- a/package/src/components/page-toolbar-css/annotation-marker/styles.module.scss +++ b/package/src/components/page-toolbar-css/annotation-marker/styles.module.scss @@ -60,6 +60,7 @@ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(0, 0, 0, 0.04); + -webkit-user-select: none; user-select: none; will-change: transform, opacity; contain: layout style; diff --git a/package/src/components/page-toolbar-css/styles.module.scss b/package/src/components/page-toolbar-css/styles.module.scss index 03546fef..d57226b2 100644 --- a/package/src/components/page-toolbar-css/styles.module.scss +++ b/package/src/components/page-toolbar-css/styles.module.scss @@ -202,6 +202,7 @@ .toolbarContainer { position: relative; + -webkit-user-select: none; user-select: none; margin-left: auto; align-self: flex-end; @@ -310,6 +311,7 @@ position: absolute; top: -13px; right: -13px; + -webkit-user-select: none; user-select: none; min-width: 18px; height: 18px; @@ -917,6 +919,7 @@ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(0, 0, 0, 0.04); + -webkit-user-select: none; user-select: none; will-change: transform, opacity; contain: layout style;