diff options
| -rw-r--r-- | pnpm-lock.yaml | 368 | ||||
| -rw-r--r-- | web/package.json | 2 | ||||
| -rw-r--r-- | web/src/components/app/index.css | 9 | ||||
| -rw-r--r-- | web/src/components/app/index.ts | 3 | ||||
| -rw-r--r-- | web/src/components/grid/cellAtCoord.ts | 40 | ||||
| -rw-r--r-- | web/src/components/grid/drawGrid.ts | 95 | ||||
| -rw-r--r-- | web/src/components/grid/index.css | 1 | ||||
| -rw-r--r-- | web/src/components/grid/index.ts | 36 | ||||
| -rw-r--r-- | web/src/components/grid/renderGrid.ts | 2 | ||||
| -rw-r--r-- | web/src/components/index.ts | 1 | ||||
| -rw-r--r-- | web/src/components/toolbar/index.css | 50 | ||||
| -rw-r--r-- | web/src/components/toolbar/index.ts | 24 | ||||
| -rw-r--r-- | web/src/html.ts | 10 | ||||
| -rw-r--r-- | web/src/index.css | 10 | ||||
| -rw-r--r-- | web/src/index.ts | 38 | ||||
| -rw-r--r-- | web/src/selection.ts | 19 | ||||
| -rw-r--r-- | web/vite.config.ts | 8 |
17 files changed, 683 insertions, 33 deletions
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a01457..ba7893e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,9 +30,15 @@ importers: specifier: ^1.9.1 version: 1.9.1 devDependencies: + '@tailwindcss/vite': + specifier: ^4.1.16 + version: 4.1.16(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)) + tailwindcss: + specifier: ^4.1.16 + version: 4.1.16 vite: specifier: ^7.1.12 - version: 7.1.12 + version: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2) packages: @@ -192,6 +198,22 @@ packages: cpu: [x64] os: [win32] + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -306,6 +328,96 @@ packages: cpu: [x64] os: [win32] + '@tailwindcss/node@4.1.16': + resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==} + + '@tailwindcss/oxide-android-arm64@4.1.16': + resolution: {integrity: sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.16': + resolution: {integrity: sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.16': + resolution: {integrity: sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.16': + resolution: {integrity: sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': + resolution: {integrity: sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': + resolution: {integrity: sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': + resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': + resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.16': + resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.16': + resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': + resolution: {integrity: sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': + resolution: {integrity: sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.16': + resolution: {integrity: sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.16': + resolution: {integrity: sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -319,10 +431,18 @@ packages: resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==} engines: {node: '>=12.20'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-newline@4.0.1: resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + esbuild@0.25.11: resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} engines: {node: '>=18'} @@ -345,10 +465,90 @@ packages: git-hooks-list@4.1.1: resolution: {integrity: sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA==} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -425,6 +625,13 @@ packages: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} + tailwindcss@4.1.16: + resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -588,6 +795,25 @@ snapshots: '@esbuild/win32-x64@0.25.11': optional: true + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@pkgr/core@0.2.9': {} '@rollup/rollup-android-arm-eabi@4.52.5': @@ -656,6 +882,74 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.52.5': optional: true + '@tailwindcss/node@4.1.16': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.16 + + '@tailwindcss/oxide-android-arm64@4.1.16': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.16': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.16': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.16': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.16': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': + optional: true + + '@tailwindcss/oxide@4.1.16': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.16 + '@tailwindcss/oxide-darwin-arm64': 4.1.16 + '@tailwindcss/oxide-darwin-x64': 4.1.16 + '@tailwindcss/oxide-freebsd-x64': 4.1.16 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.16 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.16 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.16 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.16 + '@tailwindcss/oxide-linux-x64-musl': 4.1.16 + '@tailwindcss/oxide-wasm32-wasi': 4.1.16 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.16 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.16 + + '@tailwindcss/vite@4.1.16(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2))': + dependencies: + '@tailwindcss/node': 4.1.16 + '@tailwindcss/oxide': 4.1.16 + tailwindcss: 4.1.16 + vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2) + '@types/estree@1.0.8': {} css-declaration-sorter@7.3.0(postcss@8.5.6): @@ -664,8 +958,15 @@ snapshots: detect-indent@7.0.2: {} + detect-libc@2.1.2: {} + detect-newline@4.0.1: {} + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + esbuild@0.25.11: optionalDependencies: '@esbuild/aix-ppc64': 0.25.11 @@ -704,8 +1005,65 @@ snapshots: git-hooks-list@4.1.1: {} + graceful-fs@4.2.11: {} + is-plain-obj@4.1.0: {} + jiti@2.6.1: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + nanoid@3.3.11: {} open-color@1.9.1: {} @@ -794,6 +1152,10 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 + tailwindcss@4.1.16: {} + + tapable@2.3.0: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -828,7 +1190,7 @@ snapshots: typescript@5.9.3: {} - vite@7.1.12: + vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2): dependencies: esbuild: 0.25.11 fdir: 6.5.0(picomatch@4.0.3) @@ -838,3 +1200,5 @@ snapshots: tinyglobby: 0.2.15 optionalDependencies: fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 diff --git a/web/package.json b/web/package.json index e15cd1e..a565e36 100644 --- a/web/package.json +++ b/web/package.json @@ -8,6 +8,8 @@ "open-color": "^1.9.1" }, "devDependencies": { + "@tailwindcss/vite": "^4.1.16", + "tailwindcss": "^4.1.16", "vite": "^7.1.12" } } diff --git a/web/src/components/app/index.css b/web/src/components/app/index.css index 3eeaee9..aaf2ced 100644 --- a/web/src/components/app/index.css +++ b/web/src/components/app/index.css @@ -1,3 +1,12 @@ ntv-app { display: block; + padding: 1.5rem; +} + +ntv-app > ntv-toolbar { + margin-bottom: 1.5rem; +} + +ntv-app > ntv-grid + ntv-grid { + margin-top: 1.5rem; } diff --git a/web/src/components/app/index.ts b/web/src/components/app/index.ts index 2782e22..910aa52 100644 --- a/web/src/components/app/index.ts +++ b/web/src/components/app/index.ts @@ -1,9 +1,12 @@ +import h from "../../html"; import ntvGrid from "../grid"; +import ntvToolbar from "../toolbar"; import "./index.css"; class NotiveAppElement extends HTMLElement { connectedCallback() { this.append( + ntvToolbar(), ...window.notive.doc.grids.map((grid) => { return ntvGrid({ gridId: grid.id }); }), diff --git a/web/src/components/grid/cellAtCoord.ts b/web/src/components/grid/cellAtCoord.ts new file mode 100644 index 0000000..dd594a4 --- /dev/null +++ b/web/src/components/grid/cellAtCoord.ts @@ -0,0 +1,40 @@ +import Coord from "../../math/Coord"; +import { CellRef } from "../../types"; +import { RenderedGrid, RenderedRow } from "./renderGrid"; + +function rowAtCoord(grid: RenderedGrid, coord: Coord): RenderedRow | undefined { + if (coord.y <= grid.rect.topLeft.y) { + return grid.renderedRows[0]; + } + + if (coord.y >= grid.rect.bottomRight.y) { + return grid.renderedRows.at(-1); + } + + return grid.renderedRows.find((row) => + row.rect.verticallyContainsCoord(coord), + ); +} + +export default function cellAtCoord( + grid: RenderedGrid, + x: number, + y: number, +): CellRef | undefined { + const coord = new Coord(x, y); + const row = rowAtCoord(grid, coord); + + if (!row) return; + + if (x <= row.rect.topLeft.x) { + return row.renderedCells[0]?.cellRef; + } + + if (x >= row.rect.bottomRight.x) { + return row.renderedCells.at(-1)?.cellRef; + } + + return row.renderedCells.find((cell) => + cell.rect.horizontallyContainsCoord(coord), + )?.cellRef; +} diff --git a/web/src/components/grid/drawGrid.ts b/web/src/components/grid/drawGrid.ts index 6284693..01240b5 100644 --- a/web/src/components/grid/drawGrid.ts +++ b/web/src/components/grid/drawGrid.ts @@ -1,16 +1,21 @@ -import { RenderedGrid } from "./renderGrid"; -import colors from "open-color"; +import colors from "tailwindcss/colors"; +import { PendingSelection, Selection } from "../../selection"; +import { CellRef } from "../../types"; +import { RenderedCell, RenderedGrid } from "./renderGrid"; -export default function drawGrid( - ctx: CanvasRenderingContext2D, - grid: RenderedGrid, -) { +function fillBackground(ctx: CanvasRenderingContext2D, grid: RenderedGrid) { ctx.clearRect(0, 0, grid.rect.width, grid.rect.height); - - ctx.fillStyle = colors.gray[8]; + ctx.fillStyle = colors.neutral[800]; ctx.fillRect(0, 0, grid.rect.width, grid.rect.height); +} + +function strokeGrid(ctx: CanvasRenderingContext2D, grid: RenderedGrid) { + ctx.strokeStyle = colors.neutral[700]; + ctx.strokeRect(0.5, 0.5, grid.rect.width - 1, grid.rect.height - 1); +} - ctx.strokeStyle = colors.gray[7]; +function strokeGridLines(ctx: CanvasRenderingContext2D, grid: RenderedGrid) { + ctx.strokeStyle = colors.neutral[700]; grid.renderedRows.forEach((row, renderedRowIndex) => { const isLastRow = renderedRowIndex === grid.renderedRows.length - 1; @@ -28,3 +33,75 @@ export default function drawGrid( }); }); } + +function getRenderedCell( + grid: RenderedGrid, + cellRef: CellRef, +): RenderedCell | undefined { + const rowsPerPart = grid.renderedRows.length / grid.parts.length; + const renderedRowIndex = cellRef.partIndex * rowsPerPart + cellRef.rowIndex; + return grid.renderedRows[renderedRowIndex]?.renderedCells[cellRef.cellIndex]; +} + +function drawPendingSelection( + ctx: CanvasRenderingContext2D, + grid: RenderedGrid, + selection: PendingSelection, +) {} + +function drawSelection( + ctx: CanvasRenderingContext2D, + grid: RenderedGrid, + selection: Selection, +) { + if (selection.gridId !== grid.id) return; + + const cell = getRenderedCell(grid, selection.activeCellRef); + + if (!cell) return; + + const isLastCell = cell.rect.bottomRight.x === grid.rect.bottomRight.x; + const isLastRow = cell.rect.bottomRight.y === grid.rect.bottomRight.y; + + // ctx.fillStyle = colors.green[4] + "30"; + + // ctx.fillRect( + // cell.rect.topLeft.x + 1, + // cell.rect.topLeft.y + 1, + // cell.rect.width - 1, + // cell.rect.height - 1, + // ); + + ctx.strokeStyle = colors.green[600]; + ctx.lineWidth = 2; + + ctx.strokeRect( + cell.rect.topLeft.x + 1, + cell.rect.topLeft.y + 1, + isLastCell ? cell.rect.width - 2 : cell.rect.width - 1, + isLastRow ? cell.rect.height - 2 : cell.rect.height - 1, + ); +} + +export default function drawGrid( + ctx: CanvasRenderingContext2D, + grid: RenderedGrid, + selection?: Selection, + pendingSelection?: PendingSelection, +) { + const excursion = (f: () => void) => { + ctx.save(); + f(); + ctx.restore(); + }; + + excursion(() => fillBackground(ctx, grid)); + excursion(() => strokeGridLines(ctx, grid)); + excursion(() => strokeGrid(ctx, grid)); + + if (pendingSelection) { + excursion(() => drawPendingSelection(ctx, grid, pendingSelection)); + } else if (selection) { + excursion(() => drawSelection(ctx, grid, selection)); + } +} diff --git a/web/src/components/grid/index.css b/web/src/components/grid/index.css index 0fad720..a733015 100644 --- a/web/src/components/grid/index.css +++ b/web/src/components/grid/index.css @@ -1,6 +1,5 @@ ntv-grid { display: block; - padding: 1.5rem; } ntv-grid > canvas { diff --git a/web/src/components/grid/index.ts b/web/src/components/grid/index.ts index 829a511..0acace4 100644 --- a/web/src/components/grid/index.ts +++ b/web/src/components/grid/index.ts @@ -1,5 +1,5 @@ import h, { type CreateElement } from "../../html"; -import renderGrid from "./renderGrid"; +import cellAtCoord from "./cellAtCoord"; import drawGrid from "./drawGrid"; import "./index.css"; @@ -15,6 +15,10 @@ class NotiveGridElement extends HTMLElement { this.setAttribute("grid-id", val); } + get renderedGrid() { + return window.notive.getGrid(this.#gridId)!; + } + canvasEl: HTMLCanvasElement = h.canvas(); connectedCallback() { @@ -22,19 +26,41 @@ class NotiveGridElement extends HTMLElement { throw new Error("ntv-grid requries gridId attribute"); } + this.canvasEl.addEventListener("mousedown", (event) => { + const clientRect = this.canvasEl.getBoundingClientRect(); + const x = event.x - clientRect.x; + const y = event.y - clientRect.y; + const cellRef = cellAtCoord(this.renderedGrid, x, y); + if (!cellRef) return; + window.notive.selectCell(this.#gridId, cellRef); + }); + + window.addEventListener("ntv:selection-changed", () => { + this.draw(); + }); + this.append(this.canvasEl); this.draw(); } draw() { const ctx = this.canvasEl.getContext("2d"); + if (!ctx) throw new Error("Unable to get canvas context"); + const grid = window.notive.getGrid(this.gridId); + if (!grid) return; - const renderedGrid = renderGrid(grid); - this.canvasEl.setAttribute("width", renderedGrid.rect.width + "px"); - this.canvasEl.setAttribute("height", renderedGrid.rect.height + "px"); - drawGrid(ctx, renderedGrid); + + this.canvasEl.setAttribute("width", grid.rect.width + "px"); + this.canvasEl.setAttribute("height", grid.rect.height + "px"); + + drawGrid( + ctx, + grid, + window.notive.selection, + window.notive.pendingSelection, + ); } } diff --git a/web/src/components/grid/renderGrid.ts b/web/src/components/grid/renderGrid.ts index 5666f66..7ef8813 100644 --- a/web/src/components/grid/renderGrid.ts +++ b/web/src/components/grid/renderGrid.ts @@ -1,6 +1,6 @@ import Ratio from "../../math/Ratio"; import Rect from "../../math/Rect"; -import { Cell, CellRef, Grid, Row, RowRef } from "./types"; +import { Cell, CellRef, Grid, Row, RowRef } from "../../types"; export interface RenderedCell extends Cell { cellRef: CellRef; diff --git a/web/src/components/index.ts b/web/src/components/index.ts index 8bc14e7..b7f6f55 100644 --- a/web/src/components/index.ts +++ b/web/src/components/index.ts @@ -1,2 +1,3 @@ import "./app"; import "./grid"; +import "./toolbar"; diff --git a/web/src/components/toolbar/index.css b/web/src/components/toolbar/index.css new file mode 100644 index 0000000..3f78671 --- /dev/null +++ b/web/src/components/toolbar/index.css @@ -0,0 +1,50 @@ +ntv-toolbar { + display: flex; + border-radius: 4px; + background: var(--color-neutral-800); + width: min-content; +} + +ntv-toolbar > section { + display: flex; + gap: 0.5rem; + padding: 0.5rem; +} + +ntv-toolbar button[data-variant="menu"] { + border-radius: 4px; + background: var(--color-neutral-700); + padding: 0 0.625rem; + height: 1.5rem; + color: white; + font-size: 0.75rem; +} + +ntv-toolbar button[data-variant="icon"] { + display: flex; + justify-content: center; + align-items: center; + border-radius: 4px; + background: var(--color-neutral-700); + padding: 0.125rem 0.625rem; + aspect-ratio: 1; + height: 1.5rem; + color: white; + font-weight: 600; + font-size: 0.75rem; +} + +ntv-toolbar button:hover { + background: var(--color-neutral-600); +} + +ntv-toolbar input { + border: 1px solid var(--color-neutral-700); + border-radius: 4px; + background: var(--color-neutral-900); + width: 2.5rem; + height: 1.5rem; + color: white; + font-size: 0.75rem; + text-align: center; +} diff --git a/web/src/components/toolbar/index.ts b/web/src/components/toolbar/index.ts new file mode 100644 index 0000000..d844a69 --- /dev/null +++ b/web/src/components/toolbar/index.ts @@ -0,0 +1,24 @@ +import h, { CreateElement } from "../../html"; +import "./index.css"; + +class NotiveToolbarElement extends HTMLElement { + connectedCallback() { + this.append( + h.section( + h.button({ dataset: { variant: "menu" } }, "File"), + h.button({ dataset: { variant: "menu" } }, "Edit"), + h.button({ dataset: { variant: "menu" } }, "Format"), + ), + h.section( + h.button({ dataset: { variant: "icon" } }, "-"), + h.input({ type: "text", value: "1" }), + h.button({ dataset: { variant: "icon" } }, "+"), + ), + ); + } +} + +customElements.define("ntv-toolbar", NotiveToolbarElement); + +export default ((...args: any[]): NotiveToolbarElement => + (h as any)["ntv-toolbar"](...args)) as CreateElement<NotiveToolbarElement>; diff --git a/web/src/html.ts b/web/src/html.ts index 5bfff21..8802e50 100644 --- a/web/src/html.ts +++ b/web/src/html.ts @@ -13,8 +13,14 @@ const h = new Proxy({} as ElementCreator, { (...args: any[]) => { const el = document.createElement(tag); - if (typeof args[0] === "object") { - Object.assign(el, args.shift()); + if (args[0]?.constructor === Object) { + const { dataset, ...attrs } = args.shift(); + + Object.assign(el, attrs); + + if (dataset) { + Object.assign(el.dataset, dataset); + } } el.append(...args.flat()); diff --git a/web/src/index.css b/web/src/index.css index ba2a6a7..4fe2764 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,11 +1,5 @@ -@import "open-color"; +@import "tailwindcss"; body { - margin: 0; - background: var(--oc-gray-9); - color: var(--oc-white); - font-weight: normal; - font-family: - Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, - sans-serif; + background: var(--color-neutral-900); } diff --git a/web/src/index.ts b/web/src/index.ts index fbbf37c..ac4870c 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -1,5 +1,7 @@ import Ratio from "./math/Ratio"; -import { Cell, Doc, Grid } from "./types"; +import { Cell, CellRef, Doc, Grid } from "./types"; +import { ActiveCellSelection, PendingSelection, Selection } from "./selection"; +import renderGrid, { RenderedGrid } from "./components/grid/renderGrid"; function defaultDoc(): Doc { const defaultCells: Cell[] = Array.from({ length: 16 }, () => ({ @@ -20,17 +22,47 @@ function defaultDoc(): Doc { }, ], }, + { + id: window.crypto.randomUUID(), + baseCellSize: 48, + baseCellWidthRatio: new Ratio(1, 16), + parts: [ + { + rows: Array.from({ length: 4 }, () => ({ + cells: [...defaultCells], + })), + }, + ], + }, ], }; } export default class Notive { doc: Doc = defaultDoc(); - gridsById = Object.fromEntries(this.doc.grids.map((grid) => [grid.id, grid])); - getGrid(id: string): Grid | undefined { + gridsById = Object.fromEntries( + this.doc.grids.map((grid) => [grid.id, renderGrid(grid)]), + ); + + selection?: Selection; + + pendingSelection?: Selection; + + getGrid(id: string): RenderedGrid | undefined { return this.gridsById[id]; } + + selectCell(gridId: string, cellRef: CellRef) { + const previousSelection = this.selection; + this.selection = new ActiveCellSelection(gridId, cellRef); + + window.dispatchEvent( + new CustomEvent("ntv:selection-changed", { + detail: { selection: this.selection, previousSelection }, + }), + ); + } } window.notive = new Notive(); diff --git a/web/src/selection.ts b/web/src/selection.ts new file mode 100644 index 0000000..88d394b --- /dev/null +++ b/web/src/selection.ts @@ -0,0 +1,19 @@ +import { CellRef } from "./types"; + +export abstract class Selection { + readonly gridId: string; + readonly activeCellRef: CellRef; + + constructor(gridId: string, activeCellRef: CellRef) { + this.gridId = gridId; + this.activeCellRef = activeCellRef; + } +} + +export class ActiveCellSelection extends Selection {} + +export class RangeSelection extends Selection {} + +export class AllSelection extends Selection {} + +export class PendingSelection extends Selection {} diff --git a/web/vite.config.ts b/web/vite.config.ts index d59c396..21088ad 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,3 +1,7 @@ -export default { +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ root: "src", -}; + plugins: [tailwindcss()], +}); |
