playwright-reborn 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.4
4
+
5
+ - Refresh the report UI with a persistent sidebar, dashboard overview, and clearer test exploration.
6
+ - Add Tags and Sort dropdowns, remembered light and dark themes, improved filter controls, and responsive long-name handling.
7
+
3
8
  ## 0.2.3
4
9
 
5
10
  - `steps: 'user'` leaves expectations and `test.attach` calls out of the step tree.
package/dist/render.js CHANGED
@@ -35,7 +35,7 @@ function renderHtml(json, assets) {
35
35
  <title>Report</title>
36
36
  ${style}
37
37
  </head>
38
- <body style="margin:0;background:#080808;color:#f0f0f0">
38
+ <body style="margin:0;background:#f3f7fd;color:#17324f">
39
39
  <div class="light"></div>
40
40
  <a class="skip" href="#tests">Skip to tests</a>
41
41
  <main class="page" id="app"></main>
@@ -31,22 +31,30 @@
31
31
  }
32
32
 
33
33
  :root {
34
- color-scheme: dark;
35
- --base: #080808;
36
- --surface: #111111;
37
- --border: #2b302e;
38
- --text: #f0f0f0;
39
- --subtle: #a2aaa6;
40
- --accent: #00ff88;
41
- --accent-hover: #00cf6e;
42
- --wash: rgba(0, 255, 136, 0.16);
43
- --hairline: rgba(255, 255, 255, 0.08);
44
- --pass: #96cbb0;
45
- --fail: #ff8989;
46
- --flaky: #edc66f;
47
- --skip: #888888;
48
- --timeout: #edc66f;
49
- --interrupted: #888888;
34
+ color-scheme: light;
35
+ --base: #f3f7fd;
36
+ --surface: #ffffff;
37
+ --control-active: #315386;
38
+ --pass-wash: #e8f5ee;
39
+ --fail-wash: #fff0f1;
40
+ --warning-wash: #fff4e5;
41
+ --heading: #315386;
42
+ --chart-2: #f58220;
43
+ --chart-3: #8aafd4;
44
+ --chart-4: #d0e0f3;
45
+ --border: #dbe5f0;
46
+ --text: #17324f;
47
+ --subtle: #5d7188;
48
+ --accent: #f58220;
49
+ --accent-hover: #aa4900;
50
+ --wash: #eaf2fc;
51
+ --hairline: rgba(49, 83, 134, 0.10);
52
+ --pass: #177b59;
53
+ --fail: #bd3740;
54
+ --flaky: #a15b08;
55
+ --skip: #7c8da2;
56
+ --timeout: #a15b08;
57
+ --interrupted: #6f7890;
50
58
  --sans: Geist, "Segoe UI", sans-serif;
51
59
  --display: Geist, "Segoe UI", sans-serif;
52
60
  --mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
@@ -54,26 +62,26 @@
54
62
  }
55
63
 
56
64
  body[data-accent="red"] {
57
- --accent: #ff4d4d;
58
- --accent-hover: #e03b3b;
65
+ --accent: #c64246;
66
+ --accent-hover: #a22c34;
59
67
  --wash: rgba(255, 77, 77, 0.16);
60
68
  }
61
69
 
62
70
  body[data-accent="blue"] {
63
- --accent: #4da3ff;
64
- --accent-hover: #2f86e0;
71
+ --accent: #315386;
72
+ --accent-hover: #203e6b;
65
73
  --wash: rgba(77, 163, 255, 0.16);
66
74
  }
67
75
 
68
76
  body[data-accent="amber"] {
69
- --accent: #ffb020;
70
- --accent-hover: #e09610;
77
+ --accent: #a76500;
78
+ --accent-hover: #8c5100;
71
79
  --wash: rgba(255, 176, 32, 0.16);
72
80
  }
73
81
 
74
82
  body[data-accent="violet"] {
75
- --accent: #b07cff;
76
- --accent-hover: #945fe0;
83
+ --accent: #7952b8;
84
+ --accent-hover: #5c3b92;
77
85
  --wash: rgba(176, 124, 255, 0.16);
78
86
  }
79
87
 
@@ -99,7 +107,7 @@ body {
99
107
  left: 12px;
100
108
  top: -40px;
101
109
  background: var(--accent);
102
- color: #080808;
110
+ color: var(--text);
103
111
  padding: 8px 12px;
104
112
  z-index: 10;
105
113
  }
@@ -108,9 +116,9 @@ body {
108
116
 
109
117
 
110
118
 
111
- button, input { font: inherit; color: inherit; }
119
+ button, input, select { font: inherit; color: inherit; }
112
120
  button { cursor: pointer; }
113
- :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
121
+ :focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
114
122
  ::selection { background: var(--wash); color: var(--text); }
115
123
 
116
124
  .top {
@@ -367,7 +375,7 @@ button { cursor: pointer; }
367
375
  transform: none;
368
376
  }
369
377
 
370
- .segment[data-status="passed"] { background: rgba(255, 255, 255, 0.55); }
378
+ .segment[data-status="passed"] { background: var(--pass); }
371
379
  .segment[data-status="failed"],
372
380
  .segment[data-status="timedOut"] { background: var(--fail); }
373
381
  .segment[data-status="interrupted"] { background: transparent; box-shadow: inset 0 0 0 1px var(--border); }
@@ -504,18 +512,18 @@ button { cursor: pointer; }
504
512
  }
505
513
 
506
514
  .run-row.is-dim { opacity: 0.28; }
507
- .run-row:hover { background: rgba(255, 255, 255, 0.04); }
515
+ .run-row:hover { background: var(--wash); }
508
516
  .run-row[aria-current="true"] { background: var(--wash); }
509
517
 
510
518
  .run-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
511
519
 
512
520
  .run-track {
513
521
  height: 8px;
514
- background: rgba(255, 255, 255, 0.08);
522
+ background: var(--hairline);
515
523
  overflow: hidden;
516
524
  }
517
525
 
518
- .run-fill { display: block; height: 100%; background: rgba(255, 255, 255, 0.55); }
526
+ .run-fill { display: block; height: 100%; background: var(--pass); }
519
527
  .run-fill[data-status="failed"],
520
528
  .run-fill[data-status="timedOut"],
521
529
  .run-fill[data-status="flaky"] { background: var(--accent); }
@@ -602,7 +610,7 @@ button { cursor: pointer; }
602
610
  .row[data-status="skipped"],
603
611
  .row[data-status="interrupted"] { border-left-color: var(--border); }
604
612
  .row[aria-current="true"] { background: var(--hairline); }
605
- .row:hover { background: rgba(255, 255, 255, 0.04); }
613
+ .row:hover { background: var(--wash); }
606
614
 
607
615
  .row-title {
608
616
  font-weight: 500;
@@ -848,7 +856,7 @@ button { cursor: pointer; }
848
856
  }
849
857
 
