summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosh Kingsley <josh@joshkingsley.me>2025-11-06 22:31:12 +0200
committerJosh Kingsley <josh@joshkingsley.me>2025-11-06 22:31:12 +0200
commit6ea86b1b56aebbae7edeb37b01d7bf5cd145bf60 (patch)
treebfd0178097a56087471543950caebed5f8280040
parentdad5b47f0bb480532043df8d488f5609f731b00d (diff)
feat(web): change subvisions
-rw-r--r--package.json2
-rw-r--r--pnpm-lock.yaml319
-rw-r--r--turbo.json4
-rw-r--r--web/package.json13
-rw-r--r--web/src/components/app/index.ts47
-rw-r--r--web/src/components/grid/index.ts11
-rw-r--r--web/src/components/grid/selection.ts5
-rw-r--r--web/src/components/toolbar/index.css2
-rw-r--r--web/src/components/toolbar/index.ts62
-rw-r--r--web/src/defaultDoc.ts4
-rw-r--r--web/src/grid.test.ts45
-rw-r--r--web/src/grid.ts109
-rw-r--r--web/src/math/Ratio.test.ts27
-rw-r--r--web/src/math/Ratio.ts28
-rw-r--r--web/src/math/index.ts3
-rw-r--r--web/src/types.ts50
16 files changed, 639 insertions, 92 deletions
diff --git a/package.json b/package.json
index d5a1cb5..3097c19 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"prettier": "^3.6.2",
"prettier-plugin-css-order": "^2.1.2",
"prettier-plugin-packagejson": "^2.5.19",
- "turbo": "^2.5.8",
+ "turbo": "^2.6.0",
"typescript": "^5.9.3"
},
"packageManager": "pnpm@10.18.0"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1db75b4..c86f65a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -18,14 +18,17 @@ importers:
specifier: ^2.5.19
version: 2.5.19(prettier@3.6.2)
turbo:
- specifier: ^2.5.8
- version: 2.5.8
+ specifier: ^2.6.0
+ version: 2.6.0
typescript:
specifier: ^5.9.3
version: 5.9.3
web:
dependencies:
+ immer:
+ specifier: ^10.2.0
+ version: 10.2.0
tailwindcss:
specifier: ^4.1.16
version: 4.1.16
@@ -39,6 +42,9 @@ importers:
vite:
specifier: ^7.1.12
version: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)
+ vitest:
+ specifier: ^4.0.6
+ version: 4.0.6(jiti@2.6.1)(lightningcss@1.30.2)
packages:
@@ -332,6 +338,9 @@ packages:
cpu: [x64]
os: [win32]
+ '@standard-schema/spec@1.0.0':
+ resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
+
'@tailwindcss/node@4.1.16':
resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==}
@@ -422,19 +431,71 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6 || ^7
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+ '@vitest/expect@4.0.6':
+ resolution: {integrity: sha512-5j8UUlBVhOjhj4lR2Nt9sEV8b4WtbcYh8vnfhTNA2Kn5+smtevzjNq+xlBuVhnFGXiyPPNzGrOVvmyHWkS5QGg==}
+
+ '@vitest/mocker@4.0.6':
+ resolution: {integrity: sha512-3COEIew5HqdzBFEYN9+u0dT3i/NCwppLnO1HkjGfAP1Vs3vti1Hxm/MvcbC4DAn3Szo1M7M3otiAaT83jvqIjA==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^6.0.0 || ^7.0.0-0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@4.0.6':
+ resolution: {integrity: sha512-4vptgNkLIA1W1Nn5X4x8rLJBzPiJwnPc+awKtfBE5hNMVsoAl/JCCPPzNrbf+L4NKgklsis5Yp2gYa+XAS442g==}
+
+ '@vitest/runner@4.0.6':
+ resolution: {integrity: sha512-trPk5qpd7Jj+AiLZbV/e+KiiaGXZ8ECsRxtnPnCrJr9OW2mLB72Cb824IXgxVz/mVU3Aj4VebY+tDTPn++j1Og==}
+
+ '@vitest/snapshot@4.0.6':
+ resolution: {integrity: sha512-PaYLt7n2YzuvxhulDDu6c9EosiRuIE+FI2ECKs6yvHyhoga+2TBWI8dwBjs+IeuQaMtZTfioa9tj3uZb7nev1g==}
+
+ '@vitest/spy@4.0.6':
+ resolution: {integrity: sha512-g9jTUYPV1LtRPRCQfhbMintW7BTQz1n6WXYQYRQ25qkyffA4bjVXjkROokZnv7t07OqfaFKw1lPzqKGk1hmNuQ==}
+
+ '@vitest/utils@4.0.6':
+ resolution: {integrity: sha512-bG43VS3iYKrMIZXBo+y8Pti0O7uNju3KvNn6DrQWhQQKcLavMB+0NZfO1/QBAEbq0MaQ3QjNsnnXlGQvsh0Z6A==}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
automation-events@7.1.13:
resolution: {integrity: sha512-1Hay5TQPzxsskSqPTH3YXyzE9Iirz82zZDse2vr3+kOR7Sc7om17qIEPsESchlNX0EgKxANwR40i2g/O3GM1Tw==}
engines: {node: '>=18.2.0'}
+ chai@6.2.0:
+ resolution: {integrity: sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==}
+ engines: {node: '>=18'}
+
css-declaration-sorter@7.3.0:
resolution: {integrity: sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==}
engines: {node: ^14 || ^16 || >=18}
peerDependencies:
postcss: ^8.0.9
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
detect-indent@7.0.2:
resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==}
engines: {node: '>=12.20'}
@@ -451,11 +512,21 @@ packages:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
+ es-module-lexer@1.7.0:
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+
esbuild@0.25.11:
resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==}
engines: {node: '>=18'}
hasBin: true
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ expect-type@1.2.2:
+ resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
+ engines: {node: '>=12.0.0'}
+
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -476,6 +547,9 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+ immer@10.2.0:
+ resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
+
is-plain-obj@4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
@@ -557,11 +631,17 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -614,6 +694,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
sort-object-keys@1.1.3:
resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==}
@@ -626,9 +709,15 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
standardized-audio-context@25.3.77:
resolution: {integrity: sha512-Ki9zNz6pKcC5Pi+QPjPyVsD9GwJIJWgryji0XL9cAJXMGyn+dPOf6Qik1AHei0+UNVcc4BOCa0hWLBzlwqsW/A==}
+ std-env@3.10.0:
+ resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+
synckit@0.11.11:
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -640,48 +729,58 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@0.3.2:
+ resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
+
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
+ tinyrainbow@3.0.3:
+ resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
+ engines: {node: '>=14.0.0'}
+
tone@15.1.22:
resolution: {integrity: sha512-TCScAGD4sLsama5DjvTUXlLDXSqPealhL64nsdV1hhr6frPWve0DeSo63AKnSJwgfg55fhvxj0iPPRwPN5o0ag==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
- turbo-darwin-64@2.5.8:
- resolution: {integrity: sha512-Dh5bCACiHO8rUXZLpKw+m3FiHtAp2CkanSyJre+SInEvEr5kIxjGvCK/8MFX8SFRjQuhjtvpIvYYZJB4AGCxNQ==}
+ turbo-darwin-64@2.6.0:
+ resolution: {integrity: sha512-6vHnLAubHj8Ib45Knu+oY0ZVCLO7WcibzAvt5b1E72YHqAs4y8meMAGMZM0jLqWPh/9maHDc16/qBCMxtW4pXg==}
cpu: [x64]
os: [darwin]
- turbo-darwin-arm64@2.5.8:
- resolution: {integrity: sha512-f1H/tQC9px7+hmXn6Kx/w8Jd/FneIUnvLlcI/7RGHunxfOkKJKvsoiNzySkoHQ8uq1pJnhJ0xNGTlYM48ZaJOQ==}
+ turbo-darwin-arm64@2.6.0:
+ resolution: {integrity: sha512-IU+gWMEXNBw8H0pxvE7nPEa5p6yahxbN8g/Q4Bf0AHymsAFqsScgV0peeNbWybdmY9jk1LPbALOsF2kY1I7ZiQ==}
cpu: [arm64]
os: [darwin]
- turbo-linux-64@2.5.8:
- resolution: {integrity: sha512-hMyvc7w7yadBlZBGl/bnR6O+dJTx3XkTeyTTH4zEjERO6ChEs0SrN8jTFj1lueNXKIHh1SnALmy6VctKMGnWfw==}
+ turbo-linux-64@2.6.0:
+ resolution: {integrity: sha512-CKoiJ2ZFJLCDsWdRlZg+ew1BkGn8iCEGdePhISVpjsGwkJwSVhVu49z2zKdBeL1IhcSKS2YALwp9ellNZANJxw==}
cpu: [x64]
os: [linux]
- turbo-linux-arm64@2.5.8:
- resolution: {integrity: sha512-LQELGa7bAqV2f+3rTMRPnj5G/OHAe2U+0N9BwsZvfMvHSUbsQ3bBMWdSQaYNicok7wOZcHjz2TkESn1hYK6xIQ==}
+ turbo-linux-arm64@2.6.0:
+ resolution: {integrity: sha512-WroVCdCvJbrhNxNdw7XB7wHAfPPJPV+IXY+ZKNed+9VdfBu/2mQNfKnvqTuFTH7n+Pdpv8to9qwhXRTJe26upg==}
cpu: [arm64]
os: [linux]
- turbo-windows-64@2.5.8:
- resolution: {integrity: sha512-3YdcaW34TrN1AWwqgYL9gUqmZsMT4T7g8Y5Azz+uwwEJW+4sgcJkIi9pYFyU4ZBSjBvkfuPZkGgfStir5BBDJQ==}
+ turbo-windows-64@2.6.0:
+ resolution: {integrity: sha512-7pZo5aGQPR+A7RMtWCZHusarJ6y15LQ+o3jOmpMxTic/W6Bad+jSeqo07TWNIseIWjCVzrSv27+0odiYRYtQdA==}
cpu: [x64]
os: [win32]
- turbo-windows-arm64@2.5.8:
- resolution: {integrity: sha512-eFC5XzLmgXJfnAK3UMTmVECCwuBcORrWdewoiXBnUm934DY6QN8YowC/srhNnROMpaKaqNeRpoB5FxCww3eteQ==}
+ turbo-windows-arm64@2.6.0:
+ resolution: {integrity: sha512-1Ty+NwIksQY7AtFUCPrTpcKQE7zmd/f7aRjdT+qkqGFQjIjFYctEtN7qo4vpQPBgCfS1U3ka83A2u/9CfJQ3wQ==}
cpu: [arm64]
os: [win32]
- turbo@2.5.8:
- resolution: {integrity: sha512-5c9Fdsr9qfpT3hA0EyYSFRZj1dVVsb6KIWubA9JBYZ/9ZEAijgUEae0BBR/Xl/wekt4w65/lYLTFaP3JmwSO8w==}
+ turbo@2.6.0:
+ resolution: {integrity: sha512-kC5VJqOXo50k0/0jnJDDjibLAXalqT9j7PQ56so0pN+81VR4Fwb2QgIE9dTzT3phqOTQuEXkPh3sCpnv5Isz2g==}
hasBin: true
typescript@5.9.3:
@@ -729,6 +828,45 @@ packages:
yaml:
optional: true
+ vitest@4.0.6:
+ resolution: {integrity: sha512-gR7INfiVRwnEOkCk47faros/9McCZMp5LM+OMNWGLaDBSvJxIzwjgNFufkuePBNaesGRnLmNfW+ddbUJRZn0nQ==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@types/debug': ^4.1.12
+ '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 4.0.6
+ '@vitest/browser-preview': 4.0.6
+ '@vitest/browser-webdriverio': 4.0.6
+ '@vitest/ui': 4.0.6
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@types/debug':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
snapshots:
'@babel/runtime@7.28.4': {}
@@ -898,6 +1036,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.52.5':
optional: true
+ '@standard-schema/spec@1.0.0': {}
+
'@tailwindcss/node@4.1.16':
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -966,17 +1106,71 @@ snapshots:
tailwindcss: 4.1.16
vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
+ '@types/deep-eql@4.0.2': {}
+
'@types/estree@1.0.8': {}
+ '@vitest/expect@4.0.6':
+ dependencies:
+ '@standard-schema/spec': 1.0.0
+ '@types/chai': 5.2.3
+ '@vitest/spy': 4.0.6
+ '@vitest/utils': 4.0.6
+ chai: 6.2.0
+ tinyrainbow: 3.0.3
+
+ '@vitest/mocker@4.0.6(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2))':
+ dependencies:
+ '@vitest/spy': 4.0.6
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)
+
+ '@vitest/pretty-format@4.0.6':
+ dependencies:
+ tinyrainbow: 3.0.3
+
+ '@vitest/runner@4.0.6':
+ dependencies:
+ '@vitest/utils': 4.0.6
+ pathe: 2.0.3
+
+ '@vitest/snapshot@4.0.6':
+ dependencies:
+ '@vitest/pretty-format': 4.0.6
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@4.0.6': {}
+
+ '@vitest/utils@4.0.6':
+ dependencies:
+ '@vitest/pretty-format': 4.0.6
+ tinyrainbow: 3.0.3
+
+ assertion-error@2.0.1: {}
+
automation-events@7.1.13:
dependencies:
'@babel/runtime': 7.28.4
tslib: 2.8.1
+ chai@6.2.0: {}
+
css-declaration-sorter@7.3.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
detect-indent@7.0.2: {}
detect-libc@2.1.2: {}
@@ -988,6 +1182,8 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
+ es-module-lexer@1.7.0: {}
+
esbuild@0.25.11:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.11
@@ -1017,6 +1213,12 @@ snapshots:
'@esbuild/win32-ia32': 0.25.11
'@esbuild/win32-x64': 0.25.11
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
+ expect-type@1.2.2: {}
+
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
@@ -1028,6 +1230,8 @@ snapshots:
graceful-fs@4.2.11: {}
+ immer@10.2.0: {}
+
is-plain-obj@4.1.0: {}
jiti@2.6.1: {}
@@ -1085,8 +1289,12 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
+ ms@2.1.3: {}
+
nanoid@3.3.11: {}
+ pathe@2.0.3: {}
+
picocolors@1.1.1: {}
picomatch@4.0.3: {}
@@ -1153,6 +1361,8 @@ snapshots:
semver@7.7.3: {}
+ siginfo@2.0.0: {}
+
sort-object-keys@1.1.3: {}
sort-package-json@3.4.0:
@@ -1167,12 +1377,16 @@ snapshots:
source-map-js@1.2.1: {}
+ stackback@0.0.2: {}
+
standardized-audio-context@25.3.77:
dependencies:
'@babel/runtime': 7.28.4
automation-events: 7.1.13
tslib: 2.8.1
+ std-env@3.10.0: {}
+
synckit@0.11.11:
dependencies:
'@pkgr/core': 0.2.9
@@ -1181,11 +1395,17 @@ snapshots:
tapable@2.3.0: {}
+ tinybench@2.9.0: {}
+
+ tinyexec@0.3.2: {}
+
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
+ tinyrainbow@3.0.3: {}
+
tone@15.1.22:
dependencies:
standardized-audio-context: 25.3.77
@@ -1193,32 +1413,32 @@ snapshots:
tslib@2.8.1: {}
- turbo-darwin-64@2.5.8:
+ turbo-darwin-64@2.6.0:
optional: true
- turbo-darwin-arm64@2.5.8:
+ turbo-darwin-arm64@2.6.0:
optional: true
- turbo-linux-64@2.5.8:
+ turbo-linux-64@2.6.0:
optional: true
- turbo-linux-arm64@2.5.8:
+ turbo-linux-arm64@2.6.0:
optional: true
- turbo-windows-64@2.5.8:
+ turbo-windows-64@2.6.0:
optional: true
- turbo-windows-arm64@2.5.8:
+ turbo-windows-arm64@2.6.0:
optional: true
- turbo@2.5.8:
+ turbo@2.6.0:
optionalDependencies:
- turbo-darwin-64: 2.5.8
- turbo-darwin-arm64: 2.5.8
- turbo-linux-64: 2.5.8
- turbo-linux-arm64: 2.5.8
- turbo-windows-64: 2.5.8
- turbo-windows-arm64: 2.5.8
+ turbo-darwin-64: 2.6.0
+ turbo-darwin-arm64: 2.6.0
+ turbo-linux-64: 2.6.0
+ turbo-linux-arm64: 2.6.0
+ turbo-windows-64: 2.6.0
+ turbo-windows-arm64: 2.6.0
typescript@5.9.3: {}
@@ -1234,3 +1454,44 @@ snapshots:
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.30.2
+
+ vitest@4.0.6(jiti@2.6.1)(lightningcss@1.30.2):
+ dependencies:
+ '@vitest/expect': 4.0.6
+ '@vitest/mocker': 4.0.6(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2))
+ '@vitest/pretty-format': 4.0.6
+ '@vitest/runner': 4.0.6
+ '@vitest/snapshot': 4.0.6
+ '@vitest/spy': 4.0.6
+ '@vitest/utils': 4.0.6
+ debug: 4.4.3
+ es-module-lexer: 1.7.0
+ expect-type: 1.2.2
+ magic-string: 0.30.21
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 3.10.0
+ tinybench: 2.9.0
+ tinyexec: 0.3.2
+ tinyglobby: 0.2.15
+ tinyrainbow: 3.0.3
+ vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)
+ why-is-node-running: 2.3.0
+ transitivePeerDependencies:
+ - jiti
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - tsx
+ - yaml
+
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
diff --git a/turbo.json b/turbo.json
index b2299dd..48b01b8 100644
--- a/turbo.json
+++ b/turbo.json
@@ -4,6 +4,10 @@
"dev": {
"cache": false,
"persistent": true
+ },
+ "test": {
+ "cache": false,
+ "persistent": true
}
}
}
diff --git a/web/package.json b/web/package.json
index 282a138..e06cb6c 100644
--- a/web/package.json
+++ b/web/package.json
@@ -2,14 +2,17 @@
"name": "@notive/web",
"private": true,
"scripts": {
- "dev": "vite --clearScreen false"
- },
- "devDependencies": {
- "@tailwindcss/vite": "^4.1.16",
- "vite": "^7.1.12"
+ "dev": "vite --clearScreen false",
+ "test": "vitest"
},
"dependencies": {
+ "immer": "^10.2.0",
"tailwindcss": "^4.1.16",
"tone": "^15.1.22"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.1.16",
+ "vite": "^7.1.12",
+ "vitest": "^4.0.6"
}
}
diff --git a/web/src/components/app/index.ts b/web/src/components/app/index.ts
index aa7c738..a2c0c9d 100644
--- a/web/src/components/app/index.ts
+++ b/web/src/components/app/index.ts
@@ -1,5 +1,10 @@
+import { produce } from "immer";
import defaultDoc from "../../defaultDoc";
import NotiveElement, { customElement } from "../../element";
+import {
+ changeSelectedSubdivisions,
+ getSelectedSubdivisionsCount,
+} from "../../grid";
import { Doc } from "../../types";
import ntvGrid, { NotiveGridElement } from "../grid";
import renderGrid from "../grid/renderGrid";
@@ -15,9 +20,21 @@ export class NotiveAppElement extends NotiveElement {
#selection?: GridSelection;
setSelection(gridId: string, selection: GridSelection) {
+ const grid = this.doc.grids.find((grid) => grid.id === gridId);
+ if (!grid) throw new Error("Invalid grid ID");
+
this.#selectedGridId = gridId;
this.#selection = selection;
this.#updateGridSelections();
+
+ this.#toolbar.subdivisions = getSelectedSubdivisionsCount(grid, selection);
+ }
+
+ clearSelection() {
+ this.#selectedGridId = undefined;
+ this.#selection = undefined;
+ this.#updateGridSelections();
+ this.#toolbar.subdivisions = undefined;
}
#updateGridSelections() {
@@ -27,10 +44,36 @@ export class NotiveAppElement extends NotiveElement {
});
}
+ #toolbar = ntvToolbar({
+ onsubdivisionschange: ({ subdivisions }) => {
+ if (!subdivisions) return;
+
+ const gridId = this.#selectedGridId;
+ const selection = this.#selection;
+
+ if (!gridId || !selection) return;
+
+ const gridIndex = this.doc.grids.findIndex((grid) => grid.id === gridId);
+
+ this.doc = produce(this.doc, (doc) => {
+ doc.grids[gridIndex] = changeSelectedSubdivisions(
+ this.doc.grids[gridIndex],
+ selection,
+ subdivisions,
+ );
+ });
+
+ this.querySelector<NotiveGridElement>(
+ `ntv-grid[data-grid-id="${gridId}"]`,
+ )!.grid = renderGrid(this.doc.grids[gridIndex]);
+
+ this.clearSelection();
+ },
+ });
+
connectedCallback() {
this.append(
- ntvToolbar(),
-
+ this.#toolbar,
...this.doc.grids.map((grid) =>
ntvGrid({
grid: renderGrid(grid),
diff --git a/web/src/components/grid/index.ts b/web/src/components/grid/index.ts
index 78bb14e..3189409 100644
--- a/web/src/components/grid/index.ts
+++ b/web/src/components/grid/index.ts
@@ -12,7 +12,16 @@ import { extendSelection, GridSelection } from "./selection";
export class NotiveGridElement extends NotiveElement {
#internals: ElementInternals = this.attachInternals();
- grid?: RenderedGrid;
+ #grid?: RenderedGrid;
+
+ get grid(): RenderedGrid | undefined {
+ return this.#grid;
+ }
+
+ set grid(grid: RenderedGrid | undefined) {
+ this.#grid = grid;
+ this.draw();
+ }
#selection?: GridSelection;
diff --git a/web/src/components/grid/selection.ts b/web/src/components/grid/selection.ts
index a24bbf5..517f8ae 100644
--- a/web/src/components/grid/selection.ts
+++ b/web/src/components/grid/selection.ts
@@ -1,4 +1,5 @@
import { CellRef, cellRefEquals } from "../../types";
+import { RenderedGrid } from "./renderGrid";
export type CellRange = [start: CellRef, end: CellRef];
@@ -21,3 +22,7 @@ export function extendSelection(
return { ...selection, range: [selection.activeCellRef, cellRef] };
}
+
+export function getSelectionRange(selection: GridSelection): CellRange {
+ return selection.range ?? [selection.activeCellRef, selection.activeCellRef];
+}
diff --git a/web/src/components/toolbar/index.css b/web/src/components/toolbar/index.css
index e082f7d..653c326 100644
--- a/web/src/components/toolbar/index.css
+++ b/web/src/components/toolbar/index.css
@@ -18,11 +18,13 @@
padding: 0 0.5rem;
height: 1.25rem;
color: white;
+ font-weight: 600;
font-size: 0.75rem;
}
ntv-toolbar button:hover {
background: var(--color-green-400);
+ color: var(--color-neutral-900);
}
ntv-toolbar button[data-icon] {
diff --git a/web/src/components/toolbar/index.ts b/web/src/components/toolbar/index.ts
index da4b69d..b8a383d 100644
--- a/web/src/components/toolbar/index.ts
+++ b/web/src/components/toolbar/index.ts
@@ -1,24 +1,66 @@
-import NotiveElement, { customElement } from "../../element";
-import h, { fragment } from "../../html";
+import NotiveElement, { customElement, eventHandler } from "../../element";
+import h from "../../html";
+import { minus16Icon, plus16Icon } from "../icons";
import "./index.css";
-@customElement("ntv-toolbar")
-class NotiveToolbarElement extends NotiveElement {
- connectedCallback() {
- this.append(this.#view());
+export class SubdivisionsChangeEvent extends Event {
+ static readonly TYPE = "ntv:toolbar:subdivisionschange";
+
+ constructor(public subdivisions: number | undefined) {
+ super(SubdivisionsChangeEvent.TYPE);
}
+}
+@customElement("ntv-toolbar")
+class NotiveToolbarElement extends NotiveElement {
#subdivisionsInputEl: HTMLInputElement = h.input({
title: "Subdivisions",
disabled: true,
});
- #view() {
- return fragment(
+ get subdivisions(): number | undefined {
+ if (this.#subdivisionsInputEl.value === "") return;
+ return parseInt(this.#subdivisionsInputEl.value);
+ }
+
+ set subdivisions(n: number | undefined) {
+ const m = n && Math.max(n, 1);
+ this.#subdivisionsInputEl.value = m === undefined ? "" : m.toString();
+ }
+
+ @eventHandler(SubdivisionsChangeEvent.TYPE)
+ onsubdivisionschange?: (event: SubdivisionsChangeEvent) => any;
+
+ connectedCallback() {
+ this.append(
h.section(
- h.button({ dataset: { icon: "" } }, "-"),
+ h.button(
+ {
+ dataset: { icon: "" },
+ onclick: () => {
+ if (!this.subdivisions) return;
+ this.subdivisions = this.subdivisions - 1;
+ this.dispatchEvent(
+ new SubdivisionsChangeEvent(this.subdivisions),
+ );
+ },
+ },
+ h.span(minus16Icon()),
+ ),
this.#subdivisionsInputEl,
- h.button({ dataset: { icon: "" } }, "+"),
+ h.button(
+ {
+ dataset: { icon: "" },
+ onclick: () => {
+ if (!this.subdivisions) return;
+ this.subdivisions = this.subdivisions + 1;
+ this.dispatchEvent(
+ new SubdivisionsChangeEvent(this.subdivisions),
+ );
+ },
+ },
+ h.span(plus16Icon()),
+ ),
),
h.section(h.button("Play")),
);
diff --git a/web/src/defaultDoc.ts b/web/src/defaultDoc.ts
index 7409c1a..0a3fbfb 100644
--- a/web/src/defaultDoc.ts
+++ b/web/src/defaultDoc.ts
@@ -9,7 +9,7 @@ export default function defaultDoc(): Doc {
return {
grids: [
{
- id: window.crypto.randomUUID(),
+ id: globalThis.crypto.randomUUID(),
baseCellSize: 42,
baseCellWidthRatio: new Ratio(1, 16),
parts: [
@@ -21,7 +21,7 @@ export default function defaultDoc(): Doc {
],
},
{
- id: window.crypto.randomUUID(),
+ id: globalThis.crypto.randomUUID(),
baseCellSize: 42,
baseCellWidthRatio: new Ratio(1, 16),
parts: [
diff --git a/web/src/grid.test.ts b/web/src/grid.test.ts
new file mode 100644
index 0000000..50c0626
--- /dev/null
+++ b/web/src/grid.test.ts
@@ -0,0 +1,45 @@
+import { expect, test } from "vitest";
+import defaultDoc from "./defaultDoc";
+import renderGrid from "./components/grid/renderGrid";
+import { changeSelectedSubdivisions } from "./grid";
+import { GridSelection } from "./components/grid/selection";
+
+test("foo", () => {
+ const doc = defaultDoc();
+ const grid = doc.grids[1];
+
+ const selection: GridSelection = {
+ activeCellRef: { partIndex: 0, rowIndex: 0, cellIndex: 0 },
+ range: [
+ { partIndex: 0, rowIndex: 0, cellIndex: 0 },
+ { partIndex: 0, rowIndex: 0, cellIndex: 3 },
+ ],
+ };
+
+ const newGrid = changeSelectedSubdivisions(grid, selection, 3);
+ const renderedGrid = renderGrid(newGrid);
+
+ expect(
+ renderedGrid.renderedRows.map((row) => row.renderedCells.length),
+ ).toStrictEqual([15, 16, 16, 16]);
+
+ expect(
+ newGrid.parts[0].rows[0].cells.map((cell) => cell.widthRatio.toData()),
+ ).toStrictEqual([
+ [1, 12],
+ [1, 12],
+ [1, 12],
+ [1, 16],
+ [1, 16],
+ [1, 16],
+ [1, 16],
+ [1, 16],
+ [1, 16],
+ [1, 16],
+ [1, 16],
+ [1, 16],
+ [1, 16],
+ [1, 16],
+ [1, 16],
+ ]);
+});
diff --git a/web/src/grid.ts b/web/src/grid.ts
new file mode 100644
index 0000000..e849803
--- /dev/null
+++ b/web/src/grid.ts
@@ -0,0 +1,109 @@
+import { produce } from "immer";
+import renderGrid, { getRenderedCell } from "./components/grid/renderGrid";
+import { getSelectionRange, GridSelection } from "./components/grid/selection";
+import Ratio from "./math/Ratio";
+import { Cell, Grid, renderedRowIndexToRef } from "./types";
+
+export function getSelectedSubdivisionsCount(
+ grid: Grid,
+ selection: GridSelection,
+): number | undefined {
+ const renderedGrid = renderGrid(grid);
+
+ const [startCellRef, endCellRef] = getSelectionRange(selection);
+ const startCell = getRenderedCell(renderedGrid, startCellRef);
+ const endCell = getRenderedCell(renderedGrid, endCellRef);
+
+ if (!startCell || !endCell) throw new Error("Invalid cell refs");
+
+ const startRenderedRowIndex = Math.min(
+ startCell.renderedRowIndex,
+ endCell.renderedRowIndex,
+ );
+
+ const endRenderedRowIndex = Math.max(
+ startCell.renderedRowIndex,
+ endCell.renderedRowIndex,
+ );
+
+ const startRatio = Ratio.min(startCell.startRatio, endCell.startRatio);
+ const endRatio = Ratio.max(startCell.endRatio, endCell.endRatio);
+
+ return Math.min(
+ ...renderedGrid.renderedRows
+ .slice(startRenderedRowIndex, endRenderedRowIndex + 1)
+ .map((row) => {
+ const startCellIndex = row.renderedCells.findIndex((cell) =>
+ cell.startRatio.equals(startRatio),
+ );
+
+ const endCellIndex = row.renderedCells.findLastIndex((cell) =>
+ cell.endRatio.equals(endRatio),
+ );
+
+ return endCellIndex - startCellIndex + 1;
+ }),
+ );
+}
+
+export function changeSelectedSubdivisions(
+ grid: Grid,
+ selection: GridSelection,
+ subdivisions: number,
+): Grid {
+ const renderedGrid = renderGrid(grid);
+ const [startCellRef, endCellRef] = getSelectionRange(selection);
+ const startCell = getRenderedCell(renderedGrid, startCellRef);
+ const endCell = getRenderedCell(renderedGrid, endCellRef);
+ if (!startCell || !endCell) throw new Error("Invalid cell refs");
+
+ const startRenderedRowIndex = Math.min(
+ startCell.renderedRowIndex,
+ endCell.renderedRowIndex,
+ );
+
+ const endRenderedRowIndex = Math.max(
+ startCell.renderedRowIndex,
+ endCell.renderedRowIndex,
+ );
+
+ const startRatio = Ratio.min(startCell.startRatio, endCell.startRatio);
+ const endRatio = Ratio.max(startCell.endRatio, endCell.endRatio);
+ const selectedWidthRatio = endRatio.subtract(startRatio);
+ const widthRatio = selectedWidthRatio.divideRatio(
+ Ratio.fromInteger(subdivisions),
+ );
+
+ return produce(grid, (draft) => {
+ for (
+ let renderedRowIndex = startRenderedRowIndex;
+ renderedRowIndex <= endRenderedRowIndex;
+ renderedRowIndex++
+ ) {
+ const renderedRow = renderedGrid.renderedRows[renderedRowIndex];
+
+ const startCellIndex = renderedRow.renderedCells.findIndex((cell) =>
+ cell.startRatio.equals(startRatio),
+ );
+
+ const endCellIndex = renderedRow.renderedCells.findLastIndex((cell) =>
+ cell.endRatio.equals(endRatio),
+ );
+
+ const { partIndex, rowIndex } = renderedRowIndexToRef(
+ grid,
+ renderedRowIndex,
+ );
+
+ const row = draft.parts[partIndex].rows[rowIndex];
+ const previousCells = row.cells.slice(0, startCellIndex);
+ const nextCells = row.cells.slice(endCellIndex + 1);
+
+ const newCells: Cell[] = Array.from({ length: subdivisions }, () => ({
+ widthRatio,
+ }));
+
+ row.cells = [...previousCells, ...newCells, ...nextCells];
+ }
+ });
+}
diff --git a/web/src/math/Ratio.test.ts b/web/src/math/Ratio.test.ts
new file mode 100644
index 0000000..da6fef2
--- /dev/null
+++ b/web/src/math/Ratio.test.ts
@@ -0,0 +1,27 @@
+import { describe, expect, test } from "vitest";
+import Ratio from "./Ratio";
+
+describe(Ratio, () => {
+ describe(Ratio.prototype.add, () => {
+ test("returns fractions in simplest form", () => {
+ const a = Ratio.fromInteger(0);
+ const b = new Ratio(1, 4);
+
+ const c = a.add(b);
+ expect(c.numerator).toBe(1);
+ expect(c.denominator).toBe(4);
+
+ const d = c.add(b);
+ expect(d.numerator).toBe(1);
+ expect(d.denominator).toBe(2);
+
+ const e = d.add(b);
+ expect(e.numerator).toBe(3);
+ expect(e.denominator).toBe(4);
+
+ const f = e.add(b);
+ expect(f.numerator).toBe(1);
+ expect(f.denominator).toBe(1);
+ });
+ });
+});
diff --git a/web/src/math/Ratio.ts b/web/src/math/Ratio.ts
index 0cca966..e2a1fbf 100644
--- a/web/src/math/Ratio.ts
+++ b/web/src/math/Ratio.ts
@@ -1,3 +1,5 @@
+import { gcd } from ".";
+
/** Serializable representation of a ratio. */
export type RatioData = [numerator: number, denominator: number];
@@ -25,8 +27,10 @@ export default class Ratio {
throw new RangeError("Ratio demnominator cannot be zero");
}
- this.#numerator = numerator;
- this.#denominator = denominator;
+ const divisor = gcd(numerator, denominator);
+
+ this.#numerator = numerator / divisor;
+ this.#denominator = denominator / divisor;
}
multiplyRatio(other: Ratio): Ratio {
@@ -63,10 +67,22 @@ export default class Ratio {
return left < right ? -1 : left > right ? 1 : 0;
}
+ equals(other: Ratio): boolean {
+ return this.compare(other) === 0;
+ }
+
toNumber(): number {
return this.numerator / this.denominator;
}
+ toString(): string {
+ return `${this.numerator}/${this.denominator}`;
+ }
+
+ [Symbol.for("nodejs.util.inspect.custom")](): string {
+ return `Ratio { ${this.numerator}/${this.denominator} }`;
+ }
+
static fromInteger(n: number): Ratio {
return new Ratio(n, 1);
}
@@ -78,4 +94,12 @@ export default class Ratio {
static fromData(ratio: RatioData): Ratio {
return new Ratio(ratio[0], ratio[1]);
}
+
+ static min(...ratios: Ratio[]): Ratio {
+ return ratios.reduce((a, b) => (a.compare(b) <= 0 ? a : b));
+ }
+
+ static max(...ratios: Ratio[]): Ratio {
+ return ratios.reduce((a, b) => (a.compare(b) >= 0 ? a : b));
+ }
}
diff --git a/web/src/math/index.ts b/web/src/math/index.ts
new file mode 100644
index 0000000..70dbb67
--- /dev/null
+++ b/web/src/math/index.ts
@@ -0,0 +1,3 @@
+export function gcd(a: number, b: number): number {
+ return b === 0 ? Math.abs(a) : gcd(b, a % b);
+}
diff --git a/web/src/types.ts b/web/src/types.ts
index b41bb9a..dc26c89 100644
--- a/web/src/types.ts
+++ b/web/src/types.ts
@@ -1,9 +1,10 @@
+import { Immutable } from "immer";
import Ratio from "./math/Ratio";
-export interface Cell {
+export type Cell = Immutable<{
value?: string;
widthRatio: Ratio;
-}
+}>;
export interface Row {
cells: Cell[];
@@ -44,42 +45,11 @@ export function cellRefEquals(a: CellRef, b: CellRef): boolean {
);
}
-export function mapRowsInRange(
- doc: Doc,
- gridId: string,
- startRef: CellRef,
- endRef: CellRef,
- mapFn: (row: Row, ref: RowRef) => Row,
-): Doc {
- const firstPartIndex = Math.min(startRef.partIndex, endRef.partIndex);
- const lastPartIndex = Math.max(startRef.partIndex, endRef.partIndex);
- const firstRowIndex = Math.min(startRef.rowIndex, endRef.rowIndex);
- const lastRowIndex = Math.max(startRef.rowIndex, endRef.rowIndex);
-
- return {
- ...doc,
- grids: doc.grids.map((grid) => {
- if (grid.id !== gridId) return grid;
-
- return {
- ...grid,
- parts: grid.parts.map((part, partIndex) => {
- if (partIndex < firstPartIndex || partIndex > lastPartIndex) {
- return part;
- }
-
- return {
- ...part,
- rows: part.rows.map((row, rowIndex) => {
- if (rowIndex < firstRowIndex || rowIndex > lastRowIndex) {
- return row;
- }
-
- return mapFn(row, { partIndex, rowIndex });
- }),
- };
- }),
- };
- }),
- };
+export function renderedRowIndexToRef(
+ grid: Grid,
+ renderedRowIndex: number,
+): RowRef {
+ const partIndex = renderedRowIndex % grid.parts.length;
+ const rowIndex = Math.floor(renderedRowIndex / grid.parts.length);
+ return { partIndex, rowIndex };
}