mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
This repo could never sync. There was NO merge-base with tamagui — 372 commits
here against 15,791 there, unrelated histories — because it began as a
copy-paste rebrand with a flattened layout. Every "sync" since was a hand
re-application that drifted, which is why the tree sat on 7.3.1 while upstream
shipped fixes we then rediscovered by hand.
This commit's PARENT IS TAMAGUI. A future sync is `git merge tama/main` then
`node scripts/rebrand.mjs` — a real merge, because the history is now shared.
174 packages: 166 upstream, our 8 restored (chrome, shell, telemetry, recipes,
get, font-geist-{sans,mono}, @hanzo/booker), bento-get gained. 170/170 build.
Our patches to files UPSTREAM ALSO OWNS are carried, which the package-level
delta missed and which was where the value actually was:
- useMedia's server snapshot (`initState ?? getMedia()`). Upstream still
returns undefined there, so every Tamagui Dialog kills a prerender — the
exact crash hanzo.app spent a day on.
- data-slot on web host nodes, absent upstream entirely.
Both applied ONTO upstream's newer files (it rewrote useMedia +150/-63 and
createComponent +160/-55), marked `PATCH (ours, carried across the transplant)`.
Seven traps are encoded in scripts/rebrand.mjs, each found by a gate:
skip itself and lockfiles; special-case the double-branded names in BOTH scoped
and bare form; map path strings, not just paths; drop a level for the flattened
group; check disk rather than a stale index.
Rust build artifacts are NOT committed. An earlier draft of this commit swept
apps/desktop/src-tauri/target/debug in via `git add -A` — 3,859 files including
a 272MB .a, over GitHub's 100MB cap, so the push could never have landed. They
are gitignored now.
Co-Authored-By: Hanzo Dev <[email protected]>
14 KiB
14 KiB
Toast v2 Implementation Plan
Overview
Revamp @hanzogui/toast to v2, inspired by Sonner's excellent UX. Must be fully cross-platform using Gui's animation system - NO CSS cheats.
Reference Implementations
- ~/github/sonner - primary inspiration
- ~/github/base-ui - additional patterns
- @hanzogui/sheet - gesture/drag patterns, resistive pull
Current Session Issues Found & Fixes
BUGS FOUND (need fixing)
-
Top position toasts outside frame ✅ FIXED
- Issue: Toasts with top position were rendering outside the container
- Fix: Changed from hardcoded
bottom: 0to dynamic{...(isTop ? { top: 0 } : { bottom: 0 })}
-
Action buttons overlapping ✅ FIXED
- Issue: Cancel/Confirm buttons used ToastCloseButton (20x20px fixed) causing overlap
- Fix: Created new ToastActionButton component with proper padding for text buttons
-
Hover stack → leave → rest fly away, one stays ✅ FIXED
- Issue: When hovering stacked toasts then leaving, most toasts dismiss but one stays longer
- Cause: pauseTimer was being called multiple times, double-counting elapsed time
- Fix: Added
lastPauseTimeRefguard (Sonner pattern) - only calculate elapsed time if timer was started after last pause
-
Exit animations fly too far ✅ FIXED
- Issue: Toasts flew 100px on exit, too dramatic
- Fix: Reduced to 30px for swipe, 10px for normal exit
-
Focus outline shows on click ✅ FIXED
- Issue: Focus ring appeared on mouse click, should only show on keyboard
- Fix: Changed
focusStyletofocusVisibleStyle
-
Expanded state showed ALL toasts ✅ FIXED
- Issue: When expanded, ALL toasts showed instead of respecting visibleToasts limit
- Sonner behavior: visibleToasts limit applies in BOTH collapsed and expanded states
- Fix: Changed opacity logic to hide toasts beyond visibleToasts in both states
- Also added
pointerEvents: 'none'for hidden toasts
CHANGES MADE THIS SESSION
- visibleToasts default: 3 → 4
- Fade out last visible toast - opacity 0.5 for toast at visibleToasts-1 index
- Top position anchor - dynamic top/bottom based on position
- ToastActionButton component - proper text button for action/cancel
- Subtle exit animations - 10px/30px instead of 100px
- focusVisibleStyle - outline only on keyboard navigation
DESIGN DECISIONS (user feedback)
- Keep styles minimal in core - demo can add more styles
- No sonnerStyle close button - keep it simple, inline close button is fine
- Use animateOnly={['transform', 'opacity']} in examples for performance
- Export .Close component - like Dialog, just wraps events without styling
Style Checklist
Container (Toaster)
- position: fixed (web) / absolute (native)
- z-index: 100000+ (very high)
- width: 356px (TOAST_WIDTH constant)
- Viewport offsets: 24px desktop
- pointerEvents: 'box-none' for pass-through
Position Variants
- bottom-right: bottom + right offsets
- bottom-left: bottom + left offsets
- bottom-center: bottom + left 50% + translateX(-50%)
- top-right: top + right offsets ✅ FIXED anchor
- top-left: top + left offsets ✅ FIXED anchor
- top-center: top + left 50% + translateX(-50%) ✅ FIXED anchor
Toast Item Frame
- position: absolute (within container)
- left: 0, right: 0 (full width of container)
- Dynamic anchor: top:0 for top positions, bottom:0 for bottom positions ✅ FIXED
- background: $background
- border-radius: $4 (8px)
- paddingHorizontal: $4, paddingVertical: $3
- border: 1px solid $borderColor
- elevation: $4 + shadow for depth
- focusable: true
- focusVisibleStyle instead of focusStyle ✅ FIXED
Toast Content Layout
- flex row with icon, content, close button
- icon: 16x16px, flex-shrink: 0
- content: flex column, gap $1
- title: fontWeight 600, $color, size $4
- description: $color11, size $2
- gap: $3 between elements
Close Button
- positioned inline (user decided against absolute overlap)
- 20x20px circle
- borderRadius: $10 (circular)
- backgroundColor: $color5
- hoverStyle: $color6
- pressStyle: $color7
Action Buttons ✅ FIXED
- New ToastActionButton component with proper sizing
- borderRadius: $2
- paddingHorizontal: $2
- height: 24px
- primary variant for action button (dark bg, light text)
- marginTop: $3 spacing
- gap: $2 between buttons
- justifyContent: flex-end
Rich Colors (Type Variants)
- success: $green2 background, $green6 border
- error: $red2 background, $red6 border
- warning: $yellow2 background, $yellow6 border
- info: $blue2 background, $blue6 border
- loading: default (neutral)
Icons
- success: ✓ in $green10
- error: ✕ in $red10
- warning: ⚠ in $yellow10
- info: ℹ in $blue10
- loading: ⟳ in $color11
- close: ✕ in $color11
Feature Checklist
Core Toast API
- toast() - basic toast
- toast.success() - success type
- toast.error() - error type
- toast.warning() - warning type
- toast.info() - info type
- toast.loading() - loading type (no auto-dismiss)
- toast.promise() - promise with loading/success/error states
- toast.custom() - custom JSX
- toast.dismiss(id) - dismiss specific toast
- toast.dismiss() - dismiss all
Toast Options
- id - custom id for updating
- title - main text (string or function)
- description - secondary text (string or function)
- duration - auto-dismiss time (default 4000ms)
- icon - custom icon
- action - action button with label/onClick
- cancel - cancel button
- dismissible - can be dismissed (default true)
- onDismiss - callback when dismissed
- onAutoClose - callback when auto-closed
- closeButton - per-toast close button override
Toaster Props
- position - 6 positions
- expand - always show expanded
- visibleToasts - max visible (default 4) ✅ CHANGED from 3
- gap - space between toasts (default 14px)
- duration - default duration (4000ms)
- offset - viewport padding (24px)
- hotkey - keyboard shortcut (alt+T)
- swipeDirection - dismiss direction ('right')
- swipeThreshold - swipe distance (50px)
- closeButton - show close buttons
- richColors - colored backgrounds
- icons - custom icons per type
- theme - light/dark/system
Stacking Behavior
- Only show visibleToasts (default 4)
- Scale down non-front toasts:
1 - (index * 0.05) - Y-offset for collapsed stack: 10px per toast
- Expanded state shows full offset with gaps
- Front toast height applied to hidden toasts
- z-index:
visibleToasts - index(front highest) - transformOrigin: 'bottom center' for bottom, 'top center' for top
- Fade out last visible toast (opacity 0.5) ✅ NEW
Hover Expand ✅ COMPLETE
- Expand on hover (onMouseEnter + onMouseMove)
- Pause timers when expanded/hovered
- Resume timers when mouse leaves
- Gap filler View to prevent flicker when mouse moves between toasts
- Collapse when only 1 toast remains
- interacting state tracks pointer down/up
- Timer sync issue when leaving hover ✅ FIXED with lastPauseTimeRef guard
Swipe to Dismiss ✅ COMPLETE
- Pointer event tracking (setPointerCapture)
- Direction lock on first significant movement
- Threshold-based dismiss (50px default)
- Velocity-based dismiss (0.11 px/ms)
- Resistive pull in wrong direction (sqrt curve from Sheet)
- Exit animation in swipe direction
- Snap back on cancel
Enter/Exit Animations ✅ REFINED
- enterStyle: opacity 0, y: ±10, scale: 0.95 (subtle)
- exitStyle: opacity 0, x/y: ±30 for swipe, ±10 for normal, scale: 0.95
- AnimatePresence for mount/unmount
- transition: 'quick' (Gui animation)
- Disable animation while dragging
Keyboard Support
- Escape to dismiss focused toast
- Hotkey to expand toaster (alt+T)
- Focus management (tabIndex, lastFocusedElementRef)
Accessibility
- role="status"
- aria-live="polite"
- aria-atomic
- aria-label on container
- tabIndex for focusable
- aria-label="Close toast" on close button
Test Checklist
Basic Toast Tests ✅
- shows a default toast
- shows typed toasts (success, error, warning, info)
- shows loading toast
- shows multiple stacked toasts
Interaction Tests ✅
- dismisses all toasts
- closes toast when clicking close button
- keyboard escape dismisses focused toast
- shows action button
Promise Tests ✅
- promise toast transitions loading -> success
- promise toast shows error on rejection
Position Tests ✅
- changes position when clicking position buttons
- all 6 positions verified with screenshots
Hover Tests ✅
- hover expands stacked toasts
- hover pauses auto-dismiss timer
- mouse leave collapses stack
- no flicker when mouse moves between toasts
Swipe Tests ✅
- swipe right dismisses (default)
- swipe threshold works
- velocity-based dismiss
- resistive pull in wrong direction
- swipe cancel snaps back
Interrupt Tests ✅ NEW
- toast closing does not interrupt on mouse re-entry
- drag gesture moves toast visually
- fast swipe dismisses via velocity
TODO - Remaining Work (DEADLINE: 9:00 AM DEMO VIDEO)
TIMELINE
- NOW - 8:20: Fix top position bug, verify tests pass
- 8:20 - 8:35: Write clean ToastDemo, verify on hanzogui.dev /ui/toast (mobile + safe areas)
- 8:35 - 8:45: Update docs, update version-two blog post
- 8:45 - 8:55: Full yarn test, lint, typecheck, sub-agent review
- 8:55 - 9:00: Clean commit, push to CI, /alert user
CRITICAL - Before 8:20
- Fix timer sync bug ✅ DONE - added lastPauseTimeRef guard
- FIX: Top position toast outside viewport ✅ FIXED - explicitly set top/bottom to avoid conflicts
- Test action buttons after ToastActionButton fix ✅ VERIFIED - buttons render correctly
Before 8:35 (Demo Ready)
- Write clean ToastDemo for hanzogui.dev ✅ DONE - apps/demos/src/ToastDemo.tsx
- Verify on yarn dev site at /ui/toast ✅ VERIFIED - page renders, toast works
- Mobile web + safe areas check (SSR safe!) ✅ FIXED - use-window-dimensions isClient guard
- Web-only code but use conditionals for future native support
Before 8:45 (Docs + Blog)
- Update Toast docs - clean up (2.0.0.mdx exists but has old API docs)
- Update version-two blog post with Toast feature/demo hero
Before 8:55 (Testing + Review)
- yarn test - 31 toast tests passing ✅
- yarn lint, yarn typecheck ✅ - fixed biome issues, types clean
- Sub-agent code review
Before 9:00 (Ship It)
- Clean commit and push to CI
- /alert user every 30s until acknowledged
Low Priority (Post-Demo)
- Export .Close component (Dialog pattern)
- Add animateOnly={['transform', 'opacity']} to examples
- All animation drivers tested
- SVG icons instead of text characters
- RTL support
Key Formulas (from Sonner)
Stack Scale
scale = 1 - (index * 0.05)
// index 0: 1.0, index 1: 0.95, index 2: 0.90
Collapsed Y Offset
// For bottom position
stackY = isFront ? 0 : -peekAmount * index
// For top position
stackY = isFront ? 0 : peekAmount * index
Expanded Y Offset
offset = (heightIndex * gap) + toastsHeightBefore
// For bottom: -offset
// For top: +offset
Resistive Pull (from Sheet)
function resisted(delta: number, maxResist = 25): number {
if (delta >= 0) return delta
const pastBoundary = Math.abs(delta)
const resistedDistance = Math.sqrt(pastBoundary) * 2
return -Math.min(resistedDistance, maxResist)
}
Velocity Threshold
VELOCITY_THRESHOLD = 0.11 // px/ms
shouldDismiss = passedThreshold || velocity > VELOCITY_THRESHOLD
Gap Filler (prevents hover flicker)
// Invisible hit area above/below toast when expanded
const gapFillerHeight = expanded ? gap + 1 : 0
<View
position="absolute"
left={0}
right={0}
height={gapFillerHeight}
pointerEvents="auto"
{...(isTop ? { top: '100%' } : { bottom: '100%' })}
/>
Opacity Fade for Limit
// Fade out toasts as they approach visibility limit
let computedOpacity = 1
if (!expanded) {
if (index >= visibleToasts) {
computedOpacity = 0 // completely hidden beyond limit
} else if (index === visibleToasts - 1) {
computedOpacity = 0.5 // last visible toast fades
}
}
Files Modified
-
/pkgs/ui/toast/src/Toaster.tsx- Container positioning with minHeight: 1
- onMouseEnter + onMouseMove for hover expand
- Auto-collapse when 1 toast remains
- visibleToasts default: 4
-
/pkgs/ui/toast/src/ToastItem.tsx- Stacking logic (scale, y, z-index, height, transformOrigin)
- Gap filler View for hover flicker prevention
- dataSet for RN Web data attribute compatibility
- Enter/exit styles with AnimatePresence
- Dynamic top/bottom anchor based on position
- ToastActionButton component for action/cancel
- focusVisibleStyle for keyboard-only focus
- Subtle exit animations (10px/30px)
- Opacity fade for limit
-
/pkgs/ui/toast/src/useDragGesture.ts- Resistive pull with sqrt curve (Sheet pattern)
- Direction lock on first movement
- Velocity-based dismiss
-
/apps/kitchen-sink/tests/ToastMultiple.test.tsx- Loading toast test fix (text selector vs data-type)
-
/apps/kitchen-sink/tests/ToastHover.test.tsx- NEW- Hover expand tests
- Flicker prevention tests
- Timer pause tests
-
/apps/kitchen-sink/tests/ToastVisual.test.tsx- NEW- Visual verification tests
- Swipe tests
-
/apps/kitchen-sink/tests/ToastInterrupt.test.tsx- NEW- Interrupt behavior tests
- Drag gesture tests
- Velocity dismiss tests
Test Results Summary
Total: 25 tests passing
- Toast.test.tsx: 3 tests (focus management)
- ToastMultiple.test.tsx: 12 tests (core API)
- ToastHover.test.tsx: 3 tests (hover behavior)
- ToastVisual.test.tsx: 4 tests (swipe, visual)
- ToastInterrupt.test.tsx: 3 tests (interrupt, drag, velocity)
All tests pass with CSS animation driver.