Generated by Spark: half the app is gone

This commit is contained in:
2026-01-17 15:06:34 +00:00
committed by GitHub
parent fa967521b9
commit 76be2e63d6
4 changed files with 112 additions and 37 deletions

67
PRD.md
View File

@@ -1,61 +1,72 @@
# Planning Guide
A simple, interactive counter application that allows users to increment and decrement a numerical value with clear visual feedback.
A unique, constraint-based counter application where reaching a new maximum permanently limits future counting to half that value - creating an intriguing game of strategic counting.
**Experience Qualities**:
1. **Playful** - The counter should feel fun and responsive, encouraging interaction through satisfying button clicks and smooth animations.
2. **Clear** - The current count value should be immediately obvious and easy to read at any time.
3. **Tactile** - Button interactions should provide strong visual feedback that makes the interface feel physical and responsive.
1. **Intriguing** - The counter should create curiosity and tension as users discover the constraint mechanic and consider their counting strategy.
2. **Clear** - The current count value and imposed limits should be immediately obvious and easy to understand.
3. **Consequential** - Every increment feels meaningful because reaching new highs creates permanent limitations.
**Complexity Level**: Micro Tool (single-purpose application) - This is a focused counter with a single clear purpose: track a numerical value through increment and decrement actions.
**Complexity Level**: Micro Tool (single-purpose application) - This is a focused counter with a clever twist: once you reach a number, you can only count up to half of your all-time maximum.
## Essential Features
**Increment Counter**
- Functionality: Increases the counter value by 1
- Purpose: Allows users to count up for any tracking need
**Increment Counter (with Constraints)**
- Functionality: Increases the counter value by 1, but only up to half of the all-time maximum
- Purpose: Allows users to count up while experiencing the consequence of their previous highest value
- Trigger: Click/tap the increment (+) button
- Progression: User clicks button → Value increases by 1 → New value displays with brief animation
- Success criteria: Counter value increases correctly and persists between sessions
- Progression: User clicks button → System checks if increment would exceed limit → If allowed, value increases by 1 with animation → If blocked, error toast appears explaining the constraint
- Success criteria: Counter increases correctly when under limit, button disables at limit, error message displays when limit reached
**Track Maximum Reached**
- Functionality: Automatically tracks the highest value ever reached and calculates half of it as the new limit
- Purpose: Creates the core constraint mechanic that makes each new high consequential
- Trigger: Automatic when counter value exceeds previous maximum
- Progression: Counter reaches new high → Maximum value updates → Limit recalculates to half of new maximum → Limit indicator appears/updates
- Success criteria: Maximum tracks correctly, limit displays accurately, constraint is enforced
**Decrement Counter**
- Functionality: Decreases the counter value by 1
- Purpose: Allows users to count down or correct mistakes
- Functionality: Decreases the counter value by 1 (no lower limit)
- Purpose: Allows users to count down without restrictions
- Trigger: Click/tap the decrement (-) button
- Progression: User clicks button → Value decreases by 1 → New value displays with brief animation
- Success criteria: Counter value decreases correctly and persists between sessions
**Reset Counter**
- Functionality: Returns the counter to zero
- Purpose: Quickly start fresh without multiple decrements
- Functionality: Returns both the counter and maximum reached to zero, removing all limits
- Purpose: Start completely fresh and restore "full power" to the counter
- Trigger: Click/tap the reset button
- Progression: User clicks reset → Counter returns to 0 → Visual confirmation
- Success criteria: Counter resets to zero instantly
- Progression: User clicks reset → Counter and max both return to 0 → Limit indicator disappears → Success toast appears → Visual confirmation
- Success criteria: Counter and maximum both reset to zero, limits are removed, app returns to unlimited state
## Edge Case Handling
- **Negative Numbers**: Allow negative values - no lower limit restriction
- **Large Numbers**: Display properly formatted large numbers (with commas for readability)
- **Rapid Clicking**: Handle multiple rapid button presses smoothly without lag
- **Initial Load**: Start at 0 if no saved value exists
- **Rapid Clicking**: Handle multiple rapid button presses smoothly, showing toast only once when hitting limit
- **Initial Load**: Start at 0 with no limits if no saved value exists
- **At Limit State**: Disable increment button and show visual warning when at the imposed limit
- **Odd Maximum Values**: When maximum is odd (e.g., 7), half rounds down (limit becomes 3)
## Design Direction
The design should evoke a sense of **precision, control, and satisfaction** - like using a premium mechanical device. Think digital tally counter meets modern minimalism, with bold typography that makes the number feel important and tactile buttons that beg to be pressed.
The design should evoke a sense of **tension, consequence, and strategic thinking** - like a puzzle or game where every action has weight. The interface should clearly communicate the constraint mechanic through warning colors and limit indicators, while maintaining the satisfying tactile feel of the original counter. Think digital tally counter meets resource management game.
## Color Selection
A bold, high-contrast scheme that feels modern and precise.
A bold, high-contrast scheme with added warning colors to communicate constraints.
- **Primary Color**: Deep Electric Blue (oklch(0.45 0.20 240)) - Communicates precision and digital accuracy, used for primary action buttons
- **Secondary Colors**:
- Rich Navy (oklch(0.15 0.03 240)) for depth and cards
- Slate Gray (oklch(0.25 0.02 240)) for secondary elements
- **Accent Color**: Vibrant Cyan (oklch(0.75 0.15 200)) - Eye-catching highlight for the counter value itself and focus states
- **Destructive Color**: Warning Red (oklch(0.577 0.245 27.325)) - Used for limit warnings and error states
- **Foreground/Background Pairings**:
- Background (Dark Navy oklch(0.10 0.02 240)): Light Gray text (oklch(0.95 0.01 240)) - Ratio 15.2:1 ✓
- Primary (Electric Blue oklch(0.45 0.20 240)): White text (oklch(0.98 0 0)) - Ratio 5.8:1 ✓
- Accent (Vibrant Cyan oklch(0.75 0.15 200)): Dark Navy text (oklch(0.10 0.02 240)) - Ratio 12.1:1 ✓
- Destructive (Warning Red oklch(0.577 0.245 27.325)): White text (oklch(0.98 0 0)) - Ratio 4.5:1 ✓
## Font Selection
@@ -68,32 +79,40 @@ Typography should feel technical yet approachable, with numeric characters that
## Animations
Animations should emphasize the counting action - the number should briefly scale and glow when changed, buttons should have satisfying press states with subtle scale transforms, and all transitions should use snappy easing (0.2s) to feel responsive. The reset action gets a more pronounced animation to signal the larger state change.
Animations should emphasize both the counting action and the constraint system - the number should briefly scale and glow when changed, buttons should have satisfying press states with subtle scale transforms, and the limit indicator should smoothly animate in when constraints activate. Error states get a shake animation. All transitions should use snappy easing (0.2s) to feel responsive. The reset action gets a more pronounced animation to signal the restoration of full functionality.
## Component Selection
- **Components**:
- Button (shadcn) for all interactive controls, with `size="lg"` for main increment/decrement, and `variant="outline"` for reset
- Button (shadcn) for all interactive controls, with `size="lg"` for main increment/decrement, `variant="outline"` for reset, and `disabled` state for increment when at limit
- Card (shadcn) as the main container for the counter interface
- Separator (shadcn) to divide counter from controls
- Toast (sonner) for error messages when hitting limit and success message on reset
- **Customizations**:
- Custom counter display component with animated number transitions using framer-motion
- Button hover/active states enhanced with scale transforms and glow effects
- Limit indicator that appears beside the counter value showing the imposed maximum
- Warning banner component with destructive styling that explains the constraint
- Button hover/active states enhanced with scale transforms
- Gradient background pattern using CSS for visual interest
- **States**:
- Buttons: default has subtle shadow, hover scales to 1.05 and brightens, active scales to 0.95, disabled is muted
- Buttons: default has subtle shadow, hover scales to 1.05 and brightens, active scales to 0.95, disabled is muted and doesn't scale
- Counter display: pulses briefly on value change with scale animation
- Increment button: disabled state when at limit with reduced opacity
- Warning banner: only visible when a limit is active
- **Icon Selection**:
- Plus (bold weight) for increment
- Minus (bold weight) for decrement
- ArrowCounterClockwise (bold weight) for reset
- Warning (bold weight) for constraint alerts
- **Spacing**:
- Outer container: p-8
- Card padding: p-12
- Button gaps: gap-4
- Section spacing: space-y-8
- Warning banner: p-3 with gap-2 for icon and text
- **Mobile**:
- Stack buttons vertically on mobile with full-width layout
- Reduce counter font size to 64px on small screens
- Maintain tap-friendly 44px minimum touch targets
- Card padding reduces to p-6 on mobile
- Limit indicator repositions for smaller screens

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Counter</title>
<title>Half Counter</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">

