feat: revolutionary single-element editable NumberFlow with live abacus updates

Major improvements to InteractiveAbacus component:

## 🎯 Single-Element Solution
- Replace complex input overlay with elegant keyboard event handling
- Use single NumberFlow element that responds to direct typing
- No hidden input fields or z-index complexity

##  Real-Time Updates
- Abacus beads update instantly as you type each digit
- Live connection between keyboard input and bead positions
- No delays - see changes immediately on every keystroke

## 🎨 Enhanced UX
- Click number to start editing with visual feedback
- Subtle color change and pulsing underline when editing
- Type numbers directly, backspace, delete, Enter/Escape
- Compact mode support for space-efficient layouts

## 🔧 Technical Excellence
- Clean keyboard event handling (0-9, Backspace, Delete, Enter, Escape)
- Boundary checking prevents exceeding column limits
- Proper focus management with tabIndex and blur handling
- Live value updates without complex state synchronization

## 💫 User Experience
- Seamless editing: click → type → see beads move instantly
- No mode switching or jarring transitions
- Beautiful NumberFlow animations combined with direct editability
- Intuitive keyboard shortcuts and visual indicators

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-15 18:25:01 -05:00
parent e330d3539d
commit 4bccd65305
4 changed files with 770 additions and 213 deletions

View File

