@marimo-team/frontend 0.24.1-dev2 → 0.24.1-dev20

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 (104) hide show
  1. package/dist/assets/{CellStatus-DqJypoFY.js → CellStatus-B_dBNTJO.js} +1 -1
  2. package/dist/assets/{JsonOutput-DnrBQBRT.js → JsonOutput-C5g_bqiZ.js} +1 -1
  3. package/dist/assets/{MarimoErrorOutput-BVdFtnJz.js → MarimoErrorOutput-Cb0evuEp.js} +1 -1
  4. package/dist/assets/{RenderHTML-X9kskQZ0.js → RenderHTML-BoN8Qu7U.js} +1 -1
  5. package/dist/assets/{RunButton-D83R2wvd.js → RunButton-C3V3v58F.js} +1 -1
  6. package/dist/assets/{add-cell-with-ai-HD16K3PB.js → add-cell-with-ai-C5JH9LS2.js} +1 -1
  7. package/dist/assets/{add-connection-dialog-V86wrXca.js → add-connection-dialog-DHyonb9T.js} +1 -1
  8. package/dist/assets/{agent-panel-XdLSrKjl.js → agent-panel-D7DLUBam.js} +1 -1
  9. package/dist/assets/{ai-model-dropdown-BJp70TST.js → ai-model-dropdown-DWDm50eM.js} +4 -4
  10. package/dist/assets/app-config-button-DhZke6rt.js +1 -0
  11. package/dist/assets/{blob-BKW9TqBu.js → blob-XD7AvCQ1.js} +1 -1
  12. package/dist/assets/{cell-editor-DQSrfeL3.js → cell-editor-DZg8uKWr.js} +14 -14
  13. package/dist/assets/{cell-link-D26WjdN1.js → cell-link-WDIIwzUn.js} +1 -1
  14. package/dist/assets/{cells-DTFqEQb0.js → cells-BvgwUKB9.js} +1 -1
  15. package/dist/assets/{chat-display-CclXFQQP.js → chat-display-CR9X_gwB.js} +1 -1
  16. package/dist/assets/{chat-panel-ft0VIKS8.js → chat-panel-DnGBFlLk.js} +1 -1
  17. package/dist/assets/{chat-ui-DUYenr6D.js → chat-ui-BVFULpKr.js} +1 -1
  18. package/dist/assets/{column-preview-BLap3eAk.js → column-preview-DMqGohE5.js} +1 -1
  19. package/dist/assets/{command-palette-C7nz0zHt.js → command-palette-fK5U3VCd.js} +1 -1
  20. package/dist/assets/{common-DKQZgzC9.js → common-Dx_4ekWj.js} +1 -1
  21. package/dist/assets/{components-Bf_hykBb.js → components-DVs9Yu7X.js} +1 -1
  22. package/dist/assets/config-DFK5eCC3.js +1 -0
  23. package/dist/assets/{datasource-DZgUyqAK.js → datasource-Le540Ng6.js} +1 -1
  24. package/dist/assets/{dependency-graph-panel-CH6kIoat.js → dependency-graph-panel-BsdBM_AS.js} +1 -1
  25. package/dist/assets/{documentation-panel-BQYA8gCW.js → documentation-panel-RJNMjUK6.js} +1 -1
  26. package/dist/assets/{download-_oAlkvj6.js → download-B8NwKEtr.js} +1 -1
  27. package/dist/assets/{edit-page-DnXTBsfC.js → edit-page-DUnHDaYA.js} +6 -6
  28. package/dist/assets/{error-entries-uxjSsTal.js → error-entries-yYB8PZoo.js} +1 -1
  29. package/dist/assets/{error-panel-Bq0TKquh.js → error-panel-Dr928gBs.js} +1 -1
  30. package/dist/assets/{file-explorer-panel-uhg7VNMd.js → file-explorer-panel-Brf39GIG.js} +1 -1
  31. package/dist/assets/{file-icons-V3puz20c.js → file-icons-D39zbjsS.js} +1 -1
  32. package/dist/assets/{file-name-input-DWf6YZwM.js → file-name-input-BLH5fTEA.js} +1 -1
  33. package/dist/assets/{floating-outline-Vqr5D17R.js → floating-outline-UPrDtFZF.js} +1 -1
  34. package/dist/assets/{focus-BTlyH5K_.js → focus-BArgHYf8.js} +1 -1
  35. package/dist/assets/{form-CRSnWxLR.js → form-BN1AKD58.js} +1 -1
  36. package/dist/assets/{form-Bb1JVuzT.js → form-BvzcNsxB.js} +1 -1
  37. package/dist/assets/{home-page-RduyhdYP.js → home-page-Dwl_Carc.js} +1 -1
  38. package/dist/assets/{html-to-image-BsXiWo0v.js → html-to-image-DKEUbBwc.js} +1 -1
  39. package/dist/assets/index-Dqr3-dR1.js +38 -0
  40. package/dist/assets/{index-WoaGtlKd.css → index-n_AVX7aW.css} +1 -1
  41. package/dist/assets/{kiosk-mode-BH15-Gn-.js → kiosk-mode-B8UhlOLw.js} +1 -1
  42. package/dist/assets/{layout-1X5SpE18.js → layout-YbL5eg7A.js} +3 -3
  43. package/dist/assets/{logs-panel-CVxksxou.js → logs-panel-DItocggZ.js} +1 -1
  44. package/dist/assets/{markdown-renderer-BjuEVUOa.js → markdown-renderer-D7RLn6TA.js} +1 -1
  45. package/dist/assets/{name-cell-input-CtYGGTR5.js → name-cell-input-jausH-CL.js} +1 -1
  46. package/dist/assets/{outline-panel-BhxoKAbS.js → outline-panel-C_pASL6b.js} +1 -1
  47. package/dist/assets/{packages-panel-BWP3YWNW.js → packages-panel-ItQn6qH4.js} +1 -1
  48. package/dist/assets/{pair-with-agent-modal-BCMZbqSR.js → pair-with-agent-modal-DiDLAjxV.js} +1 -1
  49. package/dist/assets/{panel-accordion-state-0CKLFeAf.js → panel-accordion-state-DaMmC7UV.js} +1 -1
  50. package/dist/assets/{panels-D40pa9F1.js → panels-Bhzy-m2X.js} +1 -1
  51. package/dist/assets/{process-output-WaMx3G7b.js → process-output-ClE9lj2U.js} +1 -1
  52. package/dist/assets/{radio-group-5KDs5aO1.js → radio-group-CSwRGU0A.js} +1 -1
  53. package/dist/assets/{readonly-code-display-DnHcjfFl.js → readonly-code-display-kRUXu5aJ.js} +1 -1
  54. package/dist/assets/{readonly-python-code-BV8otm7e.js → readonly-python-code-BCOkadQf.js} +1 -1
  55. package/dist/assets/{reveal-component-yVci-fc9.js → reveal-component-CnIgubBp.js} +1 -1
  56. package/dist/assets/{run-page-aQycuCjn.js → run-page-Du8ylzny.js} +1 -1
  57. package/dist/assets/{scratchpad-panel-lsqN24Au.js → scratchpad-panel-BEn7TrAb.js} +1 -1
  58. package/dist/assets/{session-panel-C4JUkPt7.js → session-panel-CidCOM4_.js} +1 -1
  59. package/dist/assets/{skeleton-DOZFvsSE.js → skeleton-CL2xKYhl.js} +1 -1
  60. package/dist/assets/{snippets-panel-QNY_Dl2J.js → snippets-panel-V4x4wLMQ.js} +1 -1
  61. package/dist/assets/state-C7arWhwL.js +3 -0
  62. package/dist/assets/{state-ByxLiTvP.js → state-m2zUPztG.js} +1 -1
  63. package/dist/assets/{switch-CDT2KH2g.js → switch-ulnbZFaI.js} +1 -1
  64. package/dist/assets/{terminal-UyHT9ie2.js → terminal-Bo6pnmSU.js} +1 -1
  65. package/dist/assets/{tracing-BiBPyXVg.js → tracing-Cmwuiwj9.js} +1 -1
  66. package/dist/assets/{tracing-panel-D8LtcU_g.js → tracing-panel-CCXeYfOu.js} +2 -2
  67. package/dist/assets/{tree-actions-DUAdNn1i.js → tree-actions-QMLf8Qhz.js} +1 -1
  68. package/dist/assets/{useBoolean-CgN_4RAl.js → useBoolean-C_UV6tQ7.js} +1 -1
  69. package/dist/assets/{useCellActionButton-Bp2eEIzO.js → useCellActionButton-D8qyXNKv.js} +1 -1
  70. package/dist/assets/{useDeleteCell-DO-9USfB.js → useDeleteCell-Ctnxl8AT.js} +1 -1
  71. package/dist/assets/{useDependencyPanelTab-Bi8pUBnn.js → useDependencyPanelTab-CYSEZi6j.js} +1 -1
  72. package/dist/assets/useNotebookActions-CblNR75b.js +12 -0
  73. package/dist/assets/{useRunCells-BpZ_0Xuz.js → useRunCells-DjQN3XYP.js} +1 -1
  74. package/dist/assets/{useSplitCell-C2fjcfID.js → useSplitCell-BG8Zd4lw.js} +1 -1
  75. package/dist/assets/{vega-component-3G_aA1Wa.js → vega-component-1VoQOWvU.js} +1 -1
  76. package/dist/index.html +27 -27
  77. package/package.json +1 -1
  78. package/src/__mocks__/requests.ts +4 -0
  79. package/src/components/app-config/optional-features.test.ts +78 -0
  80. package/src/components/app-config/optional-features.tsx +33 -6
  81. package/src/components/editor/actions/export-dialog/__tests__/export-dialog.test.tsx +206 -34
  82. package/src/components/editor/actions/export-dialog/export-dialog.tsx +9 -3
  83. package/src/components/editor/actions/export-dialog/format-notice.tsx +69 -24
  84. package/src/components/editor/actions/export-dialog/use-export-dialog.ts +27 -2
  85. package/src/components/ui/toaster.tsx +14 -1
  86. package/src/core/codemirror/code-lens/__tests__/popover.test.ts +13 -0
  87. package/src/core/codemirror/code-lens/popover.tsx +11 -5
  88. package/src/core/islands/bridge.ts +1 -0
  89. package/src/core/mode.ts +6 -1
  90. package/src/core/network/__tests__/requests-lazy.test.ts +40 -0
  91. package/src/core/network/__tests__/requests-network.test.ts +13 -0
  92. package/src/core/network/requests-lazy.ts +11 -4
  93. package/src/core/network/requests-network.ts +8 -0
  94. package/src/core/network/requests-static.ts +1 -0
  95. package/src/core/network/requests-toasting.tsx +1 -0
  96. package/src/core/network/types.ts +5 -0
  97. package/src/core/wasm/bridge.ts +1 -0
  98. package/src/plugins/impl/MatrixPlugin.tsx +256 -35
  99. package/src/plugins/impl/__tests__/MatrixPlugin.test.tsx +540 -3
  100. package/dist/assets/app-config-button-C2zJXSrK.js +0 -1
  101. package/dist/assets/config-EoJoeZuK.js +0 -1
  102. package/dist/assets/index-C5WESBbZ.js +0 -38
  103. package/dist/assets/state-Ci7n55A9.js +0 -3
  104. package/dist/assets/useNotebookActions-Bm9VzJ_M.js +0 -12
