@marimo-team/frontend 0.25.3-dev135 → 0.25.3-dev137

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 (88) hide show
  1. package/dist/assets/{CellStatus-Bu_cFOaf.js → CellStatus-B9hoYYSq.js} +1 -1
  2. package/dist/assets/{ErrorBoundary-Cy4lxM4U.js → ErrorBoundary-DIoi62xQ.js} +2 -2
  3. package/dist/assets/{JsonOutput-Dnd0uzN4.js → JsonOutput-f0zvWw7g.js} +3 -3
  4. package/dist/assets/{MarimoErrorOutput-DWgmERpO.js → MarimoErrorOutput-DFCWAXPj.js} +1 -1
  5. package/dist/assets/{RenderHTML-BuZ8DdVv.js → RenderHTML-CJOIsfDF.js} +1 -1
  6. package/dist/assets/{RunButton-BXXDV5w-.js → RunButton-BhbOSnBI.js} +1 -1
  7. package/dist/assets/{add-cell-with-ai-DYahRMb_.js → add-cell-with-ai-xzt8ZIZY.js} +1 -1
  8. package/dist/assets/{add-connection-dialog-CjRaQbDW.js → add-connection-dialog-CLcZdzEt.js} +1 -1
  9. package/dist/assets/{agent-panel-Cb9PZdDM.js → agent-panel-x_kvcauj.js} +1 -1
  10. package/dist/assets/{ai-model-dropdown-Bbr2oylS.js → ai-model-dropdown-DcOMJlL4.js} +1 -1
  11. package/dist/assets/{app-config-button-CPNc_---.js → app-config-button-gr-9epH3.js} +1 -1
  12. package/dist/assets/{blob-CQAkRxR1.js → blob-B25C4ORK.js} +1 -1
  13. package/dist/assets/{cache-panel-DuBsA6oO.js → cache-panel-BK4-1NJC.js} +1 -1
  14. package/dist/assets/{cell-editor-CjvWSYDB.js → cell-editor-CO_2xdmY.js} +1 -1
  15. package/dist/assets/{cell-link-DIOanEQB.js → cell-link-DBP_BcJY.js} +1 -1
  16. package/dist/assets/{cells-lwP9-xsQ.js → cells-B9RIIuYa.js} +1 -1
  17. package/dist/assets/{chat-display-DpCQoui-.js → chat-display-CvnF2GaH.js} +1 -1
  18. package/dist/assets/{chat-panel-C79_pWxF.js → chat-panel-CJFHiZ2R.js} +1 -1
  19. package/dist/assets/{chat-ui-DOOiaOcb.js → chat-ui-DbPlwime.js} +1 -1
  20. package/dist/assets/{column-preview-DIvrCqra.js → column-preview-j0kenrsq.js} +1 -1
  21. package/dist/assets/{command-palette-BYgBVslq.js → command-palette-CQ1DPGNl.js} +1 -1
  22. package/dist/assets/{common-B7U2CeDc.js → common-C9Gvbmz5.js} +1 -1
  23. package/dist/assets/{components-Vz1pD133.js → components-CMj235gS.js} +1 -1
  24. package/dist/assets/{datasource-Bp8TnG7a.js → datasource-BsklC6tp.js} +1 -1
  25. package/dist/assets/{dependency-graph-panel-B31-YcSF.js → dependency-graph-panel-BNqY5al9.js} +1 -1
  26. package/dist/assets/{documentation-panel-C2MJwSef.js → documentation-panel-UrRQcsOB.js} +1 -1
  27. package/dist/assets/{download-J6sWXUJR.js → download-BRqw2JzE.js} +1 -1
  28. package/dist/assets/{edit-page-DfxHDnWP.js → edit-page-Ca9DttGG.js} +4 -4
  29. package/dist/assets/{error-entries-BcRbOxwK.js → error-entries-CuIzi5P7.js} +1 -1
  30. package/dist/assets/{error-panel-jZlFCjdy.js → error-panel-CFCs0M8e.js} +1 -1
  31. package/dist/assets/{feedback-button-C2XmZw_y.js → feedback-button-DZVOiUEf.js} +1 -1
  32. package/dist/assets/feedback-button-J9hRaX_K.js +1 -0
  33. package/dist/assets/{file-explorer-panel-Cg9UovsS.js → file-explorer-panel-Cqey5tMY.js} +1 -1
  34. package/dist/assets/{file-name-input-y9ggAMGJ.js → file-name-input-khW7mtt2.js} +1 -1
  35. package/dist/assets/{floating-outline-CE1olGl0.js → floating-outline-CSwtWNm9.js} +1 -1
  36. package/dist/assets/{formatting-DW2u7HXI.js → formatting-CMgDQsM2.js} +1 -1
  37. package/dist/assets/{gallery-page-ClEnoAT-.js → gallery-page-CcG0Msi3.js} +1 -1
  38. package/dist/assets/{glide-data-editor-Cp0q28Zz.js → glide-data-editor-B46-8C6x.js} +1 -1
  39. package/dist/assets/{home-page-DJsI6Od_.js → home-page-C5qcnnak.js} +1 -1
  40. package/dist/assets/{hooks-DGSryuSD.js → hooks-CiNPyjez.js} +1 -1
  41. package/dist/assets/index-DRLfeAur.js +30 -0
  42. package/dist/assets/{input-DLA5Dhn3.js → input-BQBy_25D.js} +1 -1
  43. package/dist/assets/{layout-DRVIFud8.js → layout-DSsztKhq.js} +3 -3
  44. package/dist/assets/{logs-panel-Db38azhc.js → logs-panel-DZ8zJWNT.js} +1 -1
  45. package/dist/assets/{markdown-renderer-D2td9eFc.js → markdown-renderer-DszNRkaL.js} +1 -1
  46. package/dist/assets/{name-cell-input-CJpi0Kvu.js → name-cell-input-BIpvW2kg.js} +1 -1
  47. package/dist/assets/numbers-BB-lSqeY.js +1 -0
  48. package/dist/assets/{outline-panel-DxQKP4eN.js → outline-panel-CHwFhhFo.js} +1 -1
  49. package/dist/assets/{packages-panel-BFzN4mNb.js → packages-panel-qT6TuHGP.js} +1 -1
  50. package/dist/assets/{panel-accordion-state-D3RiRniG.js → panel-accordion-state-DROYE5RR.js} +1 -1
  51. package/dist/assets/{panels-CpUpLOy4.js → panels-jXKzMfaY.js} +1 -1
  52. package/dist/assets/{process-output-B6v1FE29.js → process-output-CPh21qjo.js} +1 -1
  53. package/dist/assets/{readonly-code-display-Deg2hzFh.js → readonly-code-display-DKoAsBb-.js} +1 -1
  54. package/dist/assets/{readonly-python-code-DPGknu49.js → readonly-python-code-QqiDzhYb.js} +1 -1
  55. package/dist/assets/{reveal-component-CXkDpGxy.js → reveal-component-Dd29QoYx.js} +1 -1
  56. package/dist/assets/{run-page-qp2KKYgs.js → run-page-VQPLnDi_.js} +1 -1
  57. package/dist/assets/{runtime-details-state-BiGF299n.js → runtime-details-state-BlarRoM1.js} +1 -1
  58. package/dist/assets/{scratchpad-panel-QV0lacAe.js → scratchpad-panel-BPw7VxO6.js} +1 -1
  59. package/dist/assets/{secrets-panel-ConQpchl.js → secrets-panel-j7vcP2r2.js} +1 -1
  60. package/dist/assets/{session-panel-CrWluawa.js → session-panel-BnszHmkW.js} +1 -1
  61. package/dist/assets/{snippets-panel-CM7g9b6c.js → snippets-panel-a-A15D0n.js} +1 -1
  62. package/dist/assets/{state-lEPoZS4k.js → state-2U7LSUNv.js} +1 -1
  63. package/dist/assets/{state-DhMztz1E.js → state-B_SxQGvw.js} +1 -1
  64. package/dist/assets/{switch-D2gdC_ep.js → switch-DLzvDOLo.js} +1 -1
  65. package/dist/assets/{tracing-DRA8maZB.js → tracing-CpbvZwxP.js} +1 -1
  66. package/dist/assets/{tracing-panel-DLxPRadV.js → tracing-panel-Dxtm4-GJ.js} +2 -2
  67. package/dist/assets/{tree-actions-COtU5qy_.js → tree-actions-CIBdpNC-.js} +1 -1
  68. package/dist/assets/{useAddCell-BAEOA87M.js → useAddCell-B6my967m.js} +1 -1
  69. package/dist/assets/{useCellActionButton-BobjsJfu.js → useCellActionButton-BwBEO8zi.js} +1 -1
  70. package/dist/assets/{useDeleteCell-gedazkBF.js → useDeleteCell-DYRsSrAa.js} +1 -1
  71. package/dist/assets/{useDependencyPanelTab-DAJ6BOUm.js → useDependencyPanelTab-lNbBYJZs.js} +1 -1
  72. package/dist/assets/{useInstallPackage-BfNKN33w.js → useInstallPackage-Q9agSKO7.js} +1 -1
  73. package/dist/assets/{useNotebookActions-CnLArLoz.js → useNotebookActions-CmG2irDr.js} +1 -1
  74. package/dist/assets/{useRunCells-CUknep7p.js → useRunCells-Bl702L84.js} +1 -1
  75. package/dist/assets/{useSplitCell-DeXniINv.js → useSplitCell-DQRIXpFK.js} +1 -1
  76. package/dist/assets/{write-secret-modal-CBi2vNK_.js → write-secret-modal-BujVAO67.js} +1 -1
  77. package/dist/index.html +25 -25
  78. package/package.json +1 -1
  79. package/src/components/ui/__tests__/range-slider.test.tsx +165 -0
  80. package/src/components/ui/range-slider.tsx +22 -9
  81. package/src/core/websocket/useNotebookSession.tsx +8 -3
  82. package/src/plugins/impl/RangeSliderPlugin.tsx +17 -2
  83. package/src/plugins/impl/__tests__/RangeSliderPlugin.test.tsx +109 -0
  84. package/src/utils/__tests__/numbers.test.ts +57 -0
  85. package/src/utils/numbers.ts +59 -9
  86. package/dist/assets/feedback-button-94vZrmBy.js +0 -1
  87. package/dist/assets/index-BjNf54kO.js +0 -30
  88. package/dist/assets/numbers-DJK15_4S.js +0 -1
