From 810ff80a2ced907c90bc165f1a4c51f4b518b803 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Jan 2026 15:42:49 +0000 Subject: [PATCH] Reorganize action buttons with progressive disclosure UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move NotificationBellButton from bottom to top (plan-name-row) alongside Save/Lock buttons - Replace visible Share/New/Load buttons with collapsed menu (⋯ button) - Menu expands on click to reveal Share/New/Load options - Share expands further to show Text/Link options with Back button - Add smooth CSS animations for all UI transitions: - Scale and opacity transitions for menu collapse/expand - Staggered animation delays for button appearances - Transform transitions for slide-in effects - Keep bottom + button with New trip/Return trip unchanged - Clean up CSS with organized progressive disclosure section Co-authored-by: bill.frasure --- frontend/src/main/resources/styling/style.css | 122 ++++++++++++++++++ .../crestedbutte/laminar/Components.scala | 110 ++++++++-------- 2 files changed, 173 insertions(+), 59 deletions(-) diff --git a/frontend/src/main/resources/styling/style.css b/frontend/src/main/resources/styling/style.css index 3bc2438d..a6f1548b 100644 --- a/frontend/src/main/resources/styling/style.css +++ b/frontend/src/main/resources/styling/style.css @@ -925,3 +925,125 @@ body { .bell-button.bell-button-active .bell-icon { animation: bell-ring 1s ease-in-out; } + +/* ======================================== + Progressive Disclosure Menu Styles + ======================================== */ + +/* Menu toggle button (collapsed state) */ +.menu-collapsed-row { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + opacity: 1; + transform: scale(1); + transition: + opacity 300ms ease, + transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.menu-collapsed-row.menu-collapsed-hidden { + opacity: 0; + transform: scale(0.8); + pointer-events: none; + position: absolute; +} + +.menu-toggle-button { + min-width: 48px; + width: 48px; + height: 40px; + padding: 8px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; +} + +.menu-dots { + font-size: 1.4rem; + font-weight: bold; + letter-spacing: 2px; + line-height: 1; +} + +/* Menu expanded row (Share, New, Load) */ +.menu-expanded-row { + opacity: 1; + transform: scale(1); + transition: + opacity 300ms ease, + transform 400ms cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.menu-expanded-row.menu-row-hidden { + opacity: 0; + transform: scale(0.9); + pointer-events: none; + position: absolute; +} + +/* Share expanded row (Back, Text, Link) */ +.share-expanded-row { + opacity: 1; + transform: translateX(0); + transition: + opacity 300ms ease, + transform 400ms cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.share-expanded-row.share-row-hidden { + opacity: 0; + transform: translateX(30px); + pointer-events: none; + position: absolute; +} + +/* Action button animation class */ +.action-button-animate { + transition: + transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1), + opacity 250ms ease, + background-color 200ms ease, + box-shadow 200ms ease; +} + +.action-button-animate:active { + transform: scale(0.95); +} + +/* Icon-only button (for back arrow) */ +.button-icon-only { + min-width: 40px; + width: 40px; + height: 40px; + padding: 8px; + border-radius: 8px; + font-size: 1.2rem; +} + +/* Staggered animation for menu items appearing */ +.menu-expanded-row .button:nth-child(1) { + transition-delay: 0ms; +} + +.menu-expanded-row .button:nth-child(2) { + transition-delay: 50ms; +} + +.menu-expanded-row .button:nth-child(3) { + transition-delay: 100ms; +} + +.share-expanded-row .button:nth-child(1) { + transition-delay: 0ms; +} + +.share-expanded-row .button:nth-child(2) { + transition-delay: 50ms; +} + +.share-expanded-row .button:nth-child(3) { + transition-delay: 100ms; +} diff --git a/frontend/src/main/scala/crestedbutte/laminar/Components.scala b/frontend/src/main/scala/crestedbutte/laminar/Components.scala index 46b64ee7..7afd8768 100644 --- a/frontend/src/main/scala/crestedbutte/laminar/Components.scala +++ b/frontend/src/main/scala/crestedbutte/laminar/Components.scala @@ -635,11 +635,6 @@ object Components { ) }, cls := "centered", - // Notification bell button - in its own row above trip buttons - div( - cls := "notification-bell-row", - NotificationBellButton($plan), - ), div( cls := "trip-button-container", @@ -1091,6 +1086,8 @@ object Components { isLocked.update(!_) }, ), + // Bell notification button - moved to top row + NotificationBellButton($plan), ) def copyButtons( @@ -1103,6 +1100,8 @@ object Components { focusPlanNameInput: Var[Boolean], hasSavedPlans: Var[Boolean], ) = { + // Progressive disclosure state: menu collapsed -> menu expanded -> share expanded + val menuExpanded: Var[Boolean] = Var(false) val shareExpanded: Var[Boolean] = Var(false) val saveExpanded: Var[Boolean] = Var(false) val tripName: Var[String] = Var("") @@ -1117,6 +1116,7 @@ object Components { if ( actionContainer != null && !actionContainer.contains(target) ) { + menuExpanded.set(false) shareExpanded.set(false) saveExpanded.set(false) saveConfirmation.set(None) @@ -1145,42 +1145,41 @@ object Components { div( cls := "action-buttons-container share-save-buttons-container", - // Collapsed state: Share, New, and Load buttons + // Level 0: Collapsed state - just the menu button (⋯) div( - cls := "expanded-buttons-row collapsed-buttons-row", - cls <-- shareExpanded.signal - .combineWith(saveExpanded.signal) - .map { case (share, save) => - if (share || save) "delayed-appear" else "" - }, - styleProp("opacity") <-- shareExpanded.signal - .combineWith(saveExpanded.signal) - .map { case (share, save) => - if (share || save) "0" else "1" - }, - styleProp( - "pointer-events", - ) <-- shareExpanded.signal - .combineWith(saveExpanded.signal) - .map { case (share, save) => - if (share || save) "none" else "auto" + cls := "menu-collapsed-row", + cls <-- menuExpanded.signal.map(expanded => + if (expanded) "menu-collapsed-hidden" else "", + ), + button( + cls := "button menu-toggle-button", + title := "More actions", + span(cls := "menu-dots", "⋯"), + onClick --> Observer { _ => + menuExpanded.set(true) }, - styleProp("position") <-- shareExpanded.signal + ), + ), + + // Level 1: Menu expanded - Share, New, Load buttons + div( + cls := "expanded-buttons-row menu-expanded-row", + cls <-- menuExpanded.signal + .combineWith(shareExpanded.signal) .combineWith(saveExpanded.signal) - .map { case (share, save) => - if (share || save) "absolute" else "relative" + .map { case (menuOpen, shareOpen, saveOpen) => + if (!menuOpen || shareOpen || saveOpen) "menu-row-hidden" else "" }, button( - cls := "button button-fixed-width", + cls := "button button-fixed-width action-button-animate", SvgIcon.share("share-icon"), onClick --> Observer { _ => shareExpanded.set(true) - saveExpanded.set(false) }, ), // New button - creates a fresh empty trip button( - cls := "button button-fixed-width", + cls := "button button-fixed-width action-button-animate", "New", onClick --> Observer { _ => // Create a fresh empty plan @@ -1195,17 +1194,19 @@ object Components { isLocked.set(false) // Start in adding new route mode addingNewRoute.set(true) + menuExpanded.set(false) }, ), // Load button - dynamically show/hide based on saved plans child <-- hasSavedPlans.signal.map { hasPlans => if (hasPlans) { button( - cls := "button button-fixed-width", + cls := "button button-fixed-width action-button-animate", "Load", onClick --> Observer { _ => loadTripsMode.set(true) addingNewRoute.set(true) + menuExpanded.set(false) }, ) } @@ -1215,28 +1216,24 @@ object Components { }, ), - // Share expanded: Text and Link buttons + // Level 2: Share expanded - Text and Link buttons div( - cls := "expanded-buttons-row", - styleProp( - "pointer-events", - ) <-- shareExpanded.signal - .map(expanded => if (expanded) "auto" else "none", - ), - styleProp("position") <-- shareExpanded.signal - .map(expanded => - if (expanded) "relative" else "absolute", - ), - // Text button - emerges from Share position (left side, no translation needed) + cls := "expanded-buttons-row share-expanded-row", + cls <-- shareExpanded.signal.map(expanded => + if (expanded) "" else "share-row-hidden", + ), + // Back button to return to menu level button( - cls := "button button-fixed-width expand-from-left", - styleProp("transform") <-- shareExpanded.signal - .map(expanded => - if (expanded) "translateX(0) scale(1)" - else "translateX(60px) scale(0)", - ), - styleProp("opacity") <-- shareExpanded.signal - .map(expanded => if (expanded) "1" else "0"), + cls := "button button-icon-only action-button-animate", + title := "Back", + span("←"), + onClick --> Observer { _ => + shareExpanded.set(false) + }, + ), + // Text button + button( + cls := "button button-fixed-width action-button-animate", "Text", onClick --> Observer { _ => val text = plan.plainTextRepresentation @@ -1260,18 +1257,12 @@ object Components { .writeText(text) } shareExpanded.set(false) + menuExpanded.set(false) }, ), - // Link button - emerges from Share position (needs to slide right to its final position) + // Link button button( - cls := "button button-fixed-width expand-from-right", - styleProp("transform") <-- shareExpanded.signal - .map(expanded => - if (expanded) "translateX(0) scale(1)" - else "translateX(-120px) scale(0)", - ), - styleProp("opacity") <-- shareExpanded.signal - .map(expanded => if (expanded) "1" else "0"), + cls := "button button-fixed-width action-button-animate", "Link", onClick --> Observer { _ => val url = @@ -1300,6 +1291,7 @@ object Components { .writeText(url) } shareExpanded.set(false) + menuExpanded.set(false) }, ), ),