View File

@@ -1,3 +1,4 @@
{
"templateVersion": 0,
{
"templateVersion": 0,
"dbType": null
}

View File

@@ -2,14 +2,35 @@ import { useKV } from '@github/spark/hooks'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { Plus, Minus, ArrowCounterClockwise } from '@phosphor-icons/react'
import { motion } from 'framer-motion'
import { Plus, Minus, ArrowCounterClockwise, Warning } from '@phosphor-icons/react'
import { motion, AnimatePresence } from 'framer-motion'
import { toast } from 'sonner'
function App() {
const [count, setCount] = useKV<number>('counter-value', 0)
const [maxReached, setMaxReached] = useKV<number>('max-reached', 0)
const currentCount = count ?? 0
const currentMax = maxReached ?? 0
const limit = Math.floor(currentMax / 2)
const isLimited = currentMax > 0
const isAtLimit = isLimited && currentCount >= limit
const increment = () => {
setCount((current) => (current ?? 0) + 1)
const newCount = currentCount + 1
if (isLimited && newCount > limit) {
toast.error('Half the app is gone!', {
description: `You can only count to ${limit} now (half of ${currentMax})`
})
return
}
setCount(newCount)
if (newCount > currentMax) {
setMaxReached(newCount)
}
}
const decrement = () => {
@@ -18,9 +39,11 @@ function App() {
const reset = () => {
setCount(0)
setMaxReached(0)
toast.success('Counter reset - full power restored!')
}
const formattedCount = (count ?? 0).toLocaleString()
const formattedCount = currentCount.toLocaleString()
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4 sm:p-8 relative overflow-hidden">
@@ -37,15 +60,17 @@ function App() {
<Card className="w-full max-w-md p-6 sm:p-12 shadow-2xl relative z-10 border-2">
<div className="space-y-8">
<div className="text-center">
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">Counter</h1>
<p className="text-sm text-muted-foreground">Track anything, one click at a time</p>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">Half Counter</h1>
<p className="text-sm text-muted-foreground">
{isLimited ? `Once you reach a number, you can only count to half of it` : 'Count up... if you dare'}
</p>
</div>
<Separator />
<div className="flex items-center justify-center py-8">
<div className="flex items-center justify-center py-8 relative">
<motion.div
key={count}
key={currentCount}
initial={{ scale: 1.2, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
@@ -57,8 +82,37 @@ function App() {
{formattedCount}
</div>
</motion.div>
<AnimatePresence>
{isLimited && (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
className="absolute -right-2 sm:-right-8 top-1/2 -translate-y-1/2"
>
<div className="text-sm text-muted-foreground text-right">
<div className="text-xs opacity-60">limit</div>
<div className="text-lg font-bold text-destructive">{limit}</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{isLimited && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="bg-destructive/10 border border-destructive/20 rounded-lg p-3 flex items-start gap-2"
>
<Warning className="text-destructive shrink-0 mt-0.5" weight="bold" size={20} />
<div className="text-xs text-destructive">
<strong>Half the app is gone!</strong> Your max was {currentMax}, now you can only reach {limit}.
</div>
</motion.div>
)}
<Separator />
<div className="flex flex-col sm:flex-row gap-4">
@@ -73,7 +127,8 @@ function App() {
<Button
onClick={increment}
size="lg"
className="flex-1 h-14 text-lg font-medium transition-all hover:scale-105 active:scale-95"
disabled={isAtLimit}
className="flex-1 h-14 text-lg font-medium transition-all hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
>
<Plus className="mr-2" weight="bold" />
Increment