diff --git a/src/e3sm_quickview/app.py b/src/e3sm_quickview/app.py index ff12d06..a5bc231 100644 --- a/src/e3sm_quickview/app.py +++ b/src/e3sm_quickview/app.py @@ -1,7 +1,6 @@ import asyncio import datetime import json -import math import os import time from functools import partial @@ -221,11 +220,12 @@ def _build_ui(self, **_): with v3.VContainer(classes="h-100 pa-0", fluid=True): with client.SizeObserver("main_size"): - # Take space to push content below the fixed overlay - html.Div(style=("`height: ${top_padding}px`",)) - - # Fixed overlay for toolbars + # Sticky toolbar overlay with html.Div(style=css.TOOLBARS_FIXED_OVERLAY): + client.SizeObserver( + "toolbar_size", + style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;", + ) toolbars.Layout( apply_size=self.view_manager.apply_size, zoom=self.view_manager.zoom, @@ -613,22 +613,10 @@ async def _on_projection(self, projection, **_): @change("active_tools", "available_animation_tracks") def _on_toolbar_change(self, active_tools, **_): + # Initial estimate; client-side ResizeObserver will override with actual height top_padding = 0 for name in active_tools: - if name == "select-slice-time": - track_count = len(self.state.available_animation_tracks or []) - rows_needed = 1 - if track_count > 3: - if track_count % 3 == 0 or (track_count + 1) % 3 == 0: - rows_needed = math.ceil(track_count / 3) - elif track_count % 2 == 0: - rows_needed = track_count / 2 - else: - rows_needed = math.ceil(track_count / 3) - - top_padding += 70 * rows_needed - else: - top_padding += toolbars.SIZES.get(name, 0) + top_padding += toolbars.SIZES.get(name, 0) self.state.top_padding = top_padding diff --git a/src/e3sm_quickview/components/css.py b/src/e3sm_quickview/components/css.py index ea01085..0c80d8c 100644 --- a/src/e3sm_quickview/components/css.py +++ b/src/e3sm_quickview/components/css.py @@ -4,7 +4,7 @@ ) TOOLBARS_FIXED_OVERLAY = ( - "`position:fixed;top:0;width:${Math.floor(main_size?.size?.width || 0)}px;z-index:1;`", + "`position:sticky;top:0;width:${Math.floor(main_size?.size?.width || 0)}px;z-index:1;background:rgb(var(--v-theme-surface));`", ) diff --git a/src/e3sm_quickview/components/toolbars.py b/src/e3sm_quickview/components/toolbars.py index 9970760..b72663c 100644 --- a/src/e3sm_quickview/components/toolbars.py +++ b/src/e3sm_quickview/components/toolbars.py @@ -10,14 +10,14 @@ DENSITY = { "adjust-layout": "compact", "adjust-databounds": "default", - "select-slice-time": "default", + "select-slice-time": "compact", "animation-controls": "compact", } SIZES = { "adjust-layout": 49, "adjust-databounds": 65, - "select-slice-time": 70, + "select-slice-time": 49, "animation-controls": 49, } @@ -447,102 +447,99 @@ def __init__(self): ) super().__init__(**style) + self.state.setdefault("expanded_slice_track", None) + with self: - with v3.VTooltip( - text=( - "slice_slider_edit ? 'Toggle to text edit' : 'Toggle to slider edit'", - ), - ): - with v3.Template(v_slot_activator="{ props }"): - v3.VIcon( - "mdi-tune-variant", - v_bind="props", - classes="ml-3 mr-2 opacity-50", - click="slice_slider_edit = !slice_slider_edit", - ) + v3.VIcon("mdi-tune-variant", classes="ml-3 mr-2 opacity-50") - with v3.VRow( - classes="ma-0 pr-2 flex-wrap flex-grow-1", - dense=True, - v_if=("slice_slider_edit", True), + with html.Div( + classes="d-flex align-center flex-wrap flex-grow-1 ga-1 py-1 pr-2" ): - # Debug: Show animation_tracks array - # html.Div( - # "Animation Tracks: {{ JSON.stringify(available_animation_tracks) }}", - # classes="col-12", - # ) - # Each track gets a column (3 per row) - with v3.VCol( - cols=("utils.quickview.cols(available_animation_tracks.length)",), + with html.Template( v_for="(track, idx) in available_animation_tracks", key="idx", - classes="pa-2", ): with client.Getter(name=("track",), value_name="t_values"): with client.Getter( name=("track + '_idx'",), value_name="t_idx" ): - with v3.VRow(classes="ma-0 align-center", dense=True): - v3.VLabel( - "{{track}}", - classes="text-subtitle-2", - ) - v3.VSpacer() - v3.VLabel( - "{{ dim_units[track] ? parseFloat(t_values[t_idx]).toFixed(2) + ' ' + dim_units[track] : 'Index value: ' + t_idx }} (k={{ t_idx }})", - classes="text-body-2", - ) - v3.VSlider( - model_value=("t_idx",), - update_modelValue=( - self.on_update_slider, - "[track, $event]", + # --- Per-variable group --- + with v3.VSheet( + classes="d-flex align-center rounded px-1 ga-1", + color=( + "expanded_slice_track === track ? 'grey-lighten-3' : 'grey-lighten-4'", ), - min=0, - # max=100,#("get(track.value).length - 1",), - max=("t_values.length - 1",), - step=1, - density="compact", - hide_details=True, - ) - with v3.VRow( - classes="ma-0 pl-6 pr-2 align-center ga-4", - v_if="!slice_slider_edit", - ): - with v3.VCol( - v_for="(track, idx) in available_animation_tracks", - key="idx", - ): - with client.Getter(name=("track",), value_name="t_values"): - with client.Getter( - name=("track + '_idx'",), value_name="t_idx" - ): - with v3.VRow(classes="ma-0 align-center", dense=True): - v3.VNumberInput( - model_value=("Number(t_idx)",), - update_modelValue=( - self.on_update_slider, - "[track, Number($event)]", - ), - key=("track + '_' + t_idx",), - min=[0], - max=["t_values ? t_values.length - 1 : 0"], - step=[1], - hide_details=True, - density="comfortable", - variant="plain", + ): + # Toggle button with track name + v3.VBtn( + "{{ track }}", + v_tooltip_bottom="'Toggle ' + track + ' controls'", flat=True, - control_variant="stacked", - style="max-width: 100px;", - reverse=True, + variant=( + "expanded_slice_track === track ? 'flat' : 'outlined'", + ), + rounded=True, + click="expanded_slice_track = expanded_slice_track === track ? null : track", + color=( + "expanded_slice_track === track ? 'primary' : ''", + ), + style=( + "'text-transform: none;' + (expanded_slice_track === track ? '' : ' background-color: white;')", + ), ) + # Expanded controls + with html.Div( + v_if="expanded_slice_track === track", + classes="d-flex align-center ga-1", + style="height: 36px; overflow: visible;", + ): + v3.VDivider(vertical=True, classes="mx-1") + # Text input + html.Input( + type="number", + value=("t_idx",), + min=[0], + max=["t_values ? t_values.length - 1 : 0"], + step=[1], + change=( + self.on_update_slider, + "[track, Number($event.target.value)]", + ), + style="width: 60px; height: 28px; padding: 16px 4px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; box-sizing: border-box; text-align: right;", + ) + # Slider + v3.VSlider( + v_tooltip_bottom=( + "dim_units[track] ? parseFloat(t_values[t_idx]).toFixed(2) + ' ' + dim_units[track] : 'Index: ' + t_idx", + ), + model_value=("t_idx",), + update_modelValue=( + self.on_update_slider, + "[track, $event]", + ), + min=0, + max=("t_values.length - 1",), + step=1, + show_ticks="always", + hide_details=True, + density="compact", + style="min-width: 200px; max-width: 400px;", + ) + # Index label (shown when collapsed) v3.VLabel( - "{{track}}", - classes="text-subtitle-2 ml-2 mt-1", + v_if="expanded_slice_track !== track", + v_html="'(' + t_idx + ')'", + style="opacity: 1; color: rgba(0,0,0,0.87);", ) + # Value + units label v3.VLabel( - "{{ dim_units[track] ? parseFloat(t_values[Number(t_idx)]).toFixed(2) + ' ' + dim_units[track] : 'Index value: ' + t_idx }}", - classes="text-body-2 text-no-wrap ml-2 mt-1", + v_if=( + "dim_units[track] && isNaN(Number(dim_units[track]))", + ), + v_html=( + "'' + parseFloat(t_values[t_idx]).toFixed(2) + ' ' + dim_units[track] + ''", + ), + style="opacity: 1; color: rgba(0,0,0,0.87);", ) def on_update_slider(self, dimension, index, *_, **__): diff --git a/src/e3sm_quickview/view_manager.py b/src/e3sm_quickview/view_manager.py index f09f3b9..c5c5956 100644 --- a/src/e3sm_quickview/view_manager.py +++ b/src/e3sm_quickview/view_manager.py @@ -894,7 +894,7 @@ def _build_ui(self): with v3.VCard( variant="tonal", style=( - "active_layout !== 'auto_layout' ? `height: calc(100% - ${top_padding}px;` : 'overflow-hidden'", + "active_layout !== 'auto_layout' ? `height: calc(100% - ${toolbar_size?.size?.height || 0}px)` : 'overflow-hidden'", ), tile=("active_layout !== 'auto_layout'",), raw_attrs=[f'data-field-name="{self.variable_name}"'],