feat(nav): bouton action intégré dans la navbar v0.4.8
- ActionButtonContext : contexte React permettant aux pages d'injecter leur bouton action dans la navbar (Shopping=fa-cart-plus, Todos/Notes=+) - BottomNav : 5e slot dédié avec dock circulaire visuel permanent ; bouton rendu 10px au-dessus du centre du slot (effet soulevé) - Layout : ActionButtonProvider + overflow visible sur le conteneur nav - Pages : useEffect enregistre/vide le bouton action — plus de FAB flottant sur le contenu, liste entièrement visible Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import type { Note, NoteFilters } from '../api/notes'
|
||||
import { fetchNotes, createNote, updateNote, deleteNote, addAttachment, deleteAttachment } from '../api/notes'
|
||||
import Modal from '../components/Modal'
|
||||
import NoteForm from '../components/notes/NoteForm'
|
||||
import { useActionButton } from '../contexts/ActionButtonContext'
|
||||
|
||||
const noSelect: React.CSSProperties = { userSelect: 'none' }
|
||||
|
||||
@@ -163,6 +164,24 @@ export default function NotesPage() {
|
||||
const [searchInput, setSearchInput] = useState('')
|
||||
const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const { setActionButton } = useActionButton()
|
||||
useEffect(() => {
|
||||
setActionButton(
|
||||
<button
|
||||
onClick={() => setShowForm(true)}
|
||||
aria-label="Nouvelle note"
|
||||
style={{
|
||||
width: 56, height: 56, borderRadius: '50%',
|
||||
background: 'var(--accent)', color: '#1d2021', border: 'none',
|
||||
fontSize: 24, cursor: 'pointer',
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.5)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}
|
||||
>+</button>
|
||||
)
|
||||
return () => setActionButton(null)
|
||||
}, [setActionButton])
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
@@ -348,20 +367,6 @@ export default function NotesPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FAB */}
|
||||
<button
|
||||
className="fixed bottom-[10px] right-3 lg:bottom-6 lg:right-6"
|
||||
onClick={() => setShowForm(true)}
|
||||
aria-label="Nouvelle note"
|
||||
style={{
|
||||
width: 56, height: 56, borderRadius: '50%',
|
||||
background: 'var(--accent)', color: '#1d2021',
|
||||
border: 'none', fontSize: 28, cursor: 'pointer',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>+</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// frontend/src/pages/ShoppingPage.tsx
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { matchesSearch } from '../utils/search'
|
||||
import type { ShoppingListDetail, ShoppingList, Store, Product, ShoppingItem } from '../api/shopping'
|
||||
import {
|
||||
@@ -13,6 +13,7 @@ import ItemRow from '../components/shopping/ItemRow'
|
||||
import CatalogueModal from '../components/shopping/CatalogueModal'
|
||||
import BoutiquesModal from '../components/shopping/BoutiquesModal'
|
||||
import { useWakeLock } from '../hooks/useWakeLock'
|
||||
import { useActionButton } from '../contexts/ActionButtonContext'
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
@@ -76,6 +77,33 @@ export default function ShoppingPage() {
|
||||
|
||||
useWakeLock(currentList !== null)
|
||||
|
||||
const { setActionButton } = useActionButton()
|
||||
const openAddSheetRef = useRef(openAddSheet)
|
||||
useEffect(() => { openAddSheetRef.current = openAddSheet })
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentList || showAddSheet) {
|
||||
setActionButton(null)
|
||||
return
|
||||
}
|
||||
setActionButton(
|
||||
<button
|
||||
onClick={() => openAddSheetRef.current()}
|
||||
aria-label="Ajouter un article"
|
||||
style={{
|
||||
width: 56, height: 56, borderRadius: '50%',
|
||||
background: 'var(--accent)', color: '#1d2021', border: 'none',
|
||||
fontSize: 22, cursor: 'pointer',
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.5)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<i className="fa-solid fa-cart-plus" />
|
||||
</button>
|
||||
)
|
||||
return () => setActionButton(null)
|
||||
}, [currentList, showAddSheet, setActionButton])
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
@@ -526,22 +554,6 @@ export default function ShoppingPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* FAB — masqué quand le sheet est ouvert */}
|
||||
{!showAddSheet && (
|
||||
<button
|
||||
onClick={openAddSheet}
|
||||
aria-label="Ajouter un article"
|
||||
style={{
|
||||
position: 'fixed', bottom: 10, right: 12,
|
||||
width: 56, height: 56, borderRadius: '50%',
|
||||
background: 'var(--accent)', color: '#1d2021', border: 'none',
|
||||
fontSize: 22, cursor: 'pointer', boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<i className="fa-solid fa-cart-plus" />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fetchTodos, createTodo, updateTodo, deleteTodo, postponeTodo } from '..
|
||||
import SwipeableRow from '../components/todos/SwipeableRow'
|
||||
import TodoForm, { DOMAIN_COLORS } from '../components/todos/TodoForm'
|
||||
import Modal from '../components/Modal'
|
||||
import { useActionButton } from '../contexts/ActionButtonContext'
|
||||
|
||||
type EditingTodo = Todo | null
|
||||
|
||||
@@ -41,6 +42,24 @@ export default function TodosPage() {
|
||||
const [editingTodo, setEditingTodo] = useState<EditingTodo>(null)
|
||||
const [filters, setFilters] = useState<TodoFilters>({ status: 'pending' })
|
||||
|
||||
const { setActionButton } = useActionButton()
|
||||
useEffect(() => {
|
||||
setActionButton(
|
||||
<button
|
||||
onClick={() => setShowForm(true)}
|
||||
aria-label="Nouvelle tâche"
|
||||
style={{
|
||||
width: 56, height: 56, borderRadius: '50%',
|
||||
background: 'var(--accent)', color: '#1d2021', border: 'none',
|
||||
fontSize: 24, cursor: 'pointer',
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.5)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}
|
||||
>+</button>
|
||||
)
|
||||
return () => setActionButton(null)
|
||||
}, [setActionButton])
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
@@ -395,27 +414,6 @@ export default function TodosPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* FAB création — mobile : au-dessus de la barre de nav ; laptop : coin bas-droit */}
|
||||
<button
|
||||
className="fixed bottom-[10px] right-3 lg:bottom-6 lg:right-6"
|
||||
onClick={() => setShowForm(true)}
|
||||
aria-label="Nouvelle tâche"
|
||||
style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: '50%',
|
||||
background: 'var(--accent)',
|
||||
color: '#1d2021',
|
||||
border: 'none',
|
||||
fontSize: 28,
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>+</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user