850
858
  .tile {
851
- border: 1px solid rgba(255, 255, 255, 0.08);
859
+ border: 1px solid var(--hairline);
852
860
  border-radius: 0;
853
861
  overflow: hidden;
854
862
  background: transparent;
@@ -862,7 +870,7 @@ button { cursor: pointer; }
862
870
 
863
871
  .log-card, .file-card {
864
872
  border: 0;
865
- border-top: 1px solid rgba(255, 255, 255, 0.08);
873
+ border-top: 1px solid var(--hairline);
866
874
  border-radius: 0;
867
875
  background: transparent;
868
876
  box-shadow: none;
@@ -907,7 +915,7 @@ dialog .close-shot {
907
915
  border-radius: 0;
908
916
  padding: 6px 12px;
909
917
  }
910
- dialog::backdrop { background: rgba(8, 8, 8, 0.92); }
918
+ dialog::backdrop { background: rgba(23, 50, 79, 0.72); }
911
919
 
912
920
  @keyframes rise {
913
921
  from { opacity: 0; transform: translateY(40px); }
@@ -924,230 +932,6 @@ dialog::backdrop { background: rgba(8, 8, 8, 0.92); }
924
932
  #tests { max-height: 45vh; overflow-y: auto; overscroll-behavior: contain; padding-bottom: 12px; }
925
933
  }
926
934
 
927
- .atmosphere {
928
- position: fixed;
929
- inset: 0;
930
- z-index: 0;
931
- pointer-events: none;
932
- background: var(--base);
933
- overflow: hidden;
934
- }
935
-
936
- .bands {
937
- position: absolute;
938
- left: 0;
939
- right: 0;
940
- top: 0;
941
- width: 100%;
942
- height: 100%;
943
- display: block;
944
- filter: blur(18px) saturate(1.05);
945
- opacity: 0.3;
946
- -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 18%, rgba(0, 0, 0, 0.4) 34%, transparent 52%);
947
- mask-image: linear-gradient(180deg, #000 0%, #000 18%, rgba(0, 0, 0, 0.4) 34%, transparent 52%);
948
- }
949
-
950
- .grain {
951
- position: absolute;
952
- inset: -20%;
953
- opacity: 0.035;
954
- mix-blend-mode: overlay;
955
- background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='0.85'/></svg>");
956
- background-size: 180px 180px;
957
- animation: grain 0.7s steps(2) infinite;
958
- }
959
-
960
- .veil {
961
- position: absolute;
962
- inset: 0;
963
- background: linear-gradient(180deg, rgba(5, 8, 6, 0) 0%, rgba(5, 8, 6, 0.12) 14%, rgba(5, 8, 6, 0.72) 30%, rgba(5, 8, 6, 0.9) 42%, rgba(5, 8, 6, 0.96) 100%);
964
- }
965
-
966
- .shell { position: relative; z-index: 1; }
967
-
968
- @keyframes grain {
969
- 0% { transform: translate(0, 0); }
970
- 100% { transform: translate(-2%, 1%); }
971
- }
972
-
973
- .page { width: 100%; max-width: none; margin: 0; padding: 0; }
974
-
975
- .shell {
976
- display: flex;
977
- flex-direction: column;
978
- min-height: 100vh;
979
- }
980
-
981
- .rail {
982
- position: sticky;
983
- top: 0;
984
- z-index: 5;
985
- height: auto;
986
- display: flex;
987
- flex-direction: row;
988
- align-items: center;
989
- gap: 28px;
990
- padding: 12px clamp(20px, 3vw, 48px);
991
- border-right: 0;
992
- background: rgba(8, 8, 8, 0.9);
993
- backdrop-filter: blur(16px);
994
- border-bottom: 1px solid var(--hairline);
995
- transition: transform 220ms ease;
996
- }
997
-
998
- .rail.is-hidden {
999
- transform: translateY(-110%);
1000
- pointer-events: none;
1001
- }
1002
-
1003
- .rail .meta { margin: 0 0 0 auto; flex-direction: row; gap: 16px; justify-content: flex-end; text-align: right; }
1004
- .rail .meta { min-width: 0; }
1005
- .rail .meta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 30ch; }
1006
-
1007
- .rail .tabbar {
1008
- flex-direction: row;
1009
- align-items: center;
1010
- width: auto;
1011
- flex: 1;
1012
- min-width: 0;
1013
- gap: 22px;
1014
- margin: 0;
1015
- padding: 0;
1016
- border: 0;
1017
- }
1018
-
1019
- .rail .tab {
1020
- text-align: left;
1021
- padding: 8px 0;
1022
- color: rgba(240, 240, 240, 0.72);
1023
- }
1024
-
1025
- .rail .tab[aria-selected="true"] {
1026
- background: none;
1027
- color: var(--text);
1028
- box-shadow: inset 0 -1px 0 var(--accent);
1029
- }
1030
-
1031
- .stage {
1032
- width: min(1440px, 100%);
1033
- margin: 0 auto;
1034
- padding: 24px clamp(20px, 3vw, 48px) 48px;
1035
- }
1036
-
1037
- .hero { margin: 8px 0 28px; }
1038
-
1039
- .word {
1040
- font-size: clamp(42px, 6vw, 80px);
1041
- font-weight: 500;
1042
- letter-spacing: -0.045em;
1043
- max-width: 100%;
1044
- overflow-wrap: anywhere;
1045
- }
1046
-
1047
-
1048
-
1049
- #panel-tests,
1050
- #panel-screenshots,
1051
- #panel-logs,
1052
- #panel-files {
1053
- background: transparent;
1054
- border: 0;
1055
- padding: 0;
1056
- }
1057
-
1058
- .console {
1059
- background: transparent;
1060
- border: 0;
1061
- }
1062
-
1063
- .console-bar {
1064
- display: flex;
1065
- justify-content: space-between;
1066
- gap: 16px;
1067
- padding: 12px 18px;
1068
- border-bottom: 1px solid rgba(255, 255, 255, 0.06);
1069
- color: var(--subtle);
1070
- font-size: 13px;
1071
- }
1072
-
1073
- .console-body { padding: 0; }
1074
-
1075
- .console .section-label { margin-top: 18px; }
1076
-
1077
- .console .strip-window,
1078
- .console .longest {
1079
- background: transparent;
1080
- border: 0;
1081
- box-shadow: none;
1082
- animation: none;
1083
- }
1084
-
1085
- #legend.legend {
1086
- background: transparent;
1087
- border: 0;
1088
- gap: 0;
1089
- }
1090
-
1091
- #legend .count {
1092
- background: transparent;
1093
- min-height: 0;
1094
- padding: 8px 18px 8px 0;
1095
- border-right: 1px solid rgba(255, 255, 255, 0.06);
1096
- }
1097
-
1098
- #legend .count:last-child { border-right: 0; }
1099
-
1100
- .strip.timeline .segment { height: 18px; }
1101
-
1102
- .section-label {
1103
- margin: 32px 0 10px;
1104
- color: var(--subtle);
1105
- font-size: 13px;
1106
- font-weight: 500;
1107
- }
1108
-
1109
- #legend.legend {
1110
- display: grid;
1111
- grid-template-columns: repeat(6, minmax(0, 1fr));
1112
- gap: 1px;
1113
- margin-top: 0;
1114
- background: transparent;
1115
- border: 0;
1116
- }
1117
-
1118
- #legend .count {
1119
- display: flex;
1120
- flex-direction: column;
1121
- align-items: flex-start;
1122
- gap: 4px;
1123
- min-height: 0;
1124
- padding: 4px 16px 4px 0;
1125
- background: transparent;
1126
- border: 0;
1127
- color: var(--subtle);
1128
- }
1129
-
1130
- #legend .count .n {
1131
- font-size: 28px;
1132
- line-height: 1;
1133
- letter-spacing: -0.04em;
1134
- }
1135
-
1136
- #legend .count[aria-pressed="true"] { background: transparent; }
1137
-
1138
- .strip.timeline {
1139
- display: block;
1140
- position: relative;
1141
- height: auto;
1142
- min-width: 0;
1143
- }
1144
-
1145
- .strip.timeline .segment {
1146
- position: absolute;
1147
- height: 18px;
1148
- min-width: 0;
1149
- }
1150
-
1151
935
  .evidence {
1152
936
  display: grid;
1153
937
  grid-template-columns: minmax(0, 1.1fr) minmax(220px, 0.9fr);
@@ -1218,7 +1002,7 @@ dialog::backdrop { background: rgba(8, 8, 8, 0.92); }
1218
1002
  .row-meta { flex-wrap: wrap; gap: 2px 12px; }
1219
1003
  .band { padding: 6px 0; }
1220
1004
  .band:hover .band-label { color: var(--text); }
1221
- .find { padding: 8px 10px; border: 1px solid var(--border); background: rgba(8, 8, 8, 0.65); border-radius: 6px; }
1005
+ .find { padding: 8px 10px; border: 1px solid var(--border); background: var(--surface); border-radius: 6px; }
1222
1006
  .find:focus { border-color: var(--accent); }
1223
1007
  .list-summary { display: flex; align-items: center; gap: 16px; margin: 18px 0 10px; color: var(--subtle); font-size: 13px; }
1224
1008
  .reset-filters { background: none; border: 0; color: var(--accent); padding: 4px 0; }
@@ -1229,7 +1013,7 @@ dialog::backdrop { background: rgba(8, 8, 8, 0.92); }
1229
1013
  .row[aria-current="true"] { background: var(--wash); }
1230
1014
  .error { border-left-color: var(--fail); }
1231
1015
  .step.bad .mark { background: var(--fail); }
1232
- .gallery .tile { border-radius: 8px; background: rgba(17, 17, 17, 0.6); }
1016
+ .gallery .tile { border-radius: 8px; background: var(--surface); }
1233
1017
  .tile img { object-fit: contain; background: var(--base); }
1234
1018
  .well pre { padding: 12px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; }
1235
1019
  dialog { border: 1px solid var(--border); border-radius: 8px; padding: 12px; max-height: 95vh; }
@@ -1246,3 +1030,307 @@ dialog img { max-width: calc(92vw - 26px); object-fit: contain; }
1246
1030
  @media (prefers-reduced-motion: reduce) {
1247
1031
  *, *::before, *::after { scroll-behavior: auto !important; }
1248
1032
  }
1033
+
1034
+ /* Report workspace: persistent navigation and a data-focused dashboard. */
1035
+ body { background: var(--base) !important; color: var(--text) !important; }
1036
+ .light { display: none; }
1037
+ .page { width: 100%; max-width: none; margin: 0; padding: 0; }
1038
+ .shell { display: grid; grid-template-columns: 244px minmax(0, 1fr); min-height: 100vh; }
1039
+ .icon { width: 20px; height: 20px; flex-shrink: 0; }
1040
+ .rail {
1041
+ position: sticky;
1042
+ top: 0;
1043
+ height: 100vh;
1044
+ overflow-y: auto;
1045
+ display: flex;
1046
+ flex-direction: column;
1047
+ padding: 28px 18px 20px;
1048
+ background: var(--surface);
1049
+ border-right: 1px solid var(--border);
1050
+ z-index: 5;
1051
+ }
1052
+ .sidebar-brand { display: flex; align-items: center; gap: 12px; padding: 0 8px; font-size: 23px; font-weight: 600; letter-spacing: -.045em; }
1053
+ .sidebar-brand > .icon { width: 40px; height: 40px; padding: 8px; border-radius: 12px; color: #ffffff; background: #315386; stroke-width: 2.2; }
1054
+ .brand-caption { display: block; font-size: 11px; color: var(--subtle); font-weight: 400; letter-spacing: 0; margin-top: -2px; }
1055
+ .project-card { display: flex; align-items: center; gap: 12px; margin: 32px 0 28px; padding: 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); }
1056
+ .project-card > .icon { color: var(--accent); }
1057
+ .project-card > div { min-width: 0; }
1058
+ .project-caption { display: block; font-size: 11px; color: var(--subtle); }
1059
+ .project-card strong { display: block; font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1060
+ .nav-caption { font-size: 12px; color: var(--subtle); margin: 0 12px 12px; }
1061
+ .rail .tabbar { display: flex; flex-direction: column; flex-wrap: nowrap; align-items: stretch; gap: 6px; padding: 0; margin: 0; width: 100%; border: 0; }
1062
+ .rail .tab { display: flex; align-items: center; gap: 12px; min-height: 46px; padding: 12px; text-align: left; border: 1px solid transparent; border-radius: 8px; color: var(--subtle); font-size: 14px; font-weight: 500; }
1063
+ .rail .tab[aria-selected="true"] { color: var(--heading); background: var(--wash); border-color: var(--border); box-shadow: inset 3px 0 0 var(--accent); }
1064
+ .rail .tab:hover { background: var(--wash); color: var(--text); }
1065
+ .nav-count { margin-left: auto; min-width: 24px; padding: 1px 5px; border-radius: 4px; text-align: center; font-size: 11px; background: var(--hairline); }
1066
+ .sidebar-bottom { margin-top: auto; padding: 44px 12px 0; }
1067
+ .sidebar-bottom .nav-caption { margin-left: 0; }
1068
+ .sidebar-state { font-size: 12px; }
1069
+ .sidebar-note { font-size: 11px; color: var(--subtle); margin: 12px 0 22px; }
1070
+ .sidebar-note span { display: block; margin-top: 3px; }
1071
+ kbd { display: inline-grid; place-items: center; width: 22px; height: 22px; border: 1px solid var(--border); border-radius: 5px; font-family: var(--sans); font-size: 12px; color: var(--subtle); }
1072
+ .keyboard-hint { display: flex; gap: 10px; align-items: center; color: var(--subtle); font-size: 12px; padding-top: 16px; border-top: 1px solid var(--border); }
1073
+ .stage { min-width: 0; width: 100%; }
1074
+ .workspace-bar { height: 76px; display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 0 36px; border-bottom: 1px solid var(--border); background: var(--surface); }
1075
+ .breadcrumb { min-width: 0; display: flex; align-items: center; gap: 14px; font-size: 13px; color: var(--subtle); }
1076
+ #breadcrumb-company { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
1077
+ .breadcrumb strong { font-weight: 500; color: var(--text); }
1078
+ .breadcrumb-separator { color: #8296b1; }
1079
+ .report-content { max-width: 1600px; margin: 0 auto; padding: 30px 36px; }
1080
+ .panel { margin-top: 0; }
1081
+ .hero { margin: 0; gap: 18px; align-items: center; animation: none; }
1082
+ .dashboard-kicker { margin: 0 0 8px; font-size: 12px; color: var(--accent); }
1083
+ .word { font-size: 36px; font-weight: 600; letter-spacing: -.045em; line-height: 1.15; }
1084
+ .product { font-size: 15px; color: var(--subtle); letter-spacing: 0; margin-top: 10px; font-weight: 400; }
1085
+ .summary { font-size: 13px; margin-top: 6px; max-width: 65ch; }
1086
+ .summary:empty { display: none; }
1087
+ #meta { margin: 18px 0 0; font-size: 12px; gap: 6px 18px; }
1088
+ #meta span { overflow-wrap: anywhere; }
1089
+ .metric-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; margin: 26px 0 24px; }
1090
+ .metric { padding: 20px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; min-width: 0; }
1091
+ .metric-label { display: flex; align-items: center; justify-content: space-between; gap: 6px; color: var(--text); font-size: 12px; }
1092
+ .metric-label .icon { width: 17px; height: 17px; color: var(--heading); }
1093
+ .metric-value { display: block; margin: 12px 0 6px; font-size: clamp(26px, 3vw, 34px); line-height: 1.1; font-weight: 500; letter-spacing: -.045em; font-variant-numeric: tabular-nums; }
1094
+ .metric-note { font-size: 11px; color: var(--subtle); }
1095
+ .metric.positive .metric-value, .metric.positive .icon { color: var(--pass); }
1096
+ .metric.warning .metric-value, .metric.warning .icon { color: var(--flaky); }
1097
+ .dashboard-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr); gap: 24px; align-items: stretch; }
1098
+ .dashboard-card { min-width: 0; padding: 24px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; }
1099
+ .card-heading { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 24px; }
1100
+ .card-heading h2, .range-head .section-label { margin: 0; font-size: 15px; color: var(--text); font-weight: 500; }
1101
+ .card-detail { font-size: 11px; color: var(--subtle); background: var(--hairline); border-radius: 5px; padding: 3px 8px; }
1102
+ .result-body { display: flex; align-items: center; gap: 28px; min-height: 185px; }
1103
+ .result-ring { width: 164px; height: 164px; flex: 0 0 164px; padding: 12px; border-radius: 50%; transform: rotate(0deg); }
1104
+ .result-ring > div { width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 50%; background: var(--surface); }
1105
+ .result-ring strong { font-size: 30px; font-weight: 500; letter-spacing: -.04em; }
1106
+ .result-ring span { font-size: 12px; color: var(--subtle); }
1107
+ #legend.legend { flex: 1; display: flex; flex-direction: column; align-items: stretch; gap: 2px; margin: 0; min-width: 0; }
1108
+ #legend .count { display: flex; flex-direction: row-reverse; align-items: center; justify-content: flex-end; gap: 10px; min-height: 29px; padding: 4px 0; border: 0; font-size: 12px; color: var(--subtle); background: transparent; }
1109
+ #legend .count[data-status="all"] { display: none; }
1110
+ #legend .count .n { font-size: 13px; font-weight: 500; margin: 0 0 0 auto; color: var(--text); line-height: 1.4; letter-spacing: 0; }
1111
+ #legend .count::after { content: ''; width: 7px; height: 7px; border-radius: 50%; background: var(--skip); }
1112
+ #legend .count[data-status="passed"]::after { background: var(--pass); }
1113
+ #legend .count[data-status="failed"]::after, #legend .count[data-status="timedOut"]::after { background: var(--fail); }
1114
+ #legend .count[data-status="flaky"]::after { background: var(--flaky); }
1115
+ #legend .count[data-status="interrupted"]::after { background: var(--timeout); }
1116
+ #legend .count:hover { color: var(--text); }
1117
+ #legend .count[aria-pressed="true"] { box-shadow: none; }
1118
+ .card-note { font-size: 11px; color: var(--subtle); margin: 22px 0 0; line-height: 1.6; }
1119
+ .range-head { margin: 0 0 12px; align-items: center; }
1120
+ .views { padding: 3px; gap: 0; background: var(--base); border: 1px solid var(--border); border-radius: 6px; }
1121
+ .view { padding: 4px 10px; font-size: 11px; border-radius: 4px; min-height: 26px; }
1122
+ .view[aria-pressed="true"] { background: var(--control-active); color: #ffffff; box-shadow: none; }
1123
+ .section-note { margin: 0; font-size: 11px; line-height: 1.6; max-width: 60ch; }
1124
+ .charts { grid-template-columns: 130px minmax(0, 1fr); gap: 22px; margin-top: 28px; min-height: 164px; }
1125
+ .pie { width: 130px; height: auto; margin: 0; }
1126
+ .band { padding: 6px 0; grid-template-columns: 7px minmax(0, 1fr) auto; gap: 5px 8px; }
1127
+ .swatch { width: 7px; height: 7px; border-radius: 2px; margin-top: 5px; }
1128
+ .band-label { font-size: 11px; }
1129
+ .band .n { font-size: 12px; }
1130
+ .band-track, .band-fill { border-radius: 3px; }
1131
+ .band-track { height: 4px; }
1132
+ .strip-window { padding: 0; margin-top: 18px; background: transparent; border: 0; animation: none; max-height: 240px; overflow-y: auto; }
1133
+ .strip-scroll { overflow-x: hidden; }
1134
+ .console .run-row { grid-template-columns: 60px minmax(0, 1fr) auto; gap: 8px; padding: 7px 6px; font-size: 11px; }
1135
+ .console .run-track { display: none; }
1136
+ .console .run-time { font-size: 10px; }
1137
+ .dashboard-link { display: inline-flex; align-items: center; gap: 6px; padding: 0; border: 0; background: transparent; color: var(--accent); font-size: 11px; }
1138
+ .dashboard-link .icon { width: 14px; height: 14px; }
1139
+ .attention-card .card-heading { margin-bottom: 12px; }
1140
+ .attention-row { display: grid; grid-template-columns: 56px minmax(0, 1fr) auto; gap: 12px; align-items: center; width: 100%; padding: 16px 0; border: 0; border-top: 1px solid var(--hairline); color: var(--text); background: transparent; text-align: left; }
1141
+ .attention-row:hover { background: var(--wash); }
1142
+ .attention-status { font-size: 11px; }
1143
+ .attention-test { min-width: 0; }
1144
+ .attention-test strong { display: block; font-size: 12px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1145
+ .attention-test > span { display: block; color: var(--subtle); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 4px; }
1146
+ .attention-time { font-size: 11px; color: var(--subtle); }
1147
+ .environment-card .card-heading > .icon { width: 16px; color: var(--subtle); }
1148
+ .run-details { margin: 0; }
1149
+ .run-details > div { display: flex; justify-content: space-between; align-items: baseline; gap: 20px; margin: 14px 0; font-size: 12px; }
1150
+ .run-details dt { color: var(--subtle); }
1151
+ .run-details dd { margin: 0; text-align: right; overflow-wrap: anywhere; min-width: 0; }
1152
+ .info { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; margin: 20px 0 0; padding-top: 20px; border-top: 1px solid var(--border); }
1153
+ .info dd { font-size: 12px; }
1154
+ .info dt { font-size: 11px; }
1155
+ .foot { font-size: 11px; margin-top: 28px; border-color: var(--border); }
1156
+ .page-heading { margin: 0 0 24px; }
1157
+ .page-heading h2, .panel > h2 { font-size: 30px; font-weight: 600; letter-spacing: -.035em; margin: 0 0 8px; }
1158
+ .page-heading p { font-size: 13px; color: var(--subtle); margin: 0; }
1159
+ #test-tools { padding: 16px; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; gap: 12px 18px; }
1160
+ #test-tools .count { min-height: 30px; padding: 4px 8px; border-radius: 5px; }
1161
+ #test-tools .count[aria-pressed="true"] { background: var(--wash); border-color: transparent; color: var(--accent); }
1162
+ .split { margin-top: 16px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: clip; }
1163
+ #tests { padding: 8px 16px 16px; }
1164
+ .detail { padding: 24px; top: 20px; max-height: calc(100vh - 40px); }
1165
+ .detail h2 { font-size: 22px; line-height: 1.3; }
1166
+ .gallery .tile { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; }
1167
+ .log-card, .file-group { padding: 20px; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; margin-top: 16px; }
1168
+ .log-card h3 { font-size: 16px; font-weight: 500; overflow-wrap: anywhere; }
1169
+ .well pre { background: var(--base); }
1170
+ .run-track, .run-fill { border-radius: 4px; }
1171
+ .banner { background: var(--fail-wash); padding: 14px; border-radius: 6px; font-size: 12px; }
1172
+ @media (min-width: 801px) and (max-width: 1200px) {
1173
+ .shell { grid-template-columns: 216px minmax(0, 1fr); }
1174
+ .workspace-bar { padding: 0 24px; }
1175
+ .report-content { padding: 28px 24px; }
1176
+ .dashboard-card { padding: 20px; }
1177
+ .result-body { gap: 16px; }
1178
+ .result-ring { width: 128px; height: 128px; flex-basis: 128px; padding: 10px; }
1179
+ .charts { grid-template-columns: 105px minmax(0, 1fr); gap: 14px; }
1180
+ .pie { width: 105px; }
1181
+ .metric { padding: 16px; }
1182
+ }
1183
+ @media (max-width: 1050px) and (min-width: 801px) {
1184
+ .dashboard-grid { grid-template-columns: 1fr; }
1185
+ .metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1186
+ .result-body { justify-content: center; }
1187
+ #legend.legend { max-width: 280px; }
1188
+ .charts { grid-template-columns: 150px minmax(0, 1fr); }
1189
+ .pie { width: 150px; }
1190
+ }
1191
+ @media (max-width: 800px) {
1192
+ .shell { grid-template-columns: minmax(0, 1fr); }
1193
+ .rail { position: relative; top: 0; height: auto; overflow: visible; padding: 16px; border-right: 0; border-bottom: 1px solid var(--border); gap: 0; }
1194
+ .sidebar-brand { font-size: 20px; padding: 0; }
1195
+ .sidebar-brand > .icon { width: 34px; height: 34px; }
1196
+ .brand-caption { display: none; }
1197
+ .project-card { display: none; }
1198
+ .rail > .nav-caption, .sidebar-bottom { display: none; }
1199
+ .rail .tabbar { flex-direction: row; gap: 6px; overflow-x: auto; margin-top: 16px; padding-bottom: 2px; }
1200
+ .rail .tab { flex: 0 0 auto; min-height: 40px; padding: 8px 10px; font-size: 12px; gap: 7px; }
1201
+ .rail .tab .icon { width: 16px; height: 16px; }
1202
+ .rail .tab[aria-selected="true"] { box-shadow: none; }
1203
+ .nav-count { display: none; }
1204
+ .workspace-bar { height: 58px; padding: 0 16px; gap: 12px; }
1205
+ .breadcrumb { font-size: 11px; gap: 10px; }
1206
+ .report-content { padding: 24px 16px; }
1207
+ .hero { flex-direction: row; align-items: flex-start; gap: 12px; }
1208
+ .word { font-size: 30px; }
1209
+ .hero .run-state { font-size: 11px; padding: 5px 8px; margin-top: 24px; }
1210
+ .product { font-size: 13px; }
1211
+ #meta { font-size: 11px; }
1212
+ .metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 22px; }
1213
+ .metric { padding: 14px; }
1214
+ .metric-label { font-size: 11px; }
1215
+ .metric-value { font-size: 28px; }
1216
+ .metric-note { font-size: 10px; }
1217
+ .dashboard-grid { grid-template-columns: minmax(0, 1fr); gap: 16px; }
1218
+ .dashboard-card { padding: 18px; }
1219
+ .card-heading { gap: 12px; }
1220
+ .result-body { gap: 22px; justify-content: center; }
1221
+ .result-ring { width: 128px; height: 128px; flex-basis: 128px; padding: 10px; }
1222
+ #legend.legend { max-width: 250px; }
1223
+ #legend .count { min-height: 32px; }
1224
+ .charts { grid-template-columns: 104px minmax(0, 1fr); gap: 14px; margin-top: 20px; }
1225
+ .pie { width: 104px; }
1226
+ .band { min-height: 36px; }
1227
+ .band-label { font-size: 10px; }
1228
+ .views .view { min-height: 28px; }
1229
+ .attention-row { gap: 8px; grid-template-columns: 48px minmax(0, 1fr) auto; }
1230
+ .dashboard-link { font-size: 10px; }
1231
+ #test-tools { padding: 12px; }
1232
+ .detail { position: static; padding: 20px 16px; max-height: none; }
1233
+ #tests { padding: 8px 12px 12px; }
1234
+ .page-heading h2, .panel > h2 { font-size: 28px; }
1235
+ }
1236
+ @media (prefers-reduced-motion: reduce) {
1237
+ .panel-enter { animation: none; }
1238
+ }
1239
+
1240
+ /* Light workspace theme with navy typography and warm accent details. */
1241
+ .sidebar-brand { color: var(--heading); }
1242
+ .sidebar-brand > .icon { border-radius: 13px; box-shadow: inset 0 -3px 0 var(--accent); }
1243
+ .project-card { background: var(--base); border-radius: 14px; }
1244
+ .project-card > .icon { color: var(--heading); }
1245
+ .rail .tab { border-radius: 24px; }
1246
+ .rail .tab[aria-selected="true"] { box-shadow: inset 3px 0 0 var(--accent); }
1247
+ .rail .tab:hover { color: var(--heading); }
1248
+ .workspace-bar { box-shadow: 0 1px 6px rgba(49, 83, 134, .025); }
1249
+ .dashboard-kicker { color: var(--heading); }
1250
+ .word { color: var(--heading); font-size: 40px; font-weight: 500; }
1251
+ .word::after { content: ''; display: block; width: 52px; height: 4px; margin-top: 14px; border-radius: 4px; background: var(--accent); }
1252
+ .hero .product { margin-top: 14px; }
1253
+ .metric, .dashboard-card { border-radius: 16px; border-color: var(--border); box-shadow: 0 3px 15px rgba(49, 83, 134, .035); }
1254
+ .metric-label .icon { box-sizing: content-box; padding: 7px; background: var(--wash); border-radius: 50%; }
1255
+ .metric-value { color: var(--heading); }
1256
+ .card-heading h2, .range-head .section-label, .page-heading h2, .panel > h2, .detail h2, .file-group h3, .log-card h3 { color: var(--heading); }
1257
+ .card-detail { background: var(--wash); color: var(--heading); border-radius: 12px; padding: 4px 10px; }
1258
+ .views { border-radius: 20px; padding: 4px; border-color: var(--border); }
1259
+ .view { border-radius: 16px; padding: 5px 12px; }
1260
+ .result-ring strong { color: var(--heading); }
1261
+ .dashboard-link { color: var(--heading); padding-bottom: 3px; border-bottom: 2px solid var(--accent); }
1262
+ .dashboard-link:hover { color: var(--accent-hover); }
1263
+ #test-tools .count[aria-pressed="true"] { color: var(--heading); }
1264
+ .subtab[aria-pressed="true"], .attempt[aria-pressed="true"], .order-btn[aria-pressed="true"] { color: var(--heading); box-shadow: inset 0 -2px 0 var(--accent); }
1265
+ .file-link, .open-test, .more, .reset-filters { color: var(--heading); }
1266
+ .file-link:hover, .open-test:hover { color: var(--accent-hover); }
1267
+ .attention-row { color: var(--text); }
1268
+ .row[aria-current="true"] .row-title { color: var(--heading); }
1269
+ .band[aria-pressed="true"] .band-label, .band[aria-pressed="true"] .n, .run-row[aria-current="true"] .run-name { color: var(--heading); }
1270
+ .log-card, .file-group, .gallery .tile, .split { border-radius: 14px; }
1271
+ .run-state { border-color: transparent; background: var(--wash); font-weight: 500; }
1272
+ .run-state[data-status="passed"] { background: var(--pass-wash); }
1273
+ .run-state[data-status="failed"] { background: var(--fail-wash); }
1274
+ .run-state[data-status="timedout"], .run-state[data-status="interrupted"] { background: var(--warning-wash); }
1275
+ .banner { border-left-color: var(--fail); }
1276
+ .find:focus { border-color: var(--heading); }
1277
+ .skip { background: var(--heading); color: #ffffff; }
1278
+ @media (max-width: 800px) {
1279
+ .word { font-size: 30px; }
1280
+ .rail .tab[aria-selected="true"] { box-shadow: none; }
1281
+ }
1282
+
1283
+ /* Shared controls and theme tokens keep both appearances consistent. */
1284
+ body[data-theme="dark"] {
1285
+ color-scheme: dark;
1286
+ --base: #101b2b;
1287
+ --surface: #18263a;
1288
+ --border: #2c405a;
1289
+ --text: #e7eef8;
1290
+ --subtle: #a1b2ca;
1291
+ --heading: #aecaf0;
1292
+ --control-active: #315386;
1293
+ --wash: #223a58;
1294
+ --hairline: rgba(174, 202, 240, .12);
1295
+ --pass: #67d4a7;
1296
+ --fail: #ff8b92;
1297
+ --flaky: #f7bb61;
1298
+ --skip: #8b9bb0;
1299
+ --timeout: #f7bb61;
1300
+ --interrupted: #8b9bb0;
1301
+ --pass-wash: #193d35;
1302
+ --fail-wash: #452b3a;
1303
+ --warning-wash: #403528;
1304
+ --chart-3: #709bce;
1305
+ --chart-4: #3e597b;
1306
+ --accent-hover: #ffbc83;
1307
+ }
1308
+ .theme-toggle { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 9px; background: transparent; border: 1px solid var(--border); border-radius: 20px; color: var(--subtle); padding: 8px 12px; font-size: 12px; }
1309
+ .theme-toggle .icon { width: 16px; height: 16px; }
1310
+ .theme-toggle:hover { color: var(--heading); background: var(--wash); }
1311
+ .theme-switch { width: 28px; height: 16px; padding: 2px; background: var(--border); border-radius: 9px; }
1312
+ .theme-switch::before { content: ''; display: block; width: 12px; height: 12px; background: var(--surface); border-radius: 50%; }
1313
+ .theme-toggle[aria-checked="true"] .theme-switch { background: var(--accent); }
1314
+ .theme-toggle[aria-checked="true"] .theme-switch::before { transform: translateX(12px); background: #ffffff; }
1315
+ .filter-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 24px; margin-top: 16px; }
1316
+ .filter-toolbar .tags, .filter-toolbar .order { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; margin: 0; }
1317
+ .filter-control { display: inline-flex; align-items: center; gap: 10px; min-width: 0; font-size: 12px; color: var(--subtle); }
1318
+ .filter-select { min-width: 148px; max-width: 240px; height: 38px; padding: 7px 34px 7px 12px; border: 1px solid var(--border); border-radius: 8px; color: var(--text); background-color: var(--surface); appearance: auto; cursor: pointer; font-size: 12px; }
1319
+ .filter-select:disabled { cursor: default; opacity: .6; }
1320
+ .filter-select:hover:not(:disabled) { border-color: var(--heading); }
1321
+ .filter-select:focus-visible { outline: 2px solid var(--heading); outline-offset: 2px; }
1322
+ .filter-toolbar .band-clear { display: inline-flex; align-items: center; gap: 8px; padding: 6px 10px; border: 1px solid var(--border); border-radius: 16px; background: var(--wash); color: var(--heading); font-size: 11px; min-height: 30px; }
1323
+ .filter-toolbar .band-clear:hover { border-color: var(--heading); }
1324
+ .list-summary { margin: 14px 0 10px; }
1325
+ .row-title { white-space: normal; line-height: 1.4; overflow-wrap: anywhere; }
1326
+ .row-time { align-self: start; }
1327
+ #shot-filters .shot-filter { padding: 6px 12px; border-radius: 18px; border: 1px solid var(--border); }
1328
+ #shot-filters .shot-filter[aria-pressed="true"] { background: var(--wash); color: var(--heading); border-color: var(--heading); }
1329
+ @media (max-width: 800px) {
1330
+ .theme-toggle { padding: 8px; gap: 6px; }
1331
+ .theme-toggle > span:not(.theme-switch) { display: none; }
1332
+ .filter-toolbar { gap: 12px 18px; }
1333
+ .filter-toolbar .tags, .filter-toolbar .order { gap: 12px; }
1334
+ .filter-control { gap: 8px; }
1335
+ .filter-select { min-width: 115px; max-width: 190px; }
1336
+ }
package/dist/ui/report.js CHANGED
@@ -47,18 +47,22 @@
47
47
  var findTimer = 0;
48
48
 
49
49
  app.innerHTML = [
50
- '<div class="atmosphere" aria-hidden="true"><canvas class="bands"></canvas><div class="grain"></div><div class="veil"></div></div>',
51
50
  '<div class="shell">',
52
51
  '<aside class="rail">',
53
- '<div class="tabbar" role="tablist" aria-label="Report">',
52
+ '<div class="sidebar-brand">' + icon('brand') + '<span>Reborn<span class="brand-caption">Test reports</span></span></div>',
53
+ '<div class="project-card">' + icon('folder') + '<div><span class="project-caption">Workspace</span><strong id="workspace-name"></strong></div></div>',
54
+ '<p class="nav-caption">Run workspace</p>',
55
+ '<div class="tabbar" role="tablist" aria-label="Report" aria-orientation="vertical">',
54
56
 
55
57
  TABS.map(function (id) {
56
- return tabButton(id, id.charAt(0).toUpperCase() + id.slice(1));
58
+ return tabButton(id, id === 'overview' ? 'Dashboard' : id.charAt(0).toUpperCase() + id.slice(1));
57
59
  }).join(''),
58
60
  '</div>',
59
- '<p class="meta" id="meta"></p>',
61
+ '<div class="sidebar-bottom"><p class="nav-caption">Current run</p><p class="sidebar-state run-state" id="sidebar-state"></p><p class="sidebar-note">Completed <span id="sidebar-time"></span></p><div class="keyboard-hint"><kbd>/</kbd><span>Find a test</span></div></div>',
60
62
  '</aside>',
61
63
  '<div class="stage">',
64
+ '<header class="workspace-bar"><div class="breadcrumb"><span id="breadcrumb-company"></span><span class="breadcrumb-separator">/</span><strong id="page-name">Dashboard</strong></div><button class="theme-toggle" id="theme-toggle" type="button" role="switch" aria-checked="false" aria-label="Dark mode">' + icon('moon') + '<span>Dark mode</span><span class="theme-switch" aria-hidden="true"></span></button></header>',
65
+ '<div class="report-content">',
62
66
  '<div id="panel-overview" role="tabpanel"></div>',
63
67
  '<div id="panel-tests" role="tabpanel" hidden></div>',
64
68
  '<div id="panel-screenshots" role="tabpanel" hidden></div>',
@@ -67,11 +71,16 @@
67
71
  '<p class="foot" id="foot"></p>',
68
72
  '</div>',
69
73
  '</div>',
74
+ '</div>',
70
75
  ].join('');
71
76
 
72
77
  document.body.dataset.accent = accentName();
78
+ applyTheme(readTheme());
73
79
  document.title = companyName() + (data.summary ? ' · ' + data.summary : '');
74
- fill(document.getElementById('meta'), metaBits().map(function (bit) { return el('span', null, [bit]); }));
80
+ document.getElementById('workspace-name').textContent = companyName();
81
+ document.getElementById('breadcrumb-company').textContent = companyName();
82
+ document.getElementById('sidebar-time').textContent = formatWhen(data.generatedAt);
83
+ setRunState(document.getElementById('sidebar-state'));
75
84
  var footBits = [];
76
85
  if (data.showCredit === true) footBits.push(el('span', null, ['Open source by World of Z']));
77
86
  footBits.push(el('span', null, [formatWhen(data.generatedAt)]));
@@ -83,94 +92,41 @@
83
92
  ['3to5', '3 to 5 minutes'],
84
93
  ['over5', 'Over 5 minutes'],
85
94
  ];
86
- var CHART_COLORS = ['var(--accent)', 'rgba(255,255,255,0.72)', 'rgba(255,255,255,0.42)', 'rgba(255,255,255,0.18)'];
95
+ var CHART_COLORS = ['var(--heading)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)'];
87
96
 
97
+ syncTabOrientation();
98
+ window.addEventListener('resize', syncTabOrientation);
88
99
  paintOverview();
89
100
  showTab(state.tab, true);
90
101
  bind();
91
- watchHeader();
92
- startCurtain();
93
-
94
- function startCurtain() {
95
- var canvas = document.querySelector('.atmosphere .bands');
96
- if (!canvas || !canvas.getContext) return;
97
- var ctx = canvas.getContext('2d');
98
- if (!ctx) return;
99
- var ratio = 1;
100
- var columns = [];
101
- var count = 22;
102
- for (var i = 0; i < count; i += 1) {
103
- var n = Math.sin(210 + i * 2.17);
104
- columns.push({
105
- at: (i + 0.35 + n * 0.15) / count,
106
- width: 0.012 + (0.5 + n * 0.5) * 0.018,
107
- speed: 0.22 + (i % 6) * 0.05,
108
- phase: i * 1.37,
109
- depth: i % 3,
110
- });
111
- }
112
- function resize() {
113
- ratio = Math.min(window.devicePixelRatio || 1, 2);
114
- var rect = canvas.getBoundingClientRect();
115
- canvas.width = Math.max(1, Math.floor(rect.width * ratio));
116
- canvas.height = Math.max(1, Math.floor(rect.height * ratio));
117
- }
118
- var rgb = accentParts();
119
- function draw(now) {
120
- var w = canvas.width;
121
- var h = canvas.height;
122
- var t = reduce ? 8 : now / 1000;
123
- ctx.clearRect(0, 0, w, h);
124
- var wash = ctx.createLinearGradient(0, 0, 0, h);
125
- wash.addColorStop(0, 'rgba(' + rgb + ', 0.10)');
126
- wash.addColorStop(0.42, '#080b0a');
127
- wash.addColorStop(1, '#080808');
128
- ctx.fillStyle = wash;
129
- ctx.fillRect(0, 0, w, h);
130
- columns.forEach(function (column) {
131
- var sway = Math.sin(t * column.speed + column.phase) * 0.02;
132
- var breath = 0.4 + 0.6 * (0.5 + 0.5 * Math.sin(t * column.speed * 1.4 + column.phase));
133
- var x = (column.at + sway) * w;
134
- var spread = Math.max(10, column.width * w * (0.55 + breath * 0.35));
135
- var gradient = ctx.createLinearGradient(x - spread, 0, x + spread, 0);
136
- var alpha = 0.28 + 0.62 * breath;
137
- gradient.addColorStop(0, 'rgba(' + rgb + ', 0)');
138
- gradient.addColorStop(0.42, 'rgba(' + rgb + ', ' + (alpha * 0.45).toFixed(3) + ')');
139
- gradient.addColorStop(0.5, 'rgba(' + rgb + ', ' + alpha.toFixed(3) + ')');
140
- gradient.addColorStop(0.58, 'rgba(' + rgb + ', ' + (alpha * 0.45).toFixed(3) + ')');
141
- gradient.addColorStop(1, 'rgba(' + rgb + ', 0)');
142
- ctx.fillStyle = gradient;
143
- ctx.fillRect(x - spread, 0, spread * 2, h * 0.92);
144
- });
145
- if (!reduce && !document.hidden) curtainFrame = requestAnimationFrame(draw);
146
- }
147
- var curtainFrame = 0;
148
- resize();
149
- window.addEventListener('resize', function () { resize(); if (reduce) draw(0); });
150
- document.addEventListener('visibilitychange', function () {
151
- cancelAnimationFrame(curtainFrame);
152
- if (!document.hidden) curtainFrame = requestAnimationFrame(draw);
153
- });
154
- curtainFrame = requestAnimationFrame(function (now) {
155
- if (!canvas.width) resize();
156
- draw(now);
157
- });
102
+
103
+
104
+ function icon(name) {
105
+ var paths = {
106
+ brand: '<path d="m8 5-5 7 5 7m8-14 5 7-5 7m-8-7 3 3 5-6"/>',
107
+ overview: '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>',
108
+ tests: '<path d="m9 11 2 2 4-4"/><rect x="5" y="4" width="14" height="17" rx="2"/><path d="M9 4V2h6v2"/>',
109
+ screenshots: '<rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8" cy="8" r="1.5"/><path d="m3 17 6-6 4 4 3-3 5 5"/>',
110
+ logs: '<path d="m5 6 4 4-4 4m7 3h7"/><rect x="2" y="2" width="20" height="20" rx="3"/>',
111
+ files: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zm0 0v6h6M8 13h8m-8 4h5"/>',
112
+ folder: '<path d="M3 7V5a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/>',
113
+ search: '<circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/>',
114
+ clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
115
+ check: '<circle cx="12" cy="12" r="9"/><path d="m8 12 3 3 5-6"/>',
116
+ alert: '<path d="m12 3 10 18H2Z M12 9v4m0 3v1"/>',
117
+ moon: '<path d="M21 13a9 9 0 0 1-10-10 9 9 0 1 0 10 10Z"/>',
118
+ arrow: '<path d="M5 12h14m-5-5 5 5-5 5"/>',
119
+ };
120
+ return '<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + (paths[name] || paths.tests) + '</svg>';
121
+ }
122
+
123
+ function syncTabOrientation() {
124
+ document.querySelector('.tabbar').setAttribute('aria-orientation', window.innerWidth <= 800 ? 'horizontal' : 'vertical');
158
125
  }
159
126
 
160
- function watchHeader() {
161
- var rail = document.querySelector('.rail');
162
- if (!rail) return;
163
- rail.addEventListener('focusin', function () { rail.classList.remove('is-hidden'); });
164
- var lastY = window.scrollY;
165
- window.addEventListener('scroll', function () {
166
- var y = window.scrollY;
167
- var down = y > lastY + 6;
168
- var up = y < lastY - 6;
169
- if (y < 24) rail.classList.remove('is-hidden');
170
- else if (down && !rail.contains(document.activeElement)) rail.classList.add('is-hidden');
171
- else if (up) rail.classList.remove('is-hidden');
172
- lastY = y;
173
- }, { passive: true });
127
+ function setRunState(node) {
128
+ node.dataset.status = data.status;
129
+ node.textContent = { passed: 'Run passed', failed: 'Run failed', timedout: 'Run timed out', interrupted: 'Run interrupted' }[data.status] || 'Run finished';
174
130
  }
175
131
 
176
132
  function companyName() {
@@ -183,18 +139,22 @@
183
139
  return allowed[data.accent] ? data.accent : 'green';
184
140
  }
185
141
 
186
- function accentParts() {
187
- var probe = document.createElement('span');
188
- probe.style.color = 'var(--accent)';
189
- document.body.appendChild(probe);
190
- var color = getComputedStyle(probe).color;
191
- probe.remove();
192
- var parts = color.match(/\d+/g) || ['0', '255', '136'];
193
- return parts.slice(0, 3).join(', ');
142
+ function readTheme() {
143
+ try {
144
+ var saved = localStorage.getItem('reborn-theme');
145
+ if (saved === 'light' || saved === 'dark') return saved;
146
+ } catch (error) { /* Storage may be unavailable for local reports. */ }
147
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
148
+ }
149
+
150
+ function applyTheme(theme) {
151
+ document.body.dataset.theme = theme;
152
+ document.getElementById('theme-toggle').setAttribute('aria-checked', theme === 'dark' ? 'true' : 'false');
194
153
  }
195
154
 
196
155
  function tabButton(id, label) {
197
- return '<button id="tab-' + id + '" class="tab" role="tab" type="button" data-tab="' + id + '" aria-selected="false" aria-controls="panel-' + id + '">' + label + '</button>';
156
+ var count = id === 'tests' ? data.tests.length : null;
157
+ return '<button id="tab-' + id + '" class="tab" role="tab" type="button" data-tab="' + id + '" aria-selected="false" aria-controls="panel-' + id + '">' + icon(id) + '<span>' + label + '</span>' + (count === null ? '' : '<span class="nav-count" aria-hidden="true">' + count + '</span>') + '</button>';
198
158
  }
199
159
 
200
160
  function readRoute() {
@@ -216,6 +176,7 @@
216
176
  function showTab(tab, quiet) {
217
177
  if (TABS.indexOf(tab) === -1) tab = 'overview';
218
178
  state.tab = tab;
179
+ document.getElementById('page-name').textContent = tab === 'overview' ? 'Dashboard' : tab.charAt(0).toUpperCase() + tab.slice(1);
219
180
  document.querySelectorAll('[role="tab"]').forEach(function (button) {
220
181
  button.setAttribute('aria-selected', button.dataset.tab === tab ? 'true' : 'false');
221
182
  button.tabIndex = button.dataset.tab === tab ? 0 : -1;
@@ -254,38 +215,33 @@
254
215
  var host = document.getElementById('panel-overview');
255
216
  host.innerHTML = [
256
217
  '<header class="hero">',
257
- '<div><h1 class="word" id="word"></h1><p class="product" id="product"></p><p class="summary" id="summary"></p></div>',
218
+ '<div><p class="dashboard-kicker">Run overview</p><h1 class="word">Dashboard</h1><p class="product" id="product"></p><p class="summary" id="summary"></p></div>',
258
219
  '<p class="run-state" id="run-state"></p>',
259
220
  '</header>',
221
+ '<p class="meta" id="meta"></p>',
260
222
  '<div id="errors"></div>',
261
- '<dl class="info" id="info"></dl>',
262
- '<section class="console">',
263
- '<div class="console-body">',
264
- '<p class="section-label">Results</p>',
265
- '<div class="legend" id="legend"></div>',
266
- '<div class="range-head">',
267
- '<p class="section-label" id="range-label">Duration</p>',
268
- '<div class="views" role="group" aria-label="Overview shape">',
269
- '<button type="button" class="view" data-overview="chart">Chart</button>',
270
- '<button type="button" class="view" data-overview="timeline">Timeline</button>',
271
- '</div>',
272
- '</div>',
273
- '<p class="section-note" id="range-note"></p>',
274
- '<div class="charts" id="duration"></div>',
223
+ '<div class="metric-grid" id="metrics"></div>',
224
+ '<div class="dashboard-grid">',
225
+ '<section class="dashboard-card result-card"><div class="card-heading"><h2>Test results</h2><span class="card-detail">This run</span></div>',
226
+ '<div class="result-body"><div class="result-ring" id="result-ring"><div><strong id="pass-percent"></strong><span>Passed</span></div></div>',
227
+ '<div class="legend" id="legend"></div></div><p class="card-note" id="result-note"></p></section>',
228
+ '<section class="dashboard-card console"><div class="console-body">',
229
+ '<div class="range-head"><h2 class="section-label" id="range-label">Duration</h2>',
230
+ '<div class="views" role="group" aria-label="Overview shape"><button type="button" class="view" data-overview="chart">Chart</button><button type="button" class="view" data-overview="timeline">Timeline</button></div></div>',
231
+ '<p class="section-note" id="range-note"></p><div class="charts" id="duration"></div>',
275
232
  '<div class="strip-window" id="strip-window"><div class="strip-scroll"><div class="strip" id="strip" role="group" aria-label="Tests by time"></div></div></div>',
233
+ '</div></section>',
234
+ '<section class="dashboard-card attention-card"><div class="card-heading"><h2>Needs attention</h2><button type="button" class="dashboard-link" data-go-tests>View all tests ' + icon('arrow') + '</button></div><div id="attention-list"></div></section>',
235
+ '<section class="dashboard-card environment-card"><div class="card-heading"><h2>Run details</h2>' + icon('folder') + '</div><dl class="run-details" id="run-details"></dl><dl class="info" id="info"></dl></section>',
276
236
  '</div>',
277
- '</section>',
278
237
  ].join('');
279
- var word = document.getElementById('word');
280
- word.textContent = companyName();
281
- var product = document.getElementById('product');
282
238
  var subtitle = typeof data.productSubtitle === 'string' ? data.productSubtitle.trim() : 'Test Automation';
283
- if (subtitle) product.textContent = subtitle;
284
- else product.hidden = true;
239
+ document.getElementById('product').textContent = subtitle;
240
+ document.getElementById('product').hidden = !subtitle;
285
241
  document.getElementById('summary').textContent = data.summary || '';
286
- var runState = document.getElementById('run-state');
287
- runState.dataset.status = data.status;
288
- runState.textContent = { passed: 'Run passed', failed: 'Run failed', timedout: 'Run timed out', interrupted: 'Run interrupted' }[data.status] || 'Run finished';
242
+ fill(document.getElementById('meta'), metaBits().map(function (bit) { return el('span', null, [bit]); }));
243
+ setRunState(document.getElementById('run-state'));
244
+ paintDashboard();
289
245
  paintErrors();
290
246
  paintInfo();
291
247
  paintCounts(document.getElementById('legend'), false);
@@ -294,6 +250,60 @@
294
250
  applyOverview();
295
251
  }
296
252
 
253
+ function paintDashboard() {
254
+ var counts = data.counts || {};
255
+ var total = data.tests.length;
256
+ var passed = counts.passed || 0;
257
+ var percent = total ? Math.round(passed / total * 100) : 0;
258
+ var problems = data.tests.filter(isProblem);
259
+ var metrics = [
260
+ ['tests', 'Total tests', String(total), 'Across ' + uniqueValues(function (test) { return [test.file]; }).length + ' test files', ''],
261
+ ['check', 'Pass rate', total ? percent + '%' : '—', passed + ' of ' + total + ' tests passed', 'positive'],
262
+ ['clock', 'Run duration', formatDuration(data.duration || 0), 'Wall time including retries', ''],
263
+ ['alert', 'Needs attention', String(problems.length), 'Failed, flaky or interrupted', problems.length ? 'warning' : 'positive'],
264
+ ];
265
+ fill(document.getElementById('metrics'), metrics.map(function (metric) {
266
+ var node = el('div', { class: 'metric ' + metric[4] }, [
267
+ el('div', { class: 'metric-label' }, [metric[1]]),
268
+ el('strong', { class: 'metric-value' }, [metric[2]]),
269
+ el('span', { class: 'metric-note' }, [metric[3]]),
270
+ ]);
271
+ node.querySelector('.metric-label').insertAdjacentHTML('beforeend', icon(metric[0]));
272
+ return node;
273
+ }));
274
+ document.getElementById('pass-percent').textContent = total ? percent + '%' : '—';
275
+ var colors = { passed: 'var(--pass)', failed: 'var(--fail)', timedOut: 'var(--fail)', flaky: 'var(--flaky)', skipped: 'var(--skip)', interrupted: 'var(--timeout)' };
276
+ var offset = 0;
277
+ var stops = [];
278
+ Object.keys(colors).forEach(function (status) {
279
+ var amount = counts[status] || 0;
280
+ if (!amount || !total) return;
281
+ var next = offset + amount / total * 100;
282
+ stops.push(colors[status] + ' ' + offset + '% ' + next + '%');
283
+ offset = next;
284
+ });
285
+ document.getElementById('result-ring').style.background = stops.length ? 'conic-gradient(' + stops.join(',') + ')' : 'var(--hairline)';
286
+ document.getElementById('result-note').textContent = 'Select a result to explore its tests. Pass rate includes skipped tests in the total.';
287
+ var attention = problems.slice(0, 5).map(function (test) {
288
+ return el('button', { type: 'button', class: 'attention-row open-test', 'data-id': test.id, 'data-attempt': String(failedAttempt(test)) }, [
289
+ el('span', { class: 'attention-status sw', 'data-status': test.status }, [statusLabel(test)]),
290
+ el('span', { class: 'attention-test' }, [el('strong', null, [test.title]), el('span', null, [test.file])]),
291
+ el('span', { class: 'attention-time' }, [formatDuration(test.duration)]),
292
+ ]);
293
+ });
294
+ if (!attention.length) attention.push(el('p', { class: 'empty' }, [total ? 'No tests need attention. Your run is looking good.' : 'No tests ran. Run your test suite to see results here.']));
295
+ if (problems.length > 5) attention.push(el('p', { class: 'card-note' }, ['Showing 5 of ' + problems.length + ' tests. View all tests to explore the rest.']));
296
+ fill(document.getElementById('attention-list'), attention);
297
+ var rows = [['Started', formatWhen(data.startTime)], ['Workers', String(data.workers || 0)], ['Screenshots', { off: 'Off', failure: 'On failure', last: 'Last step', steps: 'Every step' }[data.screenshots] || 'Off']];
298
+ if (data.showProjects === true && (data.projects || []).length) rows.push(['Projects', data.projects.join(', ')]);
299
+ if (data.shard) rows.push(['Shard', data.shard]);
300
+ var customLabels = (data.info || []).map(function (row) { return String(row.label).toLowerCase(); });
301
+ rows = rows.filter(function (row) { return customLabels.indexOf(row[0].toLowerCase()) === -1; });
302
+ fill(document.getElementById('run-details'), rows.map(function (row) {
303
+ return el('div', null, [el('dt', null, [row[0]]), el('dd', null, [row[1]])]);
304
+ }));
305
+ }
306
+
297
307
  function paintInfo() {
298
308
  var host = document.getElementById('info');
299
309
  if (!host) return;
@@ -367,7 +377,7 @@
367
377
  empty.setAttribute('cx', '110');
368
378
  empty.setAttribute('cy', '110');
369
379
  empty.setAttribute('r', '96');
370
- empty.setAttribute('fill', 'rgba(255,255,255,0.08)');
380
+ empty.setAttribute('fill', 'var(--hairline)');
371
381
  svg.appendChild(empty);
372
382
  return svg;
373
383
  }
@@ -496,22 +506,9 @@
496
506
  var buttons = filters.map(function (filter) {
497
507
  return el('button', { class: 'count', type: 'button', 'data-status': filter[0], 'aria-pressed': 'false' }, [
498
508
  el('span', { class: 'n' }, [String(filter[2])]),
499
- filter[1],
509
+ ' ' + filter[1],
500
510
  ]);
501
511
  });
502
- if (host.id === 'legend') {
503
- buttons.unshift(el('div', { class: 'count count-static' }, [
504
- el('span', { class: 'n' }, [formatDuration(data.duration || 0)]),
505
- 'total',
506
- ]));
507
- buttons.forEach(function (button) {
508
- if (button.classList.contains('count-static')) return;
509
- var node = button.querySelector('.n');
510
- var value = Number(node.textContent);
511
- node.textContent = '0';
512
- countUp(node, value, function (next) { return String(Math.round(next)); });
513
- });
514
- }
515
512
  var children = buttons;
516
513
  if (withFind) {
517
514
  children = buttons.concat([el('div', { class: 'find-wrap' }, [
@@ -611,33 +608,25 @@
611
608
  return Object.keys(seen).sort();
612
609
  }
613
610
 
611
+ function selectControl(id, label, choices, current) {
612
+ var select = el('select', { id: id, class: 'filter-select' }, choices.map(function (choice) {
613
+ return el('option', { value: choice[0] }, [choice[1]]);
614
+ }));
615
+ select.value = current || '';
616
+ return el('label', { class: 'filter-control', for: id }, [el('span', null, [label]), select]);
617
+ }
618
+
614
619
  function paintTags() {
615
620
  var host = document.getElementById('tags');
616
621
  if (!host) return;
617
622
  var tags = uniqueValues(function (test) { return (test.tags || []).filter(keptTag); });
618
623
  var projects = uniqueValues(function (test) { return test.project ? [test.project] : []; });
619
- var nodes = [];
620
- if (tags.length) {
621
- nodes.push(el('span', { class: 'order-label' }, ['Tags']));
622
- nodes.push(chip('tag-btn', '', 'All', state.tag || ''));
623
- tags.forEach(function (tag) { nodes.push(chip('tag-btn', tag, tag, state.tag)); });
624
- }
624
+ var nodes = [selectControl('tag-select', 'Tags', [['', 'All tags']].concat(tags.map(function (tag) { return [tag, tag]; })), state.tag)];
625
625
  if (data.showProjectFilter === true && projects.length > 1) {
626
- nodes.push(el('span', { class: 'order-label' }, ['Project']));
627
- nodes.push(chip('project-btn', '', 'All', state.projectFilter || ''));
628
- projects.forEach(function (project) { nodes.push(chip('project-btn', project, project, state.projectFilter)); });
626
+ nodes.push(selectControl('project-select', 'Project', [['', 'All projects']].concat(projects.map(function (project) { return [project, project]; })), state.projectFilter));
629
627
  }
630
- host.hidden = nodes.length === 0;
631
628
  fill(host, nodes);
632
- }
633
-
634
- function chip(className, value, label, current) {
635
- return el('button', {
636
- class: className,
637
- type: 'button',
638
- 'data-value': value,
639
- 'aria-pressed': current === value ? 'true' : 'false',
640
- }, [label]);
629
+ if (!tags.length) document.getElementById('tag-select').disabled = true;
641
630
  }
642
631
 
643
632
  function sortTests(tests) {
@@ -657,17 +646,10 @@
657
646
  function paintOrder() {
658
647
  var host = document.getElementById('order');
659
648
  if (!host) return;
660
- var orders = [['grouped', 'Grouped'], ['slowest', 'Slowest'], ['fastest', 'Fastest'], ['started', 'Started']];
661
- var nodes = [el('span', { class: 'order-label' }, ['Order'])].concat(orders.map(function (order) {
662
- return el('button', {
663
- class: 'order-btn',
664
- type: 'button',
665
- 'data-order': order[0],
666
- 'aria-pressed': state.order === order[0] ? 'true' : 'false',
667
- }, [order[1]]);
668
- }));
649
+ var orders = [['grouped', 'Grouped'], ['slowest', 'Slowest first'], ['fastest', 'Fastest first'], ['started', 'Start time']];
650
+ var nodes = [selectControl('sort-select', 'Sort', orders, state.order)];
669
651
  if (state.band !== 'all') {
670
- nodes.push(el('button', { class: 'band-clear', type: 'button', 'aria-pressed': 'true' }, [bandLabel(state.band)]));
652
+ nodes.push(el('button', { class: 'band-clear', type: 'button', 'aria-label': 'Clear duration filter: ' + bandLabel(state.band) }, [bandLabel(state.band), el('span', { 'aria-hidden': 'true' }, [' ×'])]));
671
653
  }
672
654
  fill(host, nodes);
673
655
  }
@@ -676,7 +658,7 @@
676
658
  if (built.tests) return;
677
659
  built.tests = true;
678
660
  var host = document.getElementById('panel-tests');
679
- host.innerHTML = '<h2 class="visually-hidden">Tests</h2><div class="legend" id="test-tools"></div><div class="tags" id="tags"></div><div class="order" id="order"></div><div class="list-summary" id="list-summary"></div><div class="split"><div id="tests"></div><section class="detail" id="detail"></section></div>';
661
+ host.innerHTML = '<div class="page-heading"><h2>Tests</h2><p>Explore results, retries and the steps behind every test.</p></div><div class="legend" id="test-tools"></div><div class="filter-toolbar"><div class="tags" id="tags"></div><div class="order" id="order"></div></div><div class="list-summary" id="list-summary"></div><div class="split"><div id="tests"></div><section class="detail" id="detail"></section></div>';
680
662
  paintCounts(document.getElementById('test-tools'), true);
681
663
  paintTags();
682
664
  paintOrder();
@@ -1070,6 +1052,17 @@
1070
1052
  document.getElementById('find').focus();
1071
1053
  return;
1072
1054
  }
1055
+ if (target.closest('#theme-toggle')) {
1056
+ var theme = document.body.dataset.theme === 'dark' ? 'light' : 'dark';
1057
+ applyTheme(theme);
1058
+ try { localStorage.setItem('reborn-theme', theme); } catch (error) { /* Theme still works without storage. */ }
1059
+ return;
1060
+ }
1061
+ if (target.closest('[data-go-tests]')) {
1062
+ showTab('tests');
1063
+ document.getElementById('find').focus();
1064
+ return;
1065
+ }
1073
1066
  var tab = target.closest('.tab');
1074
1067
  if (tab) {
1075
1068
  showTab(tab.dataset.tab);
@@ -1118,35 +1111,6 @@
1118
1111
  paintDetail();
1119
1112
  return;
1120
1113
  }
1121
- var tag = target.closest('.tag-btn');
1122
- if (tag) {
1123
- state.tag = tag.dataset.value || null;
1124
- state.listLimit = 200;
1125
- keepSelectionInFilter();
1126
- paintTags();
1127
- paintList();
1128
- paintDetail();
1129
- return;
1130
- }
1131
- var projectButton = target.closest('.project-btn');
1132
- if (projectButton) {
1133
- state.projectFilter = projectButton.dataset.value || null;
1134
- state.listLimit = 200;
1135
- keepSelectionInFilter();
1136
- paintTags();
1137
- paintList();
1138
- paintDetail();
1139
- return;
1140
- }
1141
- var order = target.closest('.order-btn');
1142
- if (order) {
1143
- state.order = order.dataset.order || 'grouped';
1144
- keepSelectionInFilter();
1145
- paintOrder();
1146
- paintList();
1147
- paintDetail();
1148
- return;
1149
- }
1150
1114
  var count = target.closest('.count');
1151
1115
  if (count && !count.classList.contains('shot-filter') && !count.classList.contains('count-static')) {
1152
1116
  state.filter = count.dataset.status || 'all';
@@ -1209,6 +1173,19 @@
1209
1173
  if (shot) openShot(shot.dataset.src, shot.dataset.alt || 'Screenshot');
1210
1174
  });
1211
1175
 
1176
+ app.addEventListener('change', function (event) {
1177
+ var control = event.target;
1178
+ if (!control || ['tag-select', 'project-select', 'sort-select'].indexOf(control.id) === -1) return;
1179
+ if (control.id === 'tag-select') state.tag = control.value || null;
1180
+ if (control.id === 'project-select') state.projectFilter = control.value || null;
1181
+ if (control.id === 'sort-select') state.order = control.value || 'grouped';
1182
+ state.listLimit = 200;
1183
+ keepSelectionInFilter();
1184
+ paintList();
1185
+ paintDetail();
1186
+ markStrip();
1187
+ });
1188
+
1212
1189
  app.addEventListener('input', function (event) {
1213
1190
  if (!event.target) return;
1214
1191
  if (event.target.id === 'find') {
@@ -1231,6 +1208,7 @@
1231
1208
 
1232
1209
  document.addEventListener('keydown', function (event) {
1233
1210
  if (event.defaultPrevented || event.isComposing || event.metaKey || event.ctrlKey || event.altKey || document.querySelector('dialog[open]')) return;
1211
+ if (event.target instanceof HTMLSelectElement) return;
1234
1212
  var typing = event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLSelectElement || event.target.isContentEditable;
1235
1213
  if (typing) {
1236
1214
  if (event.key === 'Escape') {
@@ -1262,12 +1240,12 @@
1262
1240
  if (find) find.focus();
1263
1241
  return;
1264
1242
  }
1265
- if ((event.key === 'ArrowRight' || event.key === 'ArrowLeft' || event.key === 'Home' || event.key === 'End') && event.target.closest && event.target.closest('.tabbar')) {
1243
+ if ((event.key === 'ArrowRight' || event.key === 'ArrowLeft' || event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Home' || event.key === 'End') && event.target.closest && event.target.closest('.tabbar')) {
1266
1244
  event.preventDefault();
1267
1245
  var index = TABS.indexOf(state.tab);
1268
1246
  if (event.key === 'Home') index = 0;
1269
1247
  else if (event.key === 'End') index = TABS.length - 1;
1270
- else if (event.key === 'ArrowRight') index = (index + 1) % TABS.length;
1248
+ else if (event.key === 'ArrowRight' || event.key === 'ArrowDown') index = (index + 1) % TABS.length;
1271
1249
  else index = (index - 1 + TABS.length) % TABS.length;
1272
1250
  showTab(TABS[index]);
1273
1251
  var next = document.querySelector('.tab[data-tab="' + TABS[index] + '"]');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright-reborn",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "A Playwright reporter that writes one self-contained HTML report when the run finishes.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",