Improve mobile UI: compact bottom nav and sticky action bar
All checks were successful
Deploy / deploy (push) Successful in 2m38s
All checks were successful
Deploy / deploy (push) Successful in 2m38s
- Enable daisyUI and use btm-nav component for compact bottom navigation - Add sticky ActionBar component for form submit buttons on mobile - Form buttons now float above the bottom nav, preventing obscuring - Update all form templates (actions, eggs, feed, move) to use ActionBar - Menu drawer header now shows AnimalTrack + version like desktop sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -206,7 +206,7 @@ def create_app(
|
|||||||
# because Starlette applies middleware in reverse order (last in list wraps first)
|
# because Starlette applies middleware in reverse order (last in list wraps first)
|
||||||
app, rt = fast_app(
|
app, rt = fast_app(
|
||||||
before=beforeware,
|
before=beforeware,
|
||||||
hdrs=(*Theme.slate.headers(), htmx_config), # Dark industrial theme + HTMX config
|
hdrs=(*Theme.slate.headers(daisy=True), htmx_config), # Dark theme + daisyUI
|
||||||
exts=["head-support", "preload"],
|
exts=["head-support", "preload"],
|
||||||
static_path=static_path_for_fasthtml,
|
static_path=static_path_for_fasthtml,
|
||||||
middleware=[
|
middleware=[
|
||||||
|
|||||||
64
src/animaltrack/web/templates/action_bar.py
Normal file
64
src/animaltrack/web/templates/action_bar.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# ABOUTME: Sticky action bar for mobile form submission.
|
||||||
|
# ABOUTME: Fixed above dock on mobile, inline on desktop.
|
||||||
|
|
||||||
|
from fasthtml.common import Div, Style
|
||||||
|
|
||||||
|
|
||||||
|
def ActionBarStyles(): # noqa: N802
|
||||||
|
"""CSS styles for sticky action bar - include in page head."""
|
||||||
|
return Style("""
|
||||||
|
/* Action bar sticks above btm-nav on mobile */
|
||||||
|
.action-bar {
|
||||||
|
position: fixed;
|
||||||
|
/* btm-nav-sm height ~4rem + safe-area-inset-bottom */
|
||||||
|
bottom: calc(4rem + env(safe-area-inset-bottom, 0));
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 45; /* Below btm-nav */
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background-color: rgba(20, 20, 19, 0.95);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border-top: 1px solid #404040;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop: inline, no fixed positioning */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.action-bar {
|
||||||
|
position: static;
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
backdrop-filter: none;
|
||||||
|
border-top: none;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def ActionBar(*buttons): # noqa: N802
|
||||||
|
"""
|
||||||
|
Sticky action bar for mobile forms.
|
||||||
|
|
||||||
|
On mobile: Fixed position above the dock (bottom nav).
|
||||||
|
On desktop: Inline at end of form.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# Buttons should have form="form-id" attribute to submit external forms
|
||||||
|
ActionBar(
|
||||||
|
Button("Cancel", cls=ButtonT.ghost, onclick="history.back()"),
|
||||||
|
Button("Save", type="submit", form="my-form", cls=ButtonT.primary),
|
||||||
|
)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*buttons: Button components to render in the action bar
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
FT component with the action bar
|
||||||
|
"""
|
||||||
|
return Div(
|
||||||
|
*buttons,
|
||||||
|
cls="action-bar",
|
||||||
|
)
|
||||||
@@ -19,6 +19,7 @@ from ulid import ULID
|
|||||||
from animaltrack.models.animals import Animal
|
from animaltrack.models.animals import Animal
|
||||||
from animaltrack.models.reference import Location, Species
|
from animaltrack.models.reference import Location, Species
|
||||||
from animaltrack.selection.validation import SelectionDiff
|
from animaltrack.selection.validation import SelectionDiff
|
||||||
|
from animaltrack.web.templates.action_bar import ActionBar
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Selection Diff Confirmation Panel
|
# Selection Diff Confirmation Panel
|
||||||
@@ -334,8 +335,10 @@ def cohort_form(
|
|||||||
event_datetime_field("cohort_datetime", datetime_value, datetime_ts),
|
event_datetime_field("cohort_datetime", datetime_value, datetime_ts),
|
||||||
# Hidden nonce for idempotency
|
# Hidden nonce for idempotency
|
||||||
Hidden(name="nonce", value=str(ULID())),
|
Hidden(name="nonce", value=str(ULID())),
|
||||||
# Submit button
|
# Submit button in sticky action bar for mobile
|
||||||
Button("Create Cohort", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
ActionBar(
|
||||||
|
Button("Create Cohort", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
||||||
|
),
|
||||||
# Form submission via standard action/method (hx-boost handles AJAX)
|
# Form submission via standard action/method (hx-boost handles AJAX)
|
||||||
action=action,
|
action=action,
|
||||||
method="post",
|
method="post",
|
||||||
@@ -463,8 +466,10 @@ def hatch_form(
|
|||||||
event_datetime_field("hatch_datetime"),
|
event_datetime_field("hatch_datetime"),
|
||||||
# Hidden nonce for idempotency
|
# Hidden nonce for idempotency
|
||||||
Hidden(name="nonce", value=str(ULID())),
|
Hidden(name="nonce", value=str(ULID())),
|
||||||
# Submit button
|
# Submit button in sticky action bar for mobile
|
||||||
Button("Record Hatch", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
ActionBar(
|
||||||
|
Button("Record Hatch", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
||||||
|
),
|
||||||
# Form submission via standard action/method (hx-boost handles AJAX)
|
# Form submission via standard action/method (hx-boost handles AJAX)
|
||||||
action=action,
|
action=action,
|
||||||
method="post",
|
method="post",
|
||||||
@@ -586,12 +591,14 @@ def promote_form(
|
|||||||
# Hidden fields
|
# Hidden fields
|
||||||
Hidden(name="animal_id", value=animal.animal_id),
|
Hidden(name="animal_id", value=animal.animal_id),
|
||||||
Hidden(name="nonce", value=str(ULID())),
|
Hidden(name="nonce", value=str(ULID())),
|
||||||
# Submit button - text changes for rename vs promote
|
# Submit button in sticky action bar for mobile
|
||||||
Button(
|
ActionBar(
|
||||||
"Save Changes" if is_rename else "Promote to Identified",
|
Button(
|
||||||
type="submit",
|
"Save Changes" if is_rename else "Promote to Identified",
|
||||||
cls=ButtonT.primary,
|
type="submit",
|
||||||
hx_disabled_elt="this",
|
cls=ButtonT.primary,
|
||||||
|
hx_disabled_elt="this",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
# Form submission via standard action/method (hx-boost handles AJAX)
|
# Form submission via standard action/method (hx-boost handles AJAX)
|
||||||
action=action,
|
action=action,
|
||||||
@@ -717,8 +724,10 @@ def tag_add_form(
|
|||||||
Hidden(name="roster_hash", value=roster_hash),
|
Hidden(name="roster_hash", value=roster_hash),
|
||||||
Hidden(name="confirmed", value=""),
|
Hidden(name="confirmed", value=""),
|
||||||
Hidden(name="nonce", value=str(ULID())),
|
Hidden(name="nonce", value=str(ULID())),
|
||||||
# Submit button
|
# Submit button in sticky action bar for mobile
|
||||||
Button("Add Tag", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
ActionBar(
|
||||||
|
Button("Add Tag", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
||||||
|
),
|
||||||
# Form submission via standard action/method (hx-boost handles AJAX)
|
# Form submission via standard action/method (hx-boost handles AJAX)
|
||||||
action=action,
|
action=action,
|
||||||
method="post",
|
method="post",
|
||||||
@@ -893,13 +902,15 @@ def tag_end_form(
|
|||||||
Hidden(name="roster_hash", value=roster_hash),
|
Hidden(name="roster_hash", value=roster_hash),
|
||||||
Hidden(name="confirmed", value=""),
|
Hidden(name="confirmed", value=""),
|
||||||
Hidden(name="nonce", value=str(ULID())),
|
Hidden(name="nonce", value=str(ULID())),
|
||||||
# Submit button
|
# Submit button in sticky action bar for mobile
|
||||||
Button(
|
ActionBar(
|
||||||
"End Tag",
|
Button(
|
||||||
type="submit",
|
"End Tag",
|
||||||
cls=ButtonT.primary,
|
type="submit",
|
||||||
disabled=not active_tags,
|
cls=ButtonT.primary,
|
||||||
hx_disabled_elt="this",
|
disabled=not active_tags,
|
||||||
|
hx_disabled_elt="this",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
# Form submission via standard action/method (hx-boost handles AJAX)
|
# Form submission via standard action/method (hx-boost handles AJAX)
|
||||||
action=action,
|
action=action,
|
||||||
@@ -1099,8 +1110,10 @@ def attrs_form(
|
|||||||
Hidden(name="roster_hash", value=roster_hash),
|
Hidden(name="roster_hash", value=roster_hash),
|
||||||
Hidden(name="confirmed", value=""),
|
Hidden(name="confirmed", value=""),
|
||||||
Hidden(name="nonce", value=str(ULID())),
|
Hidden(name="nonce", value=str(ULID())),
|
||||||
# Submit button
|
# Submit button in sticky action bar for mobile
|
||||||
Button("Update Attributes", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
ActionBar(
|
||||||
|
Button("Update Attributes", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
||||||
|
),
|
||||||
# Form submission via standard action/method (hx-boost handles AJAX)
|
# Form submission via standard action/method (hx-boost handles AJAX)
|
||||||
action=action,
|
action=action,
|
||||||
method="post",
|
method="post",
|
||||||
@@ -1353,8 +1366,12 @@ def outcome_form(
|
|||||||
Hidden(name="roster_hash", value=roster_hash),
|
Hidden(name="roster_hash", value=roster_hash),
|
||||||
Hidden(name="confirmed", value=""),
|
Hidden(name="confirmed", value=""),
|
||||||
Hidden(name="nonce", value=str(ULID())),
|
Hidden(name="nonce", value=str(ULID())),
|
||||||
# Submit button
|
# Submit button in sticky action bar for mobile
|
||||||
Button("Record Outcome", type="submit", cls=ButtonT.destructive, hx_disabled_elt="this"),
|
ActionBar(
|
||||||
|
Button(
|
||||||
|
"Record Outcome", type="submit", cls=ButtonT.destructive, hx_disabled_elt="this"
|
||||||
|
),
|
||||||
|
),
|
||||||
# Form submission via standard action/method (hx-boost handles AJAX)
|
# Form submission via standard action/method (hx-boost handles AJAX)
|
||||||
action=action,
|
action=action,
|
||||||
method="post",
|
method="post",
|
||||||
@@ -1534,8 +1551,12 @@ def status_correct_form(
|
|||||||
Hidden(name="roster_hash", value=roster_hash),
|
Hidden(name="roster_hash", value=roster_hash),
|
||||||
Hidden(name="confirmed", value=""),
|
Hidden(name="confirmed", value=""),
|
||||||
Hidden(name="nonce", value=str(ULID())),
|
Hidden(name="nonce", value=str(ULID())),
|
||||||
# Submit button
|
# Submit button in sticky action bar for mobile
|
||||||
Button("Correct Status", type="submit", cls=ButtonT.destructive, hx_disabled_elt="this"),
|
ActionBar(
|
||||||
|
Button(
|
||||||
|
"Correct Status", type="submit", cls=ButtonT.destructive, hx_disabled_elt="this"
|
||||||
|
),
|
||||||
|
),
|
||||||
# Form submission via standard action/method (hx-boost handles AJAX)
|
# Form submission via standard action/method (hx-boost handles AJAX)
|
||||||
action=action,
|
action=action,
|
||||||
method="post",
|
method="post",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from fasthtml.common import Container, Div, HttpHeader, Script, Style, Title
|
|||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
from animaltrack.models.reference import UserRole
|
from animaltrack.models.reference import UserRole
|
||||||
|
from animaltrack.web.templates.action_bar import ActionBarStyles
|
||||||
from animaltrack.web.templates.nav import BottomNav, BottomNavStyles
|
from animaltrack.web.templates.nav import BottomNav, BottomNavStyles
|
||||||
from animaltrack.web.templates.shared_scripts import slide_over_script
|
from animaltrack.web.templates.shared_scripts import slide_over_script
|
||||||
from animaltrack.web.templates.sidebar import (
|
from animaltrack.web.templates.sidebar import (
|
||||||
@@ -187,6 +188,7 @@ def page(
|
|||||||
return (
|
return (
|
||||||
Title(title),
|
Title(title),
|
||||||
BottomNavStyles(),
|
BottomNavStyles(),
|
||||||
|
ActionBarStyles(),
|
||||||
SidebarStyles(),
|
SidebarStyles(),
|
||||||
TabStyles(),
|
TabStyles(),
|
||||||
SelectStyles(),
|
SelectStyles(),
|
||||||
@@ -201,14 +203,14 @@ def page(
|
|||||||
# Event detail slide-over panel
|
# Event detail slide-over panel
|
||||||
EventSlideOver(),
|
EventSlideOver(),
|
||||||
# Main content with responsive padding/margin
|
# Main content with responsive padding/margin
|
||||||
# pb-20 for mobile bottom nav, md:pb-4 for desktop (no bottom nav)
|
# pb-28 for mobile (dock ~56px + action bar ~56px), md:pb-4 for desktop
|
||||||
# md:ml-60 to offset for desktop sidebar
|
# md:ml-60 to offset for desktop sidebar
|
||||||
# hx-boost enables AJAX for all descendant links/forms
|
# hx-boost enables AJAX for all descendant links/forms
|
||||||
Div(
|
Div(
|
||||||
Container(content),
|
Container(content),
|
||||||
hx_boost="true",
|
hx_boost="true",
|
||||||
hx_target="body",
|
hx_target="body",
|
||||||
cls="pb-20 md:pb-4 md:ml-60 min-h-screen bg-[#0f0f0e] text-stone-100",
|
cls="pb-28 md:pb-4 md:ml-60 min-h-screen bg-[#0f0f0e] text-stone-100",
|
||||||
),
|
),
|
||||||
# Toast container with hx-preserve to survive body swaps for OOB toast injection
|
# Toast container with hx-preserve to survive body swaps for OOB toast injection
|
||||||
Div(id="fh-toast-container", hx_preserve=True, aria_live="polite"),
|
Div(id="fh-toast-container", hx_preserve=True, aria_live="polite"),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from ulid import ULID
|
|||||||
|
|
||||||
from animaltrack.models.events import Event
|
from animaltrack.models.events import Event
|
||||||
from animaltrack.models.reference import Location, Product
|
from animaltrack.models.reference import Location, Product
|
||||||
|
from animaltrack.web.templates.action_bar import ActionBar
|
||||||
from animaltrack.web.templates.actions import event_datetime_field
|
from animaltrack.web.templates.actions import event_datetime_field
|
||||||
from animaltrack.web.templates.recent_events import recent_events_section
|
from animaltrack.web.templates.recent_events import recent_events_section
|
||||||
|
|
||||||
@@ -240,8 +241,10 @@ def harvest_form(
|
|||||||
event_datetime_field("harvest_datetime"),
|
event_datetime_field("harvest_datetime"),
|
||||||
# Hidden nonce for idempotency
|
# Hidden nonce for idempotency
|
||||||
Hidden(name="nonce", value=str(ULID())),
|
Hidden(name="nonce", value=str(ULID())),
|
||||||
# Submit button
|
# Submit button in sticky action bar for mobile
|
||||||
Button("Record Harvest", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
ActionBar(
|
||||||
|
Button("Record Harvest", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
||||||
|
),
|
||||||
# Form submission via standard action/method (hx-boost handles AJAX)
|
# Form submission via standard action/method (hx-boost handles AJAX)
|
||||||
action=action,
|
action=action,
|
||||||
method="post",
|
method="post",
|
||||||
@@ -393,8 +396,10 @@ def sell_form(
|
|||||||
event_datetime_field("sell_datetime"),
|
event_datetime_field("sell_datetime"),
|
||||||
# Hidden nonce for idempotency
|
# Hidden nonce for idempotency
|
||||||
Hidden(name="nonce", value=str(ULID())),
|
Hidden(name="nonce", value=str(ULID())),
|
||||||
# Submit button
|
# Submit button in sticky action bar for mobile
|
||||||
Button("Record Sale", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
ActionBar(
|
||||||
|
Button("Record Sale", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
||||||
|
),
|
||||||
# Form submission via standard action/method (hx-boost handles AJAX)
|
# Form submission via standard action/method (hx-boost handles AJAX)
|
||||||
action=action,
|
action=action,
|
||||||
method="post",
|
method="post",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from ulid import ULID
|
|||||||
|
|
||||||
from animaltrack.models.events import Event
|
from animaltrack.models.events import Event
|
||||||
from animaltrack.models.reference import FeedType, Location
|
from animaltrack.models.reference import FeedType, Location
|
||||||
|
from animaltrack.web.templates.action_bar import ActionBar
|
||||||
from animaltrack.web.templates.actions import event_datetime_field
|
from animaltrack.web.templates.actions import event_datetime_field
|
||||||
from animaltrack.web.templates.recent_events import recent_events_section
|
from animaltrack.web.templates.recent_events import recent_events_section
|
||||||
|
|
||||||
@@ -264,8 +265,10 @@ def give_feed_form(
|
|||||||
event_datetime_field("feed_given_datetime"),
|
event_datetime_field("feed_given_datetime"),
|
||||||
# Hidden nonce
|
# Hidden nonce
|
||||||
Hidden(name="nonce", value=str(ULID())),
|
Hidden(name="nonce", value=str(ULID())),
|
||||||
# Submit button
|
# Submit button in sticky action bar for mobile
|
||||||
Button("Record Feed Given", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
ActionBar(
|
||||||
|
Button("Record Feed Given", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
||||||
|
),
|
||||||
action=action,
|
action=action,
|
||||||
method="post",
|
method="post",
|
||||||
cls="space-y-4",
|
cls="space-y-4",
|
||||||
@@ -408,8 +411,10 @@ def purchase_feed_form(
|
|||||||
event_datetime_field("feed_purchase_datetime"),
|
event_datetime_field("feed_purchase_datetime"),
|
||||||
# Hidden nonce
|
# Hidden nonce
|
||||||
Hidden(name="nonce", value=str(ULID())),
|
Hidden(name="nonce", value=str(ULID())),
|
||||||
# Submit button
|
# Submit button in sticky action bar for mobile
|
||||||
Button("Record Purchase", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
ActionBar(
|
||||||
|
Button("Record Purchase", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
||||||
|
),
|
||||||
action=action,
|
action=action,
|
||||||
method="post",
|
method="post",
|
||||||
cls="space-y-4",
|
cls="space-y-4",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from ulid import ULID
|
|||||||
from animaltrack.models.events import Event
|
from animaltrack.models.events import Event
|
||||||
from animaltrack.models.reference import Location
|
from animaltrack.models.reference import Location
|
||||||
from animaltrack.selection.validation import SelectionDiff
|
from animaltrack.selection.validation import SelectionDiff
|
||||||
|
from animaltrack.web.templates.action_bar import ActionBar
|
||||||
from animaltrack.web.templates.actions import event_datetime_field
|
from animaltrack.web.templates.actions import event_datetime_field
|
||||||
from animaltrack.web.templates.recent_events import recent_events_section
|
from animaltrack.web.templates.recent_events import recent_events_section
|
||||||
|
|
||||||
@@ -173,8 +174,10 @@ def move_form(
|
|||||||
Hidden(name="resolver_version", value="v1"),
|
Hidden(name="resolver_version", value="v1"),
|
||||||
Hidden(name="confirmed", value=""),
|
Hidden(name="confirmed", value=""),
|
||||||
Hidden(name="nonce", value=str(ULID())),
|
Hidden(name="nonce", value=str(ULID())),
|
||||||
# Submit button
|
# Submit button in sticky action bar for mobile
|
||||||
Button("Move Animals", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
ActionBar(
|
||||||
|
Button("Move Animals", type="submit", cls=ButtonT.primary, hx_disabled_elt="this"),
|
||||||
|
),
|
||||||
# Form submission via standard action/method (hx-boost handles AJAX)
|
# Form submission via standard action/method (hx-boost handles AJAX)
|
||||||
action=action,
|
action=action,
|
||||||
method="post",
|
method="post",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# ABOUTME: Bottom navigation component for AnimalTrack mobile UI.
|
# ABOUTME: Bottom navigation component for AnimalTrack mobile UI.
|
||||||
# ABOUTME: Industrial farm aesthetic with large touch targets and high contrast.
|
# ABOUTME: Uses daisyUI btm-nav component for compact, mobile-friendly navigation.
|
||||||
|
|
||||||
from fasthtml.common import A, Button, Div, Span, Style
|
from fasthtml.common import A, Button, Div, Span, Style
|
||||||
|
|
||||||
from animaltrack.web.templates.icons import NAV_ICONS
|
from animaltrack.web.templates.icons import NAV_ICONS
|
||||||
|
|
||||||
# Navigation items configuration (simplified to 4 items)
|
# Navigation items configuration
|
||||||
NAV_ITEMS = [
|
NAV_ITEMS = [
|
||||||
{"id": "eggs", "label": "Eggs", "href": "/"},
|
{"id": "eggs", "label": "Eggs", "href": "/"},
|
||||||
{"id": "feed", "label": "Feed", "href": "/feed"},
|
{"id": "feed", "label": "Feed", "href": "/feed"},
|
||||||
@@ -15,53 +15,56 @@ NAV_ITEMS = [
|
|||||||
|
|
||||||
|
|
||||||
def BottomNavStyles(): # noqa: N802
|
def BottomNavStyles(): # noqa: N802
|
||||||
"""CSS styles for bottom navigation - include in page head."""
|
"""CSS styles for bottom navigation - supplement daisyUI btm-nav."""
|
||||||
return Style("""
|
return Style("""
|
||||||
/* Bottom nav industrial styling */
|
/* Industrial styling overrides for btm-nav */
|
||||||
#bottom-nav {
|
#bottom-nav.btm-nav {
|
||||||
|
background-color: #1a1a18;
|
||||||
|
border-top: 1px solid #404040;
|
||||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Safe area for iOS notch devices */
|
/* Active item golden accent */
|
||||||
.safe-area-pb {
|
#bottom-nav .active,
|
||||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
#bottom-nav .active:hover {
|
||||||
|
color: #d97706;
|
||||||
|
border-top-color: #d97706;
|
||||||
|
background-color: rgba(217, 119, 6, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Active item subtle glow effect */
|
/* Inactive items muted */
|
||||||
.nav-item-active::after {
|
#bottom-nav > *:not(.active) {
|
||||||
content: '';
|
color: #78716c;
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 40%;
|
|
||||||
height: 2px;
|
|
||||||
background: linear-gradient(90deg, transparent, #b8860b, transparent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover state for non-touch devices */
|
/* Hover state for non-touch devices */
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
#bottom-nav a:hover {
|
#bottom-nav > *:not(.active):hover {
|
||||||
background-color: rgba(184, 134, 11, 0.1);
|
background-color: rgba(184, 134, 11, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure consistent icon rendering */
|
/* Hide on desktop */
|
||||||
#bottom-nav svg {
|
@media (min-width: 768px) {
|
||||||
flex-shrink: 0;
|
#bottom-nav.btm-nav {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typography for labels */
|
/* Normalize button to match anchor styling in btm-nav */
|
||||||
#bottom-nav span {
|
#bottom-nav button {
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
border: none;
|
||||||
letter-spacing: 0.05em;
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
def BottomNav(active_id: str = "eggs"): # noqa: N802
|
def BottomNav(active_id: str = "eggs"): # noqa: N802
|
||||||
"""
|
"""
|
||||||
Fixed bottom navigation bar for AnimalTrack (mobile only).
|
Fixed bottom navigation bar using daisyUI btm-nav (mobile only).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
active_id: Currently active nav item ('eggs', 'feed', 'move')
|
active_id: Currently active nav item ('eggs', 'feed', 'move')
|
||||||
@@ -74,52 +77,35 @@ def BottomNav(active_id: str = "eggs"): # noqa: N802
|
|||||||
is_active = item["id"] == active_id
|
is_active = item["id"] == active_id
|
||||||
icon_fn = NAV_ICONS[item["id"]]
|
icon_fn = NAV_ICONS[item["id"]]
|
||||||
|
|
||||||
# Active: golden highlight, inactive: muted stone gray
|
# daisyUI v4 uses 'active' class for active state
|
||||||
label_cls = "text-xs font-semibold tracking-wide uppercase mt-1 "
|
cls = "active" if is_active else ""
|
||||||
label_cls += "text-amber-600" if is_active else "text-stone-500"
|
|
||||||
|
|
||||||
item_cls = "flex flex-col items-center justify-center py-2 px-4 "
|
# Content: icon + label
|
||||||
if is_active:
|
content = [
|
||||||
item_cls += "bg-stone-900/50 rounded-lg"
|
|
||||||
|
|
||||||
wrapper_cls = (
|
|
||||||
"relative flex-1 flex items-center justify-center min-h-[64px] "
|
|
||||||
"transition-all duration-150 active:scale-95 "
|
|
||||||
)
|
|
||||||
if is_active:
|
|
||||||
wrapper_cls += "nav-item-active"
|
|
||||||
|
|
||||||
inner = Div(
|
|
||||||
icon_fn(active=is_active),
|
icon_fn(active=is_active),
|
||||||
Span(item["label"], cls=label_cls),
|
Span(item["label"], cls="btm-nav-label"),
|
||||||
cls=item_cls,
|
]
|
||||||
)
|
|
||||||
|
|
||||||
# Menu item is a button that opens the drawer
|
# Menu item is a button that opens the drawer
|
||||||
if item["id"] == "menu":
|
if item["id"] == "menu":
|
||||||
return Button(
|
return Button(
|
||||||
inner,
|
*content,
|
||||||
onclick="openMenuDrawer()",
|
onclick="openMenuDrawer()",
|
||||||
cls=wrapper_cls,
|
cls=cls,
|
||||||
type="button",
|
type="button",
|
||||||
aria_label="Open navigation menu",
|
aria_label="Open navigation menu",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Regular nav items are links
|
# Regular nav items are links
|
||||||
return A(
|
return A(
|
||||||
inner,
|
*content,
|
||||||
href=item["href"],
|
href=item["href"],
|
||||||
cls=wrapper_cls,
|
cls=cls,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# daisyUI btm-nav: fixed at bottom, flex layout for children
|
||||||
return Div(
|
return Div(
|
||||||
# Top border with subtle texture effect
|
*[nav_item(item) for item in NAV_ITEMS],
|
||||||
Div(cls="h-px bg-gradient-to-r from-transparent via-stone-700 to-transparent"),
|
cls="btm-nav btm-nav-sm",
|
||||||
# Nav container
|
|
||||||
Div(
|
|
||||||
*[nav_item(item) for item in NAV_ITEMS],
|
|
||||||
cls="flex items-stretch bg-[#1a1a18] safe-area-pb",
|
|
||||||
),
|
|
||||||
cls="fixed bottom-0 left-0 right-0 z-50 md:hidden",
|
|
||||||
id="bottom-nav",
|
id="bottom-nav",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -248,9 +248,12 @@ def MenuDrawer(user_role: UserRole | None = None): # noqa: N802
|
|||||||
),
|
),
|
||||||
# Drawer panel
|
# Drawer panel
|
||||||
Div(
|
Div(
|
||||||
# Header with close button
|
# Header with logo and close button
|
||||||
Div(
|
Div(
|
||||||
Span("MENU", cls="text-amber-600 font-bold tracking-wider text-sm"),
|
Div(
|
||||||
|
Div("ANIMALTRACK", cls="text-amber-600 font-bold tracking-wider text-sm"),
|
||||||
|
Div(get_build_info(), cls="text-stone-600 text-[10px] tracking-wide"),
|
||||||
|
),
|
||||||
Button(
|
Button(
|
||||||
_close_icon(),
|
_close_icon(),
|
||||||
hx_on_click="closeMenuDrawer()",
|
hx_on_click="closeMenuDrawer()",
|
||||||
|
|||||||
Reference in New Issue
Block a user