@marimo-team/islands 0.23.10-dev9 → 0.23.10

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 (188) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CyV83R2m.js → ConnectedDataExplorerComponent-Z3RP7Vmm.js} +31 -31
  2. package/dist/{ErrorBoundary-rULOrC_p.js → ErrorBoundary-DpbaKVv7.js} +1 -1
  3. package/dist/{any-language-editor-CiES2a2h.js → any-language-editor-CAgFD4Kd.js} +20 -20
  4. package/dist/assets/__vite-browser-external-eshhtsgZ.js +1 -0
  5. package/dist/assets/worker-CC0Oul9k.js +73 -0
  6. package/dist/{chat-ui-K84W5ecY.js → chat-ui-C4-GMW9y.js} +77 -77
  7. package/dist/{check-DTbrK0zt.js → check-BCaJeT-J.js} +1 -1
  8. package/dist/{chunk-5FQGJX7Z-BNjes6Yx.js → chunk-5FQGJX7Z-BbqSm5gU.js} +1227 -1222
  9. package/dist/{code-block-37QAKDTI-m92Yc8pv.js → code-block-37QAKDTI-O6IH6wAi.js} +1 -1
  10. package/dist/{code-visibility-v6ucEVUg.js → code-visibility-DrFozVlm.js} +1943 -1223
  11. package/dist/{copy-5jQ_kGE1.js → copy-UqRYxiOg.js} +1 -1
  12. package/dist/dist-7QfXoMdB.js +5 -0
  13. package/dist/{dist-DgnE8F-r.js → dist-A2846XWO.js} +1 -1
  14. package/dist/dist-BEXXyZig.js +5 -0
  15. package/dist/{dist-B3pZ0Ab6.js → dist-BR_gyG9L.js} +3 -3
  16. package/dist/{dist-CcXxepx6.js → dist-BSAt6RhH.js} +27 -27
  17. package/dist/{dist-Bde4a2kU.js → dist-BY018Paw.js} +8 -8
  18. package/dist/dist-BYj57OV4.js +5 -0
  19. package/dist/{dist-CUCNs1ja.js → dist-BaoDKvdy.js} +2 -2
  20. package/dist/{dist-Cy1WxgBD.js → dist-Bf7SHuNp.js} +5 -5
  21. package/dist/{dist-Bz_sYWbr.js → dist-Bk75fBZA.js} +2 -2
  22. package/dist/dist-BlSvQzNr.js +5 -0
  23. package/dist/{dist-C5VC_yzu.js → dist-BzEzfugY.js} +1 -1
  24. package/dist/dist-CCBlxAgS.js +8 -0
  25. package/dist/dist-CIDTVIUf.js +5 -0
  26. package/dist/{dist-CLUtPrdy.js → dist-CIYBwstr.js} +1 -1
  27. package/dist/{dist-BotSqB48.js → dist-C_Y3oV3C.js} +12 -12
  28. package/dist/{dist-BTfv03uy.js → dist-CcWX6tmx.js} +2 -2
  29. package/dist/{dist-BhM8gdSO.js → dist-CoXAujgg.js} +4 -4
  30. package/dist/{dist-4j4c7bjm.js → dist-CpxNdDkw.js} +3 -3
  31. package/dist/dist-CqQyhAM8.js +8 -0
  32. package/dist/dist-CwRu2Xzh.js +5 -0
  33. package/dist/{dist-BcuoonNH.js → dist-CxJDU6Bh.js} +9 -9
  34. package/dist/{dist-DxvORzUR.js → dist-D-W5ny5a.js} +8 -8
  35. package/dist/dist-D8CDTVgf.js +6 -0
  36. package/dist/dist-D8DNB0nO.js +8 -0
  37. package/dist/dist-DL6N_q-A.js +5 -0
  38. package/dist/{dist-BbbIBDiQ.js → dist-DMjWuVs8.js} +1 -1
  39. package/dist/dist-DOFbNV_b.js +8 -0
  40. package/dist/dist-DPrYzMY0.js +6 -0
  41. package/dist/{dist-h2c8sZvT.js → dist-DZORgqKY.js} +1 -1
  42. package/dist/{dist-B3P2fFpz.js → dist-DZo4nSS0.js} +14 -14
  43. package/dist/{dist-D4CewLk6.js → dist-Dax--nl9.js} +1 -1
  44. package/dist/{dist-DRfcqpxJ.js → dist-DgGbNavJ.js} +2 -2
  45. package/dist/{dist-C1BYNeCR.js → dist-Dk6PV_d3.js} +10 -10
  46. package/dist/{dist-fQ0ViXGs.js → dist-Dv_Y15yk.js} +107 -107
  47. package/dist/{dist-Bfwsv11D.js → dist-DyyjKEYf.js} +2 -2
  48. package/dist/{dist-p2qyWijU.js → dist-GZXUmt0b.js} +2 -2
  49. package/dist/{dist-CLJWPTX2.js → dist-LTU8Hdvn.js} +3 -3
  50. package/dist/{dist-DqAWR3CS.js → dist-M9Vag9Y0.js} +20 -20
  51. package/dist/{dist-DNdhYsgW.js → dist-U4F-tbMs.js} +79 -62
  52. package/dist/{dist-RqXTaiir.js → dist-abid3KgM.js} +11 -11
  53. package/dist/dist-cdmMjgsn.js +5 -0
  54. package/dist/dist-hT4QzYX-.js +1247 -0
  55. package/dist/{dist-luvabDEB.js → dist-t9Kf7xqC.js} +2 -2
  56. package/dist/{error-banner-5bz0L9hS.js → error-banner-CJXYJ6Sb.js} +11 -6
  57. package/dist/esm-BaH2eg5-.js +1171 -0
  58. package/dist/{esm-Duie8iU-.js → esm-ga2Bf3O2.js} +43 -43
  59. package/dist/{extends-CkydH1Q5.js → extends-D_hDsj6R.js} +4 -4
  60. package/dist/files/wasm-intro.py +11 -8
  61. package/dist/{formats-DHxc-FdY.js → formats-C4wO47tk.js} +1 -1
  62. package/dist/{glide-data-editor-CgIxTzAP.js → glide-data-editor-BPkCPs7L.js} +255 -255
  63. package/dist/{html-to-image-B93KtAVY.js → html-to-image-BwfcJJxA.js} +2412 -2282
  64. package/dist/{input-_2sjvfne.js → input-OdWHkobi.js} +263 -245
  65. package/dist/{label-LWtdw5i8.js → label-CC4ytI1X.js} +1 -1
  66. package/dist/main.js +7630 -7558
  67. package/dist/{mermaid-4DMBBIKO-DIdL224_.js → mermaid-4DMBBIKO-Eg3D4FVz.js} +1 -1
  68. package/dist/{mermaid-lXOw5Py9.js → mermaid-BotvIKg9.js} +4 -4
  69. package/dist/{process-output-BOXi9fnS.js → process-output-DsloEDna.js} +3 -3
  70. package/dist/{reveal-component-gRcjQbNL.js → reveal-component-3YQBHWuD.js} +698 -620
  71. package/dist/{spec-B96zNUEA.js → spec-X7FwLJni.js} +4 -4
  72. package/dist/{strings-Bu3vlb6W.js → strings-J57tzLr3.js} +47 -46
  73. package/dist/style.css +1 -1
  74. package/dist/{toDate-x-WRDCH7.js → toDate-d8RCRrRd.js} +2 -2
  75. package/dist/{tooltip-C5FYOpQc.js → tooltip-DpcyNkQ2.js} +2 -2
  76. package/dist/{types-CVvp1fKr.js → types-ChtMFmZ2.js} +1 -1
  77. package/dist/{useAsyncData-iRgKDT5s.js → useAsyncData-PonK__yh.js} +1 -1
  78. package/dist/{useDateFormatter-BA4FCquG.js → useDateFormatter-QB-3MpYr.js} +2 -2
  79. package/dist/{useDeepCompareMemoize-CkQ57VS2.js → useDeepCompareMemoize-D3NGWke6.js} +1 -1
  80. package/dist/{useLifecycle-BBO9PIph.js → useLifecycle-00mO3OSS.js} +2 -2
  81. package/dist/{useTheme-DHIrRQOe.js → useTheme-DEhDzATN.js} +1 -1
  82. package/dist/{vega-component-Dq-SH463.js → vega-component-DGPUhbDs.js} +8 -8
  83. package/dist/{zod-CoBiJ5v4.js → zod-aLSua2NL.js} +24 -23
  84. package/package.json +4 -3
  85. package/src/__tests__/setup.ts +16 -0
  86. package/src/components/chat/chat-display.tsx +2 -2
  87. package/src/components/data-table/TableBottomBar.tsx +1 -15
  88. package/src/components/data-table/TableTopBar.tsx +8 -13
  89. package/src/components/data-table/__tests__/TableBottomBar.test.tsx +6 -12
  90. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +227 -0
  91. package/src/components/data-table/column-visibility-dropdown.tsx +204 -0
  92. package/src/components/data-table/data-table.tsx +1 -1
  93. package/src/components/data-table/filter-by-values-picker.tsx +39 -17
  94. package/src/components/data-table/filter-pills.tsx +1 -1
  95. package/src/components/datasources/__tests__/filter-empty.test.ts +183 -0
  96. package/src/components/datasources/datasources.tsx +92 -3
  97. package/src/components/editor/cell/code/cell-editor.tsx +70 -32
  98. package/src/components/editor/cell/code/code-placeholder.css +18 -0
  99. package/src/components/editor/cell/code/code-placeholder.tsx +47 -0
  100. package/src/components/editor/cell/code/language-toggle.tsx +7 -1
  101. package/src/components/editor/chrome/wrapper/app-chrome.tsx +97 -52
  102. package/src/components/editor/chrome/wrapper/lazy-panels.ts +91 -0
  103. package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -0
  104. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +9 -2
  105. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +62 -27
  106. package/src/components/editor/connections/storage/add-storage-form.tsx +5 -1
  107. package/src/components/editor/connections/storage/as-code.ts +20 -6
  108. package/src/components/editor/documentation.css +35 -0
  109. package/src/components/editor/file-tree/file-explorer.tsx +8 -18
  110. package/src/components/editor/file-tree/tree-actions.tsx +46 -1
  111. package/src/components/editor/notebook-cell.tsx +3 -3
  112. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +20 -0
  113. package/src/components/editor/renderers/slides-layout/types.ts +1 -0
  114. package/src/components/markdown/__tests__/markdown-renderer.test.tsx +43 -0
  115. package/src/components/markdown/markdown-renderer.tsx +24 -1
  116. package/src/components/slides/__tests__/minimap-actions.test.tsx +166 -0
  117. package/src/components/slides/__tests__/reveal-component.test.ts +425 -0
  118. package/src/components/slides/minimap.tsx +127 -10
  119. package/src/components/slides/reveal-component.tsx +287 -61
  120. package/src/components/slides/slide-cell-view.tsx +26 -2
  121. package/src/components/slides/slide-form.tsx +26 -4
  122. package/src/components/storage/__tests__/storage-inspector.test.ts +180 -0
  123. package/src/components/storage/storage-inspector.tsx +535 -81
  124. package/src/components/ui/combobox.tsx +51 -32
  125. package/src/components/ui/number-field.tsx +5 -1
  126. package/src/components/ui/reorderable-list.tsx +13 -0
  127. package/src/components/ui/select-core/__tests__/use-select-list.test.ts +294 -0
  128. package/src/components/ui/select-core/__tests__/utils.test.ts +222 -0
  129. package/src/components/ui/select-core/index.ts +16 -0
  130. package/src/components/ui/select-core/option-row.tsx +33 -0
  131. package/src/components/ui/select-core/render-slot.ts +20 -0
  132. package/src/components/ui/select-core/select-list.tsx +248 -0
  133. package/src/components/ui/select-core/types.ts +44 -0
  134. package/src/components/ui/select-core/use-select-list.ts +347 -0
  135. package/src/components/ui/select-core/utils.ts +121 -0
  136. package/src/components/ui/toast.tsx +1 -0
  137. package/src/core/cells/__tests__/document-changes.test.ts +2 -2
  138. package/src/core/cells/__tests__/focus.test.ts +45 -2
  139. package/src/core/cells/focus.ts +1 -1
  140. package/src/core/cells/scrollCellIntoView.ts +4 -0
  141. package/src/core/cells/utils.ts +1 -1
  142. package/src/core/codemirror/__tests__/editor-mount-scheduler.test.ts +143 -0
  143. package/src/core/codemirror/copilot/getCodes.ts +3 -1
  144. package/src/core/codemirror/editor-mount-scheduler.ts +101 -0
  145. package/src/core/codemirror/language/languages/python.ts +2 -0
  146. package/src/core/codemirror/language/languages/sql/utils.ts +3 -1
  147. package/src/core/codemirror/lsp/__tests__/markdown-renderer.test.ts +41 -0
  148. package/src/core/codemirror/lsp/markdown-renderer.ts +59 -0
  149. package/src/core/datasets/data-source-connections.ts +2 -0
  150. package/src/core/errors/errors.ts +1 -1
  151. package/src/core/storage/__tests__/state.test.ts +49 -0
  152. package/src/core/storage/__tests__/useStorageEntries.test.tsx +199 -3
  153. package/src/core/storage/state.ts +124 -13
  154. package/src/core/storage/types.ts +6 -0
  155. package/src/core/wasm/worker/bootstrap.ts +12 -4
  156. package/src/plugins/core/__test__/registerReactComponent.test.ts +96 -0
  157. package/src/plugins/core/registerReactComponent.tsx +42 -10
  158. package/src/plugins/impl/MultiselectPlugin.tsx +19 -142
  159. package/src/plugins/impl/SearchableSelect.tsx +16 -97
  160. package/src/plugins/impl/SliderPlugin.tsx +132 -18
  161. package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
  162. package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
  163. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +278 -8
  164. package/src/utils/__tests__/numbers.test.ts +20 -0
  165. package/src/utils/errors.ts +9 -0
  166. package/src/utils/lazy.ts +6 -1
  167. package/src/utils/numbers.ts +27 -0
  168. package/src/utils/schedule-task.ts +71 -0
  169. package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +0 -1
  170. package/dist/assets/worker-ip3AI_sN.js +0 -73
  171. package/dist/dist-0Fif7jnk.js +0 -5
  172. package/dist/dist-B5h_9sHB.js +0 -6
  173. package/dist/dist-B9M6R5ye.js +0 -5
  174. package/dist/dist-BCt3tnck.js +0 -8
  175. package/dist/dist-BUIJwMwn.js +0 -8
  176. package/dist/dist-BpquMd3k.js +0 -5
  177. package/dist/dist-BzJsqYfz.js +0 -5
  178. package/dist/dist-CA5ELXAf.js +0 -6
  179. package/dist/dist-CLBRs6Uv.js +0 -5
  180. package/dist/dist-CStVCMbq.js +0 -5
  181. package/dist/dist-CZRIEY3Y.js +0 -8
  182. package/dist/dist-CuUHbFD0.js +0 -5
  183. package/dist/dist-DV7Iabxb.js +0 -8
  184. package/dist/dist-DhHh0jLg.js +0 -1247
  185. package/dist/dist-DuEeHMvL.js +0 -5
  186. package/dist/esm-CCuYCd3R.js +0 -1171
  187. package/src/plugins/impl/multiselectFilterFn.tsx +0 -22
  188. /package/src/components/{data-table → ui}/value-chips.tsx +0 -0
