issue-flow 0.7.7 → 0.8.0
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/dist/{analyze-2RVGY5OK.js → analyze-RBNKXWL5.js} +6 -6
- package/dist/{chunk-6L2BPOE3.js → chunk-36ECROTC.js} +7 -7
- package/dist/{chunk-EOTULFWV.js → chunk-A4M6AKIK.js} +6 -6
- package/dist/{chunk-RUTDK6ZF.js → chunk-BASRRXAM.js} +2 -2
- package/dist/{chunk-WO6ERABE.js → chunk-BH7DYADV.js} +10 -1
- package/dist/chunk-BH7DYADV.js.map +1 -0
- package/dist/{chunk-JOK7VXRV.js → chunk-F52S6ZGF.js} +3 -3
- package/dist/{chunk-EYVXLRRD.js → chunk-HSGTZ3CT.js} +3 -3
- package/dist/{chunk-XPW46TI5.js → chunk-J3PJBZXJ.js} +3 -3
- package/dist/{chunk-6UTVPKNF.js → chunk-J4SICTAN.js} +7 -7
- package/dist/{chunk-KHTLN54U.js → chunk-JWZKV3CL.js} +2 -2
- package/dist/{chunk-3E6KASSE.js → chunk-MK43N4X3.js} +7 -7
- package/dist/{chunk-ZYBYMEWG.js → chunk-QO2MWMAZ.js} +2 -2
- package/dist/{chunk-OPIDR2DC.js → chunk-RQW5W4XC.js} +2 -2
- package/dist/{chunk-WGDLUPSF.js → chunk-TGP6WK65.js} +5 -5
- package/dist/{chunk-G37JNQKB.js → chunk-VPDWTZ3F.js} +2 -2
- package/dist/{chunk-6IOPYFUM.js → chunk-VSF2CR3G.js} +6 -6
- package/dist/cli.js +15 -15
- package/dist/{config-OAZG7KIK.js → config-R7O5YC65.js} +3 -3
- package/dist/execute-LU3HWA54.js +14 -0
- package/dist/{generate-BEJF3SVJ.js → generate-TGJQLGMV.js} +6 -6
- package/dist/init-WP7Z2LFQ.js +9 -0
- package/dist/plan-2MWMCKRY.js +16 -0
- package/dist/pr-XVQDEQ7B.js +16 -0
- package/dist/pr-review-VO6ZXAFM.js +16 -0
- package/dist/prd-FPZAHQEO.js +16 -0
- package/dist/review-AHALLMXO.js +14 -0
- package/dist/{run-SEB6F3W4.js → run-2CXVR3VZ.js} +16 -16
- package/dist/{web-3SBYL7QJ.js → web-JVPK2PIF.js} +4 -4
- package/package.json +1 -1
- package/web/CLAUDE.md +48 -0
- package/web/public/app.css +266 -0
- package/web/public/app.js +237 -0
- package/web/public/index.html +26 -0
- package/dist/chunk-WO6ERABE.js.map +0 -1
- package/dist/execute-AOEM52CD.js +0 -14
- package/dist/init-TBC3WI74.js +0 -9
- package/dist/plan-YDZYB2BO.js +0 -16
- package/dist/pr-SIXMJIZF.js +0 -16
- package/dist/pr-review-AY56ZPJR.js +0 -16
- package/dist/prd-MBSSBLGO.js +0 -16
- package/dist/review-2MDDHZ4X.js +0 -14
- /package/dist/{analyze-2RVGY5OK.js.map → analyze-RBNKXWL5.js.map} +0 -0
- /package/dist/{chunk-6L2BPOE3.js.map → chunk-36ECROTC.js.map} +0 -0
- /package/dist/{chunk-EOTULFWV.js.map → chunk-A4M6AKIK.js.map} +0 -0
- /package/dist/{chunk-RUTDK6ZF.js.map → chunk-BASRRXAM.js.map} +0 -0
- /package/dist/{chunk-JOK7VXRV.js.map → chunk-F52S6ZGF.js.map} +0 -0
- /package/dist/{chunk-EYVXLRRD.js.map → chunk-HSGTZ3CT.js.map} +0 -0
- /package/dist/{chunk-XPW46TI5.js.map → chunk-J3PJBZXJ.js.map} +0 -0
- /package/dist/{chunk-6UTVPKNF.js.map → chunk-J4SICTAN.js.map} +0 -0
- /package/dist/{chunk-KHTLN54U.js.map → chunk-JWZKV3CL.js.map} +0 -0
- /package/dist/{chunk-3E6KASSE.js.map → chunk-MK43N4X3.js.map} +0 -0
- /package/dist/{chunk-ZYBYMEWG.js.map → chunk-QO2MWMAZ.js.map} +0 -0
- /package/dist/{chunk-OPIDR2DC.js.map → chunk-RQW5W4XC.js.map} +0 -0
- /package/dist/{chunk-WGDLUPSF.js.map → chunk-TGP6WK65.js.map} +0 -0
- /package/dist/{chunk-G37JNQKB.js.map → chunk-VPDWTZ3F.js.map} +0 -0
- /package/dist/{chunk-6IOPYFUM.js.map → chunk-VSF2CR3G.js.map} +0 -0
- /package/dist/{config-OAZG7KIK.js.map → config-R7O5YC65.js.map} +0 -0
- /package/dist/{execute-AOEM52CD.js.map → execute-LU3HWA54.js.map} +0 -0
- /package/dist/{generate-BEJF3SVJ.js.map → generate-TGJQLGMV.js.map} +0 -0
- /package/dist/{init-TBC3WI74.js.map → init-WP7Z2LFQ.js.map} +0 -0
- /package/dist/{plan-YDZYB2BO.js.map → plan-2MWMCKRY.js.map} +0 -0
- /package/dist/{pr-SIXMJIZF.js.map → pr-XVQDEQ7B.js.map} +0 -0
- /package/dist/{pr-review-AY56ZPJR.js.map → pr-review-VO6ZXAFM.js.map} +0 -0
- /package/dist/{prd-MBSSBLGO.js.map → prd-FPZAHQEO.js.map} +0 -0
- /package/dist/{review-2MDDHZ4X.js.map → review-AHALLMXO.js.map} +0 -0
- /package/dist/{run-SEB6F3W4.js.map → run-2CXVR3VZ.js.map} +0 -0
- /package/dist/{web-3SBYL7QJ.js.map → web-JVPK2PIF.js.map} +0 -0
package/web/public/app.css
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
--ok-bg: #dcfce7;
|
|
19
19
|
--idle-bg: #e5e7eb;
|
|
20
20
|
--shadow: 0 1px 2px rgb(0 0 0 / 0.06);
|
|
21
|
+
--overlay: rgb(0 0 0 / 0.45);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
@media (prefers-color-scheme: dark) {
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
--ok-bg: #14301f;
|
|
39
40
|
--idle-bg: #262d38;
|
|
40
41
|
--shadow: none;
|
|
42
|
+
--overlay: rgb(0 0 0 / 0.65);
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -110,6 +112,53 @@ a:hover {
|
|
|
110
112
|
text-decoration: underline;
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
/* ---- Abas ---- */
|
|
116
|
+
|
|
117
|
+
.tabs {
|
|
118
|
+
display: flex;
|
|
119
|
+
gap: 4px;
|
|
120
|
+
border-bottom: 1px solid var(--border);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.tab {
|
|
124
|
+
appearance: none;
|
|
125
|
+
background: none;
|
|
126
|
+
border: none;
|
|
127
|
+
border-bottom: 2px solid transparent;
|
|
128
|
+
margin-bottom: -1px;
|
|
129
|
+
padding: 8px 14px;
|
|
130
|
+
font: inherit;
|
|
131
|
+
font-weight: 600;
|
|
132
|
+
color: var(--muted);
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
border-radius: 6px 6px 0 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.tab:hover {
|
|
138
|
+
color: var(--fg);
|
|
139
|
+
background: var(--card);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.tab.is-active {
|
|
143
|
+
color: var(--accent);
|
|
144
|
+
border-bottom-color: var(--accent);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* O painel repete o grid de main: as seções dentro dele são os cards que antes
|
|
148
|
+
eram filhos diretos de main e dependem desse espaçamento. */
|
|
149
|
+
.tab-panel {
|
|
150
|
+
display: grid;
|
|
151
|
+
gap: 16px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.tab-panel[hidden] {
|
|
155
|
+
display: none;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.tab-panel > * {
|
|
159
|
+
min-width: 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
113
162
|
/* ---- Cabeçalho ---- */
|
|
114
163
|
|
|
115
164
|
.header {
|
|
@@ -534,6 +583,223 @@ progress {
|
|
|
534
583
|
word-break: break-word;
|
|
535
584
|
}
|
|
536
585
|
|
|
586
|
+
/* ---- Kanban ---- */
|
|
587
|
+
|
|
588
|
+
.kanban {
|
|
589
|
+
display: grid;
|
|
590
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
591
|
+
gap: 12px;
|
|
592
|
+
align-items: start;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.kanban-column {
|
|
596
|
+
background: var(--bg);
|
|
597
|
+
border: 1px solid var(--border);
|
|
598
|
+
border-radius: 8px;
|
|
599
|
+
padding: 10px;
|
|
600
|
+
display: grid;
|
|
601
|
+
gap: 8px;
|
|
602
|
+
min-width: 0;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.kanban-column-head {
|
|
606
|
+
display: flex;
|
|
607
|
+
align-items: center;
|
|
608
|
+
justify-content: space-between;
|
|
609
|
+
gap: 8px;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.kanban-column-title {
|
|
613
|
+
margin: 0;
|
|
614
|
+
font-size: 0.72rem;
|
|
615
|
+
font-weight: 600;
|
|
616
|
+
text-transform: uppercase;
|
|
617
|
+
letter-spacing: 0.06em;
|
|
618
|
+
color: var(--muted);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.kanban-column-count {
|
|
622
|
+
flex: none;
|
|
623
|
+
background: var(--idle-bg);
|
|
624
|
+
color: var(--muted);
|
|
625
|
+
border-radius: 999px;
|
|
626
|
+
padding: 0 7px;
|
|
627
|
+
font-size: 0.74rem;
|
|
628
|
+
font-variant-numeric: tabular-nums;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.kanban-card {
|
|
632
|
+
appearance: none;
|
|
633
|
+
display: grid;
|
|
634
|
+
gap: 6px;
|
|
635
|
+
justify-items: start;
|
|
636
|
+
width: 100%;
|
|
637
|
+
text-align: left;
|
|
638
|
+
background: var(--card);
|
|
639
|
+
border: 1px solid var(--border);
|
|
640
|
+
border-radius: 8px;
|
|
641
|
+
padding: 10px;
|
|
642
|
+
font: inherit;
|
|
643
|
+
color: inherit;
|
|
644
|
+
cursor: pointer;
|
|
645
|
+
box-shadow: var(--shadow);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.kanban-card:hover {
|
|
649
|
+
border-color: var(--accent);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.kanban-card-head {
|
|
653
|
+
display: flex;
|
|
654
|
+
align-items: center;
|
|
655
|
+
gap: 2px;
|
|
656
|
+
color: var(--muted);
|
|
657
|
+
font-size: 0.8rem;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.kanban-card-title {
|
|
661
|
+
display: block;
|
|
662
|
+
font-weight: 600;
|
|
663
|
+
line-height: 1.35;
|
|
664
|
+
overflow-wrap: anywhere;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/* Trecho curto: limita a 3 linhas sem truncar a string, então o texto completo
|
|
668
|
+
continua no DOM para leitores de tela e para o drawer. */
|
|
669
|
+
.kanban-card-desc {
|
|
670
|
+
margin: 0;
|
|
671
|
+
color: var(--muted);
|
|
672
|
+
font-size: 0.84rem;
|
|
673
|
+
line-height: 1.4;
|
|
674
|
+
overflow: hidden;
|
|
675
|
+
display: -webkit-box;
|
|
676
|
+
-webkit-box-orient: vertical;
|
|
677
|
+
-webkit-line-clamp: 3;
|
|
678
|
+
line-clamp: 3;
|
|
679
|
+
overflow-wrap: anywhere;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.kanban-empty {
|
|
683
|
+
font-size: 0.84rem;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
@media (max-width: 719px) {
|
|
687
|
+
.kanban {
|
|
688
|
+
grid-template-columns: minmax(0, 1fr);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/* ---- Drawer de detalhes ---- */
|
|
693
|
+
|
|
694
|
+
/* Acima do .banner sticky (z-index 10), que senão atravessa o overlay. */
|
|
695
|
+
.drawer-overlay {
|
|
696
|
+
position: fixed;
|
|
697
|
+
inset: 0;
|
|
698
|
+
z-index: 20;
|
|
699
|
+
background: var(--overlay);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.drawer {
|
|
703
|
+
position: fixed;
|
|
704
|
+
top: 0;
|
|
705
|
+
right: 0;
|
|
706
|
+
bottom: 0;
|
|
707
|
+
width: min(420px, 100vw);
|
|
708
|
+
z-index: 21;
|
|
709
|
+
display: flex;
|
|
710
|
+
flex-direction: column;
|
|
711
|
+
background: var(--card);
|
|
712
|
+
border-left: 1px solid var(--border);
|
|
713
|
+
padding: 16px;
|
|
714
|
+
gap: 12px;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.drawer[hidden],
|
|
718
|
+
.drawer-overlay[hidden] {
|
|
719
|
+
display: none;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.drawer-head {
|
|
723
|
+
display: flex;
|
|
724
|
+
align-items: flex-start;
|
|
725
|
+
justify-content: space-between;
|
|
726
|
+
gap: 12px;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.drawer-head h2 {
|
|
730
|
+
margin: 0;
|
|
731
|
+
/* O título carrega o id + o título da story: fora do padrão caixa-alta das
|
|
732
|
+
demais seções, que são rótulos fixos e curtos. */
|
|
733
|
+
font-size: 0.95rem;
|
|
734
|
+
text-transform: none;
|
|
735
|
+
letter-spacing: normal;
|
|
736
|
+
color: var(--fg);
|
|
737
|
+
overflow-wrap: anywhere;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.drawer-close {
|
|
741
|
+
appearance: none;
|
|
742
|
+
flex: none;
|
|
743
|
+
background: none;
|
|
744
|
+
border: none;
|
|
745
|
+
padding: 2px 6px;
|
|
746
|
+
font: inherit;
|
|
747
|
+
color: var(--muted);
|
|
748
|
+
cursor: pointer;
|
|
749
|
+
border-radius: 6px;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.drawer-close:hover {
|
|
753
|
+
color: var(--fg);
|
|
754
|
+
background: var(--idle-bg);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/* A rolagem vive aqui, não na página: o drawer é fixo e ocupa a altura toda. */
|
|
758
|
+
.drawer-body {
|
|
759
|
+
overflow-y: auto;
|
|
760
|
+
display: grid;
|
|
761
|
+
gap: 14px;
|
|
762
|
+
align-content: start;
|
|
763
|
+
justify-items: start;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.drawer-timing {
|
|
767
|
+
margin: 0;
|
|
768
|
+
font-size: 0.84rem;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.drawer-section {
|
|
772
|
+
width: 100%;
|
|
773
|
+
display: grid;
|
|
774
|
+
gap: 6px;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.drawer-section-title {
|
|
778
|
+
margin: 0;
|
|
779
|
+
font-size: 0.72rem;
|
|
780
|
+
font-weight: 600;
|
|
781
|
+
text-transform: uppercase;
|
|
782
|
+
letter-spacing: 0.06em;
|
|
783
|
+
color: var(--muted);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.drawer-text {
|
|
787
|
+
margin: 0;
|
|
788
|
+
white-space: pre-wrap;
|
|
789
|
+
overflow-wrap: anywhere;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.drawer-list {
|
|
793
|
+
margin: 0;
|
|
794
|
+
padding-left: 18px;
|
|
795
|
+
display: grid;
|
|
796
|
+
gap: 4px;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.drawer-list li {
|
|
800
|
+
overflow-wrap: anywhere;
|
|
801
|
+
}
|
|
802
|
+
|
|
537
803
|
/* ---- Diversos ---- */
|
|
538
804
|
|
|
539
805
|
.empty {
|
package/web/public/app.js
CHANGED
|
@@ -32,6 +32,15 @@
|
|
|
32
32
|
done: 'concluída',
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
// Colunas do Kanban, na ordem em que a execução avança. Os títulos são os das
|
|
36
|
+
// colunas, não os rótulos dos badges (STORY_STATUS_LABELS), que seguem em minúsculas.
|
|
37
|
+
const KANBAN_COLUMNS = [
|
|
38
|
+
{ status: 'backlog', title: 'Backlog' },
|
|
39
|
+
{ status: 'in_progress', title: 'Em andamento' },
|
|
40
|
+
{ status: 'in_review', title: 'Em revisão' },
|
|
41
|
+
{ status: 'done', title: 'Concluído' },
|
|
42
|
+
];
|
|
43
|
+
|
|
35
44
|
const els = {
|
|
36
45
|
banner: document.getElementById('banner-disconnected'),
|
|
37
46
|
issueLink: document.getElementById('issue-link'),
|
|
@@ -56,6 +65,13 @@
|
|
|
56
65
|
logFilter: document.getElementById('log-filter'),
|
|
57
66
|
logs: document.getElementById('logs'),
|
|
58
67
|
sessionMeta: document.getElementById('session-meta'),
|
|
68
|
+
tabs: Array.prototype.slice.call(document.querySelectorAll('[role="tab"]')),
|
|
69
|
+
kanban: document.getElementById('kanban'),
|
|
70
|
+
drawer: document.getElementById('drawer'),
|
|
71
|
+
drawerOverlay: document.getElementById('drawer-overlay'),
|
|
72
|
+
drawerClose: document.getElementById('drawer-close'),
|
|
73
|
+
drawerTitle: document.getElementById('drawer-title'),
|
|
74
|
+
drawerBody: document.getElementById('drawer-body'),
|
|
59
75
|
};
|
|
60
76
|
|
|
61
77
|
const state = {
|
|
@@ -66,6 +82,10 @@
|
|
|
66
82
|
timer: null,
|
|
67
83
|
polling: false,
|
|
68
84
|
logFilter: 'all',
|
|
85
|
+
activeTab: 'tab-execution',
|
|
86
|
+
// Só o id: o card que abriu o drawer é destruído no próximo render, então
|
|
87
|
+
// guardar o nó levaria a uma referência morta.
|
|
88
|
+
selectedStoryId: null,
|
|
69
89
|
};
|
|
70
90
|
|
|
71
91
|
// ---- Utilitários ----------------------------------------------------------
|
|
@@ -189,6 +209,61 @@
|
|
|
189
209
|
return match ? match[1] : null;
|
|
190
210
|
}
|
|
191
211
|
|
|
212
|
+
// ---- Camada de leitura das stories ----------------------------------------
|
|
213
|
+
// Único ponto de acesso a uma story individual: o Kanban e o drawer nunca
|
|
214
|
+
// varrem snapshot.stories por conta própria. Quando a escrita existir, é aqui
|
|
215
|
+
// que a leitura passa a conversar com ela, sem tocar na UI.
|
|
216
|
+
|
|
217
|
+
function text(value) {
|
|
218
|
+
return typeof value === 'string' ? value : '';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function list(value) {
|
|
222
|
+
return Array.isArray(value) ? value : [];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Normaliza os campos que um session.json anterior pode não ter gravado, para
|
|
226
|
+
// que nenhum consumidor precise repetir a checagem de ausência.
|
|
227
|
+
function normalizeStory(story) {
|
|
228
|
+
const status = story.status !== null && story.status !== undefined ? story.status : 'backlog';
|
|
229
|
+
return {
|
|
230
|
+
...story,
|
|
231
|
+
status: STORY_STATUS_LABELS[status] !== undefined ? status : 'backlog',
|
|
232
|
+
dependencies: list(story.dependencies),
|
|
233
|
+
description: text(story.description),
|
|
234
|
+
acceptanceCriteria: list(story.acceptanceCriteria),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function getStoryById(snapshot, id) {
|
|
239
|
+
if (!snapshot) return null;
|
|
240
|
+
const stories = list(snapshot.stories);
|
|
241
|
+
for (const story of stories) {
|
|
242
|
+
if (story.id === id) return normalizeStory(story);
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function getStories(snapshot) {
|
|
248
|
+
if (!snapshot) return [];
|
|
249
|
+
return list(snapshot.stories).map(normalizeStory);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ---- Abas -----------------------------------------------------------------
|
|
253
|
+
// Troca apenas visibilidade e estado ARIA. Não toca no polling: render()
|
|
254
|
+
// segue atualizando os dois painéis, então a aba inativa nunca fica defasada.
|
|
255
|
+
|
|
256
|
+
function setActiveTab(tabId) {
|
|
257
|
+
state.activeTab = tabId;
|
|
258
|
+
for (const tab of els.tabs) {
|
|
259
|
+
const active = tab.id === tabId;
|
|
260
|
+
tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
261
|
+
tab.classList.toggle('is-active', active);
|
|
262
|
+
const panel = document.getElementById(tab.getAttribute('aria-controls'));
|
|
263
|
+
if (panel) panel.hidden = !active;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
192
267
|
// ---- Polling: intervalo configurável, aba oculta e backoff ----------------
|
|
193
268
|
|
|
194
269
|
function readStoredRefresh() {
|
|
@@ -282,6 +357,9 @@
|
|
|
282
357
|
renderPhases(snapshot);
|
|
283
358
|
renderNextSteps(snapshot);
|
|
284
359
|
renderStories(snapshot);
|
|
360
|
+
// Incondicional: a aba inativa não pode ficar defasada até ser aberta.
|
|
361
|
+
renderKanban(snapshot);
|
|
362
|
+
renderDrawer(snapshot);
|
|
285
363
|
renderGit(snapshot);
|
|
286
364
|
renderLogs(snapshot);
|
|
287
365
|
renderMeta(snapshot);
|
|
@@ -544,6 +622,157 @@
|
|
|
544
622
|
}
|
|
545
623
|
}
|
|
546
624
|
|
|
625
|
+
// As stories chegam já normalizadas por getStories(), então aqui nenhum campo
|
|
626
|
+
// precisa de checagem de ausência.
|
|
627
|
+
// <button> em vez de <div role="button">: acionamento por Enter/Espaço e foco
|
|
628
|
+
// saem de graça. Por isso todo o conteúdo é <span> — <p>/<div> não são
|
|
629
|
+
// conteúdo válido dentro de um botão.
|
|
630
|
+
function storyCard(story) {
|
|
631
|
+
const card = el('button', 'kanban-card');
|
|
632
|
+
card.type = 'button';
|
|
633
|
+
card.dataset.storyId = story.id;
|
|
634
|
+
|
|
635
|
+
const head = el('span', 'kanban-card-head');
|
|
636
|
+
head.appendChild(
|
|
637
|
+
el('span', 'item-icon icon-' + (story.passes ? 'completed' : 'pending'), story.passes ? '✓' : '○'),
|
|
638
|
+
);
|
|
639
|
+
head.appendChild(el('span', 'story-id', story.id));
|
|
640
|
+
card.appendChild(head);
|
|
641
|
+
|
|
642
|
+
card.appendChild(el('span', 'kanban-card-title', story.title));
|
|
643
|
+
if (story.description) card.appendChild(el('span', 'kanban-card-desc', story.description));
|
|
644
|
+
card.appendChild(
|
|
645
|
+
el('span', 'badge story-status-' + story.status, STORY_STATUS_LABELS[story.status]),
|
|
646
|
+
);
|
|
647
|
+
card.addEventListener('click', () => openDrawer(story.id));
|
|
648
|
+
return card;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function renderKanban(snapshot) {
|
|
652
|
+
clear(els.kanban);
|
|
653
|
+
const stories = getStories(snapshot);
|
|
654
|
+
for (const column of KANBAN_COLUMNS) {
|
|
655
|
+
const entries = stories.filter((story) => story.status === column.status);
|
|
656
|
+
|
|
657
|
+
const node = el('section', 'kanban-column');
|
|
658
|
+
const head = el('div', 'kanban-column-head');
|
|
659
|
+
head.appendChild(el('h3', 'kanban-column-title', column.title));
|
|
660
|
+
head.appendChild(el('span', 'kanban-column-count', String(entries.length)));
|
|
661
|
+
node.appendChild(head);
|
|
662
|
+
|
|
663
|
+
if (entries.length === 0) {
|
|
664
|
+
node.appendChild(el('p', 'empty kanban-empty', 'Nenhuma story.'));
|
|
665
|
+
} else {
|
|
666
|
+
for (const story of entries) node.appendChild(storyCard(story));
|
|
667
|
+
}
|
|
668
|
+
els.kanban.appendChild(node);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ---- Drawer de detalhes da story ------------------------------------------
|
|
673
|
+
|
|
674
|
+
function onDrawerKeydown(event) {
|
|
675
|
+
if (event.key === 'Escape') closeDrawer();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Reidrata a partir do id a cada render: o drawer aberto sobrevive ao poll.
|
|
679
|
+
function renderDrawer(snapshot) {
|
|
680
|
+
if (state.selectedStoryId === null) return;
|
|
681
|
+
const story = getStoryById(snapshot, state.selectedStoryId);
|
|
682
|
+
// A story saiu do plano: manter o drawer aberto exibiria dados obsoletos.
|
|
683
|
+
if (story === null) {
|
|
684
|
+
closeDrawer();
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
els.drawerTitle.textContent = story.id + ' · ' + story.title;
|
|
688
|
+
|
|
689
|
+
clear(els.drawerBody);
|
|
690
|
+
els.drawerBody.appendChild(
|
|
691
|
+
el('span', 'badge story-status-' + story.status, STORY_STATUS_LABELS[story.status]),
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
const timing = itemSideText([
|
|
695
|
+
story.completedAt ? 'concluída ' + formatClock(story.completedAt) : '',
|
|
696
|
+
metric(story.durationSeconds) !== null ? formatDuration(story.durationSeconds) : '',
|
|
697
|
+
]);
|
|
698
|
+
if (timing) els.drawerBody.appendChild(el('p', 'muted drawer-timing', timing));
|
|
699
|
+
|
|
700
|
+
drawerSection('Descrição', (body) => {
|
|
701
|
+
body.appendChild(
|
|
702
|
+
story.description
|
|
703
|
+
? el('p', 'drawer-text', story.description)
|
|
704
|
+
: el('p', 'empty', 'Sem descrição.'),
|
|
705
|
+
);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
drawerSection('Critérios de aceite', (body) => {
|
|
709
|
+
if (story.acceptanceCriteria.length === 0) {
|
|
710
|
+
body.appendChild(el('p', 'empty', 'Nenhum critério declarado.'));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const items = el('ul', 'drawer-list');
|
|
714
|
+
for (const criterion of story.acceptanceCriteria) items.appendChild(el('li', null, criterion));
|
|
715
|
+
body.appendChild(items);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
drawerSection('Dependências', (body) => {
|
|
719
|
+
if (story.dependencies.length === 0) {
|
|
720
|
+
body.appendChild(el('p', 'empty', 'Nenhuma dependência.'));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const row = el('div', 'badge-row');
|
|
724
|
+
for (const dependency of story.dependencies) {
|
|
725
|
+
row.appendChild(el('span', 'badge label-badge', dependency));
|
|
726
|
+
}
|
|
727
|
+
body.appendChild(row);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Nenhum campo de histórico existe no snapshot hoje; a seção só aparece se
|
|
731
|
+
// uma versão futura publicar um.
|
|
732
|
+
const history = list(story.history);
|
|
733
|
+
if (history.length > 0) {
|
|
734
|
+
drawerSection('Histórico', (body) => {
|
|
735
|
+
const entries = el('ol', 'drawer-list');
|
|
736
|
+
for (const entry of history) {
|
|
737
|
+
const item = el('li');
|
|
738
|
+
if (entry.at) item.appendChild(el('span', 'log-time', formatClock(entry.at)));
|
|
739
|
+
item.appendChild(document.createTextNode(text(entry.message)));
|
|
740
|
+
entries.appendChild(item);
|
|
741
|
+
}
|
|
742
|
+
body.appendChild(entries);
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function drawerSection(title, fill) {
|
|
748
|
+
const section = el('section', 'drawer-section');
|
|
749
|
+
section.appendChild(el('h3', 'drawer-section-title', title));
|
|
750
|
+
fill(section);
|
|
751
|
+
els.drawerBody.appendChild(section);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function openDrawer(id) {
|
|
755
|
+
state.selectedStoryId = id;
|
|
756
|
+
els.drawer.hidden = false;
|
|
757
|
+
els.drawerOverlay.hidden = false;
|
|
758
|
+
document.addEventListener('keydown', onDrawerKeydown);
|
|
759
|
+
renderDrawer(state.snapshot);
|
|
760
|
+
els.drawer.focus();
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function closeDrawer() {
|
|
764
|
+
const id = state.selectedStoryId;
|
|
765
|
+
state.selectedStoryId = null;
|
|
766
|
+
els.drawer.hidden = true;
|
|
767
|
+
els.drawerOverlay.hidden = true;
|
|
768
|
+
document.removeEventListener('keydown', onDrawerKeydown);
|
|
769
|
+
clear(els.drawerBody);
|
|
770
|
+
// O card é recriado a cada render, então o foco volta pelo id, não por uma
|
|
771
|
+
// referência guardada na abertura.
|
|
772
|
+
const card = id ? els.kanban.querySelector('[data-story-id="' + id + '"]') : null;
|
|
773
|
+
if (card) card.focus();
|
|
774
|
+
}
|
|
775
|
+
|
|
547
776
|
function renderGit(snapshot) {
|
|
548
777
|
const repo = repoUrl(snapshot);
|
|
549
778
|
|
|
@@ -634,6 +863,14 @@
|
|
|
634
863
|
// ---- Inicialização --------------------------------------------------------
|
|
635
864
|
|
|
636
865
|
async function init() {
|
|
866
|
+
for (const tab of els.tabs) {
|
|
867
|
+
tab.addEventListener('click', () => setActiveTab(tab.id));
|
|
868
|
+
}
|
|
869
|
+
setActiveTab(state.activeTab);
|
|
870
|
+
|
|
871
|
+
els.drawerClose.addEventListener('click', closeDrawer);
|
|
872
|
+
els.drawerOverlay.addEventListener('click', closeDrawer);
|
|
873
|
+
|
|
637
874
|
els.refreshSelect.addEventListener('change', () => {
|
|
638
875
|
state.refreshSeconds = Number(els.refreshSelect.value);
|
|
639
876
|
storeRefresh(state.refreshSeconds);
|
package/web/public/index.html
CHANGED
|
@@ -41,6 +41,14 @@
|
|
|
41
41
|
<div id="alerts-body"></div>
|
|
42
42
|
</section>
|
|
43
43
|
|
|
44
|
+
<div class="tabs" role="tablist" aria-label="Visualizações do painel">
|
|
45
|
+
<button type="button" class="tab" role="tab" id="tab-execution"
|
|
46
|
+
aria-controls="panel-execution" aria-selected="true">Execução</button>
|
|
47
|
+
<button type="button" class="tab" role="tab" id="tab-kanban"
|
|
48
|
+
aria-controls="panel-kanban" aria-selected="false">Kanban</button>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="tab-panel" id="panel-execution" role="tabpanel" aria-labelledby="tab-execution" tabindex="0">
|
|
44
52
|
<div class="columns">
|
|
45
53
|
<section class="card">
|
|
46
54
|
<h2>Resumo da issue</h2>
|
|
@@ -108,12 +116,30 @@
|
|
|
108
116
|
</div>
|
|
109
117
|
<ol id="logs" class="plain-list logs"></ol>
|
|
110
118
|
</section>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div class="tab-panel" id="panel-kanban" role="tabpanel" aria-labelledby="tab-kanban" tabindex="0" hidden>
|
|
122
|
+
<section class="card">
|
|
123
|
+
<h2>Kanban de user stories</h2>
|
|
124
|
+
<div id="kanban" class="kanban"></div>
|
|
125
|
+
</section>
|
|
126
|
+
</div>
|
|
111
127
|
|
|
112
128
|
<footer class="muted footer">
|
|
113
129
|
<span id="session-meta"></span>
|
|
114
130
|
</footer>
|
|
115
131
|
</main>
|
|
116
132
|
|
|
133
|
+
<div id="drawer-overlay" class="drawer-overlay" hidden></div>
|
|
134
|
+
<aside id="drawer" class="drawer" role="dialog" aria-modal="true"
|
|
135
|
+
aria-labelledby="drawer-title" tabindex="-1" hidden>
|
|
136
|
+
<div class="drawer-head">
|
|
137
|
+
<h2 id="drawer-title">Detalhes da user story</h2>
|
|
138
|
+
<button type="button" id="drawer-close" class="drawer-close" aria-label="Fechar detalhes">✕</button>
|
|
139
|
+
</div>
|
|
140
|
+
<div id="drawer-body" class="drawer-body"></div>
|
|
141
|
+
</aside>
|
|
142
|
+
|
|
117
143
|
<noscript>
|
|
118
144
|
<p class="noscript">Esta interface precisa de JavaScript. O estado bruto está disponível em
|
|
119
145
|
<a href="status.json">status.json</a>.</p>
|