@opengeni/react 0.48.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/README.md +138 -2
  2. package/dist/artifacts-document.d.ts +3 -0
  3. package/dist/artifacts-document.js +17 -0
  4. package/dist/artifacts-document.js.map +1 -0
  5. package/dist/artifacts-presentation.d.ts +3 -0
  6. package/dist/artifacts-presentation.js +17 -0
  7. package/dist/artifacts-presentation.js.map +1 -0
  8. package/dist/artifacts-spreadsheet.d.ts +3 -0
  9. package/dist/artifacts-spreadsheet.js +17 -0
  10. package/dist/artifacts-spreadsheet.js.map +1 -0
  11. package/dist/artifacts.d.ts +2 -0
  12. package/dist/artifacts.js +199 -0
  13. package/dist/artifacts.js.map +1 -0
  14. package/dist/{chunk-NJ7M2B6Z.js → chunk-5UQPNLIO.js} +16 -8
  15. package/dist/{chunk-NJ7M2B6Z.js.map → chunk-5UQPNLIO.js.map} +1 -1
  16. package/dist/{chunk-LHLYWG5R.js → chunk-6KLX2RH5.js} +461 -38
  17. package/dist/chunk-6KLX2RH5.js.map +1 -0
  18. package/dist/chunk-7WCLAEFV.js +2828 -0
  19. package/dist/chunk-7WCLAEFV.js.map +1 -0
  20. package/dist/{chunk-I4MPFDRP.js → chunk-B6WPZSTT.js} +104 -104
  21. package/dist/{chunk-I4MPFDRP.js.map → chunk-B6WPZSTT.js.map} +1 -1
  22. package/dist/{chunk-EKZH6IDG.js → chunk-C5ZPSSS5.js} +111 -1
  23. package/dist/chunk-C5ZPSSS5.js.map +1 -0
  24. package/dist/{chunk-OL6QVXGF.js → chunk-HA46CYQK.js} +152 -8
  25. package/dist/chunk-HA46CYQK.js.map +1 -0
  26. package/dist/chunk-HUIKIDM5.js +2148 -0
  27. package/dist/chunk-HUIKIDM5.js.map +1 -0
  28. package/dist/{chunk-FWXGSNXT.js → chunk-IGC6A2ZI.js} +585 -34
  29. package/dist/chunk-IGC6A2ZI.js.map +1 -0
  30. package/dist/chunk-JVYQRRC4.js +53 -0
  31. package/dist/chunk-JVYQRRC4.js.map +1 -0
  32. package/dist/chunk-KCGXPP5V.js +157 -0
  33. package/dist/chunk-KCGXPP5V.js.map +1 -0
  34. package/dist/chunk-LLOLUHVG.js +5937 -0
  35. package/dist/chunk-LLOLUHVG.js.map +1 -0
  36. package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
  37. package/dist/chunk-MKPIAGVL.js.map +1 -0
  38. package/dist/chunk-PEI6RANI.js +3234 -0
  39. package/dist/chunk-PEI6RANI.js.map +1 -0
  40. package/dist/chunk-R3AZOAB5.js +933 -0
  41. package/dist/chunk-R3AZOAB5.js.map +1 -0
  42. package/dist/{chunk-O4VXOBGF.js → chunk-WSK7G5LE.js} +70 -2
  43. package/dist/chunk-WSK7G5LE.js.map +1 -0
  44. package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
  45. package/dist/chunk-XBZWOM2K.js.map +1 -0
  46. package/dist/{chunk-XCNTXWBB.js → chunk-Y7OEDUJ5.js} +856 -338
  47. package/dist/chunk-Y7OEDUJ5.js.map +1 -0
  48. package/dist/client.d.ts +10 -2
  49. package/dist/clipboard.d.ts +1 -0
  50. package/dist/clipboard.js +7 -0
  51. package/dist/clipboard.js.map +1 -0
  52. package/dist/commands/index.d.ts +3 -3
  53. package/dist/commands/registry.d.ts +1 -1
  54. package/dist/commands/types.d.ts +1 -1
  55. package/dist/components/approval-surface.d.ts +2 -2
  56. package/dist/components/artifacts/artifact-surface.d.ts +18 -0
  57. package/dist/components/artifacts/document-editor.d.ts +172 -0
  58. package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
  59. package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
  60. package/dist/components/artifacts/editable-document.d.ts +24 -0
  61. package/dist/components/artifacts/editable-presentation.d.ts +24 -0
  62. package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
  63. package/dist/components/artifacts/index.d.ts +9 -0
  64. package/dist/components/artifacts/presentation-editor.d.ts +297 -0
  65. package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
  66. package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
  67. package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
  68. package/dist/components/browser-input.d.ts +7 -0
  69. package/dist/components/browser-viewer.d.ts +30 -0
  70. package/dist/components/chat-composer.d.ts +8 -8
  71. package/dist/components/code-editor.d.ts +25 -3
  72. package/dist/components/command-palette.d.ts +2 -2
  73. package/dist/components/composer-transcription-control.d.ts +2 -2
  74. package/dist/components/composer.d.ts +17 -17
  75. package/dist/components/computer-viewer.d.ts +24 -0
  76. package/dist/components/copy-button.d.ts +2 -2
  77. package/dist/components/desktop-viewer.d.ts +50 -22
  78. package/dist/components/diff-view.d.ts +1 -1
  79. package/dist/components/enrollment-consent.d.ts +1 -1
  80. package/dist/components/enrollment-device-flow.d.ts +1 -1
  81. package/dist/components/file-browser.d.ts +2 -2
  82. package/dist/components/fleet-tile.d.ts +1 -1
  83. package/dist/components/generated-video-player.d.ts +10 -0
  84. package/dist/components/human-input-form.d.ts +1 -1
  85. package/dist/components/human-input-surface.d.ts +2 -2
  86. package/dist/components/interaction-intervention-banner.d.ts +11 -0
  87. package/dist/components/interactive-workbench-viewers.d.ts +2 -0
  88. package/dist/components/machine-card.d.ts +2 -2
  89. package/dist/components/machine-dock-bar.d.ts +3 -3
  90. package/dist/components/machine-health-pill.d.ts +2 -2
  91. package/dist/components/machine-input-display.d.ts +1 -1
  92. package/dist/components/machine-metrics.d.ts +2 -2
  93. package/dist/components/machine-status-pill.d.ts +4 -4
  94. package/dist/components/machines/health.d.ts +1 -1
  95. package/dist/components/machines/machine-detail.d.ts +3 -3
  96. package/dist/components/machines/metric-history-chart.d.ts +1 -1
  97. package/dist/components/machines/metric-sparkline.d.ts +2 -2
  98. package/dist/components/machines/series.d.ts +2 -2
  99. package/dist/components/machines-dashboard.d.ts +2 -2
  100. package/dist/components/markdown.d.ts +1 -1
  101. package/dist/components/message-timeline.d.ts +12 -5
  102. package/dist/components/model-picker.d.ts +2 -2
  103. package/dist/components/model-policy-picker.d.ts +7 -7
  104. package/dist/components/pierre-diff.d.ts +1 -1
  105. package/dist/components/pierre-file.d.ts +1 -1
  106. package/dist/components/queue-draft-policy.d.ts +1 -1
  107. package/dist/components/queue-surface-implementation.d.ts +3 -3
  108. package/dist/components/queue-surface-state.d.ts +4 -4
  109. package/dist/components/queue-surface.d.ts +5 -5
  110. package/dist/components/sandbox-files.d.ts +3 -3
  111. package/dist/components/sandbox-terminal.d.ts +24 -4
  112. package/dist/components/sandbox-workspace.d.ts +28 -9
  113. package/dist/components/session-chrome.d.ts +4 -4
  114. package/dist/components/session-status.d.ts +2 -2
  115. package/dist/components/timeline-annotation-selection.d.ts +9 -0
  116. package/dist/components/timeline-annotations-dialog.d.ts +16 -0
  117. package/dist/components/timeline-annotations.d.ts +13 -0
  118. package/dist/components/tooltip.d.ts +4 -4
  119. package/dist/components/user-message-body.d.ts +2 -2
  120. package/dist/components/workbench-changes.d.ts +2 -2
  121. package/dist/components/workspace-dock.d.ts +1 -1
  122. package/dist/composer.d.ts +11 -11
  123. package/dist/composer.js +7 -7
  124. package/dist/hooks/internal.d.ts +1 -1
  125. package/dist/hooks/use-attached-browsers.d.ts +21 -0
  126. package/dist/hooks/use-auth-runs.d.ts +32 -0
  127. package/dist/hooks/use-available-models.d.ts +2 -2
  128. package/dist/hooks/use-billing-usage.d.ts +1 -1
  129. package/dist/hooks/use-browser-downloads.d.ts +25 -0
  130. package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
  131. package/dist/hooks/use-browser-identities.d.ts +30 -0
  132. package/dist/hooks/use-browser-session.d.ts +29 -0
  133. package/dist/hooks/use-browser-sessions.d.ts +32 -0
  134. package/dist/hooks/use-channels.d.ts +26 -0
  135. package/dist/hooks/use-codex-accounts.d.ts +2 -2
  136. package/dist/hooks/use-composer.d.ts +36 -9
  137. package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
  138. package/dist/hooks/use-computer-session.d.ts +29 -0
  139. package/dist/hooks/use-computer-sessions.d.ts +28 -0
  140. package/dist/hooks/use-desktop-stream.d.ts +5 -1
  141. package/dist/hooks/use-environments.d.ts +1 -1
  142. package/dist/hooks/use-file-attachments.d.ts +1 -1
  143. package/dist/hooks/use-goal.d.ts +2 -2
  144. package/dist/hooks/use-human-input.d.ts +2 -2
  145. package/dist/hooks/use-interaction-interventions.d.ts +27 -0
  146. package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
  147. package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
  148. package/dist/hooks/use-machine-chip.d.ts +2 -2
  149. package/dist/hooks/use-machines.d.ts +4 -4
  150. package/dist/hooks/use-network-routes.d.ts +25 -0
  151. package/dist/hooks/use-packs.d.ts +7 -2
  152. package/dist/hooks/use-rigs.d.ts +1 -1
  153. package/dist/hooks/use-sandbox-files.d.ts +6 -4
  154. package/dist/hooks/use-sandbox-git.d.ts +6 -5
  155. package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
  156. package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
  157. package/dist/hooks/use-session-capabilities.d.ts +5 -4
  158. package/dist/hooks/use-session-control.d.ts +1 -1
  159. package/dist/hooks/use-session-events.d.ts +2 -2
  160. package/dist/hooks/use-session-lineage.d.ts +1 -1
  161. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  162. package/dist/hooks/use-session.d.ts +2 -2
  163. package/dist/hooks/use-site-auth-connections.d.ts +25 -0
  164. package/dist/hooks/use-slash-commands.d.ts +1 -1
  165. package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
  166. package/dist/hooks/use-terminal-stream.d.ts +11 -2
  167. package/dist/hooks/use-turn-queue.d.ts +2 -2
  168. package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
  169. package/dist/hooks/use-voice-input.d.ts +1 -1
  170. package/dist/hooks/use-workspace-capture.d.ts +1 -1
  171. package/dist/hooks/use-workspace-edit.d.ts +1 -1
  172. package/dist/hooks/use-workspace-sessions.d.ts +1 -1
  173. package/dist/hooks/use-workspaces.d.ts +1 -1
  174. package/dist/index.d.ts +162 -155
  175. package/dist/index.js +1822 -1703
  176. package/dist/index.js.map +1 -1
  177. package/dist/interaction.d.ts +33 -0
  178. package/dist/interaction.js +550 -0
  179. package/dist/interaction.js.map +1 -0
  180. package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
  181. package/dist/interactive-workbench-viewers-TOHBXJEE.js.map +1 -0
  182. package/dist/lib/host-platform.d.ts +5 -0
  183. package/dist/lib/relay-wire.d.ts +24 -1
  184. package/dist/lib/sandbox-liveness.d.ts +9 -0
  185. package/dist/lib/terminal-capability.d.ts +5 -0
  186. package/dist/lib/xterm-theme.d.ts +1 -1
  187. package/dist/machines.d.ts +30 -30
  188. package/dist/machines.js +2 -2
  189. package/dist/model-policy.d.ts +1 -1
  190. package/dist/model-policy.js +1 -1
  191. package/dist/provider.d.ts +6 -5
  192. package/dist/{queue-surface-implementation-U5JFG7HT.js → queue-surface-implementation-2KVJTYXH.js} +100 -48
  193. package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
  194. package/dist/realtime/dropdown-menu.d.ts +8 -8
  195. package/dist/realtime/realtime-control.d.ts +8 -8
  196. package/dist/realtime.d.ts +4 -4
  197. package/dist/realtime.js +38 -11
  198. package/dist/realtime.js.map +1 -1
  199. package/dist/session-context.d.ts +28 -2
  200. package/dist/session-ui.d.ts +14 -14
  201. package/dist/session-ui.js +5 -4
  202. package/dist/session.d.ts +28 -28
  203. package/dist/session.js +10 -10
  204. package/dist/timeline/activity-rail.d.ts +5 -3
  205. package/dist/timeline/disclosure-context.d.ts +1 -1
  206. package/dist/timeline/fleet-decision-projection.d.ts +1 -1
  207. package/dist/timeline/fleet-decision-row.d.ts +3 -3
  208. package/dist/timeline/index.d.ts +19 -19
  209. package/dist/timeline/parsers.d.ts +4 -1
  210. package/dist/timeline/projection.d.ts +3 -3
  211. package/dist/timeline/registry.d.ts +8 -2
  212. package/dist/timeline/screenshot-lightbox.d.ts +2 -2
  213. package/dist/timeline/shared.d.ts +21 -10
  214. package/dist/timeline/tool-diff.d.ts +2 -2
  215. package/dist/timeline/tool-renderers.d.ts +1 -1
  216. package/dist/timeline/turn-summary.d.ts +3 -3
  217. package/dist/timeline/types.d.ts +44 -3
  218. package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
  219. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
  220. package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
  221. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
  222. package/package.json +36 -7
  223. package/src/artifacts-document.ts +31 -0
  224. package/src/artifacts-presentation.ts +36 -0
  225. package/src/artifacts-spreadsheet.ts +21 -0
  226. package/src/artifacts.ts +89 -0
  227. package/src/client.ts +157 -1
  228. package/src/clipboard.ts +2 -0
  229. package/src/components/artifacts/artifact-surface.tsx +93 -0
  230. package/src/components/artifacts/document-editor.tsx +2255 -0
  231. package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
  232. package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
  233. package/src/components/artifacts/editable-document.tsx +657 -0
  234. package/src/components/artifacts/editable-presentation.tsx +1020 -0
  235. package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
  236. package/src/components/artifacts/index.ts +104 -0
  237. package/src/components/artifacts/presentation-editor.tsx +3148 -0
  238. package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
  239. package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
  240. package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
  241. package/src/components/browser-input.ts +65 -0
  242. package/src/components/browser-viewer.tsx +2702 -0
  243. package/src/components/chat-composer.tsx +13 -0
  244. package/src/components/code-editor.tsx +89 -30
  245. package/src/components/composer-transcription-control.tsx +10 -6
  246. package/src/components/computer-viewer.tsx +1636 -0
  247. package/src/components/desktop-viewer.tsx +215 -74
  248. package/src/components/generated-video-player.tsx +89 -0
  249. package/src/components/interaction-intervention-banner.tsx +99 -0
  250. package/src/components/interactive-workbench-viewers.ts +2 -0
  251. package/src/components/machine-input-display.ts +3 -0
  252. package/src/components/machines/machine-detail.tsx +1 -1
  253. package/src/components/message-timeline.tsx +289 -32
  254. package/src/components/model-policy-picker.tsx +15 -0
  255. package/src/components/queue-surface-implementation.tsx +116 -60
  256. package/src/components/sandbox-files.tsx +2 -2
  257. package/src/components/sandbox-terminal.tsx +128 -46
  258. package/src/components/sandbox-workspace.tsx +256 -90
  259. package/src/components/session-chrome.tsx +53 -13
  260. package/src/components/timeline-annotation-selection.tsx +221 -0
  261. package/src/components/timeline-annotations-dialog.tsx +213 -0
  262. package/src/components/timeline-annotations.tsx +84 -0
  263. package/src/components/workbench-changes.tsx +188 -31
  264. package/src/components/workspace-dock.tsx +59 -5
  265. package/src/hooks/use-attached-browsers.ts +154 -0
  266. package/src/hooks/use-auth-runs.ts +278 -0
  267. package/src/hooks/use-browser-downloads.ts +218 -0
  268. package/src/hooks/use-browser-frame-stream.ts +425 -0
  269. package/src/hooks/use-browser-identities.ts +266 -0
  270. package/src/hooks/use-browser-session.ts +543 -0
  271. package/src/hooks/use-browser-sessions.ts +286 -0
  272. package/src/hooks/use-channels.ts +99 -0
  273. package/src/hooks/use-composer.ts +769 -24
  274. package/src/hooks/use-computer-frame-stream.ts +434 -0
  275. package/src/hooks/use-computer-session.ts +462 -0
  276. package/src/hooks/use-computer-sessions.ts +251 -0
  277. package/src/hooks/use-desktop-stream.ts +39 -7
  278. package/src/hooks/use-interaction-interventions.ts +242 -0
  279. package/src/hooks/use-interaction-invalidation.ts +66 -0
  280. package/src/hooks/use-machine-chip.ts +15 -3
  281. package/src/hooks/use-machines.ts +33 -3
  282. package/src/hooks/use-network-routes.ts +214 -0
  283. package/src/hooks/use-packs.ts +51 -0
  284. package/src/hooks/use-sandbox-files.ts +98 -55
  285. package/src/hooks/use-sandbox-git.ts +93 -38
  286. package/src/hooks/use-sandbox-terminal.ts +3 -2
  287. package/src/hooks/use-session-capabilities.ts +59 -46
  288. package/src/hooks/use-site-auth-connections.ts +226 -0
  289. package/src/hooks/use-supergrok-accounts.ts +81 -0
  290. package/src/hooks/use-terminal-stream.ts +359 -47
  291. package/src/hooks/use-video-artifact-playback.ts +28 -0
  292. package/src/hooks/use-workspace-edit.ts +2 -1
  293. package/src/index.ts +18 -0
  294. package/src/interaction.ts +101 -0
  295. package/src/lib/host-platform.ts +10 -0
  296. package/src/lib/relay-wire.ts +47 -4
  297. package/src/lib/sandbox-liveness.ts +11 -0
  298. package/src/lib/terminal-capability.ts +10 -0
  299. package/src/model-policy.ts +19 -4
  300. package/src/provider.tsx +133 -1
  301. package/src/realtime/realtime-control.tsx +34 -6
  302. package/src/session-context.ts +156 -1
  303. package/src/timeline/activity-rail.tsx +25 -3
  304. package/src/timeline/index.ts +3 -0
  305. package/src/timeline/parsers.ts +14 -1
  306. package/src/timeline/projection.ts +269 -14
  307. package/src/timeline/registry.ts +13 -1
  308. package/src/timeline/screenshot-lightbox.tsx +19 -9
  309. package/src/timeline/shared.tsx +56 -8
  310. package/src/timeline/tool-renderers.tsx +303 -50
  311. package/src/timeline/types.ts +57 -3
  312. package/styles/compiled.css +312 -21
  313. package/styles/index.css +15 -0
  314. package/styles/responsive.css +13 -1
  315. package/dist/chunk-23EJ676W.js.map +0 -1
  316. package/dist/chunk-EKZH6IDG.js.map +0 -1
  317. package/dist/chunk-FWXGSNXT.js.map +0 -1
  318. package/dist/chunk-LHLYWG5R.js.map +0 -1
  319. package/dist/chunk-O4VXOBGF.js.map +0 -1
  320. package/dist/chunk-OL6QVXGF.js.map +0 -1
  321. package/dist/chunk-WYA65Y3F.js.map +0 -1
  322. package/dist/chunk-XCNTXWBB.js.map +0 -1
  323. package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
  324. package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
  325. package/dist/queue-surface-implementation-U5JFG7HT.js.map +0 -1
