Spaces:
Sleeping
Sleeping
Pointf5ive commited on
Commit ·
3b3730b
1
Parent(s): 9cc52b4
stage 5: hero, status banner, and totem signal gauge
Browse filesScope: implement render_hero/state-driven status row and functional SVG render_signal_gauge(value) with aria labeling and progress ring binding.
Validation: 0/35/75/100 gauge checks and status variant checks passed; callbacks remain unchanged.
app.py
CHANGED
|
@@ -339,8 +339,95 @@ TOTEM_CSS = """
|
|
| 339 |
}
|
| 340 |
|
| 341 |
.totem-hero {
|
|
|
|
| 342 |
min-height: 222px;
|
| 343 |
padding: 44px 52px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
}
|
| 345 |
|
| 346 |
.totem-status-row {
|
|
@@ -349,6 +436,34 @@ TOTEM_CSS = """
|
|
| 349 |
align-items: center;
|
| 350 |
justify-content: space-between;
|
| 351 |
padding: 0 var(--space-5);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
}
|
| 353 |
|
| 354 |
.totem-metrics-grid {
|
|
@@ -499,6 +614,11 @@ TOTEM_CSS = """
|
|
| 499 |
.totem-lower-grid {
|
| 500 |
grid-template-columns: 1fr;
|
| 501 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
}
|
| 503 |
|
| 504 |
@media (max-width: 900px) {
|
|
@@ -512,6 +632,15 @@ TOTEM_CSS = """
|
|
| 512 |
.totem-topbar {
|
| 513 |
grid-template-columns: 1fr;
|
| 514 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
.totem-shell .totem-row {
|
| 516 |
grid-template-columns: 1fr;
|
| 517 |
}
|
|
@@ -803,30 +932,75 @@ def render_topbar(state: dict) -> str:
|
|
| 803 |
"""
|
| 804 |
|
| 805 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 806 |
def render_dashboard(state: dict) -> str:
|
| 807 |
"""
|
| 808 |
Stage 3/4 static shell wrapper.
|
| 809 |
Callback wiring remains untouched until later stages.
|
| 810 |
"""
|
| 811 |
s = normalize_dashboard_state(state)
|
| 812 |
-
signal = int(s.get("totem_signal", 0) or 0)
|
| 813 |
return f"""
|
| 814 |
<div class="totem-shell">
|
| 815 |
{render_sidebar(active='Home')}
|
| 816 |
<main class="totem-main">
|
| 817 |
<div class="totem-main-inner">
|
| 818 |
{render_topbar(s)}
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
<p style="margin:10px 0 0 0;font-size:22px;color:rgba(246,241,232,.92);font-family:var(--font-ui);">
|
| 822 |
-
Data-driven insight for stronger stories.
|
| 823 |
-
</p>
|
| 824 |
-
<p class="totem-muted" style="margin-top:14px;">TOTEM Signal {signal}%</p>
|
| 825 |
-
</section>
|
| 826 |
-
<section class="totem-status-row">
|
| 827 |
-
<div>Dashboard shell ready. Stage 3 static layout.</div>
|
| 828 |
-
<div class="totem-muted">Signal {signal}%</div>
|
| 829 |
-
</section>
|
| 830 |
<section class="totem-metrics-grid">
|
| 831 |
<div class="totem-row">
|
| 832 |
<div class="totem-placeholder">Metrics Placeholder</div>
|
|
|
|
| 339 |
}
|
| 340 |
|
| 341 |
.totem-hero {
|
| 342 |
+
position: relative;
|
| 343 |
min-height: 222px;
|
| 344 |
padding: 44px 52px;
|
| 345 |
+
overflow: hidden;
|
| 346 |
+
border: 1px solid rgba(242, 193, 78, 0.35);
|
| 347 |
+
background:
|
| 348 |
+
radial-gradient(circle at 85% 50%, rgba(138, 77, 255, 0.24), transparent 30%),
|
| 349 |
+
linear-gradient(120deg, rgba(13, 23, 51, 0.98), rgba(35, 15, 62, 0.92) 58%, rgba(8, 18, 45, 0.98));
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.totem-hero::after {
|
| 353 |
+
content: "";
|
| 354 |
+
position: absolute;
|
| 355 |
+
left: 18%;
|
| 356 |
+
right: 10%;
|
| 357 |
+
top: 44%;
|
| 358 |
+
height: 76px;
|
| 359 |
+
transform: rotate(-10deg);
|
| 360 |
+
border-radius: 999px;
|
| 361 |
+
filter: blur(12px);
|
| 362 |
+
background: linear-gradient(90deg, transparent, rgba(138, 77, 255, 0.45), rgba(242, 193, 78, 0.38), transparent);
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
.totem-hero-title {
|
| 366 |
+
position: relative;
|
| 367 |
+
z-index: 1;
|
| 368 |
+
margin: 0;
|
| 369 |
+
font-family: var(--font-display);
|
| 370 |
+
font-size: 44px;
|
| 371 |
+
line-height: 1.05;
|
| 372 |
+
color: var(--totem-gold);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
.totem-hero-subtitle {
|
| 376 |
+
position: relative;
|
| 377 |
+
z-index: 1;
|
| 378 |
+
margin-top: 10px;
|
| 379 |
+
font-size: 22px;
|
| 380 |
+
color: rgba(246, 241, 232, 0.92);
|
| 381 |
+
font-family: var(--font-ui);
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.totem-signal-wrap {
|
| 385 |
+
position: absolute;
|
| 386 |
+
right: 46px;
|
| 387 |
+
top: 31px;
|
| 388 |
+
width: 160px;
|
| 389 |
+
height: 160px;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
.totem-signal {
|
| 393 |
+
width: 100%;
|
| 394 |
+
height: 100%;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.signal-track {
|
| 398 |
+
fill: none;
|
| 399 |
+
stroke: rgba(174, 183, 204, 0.28);
|
| 400 |
+
stroke-width: 8;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.signal-progress {
|
| 404 |
+
fill: none;
|
| 405 |
+
stroke: var(--totem-gold);
|
| 406 |
+
stroke-width: 8;
|
| 407 |
+
stroke-linecap: round;
|
| 408 |
+
transform: rotate(-90deg);
|
| 409 |
+
transform-origin: 80px 80px;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.signal-core-ring {
|
| 413 |
+
fill: rgba(7, 18, 45, 0.7);
|
| 414 |
+
stroke: rgba(57, 201, 255, 0.55);
|
| 415 |
+
stroke-width: 1.5;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.signal-star {
|
| 419 |
+
fill: var(--totem-gold);
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.totem-signal-label {
|
| 423 |
+
position: absolute;
|
| 424 |
+
left: 0;
|
| 425 |
+
right: 0;
|
| 426 |
+
bottom: -18px;
|
| 427 |
+
text-align: center;
|
| 428 |
+
font-family: var(--font-ui);
|
| 429 |
+
font-size: 12px;
|
| 430 |
+
color: rgba(174, 183, 204, 0.98);
|
| 431 |
}
|
| 432 |
|
| 433 |
.totem-status-row {
|
|
|
|
| 436 |
align-items: center;
|
| 437 |
justify-content: space-between;
|
| 438 |
padding: 0 var(--space-5);
|
| 439 |
+
border-radius: 10px;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.totem-status-row.state-ready,
|
| 443 |
+
.totem-status-row.state-complete {
|
| 444 |
+
background: rgba(34, 181, 115, 0.1);
|
| 445 |
+
border-color: rgba(34, 181, 115, 0.35);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.totem-status-row.state-running {
|
| 449 |
+
background: rgba(242, 169, 59, 0.1);
|
| 450 |
+
border-color: rgba(242, 169, 59, 0.35);
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
.totem-status-row.state-error {
|
| 454 |
+
background: rgba(224, 75, 69, 0.1);
|
| 455 |
+
border-color: rgba(224, 75, 69, 0.35);
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
.totem-run-btn {
|
| 459 |
+
min-height: 34px;
|
| 460 |
+
border-radius: 8px;
|
| 461 |
+
border: 1px solid rgba(34, 181, 115, 0.5);
|
| 462 |
+
background: linear-gradient(180deg, rgba(34, 181, 115, 0.25), rgba(10, 40, 29, 0.45));
|
| 463 |
+
color: rgba(217, 251, 232, 0.95);
|
| 464 |
+
padding: 0 var(--space-4);
|
| 465 |
+
font-family: var(--font-ui);
|
| 466 |
+
font-weight: 600;
|
| 467 |
}
|
| 468 |
|
| 469 |
.totem-metrics-grid {
|
|
|
|
| 614 |
.totem-lower-grid {
|
| 615 |
grid-template-columns: 1fr;
|
| 616 |
}
|
| 617 |
+
.totem-signal-wrap {
|
| 618 |
+
width: 132px;
|
| 619 |
+
height: 132px;
|
| 620 |
+
right: 28px;
|
| 621 |
+
}
|
| 622 |
}
|
| 623 |
|
| 624 |
@media (max-width: 900px) {
|
|
|
|
| 632 |
.totem-topbar {
|
| 633 |
grid-template-columns: 1fr;
|
| 634 |
}
|
| 635 |
+
.totem-hero {
|
| 636 |
+
padding: 30px 24px;
|
| 637 |
+
}
|
| 638 |
+
.totem-signal-wrap {
|
| 639 |
+
position: relative;
|
| 640 |
+
right: auto;
|
| 641 |
+
top: auto;
|
| 642 |
+
margin-top: 24px;
|
| 643 |
+
}
|
| 644 |
.totem-shell .totem-row {
|
| 645 |
grid-template-columns: 1fr;
|
| 646 |
}
|
|
|
|
| 932 |
"""
|
| 933 |
|
| 934 |
|
| 935 |
+
def render_signal_gauge(value: int) -> str:
|
| 936 |
+
clamped = max(0, min(100, int(value or 0)))
|
| 937 |
+
circumference = 427.26
|
| 938 |
+
progress = clamped / 100.0
|
| 939 |
+
dash_offset = circumference * (1 - progress)
|
| 940 |
+
return f"""
|
| 941 |
+
<div class="totem-signal-wrap">
|
| 942 |
+
<svg class="totem-signal" viewBox="0 0 160 160" role="img" aria-label="TOTEM Signal {clamped} percent">
|
| 943 |
+
<circle class="signal-track" cx="80" cy="80" r="68"></circle>
|
| 944 |
+
<circle
|
| 945 |
+
class="signal-progress"
|
| 946 |
+
cx="80"
|
| 947 |
+
cy="80"
|
| 948 |
+
r="68"
|
| 949 |
+
style="stroke-dasharray:{circumference:.2f};stroke-dashoffset:{dash_offset:.2f};"
|
| 950 |
+
></circle>
|
| 951 |
+
<circle class="signal-core-ring" cx="80" cy="80" r="34"></circle>
|
| 952 |
+
<circle class="signal-core-ring" cx="80" cy="80" r="22"></circle>
|
| 953 |
+
<path class="signal-star" d="M80 58 L84.7 73.5 L100.9 73.5 L87.8 82.9 L92.6 98.4 L80 89.1 L67.4 98.4 L72.2 82.9 L59.1 73.5 L75.3 73.5 Z"></path>
|
| 954 |
+
</svg>
|
| 955 |
+
<div class="totem-signal-label">TOTEM Signal {clamped}%</div>
|
| 956 |
+
</div>
|
| 957 |
+
"""
|
| 958 |
+
|
| 959 |
+
|
| 960 |
+
def render_hero(state: dict) -> str:
|
| 961 |
+
signal = int(state.get("totem_signal", 0) or 0)
|
| 962 |
+
return f"""
|
| 963 |
+
<section class="totem-hero">
|
| 964 |
+
<h2 class="totem-hero-title">TOTEM Studio</h2>
|
| 965 |
+
<p class="totem-hero-subtitle">Data-driven insight for stronger stories.</p>
|
| 966 |
+
{render_signal_gauge(signal)}
|
| 967 |
+
</section>
|
| 968 |
+
"""
|
| 969 |
+
|
| 970 |
+
|
| 971 |
+
def render_status_row(state: dict) -> str:
|
| 972 |
+
status = str(state.get("analysis_status", "idle") or "idle").strip().lower()
|
| 973 |
+
text_by_status = {
|
| 974 |
+
"idle": "Dashboard ready. Click Run TOTEM Analysis to refresh metrics.",
|
| 975 |
+
"ready": "Dashboard ready. Click Run TOTEM Analysis to refresh metrics.",
|
| 976 |
+
"running": "TOTEM analysis running. Updating dashboard metrics...",
|
| 977 |
+
"complete": "TOTEM analysis complete. Dashboard metrics updated.",
|
| 978 |
+
"error": "TOTEM analysis error. Review logs and retry.",
|
| 979 |
+
}
|
| 980 |
+
css_state = status if status in {"ready", "running", "complete", "error"} else "ready"
|
| 981 |
+
message = esc(text_by_status.get(status, text_by_status["ready"]))
|
| 982 |
+
return f"""
|
| 983 |
+
<section class="totem-status-row state-{css_state}">
|
| 984 |
+
<div>{message}</div>
|
| 985 |
+
<button class="totem-run-btn" type="button" aria-label="Run TOTEM Analysis visual button">Run TOTEM Analysis</button>
|
| 986 |
+
</section>
|
| 987 |
+
"""
|
| 988 |
+
|
| 989 |
+
|
| 990 |
def render_dashboard(state: dict) -> str:
|
| 991 |
"""
|
| 992 |
Stage 3/4 static shell wrapper.
|
| 993 |
Callback wiring remains untouched until later stages.
|
| 994 |
"""
|
| 995 |
s = normalize_dashboard_state(state)
|
|
|
|
| 996 |
return f"""
|
| 997 |
<div class="totem-shell">
|
| 998 |
{render_sidebar(active='Home')}
|
| 999 |
<main class="totem-main">
|
| 1000 |
<div class="totem-main-inner">
|
| 1001 |
{render_topbar(s)}
|
| 1002 |
+
{render_hero(s)}
|
| 1003 |
+
{render_status_row(s)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1004 |
<section class="totem-metrics-grid">
|
| 1005 |
<div class="totem-row">
|
| 1006 |
<div class="totem-placeholder">Metrics Placeholder</div>
|