@@ -0,0 +1,165 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { render } from "@testing-library/react";
4
+ import * as React from "react";
5
+ import { describe, expect, it, vi } from "vitest";
6
+ import { RangeSlider } from "../range-slider";
7
+
8
+ vi.mock("react-aria", () => ({
9
+ useLocale: () => ({ locale: "en-US" }),
10
+ }));
11
+
12
+ vi.mock("../tooltip", () => {
13
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
14
+ <div>{children}</div>
15
+ );
16
+ const Content = ({ children }: { children: React.ReactNode }) => (
17
+ <div>{children}</div>
18
+ );
19
+
20
+ return {
21
+ TooltipContent: Content,
22
+ TooltipPortal: Wrapper,
23
+ TooltipProvider: Wrapper,
24
+ TooltipRoot: Wrapper,
25
+ TooltipTrigger: Wrapper,
26
+ };
27
+ });
28
+
29
+ vi.mock("radix-ui", () => {
30
+ const Primitive = React.forwardRef<
31
+ HTMLDivElement,
32
+ React.HTMLAttributes<HTMLDivElement>
33
+ >(({ children, ...props }, ref) => (
34
+ <div ref={ref} {...props}>
35
+ {children}
36
+ </div>
37
+ ));
38
+ Primitive.displayName = "Primitive";
39
+
40
+ return {
41
+ Slider: {
42
+ Root: Primitive,
43
+ Track: Primitive,
44
+ Range: Primitive,
45
+ Thumb: Primitive,
46
+ },
47
+ };
48
+ });
49
+
50
+ describe("RangeSlider", () => {
51
+ it("formats both tooltip values without floating-point artifacts", () => {
52
+ const { getAllByText, queryByText } = render(
53
+ <RangeSlider
54
+ aria-label="Range"
55
+ min={0}
56
+ max={10}
57
+ step={0.01}
58
+ value={[1.2, 1.500000000000001]}
59
+ valueMap={(value) => value}
60
+ />,
61
+ );
62
+
63
+ expect(getAllByText("1.2")).toHaveLength(1);
64
+ expect(getAllByText("1.5")).toHaveLength(1);
65
+ expect(queryByText("1.500000000000001")).toBeNull();
66
+ });
67
+
68
+ it("keeps the legacy precision when no step is supplied", () => {
69
+ const { getAllByText, queryByText } = render(
70
+ <RangeSlider
71
+ aria-label="Range"
72
+ min={0}
73
+ max={10}
74
+ value={[1.2, 1.500000000000001]}
75
+ valueMap={(value) => value}
76
+ />,
77
+ );
78
+
79
+ expect(getAllByText("1.2")).toHaveLength(1);
80
+ expect(getAllByText("1.5")).toHaveLength(1);
81
+ expect(queryByText("1.500000000000001")).toBeNull();
82
+ });
83
+
84
+ it("retains value precision when no step is supplied", () => {
85
+ const { getByText } = render(
86
+ <RangeSlider
87
+ aria-label="Range"
88
+ min={0}
89
+ max={10}
90
+ value={[1.2345, 1.500000000000001]}
91
+ valueMap={(value) => value}
92
+ />,
93
+ );
94
+
95
+ expect(getByText("1.2345")).toBeDefined();
96
+ expect(getByText("1.5")).toBeDefined();
97
+ });
98
+
99
+ it("preserves precision from finer-grained steps", () => {
100
+ const { getAllByText, queryByText } = render(
101
+ <RangeSlider
102
+ aria-label="Range"
103
+ min={0}
104
+ max={10}
105
+ step={0.001}
106
+ value={[1.234, 1.235000000000001]}
107
+ valueMap={(value) => value}
108
+ />,
109
+ );
110
+
111
+ expect(getAllByText("1.234")).toHaveLength(1);
112
+ expect(getAllByText("1.235")).toHaveLength(1);
113
+ expect(queryByText("1.23")).toBeNull();
114
+ });
115
+
116
+ it("preserves precision for small tooltip values", () => {
117
+ const { getByText } = render(
118
+ <RangeSlider
119
+ aria-label="Range"
120
+ min={0}
121
+ max={0.01}
122
+ step={0.00001}
123
+ value={[0.00123, 0.0012400000000000002]}
124
+ valueMap={(value) => value}
125
+ />,
126
+ );
127
+
128
+ expect(getByText("1.23e-3")).toBeDefined();
129
+ expect(getByText("1.24e-3")).toBeDefined();
130
+ });
131
+
132
+ it("infers precision from custom mapped steps", () => {
133
+ const steps = [1, 1.234, 2];
134
+ const { getAllByText } = render(
135
+ <RangeSlider
136
+ aria-label="Range"
137
+ min={0}
138
+ max={steps.length - 1}
139
+ step={1}
140
+ steps={steps}
141
+ value={[0, 1]}
142
+ valueMap={(value) => steps[value]}
143
+ />,
144
+ );
145
+
146
+ expect(getAllByText("1")).toHaveLength(1);
147
+ expect(getAllByText("1.234")).toHaveLength(1);
148
+ });
149
+
150
+ it("preserves decimals from the slider origin", () => {
151
+ const { getAllByText } = render(
152
+ <RangeSlider
153
+ aria-label="Range"
154
+ min={0.5}
155
+ max={10.5}
156
+ step={1}
157
+ value={[0.5, 1.5]}
158
+ valueMap={(value) => value}
159
+ />,
160
+ );
161
+
162
+ expect(getAllByText("0.5")).toHaveLength(1);
163
+ expect(getAllByText("1.5")).toHaveLength(1);
164
+ });
165
+ });
@@ -4,7 +4,10 @@ import { Slider as SliderPrimitive } from "radix-ui";
4
4
  import * as React from "react";
