summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosh Kingsley <josh@joshkingsley.me>2025-10-26 14:28:55 +0200
committerJosh Kingsley <josh@joshkingsley.me>2025-10-26 14:28:55 +0200
commit0324660a26684a5382b2c6c18cd0a4e9f0169631 (patch)
tree64c16e8a4a4815f050f7e06a3b9486a668f2b4d4
parent1b8d05bf83d7bd9ab425852f519ea81bdc379444 (diff)
feat(web): add dummy toolbar + tailwindcss colors
-rw-r--r--pnpm-lock.yaml368
-rw-r--r--web/package.json2
-rw-r--r--web/src/components/app/index.css9
-rw-r--r--web/src/components/app/index.ts3
-rw-r--r--web/src/components/grid/cellAtCoord.ts40
-rw-r--r--web/src/components/grid/drawGrid.ts95
-rw-r--r--web/src/components/grid/index.css1
-rw-r--r--web/src/components/grid/index.ts36
-rw-r--r--web/src/components/grid/renderGrid.ts2
-rw-r--r--web/src/components/index.ts1
-rw-r--r--web/src/components/toolbar/index.css50
-rw-r--r--web/src/components/toolbar/index.ts24
-rw-r--r--web/src/html.ts10
-rw-r--r--web/src/index.css10
-rw-r--r--web/src/index.ts38
-rw-r--r--web/src/selection.ts19
-rw-r--r--web/vite.config.ts8
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()],
+});