8 changed files with 278 additions and 41 deletions
@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
|
||||
import { Input } from '@/components/ui/input' |
||||
import { |
||||
clampZapSats, |
||||
formatSatsGrouped, |
||||
parseGroupedIntegerInput, |
||||
SAT_GROUP_SEPARATOR, |
||||
shouldHighlightLeadingSatsGroups, |
||||
splitSatsGroupedParts |
||||
} from '@/lib/lightning' |
||||
import { superchatSatsLeadingHighlightClass } from '@/lib/superchat-ui' |
||||
import { cn } from '@/lib/utils' |
||||
import { Fragment, type ComponentProps } from 'react' |
||||
|
||||
const defaultTypography = |
||||
'tabular-nums font-semibold text-xl sm:text-2xl md:text-2xl' |
||||
|
||||
export default function GroupedSatsInput({ |
||||
sats, |
||||
onSatsChange, |
||||
id, |
||||
className, |
||||
inputClassName, |
||||
...inputProps |
||||
}: { |
||||
sats: number |
||||
onSatsChange: (next: number) => void |
||||
id?: string |
||||
className?: string |
||||
inputClassName?: string |
||||
} & Omit<ComponentProps<typeof Input>, 'value' | 'onChange' | 'id' | 'className' | 'inputMode'>) { |
||||
const clamped = clampZapSats(sats) |
||||
const displayValue = sats === 0 ? '' : formatSatsGrouped(clamped) |
||||
const highlightLeading = sats > 0 && shouldHighlightLeadingSatsGroups(clamped) |
||||
const parts = sats === 0 ? [] : splitSatsGroupedParts(clamped) |
||||
const typography = defaultTypography |
||||
|
||||
return ( |
||||
<div className={cn('relative min-w-0', className)}> |
||||
{parts.length > 0 ? ( |
||||
<div |
||||
className="pointer-events-none absolute inset-y-0 left-3 right-3 z-0 flex items-center overflow-hidden" |
||||
aria-hidden |
||||
> |
||||
<span className={cn('whitespace-nowrap', typography)}> |
||||
{parts.map((part, index) => ( |
||||
<Fragment key={`${index}-${part}`}> |
||||
{index > 0 ? SAT_GROUP_SEPARATOR : null} |
||||
<span |
||||
className={cn( |
||||
index === 0 && highlightLeading && superchatSatsLeadingHighlightClass |
||||
)} |
||||
> |
||||
{part} |
||||
</span> |
||||
</Fragment> |
||||
))} |
||||
</span> |
||||
</div> |
||||
) : null} |
||||
<Input |
||||
id={id} |
||||
inputMode="numeric" |
||||
value={displayValue} |
||||
onChange={(e) => onSatsChange(parseGroupedIntegerInput(e.target.value))} |
||||
onFocus={(e) => { |
||||
requestAnimationFrame(() => { |
||||
const val = e.target.value |
||||
e.target.setSelectionRange(val.length, val.length) |
||||
}) |
||||
}} |
||||
className={cn( |
||||
'relative z-10 bg-transparent text-transparent caret-foreground', |
||||
'selection:bg-primary/20 selection:text-transparent', |
||||
typography, |
||||
inputClassName |
||||
)} |
||||
{...inputProps} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
Loading…
Reference in new issue