@@ -54,6 +54,40 @@ export class MatrixPlugin implements IPlugin<T, Data> {
54
54
  }
55
55
 
56
56
  const PIXELS_PER_STEP = 10;
57
+ const COARSE_MULTIPLIER = 10;
58
+ const FINE_MULTIPLIER = 0.1;
59
+
60
+ /** Step multiplier from modifier keys: shift = coarse, alt = fine. */
61
+ const stepMultiplier = (e: { shiftKey: boolean; altKey: boolean }): number => {
62
+ if (e.shiftKey) {
63
+ return COARSE_MULTIPLIER;
64
+ }
65
+ if (e.altKey) {
66
+ return FINE_MULTIPLIER;
67
+ }
68
+ return 1;
69
+ };
70
+
71
+ /**
72
+ * Strip floating-point noise from step arithmetic (e.g. 3 * 0.1).
73
+ * The noise tolerance scales with the step so that legitimately tiny
74
+ * results (e.g. from a 2e-14 step) are preserved, while noise left over
75
+ * from arithmetic on ordinary magnitudes (e.g. the 5.55e-17 residue of
76
+ * 0.3 - 3 * 0.1) still snaps away.
77
+ */
78
+ const cleanFloat = (x: number, step: number): number => {
79
+ const rounded = Number(x.toFixed(12));
80
+ const tolerance =
81
+ Number.EPSILON * Math.max(Math.abs(x), Math.abs(step)) * 100;
82
+ return Math.abs(x - rounded) <= tolerance ? rounded : x;
83
+ };
84
+
85
+ interface EditState {
86
+ row: number;
87
+ col: number;
88
+ text: string;
89
+ selectAll: boolean;
90
+ }
57
91
 
