feat(UX-001): add full-width two-pane dashboard layout
This commit is contained in:
@@ -11,6 +11,7 @@ from app.pages.common import (
|
||||
dashboard_page,
|
||||
demo_spot_price,
|
||||
portfolio_snapshot,
|
||||
split_page_panes,
|
||||
strategy_catalog,
|
||||
strategy_metrics,
|
||||
)
|
||||
@@ -111,6 +112,7 @@ async def _render_hedge_page(workspace_id: str | None = None) -> None:
|
||||
spot_label = (
|
||||
f"Current spot reference: ${portfolio['spot_price']:,.2f}/oz (converted collateral spot via {quote_source})"
|
||||
)
|
||||
updated_label = f"Quote timestamp: {quote_updated_at}" if quote_updated_at else "Quote timestamp: unavailable"
|
||||
|
||||
with dashboard_page(
|
||||
"Hedge Analysis",
|
||||
@@ -118,7 +120,12 @@ async def _render_hedge_page(workspace_id: str | None = None) -> None:
|
||||
"hedge",
|
||||
workspace_id=workspace_id,
|
||||
):
|
||||
with ui.row().classes("w-full gap-6 max-lg:flex-col"):
|
||||
left_pane, right_pane = split_page_panes(
|
||||
left_testid="hedge-left-pane",
|
||||
right_testid="hedge-right-pane",
|
||||
)
|
||||
|
||||
with left_pane:
|
||||
with ui.card().classes(
|
||||
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||
):
|
||||
@@ -127,6 +134,7 @@ async def _render_hedge_page(workspace_id: str | None = None) -> None:
|
||||
slider_value = ui.label("Scenario move: +0%").classes("text-sm text-slate-500 dark:text-slate-400")
|
||||
slider = ui.slider(min=-25, max=25, value=0, step=5).classes("w-full")
|
||||
ui.label(spot_label).classes("text-sm text-slate-500 dark:text-slate-400")
|
||||
ui.label(updated_label).classes("text-xs text-slate-500 dark:text-slate-400")
|
||||
if workspace_id:
|
||||
ui.label(f"Workspace route: /{workspace_id}/hedge").classes(
|
||||
"text-xs text-slate-500 dark:text-slate-400"
|
||||
@@ -140,15 +148,18 @@ async def _render_hedge_page(workspace_id: str | None = None) -> None:
|
||||
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||
)
|
||||
|
||||
charts_row = ui.row().classes("w-full gap-6 max-lg:flex-col")
|
||||
with charts_row:
|
||||
initial_metrics = strategy_metrics(selected["strategy"], selected["scenario_pct"], portfolio=portfolio)
|
||||
cost_chart = ui.echart(_cost_benefit_options(initial_metrics)).classes(
|
||||
"h-96 w-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||
)
|
||||
waterfall_chart = ui.echart(_waterfall_options(initial_metrics)).classes(
|
||||
"h-96 w-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||
with right_pane:
|
||||
scenario_results = ui.card().classes(
|
||||
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||
)
|
||||
with ui.row().classes("w-full gap-6 max-xl:flex-col"):
|
||||
initial_metrics = strategy_metrics(selected["strategy"], selected["scenario_pct"], portfolio=portfolio)
|
||||
cost_chart = ui.echart(_cost_benefit_options(initial_metrics)).classes(
|
||||
"h-96 w-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||
)
|
||||
waterfall_chart = ui.echart(_waterfall_options(initial_metrics)).classes(
|
||||
"h-96 w-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||
)
|
||||
|
||||
def render_summary() -> None:
|
||||
metrics = strategy_metrics(selected["strategy"], selected["scenario_pct"], portfolio=portfolio)
|
||||
@@ -156,7 +167,8 @@ async def _render_hedge_page(workspace_id: str | None = None) -> None:
|
||||
summary.clear()
|
||||
with summary:
|
||||
ui.label("Scenario Summary").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||
with ui.grid(columns=3).classes("w-full gap-4 max-lg:grid-cols-2 max-sm:grid-cols-1"):
|
||||
ui.label(strategy["description"]).classes("text-sm text-slate-600 dark:text-slate-300")
|
||||
with ui.grid(columns=1).classes("w-full gap-4 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2"):
|
||||
cards = [
|
||||
("Start value", f"${portfolio['gold_value']:,.0f}"),
|
||||
("Start price", f"${portfolio['spot_price']:,.2f}/oz"),
|
||||
@@ -164,10 +176,6 @@ async def _render_hedge_page(workspace_id: str | None = None) -> None:
|
||||
("Loan amount", f"${portfolio['loan_amount']:,.0f}"),
|
||||
("Margin call LTV", f"{portfolio['margin_call_ltv']:.1%}"),
|
||||
("Monthly hedge budget", f"${portfolio['hedge_budget']:,.0f}"),
|
||||
("Scenario spot", f"${metrics['scenario_price']:,.2f}"),
|
||||
("Hedge cost", f"${strategy['estimated_cost']:,.2f}/oz"),
|
||||
("Unhedged equity", f"${metrics['unhedged_equity']:,.0f}"),
|
||||
("Hedged equity", f"${metrics['hedged_equity']:,.0f}"),
|
||||
]
|
||||
for label, value in cards:
|
||||
with ui.card().classes(
|
||||
@@ -175,7 +183,25 @@ async def _render_hedge_page(workspace_id: str | None = None) -> None:
|
||||
):
|
||||
ui.label(label).classes("text-sm text-slate-500 dark:text-slate-400")
|
||||
ui.label(value).classes("text-2xl font-bold text-slate-900 dark:text-slate-100")
|
||||
ui.label(strategy["description"]).classes("text-sm text-slate-600 dark:text-slate-300")
|
||||
|
||||
scenario_results.clear()
|
||||
with scenario_results:
|
||||
ui.label("Scenario Results").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
|
||||
with ui.grid(columns=2).classes("w-full gap-4 max-md:grid-cols-1"):
|
||||
result_cards = [
|
||||
("Scenario spot", f"${metrics['scenario_price']:,.2f}"),
|
||||
("Hedge cost", f"${strategy['estimated_cost']:,.2f}/oz"),
|
||||
("Unhedged equity", f"${metrics['unhedged_equity']:,.0f}"),
|
||||
("Hedged equity", f"${metrics['hedged_equity']:,.0f}"),
|
||||
("Net hedge benefit", f"${metrics['hedged_equity'] - metrics['unhedged_equity']:,.0f}"),
|
||||
("Scenario move", f"{selected['scenario_pct']:+d}%"),
|
||||
]
|
||||
for label, value in result_cards:
|
||||
with ui.card().classes(
|
||||
"rounded-xl border border-slate-200 bg-slate-50 p-4 shadow-none dark:border-slate-800 dark:bg-slate-950"
|
||||
):
|
||||
ui.label(label).classes("text-sm text-slate-500 dark:text-slate-400")
|
||||
ui.label(value).classes("text-2xl font-bold text-slate-900 dark:text-slate-100")
|
||||
|
||||
cost_chart.options.clear()
|
||||
cost_chart.options.update(_cost_benefit_options(metrics))
|
||||
|
||||
Reference in New Issue
Block a user