@@ -757,6 +757,7 @@ function ReadingNumbersGuide() {
<InteractiveAbacus
initialValue={0}
columns={3}
showManualInput={true}
className={css({
display: 'flex',
justifyContent: 'center',

View File

@@ -13,6 +13,8 @@ interface InteractiveAbacusProps {
onValueChange?: (value: number) => void
showValue?: boolean
showControls?: boolean
showManualInput?: boolean
compact?: boolean
}
export function InteractiveAbacus({
@@ -21,12 +23,17 @@ export function InteractiveAbacus({
className,
onValueChange,
showValue = true,
showControls = true
showControls = true,
showManualInput = false,
compact = false
}: InteractiveAbacusProps) {
const [currentValue, setCurrentValue] = useState(initialValue)
const [isChanging, setIsChanging] = useState(false)
const [previousValue, setPreviousValue] = useState(initialValue)
const [isEditing, setIsEditing] = useState(false)
const [editingValue, setEditingValue] = useState('')
const svgRef = useRef<HTMLDivElement>(null)
const numberRef = useRef<HTMLDivElement>(null)
// Remove the old spring animation since we're using NumberFlow now
@@ -192,110 +199,252 @@ export function InteractiveAbacus({
setCurrentValue(value)
}, [])
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
if (!showManualInput) return
// Handle number keys and editing
if (event.key >= '0' && event.key <= '9') {
event.preventDefault()
let newEditingValue: string
if (!isEditing) {
setIsEditing(true)
newEditingValue = event.key
setEditingValue(newEditingValue)
} else {
newEditingValue = editingValue + event.key
const numValue = parseInt(newEditingValue)
const maxValue = Math.pow(10, columns) - 1
if (numValue <= maxValue) {
setEditingValue(newEditingValue)
} else {
return // Don't update if exceeds max
}
}
// Update abacus immediately
const liveValue = parseInt(newEditingValue) || 0
setCurrentValue(liveValue)
} else if (event.key === 'Backspace') {
event.preventDefault()
if (isEditing) {
let newEditingValue: string
if (editingValue.length > 1) {
newEditingValue = editingValue.slice(0, -1)
} else {
newEditingValue = '0'
}
setEditingValue(newEditingValue)
// Update abacus immediately
const liveValue = parseInt(newEditingValue) || 0
setCurrentValue(liveValue)
}
} else if (event.key === 'Enter' || event.key === 'Escape') {
event.preventDefault()
if (isEditing) {
setIsEditing(false)
setEditingValue('')
// Value is already set from live updates
}
} else if (event.key === 'Delete') {
event.preventDefault()
setEditingValue('0')
setIsEditing(true)
// Update abacus immediately
setCurrentValue(0)
}
}, [showManualInput, isEditing, editingValue, columns])
const handleNumberClick = useCallback(() => {
if (showManualInput && numberRef.current) {
numberRef.current.focus()
if (!isEditing) {
setIsEditing(true)
setEditingValue(String(currentValue))
}
}
}, [showManualInput, isEditing, currentValue])
const handleNumberBlur = useCallback(() => {
if (isEditing) {
setIsEditing(false)
setEditingValue('')
// Value is already live-updated, no need to set again
}
}, [isEditing])
return (
<div className={className}>
<div className={css({
display: 'flex',
flexDirection: 'column',
gap: '6',
flexDirection: compact ? 'row' : 'column',
gap: compact ? '4' : '6',
alignItems: 'center'
})}>
{/* Interactive Abacus using TypstSoroban */}
<animated.div
ref={svgRef}
style={containerSpring}
className={css({
width: '300px',
height: '400px',
border: '3px solid',
borderRadius: '12px',
bg: 'gradient-to-br',
gradientFrom: 'amber.50',
gradientTo: 'orange.100',
padding: '20px',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.1)',
position: 'relative',
transition: 'all 0.2s ease',
_hover: {
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.15)',
}
})}
>
<TypstSoroban
number={currentValue}
width="180pt"
height="240pt"
{/* Interactive Abacus Container */}
<div className={css({
position: 'relative',
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
})}>
{/* Abacus with integrated value display */}
<animated.div
ref={svgRef}
style={containerSpring}
className={css({
width: '100%',
height: '100%',
transition: 'all 0.3s ease',
cursor: 'pointer',
'& [data-bead-type]': {
cursor: 'pointer',
transition: 'all 0.2s ease',
_hover: {
filter: 'brightness(1.2)',
transform: 'scale(1.05)'
}
width: compact ? '240px' : '300px',
height: compact ? '320px' : '400px',
border: '3px solid',
borderRadius: '12px',
bg: 'gradient-to-br',
gradientFrom: 'amber.50',
gradientTo: 'orange.100',
padding: compact ? '16px' : '20px',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.1)',
position: 'relative',
transition: 'all 0.2s ease',
_hover: {
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.15)',
}
})}
/>
</animated.div>
{/* Value Display */}
{showValue && (
<div
className={css({
fontSize: '3xl',
fontWeight: 'bold',
color: 'blue.600',
bg: 'blue.50',
px: '6',
py: '3',
rounded: 'xl',
border: '2px solid',
borderColor: 'blue.200',
minW: '120px',
textAlign: 'center',
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.15)'
})}
>
<NumberFlow
value={currentValue}
style={{
fontSize: 'inherit',
fontWeight: 'inherit',
color: 'inherit'
}}
<TypstSoroban
number={currentValue}
width={compact ? "144pt" : "180pt"}
height={compact ? "192pt" : "240pt"}
className={css({
width: '100%',
height: '100%',
transition: 'all 0.3s ease',
cursor: 'pointer',
'& [data-bead-type]': {
cursor: 'pointer',
transition: 'all 0.2s ease',
_hover: {
filter: 'brightness(1.2)',
transform: 'scale(1.05)'
}
}
})}
/>
</div>
)}
</animated.div>
{/* Controls */}
{/* Integrated Value Display with Always-Available Input */}
{showValue && (
<div
className={css({
position: 'absolute',
bottom: compact ? '-50px' : '-60px',
left: '50%',
transform: 'translateX(-50%)',
display: 'flex',
alignItems: 'center',
gap: '2',
bg: 'white',
border: '2px solid',
borderColor: 'blue.200',
rounded: 'xl',
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.15)',
px: '3',
py: '2',
minW: '160px'
})}
>
{/* Single Unified Number Display/Input */}
<div
ref={numberRef}
tabIndex={showManualInput ? 0 : -1}
onClick={handleNumberClick}
onKeyDown={handleKeyDown}
onBlur={handleNumberBlur}
className={css({
position: 'relative',
display: 'flex',
alignItems: 'center',
flex: '1',
outline: 'none',
cursor: showManualInput ? 'pointer' : 'default'
})}
title={showManualInput ? "Click to edit, type numbers, Enter to confirm" : undefined}
>
<NumberFlow
value={isEditing ? parseInt(editingValue) || 0 : currentValue}
style={{
fontSize: compact ? '1.5rem' : '1.875rem',
fontWeight: 'bold',
color: isEditing ? '#1d4ed8' : '#2563eb', // Slightly darker when editing
textAlign: 'center',
minWidth: '80px',
width: '100%'
}}
/>
{/* Visual editing indicator */}
{isEditing && (
<div className={css({
position: 'absolute',
bottom: '-2px',
left: '50%',
transform: 'translateX(-50%)',
width: '80%',
height: '2px',
bg: 'blue.400',
rounded: 'full',
animation: 'pulse 1s infinite'
})} />
)}
</div>
{/* Input Indicator */}
{showManualInput && (
<div
className={css({
p: '1',
rounded: 'full',
bg: 'blue.50',
color: 'blue.600',
fontSize: 'xs',
w: '6',
h: '6',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0
})}
title="Click the number to edit directly"
>
</div>
)}
</div>
)}
</div>
{/* Compact Controls */}
{showControls && (
<div className={css({
display: 'flex',
flexDirection: compact ? 'column' : 'row',
alignItems: 'center',
gap: '3',
flexWrap: 'wrap',
justifyContent: 'center'
gap: '2',
flexWrap: compact ? 'nowrap' : 'wrap',
justifyContent: 'center',
mt: showValue ? (compact ? '0' : '16') : '0'
})}>
<button
onClick={handleReset}
className={css({
px: '4',
px: '3',
py: '2',
bg: 'gray.100',
color: 'gray.700',
border: '1px solid',
borderColor: 'gray.300',
rounded: 'lg',
fontSize: 'sm',
rounded: 'md',
fontSize: 'xs',
fontWeight: 'medium',
cursor: 'pointer',
transition: 'all 0.2s ease',
minW: compact ? '60px' : 'auto',
_hover: {
bg: 'gray.200',
borderColor: 'gray.400',
@@ -309,57 +458,68 @@ export function InteractiveAbacus({
Clear
</button>
{/* Quick preset buttons */}
{[1, 5, 10, 25, 50, 99].map(preset => (
<button
key={preset}
onClick={() => handleSetValue(preset)}
className={css({
px: '3',
py: '2',
bg: 'blue.100',
color: 'blue.700',
border: '1px solid',
borderColor: 'blue.300',
rounded: 'lg',
fontSize: 'sm',
fontWeight: 'medium',
cursor: 'pointer',
transition: 'all 0.2s ease',
_hover: {
bg: 'blue.200',
borderColor: 'blue.400',
transform: 'translateY(-1px)'
},
_active: {
transform: 'scale(0.95)'
}
})}
>
{preset}
</button>
))}
{/* Compact preset buttons */}
<div className={css({
display: 'flex',
flexDirection: compact ? 'column' : 'row',
gap: '1',
flexWrap: 'wrap'
})}>
{(compact ? [1, 5, 10, 25] : [1, 5, 10, 25, 50, 99]).map(preset => (
<button
key={preset}
onClick={() => handleSetValue(preset)}
className={css({
px: '2',
py: '1',
bg: 'blue.100',
color: 'blue.700',
border: '1px solid',
borderColor: 'blue.300',
rounded: 'md',
fontSize: 'xs',
fontWeight: 'medium',
cursor: 'pointer',
transition: 'all 0.2s ease',
minW: compact ? '40px' : '32px',
_hover: {
bg: 'blue.200',
borderColor: 'blue.400',
transform: 'translateY(-1px)'
},
_active: {
transform: 'scale(0.95)'
}
})}
>
{preset}
</button>
))}
</div>
</div>
)}
{/* Instructions */}
<div className={css({
fontSize: 'sm',
color: 'gray.600',
textAlign: 'center',
maxW: '450px',
lineHeight: 'relaxed',
bg: 'gray.50',
px: '4',
py: '3',
rounded: 'lg',
border: '1px solid',
borderColor: 'gray.200'
})}>
<strong>How to use:</strong> Click on the beads to activate or deactivate them!
Heaven beads (top) are worth 5 each, earth beads (bottom) are worth 1 each.
You can also use the preset buttons below.
</div>
{/* Compact Instructions */}
{!compact && (
<div className={css({
fontSize: 'sm',
color: 'gray.600',
textAlign: 'center',
maxW: '450px',
lineHeight: 'relaxed',
bg: 'gray.50',
px: '4',
py: '3',
rounded: 'lg',
border: '1px solid',
borderColor: 'gray.200',
mt: showValue ? '16' : '0'
})}>
<strong>How to use:</strong> Click on the beads to activate or deactivate them!
Heaven beads (top) are worth 5 each, earth beads (bottom) are worth 1 each.
{showManualInput && ' You can also click the number to type directly.'}
</div>
)}
</div>
</div>
)

View File

@@ -1,21 +1,123 @@
@layer utilities {
.flex_column {
flex-direction: column
.fs_3xl {
font-size: var(--font-sizes-3xl)
}
.px_6 {
padding-inline: var(--spacing-6)
}
.min-w_120px {
min-width: 120px
}
.gap_3 {
gap: var(--spacing-3)
}
.min-w_140px {
min-width: 140px
}
.w_80px {
width: 80px
}
.fs_xl {
font-size: var(--font-sizes-xl)
}
.fs_2xl {
font-size: var(--font-sizes-2xl)
}
.text_transparent {
color: var(--colors-transparent)
}
.fs_1\.5rem {
font-size: 1.5rem
}
.fs_1\.875rem {
font-size: 1.875rem
}
.top_0 {
top: var(--spacing-0)
}
.left_0 {
left: var(--spacing-0)
}
.border_none {
border: var(--borders-none)
}
.bg_transparent {
background: var(--colors-transparent)
}
.font_bold {
font-weight: var(--font-weights-bold)
}
.text_\#2563eb {
color: #2563eb
}
.appearance_textfield {
appearance: textfield;
-webkit-appearance: textfield
}
.font_inherit {
font-family: inherit
}
.\[\&\:\:-webkit-outer-spin-button\,_\&\:\:-webkit-inner-spin-button\]\:appearance_none::-webkit-outer-spin-button, .\[\&\:\:-webkit-outer-spin-button\,_\&\:\:-webkit-inner-spin-button\]\:appearance_none::-webkit-inner-spin-button {
appearance: none;
-webkit-appearance: none
}
.\[\&\:\:-webkit-outer-spin-button\,_\&\:\:-webkit-inner-spin-button\]\:m_0::-webkit-outer-spin-button, .\[\&\:\:-webkit-outer-spin-button\,_\&\:\:-webkit-inner-spin-button\]\:m_0::-webkit-inner-spin-button {
margin: var(--spacing-0)
}
.gap_4 {
gap: var(--spacing-4)
}
.gap_6 {
gap: var(--spacing-6)
}
.w_240px {
width: 240px
}
.w_300px {
width: 300px
}
.h_320px {
height: 320px
}
.h_400px {
height: 400px
}
.p_16px {
padding: 16px
}
.p_20px {
padding: 20px
}
.border_3px_solid {
border: 3px solid
}
@@ -36,18 +138,10 @@
--gradient-to: var(--colors-orange-100)
}
.p_20px {
padding: 20px
}
.shadow_0_8px_24px_rgba\(0\,_0\,_0\,_0\.1\) {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1)
}
.pos_relative {
position: relative
}
.w_100\% {
width: 100%
}
@@ -66,28 +160,16 @@
transition: all 0.2s ease
}
.fs_3xl {
font-size: var(--font-sizes-3xl)
.bottom_-50px {
bottom: -50px
}
.font_bold {
font-weight: var(--font-weights-bold)
.bottom_-60px {
bottom: -60px
}
.text_blue\.600 {
color: var(--colors-blue-600)
}
.bg_blue\.50 {
background: var(--colors-blue-50)
}
.px_6 {
padding-inline: var(--spacing-6)
}
.rounded_xl {
border-radius: var(--radii-xl)
.bg_white {
background: var(--colors-white)
}
.border_2px_solid {
@@ -98,34 +180,126 @@
border-color: var(--colors-blue-200)
}
.min-w_120px {
min-width: 120px
.rounded_xl {
border-radius: var(--radii-xl)
}
.shadow_0_4px_12px_rgba\(59\,_130\,_246\,_0\.15\) {
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15)
}
.d_flex {
display: flex
.min-w_160px {
min-width: 160px
}
.cursor_default {
cursor: default
}
.pos_relative {
position: relative
}
.flex_1 {
flex: 1 1 0%
}
.ring_none {
outline: var(--borders-none)
}
.pos_absolute {
position: absolute
}
.bottom_-2px {
bottom: -2px
}
.left_50\% {
left: 50%
}
.transform_translateX\(-50\%\) {
transform: translateX(-50%)
}
.w_80\% {
width: 80%
}
.h_2px {
height: 2px
}
.bg_blue\.400 {
background: var(--colors-blue-400)
}
.animation_pulse_1s_infinite {
animation: pulse 1s infinite
}
.p_1 {
padding: var(--spacing-1)
}
.rounded_full {
border-radius: var(--radii-full)
}
.bg_blue\.50 {
background: var(--colors-blue-50)
}
.text_blue\.600 {
color: var(--colors-blue-600)
}
.w_6 {
width: var(--sizes-6)
}
.h_6 {
height: var(--sizes-6)
}
.shrink_0 {
flex-shrink: 0
}
.flex-wrap_nowrap {
flex-wrap: nowrap
}
.items_center {
align-items: center
}
.gap_3 {
gap: var(--spacing-3)
}
.flex-wrap_wrap {
flex-wrap: wrap
.gap_2 {
gap: var(--spacing-2)
}
.justify_center {
justify-content: center
}
.min-w_60px {
min-width: 60px
}
.min-w_auto {
min-width: auto
}
.px_3 {
padding-inline: var(--spacing-3)
}
.py_2 {
padding-block: var(--spacing-2)
}
.bg_gray\.100 {
background: var(--colors-gray-100)
}
@@ -138,12 +312,40 @@
border-color: var(--colors-gray-300)
}
.px_3 {
padding-inline: var(--spacing-3)
.flex_column {
flex-direction: column
}
.py_2 {
padding-block: var(--spacing-2)
.flex_row {
flex-direction: row
}
.d_flex {
display: flex
}
.gap_1 {
gap: var(--spacing-1)
}
.flex-wrap_wrap {
flex-wrap: wrap
}
.min-w_40px {
min-width: 40px
}
.min-w_32px {
min-width: 32px
}
.px_2 {
padding-inline: var(--spacing-2)
}
.py_1 {
padding-block: var(--spacing-1)
}
.bg_blue\.100 {
@@ -158,6 +360,14 @@
border-color: var(--colors-blue-300)
}
.rounded_md {
border-radius: var(--radii-md)
}
.fs_xs {
font-size: var(--font-sizes-xs)
}
.font_medium {
font-weight: var(--font-weights-medium)
}
@@ -170,6 +380,14 @@
transition: all 0.2s ease
}
.mt_16 {
margin-top: var(--spacing-16)
}
.mt_0 {
margin-top: var(--spacing-0)
}
.fs_sm {
font-size: var(--font-sizes-sm)
}
@@ -214,14 +432,46 @@
border-color: var(--colors-gray-200)
}
.w_144pt {
width: 144pt
}
.w_180pt {
width: 180pt
}
.h_192pt {
height: 192pt
}
.h_240pt {
height: 240pt
}
.\[\&\:focus\]\:text_blue\.600:focus {
color: var(--colors-blue-600)
}
.\[\&\:focus\]\:bg_blue\.50:focus {
background: var(--colors-blue-50)
}
.\[\&\:focus\]\:rounded_md:focus {
border-radius: var(--radii-md)
}
.\[\&\:focus\]\:ring_none:focus {
outline: var(--borders-none)
}
.\[\&\:focus\]\:border_none:focus {
border: var(--borders-none)
}
.hover\:bg_blue\.100:is(:hover, [data-hover]) {
background: var(--colors-blue-100)
}
.hover\:shadow_0_12px_32px_rgba\(0\,_0\,_0\,_0\.15\):is(:hover, [data-hover]) {
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15)
}

View File

@@ -137,10 +137,6 @@
transition: transform 0.3s ease
}
.p_16px {
padding: 16px
}
.max-h_60vh {
max-height: 60vh
}
@@ -419,10 +415,6 @@
min-height: var(--sizes-0)
}
.left_0 {
left: var(--spacing-0)
}
.w_100vw {
width: 100vw
}
@@ -439,10 +431,6 @@
top: 50%
}
.left_50\% {
left: 50%
}
.transform_translate\(-50\%\,_-50\%\) {
transform: translate(-50%, -50%)
}
@@ -575,10 +563,6 @@
height: 100vh
}
.shrink_0 {
flex-shrink: 0
}
.d_inline-flex {
display: inline-flex
}
@@ -907,10 +891,6 @@
grid-template-columns: repeat(1, minmax(0, 1fr))
}
.mt_16 {
margin-top: var(--spacing-16)
}
.gap_12 {
gap: var(--spacing-12)
}
@@ -1057,10 +1037,6 @@
position: sticky
}
.top_0 {
top: var(--spacing-0)
}
.z_30 {
z-index: 30
}
@@ -1165,10 +1141,6 @@
background: var(--colors-green-100)
}
.px_2 {
padding-inline: var(--spacing-2)
}
.cursor_not-allowed {
cursor: not-allowed
}
@@ -1205,10 +1177,6 @@
border-color: var(--colors-green-200)
}
.bg_transparent {
background: var(--colors-transparent)
}
.text_green\.700 {
color: var(--colors-green-700)
}
@@ -1266,10 +1234,6 @@
max-height: 300px
}
.ring_none {
outline: var(--borders-none)
}
.\[\&\[data-state\=checked\]\]\:bg_brand\.100[data-state=checked] {
background: var(--colors-brand-100)
}
@@ -1362,14 +1326,92 @@
gap: var(--spacing-0)
}
.px_6 {
padding-inline: var(--spacing-6)
}
.min-w_120px {
min-width: 120px
}
.min-w_140px {
min-width: 140px
}
.w_80px {
width: 80px
}
.text_transparent {
color: var(--colors-transparent)
}
.fs_1\.5rem {
font-size: 1.5rem
}
.fs_1\.875rem {
font-size: 1.875rem
}
.top_0 {
top: var(--spacing-0)
}
.left_0 {
left: var(--spacing-0)
}
.bg_transparent {
background: var(--colors-transparent)
}
.text_\#2563eb {
color: #2563eb
}
.appearance_textfield {
appearance: textfield;
-webkit-appearance: textfield
}
.font_inherit {
font-family: inherit
}
.\[\&\:\:-webkit-outer-spin-button\,_\&\:\:-webkit-inner-spin-button\]\:appearance_none::-webkit-outer-spin-button, .\[\&\:\:-webkit-outer-spin-button\,_\&\:\:-webkit-inner-spin-button\]\:appearance_none::-webkit-inner-spin-button {
appearance: none;
-webkit-appearance: none
}
.\[\&\:\:-webkit-outer-spin-button\,_\&\:\:-webkit-inner-spin-button\]\:m_0::-webkit-outer-spin-button, .\[\&\:\:-webkit-outer-spin-button\,_\&\:\:-webkit-inner-spin-button\]\:m_0::-webkit-inner-spin-button {
margin: var(--spacing-0)
}
.w_240px {
width: 240px
}
.w_300px {
width: 300px
}
.h_320px {
height: 320px
}
.h_400px {
height: 400px
}
.p_16px {
padding: 16px
}
.p_20px {
padding: 20px
}
.border_3px_solid {
border: 3px solid
}
@@ -1390,10 +1432,6 @@
--gradient-to: var(--colors-orange-100)
}
.p_20px {
padding: 20px
}
.shadow_0_8px_24px_rgba\(0\,_0\,_0\,_0\.1\) {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1)
}
@@ -1418,20 +1456,72 @@
transition: all 0.2s ease
}
.px_6 {
padding-inline: var(--spacing-6)
.bottom_-50px {
bottom: -50px
}
.min-w_120px {
min-width: 120px
.bottom_-60px {
bottom: -60px
}
.shadow_0_4px_12px_rgba\(59\,_130\,_246\,_0\.15\) {
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15)
}
.flex-wrap_wrap {
flex-wrap: wrap
.min-w_160px {
min-width: 160px
}
.cursor_default {
cursor: default
}
.ring_none {
outline: var(--borders-none)
}
.bottom_-2px {
bottom: -2px
}
.left_50\% {
left: 50%
}
.transform_translateX\(-50\%\) {
transform: translateX(-50%)
}
.w_80\% {
width: 80%
}
.h_2px {
height: 2px
}
.bg_blue\.400 {
background: var(--colors-blue-400)
}
.animation_pulse_1s_infinite {
animation: pulse 1s infinite
}
.shrink_0 {
flex-shrink: 0
}
.flex-wrap_nowrap {
flex-wrap: nowrap
}
.min-w_60px {
min-width: 60px
}
.min-w_auto {
min-width: auto
}
.bg_gray\.100 {
@@ -1442,6 +1532,22 @@
color: var(--colors-gray-700)
}
.flex-wrap_wrap {
flex-wrap: wrap
}
.min-w_40px {
min-width: 40px
}
.min-w_32px {
min-width: 32px
}
.px_2 {
padding-inline: var(--spacing-2)
}
.bg_blue\.100 {
background: var(--colors-blue-100)
}
@@ -1450,6 +1556,14 @@
transition: all 0.2s ease
}
.mt_16 {
margin-top: var(--spacing-16)
}
.mt_0 {
margin-top: var(--spacing-0)
}
.max-w_450px {
max-width: 450px
}
@@ -1470,10 +1584,18 @@
border-radius: var(--radii-lg)
}
.w_144pt {
width: 144pt
}
.w_180pt {
width: 180pt
}
.h_192pt {
height: 192pt
}
.h_240pt {
height: 240pt
}
@@ -2016,6 +2138,26 @@
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1)
}
.\[\&\:focus\]\:text_blue\.600:focus {
color: var(--colors-blue-600)
}
.\[\&\:focus\]\:bg_blue\.50:focus {
background: var(--colors-blue-50)
}
.\[\&\:focus\]\:rounded_md:focus {
border-radius: var(--radii-md)
}
.\[\&\:focus\]\:ring_none:focus {
outline: var(--borders-none)
}
.\[\&\:focus\]\:border_none:focus {
border: var(--borders-none)
}
.hover\:bg_red\.600:is(:hover, [data-hover]) {
background: var(--colors-red-600)
}
@@ -2116,6 +2258,10 @@
background: var(--colors-brand-50)
}
.hover\:bg_blue\.100:is(:hover, [data-hover]) {
background: var(--colors-blue-100)
}
.hover\:shadow_0_12px_32px_rgba\(0\,_0\,_0\,_0\.15\):is(:hover, [data-hover]) {
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15)
}