// ~/src/siteplane/components/number-field/number-field.tsx "use client"; import { NumberField as NumberFieldPrimitive } from "@base-ui/react/number-field"; import { MinusIcon, PlusIcon } from "lucide-react"; import * as React from "react"; import { type SiteplaneControlSize, useResolvedControlSize, } from "../../system/provider/siteplane-ui-provider"; import { formControlSurfaceClassNames, formControlTextClassNames, type FormControlSurface, type FormControlTextSize, surfaceClassNames, } from "../../system/styles/surface"; import { cn, type WithStringClassName } from "../../system/utils/cn"; import { slotClass } from "../../system/utils/slot-class"; import { KitButton } from "../button/button-internal"; import { Label } from "../label"; import "./number-field.css"; export const NumberFieldContext: React.Context<{ fieldId: string; size: SiteplaneControlSize; textSize?: FormControlTextSize | undefined; variant: FormControlSurface; } | null> = React.createContext<{ fieldId: string; size: SiteplaneControlSize; textSize?: FormControlTextSize | undefined; variant: FormControlSurface; } | null>(null); export function NumberField({ id, className, emphasized = false, size, textSize, variant = "default", ...props }: WithStringClassName & { /** A semibold value one text step larger, for a number that is the content. */ emphasized?: boolean | undefined; size?: SiteplaneControlSize | undefined; textSize?: FormControlTextSize | undefined; variant?: FormControlSurface | undefined; }): React.ReactElement { const generatedId = React.useId(); const fieldId = id ?? generatedId; const resolvedSize = useResolvedControlSize(size); // The value follows the field text scale, as Input does: 14 px from sm to // lg and 16 px at xl. textSize="base" sets 16 px at every size. // An emphasized value is one step larger and semibold: 16 px at sm, 18 px // at md and lg and 20 px at xl, whatever the textSize (design-rework.css). const resolvedTextSize = textSize ?? (resolvedSize === "xl" ? "base" : undefined); return ( ); } export function NumberFieldGroup({ className, ...props }: WithStringClassName): React.ReactElement { const context = React.useContext(NumberFieldContext); return ( ); } export function NumberFieldDecrement({ className, render, ...props }: WithStringClassName): React.ReactElement { const context = React.useContext(NumberFieldContext); return ( ) } {...props} > ); } export function NumberFieldIncrement({ className, render, ...props }: WithStringClassName): React.ReactElement { const context = React.useContext(NumberFieldContext); return ( ) } {...props} > ); } export function NumberFieldInput({ className, ...props }: WithStringClassName): React.ReactElement { return ( ); } export function NumberFieldScrubArea({ className, label, ...props }: WithStringClassName & { label: string; }): React.ReactElement { const context = React.useContext(NumberFieldContext); if (!context) throw new Error( "NumberFieldScrubArea must be used within a NumberField component for accessibility.", ); return ( {/* The label takes the scrub area's cursor and fades with a disabled field (design-rework.css, the shared label rules). */} ); } export function CursorGrowIcon( props: React.ComponentProps<"svg">, ): React.ReactElement { return ( ); } export { NumberFieldPrimitive }; // ~/src/siteplane/components/number-field/number-field.css @layer theme, base, components, siteplane, utilities; @layer siteplane { /* A Number Field is compact by default: two square step buttons and a 7rem * value field, room for a formatted amount such as $1,250.00. The control * sizes are the Input scale (28, 32, 36 and 48 px) and a step button is as * wide as its control is tall, so the value field keeps its width at every * size. The root reads --number-field-width; a width utility in className * replaces it. */ .sp-number-field { --number-field-control: var(--sp-rw-control-md); --number-field-width: calc(var(--number-field-control) * 2 + 7rem); } .sp-number-field[data-size="sm"] { --number-field-control: var(--sp-rw-control-sm); } .sp-number-field[data-size="lg"] { --number-field-control: var(--sp-rw-control-lg); } .sp-number-field[data-size="xl"] { --number-field-control: var(--sp-rw-control-xl); } .sp-number-field-value-window { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; pointer-events: none; color: var(--foreground); font-variant-numeric: tabular-nums; text-align: center; } .sp-number-field-value { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; line-height: inherit; will-change: opacity, transform; } .sp-number-field-value[data-roll="in"], .sp-number-field-value[data-roll="out"] { animation-duration: var(--motion-duration-base); animation-fill-mode: both; animation-timing-function: var(--motion-ease-standard); } .sp-number-field-value-window[data-roll-direction="up"] .sp-number-field-value[data-roll="in"] { animation-name: sp-number-field-roll-in-up; } .sp-number-field-value-window[data-roll-direction="up"] .sp-number-field-value[data-roll="out"] { animation-name: sp-number-field-roll-out-up; } .sp-number-field-value-window[data-roll-direction="down"] .sp-number-field-value[data-roll="in"] { animation-name: sp-number-field-roll-in-down; } .sp-number-field-value-window[data-roll-direction="down"] .sp-number-field-value[data-roll="out"] { animation-name: sp-number-field-roll-out-down; } @keyframes sp-number-field-roll-in-up { from { opacity: 0; transform: translateY(100%); } to { opacity: 1; transform: translateY(0); } } @keyframes sp-number-field-roll-out-up { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-100%); } } @keyframes sp-number-field-roll-in-down { from { opacity: 0; transform: translateY(-100%); } to { opacity: 1; transform: translateY(0); } } @keyframes sp-number-field-roll-out-down { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(100%); } } html[data-motion-reduce="true"] .sp-number-field-value[data-roll="in"], html[data-motion-reduce="true"] .sp-number-field-value[data-roll="out"] { animation-duration: 1ms; animation-delay: 0ms; } @media (prefers-reduced-motion: reduce) { .sp-number-field-value[data-roll="in"], .sp-number-field-value[data-roll="out"] { animation-duration: 1ms; animation-delay: 0ms; } } } // ~/src/siteplane/components/number-field/index.ts export * from "./number-field";