Pointf5ive commited on
Commit
3b3730b
·
1 Parent(s): 9cc52b4

stage 5: hero, status banner, and totem signal gauge

Browse files

Scope: 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.

Files changed (1) hide show
  1. app.py +186 -12
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
- <section class="totem-hero">
820
- <h2 style="margin:0;font-family:var(--font-display);font-size:44px;color:var(--totem-gold);">TOTEM Studio</h2>
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>