5
5
  import { useLocale } from "react-aria";
6
6
  import { cn } from "@/utils/cn";
7
- import { prettyScientificNumber } from "@/utils/numbers";
7
+ import {
8
+ fractionDigitsForSlider,
9
+ prettyScientificNumber,
10
+ } from "@/utils/numbers";
8
11
  import { useBoolean } from "../../hooks/useBoolean";
9
12
  import {
10
13
  TooltipContent,
@@ -20,9 +23,15 @@ const RangeSlider = React.forwardRef<
20
23
  valueMap: (sliderValue: number) => number;
21
24
  steps?: number[];
22
25
  }
23
- >(({ className, valueMap, ...props }, ref) => {
26
+ >(({ className, steps, valueMap, ...props }, ref) => {
24
27
  const [open, openActions] = useBoolean(false);
25
28
  const { locale } = useLocale();
29
+ const fractionDigits = fractionDigitsForSlider(
30
+ props.step,
31
+ steps,
32
+ props.min,
33
+ props.value?.map(valueMap),
34
+ );
26
35
 
27
36
  const isDraggingRange = React.useRef(false);
28
37
  const dragStartX = React.useRef(0);
@@ -94,11 +103,9 @@ const RangeSlider = React.forwardRef<
94
103
  const [origLeft, origRight] = dragStartValue.current;
95
104
  const rangeWidth = origRight - origLeft;
96
105
 
97
- const steps = props.steps;
98
- const step: number =
99
- steps && steps.length > 1
100
- ? Math.min(...steps.slice(1).map((s, i) => s - steps[i]))
101
- : (props.step ?? 1);
106
+ // `steps` contains mapped display values, while pointer movement happens
107
+ // in the slider's index/value space. Use the Radix step for snapping.
108
+ const step = props.step ?? 1;
102
109
  const snappedDelta = Math.round(delta / step) * step;
103
110
 
104
111
  const clampedDelta = Math.max(
@@ -177,7 +184,10 @@ const RangeSlider = React.forwardRef<
177
184
  <TooltipPortal>
178
185
  {props.value != null && props.value.length === 2 && (
179
186
  <TooltipContent key={props.value[0]}>
180
- {prettyScientificNumber(valueMap(props.value[0]), { locale })}
187
+ {prettyScientificNumber(valueMap(props.value[0]), {
188
+ maximumFractionDigits: fractionDigits,
189
+ locale,
190
+ })}
181
191
  </TooltipContent>
182
192
  )}
183
193
  </TooltipPortal>
@@ -198,7 +208,10 @@ const RangeSlider = React.forwardRef<
198
208
  <TooltipPortal>
199
209
  {props.value != null && props.value.length === 2 && (
200
210
  <TooltipContent key={props.value[1]}>
201
- {prettyScientificNumber(valueMap(props.value[1]), { locale })}
211
+ {prettyScientificNumber(valueMap(props.value[1]), {
212
+ maximumFractionDigits: fractionDigits,
213
+ locale,
214
+ })}
202
215
  </TooltipContent>
203
216
  )}
204
217
  </TooltipPortal>
@@ -263,13 +263,18 @@ export function useNotebookSession(opts: {
263
263
  filterStorageFromVariables(variableNames);
264
264
  return;
265
265
  case "variable-values":
266
- setMetadata(
267
- msg.data.variables.map((v) => ({
266
+ setMetadata([
267
+ ...(msg.data.removed ?? []).map((v) => ({
268
+ name: v.name as VariableName,
269
+ dataType: undefined,
270
+ value: undefined,
271
+ })),
272
+ ...msg.data.variables.map((v) => ({
268
273
  name: v.name as VariableName,
269
274
  dataType: v.datatype,
270
275
  value: v.value,
271
276
  })),
272
- );
277
+ ]);
273
278
  return;
274
279
  case "alert":
275
280
  toast({
@@ -5,7 +5,10 @@ import { type JSX, useEffect, useId, useState } from "react";
5
5
  import { useLocale } from "react-aria";
6
6
  import { z } from "zod";
7
7
  import { cn } from "@/utils/cn";
8
- import { prettyScientificNumber } from "@/utils/numbers";
8
+ import {
9
+ fractionDigitsForSlider,
10
+ prettyScientificNumber,
11
+ } from "@/utils/numbers";
9
12
  import { RangeSlider } from "../../components/ui/range-slider";
10
13
  import type { IPlugin, IPluginProps, Setter } from "../types";
11
14
  import { Labeled } from "./common/labeled";
@@ -74,6 +77,7 @@ const RangeSliderComponent = ({
74
77
  start,
75
78
  stop,
76
79
  step,
80
+ steps,
77
81
  debounce,
78
82
  orientation,
79
83
  showValue,
@@ -86,6 +90,12 @@ const RangeSliderComponent = ({
86
90
 
87
91
  // Hold internal value
88
92
  const [internalValue, setInternalValue] = useState(value);
93
+ const fractionDigits = fractionDigitsForSlider(
94
+ step,
95
+ steps,
96
+ start,
97
+ internalValue.map(valueMap),
98
+ );
89
99
 
90
100
  // Update internal value on prop change
91
101
  useEffect(() => {
@@ -121,6 +131,7 @@ const RangeSliderComponent = ({
121
131
  min={start}
122
132
  max={stop}
123
133
  step={step}
134
+ steps={steps ?? undefined}
124
135
  orientation={orientation}
125
136
  disabled={disabled}
126
137
  // Triggered on all value changes
@@ -154,8 +165,12 @@ const RangeSliderComponent = ({
154
165
  {showValue && (
155
166
  <div className="text-xs text-muted-foreground min-w-[16px]">
156
167
  {`${prettyScientificNumber(valueMap(internalValue[0]), {
168
+ maximumFractionDigits: fractionDigits,
169
+ locale,
170
+ })}, ${prettyScientificNumber(valueMap(internalValue[1]), {
171
+ maximumFractionDigits: fractionDigits,
157
172
  locale,
158
- })}, ${prettyScientificNumber(valueMap(internalValue[1]), { locale })}`}
173
+ })}`}
159
174
  </div>
160
175
  )}
161
176
  </div>
@@ -0,0 +1,109 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { render } from "@testing-library/react";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import type { z } from "zod";
6
+ import type { IPluginProps } from "../../types";
7
+ import { RangeSliderPlugin } from "../RangeSliderPlugin";
8
+
9
+ vi.mock("@/components/ui/range-slider", () => ({
10
+ RangeSlider: () => <div data-testid="range-slider" />,
11
+ }));
12
+
13
+ vi.mock("react-aria", () => ({
14
+ useLocale: () => ({ locale: "en-US" }),
15
+ }));
16
+
17
+ describe("RangeSliderPlugin", () => {
18
+ type PluginProps = IPluginProps<
19
+ number[],
20
+ z.infer<typeof RangeSliderPlugin.prototype.validator>
21
+ >;
22
+
23
+ const createProps = (
24
+ value: number[],
25
+ data: Partial<PluginProps["data"]> = {},
26
+ ): PluginProps => ({
27
+ host: document.createElement("div"),
28
+ value,
29
+ setValue: vi.fn(),
30
+ data: {
31
+ initialValue: value,
32
+ label: null,
33
+ start: 0,
34
+ stop: 10,
35
+ step: 0.01,
36
+ steps: null,
37
+ debounce: false,
38
+ orientation: "horizontal",
39
+ showValue: true,
40
+ fullWidth: true,
41
+ disabled: false,
42
+ ...data,
43
+ },
44
+ functions: {},
45
+ });
46
+
47
+ it("rounds floating-point artifacts in the displayed range", () => {
48
+ const plugin = new RangeSliderPlugin();
49
+ const props = createProps([1.2, 1.500000000000001]);
50
+
51
+ const { getByText } = render(plugin.render(props));
52
+
53
+ expect(getByText("1.2, 1.5")).toBeDefined();
54
+ });
55
+
56
+ it("preserves precision from finer-grained steps", () => {
57
+ const plugin = new RangeSliderPlugin();
58
+ const props = createProps([1.234, 1.235000000000001], { step: 0.001 });
59
+
60
+ const { getByText } = render(plugin.render(props));
61
+
62
+ expect(getByText("1.234, 1.235")).toBeDefined();
63
+ });
64
+
65
+ it("retains value precision when no step is supplied", () => {
66
+ const plugin = new RangeSliderPlugin();
67
+ const props = createProps([1.2345, 1.500000000000001], {
68
+ step: undefined,
69
+ });
70
+
71
+ const { getByText } = render(plugin.render(props));
72
+
73
+ expect(getByText("1.2345, 1.5")).toBeDefined();
74
+ });
75
+
76
+ it("preserves precision for small displayed values", () => {
77
+ const plugin = new RangeSliderPlugin();
78
+ const props = createProps([0.00123, 0.0012400000000000002], {
79
+ stop: 0.01,
80
+ step: 0.00001,
81
+ });
82
+
83
+ const { getByText } = render(plugin.render(props));
84
+
85
+ expect(getByText("1.23e-3, 1.24e-3")).toBeDefined();
86
+ });
87
+
88
+ it("infers precision from custom steps", () => {
89
+ const plugin = new RangeSliderPlugin();
90
+ const props = createProps([0, 1], {
91
+ stop: 2,
92
+ step: undefined,
93
+ steps: [1, 1.234, 2],
94
+ });
95
+
96
+ const { getByText } = render(plugin.render(props));
97
+
98
+ expect(getByText("1, 1.234")).toBeDefined();
99
+ });
100
+
101
+ it("preserves decimals from the slider origin", () => {
102
+ const plugin = new RangeSliderPlugin();
103
+ const props = createProps([0.5, 1.5], { start: 0.5, stop: 10, step: 1 });
104
+
105
+ const { getByText } = render(plugin.render(props));
106
+
107
+ expect(getByText("0.5, 1.5")).toBeDefined();
108
+ });
109
+ });
@@ -2,6 +2,7 @@
2
2
  import { describe, expect, it, vi } from "vitest";
3
3
  import {
4
4
  countFractionDigits,
5
+ fractionDigitsForSlider,
5
6
  maxFractionDigitsForSteps,
6
7
  prettyEngineeringNumber,
7
8
  prettyNumber,
@@ -104,6 +105,42 @@ describe("prettyScientificNumber", () => {
104
105
  "1.23456789101112",
105
106
  );
106
107
  });
108
+
109
+ it("supports caller-provided display precision", () => {
110
+ expect(
111
+ prettyScientificNumber(1.2340000000000002, {
112
+ maximumFractionDigits: 2,
113
+ locale,
114
+ }),
115
+ ).toBe("1.23");
116
+ expect(
117
+ prettyScientificNumber(1.2340000000000002, {
118
+ maximumFractionDigits: 3,
119
+ locale,
120
+ }),
121
+ ).toBe("1.234");
122
+ expect(
123
+ prettyScientificNumber(1.234, {
124
+ maximumFractionDigits: 3,
125
+ locale,
126
+ }),
127
+ ).toBe("1.234");
128
+ });
129
+
130
+ it("preserves caller-provided precision in scientific notation", () => {
131
+ expect(
132
+ prettyScientificNumber(0.0012300000000000002, {
133
+ maximumFractionDigits: 5,
134
+ locale,
135
+ }),
136
+ ).toBe("1.23e-3");
137
+ expect(
138
+ prettyScientificNumber(1_000_001.2300000001, {
139
+ maximumFractionDigits: 2,
140
+ locale,
141
+ }),
142
+ ).toBe("1.00000123e6");
143
+ });
107
144
  });
108
145
 
109
146
  describe("prettyEngineeringNumber", () => {
@@ -153,3 +190,23 @@ describe("maxFractionDigitsForSteps", () => {
153
190
  expect(maxFractionDigitsForSteps([1, 2, 3, 4], 1)).toBe(0);
154
191
  });
155
192
  });
193
+
194
+ describe("fractionDigitsForSlider", () => {
195
+ it("preserves precision for regular slider steps", () => {
196
+ expect(fractionDigitsForSlider(0.001)).toBe(3);
197
+ expect(fractionDigitsForSlider(0.01)).toBe(2);
198
+ expect(fractionDigitsForSlider(undefined)).toBe(2);
199
+ expect(fractionDigitsForSlider(undefined, null, 0, [0.1234, 0.5])).toBe(4);
200
+ });
201
+
202
+ it("infers precision from custom step values", () => {
203
+ expect(fractionDigitsForSlider(undefined, [1, 1.234, 2])).toBe(3);
204
+ expect(
205
+ fractionDigitsForSlider(undefined, [0.1, 0.2, 0.30000000000000004]),
206
+ ).toBe(1);
207
+ });
208
+
209
+ it("includes the slider origin in regular step precision", () => {
210
+ expect(fractionDigitsForSlider(1, null, 0.5)).toBe(1);
211
+ });
212
+ });
@@ -24,19 +24,19 @@ export const maxFractionalDigits = memoizeLastValue((locale: string) => {
24
24
  });
25
25
 
26
26
  const getScientificFormatter = memoizeLastValue(
27
- (locale: string) =>
27
+ (locale: string, maximumFractionDigits = 1) =>
28
28
  new Intl.NumberFormat(locale, {
29
29
  minimumFractionDigits: 1,
30
- maximumFractionDigits: 1,
30
+ maximumFractionDigits,
31
31
  notation: "scientific",
32
32
  }),
33
33
  );
34
34
 
35
35
  const getRoundedFormatter = memoizeLastValue(
36
- (locale: string) =>
36
+ (locale: string, maximumFractionDigits = 2) =>
37
37
  new Intl.NumberFormat(locale, {
38
38
  minimumFractionDigits: 0,
39
- maximumFractionDigits: 2,
39
+ maximumFractionDigits,
40
40
  }),
41
41
  );
42
42
 
@@ -60,7 +60,7 @@ export function countFractionDigits(n: number): number {
60
60
 
61
61
  /** Max fraction digits to display a set of step values cleanly. */
62
62
  export function maxFractionDigitsForSteps(
63
- steps: number[],
63
+ steps: readonly number[],
64
64
  minGap: number,
65
65
  ): number {
66
66
  let max = countFractionDigits(minGap);
@@ -70,6 +70,41 @@ export function maxFractionDigitsForSteps(
70
70
  return max;
71
71
  }
72
72
 
73
+ /** Infer display precision from a slider's regular or custom steps. */
74
+ export function fractionDigitsForSlider(
75
+ step?: number,
76
+ steps?: readonly number[] | null,
77
+ origin?: number,
78
+ values?: readonly number[],
79
+ ): number {
80
+ const originDigits = origin == null ? 0 : countFractionDigits(origin);
81
+
82
+ if (steps && steps.length > 0) {
83
+ let minGap = Number.POSITIVE_INFINITY;
84
+ for (let i = 1; i < steps.length; i++) {
85
+ const gap = Math.abs(steps[i] - steps[i - 1]);
86
+ if (gap > 0 && gap < minGap) {
87
+ minGap = gap;
88
+ }
89
+ }
90
+
91
+ return Math.max(
92
+ originDigits,
93
+ maxFractionDigitsForSteps(steps, Number.isFinite(minGap) ? minGap : 0),
94
+ );
95
+ }
96
+
97
+ if (step == null || !Number.isFinite(step) || step <= 0) {
98
+ return Math.max(
99
+ 2,
100
+ originDigits,
101
+ ...(values ?? []).map(countFractionDigits),
102
+ );
103
+ }
104
+
105
+ return Math.max(originDigits, countFractionDigits(step));
106
+ }
107
+
73
108
  /** Round away float noise so step-based inputs stay on a clean decimal grid. */
74
109
  export function roundToFractionDigits(n: number, digits: number): number {
75
110
  return Number(n.toFixed(digits));
@@ -121,6 +156,7 @@ export function prettyScientificNumber(
121
156
  value: number,
122
157
  opts: {
123
158
  shouldRound?: boolean; // Default to false
159
+ maximumFractionDigits?: number;
124
160
  locale: string;
125
161
  },
126
162
  ): string {
@@ -133,14 +169,28 @@ export function prettyScientificNumber(
133
169
  // Determine if the number should be in scientific notation
134
170
  const absValue = Math.abs(value);
135
171
  if (absValue < 1e-2 || absValue >= 1e6) {
136
- return getScientificFormatter(opts.locale).format(value).toLowerCase();
172
+ // Scientific notation counts digits after the decimal in the mantissa.
173
+ const digits =
174
+ opts.maximumFractionDigits === undefined
175
+ ? 1
176
+ : Math.max(
177
+ 1,
178
+ Math.min(
179
+ maxFractionalDigits(opts.locale),
180
+ opts.maximumFractionDigits + Math.floor(Math.log10(absValue)),
181
+ ),
182
+ );
183
+ return getScientificFormatter(opts.locale, digits)
184
+ .format(value)
185
+ .toLowerCase();
137
186
  }
138
187
 
139
188
  const { shouldRound, locale } = opts;
140
189
 
141
- if (shouldRound) {
142
- // Number has an integer part, format with 2 decimal places
143
- return getRoundedFormatter(locale).format(value);
190
+ if (shouldRound || opts.maximumFractionDigits !== undefined) {
191
+ return getRoundedFormatter(locale, opts.maximumFractionDigits ?? 2).format(
192
+ value,
193
+ );
144
194
  }
145
195
 
146
196
  // Don't round
@@ -1 +0,0 @@
1
- import{n as e}from"./feedback-button-C2XmZw_y.js";export{e as FeedbackModal};