@@ -50,11 +50,18 @@ describe("SliderPlugin", () => {
50
50
  vi.useRealTimers();
51
51
  });
52
52
 
53
- const createProps = (
54
- debounce: boolean,
55
- includeInput: boolean,
56
- setValue: ReturnType<typeof vi.fn>,
57
- ): IPluginProps<number, z.infer<typeof SliderPlugin.prototype.validator>> => {
53
+ const createProps = ({
54
+ debounce,
55
+ includeInput,
56
+ setValue,
57
+ }: {
58
+ debounce: boolean;
59
+ includeInput: boolean;
60
+ setValue: ReturnType<typeof vi.fn>;
61
+ }): IPluginProps<
62
+ number,
63
+ z.infer<typeof SliderPlugin.prototype.validator>
64
+ > => {
58
65
  return {
59
66
  host: document.createElement("div"),
60
67
  value: 5,
@@ -76,10 +83,57 @@ describe("SliderPlugin", () => {
76
83
  };
77
84
  };
78
85
 
86
+ // When `steps` are provided, the slider works in *index* space: `value`,
87
+ // `start`, `stop` and `step` are all indices into the `steps` array, while
88
+ // the editable input shows/accepts the actual step values.
89
+ const createStepsProps = ({
90
+ steps,
91
+ valueIndex,
92
+ setValue,
93
+ }: {
94
+ steps: number[];
95
+ valueIndex: number;
96
+ setValue: ReturnType<typeof vi.fn>;
97
+ }): IPluginProps<
98
+ number,
99
+ z.infer<typeof SliderPlugin.prototype.validator>
100
+ > => {
101
+ return {
102
+ host: document.createElement("div"),
103
+ value: valueIndex,
104
+ setValue,
105
+ data: {
106
+ initialValue: valueIndex,
107
+ start: 0,
108
+ stop: steps.length - 1,
109
+ step: 1,
110
+ label: "Test Slider",
111
+ debounce: false,
112
+ orientation: "horizontal" as const,
113
+ showValue: false,
114
+ fullWidth: false,
115
+ includeInput: true,
116
+ steps,
117
+ },
118
+ functions: {},
119
+ };
120
+ };
121
+
122
+ const typeAndCommit = (input: HTMLElement, value: string) => {
123
+ act(() => {
124
+ fireEvent.change(input, { target: { value } });
125
+ fireEvent.blur(input);
126
+ });
127
+ };
128
+
79
129
  it("slider triggers setValue immediately when debounce is false", () => {
80
130
  const plugin = new SliderPlugin();
81
131
  const setValue = vi.fn();
82
- const props = createProps(false, false, setValue);
132
+ const props = createProps({
133
+ debounce: false,
134
+ includeInput: false,
135
+ setValue,
136
+ });
83
137
  const { getByRole } = render(plugin.render(props));
84
138
 
85
139
  act(() => {
@@ -98,7 +152,11 @@ describe("SliderPlugin", () => {
98
152
  it("slider waits until commit before calling setValue when debounce is true", () => {
99
153
  const plugin = new SliderPlugin();
100
154
  const setValue = vi.fn();
101
- const props = createProps(true, false, setValue);
155
+ const props = createProps({
156
+ debounce: true,
157
+ includeInput: false,
158
+ setValue,
159
+ });
102
160
  const { getByRole } = render(plugin.render(props));
103
161
 
104
162
  act(() => {
@@ -124,7 +182,7 @@ describe("SliderPlugin", () => {
124
182
  it("editable input triggers setValue immediately even when slider debounce is true", () => {
125
183
  const plugin = new SliderPlugin();
126
184
  const setValue = vi.fn();
127
- const props = createProps(true, true, setValue);
185
+ const props = createProps({ debounce: true, includeInput: true, setValue });
128
186
  const { getByRole } = render(plugin.render(props));
129
187
 
130
188
  act(() => {
@@ -145,4 +203,216 @@ describe("SliderPlugin", () => {
145
203
  // setValue should be called immediately regardless of debounce=true.
146
204
  expect(setValue).toHaveBeenCalledWith(9);
147
205
  });
206
+
207
+ describe("editable input with steps (regression for #9850)", () => {
208
+ it("displays the actual step value, not the index", () => {
209
+ const plugin = new SliderPlugin();
210
+ const setValue = vi.fn();
211
+ // steps[0] === -4, displayed value must be -4 (not the index 0).
212
+ const props = createStepsProps({
213
+ steps: [-4, -3, -2, -1],
214
+ valueIndex: 0,
215
+ setValue,
216
+ });
217
+ const { getByRole } = render(plugin.render(props));
218
+
219
+ act(() => {
220
+ vi.advanceTimersByTime(0);
221
+ });
222
+
223
+ const input = getByRole("textbox") as HTMLInputElement;
224
+ expect(input.value).toBe("-4");
225
+ });
226
+
227
+ it("displays decimal step values without float artifacts", () => {
228
+ const plugin = new SliderPlugin();
229
+ const setValue = vi.fn();
230
+ const props = createStepsProps({
231
+ steps: [0.1, 0.2, 0.3, 0.4],
232
+ valueIndex: 2,
233
+ setValue,
234
+ });
235
+ const { getByRole } = render(plugin.render(props));
236
+
237
+ act(() => {
238
+ vi.advanceTimersByTime(0);
239
+ });
240
+
241
+ const input = getByRole("textbox") as HTMLInputElement;
242
+ expect(input.value).toBe("0.3");
243
+ });
244
+
245
+ it("steps decimal input increment and decrement buttons move between steps", () => {
246
+ const plugin = new SliderPlugin();
247
+ const setValue = vi.fn();
248
+ const props = createStepsProps({
249
+ steps: [0.1, 0.2, 0.3, 0.4],
250
+ valueIndex: 2,
251
+ setValue,
252
+ });
253
+ const { getByRole } = render(plugin.render(props));
254
+
255
+ act(() => {
256
+ vi.advanceTimersByTime(0);
257
+ });
258
+
259
+ const decrement = getByRole("button", {
260
+ name: "Decrease Test Slider value input",
261
+ });
262
+ const increment = getByRole("button", {
263
+ name: "Increase Test Slider value input",
264
+ });
265
+
266
+ expect(decrement).not.toBeDisabled();
267
+
268
+ act(() => {
269
+ fireEvent.click(decrement);
270
+ });
271
+ expect(setValue.mock.calls).toEqual([[1]]);
272
+
273
+ act(() => {
274
+ fireEvent.click(increment);
275
+ });
276
+ expect(setValue).toHaveBeenLastCalledWith(2);
277
+ });
278
+
279
+ it("maps a typed integer step value back to its index", () => {
280
+ const plugin = new SliderPlugin();
281
+ const setValue = vi.fn();
282
+ const props = createStepsProps({
283
+ steps: [1, 2, 3, 4],
284
+ valueIndex: 0,
285
+ setValue,
286
+ });
287
+ const { getByRole } = render(plugin.render(props));
288
+
289
+ act(() => {
290
+ vi.advanceTimersByTime(0);
291
+ });
292
+
293
+ const input = getByRole("textbox");
294
+ // Typing "4" should select the last step (index 3), not index 4.
295
+ typeAndCommit(input, "4");
296
+ expect(setValue).toHaveBeenLastCalledWith(3);
297
+
298
+ // Typing "2" should select index 1, not get "stuck" on 3.
299
+ typeAndCommit(input, "2");
300
+ expect(setValue).toHaveBeenLastCalledWith(1);
301
+ });
302
+
303
+ it("maps fractional step values back to their index", () => {
304
+ const plugin = new SliderPlugin();
305
+ const setValue = vi.fn();
306
+ const props = createStepsProps({
307
+ steps: [0.1, 0.2, 0.3, 0.4],
308
+ valueIndex: 0,
309
+ setValue,
310
+ });
311
+ const { getByRole } = render(plugin.render(props));
312
+
313
+ act(() => {
314
+ vi.advanceTimersByTime(0);
315
+ });
316
+
317
+ const input = getByRole("textbox");
318
+ typeAndCommit(input, "0.3");
319
+ expect(setValue).toHaveBeenLastCalledWith(2);
320
+ });
321
+
322
+ it("maps negative step values back to their index", () => {
323
+ const plugin = new SliderPlugin();
324
+ const setValue = vi.fn();
325
+ const props = createStepsProps({
326
+ steps: [-4, -3, -2, -1],
327
+ valueIndex: 0,
328
+ setValue,
329
+ });
330
+ const { getByRole } = render(plugin.render(props));
331
+
332
+ act(() => {
333
+ vi.advanceTimersByTime(0);
334
+ });
335
+
336
+ const input = getByRole("textbox");
337
+ typeAndCommit(input, "-2");
338
+ expect(setValue).toHaveBeenLastCalledWith(2);
339
+ });
340
+
341
+ it("clamps out-of-range typed values to the nearest step index", () => {
342
+ const plugin = new SliderPlugin();
343
+ const setValue = vi.fn();
344
+ const props = createStepsProps({
345
+ steps: [1, 2, 3, 4],
346
+ valueIndex: 0,
347
+ setValue,
348
+ });
349
+ const { getByRole } = render(plugin.render(props));
350
+
351
+ act(() => {
352
+ vi.advanceTimersByTime(0);
353
+ });
354
+
355
+ const input = getByRole("textbox");
356
+ // Above the max step -> clamps to the last index.
357
+ typeAndCommit(input, "100");
358
+ expect(setValue).toHaveBeenLastCalledWith(3);
359
+ });
360
+
361
+ it("does not crash when steps shrink while the index is temporarily out of range", () => {
362
+ const plugin = new SliderPlugin();
363
+ const setValue = vi.fn();
364
+ // Start at the last index of a 5-element steps array.
365
+ const props = createStepsProps({
366
+ steps: [10, 20, 30, 40, 50],
367
+ valueIndex: 4,
368
+ setValue,
369
+ });
370
+ const { getByRole, rerender } = render(plugin.render(props));
371
+
372
+ act(() => {
373
+ vi.advanceTimersByTime(0);
374
+ });
375
+
376
+ // Shrink `steps` so the held index (4) is now out of range. The index
377
+ // syncs in an effect after this render, so the render must not throw on
378
+ // the stale, out-of-range index.
379
+ const shrunkProps = createStepsProps({
380
+ steps: [10, 20],
381
+ valueIndex: 1,
382
+ setValue,
383
+ });
384
+ expect(() => {
385
+ act(() => {
386
+ rerender(plugin.render(shrunkProps));
387
+ vi.advanceTimersByTime(0);
388
+ });
389
+ }).not.toThrow();
390
+
391
+ const input = getByRole("textbox") as HTMLInputElement;
392
+ expect(input.value).toBe("20");
393
+ });
394
+
395
+ it("nudges to the adjacent step on non-uniform steps even when the current step is nearest (known caveat)", () => {
396
+ const plugin = new SliderPlugin();
397
+ const setValue = vi.fn();
398
+ // Non-uniform steps; start at index 2 (value 10).
399
+ const props = createStepsProps({
400
+ steps: [0, 1, 10, 100],
401
+ valueIndex: 2,
402
+ setValue,
403
+ });
404
+ const { getByRole } = render(plugin.render(props));
405
+
406
+ act(() => {
407
+ vi.advanceTimersByTime(0);
408
+ });
409
+
410
+ const input = getByRole("textbox");
411
+ // 10 is far closer to 12 than 100 is, but because 12 > 10 the input nudges
412
+ // up one index. This documents the stepper-vs-typing limitation: onChange
413
+ // can't distinguish a typed value from a stepper-button increment.
414
+ typeAndCommit(input, "12");
415
+ expect(setValue).toHaveBeenLastCalledWith(3);
416
+ });
417
+ });
148
418
  });
@@ -1,6 +1,8 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { describe, expect, it } from "vitest";
3
3
  import {
4
+ countFractionDigits,
5
+ maxFractionDigitsForSteps,
4
6
  prettyEngineeringNumber,
5
7
  prettyNumber,
6
8
  prettyScientificNumber,
@@ -86,3 +88,21 @@ describe("prettyEngineeringNumber", () => {
86
88
  expect(prettyEngineeringNumber(-0.000_123_4, locale)).toBe("-123µ");
87
89
  });
88
90
  });
91
+
92
+ describe("countFractionDigits", () => {
93
+ it("counts decimal places without float noise", () => {
94
+ expect(countFractionDigits(1)).toBe(0);
95
+ expect(countFractionDigits(0.1)).toBe(1);
96
+ expect(countFractionDigits(0.3)).toBe(1);
97
+ expect(countFractionDigits(0.000025)).toBe(6);
98
+ expect(countFractionDigits(3.5)).toBe(1);
99
+ });
100
+ });
101
+
102
+ describe("maxFractionDigitsForSteps", () => {
103
+ it("infers precision from steps and gaps", () => {
104
+ expect(maxFractionDigitsForSteps([0.1, 0.2, 0.3, 0.4], 0.1)).toBe(1);
105
+ expect(maxFractionDigitsForSteps([1, 2, 3.5, 4], 0.5)).toBe(1);
106
+ expect(maxFractionDigitsForSteps([1, 2, 3, 4], 1)).toBe(0);
107
+ });
108
+ });
@@ -71,6 +71,15 @@ export class CellNotInitializedError extends Error {
71
71
  }
72
72
  }
73
73
 
74
+ export class FunctionNotFoundError extends Error {
75
+ constructor(
76
+ message = "This UI element could not reach its function on the kernel. Try re-running the cell.",
77
+ ) {
78
+ super(message);
79
+ this.name = "FunctionNotFoundError";
80
+ }
81
+ }
82
+
74
83
  export class NoKernelConnectedError extends Error {
75
84
  constructor(message = "Not yet connected to a kernel.") {
76
85
  super(message);
package/src/utils/lazy.ts CHANGED
@@ -3,7 +3,12 @@
3
3
  import React from "react";
4
4
 
5
5
  interface LazyComponentWithPreload<T> {
6
- preload: () => void;
6
+ /**
7
+ * Eagerly trigger the dynamic import. Returns the import promise so callers
8
+ * can await it or attach error handling; safe to call multiple times (the
9
+ * import is memoized).
10
+ */
11
+ preload: () => Promise<{ default: React.ComponentType<T> }>;
7
12
  Component: React.LazyExoticComponent<React.ComponentType<T>>;
8
13
  }
9
14
 
@@ -23,6 +23,33 @@ export const maxFractionalDigits = memoizeLastValue((locale: string) => {
23
23
  return 0;
24
24
  });
25
25
 
26
+ /** Decimal places needed to display `n` without float noise. */
27
+ export function countFractionDigits(n: number): number {
28
+ if (!Number.isFinite(n) || n === 0) {
29
+ return 0;
30
+ }
31
+ const normalized = n.toFixed(12).replace(/\.?0+$/, "");
32
+ const dot = normalized.indexOf(".");
33
+ return dot === -1 ? 0 : normalized.length - dot - 1;
34
+ }
35
+
36
+ /** Max fraction digits to display a set of step values cleanly. */
37
+ export function maxFractionDigitsForSteps(
38
+ steps: number[],
39
+ minGap: number,
40
+ ): number {
41
+ let max = countFractionDigits(minGap);
42
+ for (const step of steps) {
43
+ max = Math.max(max, countFractionDigits(step));
44
+ }
45
+ return max;
46
+ }
47
+
48
+ /** Round away float noise so step-based inputs stay on a clean decimal grid. */
49
+ export function roundToFractionDigits(n: number, digits: number): number {
50
+ return Number(n.toFixed(digits));
51
+ }
52
+
26
53
  export function prettyNumber(value: unknown, locale: string): string {
27
54
  if (value === undefined || value === null) {
28
55
  return "";
@@ -0,0 +1,71 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ /**
3
+ * Schedules `callback` to run as a fresh task once the current task finishes,
4
+ * letting the browser paint and handle input in between. Used to spread heavy
5
+ * work across the event loop instead of blocking it in one long task.
6
+ */
7
+ export type ScheduleTaskFn = (callback: () => void) => void;
8
+
9
+ /**
10
+ * Picks the best available "yield to the event loop" primitive, preferring
11
+ * ones that impose no artificial delay between tasks:
12
+ *
13
+ * 1. `scheduler.postTask` — the purpose-built Prioritized Task Scheduling API.
14
+ * `user-visible` runs promptly while still yielding to user-blocking input.
15
+ * 2. `MessageChannel` — a plain macrotask with no minimum-delay clamp (nested
16
+ * `setTimeout` is floored at ~4ms by the spec, which would add up over many
17
+ * tasks).
18
+ * 3. `setTimeout` — universal fallback; correct, just clamped when nested.
19
+ *
20
+ * The backend is chosen once, at module load, since it cannot change within a
21
+ * session.
22
+ */
23
+ // `scheduler` (the Prioritized Task Scheduling API) is not in this repo's
24
+ // lib.dom typings, so it is feature-detected behind this minimal cast.
25
+ interface PostTaskScheduler {
26
+ postTask: (
27
+ callback: () => void,
28
+ options?: { priority?: "user-blocking" | "user-visible" | "background" },
29
+ ) => Promise<unknown>;
30
+ }
31
+
32
+ function createScheduleTask(): ScheduleTaskFn {
33
+ const scheduler = (globalThis as { scheduler?: PostTaskScheduler }).scheduler;
34
+ if (typeof scheduler?.postTask === "function") {
35
+ return (cb) => {
36
+ // postTask rejects when the callback throws. Rethrow on a fresh task so a
37
+ // thrown callback surfaces as an uncaught error, matching the
38
+ // MessageChannel/setTimeout backends, instead of an unhandledrejection.
39
+ void scheduler
40
+ .postTask(cb, { priority: "user-visible" })
41
+ .catch((error) => {
42
+ setTimeout(() => {
43
+ throw error;
44
+ });
45
+ });
46
+ };
47
+ }
48
+
49
+ if (typeof MessageChannel === "function") {
50
+ // Both ports live on this thread and no data is sent: posting a message is
51
+ // just the cheapest way to enqueue a macrotask that yields for paint. The
52
+ // queue lets multiple callbacks be in flight at once (one per message).
53
+ const pending: Array<() => void> = [];
54
+ const channel = new MessageChannel();
55
+ channel.port1.onmessage = () => {
56
+ pending.shift()?.();
57
+ };
58
+
59
+ return (cb) => {
60
+ pending.push(cb);
61
+ channel.port2.postMessage(null);
62
+ };
63
+ }
64
+
65
+ return (cb) => {
66
+ setTimeout(cb, 0);
67
+ };
68
+ }
69
+
70
+ /** Shared task scheduler using the best primitive this environment offers. */
71
+ export const scheduleTask = createScheduleTask();
@@ -1 +0,0 @@
1
- import{t as e}from"./worker-ip3AI_sN.js";var t=e(((e,t)=>{t.exports={}}));export default t();