diff --git a/apps/app/src/components/collaboration/CollaborativeBudgetField.tsx b/apps/app/src/components/collaboration/CollaborativeBudgetField.tsx index f38c9e4c2..2443a6b56 100644 --- a/apps/app/src/components/collaboration/CollaborativeBudgetField.tsx +++ b/apps/app/src/components/collaboration/CollaborativeBudgetField.tsx @@ -4,23 +4,25 @@ import { useCollaborativeFragment } from '@/hooks/useCollaborativeFragment'; import type { BudgetData } from '@op/common/client'; import { Button } from '@op/ui/Button'; import { NumberField } from '@op/ui/NumberField'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useTranslations } from '@/lib/i18n'; import { useCollaborativeDoc } from './CollaborativeDocContext'; const DEFAULT_CURRENCY = 'USD'; -const DEFAULT_CURRENCY_SYMBOL = '$'; -const CURRENCY_SYMBOLS: Record = { - USD: DEFAULT_CURRENCY_SYMBOL, -}; - -/** Formats a number as a locale-aware currency string (e.g. 5000 → "$5,000") */ -function formatBudgetDisplay(amount: number, currencySymbol: string): string { - return `${currencySymbol}${amount.toLocaleString()}`; -} +const getCurrencySymbol = (currency: string) => + (0) + .toLocaleString(undefined, { + style: 'currency', + currency, + currencyDisplay: 'narrowSymbol', + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }) + .replace(/\d/g, '') + .trim(); interface CollaborativeBudgetFieldProps { maxAmount?: number; @@ -33,8 +35,9 @@ interface CollaborativeBudgetFieldProps { * Stores `MoneyAmount` (`{ amount, currency }`) as a JSON string in the shared doc * for future multi-currency support. * - * Displays as a pill when a value exists, switching to an inline - * NumberField on click for editing. + * Displays as a pill when a value exists or empty, switching to an inline + * NumberField on click for editing. The pill width matches the input width + * to prevent layout shifts. */ export function CollaborativeBudgetField({ maxAmount, @@ -69,11 +72,40 @@ export function CollaborativeBudgetField({ const [isEditing, setIsEditing] = useState(false); const budgetAmount = budget?.amount ?? null; - const currencySymbol = - CURRENCY_SYMBOLS[budget?.currency ?? DEFAULT_CURRENCY] ?? - DEFAULT_CURRENCY_SYMBOL; + const currency = budget?.currency ?? DEFAULT_CURRENCY; + const currencySymbol = useMemo(() => getCurrencySymbol(currency), [currency]); + + const placeholderText = maxAmount + ? t('Max {amount}', { amount: maxAmount.toLocaleString() }) + : t('Enter amount'); + + // Size the input to its placeholder text instead of the default size=20 + useLayoutEffect(() => { + if (budgetInputRef.current) { + budgetInputRef.current.size = placeholderText.length; + } + }, [placeholderText]); + + // Use the larger of the input and button natural widths so both match + const buttonRef = useRef(null); + const [sharedWidth, setSharedWidth] = useState(0); + + useEffect(() => { + if (isEditing) { + return; + } + const frame = requestAnimationFrame(() => { + const group = budgetInputRef.current?.closest('[role="group"]'); + const inputW = group instanceof HTMLElement ? group.offsetWidth : 0; + const buttonW = buttonRef.current?.scrollWidth ?? 0; + const width = Math.max(inputW, buttonW); + if (width > 0) { + setSharedWidth(width); + } + }); + return () => cancelAnimationFrame(frame); + }, [isEditing]); - // Auto-focus when switching to edit mode useEffect(() => { if (isEditing && budgetInputRef.current) { budgetInputRef.current.focus(); @@ -85,14 +117,14 @@ export function CollaborativeBudgetField({ setBudget(null); } else { setBudget({ - currency: budget?.currency ?? DEFAULT_CURRENCY, + currency, amount: value, }); } }; useEffect(() => { - const emitted: BudgetData | null = budget; + const emitted = budgetText ? (JSON.parse(budgetText) as BudgetData) : null; const key = emitted ? `${emitted.amount}:${emitted.currency}` : null; if (lastEmittedRef.current === key) { @@ -101,7 +133,7 @@ export function CollaborativeBudgetField({ lastEmittedRef.current = key ?? undefined; onChangeRef.current?.(emitted); - }, [budget]); + }, [budgetText]); const handleStartEditing = () => { setIsEditing(true); @@ -111,40 +143,44 @@ export function CollaborativeBudgetField({ setIsEditing(false); }; - // No value and not editing → "Add budget" pill - if (budgetAmount === null && !isEditing) { - return ( - - ); - } - - // Has a value and not editing → display as pill - if (budgetAmount !== null && !isEditing) { - return ( - - ); - } - - // Editing mode → inline NumberField return ( - + <> +
0 ? { minWidth: sharedWidth } : undefined} + > + +
+ {!isEditing && ( + + )} + ); }