mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-08 18:56:37 +08:00
* docs: add floating save button design spec for issue #266 * docs: add implementation plan for floating save button * feat(config): add floating save button (FAB) for issue #266
This commit is contained in:
@@ -0,0 +1,132 @@
|
|||||||
|
# Floating Save Button Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add a floating save button (FAB) to the config page that appears when configuration changes are detected.
|
||||||
|
|
||||||
|
**Architecture:** Inline FAB implementation using framer-motion AnimatePresence for enter/exit animations. Fixed-position circular button with slide-up animation, reusing existing hasChanges state and handleSave function.
|
||||||
|
|
||||||
|
**Tech Stack:** React, framer-motion (v11.18.2), shadcn-bridge/heroui Button, Tailwind CSS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Action | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `vite-frontend/src/pages/config.tsx` | Modify | Add FAB imports and component at page bottom |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add framer-motion Imports
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `vite-frontend/src/pages/config.tsx:1-5`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add AnimatePresence and motion imports**
|
||||||
|
|
||||||
|
Add import statement after existing framer-motion imports (or at top if none exist).
|
||||||
|
|
||||||
|
Current imports at line 1-2:
|
||||||
|
```typescript
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
```
|
||||||
|
|
||||||
|
Add new import after line 2:
|
||||||
|
```typescript
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit import addition**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add vite-frontend/src/pages/config.tsx
|
||||||
|
git commit -m "feat(config): add framer-motion imports for FAB animation"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Add FAB Component
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `vite-frontend/src/pages/config.tsx:1220-1224` (end of component)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add FAB at end of component (before closing div)**
|
||||||
|
|
||||||
|
Locate the end of `ConfigPage` component (line ~1223, the closing `</div>` after all modals).
|
||||||
|
|
||||||
|
Insert FAB component before the closing `</div>`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
{/* Floating Save Button (FAB) */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{hasChanges && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: 100, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
exit={{ y: 100, opacity: 0 }}
|
||||||
|
transition={{ type: "spring", damping: 20, stiffness: 300 }}
|
||||||
|
className="fixed bottom-6 right-6 z-50"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
color="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-12 h-12 rounded-full shadow-lg"
|
||||||
|
isLoading={saving}
|
||||||
|
onPress={handleSave}
|
||||||
|
>
|
||||||
|
{!saving && <SaveIcon className="w-5 h-5" />}
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run dev server to verify**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd vite-frontend && npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual verification checklist:
|
||||||
|
- Open config page at http://localhost:3000/config
|
||||||
|
- Modify any config field
|
||||||
|
- Verify FAB appears with slide-up animation
|
||||||
|
- Click FAB to save
|
||||||
|
- Verify FAB disappears with slide-down animation after save
|
||||||
|
- Scroll page and verify FAB stays fixed in viewport corner
|
||||||
|
- Test on mobile viewport (resize browser or use dev tools)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit FAB implementation**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add vite-frontend/src/pages/config.tsx
|
||||||
|
git commit -m "feat(config): add floating save button (FAB) for issue #266"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Summary
|
||||||
|
|
||||||
|
| Requirement | Verification Method |
|
||||||
|
|-------------|---------------------|
|
||||||
|
| FAB hidden by default | Visual: no FAB on page load with no changes |
|
||||||
|
| FAB appears on change | Visual: modify field → FAB slides up |
|
||||||
|
| Fixed position | Visual: scroll page → FAB stays in corner |
|
||||||
|
| Slide-up animation | Visual: observe animation timing/bounce |
|
||||||
|
| Slide-down on save | Visual: click save → FAB slides down |
|
||||||
|
| Loading state | Visual: click save → spinner shown during save |
|
||||||
|
| Mobile compatibility | Visual: resize to mobile viewport → same behavior |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review Checklist
|
||||||
|
|
||||||
|
- [x] Spec coverage: All requirements from design doc covered (imports + FAB component, animation params, button style, interaction behavior)
|
||||||
|
- [x] No placeholders: All code shown, no TBD/TODO
|
||||||
|
- [x] Type consistency: SaveIcon (line 45-59), handleSave (line 372-434), hasChanges (line 214), saving (line 213) all exist in config.tsx
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
# Floating Save Button Design
|
||||||
|
|
||||||
|
**Date:** 2026-04-01
|
||||||
|
**Issue:** https://github.com/Sagit-chu/flvx/issues/266
|
||||||
|
**Status:** Approved
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Add a Floating Action Button (FAB) to the config page (`vite-frontend/src/pages/config.tsx`) that appears when configuration changes are detected, allowing users to save without scrolling to the top.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
From Issue #266:
|
||||||
|
|
||||||
|
1. **Default hidden**: FAB not visible when no config changes
|
||||||
|
2. **Show on change**: Auto-display when `hasChanges` becomes true
|
||||||
|
3. **Fixed position**: Suspended at bottom-right corner, does not scroll with page
|
||||||
|
4. **Mobile compatible**: Same behavior on desktop and mobile devices
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
### 1. Implementation Approach
|
||||||
|
|
||||||
|
**Inline FAB in config.tsx** (not a reusable component)
|
||||||
|
|
||||||
|
- Rationale: Current need is limited to config page only
|
||||||
|
- State management (`hasChanges`, `saving`) already exists in the page
|
||||||
|
- framer-motion patterns already established in project
|
||||||
|
- Avoids over-abstraction (YAGNI)
|
||||||
|
|
||||||
|
### 2. UI Structure
|
||||||
|
|
||||||
|
Position: `fixed bottom-6 right-6` (24px from viewport edges)
|
||||||
|
|
||||||
|
Visual layout:
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────┐
|
||||||
|
│ [页面内容,可滚动] │
|
||||||
|
│ │
|
||||||
|
│ [●] │ ← FAB (fixed position)
|
||||||
|
└──────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Button Appearance
|
||||||
|
|
||||||
|
- Shape: Circular (`w-12 h-12 rounded-full`)
|
||||||
|
- Color: Primary (matches existing save button)
|
||||||
|
- Icon: SaveIcon (already defined in config.tsx)
|
||||||
|
- Shadow: `shadow-lg` for visual hierarchy
|
||||||
|
- Style: Icon-only (no text label)
|
||||||
|
|
||||||
|
### 4. Animation
|
||||||
|
|
||||||
|
Using framer-motion with `AnimatePresence`:
|
||||||
|
|
||||||
|
| Phase | Properties |
|
||||||
|
|-------|------------|
|
||||||
|
| `initial` | `{ y: 100, opacity: 0 }` - starts below viewport |
|
||||||
|
| `animate` | `{ y: 0, opacity: 1 }` - slides up to position |
|
||||||
|
| `exit` | `{ y: 100, opacity: 0 }` - slides back down on hide |
|
||||||
|
|
||||||
|
Transition config:
|
||||||
|
```typescript
|
||||||
|
transition={{ type: "spring", damping: 20, stiffness: 300 }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Spring parameters produce Material Design-like feel: smooth entrance, slight bounce settle.
|
||||||
|
|
||||||
|
### 5. Interaction Details
|
||||||
|
|
||||||
|
- **Click**: Calls existing `handleSave()` function
|
||||||
|
- **Loading state**: Button shows Spinner when `saving === true`
|
||||||
|
- **Hover**: Inherits Button component's primary color hover behavior
|
||||||
|
- **z-index**: `z-50` (above page content, below modals)
|
||||||
|
- **Prevent duplicate click**: Button disabled when `saving === true`
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
|
||||||
|
### Code Location
|
||||||
|
|
||||||
|
File: `vite-frontend/src/pages/config.tsx`
|
||||||
|
|
||||||
|
### Required Imports
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
```
|
||||||
|
|
||||||
|
### FAB Component Structure
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<AnimatePresence>
|
||||||
|
{hasChanges && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: 100, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
exit={{ y: 100, opacity: 0 }}
|
||||||
|
transition={{ type: "spring", damping: 20, stiffness: 300 }}
|
||||||
|
className="fixed bottom-6 right-6 z-50"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
color="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-12 h-12 rounded-full shadow-lg"
|
||||||
|
isLoading={saving}
|
||||||
|
onPress={handleSave}
|
||||||
|
>
|
||||||
|
{!saving && <SaveIcon className="w-5 h-5" />}
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Placement
|
||||||
|
|
||||||
|
Insert FAB at the end of the component, before the closing `</div>` (after all Cards and Modals).
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- framer-motion: Already installed (v11.18.2)
|
||||||
|
- Button: Already imported from `@/shadcn-bridge/heroui/button`
|
||||||
|
- SaveIcon: Already defined in config.tsx
|
||||||
|
|
||||||
|
## Behavior Matrix
|
||||||
|
|
||||||
|
| State | FAB Visibility | Button Enabled |
|
||||||
|
|-------|----------------|----------------|
|
||||||
|
| `hasChanges = false` | Hidden (not rendered) | N/A |
|
||||||
|
| `hasChanges = true, saving = false` | Visible, animating in | Yes |
|
||||||
|
| `hasChanges = true, saving = true` | Visible | No (loading) |
|
||||||
|
| Save success | Hidden (animating out) | N/A |
|
||||||
|
|
||||||
|
## Responsive Behavior
|
||||||
|
|
||||||
|
No special handling needed. `fixed bottom-6 right-6` works identically on:
|
||||||
|
- Desktop browsers
|
||||||
|
- Mobile browsers
|
||||||
|
- H5/WebView mode
|
||||||
|
|
||||||
|
The FAB maintains consistent 24px margin from viewport edges regardless of screen size.
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
|
||||||
|
1. **Multiple rapid toggles**: AnimatePresence handles gracefully - exit animation completes before new enter animation
|
||||||
|
2. **Page unload with unsaved changes**: Not addressed in this design (separate concern)
|
||||||
|
3. **FAB covers existing warning banner**: z-50 places FAB above the warning banner at line 1004-1013
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
After implementation, verify:
|
||||||
|
|
||||||
|
- [ ] FAB appears when any config field is modified
|
||||||
|
- [ ] FAB slides up from bottom on appearance
|
||||||
|
- [ ] FAB slides down to bottom on disappearance
|
||||||
|
- [ ] FAB fixed position during page scroll
|
||||||
|
- [ ] FAB triggers save on click
|
||||||
|
- [ ] FAB shows spinner during save
|
||||||
|
- [ ] FAB disappears after successful save
|
||||||
|
- [ ] FAB works on mobile viewport
|
||||||
|
- [ ] FAB does not interfere with Modal dialogs
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
@@ -1220,6 +1221,30 @@ export default function ConfigPage() {
|
|||||||
)}
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Floating Save Button (FAB) */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{hasChanges && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: 100, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
exit={{ y: 100, opacity: 0 }}
|
||||||
|
transition={{ type: "spring", damping: 20, stiffness: 300 }}
|
||||||
|
className="fixed bottom-6 right-6 z-50"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
color="primary"
|
||||||
|
size="lg"
|
||||||
|
className="w-12 h-12 rounded-full shadow-lg"
|
||||||
|
isLoading={saving}
|
||||||
|
onPress={handleSave}
|
||||||
|
>
|
||||||
|
{!saving && <SaveIcon className="w-5 h-5" />}
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user