58
92
  interface MatrixComponentProps extends Data {
59
93
  value: T;
@@ -80,6 +114,7 @@ const MatrixComponent = ({
80
114
  col: number;
81
115
  startX: number;
82
116
  startValue: number;
117
+ multiplier: number;
83
118
  } | null>(null);
84
119
  const [activeCell, setActiveCell] = useState<{
85
120
  row: number;
@@ -95,6 +130,16 @@ const MatrixComponent = ({
95
130
  }, [value]);
96
131
  const displayValue = activeCell == null ? value : draft;
97
132
 
133
+ // Editing state is mirrored in a ref so that the blur fired while
134
+ // committing (refocusing the cell unmounts the input) can't commit twice.
135
+ const [editing, setEditingState] = useState<EditState | null>(null);
136
+ const editingRef = useRef<EditState | null>(null);
137
+ const inputRef = useRef<HTMLInputElement | null>(null);
138
+ const setEditing = useCallback((edit: EditState | null) => {
139
+ editingRef.current = edit;
140
+ setEditingState(edit);
141
+ }, []);
142
+
98
143
  const formatValue = (val: number) =>
99
144
  scientific ? val.toExponential(precision) : val.toFixed(precision);
100
145
 
@@ -112,22 +157,131 @@ const MatrixComponent = ({
112
157
  [minValue, maxValue],
113
158
  );
114
159
 
160
+ // The single mutation chokepoint for every edit path (drag, arrow, type).
161
+ // Bounds are enforced here so no path can escape them. In symmetric mode
162
+ // the pair must stay equal *and* respect both cells' bounds, so the shared
163
+ // value is clamped to the intersection of the two ranges. Returns null
164
+ // when the clamped value is already in place, so callers can skip
165
+ // redundant updates.
166
+ const withCellValue = useCallback(
167
+ (base: T, row: number, col: number, newValue: number): T | null => {
168
+ const mirrored = symmetric && row !== col;
169
+ let clamped = clampValue(newValue, row, col);
170
+ if (mirrored) {
171
+ clamped = clampValue(clamped, col, row);
172
+ }
173
+ if (clamped === base[row][col]) {
174
+ return null;
175
+ }
176
+ const copy = base.map((r) => [...r]);
177
+ copy[row][col] = clamped;
178
+ if (mirrored) {
179
+ copy[col][row] = clamped;
180
+ }
181
+ return copy;
182
+ },
183
+ [symmetric, clampValue],
184
+ );
185
+
186
+ const startEditing = useCallback(
187
+ (row: number, col: number, seed?: string) => {
188
+ if (disabled[row][col]) {
189
+ return;
190
+ }
191
+ dragState.current = null;
192
+ setActiveCell(null);
193
+ setEditing({
194
+ row,
195
+ col,
196
+ text: seed ?? String(value[row][col]),
197
+ selectAll: seed == null,
198
+ });
199
+ },
200
+ [disabled, value, setEditing],
201
+ );
202
+
203
+ // Callback ref: focus the input when it mounts. When editing starts from
204
+ // a typed character, place the caret after the seed instead of selecting.
205
+ const focusInput = useCallback((el: HTMLInputElement | null) => {
206
+ inputRef.current = el;
207
+ if (el) {
208
+ el.focus();
209
+ if (editingRef.current?.selectAll) {
210
+ el.select();
211
+ } else {
212
+ el.setSelectionRange(el.value.length, el.value.length);
213
+ }
214
+ }
215
+ }, []);
216
+
217
+ const commitEdit = useCallback(
218
+ (refocusCell: boolean) => {
219
+ const edit = editingRef.current;
220
+ if (!edit) {
221
+ return;
222
+ }
223
+ const cell = refocusCell ? inputRef.current?.closest("td") : null;
224
+ setEditing(null);
225
+ const text = edit.text.trim();
226
+ const parsed = Number(text);
227
+ // Typed values are clamped to bounds but not snapped to `step`,
228
+ // so exact values like 2.32e7 survive.
229
+ if (text !== "" && Number.isFinite(parsed)) {
230
+ const copy = withCellValue(value, edit.row, edit.col, parsed);
231
+ if (copy) {
232
+ setDraft(copy);
233
+ setValue(copy);
234
+ }
235
+ }
236
+ cell?.focus();
237
+ },
238
+ [value, withCellValue, setValue, setEditing],
239
+ );
240
+
241
+ const cancelEdit = useCallback(() => {
242
+ const cell = inputRef.current?.closest("td");
243
+ setEditing(null);
244
+ cell?.focus();
245
+ }, [setEditing]);
246
+
247
+ const handleInputKeyDown = useCallback(
248
+ (e: React.KeyboardEvent) => {
249
+ e.stopPropagation();
250
+ if (e.key === "Enter") {
251
+ e.preventDefault();
252
+ commitEdit(true);
253
+ } else if (e.key === "Escape") {
254
+ e.preventDefault();
255
+ cancelEdit();
256
+ }
257
+ },
258
+ [commitEdit, cancelEdit],
259
+ );
260
+
115
261
  const handlePointerDown = useCallback(
116
- (e: React.PointerEvent, row: number, col: number) => {
117
- if (disabled[row][col] || !(e.target instanceof Element)) {
262
+ (e: React.PointerEvent<HTMLTableCellElement>, row: number, col: number) => {
263
+ if (
264
+ disabled[row][col] ||
265
+ editing != null ||
266
+ !(e.target instanceof Element)
267
+ ) {
118
268
  return;
119
269
  }
120
270
  e.preventDefault();
271
+ // preventDefault also suppresses the browser's click-to-focus, so
272
+ // focus the cell explicitly to keep keyboard stepping reachable.
273
+ e.currentTarget.focus();
121
274
  e.target.setPointerCapture(e.pointerId);
122
275
  dragState.current = {
123
276
  row,
124
277
  col,
125
278
  startX: e.clientX,
126
279
  startValue: displayValue[row][col],
280
+ multiplier: stepMultiplier(e),
127
281
  };
128
282
  setActiveCell({ row, col });
129
283
  },
130
- [disabled, displayValue],
284
+ [disabled, editing, displayValue],
131
285
  );
132
286
 
133
287
  const handlePointerMove = useCallback(
@@ -136,26 +290,32 @@ const MatrixComponent = ({
136
290
  if (!state) {
137
291
  return;
138
292
  }
293
+ const multiplier = stepMultiplier(e);
294
+ if (multiplier !== state.multiplier) {
295
+ // Rebase so toggling a modifier mid-drag rescales future movement
296
+ // instead of jumping the value.
297
+ state.startX = e.clientX;
298
+ state.startValue = displayValue[state.row][state.col];
299
+ state.multiplier = multiplier;
300
+ }
139
301
  const { row, col, startX, startValue } = state;
140
302
  const dx = e.clientX - startX;
141
- const cellStep = step[row][col];
303
+ const cellStep = step[row][col] * multiplier;
142
304
  const steps = Math.round(dx / PIXELS_PER_STEP);
143
- const rawValue = startValue + steps * cellStep;
144
- const newValue = clampValue(rawValue, row, col);
145
-
146
- if (newValue !== displayValue[row][col]) {
147
- const copy = displayValue.map((r) => [...r]);
148
- copy[row][col] = newValue;
149
- if (symmetric && row !== col) {
150
- copy[col][row] = newValue;
151
- }
305
+ const copy = withCellValue(
306
+ displayValue,
307
+ row,
308
+ col,
309
+ cleanFloat(startValue + steps * cellStep, cellStep),
310
+ );
311
+ if (copy) {
152
312
  setDraft(copy);
153
313
  if (!debounce) {
154
314
  setValue(copy);
155
315
  }
156
316
  }
157
317
  },
158
- [step, clampValue, displayValue, symmetric, debounce, setValue],
318
+ [step, displayValue, withCellValue, debounce, setValue],
159
319
  );
160
320
 
161
321
  const handlePointerUp = useCallback(() => {
@@ -168,27 +328,61 @@ const MatrixComponent = ({
168
328
 
169
329
  const handleKeyDown = useCallback(
170
330
  (e: React.KeyboardEvent, row: number, col: number) => {
171
- if (e.key === "ArrowUp" || e.key === "ArrowDown") {
172
- if (disabled[row][col]) {
173
- return;
174
- }
331
+ if (disabled[row][col]) {
332
+ return;
333
+ }
334
+ if (e.key === "Enter" || e.key === "F2") {
175
335
  e.preventDefault();
176
- const cellStep = step[row][col];
177
- const delta = e.key === "ArrowUp" ? cellStep : -cellStep;
178
- const newValue = clampValue(displayValue[row][col] + delta, row, col);
179
-
180
- if (newValue !== displayValue[row][col]) {
181
- const copy = displayValue.map((r) => [...r]);
182
- copy[row][col] = newValue;
183
- if (symmetric && row !== col) {
184
- copy[col][row] = newValue;
185
- }
186
- setDraft(copy);
187
- setValue(copy);
188
- }
336
+ startEditing(row, col);
337
+ return;
338
+ }
339
+ // Typing a number starts editing, seeded with the typed character.
340
+ if (
341
+ e.key.length === 1 &&
342
+ /[\d.+-]/.test(e.key) &&
343
+ !e.ctrlKey &&
344
+ !e.metaKey &&
345
+ !e.altKey
346
+ ) {
347
+ e.preventDefault();
348
+ startEditing(row, col, e.key);
349
+ return;
350
+ }
351
+ const cellStep = step[row][col];
352
+ let delta: number;
353
+ switch (e.key) {
354
+ // Right/left mirror up/down, matching the horizontal drag direction
355
+ // (and the native slider convention).
356
+ case "ArrowUp":
357
+ case "ArrowRight":
358
+ delta = cellStep * stepMultiplier(e);
359
+ break;
360
+ case "ArrowDown":
361
+ case "ArrowLeft":
362
+ delta = -cellStep * stepMultiplier(e);
363
+ break;
364
+ case "PageUp":
365
+ delta = cellStep * COARSE_MULTIPLIER;
366
+ break;
367
+ case "PageDown":
368
+ delta = -cellStep * COARSE_MULTIPLIER;
369
+ break;
370
+ default:
371
+ return;
372
+ }
373
+ e.preventDefault();
374
+ const copy = withCellValue(
375
+ displayValue,
376
+ row,
377
+ col,
378
+ cleanFloat(displayValue[row][col] + delta, delta),
379
+ );
380
+ if (copy) {
381
+ setDraft(copy);
382
+ setValue(copy);
189
383
  }
190
384
  },
191
- [disabled, step, displayValue, clampValue, symmetric, setValue],
385
+ [disabled, startEditing, step, displayValue, withCellValue, setValue],
192
386
  );
193
387
 
194
388
  const hasRowLabels = rowLabels != null && rowLabels.length > 0;
@@ -238,6 +432,10 @@ const MatrixComponent = ({
238
432
  const isDisabled = disabled[i][j];
239
433
  const isActive =
240
434
  activeCell?.row === i && activeCell?.col === j;
435
+ const cellEdit =
436
+ editing != null && editing.row === i && editing.col === j
437
+ ? editing
438
+ : null;
241
439
  const rowLabel = rowLabels?.[i] ?? `Row ${i + 1}`;
242
440
  const colLabel = columnLabels?.[j] ?? `Column ${j + 1}`;
243
441
  return (
@@ -247,24 +445,47 @@ const MatrixComponent = ({
247
445
  "relative text-center min-w-14 h-8 px-2 transition-colors touch-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-hidden",
248
446
  isDisabled
249
447
  ? "cursor-default text-muted-foreground"
250
- : "cursor-ew-resize text-link hover:bg-accent",
251
- isActive && "bg-accent",
448
+ : cellEdit
449
+ ? "cursor-text"
450
+ : "cursor-ew-resize text-link hover:bg-accent",
451
+ (isActive || cellEdit != null) && "bg-accent",
252
452
  j === 0 && "bracket-l",
253
453
  j === numCols - 1 && "bracket-r",
254
454
  i === 0 && "bracket-t",
255
455
  i === numRows - 1 && "bracket-b",
256
456
  )}
257
457
  tabIndex={isDisabled ? -1 : 0}
458
+ title={String(cellValue)}
258
459
  aria-label={`${rowLabel}, ${colLabel}`}
259
460
  aria-valuenow={cellValue}
260
461
  aria-valuemin={minValue?.[i]?.[j]}
261
462
  aria-valuemax={maxValue?.[i]?.[j]}
262
463
  aria-disabled={isDisabled || undefined}
263
464
  onPointerDown={(e) => handlePointerDown(e, i, j)}
465
+ onDoubleClick={() => startEditing(i, j)}
264
466
  onKeyDown={(e) => handleKeyDown(e, i, j)}
265
467
  data-testid={`matrix-cell-${i}-${j}`}
266
468
  >
267
- {formatValue(cellValue)}
469
+ {cellEdit ? (
470
+ <input
471
+ ref={focusInput}
472
+ className="bg-transparent text-center font-mono text-sm text-foreground outline-none select-text"
473
+ style={{
474
+ width: `${Math.max(cellEdit.text.length + 2, 6)}ch`,
475
+ }}
476
+ value={cellEdit.text}
477
+ onChange={(e) =>
478
+ setEditing({ ...cellEdit, text: e.target.value })
479
+ }
480
+ onKeyDown={handleInputKeyDown}
481
+ onBlur={() => commitEdit(false)}
482
+ onPointerDown={(e) => e.stopPropagation()}
483
+ aria-label={`Edit ${rowLabel}, ${colLabel}`}
484
+ data-testid={`matrix-input-${i}-${j}`}
485
+ />
486
+ ) : (
487
+ formatValue(cellValue)
488
+ )}
268
489
  </td>
269
490
  );
270
491
  })}