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}"'],