/* Copyright 2026 Marimo. All rights reserved. */ import { type JSX, useId } from "react"; import { z } from "zod"; import { NumberField } from "@/components/ui/number-field"; import { useDebounceControlledState } from "@/hooks/useDebounce"; import { cn } from "@/utils/cn"; import type { IPlugin, IPluginProps, Setter } from "../types"; import { Labeled } from "./common/labeled"; type T = number; interface Data { start?: T | null; stop?: T | null; step?: T; label: string | null; debounce: boolean; fullWidth: boolean; disabled?: boolean; } export class NumberPlugin implements IPlugin { tagName = "marimo-number"; validator = z.object({ initialValue: z.number().nullish(), label: z.string().nullable(), start: z.number().nullish(), stop: z.number().nullish(), step: z.number().optional(), debounce: z.boolean().default(false), fullWidth: z.boolean().default(false), disabled: z.boolean().optional(), }); render(props: IPluginProps): JSX.Element { return ( ); } } interface NumberComponentProps extends Data { value: T | null; setValue: Setter; } const NumberComponent = (props: NumberComponentProps): JSX.Element => { let id = useId(); if (import.meta.env.VITEST) { id = "test-id"; } const initialValue = withoutNaN(props.value); // Create a debounced value of 200 const { value, onChange } = useDebounceControlledState({ initialValue: initialValue, delay: 200, disabled: !props.debounce, onChange: props.setValue, }); const handleChange = (newValue: number) => { onChange(withoutNaN(newValue)); }; return ( ); }; function withoutNaN(value: number | null | undefined): number | null { if (value == null || Number.isNaN(value)) { return null; } return value; }