@@ -0,0 +1,799 @@
1
+ const TILE_SIZE = 256;
2
+ const MAX_CACHE_PHYSICAL_PIXELS = 32_000_000;
3
+ const MAX_CACHE_TILES = 96;
4
+ const MAX_DEVICE_PIXEL_RATIO = 4;
5
+ const MAX_AXIS_MARKS_PER_PAINT = 8_192;
6
+ const MAX_CANVAS_EDGE = 8_192;
7
+ const MAX_CANVAS_PIXELS = 16_000_000;
8
+ const MAX_TILES_PER_PAINT = 512;
9
+
10
+ export type SpreadsheetCanvasCellFormat = {
11
+ fill?: string | undefined;
12
+ font?:
13
+ | {
14
+ name?: string | undefined;
15
+ size?: number | undefined;
16
+ bold?: boolean | undefined;
17
+ italic?: boolean | undefined;
18
+ underline?: boolean | undefined;
19
+ color?: string | undefined;
20
+ }
21
+ | undefined;
22
+ horizontalAlignment?: "left" | "center" | "right" | "justify" | undefined;
23
+ verticalAlignment?: "top" | "center" | "bottom" | undefined;
24
+ wrapText?: boolean | undefined;
25
+ };
26
+
27
+ /** One sparse, already-authorized cell projected into the renderer. */
28
+ export type SpreadsheetCanvasCell = {
29
+ row: number;
30
+ col: number;
31
+ value: unknown;
32
+ formula?: string | null | undefined;
33
+ format: SpreadsheetCanvasCellFormat;
34
+ };
35
+
36
+ export type SpreadsheetCanvasCoverage = {
37
+ rowStart: number;
38
+ rowEnd: number;
39
+ columnStart: number;
40
+ columnEnd: number;
41
+ };
42
+
43
+ /**
44
+ * A bounded value projection. `coverage` is end-exclusive; omitting it means
45
+ * the source can answer for the entire sheet. A session-backed adapter can
46
+ * replace this object as viewport subscriptions advance without making the
47
+ * React layer or renderer own the canonical workbook model.
48
+ */
49
+ export type SpreadsheetCanvasProjection = {
50
+ sheetId: string;
51
+ generationId: string | null;
52
+ revision: string | number;
53
+ coverage?: SpreadsheetCanvasCoverage | undefined;
54
+ cells: SpreadsheetCanvasCellSource;
55
+ valueAt: (cell: SpreadsheetCanvasCell) => unknown;
56
+ };
57
+
58
+ export type SpreadsheetCanvasCellSource = {
59
+ forEachInRange: (
60
+ rowStart: number,
61
+ rowEnd: number,
62
+ columnStart: number,
63
+ columnEnd: number,
64
+ visit: (cell: SpreadsheetCanvasCell) => void,
65
+ ) => void;
66
+ };
67
+
68
+ export type SpreadsheetCanvasAxis = {
69
+ count: number;
70
+ total: number;
71
+ offsetAt: (index: number) => number;
72
+ sizeAt: (index: number) => number;
73
+ indexAtOffset: (offset: number) => number;
74
+ };
75
+
76
+ export type SpreadsheetCanvasSelection = {
77
+ top: number;
78
+ bottom: number;
79
+ left: number;
80
+ right: number;
81
+ focusRow: number;
82
+ focusColumn: number;
83
+ };
84
+
85
+ export type SpreadsheetCanvasTheme = {
86
+ background: string;
87
+ headerBackground: string;
88
+ border: string;
89
+ foreground: string;
90
+ mutedForeground: string;
91
+ accent: string;
92
+ error: string;
93
+ fontFamily: string;
94
+ fontSize: number;
95
+ headerFontSize: number;
96
+ signature: string;
97
+ };
98
+
99
+ export type SpreadsheetCanvasPaint = {
100
+ canvas: HTMLCanvasElement;
101
+ projection: SpreadsheetCanvasProjection;
102
+ rows: SpreadsheetCanvasAxis;
103
+ columns: SpreadsheetCanvasAxis;
104
+ selection: SpreadsheetCanvasSelection;
105
+ logicalScrollLeft: number;
106
+ logicalScrollTop: number;
107
+ viewportWidth: number;
108
+ viewportHeight: number;
109
+ rowHeaderWidth: number;
110
+ columnHeaderHeight: number;
111
+ dimensionRevision: string | number;
112
+ devicePixelRatio: number;
113
+ theme: SpreadsheetCanvasTheme;
114
+ };
115
+
116
+ export type SpreadsheetCanvasPaintStats = {
117
+ cacheSize: number;
118
+ paintedTiles: number;
119
+ reusedTiles: number;
120
+ uncachedTiles: number;
121
+ devicePixelRatio: number;
122
+ };
123
+
124
+ type IndexedRow = {
125
+ row: number;
126
+ cells: SpreadsheetCanvasCell[];
127
+ };
128
+
129
+ type Tile = {
130
+ canvas: HTMLCanvasElement;
131
+ lastUsed: number;
132
+ };
133
+
134
+ /**
135
+ * A row-major sparse index over stored worksheet cells. Empty cells never enter
136
+ * this structure, so a tile query costs O(log k + v), where v is the number of
137
+ * stored cells intersecting that tile.
138
+ */
139
+ export class SparseSpreadsheetCellIndex {
140
+ readonly size: number;
141
+ private readonly rows: IndexedRow[];
142
+
143
+ constructor(entries: Iterable<SpreadsheetCanvasCell>) {
144
+ const sorted = [...entries].sort((left, right) => left.row - right.row || left.col - right.col);
145
+ this.size = sorted.length;
146
+ this.rows = [];
147
+ for (const entry of sorted) {
148
+ const previous = this.rows.at(-1);
149
+ if (previous?.row === entry.row) previous.cells.push(entry);
150
+ else this.rows.push({ row: entry.row, cells: [entry] });
151
+ }
152
+ }
153
+
154
+ forEachInRange(
155
+ rowStart: number,
156
+ rowEnd: number,
157
+ columnStart: number,
158
+ columnEnd: number,
159
+ visit: (cell: SpreadsheetCanvasCell) => void,
160
+ ): void {
161
+ let rowIndex = lowerBound(this.rows, rowStart, (row) => row.row);
162
+ while (rowIndex < this.rows.length) {
163
+ const row = this.rows[rowIndex]!;
164
+ if (row.row >= rowEnd) break;
165
+ let cellIndex = lowerBound(row.cells, columnStart, (cell) => cell.col);
166
+ while (cellIndex < row.cells.length) {
167
+ const cell = row.cells[cellIndex]!;
168
+ if (cell.col >= columnEnd) break;
169
+ visit(cell);
170
+ cellIndex += 1;
171
+ }
172
+ rowIndex += 1;
173
+ }
174
+ }
175
+ }
176
+
177
+ function lowerBound<T>(
178
+ values: readonly T[],
179
+ target: number,
180
+ valueAt: (value: T) => number,
181
+ ): number {
182
+ let low = 0;
183
+ let high = values.length;
184
+ while (low < high) {
185
+ const middle = (low + high) >>> 1;
186
+ if (valueAt(values[middle]!) < target) low = middle + 1;
187
+ else high = middle;
188
+ }
189
+ return low;
190
+ }
191
+
192
+ function coverageContains(
193
+ coverage: SpreadsheetCanvasCoverage | undefined,
194
+ query: SpreadsheetCanvasCoverage,
195
+ ): boolean {
196
+ return (
197
+ coverage === undefined ||
198
+ (coverage.rowStart <= query.rowStart &&
199
+ coverage.rowEnd >= query.rowEnd &&
200
+ coverage.columnStart <= query.columnStart &&
201
+ coverage.columnEnd >= query.columnEnd)
202
+ );
203
+ }
204
+
205
+ function intersectCoverage(
206
+ query: SpreadsheetCanvasCoverage,
207
+ coverage: SpreadsheetCanvasCoverage | undefined,
208
+ ): SpreadsheetCanvasCoverage | null {
209
+ if (coverage === undefined) return query;
210
+ const intersection = {
211
+ rowStart: Math.max(query.rowStart, coverage.rowStart),
212
+ rowEnd: Math.min(query.rowEnd, coverage.rowEnd),
213
+ columnStart: Math.max(query.columnStart, coverage.columnStart),
214
+ columnEnd: Math.min(query.columnEnd, coverage.columnEnd),
215
+ };
216
+ return intersection.rowStart < intersection.rowEnd &&
217
+ intersection.columnStart < intersection.columnEnd
218
+ ? intersection
219
+ : null;
220
+ }
221
+
222
+ export function spreadsheetCanvasDevicePixelRatio(value: number): number {
223
+ if (!Number.isFinite(value) || value <= 0) return 1;
224
+ return Math.max(1, Math.min(MAX_DEVICE_PIXEL_RATIO, value));
225
+ }
226
+
227
+ /** Caps backing-store allocation while retaining the full CSS viewport. */
228
+ export function spreadsheetCanvasBackingScale(
229
+ width: number,
230
+ height: number,
231
+ devicePixelRatio: number,
232
+ ): number {
233
+ const safeWidth = finiteExtent(width);
234
+ const safeHeight = finiteExtent(height);
235
+ const requested = spreadsheetCanvasDevicePixelRatio(devicePixelRatio);
236
+ return Math.max(
237
+ Number.EPSILON,
238
+ Math.min(
239
+ requested,
240
+ MAX_CANVAS_EDGE / safeWidth,
241
+ MAX_CANVAS_EDGE / safeHeight,
242
+ Math.sqrt(MAX_CANVAS_PIXELS / (safeWidth * safeHeight)),
243
+ ),
244
+ );
245
+ }
246
+
247
+ /** Retained 2D tile renderer. Dynamic selection and headers are painted above cached body tiles. */
248
+ export class SpreadsheetCanvasRenderer {
249
+ private readonly tiles = new Map<string, Tile>();
250
+ private generation = "";
251
+ private clock = 0;
252
+
253
+ dispose(): void {
254
+ this.tiles.clear();
255
+ this.generation = "";
256
+ }
257
+
258
+ paint(input: SpreadsheetCanvasPaint): SpreadsheetCanvasPaintStats | null {
259
+ const context = safeCanvasContext(input.canvas);
260
+ if (!context) return null;
261
+
262
+ const width = finiteExtent(input.viewportWidth);
263
+ const height = finiteExtent(input.viewportHeight);
264
+ const dpr = spreadsheetCanvasBackingScale(width, height, input.devicePixelRatio);
265
+ const pixelWidth = Math.max(1, Math.min(MAX_CANVAS_EDGE, Math.round(width * dpr)));
266
+ const pixelHeight = Math.max(1, Math.min(MAX_CANVAS_EDGE, Math.round(height * dpr)));
267
+ if (input.canvas.width !== pixelWidth) input.canvas.width = pixelWidth;
268
+ if (input.canvas.height !== pixelHeight) input.canvas.height = pixelHeight;
269
+ input.canvas.style.width = `${width}px`;
270
+ input.canvas.style.height = `${height}px`;
271
+ context.setTransform(dpr, 0, 0, dpr, 0, 0);
272
+ context.clearRect(0, 0, width, height);
273
+
274
+ const generation = [
275
+ input.projection.sheetId,
276
+ input.projection.generationId ?? "<none>",
277
+ input.projection.revision,
278
+ input.dimensionRevision,
279
+ dpr,
280
+ input.theme.signature,
281
+ ].join("/");
282
+ if (generation !== this.generation) {
283
+ this.tiles.clear();
284
+ this.generation = generation;
285
+ }
286
+
287
+ setFill(context, input.theme.background, "#ffffff");
288
+ context.fillRect(0, 0, width, height);
289
+
290
+ const bodyLeft = input.rowHeaderWidth;
291
+ const bodyTop = input.columnHeaderHeight;
292
+ const visibleLeft = clamp(input.logicalScrollLeft, 0, input.columns.total);
293
+ const visibleTop = clamp(input.logicalScrollTop, 0, input.rows.total);
294
+ const visibleRight = clamp(
295
+ input.logicalScrollLeft + width - bodyLeft,
296
+ visibleLeft,
297
+ input.columns.total,
298
+ );
299
+ const visibleBottom = clamp(
300
+ input.logicalScrollTop + height - bodyTop,
301
+ visibleTop,
302
+ input.rows.total,
303
+ );
304
+ let paintedTiles = 0;
305
+ let reusedTiles = 0;
306
+ let uncachedTiles = 0;
307
+
308
+ context.save();
309
+ context.beginPath();
310
+ context.rect(bodyLeft, bodyTop, Math.max(0, width - bodyLeft), Math.max(0, height - bodyTop));
311
+ context.clip();
312
+ const firstTileColumn = Math.floor(visibleLeft / TILE_SIZE);
313
+ const lastTileColumn = Math.floor(
314
+ Math.max(visibleLeft, visibleRight - Number.EPSILON) / TILE_SIZE,
315
+ );
316
+ const firstTileRow = Math.floor(visibleTop / TILE_SIZE);
317
+ const lastTileRow = Math.floor(
318
+ Math.max(visibleTop, visibleBottom - Number.EPSILON) / TILE_SIZE,
319
+ );
320
+ let visitedTiles = 0;
321
+ tileLoop: for (let tileRow = firstTileRow; tileRow <= lastTileRow; tileRow += 1) {
322
+ for (let tileColumn = firstTileColumn; tileColumn <= lastTileColumn; tileColumn += 1) {
323
+ if (visitedTiles >= MAX_TILES_PER_PAINT) break tileLoop;
324
+ visitedTiles += 1;
325
+ const key = `${tileColumn}:${tileRow}`;
326
+ let tile = this.tiles.get(key);
327
+ if (!tile) {
328
+ const painted = this.paintTile(input, tileColumn, tileRow, dpr);
329
+ tile = {
330
+ canvas: painted.canvas,
331
+ lastUsed: ++this.clock,
332
+ };
333
+ if (painted.cacheable) this.tiles.set(key, tile);
334
+ else uncachedTiles += 1;
335
+ paintedTiles += 1;
336
+ } else {
337
+ tile.lastUsed = ++this.clock;
338
+ reusedTiles += 1;
339
+ }
340
+ const logicalX = tileColumn * TILE_SIZE;
341
+ const logicalY = tileRow * TILE_SIZE;
342
+ context.drawImage(
343
+ tile.canvas,
344
+ bodyLeft + logicalX - input.logicalScrollLeft,
345
+ bodyTop + logicalY - input.logicalScrollTop,
346
+ TILE_SIZE,
347
+ TILE_SIZE,
348
+ );
349
+ }
350
+ }
351
+
352
+ paintSelection(context, input);
353
+ paintVisibleGrid(context, input, visibleLeft, visibleTop, visibleRight, visibleBottom);
354
+ context.restore();
355
+ paintHeaders(
356
+ context,
357
+ input,
358
+ width,
359
+ height,
360
+ visibleLeft,
361
+ visibleTop,
362
+ visibleRight,
363
+ visibleBottom,
364
+ );
365
+
366
+ this.evictTiles(dpr);
367
+ return {
368
+ cacheSize: this.tiles.size,
369
+ paintedTiles,
370
+ reusedTiles,
371
+ uncachedTiles,
372
+ devicePixelRatio: dpr,
373
+ };
374
+ }
375
+
376
+ private paintTile(
377
+ input: SpreadsheetCanvasPaint,
378
+ tileColumn: number,
379
+ tileRow: number,
380
+ dpr: number,
381
+ ): { canvas: HTMLCanvasElement; cacheable: boolean } {
382
+ const tile = input.canvas.ownerDocument.createElement("canvas");
383
+ tile.width = Math.max(1, Math.round(TILE_SIZE * dpr));
384
+ tile.height = Math.max(1, Math.round(TILE_SIZE * dpr));
385
+ const context = safeCanvasContext(tile);
386
+ if (!context) return { canvas: tile, cacheable: false };
387
+ context.setTransform(dpr, 0, 0, dpr, 0, 0);
388
+ setFill(context, input.theme.background, "#ffffff");
389
+ context.fillRect(0, 0, TILE_SIZE, TILE_SIZE);
390
+
391
+ const tileLeft = tileColumn * TILE_SIZE;
392
+ const tileTop = tileRow * TILE_SIZE;
393
+ const tileRight = Math.min(input.columns.total, tileLeft + TILE_SIZE);
394
+ const tileBottom = Math.min(input.rows.total, tileTop + TILE_SIZE);
395
+ const firstColumn = input.columns.indexAtOffset(tileLeft);
396
+ const lastColumn = input.columns.indexAtOffset(Math.max(tileLeft, tileRight - Number.EPSILON));
397
+ const firstRow = input.rows.indexAtOffset(tileTop);
398
+ const lastRow = input.rows.indexAtOffset(Math.max(tileTop, tileBottom - Number.EPSILON));
399
+ const query = {
400
+ rowStart: firstRow,
401
+ rowEnd: Math.min(input.rows.count, lastRow + 1),
402
+ columnStart: firstColumn,
403
+ columnEnd: Math.min(input.columns.count, lastColumn + 1),
404
+ };
405
+ const coveredQuery = intersectCoverage(query, input.projection.coverage);
406
+ const cacheable = coverageContains(input.projection.coverage, query);
407
+ const cells: SpreadsheetCanvasCell[] = [];
408
+ if (coveredQuery) {
409
+ input.projection.cells.forEachInRange(
410
+ coveredQuery.rowStart,
411
+ coveredQuery.rowEnd,
412
+ coveredQuery.columnStart,
413
+ coveredQuery.columnEnd,
414
+ (cell) => cells.push(cell),
415
+ );
416
+ }
417
+
418
+ for (const cell of cells) {
419
+ const fill = cell.format.fill;
420
+ if (!fill) continue;
421
+ const x = input.columns.offsetAt(cell.col) - tileLeft;
422
+ const y = input.rows.offsetAt(cell.row) - tileTop;
423
+ const width = input.columns.sizeAt(cell.col);
424
+ const height = input.rows.sizeAt(cell.row);
425
+ setFill(context, fill, input.theme.background);
426
+ context.fillRect(x, y, width, height);
427
+ }
428
+
429
+ paintTileGrid(context, input, tileLeft, tileTop, tileRight, tileBottom);
430
+
431
+ for (const cell of cells) {
432
+ const value = input.projection.valueAt(cell);
433
+ const text = displayValue(value);
434
+ if (!text) continue;
435
+ paintCellText(context, input, cell, text, tileLeft, tileTop);
436
+ }
437
+
438
+ return { canvas: tile, cacheable };
439
+ }
440
+
441
+ private evictTiles(dpr: number): void {
442
+ const pixelsPerTile = Math.max(1, Math.round(TILE_SIZE * dpr)) ** 2;
443
+ const maximum = Math.max(
444
+ 4,
445
+ Math.min(MAX_CACHE_TILES, Math.floor(MAX_CACHE_PHYSICAL_PIXELS / pixelsPerTile)),
446
+ );
447
+ if (this.tiles.size <= maximum) return;
448
+ const oldest = [...this.tiles.entries()].sort(
449
+ (left, right) => left[1].lastUsed - right[1].lastUsed,
450
+ );
451
+ for (let index = 0; index < oldest.length - maximum; index += 1) {
452
+ this.tiles.delete(oldest[index]![0]);
453
+ }
454
+ }
455
+ }
456
+
457
+ function finiteExtent(value: number): number {
458
+ if (!Number.isFinite(value) || value <= 0) return 1;
459
+ return Math.max(1, Math.floor(value));
460
+ }
461
+
462
+ function safeCanvasContext(canvas: HTMLCanvasElement): CanvasRenderingContext2D | null {
463
+ try {
464
+ return canvas.getContext("2d", { alpha: false });
465
+ } catch {
466
+ return null;
467
+ }
468
+ }
469
+
470
+ function setFill(context: CanvasRenderingContext2D, value: string, fallback: string): void {
471
+ context.fillStyle = fallback;
472
+ try {
473
+ context.fillStyle = value;
474
+ } catch {
475
+ // Invalid workbook colors behave like invalid CSS declarations: ignore them.
476
+ }
477
+ }
478
+
479
+ function setStroke(context: CanvasRenderingContext2D, value: string, fallback: string): void {
480
+ context.strokeStyle = fallback;
481
+ try {
482
+ context.strokeStyle = value;
483
+ } catch {
484
+ // Invalid workbook colors behave like invalid CSS declarations: ignore them.
485
+ }
486
+ }
487
+
488
+ function paintTileGrid(
489
+ context: CanvasRenderingContext2D,
490
+ input: SpreadsheetCanvasPaint,
491
+ tileLeft: number,
492
+ tileTop: number,
493
+ tileRight: number,
494
+ tileBottom: number,
495
+ ): void {
496
+ setFill(context, input.theme.border, "#d1d5db");
497
+ visitAxisBoundaries(input.columns, tileLeft, tileRight, (offset) => {
498
+ context.fillRect(offset - tileLeft - 1, 0, 1, TILE_SIZE);
499
+ });
500
+ visitAxisBoundaries(input.rows, tileTop, tileBottom, (offset) => {
501
+ context.fillRect(0, offset - tileTop - 1, TILE_SIZE, 1);
502
+ });
503
+ }
504
+
505
+ function paintVisibleGrid(
506
+ context: CanvasRenderingContext2D,
507
+ input: SpreadsheetCanvasPaint,
508
+ visibleLeft: number,
509
+ visibleTop: number,
510
+ visibleRight: number,
511
+ visibleBottom: number,
512
+ ): void {
513
+ setFill(context, input.theme.border, "#d1d5db");
514
+ visitAxisBoundaries(input.columns, visibleLeft, visibleRight, (offset) => {
515
+ const x = input.rowHeaderWidth + offset - input.logicalScrollLeft;
516
+ context.fillRect(x - 1, input.columnHeaderHeight, 1, input.viewportHeight);
517
+ });
518
+ visitAxisBoundaries(input.rows, visibleTop, visibleBottom, (offset) => {
519
+ const y = input.columnHeaderHeight + offset - input.logicalScrollTop;
520
+ context.fillRect(input.rowHeaderWidth, y - 1, input.viewportWidth, 1);
521
+ });
522
+ }
523
+
524
+ function visitAxisBoundaries(
525
+ axis: SpreadsheetCanvasAxis,
526
+ start: number,
527
+ end: number,
528
+ visit: (offset: number) => void,
529
+ ): void {
530
+ if (axis.count <= 0 || end < start) return;
531
+ let index = axis.indexAtOffset(start);
532
+ let visits = 0;
533
+ while (index < axis.count && visits < MAX_AXIS_MARKS_PER_PAINT) {
534
+ const boundary = axis.offsetAt(index + 1);
535
+ if (boundary > end + 1) break;
536
+ if (boundary >= start) visit(boundary);
537
+ index += 1;
538
+ visits += 1;
539
+ }
540
+ }
541
+
542
+ function paintSelection(context: CanvasRenderingContext2D, input: SpreadsheetCanvasPaint): void {
543
+ const { selection } = input;
544
+ const left =
545
+ input.rowHeaderWidth + input.columns.offsetAt(selection.left) - input.logicalScrollLeft;
546
+ const right =
547
+ input.rowHeaderWidth + input.columns.offsetAt(selection.right + 1) - input.logicalScrollLeft;
548
+ const top =
549
+ input.columnHeaderHeight + input.rows.offsetAt(selection.top) - input.logicalScrollTop;
550
+ const bottom =
551
+ input.columnHeaderHeight + input.rows.offsetAt(selection.bottom + 1) - input.logicalScrollTop;
552
+ context.save();
553
+ setFill(context, input.theme.accent, "#3b82f6");
554
+ context.globalAlpha = 0.1;
555
+ context.fillRect(left, top, right - left, bottom - top);
556
+ context.restore();
557
+
558
+ const activeLeft =
559
+ input.rowHeaderWidth + input.columns.offsetAt(selection.focusColumn) - input.logicalScrollLeft;
560
+ const activeTop =
561
+ input.columnHeaderHeight + input.rows.offsetAt(selection.focusRow) - input.logicalScrollTop;
562
+ const activeWidth = input.columns.sizeAt(selection.focusColumn);
563
+ const activeHeight = input.rows.sizeAt(selection.focusRow);
564
+ context.save();
565
+ setStroke(context, input.theme.accent, "#3b82f6");
566
+ context.lineWidth = 2;
567
+ context.strokeRect(
568
+ activeLeft + 1,
569
+ activeTop + 1,
570
+ Math.max(0, activeWidth - 2),
571
+ Math.max(0, activeHeight - 2),
572
+ );
573
+ context.restore();
574
+ }
575
+
576
+ function paintHeaders(
577
+ context: CanvasRenderingContext2D,
578
+ input: SpreadsheetCanvasPaint,
579
+ width: number,
580
+ height: number,
581
+ visibleLeft: number,
582
+ visibleTop: number,
583
+ visibleRight: number,
584
+ visibleBottom: number,
585
+ ): void {
586
+ setFill(context, input.theme.headerBackground, "#f3f4f6");
587
+ context.fillRect(0, 0, width, input.columnHeaderHeight);
588
+ context.fillRect(0, input.columnHeaderHeight, input.rowHeaderWidth, height);
589
+ setFill(context, input.theme.border, "#d1d5db");
590
+ context.fillRect(0, input.columnHeaderHeight - 1, width, 1);
591
+ context.fillRect(input.rowHeaderWidth - 1, 0, 1, height);
592
+
593
+ context.save();
594
+ context.beginPath();
595
+ context.rect(
596
+ input.rowHeaderWidth,
597
+ 0,
598
+ Math.max(0, width - input.rowHeaderWidth),
599
+ input.columnHeaderHeight,
600
+ );
601
+ context.clip();
602
+ setFill(context, input.theme.mutedForeground, "#6b7280");
603
+ context.font = `600 ${input.theme.headerFontSize}px ${input.theme.fontFamily}`;
604
+ context.textAlign = "center";
605
+ context.textBaseline = "middle";
606
+ visitAxisItems(input.columns, visibleLeft, visibleRight, (column, offset, size) => {
607
+ const x = input.rowHeaderWidth + offset - input.logicalScrollLeft;
608
+ setFill(context, input.theme.border, "#d1d5db");
609
+ context.fillRect(x + size - 1, 0, 1, input.columnHeaderHeight);
610
+ if (size >= 8) {
611
+ setFill(context, input.theme.mutedForeground, "#6b7280");
612
+ context.fillText(columnName(column), x + size / 2, input.columnHeaderHeight / 2);
613
+ }
614
+ });
615
+ context.restore();
616
+
617
+ context.save();
618
+ context.beginPath();
619
+ context.rect(
620
+ 0,
621
+ input.columnHeaderHeight,
622
+ input.rowHeaderWidth,
623
+ Math.max(0, height - input.columnHeaderHeight),
624
+ );
625
+ context.clip();
626
+ setFill(context, input.theme.mutedForeground, "#6b7280");
627
+ context.font = `400 ${input.theme.headerFontSize}px ${input.theme.fontFamily}`;
628
+ context.textAlign = "center";
629
+ context.textBaseline = "middle";
630
+ visitAxisItems(input.rows, visibleTop, visibleBottom, (row, offset, size) => {
631
+ const y = input.columnHeaderHeight + offset - input.logicalScrollTop;
632
+ setFill(context, input.theme.border, "#d1d5db");
633
+ context.fillRect(0, y + size - 1, input.rowHeaderWidth, 1);
634
+ if (size >= 8) {
635
+ setFill(context, input.theme.mutedForeground, "#6b7280");
636
+ context.fillText(String(row + 1), input.rowHeaderWidth / 2, y + size / 2);
637
+ }
638
+ });
639
+ context.restore();
640
+ }
641
+
642
+ function visitAxisItems(
643
+ axis: SpreadsheetCanvasAxis,
644
+ start: number,
645
+ end: number,
646
+ visit: (index: number, offset: number, size: number) => void,
647
+ ): void {
648
+ if (axis.count <= 0 || end < start) return;
649
+ let index = axis.indexAtOffset(start);
650
+ let visits = 0;
651
+ while (index < axis.count && visits < MAX_AXIS_MARKS_PER_PAINT) {
652
+ const offset = axis.offsetAt(index);
653
+ if (offset > end) break;
654
+ visit(index, offset, axis.sizeAt(index));
655
+ index += 1;
656
+ visits += 1;
657
+ }
658
+ }
659
+
660
+ function paintCellText(
661
+ context: CanvasRenderingContext2D,
662
+ input: SpreadsheetCanvasPaint,
663
+ cell: SpreadsheetCanvasCell,
664
+ text: string,
665
+ tileLeft: number,
666
+ tileTop: number,
667
+ ): void {
668
+ const format = cell.format;
669
+ const x = input.columns.offsetAt(cell.col) - tileLeft;
670
+ const y = input.rows.offsetAt(cell.row) - tileTop;
671
+ const width = input.columns.sizeAt(cell.col);
672
+ const height = input.rows.sizeAt(cell.row);
673
+ if (width <= 2 || height <= 2) return;
674
+ const fontSize = finitePositive(format.font?.size, input.theme.fontSize);
675
+ const fontFamily = format.font?.name || input.theme.fontFamily;
676
+ const weight = format.font?.bold ? 600 : 400;
677
+ const italic = format.font?.italic ? "italic " : "";
678
+ context.save();
679
+ context.beginPath();
680
+ context.rect(x + 1, y + 1, Math.max(0, width - 2), Math.max(0, height - 2));
681
+ context.clip();
682
+ context.font = `${italic}${weight} ${fontSize}px ${fontFamily}`;
683
+ setFill(
684
+ context,
685
+ format.font?.color ?? (text.startsWith("#") ? input.theme.error : input.theme.foreground),
686
+ input.theme.foreground,
687
+ );
688
+ const alignment = format.horizontalAlignment;
689
+ context.textAlign = alignment === "center" ? "center" : alignment === "right" ? "right" : "left";
690
+ context.textBaseline = "alphabetic";
691
+ const padding = 6;
692
+ const textX =
693
+ alignment === "center"
694
+ ? x + width / 2
695
+ : alignment === "right"
696
+ ? x + width - padding
697
+ : x + padding;
698
+ const lineHeight = fontSize * 1.5;
699
+ const availableWidth = Math.max(0, width - padding * 2);
700
+ const lines = format.wrapText
701
+ ? wrapText(context, text, availableWidth, Math.max(1, Math.floor(height / lineHeight)))
702
+ : [ellipsize(context, text.replaceAll(/\r?\n/g, " "), availableWidth)];
703
+ const blockHeight = lines.length * lineHeight;
704
+ const vertical = format.verticalAlignment;
705
+ const firstBaseline =
706
+ vertical === "top"
707
+ ? y + Math.min(height, padding + fontSize)
708
+ : vertical === "bottom"
709
+ ? y + height - padding - blockHeight + fontSize
710
+ : y + (height - blockHeight) / 2 + fontSize;
711
+ for (let index = 0; index < lines.length; index += 1) {
712
+ const line = lines[index]!;
713
+ const baseline = firstBaseline + index * lineHeight;
714
+ context.fillText(line, textX, baseline);
715
+ if (format.font?.underline && line) {
716
+ const metrics = context.measureText(line);
717
+ const textWidth = metrics.width;
718
+ const startX =
719
+ alignment === "center"
720
+ ? textX - textWidth / 2
721
+ : alignment === "right"
722
+ ? textX - textWidth
723
+ : textX;
724
+ setFill(context, format.font.color ?? input.theme.foreground, input.theme.foreground);
725
+ context.fillRect(startX, baseline + 1, textWidth, Math.max(1, fontSize / 14));
726
+ }
727
+ }
728
+ context.restore();
729
+ }
730
+
731
+ function wrapText(
732
+ context: CanvasRenderingContext2D,
733
+ input: string,
734
+ maximumWidth: number,
735
+ maximumLines: number,
736
+ ): string[] {
737
+ if (maximumWidth <= 0) return [""];
738
+ const output: string[] = [];
739
+ for (const paragraph of input.split(/\r?\n/)) {
740
+ const words = paragraph.split(/\s+/).filter(Boolean);
741
+ if (words.length === 0) output.push("");
742
+ let line = "";
743
+ for (const word of words) {
744
+ const next = line ? `${line} ${word}` : word;
745
+ if (!line || context.measureText(next).width <= maximumWidth) line = next;
746
+ else {
747
+ output.push(ellipsize(context, line, maximumWidth));
748
+ line = word;
749
+ }
750
+ if (output.length >= maximumLines) break;
751
+ }
752
+ if (output.length < maximumLines && line) output.push(ellipsize(context, line, maximumWidth));
753
+ if (output.length >= maximumLines) break;
754
+ }
755
+ return output.slice(0, maximumLines);
756
+ }
757
+
758
+ function ellipsize(context: CanvasRenderingContext2D, input: string, maximumWidth: number): string {
759
+ if (!input || maximumWidth <= 0) return "";
760
+ if (context.measureText(input).width <= maximumWidth) return input;
761
+ const suffix = "…";
762
+ const suffixWidth = context.measureText(suffix).width;
763
+ if (suffixWidth > maximumWidth) return "";
764
+ let low = 0;
765
+ let high = input.length;
766
+ while (low < high) {
767
+ const middle = Math.ceil((low + high) / 2);
768
+ if (context.measureText(`${input.slice(0, middle)}${suffix}`).width <= maximumWidth)
769
+ low = middle;
770
+ else high = middle - 1;
771
+ }
772
+ return `${input.slice(0, low)}${suffix}`;
773
+ }
774
+
775
+ function finitePositive(value: number | undefined, fallback: number): number {
776
+ return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
777
+ }
778
+
779
+ function displayValue(value: unknown): string {
780
+ if (value === null || value === undefined) return "";
781
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? "" : value.toLocaleDateString();
782
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
783
+ return String(value);
784
+ }
785
+
786
+ function columnName(index: number): string {
787
+ let value = index + 1;
788
+ let result = "";
789
+ while (value > 0) {
790
+ const remainder = (value - 1) % 26;
791
+ result = String.fromCharCode(65 + remainder) + result;
792
+ value = Math.floor((value - 1) / 26);
793
+ }
794
+ return result;
795
+ }
796
+
797
+ function clamp(value: number, minimum: number, maximum: number): number {
798
+ return Math.max(minimum, Math.min(maximum, value));
799
+ }