@unson/brainbase-mcp 0.10.5 → 0.10.7
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/package.json +2 -2
- package/ui/brainbase-tokens.css +6 -0
- package/ui/graph-projects-view.js +2 -1
- package/ui/project-workspace.css +388 -0
- package/ui/project-workspace.js +449 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.7",
|
|
4
4
|
"description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -336,5 +336,5 @@
|
|
|
336
336
|
"engines": {
|
|
337
337
|
"node": ">=20"
|
|
338
338
|
},
|
|
339
|
-
"gitHead": "
|
|
339
|
+
"gitHead": "9b53aa137dac5126a8bd43b7a58b21b135244c88"
|
|
340
340
|
}
|
package/ui/brainbase-tokens.css
CHANGED
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
--bb-color-action-hover: #2a5540;
|
|
28
28
|
--bb-color-action-text: #ffffff;
|
|
29
29
|
--bb-color-focus: #20a37e;
|
|
30
|
+
--bb-color-project-ink: #172d4f;
|
|
31
|
+
--bb-color-project-muted: #566b87;
|
|
32
|
+
--bb-color-project-border: #dce4ee;
|
|
33
|
+
--bb-color-project-border-strong: #c8d4e2;
|
|
34
|
+
--bb-color-project-accent: #1261ad;
|
|
35
|
+
--bb-color-project-accent-strong: #194f8a;
|
|
30
36
|
--bb-color-success: #087d62;
|
|
31
37
|
--bb-color-success-soft: #e6f2ec;
|
|
32
38
|
--bb-color-warning: #8a631d;
|
|
@@ -250,6 +250,7 @@ export function createGraphProjectsView({
|
|
|
250
250
|
emptyNotice,
|
|
251
251
|
pageActions,
|
|
252
252
|
extraMetrics,
|
|
253
|
+
showProjectLedger = true,
|
|
253
254
|
renderRailExtensions,
|
|
254
255
|
selectedId: initialSelectedId,
|
|
255
256
|
onSelect,
|
|
@@ -595,7 +596,7 @@ export function createGraphProjectsView({
|
|
|
595
596
|
return children;
|
|
596
597
|
}
|
|
597
598
|
|
|
598
|
-
children.push(workspaceLedger(doc, {
|
|
599
|
+
if (showProjectLedger) children.push(workspaceLedger(doc, {
|
|
599
600
|
className: 'bb-graph-project-ledger',
|
|
600
601
|
ariaLabel: 'プロジェクトの一覧',
|
|
601
602
|
columns: GRAPH_PROJECT_LEDGER_COLUMNS,
|
package/ui/project-workspace.css
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/* Shared project knowledge workspace. Load after graph-view-shared.css. */
|
|
2
2
|
|
|
3
3
|
.bb-pkw {
|
|
4
|
+
--bb-color-ink: var(--bb-color-project-ink);
|
|
5
|
+
--bb-color-muted: var(--bb-color-project-muted);
|
|
6
|
+
--bb-color-border: var(--bb-color-project-border);
|
|
7
|
+
--bb-color-border-strong: var(--bb-color-project-border-strong);
|
|
8
|
+
--bb-color-surface: var(--bb-color-panel);
|
|
9
|
+
--bb-color-accent: var(--bb-color-project-accent);
|
|
10
|
+
--bb-color-accent-strong: var(--bb-color-project-accent-strong);
|
|
4
11
|
display: grid;
|
|
5
12
|
gap: var(--bb-space-4);
|
|
6
13
|
min-width: 0;
|
|
@@ -527,3 +534,384 @@
|
|
|
527
534
|
font-size: 11px;
|
|
528
535
|
line-height: 1.5;
|
|
529
536
|
}
|
|
537
|
+
|
|
538
|
+
/* Dense project routes keep the first screen useful when the host supplies
|
|
539
|
+
many records. The overview carries only quantities and the next useful
|
|
540
|
+
entry point; rows and details carry the long form. */
|
|
541
|
+
.bb-pkw-dense-overview {
|
|
542
|
+
display: grid;
|
|
543
|
+
gap: var(--bb-space-4);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.bb-pkw-dense-intro {
|
|
547
|
+
display: none;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.bb-pkw-dense-eyebrow {
|
|
551
|
+
color: var(--bb-color-eyebrow);
|
|
552
|
+
font-size: 11px;
|
|
553
|
+
font-weight: 760;
|
|
554
|
+
letter-spacing: .08em;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.bb-pkw-dense-intro h3,
|
|
558
|
+
.bb-pkw-dense-card h4 {
|
|
559
|
+
margin: 0;
|
|
560
|
+
color: var(--bb-color-ink);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.bb-pkw-dense-intro h3 {
|
|
564
|
+
font-size: clamp(18px, 2vw, 24px);
|
|
565
|
+
letter-spacing: -.03em;
|
|
566
|
+
overflow-wrap: anywhere;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.bb-pkw-dense-metrics {
|
|
570
|
+
display: grid;
|
|
571
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
572
|
+
gap: var(--bb-space-2);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.bb-pkw-dense-metric {
|
|
576
|
+
display: grid;
|
|
577
|
+
gap: 3px;
|
|
578
|
+
min-width: 0;
|
|
579
|
+
border: 1px solid var(--bb-color-border);
|
|
580
|
+
border-radius: var(--bb-radius-sm);
|
|
581
|
+
padding: 8px 12px;
|
|
582
|
+
background: var(--bb-color-panel);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.bb-pkw-dense-metric-label {
|
|
586
|
+
color: var(--bb-color-muted);
|
|
587
|
+
font-size: 11px;
|
|
588
|
+
font-weight: 720;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.bb-pkw-dense-metric-value {
|
|
592
|
+
min-width: 0;
|
|
593
|
+
overflow-wrap: anywhere;
|
|
594
|
+
color: var(--bb-color-ink);
|
|
595
|
+
font-size: 20px;
|
|
596
|
+
letter-spacing: -.03em;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.bb-pkw-dense-grid {
|
|
600
|
+
display: grid;
|
|
601
|
+
grid-template-columns: minmax(0, 1fr);
|
|
602
|
+
gap: var(--bb-space-3);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.bb-pkw-dense-supporting-grid {
|
|
606
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.bb-pkw-dense-card {
|
|
610
|
+
display: grid;
|
|
611
|
+
gap: 6px;
|
|
612
|
+
min-width: 0;
|
|
613
|
+
border: 1px solid var(--bb-color-border);
|
|
614
|
+
border-radius: var(--bb-radius-sm);
|
|
615
|
+
padding: 10px 14px;
|
|
616
|
+
background: var(--bb-color-panel);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.bb-pkw-dense-card-wide {
|
|
620
|
+
grid-column: 1 / -1;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.bb-pkw-dense-card-head,
|
|
624
|
+
.bb-pkw-dense-item-head,
|
|
625
|
+
.bb-pkw-route-heading {
|
|
626
|
+
display: flex;
|
|
627
|
+
flex-wrap: wrap;
|
|
628
|
+
gap: var(--bb-space-2);
|
|
629
|
+
align-items: center;
|
|
630
|
+
justify-content: space-between;
|
|
631
|
+
min-width: 0;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.bb-pkw-dense-action,
|
|
635
|
+
.bb-pkw-back {
|
|
636
|
+
border: 0;
|
|
637
|
+
padding: 0;
|
|
638
|
+
color: var(--bb-color-accent-strong);
|
|
639
|
+
background: transparent;
|
|
640
|
+
font: inherit;
|
|
641
|
+
font-size: 12px;
|
|
642
|
+
font-weight: 720;
|
|
643
|
+
cursor: pointer;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.bb-pkw-dense-action:hover,
|
|
647
|
+
.bb-pkw-back:hover {
|
|
648
|
+
color: var(--bb-color-accent);
|
|
649
|
+
text-decoration: underline;
|
|
650
|
+
text-underline-offset: 3px;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.bb-pkw-dense-action:focus-visible,
|
|
654
|
+
.bb-pkw-back:focus-visible,
|
|
655
|
+
.bb-pkw-dense-link:focus-visible {
|
|
656
|
+
outline: 3px solid color-mix(in srgb, var(--bb-color-focus) 45%, var(--bb-color-panel));
|
|
657
|
+
outline-offset: 2px;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.bb-pkw-dense-list,
|
|
661
|
+
.bb-pkw-dense-record-list {
|
|
662
|
+
display: grid;
|
|
663
|
+
gap: var(--bb-space-2);
|
|
664
|
+
margin: 0;
|
|
665
|
+
padding: 0;
|
|
666
|
+
list-style: none;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.bb-pkw-dense-item {
|
|
670
|
+
display: grid;
|
|
671
|
+
gap: 4px;
|
|
672
|
+
min-width: 0;
|
|
673
|
+
border-bottom: 1px solid var(--bb-color-border);
|
|
674
|
+
padding-bottom: var(--bb-space-2);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.bb-pkw-dense-item:last-child {
|
|
678
|
+
border-bottom: 0;
|
|
679
|
+
padding-bottom: 0;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.bb-pkw-dense-item-head {
|
|
683
|
+
justify-content: flex-start;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.bb-pkw-dense-link {
|
|
687
|
+
max-width: 100%;
|
|
688
|
+
overflow-wrap: anywhere;
|
|
689
|
+
border: 0;
|
|
690
|
+
padding: 0;
|
|
691
|
+
color: var(--bb-color-accent-strong);
|
|
692
|
+
background: transparent;
|
|
693
|
+
font: inherit;
|
|
694
|
+
font-weight: 720;
|
|
695
|
+
text-align: left;
|
|
696
|
+
text-decoration: underline;
|
|
697
|
+
text-decoration-color: color-mix(in srgb, var(--bb-color-accent) 35%, transparent);
|
|
698
|
+
text-underline-offset: 3px;
|
|
699
|
+
cursor: pointer;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.bb-pkw-dense-link:hover {
|
|
703
|
+
color: var(--bb-color-accent);
|
|
704
|
+
text-decoration-color: currentColor;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.bb-pkw-dense-item-facts {
|
|
708
|
+
display: flex;
|
|
709
|
+
flex-wrap: wrap;
|
|
710
|
+
gap: 4px var(--bb-space-3);
|
|
711
|
+
min-width: 0;
|
|
712
|
+
color: var(--bb-color-muted);
|
|
713
|
+
font-size: 11px;
|
|
714
|
+
line-height: 1.5;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.bb-pkw-dense-table {
|
|
718
|
+
gap: 0;
|
|
719
|
+
overflow-x: auto;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.bb-pkw-dense-table-header {
|
|
723
|
+
display: grid;
|
|
724
|
+
grid-template-columns: minmax(220px, 2fr) repeat(4, minmax(94px, 1fr));
|
|
725
|
+
gap: var(--bb-space-2);
|
|
726
|
+
min-width: 0;
|
|
727
|
+
border-bottom: 1px solid var(--bb-color-border-strong);
|
|
728
|
+
padding: 0 0 var(--bb-space-2);
|
|
729
|
+
color: var(--bb-color-muted);
|
|
730
|
+
font-size: 11px;
|
|
731
|
+
font-weight: 720;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.bb-pkw-dense-table .bb-pkw-dense-item {
|
|
735
|
+
display: grid;
|
|
736
|
+
grid-template-columns: minmax(220px, 2fr) repeat(4, minmax(94px, 1fr));
|
|
737
|
+
gap: var(--bb-space-2);
|
|
738
|
+
align-items: center;
|
|
739
|
+
min-width: 0;
|
|
740
|
+
border-bottom: 1px solid var(--bb-color-border);
|
|
741
|
+
padding: 5px 0;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.bb-pkw-dense-table .bb-pkw-dense-item-head {
|
|
745
|
+
min-width: 0;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.bb-pkw-dense-table .bb-pkw-dense-item-facts {
|
|
749
|
+
display: contents;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.bb-pkw-dense-table .bb-pkw-dense-fact,
|
|
753
|
+
.bb-pkw-dense-table .bb-pkw-dense-item-facts > .bb-pkw-muted {
|
|
754
|
+
min-width: 0;
|
|
755
|
+
overflow-wrap: anywhere;
|
|
756
|
+
color: var(--bb-color-muted);
|
|
757
|
+
font-size: 11px;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.bb-pkw-dense-table .bb-pkw-dense-item-facts > .bb-pkw-muted {
|
|
761
|
+
grid-column: 1 / -1;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.bb-pkw-dense-table .bb-pkw-dense-item:last-child {
|
|
765
|
+
border-bottom: 0;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.bb-pkw-dense-change-table .bb-pkw-dense-item {
|
|
769
|
+
grid-template-columns: minmax(220px, 2fr) repeat(4, minmax(80px, 1fr));
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.bb-pkw-dense-supporting-grid {
|
|
773
|
+
align-items: start;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.bb-pkw-route {
|
|
777
|
+
display: grid;
|
|
778
|
+
gap: var(--bb-space-4);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.bb-pkw-route-heading {
|
|
782
|
+
align-items: flex-start;
|
|
783
|
+
justify-content: flex-start;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.bb-pkw-route-title {
|
|
787
|
+
display: grid;
|
|
788
|
+
gap: var(--bb-space-1);
|
|
789
|
+
min-width: 0;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.bb-pkw-route-title h3 {
|
|
793
|
+
margin: 0;
|
|
794
|
+
overflow-wrap: anywhere;
|
|
795
|
+
color: var(--bb-color-ink);
|
|
796
|
+
font-size: clamp(18px, 2vw, 26px);
|
|
797
|
+
letter-spacing: -.03em;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.bb-pkw-work-controls {
|
|
801
|
+
display: flex;
|
|
802
|
+
flex-wrap: wrap;
|
|
803
|
+
gap: var(--bb-space-2);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.bb-pkw-work-search,
|
|
807
|
+
.bb-pkw-work-status,
|
|
808
|
+
.bb-pkw-work-owner,
|
|
809
|
+
.bb-pkw-work-due {
|
|
810
|
+
min-height: 36px;
|
|
811
|
+
border: 1px solid var(--bb-color-border-strong);
|
|
812
|
+
border-radius: var(--bb-radius-sm);
|
|
813
|
+
padding: 7px 10px;
|
|
814
|
+
color: var(--bb-color-ink);
|
|
815
|
+
background: var(--bb-color-panel);
|
|
816
|
+
font: inherit;
|
|
817
|
+
font-size: 12px;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
.bb-pkw-work-search {
|
|
821
|
+
flex: 1 1 260px;
|
|
822
|
+
min-width: 180px;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
.bb-pkw-work-status {
|
|
826
|
+
flex: 0 1 200px;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
.bb-pkw-work-owner,
|
|
830
|
+
.bb-pkw-work-due {
|
|
831
|
+
flex: 0 1 180px;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.bb-pkw-work-search:focus,
|
|
835
|
+
.bb-pkw-work-status:focus,
|
|
836
|
+
.bb-pkw-work-owner:focus,
|
|
837
|
+
.bb-pkw-work-due:focus {
|
|
838
|
+
outline: 3px solid color-mix(in srgb, var(--bb-color-focus) 35%, var(--bb-color-panel));
|
|
839
|
+
outline-offset: 1px;
|
|
840
|
+
border-color: var(--bb-color-accent);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.bb-pkw-work-count {
|
|
844
|
+
margin: 0;
|
|
845
|
+
color: var(--bb-color-muted);
|
|
846
|
+
font-size: 11px;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.bb-pkw-work-rows {
|
|
850
|
+
display: grid;
|
|
851
|
+
gap: 0;
|
|
852
|
+
min-width: 0;
|
|
853
|
+
overflow-x: auto;
|
|
854
|
+
border-top: 1px solid var(--bb-color-border);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
.bb-pkw-work-row {
|
|
858
|
+
display: grid;
|
|
859
|
+
grid-template-columns: minmax(220px, 2fr) repeat(4, minmax(94px, 1fr));
|
|
860
|
+
gap: var(--bb-space-2);
|
|
861
|
+
align-items: center;
|
|
862
|
+
min-width: 680px;
|
|
863
|
+
border-bottom: 1px solid var(--bb-color-border);
|
|
864
|
+
padding: var(--bb-space-3) 0;
|
|
865
|
+
color: var(--bb-color-muted);
|
|
866
|
+
font-size: 12px;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.bb-pkw-work-row-header {
|
|
870
|
+
color: var(--bb-color-muted);
|
|
871
|
+
font-size: 11px;
|
|
872
|
+
font-weight: 720;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
.bb-pkw-work-row > span,
|
|
876
|
+
.bb-pkw-work-row > button {
|
|
877
|
+
min-width: 0;
|
|
878
|
+
overflow-wrap: anywhere;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.bb-pkw-detail-columns {
|
|
882
|
+
display: grid;
|
|
883
|
+
grid-template-columns: minmax(0, 1.45fr) minmax(230px, .75fr);
|
|
884
|
+
gap: var(--bb-space-4);
|
|
885
|
+
align-items: start;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
.bb-pkw-detail-main,
|
|
889
|
+
.bb-pkw-detail-side {
|
|
890
|
+
display: grid;
|
|
891
|
+
gap: var(--bb-space-3);
|
|
892
|
+
min-width: 0;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.bb-pkw-detail-body {
|
|
896
|
+
margin: 0;
|
|
897
|
+
white-space: pre-wrap;
|
|
898
|
+
overflow-wrap: anywhere;
|
|
899
|
+
color: var(--bb-color-ink);
|
|
900
|
+
font-size: 13px;
|
|
901
|
+
line-height: 1.8;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
@media (max-width: 760px) {
|
|
905
|
+
.bb-pkw-dense-metrics {
|
|
906
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
.bb-pkw-dense-grid,
|
|
910
|
+
.bb-pkw-detail-columns {
|
|
911
|
+
grid-template-columns: minmax(0, 1fr);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.bb-pkw-dense-card-wide {
|
|
915
|
+
grid-column: auto;
|
|
916
|
+
}
|
|
917
|
+
}
|
package/ui/project-workspace.js
CHANGED
|
@@ -28,7 +28,7 @@ import { workspaceDefinition, workspaceNotice } from './workspace-kit.js';
|
|
|
28
28
|
|
|
29
29
|
export const PROJECT_WORKSPACE_CONTRACT_VERSION = 'brainbase.project-workspace.v1';
|
|
30
30
|
|
|
31
|
-
const CONTEXT_STATES = new Set(['ok', 'loading', 'unavailable', 'failed']);
|
|
31
|
+
const CONTEXT_STATES = new Set(['ok', 'loading', 'unknown', 'unavailable', 'failed']);
|
|
32
32
|
|
|
33
33
|
function cleanId(value) {
|
|
34
34
|
return textOrNull(value);
|
|
@@ -62,10 +62,16 @@ function normalizeContextItem(item) {
|
|
|
62
62
|
const title = cleanName(item.title, item.name);
|
|
63
63
|
if (!id || !title) return null;
|
|
64
64
|
const normalized = { id, title };
|
|
65
|
-
for (const key of ['kind', 'status', 'summary', 'owner', 'dueAt', 'updatedAt']) {
|
|
65
|
+
for (const key of ['kind', 'status', 'priority', 'summary', 'owner', 'assignee', 'dueAt', 'updatedAt', 'eventAt', 'body', 'description', 'sourceRecordId', 'relatedRecordId', 'relatedTaskId', 'sourcePath', 'provenance']) {
|
|
66
66
|
const value = textOrNull(item[key]);
|
|
67
67
|
if (value) normalized[key] = value;
|
|
68
68
|
}
|
|
69
|
+
for (const key of ['recordIds', 'unknowns']) {
|
|
70
|
+
if (Array.isArray(item[key])) {
|
|
71
|
+
const values = item[key].map((value) => textOrNull(value)).filter(Boolean);
|
|
72
|
+
if (values.length > 0) normalized[key] = values;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
69
75
|
const source = sourceLabel(item.source);
|
|
70
76
|
if (source) normalized.source = source;
|
|
71
77
|
return normalized;
|
|
@@ -82,6 +88,10 @@ function normalizeContextSection(section) {
|
|
|
82
88
|
if (message) normalized.message = message;
|
|
83
89
|
const asOf = textOrNull(section.asOf);
|
|
84
90
|
if (asOf) normalized.asOf = asOf;
|
|
91
|
+
for (const key of ['total', 'totalCount', 'count']) {
|
|
92
|
+
const value = section[key];
|
|
93
|
+
if (Number.isFinite(value) || (typeof value === 'string' && /^\d+$/.test(value))) normalized[key] = Number(value);
|
|
94
|
+
}
|
|
85
95
|
normalized.items = Array.isArray(section.items) ? section.items.map(normalizeContextItem).filter(Boolean) : [];
|
|
86
96
|
return normalized;
|
|
87
97
|
}
|
|
@@ -110,6 +120,9 @@ export function normalizeProjectContext(projectId, result) {
|
|
|
110
120
|
if (result?.state === 'unavailable') {
|
|
111
121
|
return { projectId: id, state: 'unavailable', message: firstText(result.message, result.reason) ?? '補足情報は接続されていません。', sections: [] };
|
|
112
122
|
}
|
|
123
|
+
if (result?.state === 'unknown') {
|
|
124
|
+
return { projectId: id, state: 'unknown', message: firstText(result.message, result.reason) ?? '補足情報の取得状態を確認できません。', sections: [] };
|
|
125
|
+
}
|
|
113
126
|
const payload = result?.state === 'ok' && isRecord(result.payload) ? result.payload : result;
|
|
114
127
|
if (!isRecord(payload) || !Array.isArray(payload.sections)) {
|
|
115
128
|
return { projectId: id, state: 'unavailable', message: '補足情報の形式を確認できません。', sections: [] };
|
|
@@ -331,7 +344,7 @@ function renderProjectContextGroup(doc, item) {
|
|
|
331
344
|
attrs: { 'aria-label': item.title, ...(item.state === 'loading' ? { 'aria-busy': 'true' } : {}) },
|
|
332
345
|
});
|
|
333
346
|
const heading = makeElement(doc, 'div', { className: 'bb-pkw-context-heading' });
|
|
334
|
-
heading.append(makeElement(doc, 'h4', { text: item.title }), badge(doc, item.state === 'ok' ? '利用可能' : item.state === 'loading' ? '読み込み中' : item.state === 'failed' ? '読み取り失敗' : '未接続', item.state === 'ok' ? 'success' : item.state === 'failed' ? 'danger' : 'muted'));
|
|
347
|
+
heading.append(makeElement(doc, 'h4', { text: item.title }), badge(doc, item.state === 'ok' ? '利用可能' : item.state === 'loading' ? '読み込み中' : item.state === 'unknown' ? '未確認' : item.state === 'failed' ? '読み取り失敗' : '未接続', item.state === 'ok' ? 'success' : item.state === 'failed' ? 'danger' : item.state === 'unknown' ? 'warning' : 'muted'));
|
|
335
348
|
group.append(heading);
|
|
336
349
|
if (item.message && !['failed', 'unavailable'].includes(item.state)) group.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: item.message }));
|
|
337
350
|
if (item.asOf) group.append(makeElement(doc, 'small', { className: 'bb-pkw-muted', text: `読み取った時点: ${formatDateTime(item.asOf)}` }));
|
|
@@ -341,6 +354,8 @@ function renderProjectContextGroup(doc, item) {
|
|
|
341
354
|
group.append(workspaceNotice(doc, { label: '読み取り失敗', text: item.message ?? 'このセクションを読み取れませんでした。', tone: 'danger', role: 'alert' }));
|
|
342
355
|
} else if (item.state === 'unavailable') {
|
|
343
356
|
group.append(workspaceNotice(doc, { label: '未接続', text: item.message ?? 'このセクションは接続されていません。', tone: 'warning' }));
|
|
357
|
+
} else if (item.state === 'unknown') {
|
|
358
|
+
group.append(workspaceNotice(doc, { label: '未確認', text: item.message ?? 'このセクションの取得状態は確認できません。', tone: 'warning' }));
|
|
344
359
|
} else if (item.items.length > 0) {
|
|
345
360
|
const list = makeElement(doc, 'ul', { className: 'bb-pkw-context-list' });
|
|
346
361
|
for (const entry of item.items) {
|
|
@@ -385,7 +400,7 @@ export function renderProjectContext(doc, context, {
|
|
|
385
400
|
section.append(workspaceNotice(doc, { label: '読み込み中', text: context.message ?? '補足情報を読み込んでいます。', tone: 'info', role: 'status' }));
|
|
386
401
|
if (sections.length === 0) section.append(contextSectionSkeleton(doc));
|
|
387
402
|
} else {
|
|
388
|
-
section.append(workspaceNotice(doc, { label: context.state === 'failed' ? '読み取り失敗' : '未接続', text: context.message ?? '補足情報を確認できません。', tone: context.state === 'failed' ? 'danger' : 'warning' }));
|
|
403
|
+
section.append(workspaceNotice(doc, { label: context.state === 'failed' ? '読み取り失敗' : context.state === 'unknown' ? '未確認' : '未接続', text: context.message ?? '補足情報を確認できません。', tone: context.state === 'failed' ? 'danger' : 'warning' }));
|
|
389
404
|
}
|
|
390
405
|
}
|
|
391
406
|
if (includeStatus && context.state === 'ok' && sections.length === 0) {
|
|
@@ -427,10 +442,383 @@ export function renderProjectKnowledgeSkeleton(doc, { context = null } = {}) {
|
|
|
427
442
|
return panel;
|
|
428
443
|
}
|
|
429
444
|
|
|
430
|
-
function
|
|
445
|
+
function contextSectionById(context, ids = []) {
|
|
446
|
+
const sections = Array.isArray(context?.sections) ? context.sections : [];
|
|
447
|
+
const wanted = new Set(ids.map((value) => String(value).toLocaleLowerCase()));
|
|
448
|
+
return sections.find((section) => wanted.has(String(section.id).toLocaleLowerCase())) ?? null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function contextTaskSection(context) {
|
|
452
|
+
const direct = contextSectionById(context, ['tasks', 'work', 'works']);
|
|
453
|
+
if (direct) return direct;
|
|
454
|
+
return (context?.sections ?? []).find((section) => /タスク|仕事|work|task/i.test(`${section.id} ${section.title}`)) ?? null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function contextTaskItems(context) {
|
|
458
|
+
const section = contextTaskSection(context);
|
|
459
|
+
return Array.isArray(section?.items) ? section.items.map((item) => ({ ...item, kind: item.kind ?? 'task' })) : [];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function contextRecordItems(context) {
|
|
463
|
+
const sections = Array.isArray(context?.sections) ? context.sections : [];
|
|
464
|
+
return sections
|
|
465
|
+
.filter((section) => section !== contextTaskSection(context))
|
|
466
|
+
.flatMap((section) => Array.isArray(section.items) ? section.items.map((item) => ({ ...item, kind: item.kind ?? section.id })) : []);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function explicitSectionTotal(section) {
|
|
470
|
+
for (const key of ['total', 'totalCount', 'count']) {
|
|
471
|
+
const value = section?.[key];
|
|
472
|
+
if (Number.isFinite(value)) return value;
|
|
473
|
+
if (typeof value === 'string' && /^\d+$/.test(value)) return Number(value);
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function denseDate(value) {
|
|
479
|
+
const text = textOrNull(value);
|
|
480
|
+
if (!text) return '未確認';
|
|
481
|
+
const formatted = formatDateTime(text);
|
|
482
|
+
return formatted === '—' ? text : formatted;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function denseButton(doc, label, className, onClick, attrs = {}) {
|
|
486
|
+
const button = makeElement(doc, 'button', { className, text: label, attrs: { type: 'button', ...attrs } });
|
|
487
|
+
if (typeof onClick === 'function') button.addEventListener('click', (event) => {
|
|
488
|
+
event?.preventDefault?.();
|
|
489
|
+
onClick();
|
|
490
|
+
});
|
|
491
|
+
return button;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function denseMetric(doc, label, value, note) {
|
|
495
|
+
const item = makeElement(doc, 'div', { className: 'bb-pkw-dense-metric' });
|
|
496
|
+
item.append(makeElement(doc, 'span', { className: 'bb-pkw-dense-metric-label', text: label }), makeElement(doc, 'strong', { className: 'bb-pkw-dense-metric-value', text: value ?? '未確認' }));
|
|
497
|
+
if (note) item.append(makeElement(doc, 'small', { className: 'bb-pkw-muted', text: note }));
|
|
498
|
+
return item;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function denseItemFacts(doc, item, { includeSummary = true, table = false } = {}) {
|
|
502
|
+
const facts = [];
|
|
503
|
+
if (table) {
|
|
504
|
+
facts.push(item.status ?? '未確認');
|
|
505
|
+
facts.push(item.owner || item.assignee ? `担当 ${item.owner ?? item.assignee}` : '担当 未確認');
|
|
506
|
+
facts.push(item.dueAt ? `期限 ${denseDate(item.dueAt)}` : '期限 未確認');
|
|
507
|
+
facts.push(item.updatedAt || item.eventAt ? `更新 ${denseDate(item.updatedAt ?? item.eventAt)}` : '更新 未確認');
|
|
508
|
+
} else {
|
|
509
|
+
if (item.status) facts.push(item.status);
|
|
510
|
+
if (item.owner || item.assignee) facts.push(`担当 ${item.owner ?? item.assignee}`);
|
|
511
|
+
if (item.dueAt) facts.push(`期限 ${denseDate(item.dueAt)}`);
|
|
512
|
+
if (item.updatedAt || item.eventAt) facts.push(`更新 ${denseDate(item.updatedAt ?? item.eventAt)}`);
|
|
513
|
+
}
|
|
514
|
+
const row = makeElement(doc, 'div', { className: 'bb-pkw-dense-item-facts' });
|
|
515
|
+
for (const fact of facts) row.append(makeElement(doc, 'span', { className: 'bb-pkw-dense-fact', text: fact }));
|
|
516
|
+
if (includeSummary && item.summary) row.append(makeElement(doc, 'span', { className: 'bb-pkw-muted', text: item.summary }));
|
|
517
|
+
return row;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function denseContextStateNotice(doc, section, fallback = 'この情報はまだ確認できません。') {
|
|
521
|
+
if (!section || section.state === 'ok') return null;
|
|
522
|
+
const label = section.state === 'loading' ? '読み込み中' : section.state === 'failed' ? '読み取り失敗' : section.state === 'unknown' ? '未確認' : '未接続';
|
|
523
|
+
const tone = section.state === 'failed' ? 'danger' : section.state === 'unknown' ? 'warning' : 'info';
|
|
524
|
+
return workspaceNotice(doc, { label, text: section.message ?? fallback, tone, role: section.state === 'failed' ? 'alert' : 'status' });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function renderDenseItemRow(doc, item, { kind = 'task', onOpen, table = false } = {}) {
|
|
528
|
+
const row = makeElement(doc, 'li', { className: `bb-pkw-dense-item bb-pkw-dense-item-${kind}` });
|
|
529
|
+
const head = makeElement(doc, 'div', { className: 'bb-pkw-dense-item-head' });
|
|
530
|
+
const title = cleanName(item.title, item.name) ?? '名前不明';
|
|
531
|
+
if (typeof onOpen === 'function' && item.id) head.append(denseButton(doc, title, 'bb-pkw-dense-link', () => onOpen(item.id), { 'data-item-id': item.id }));
|
|
532
|
+
else head.append(makeElement(doc, 'strong', { text: title }));
|
|
533
|
+
if (item.status) head.append(badge(doc, item.status, 'muted'));
|
|
534
|
+
row.append(head, denseItemFacts(doc, item, { table, includeSummary: !table }));
|
|
535
|
+
return row;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function renderDenseTableHeader(doc) {
|
|
539
|
+
const row = makeElement(doc, 'li', { className: 'bb-pkw-dense-table-header', attrs: { role: 'presentation' } });
|
|
540
|
+
row.append(makeElement(doc, 'span', { text: '仕事' }), makeElement(doc, 'span', { text: '状態' }), makeElement(doc, 'span', { text: '担当' }), makeElement(doc, 'span', { text: '期限' }), makeElement(doc, 'span', { text: '更新' }));
|
|
541
|
+
return row;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function sortDenseItems(items) {
|
|
545
|
+
return [...items].sort((left, right) => {
|
|
546
|
+
const leftDate = Date.parse(left.dueAt ?? left.updatedAt ?? left.eventAt ?? '') || Number.MAX_SAFE_INTEGER;
|
|
547
|
+
const rightDate = Date.parse(right.dueAt ?? right.updatedAt ?? right.eventAt ?? '') || Number.MAX_SAFE_INTEGER;
|
|
548
|
+
return leftDate - rightDate;
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function todayActionItems(items, now = new Date()) {
|
|
553
|
+
const endOfToday = new Date(now);
|
|
554
|
+
endOfToday.setHours(23, 59, 59, 999);
|
|
555
|
+
return sortDenseItems(items.filter((item) => {
|
|
556
|
+
if (/完了|取消|キャンセル|中止|取下げ|completed|cancell?ed|done|closed/i.test(item.status ?? '')) return false;
|
|
557
|
+
if (/確認待ち|要確認|承認待ち|blocked|needs.review|waiting/i.test(item.status ?? '')) return true;
|
|
558
|
+
const due = Date.parse(item.dueAt ?? '');
|
|
559
|
+
return Number.isFinite(due) && due <= endOfToday.getTime();
|
|
560
|
+
}));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function renderDenseOverview(doc, detail, context, entities, actions = {}) {
|
|
564
|
+
const payload = projectDetailPayload(detail);
|
|
565
|
+
const taskSection = contextTaskSection(context);
|
|
566
|
+
const tasks = contextTaskItems(context);
|
|
567
|
+
const records = contextRecordItems(context);
|
|
568
|
+
const graphRecords = [...(entities?.values?.() ?? [])].filter((entity) => !['project', 'person'].includes(entity.type));
|
|
569
|
+
const participants = [...new Map((payload?.participants ?? [])
|
|
570
|
+
.map((edge) => edge?.counterpart)
|
|
571
|
+
.filter((person) => person?.id && (person.name || person.title))
|
|
572
|
+
.map((person) => [person.id, person])).values()].slice(0, 6);
|
|
573
|
+
const recentRecords = records.length > 0 ? sortDenseItems(records).slice(0, 4) : graphRecords.slice(0, 4);
|
|
574
|
+
const updated = [...tasks, ...records]
|
|
575
|
+
.filter((item) => item.updatedAt || item.eventAt)
|
|
576
|
+
.sort((left, right) => (Date.parse(right.updatedAt ?? right.eventAt) || 0) - (Date.parse(left.updatedAt ?? left.eventAt) || 0))
|
|
577
|
+
.slice(0, 4);
|
|
578
|
+
const due = todayActionItems(tasks).slice(0, 6);
|
|
579
|
+
const total = explicitSectionTotal(taskSection);
|
|
580
|
+
const taskMetric = total === null ? (taskSection?.state === 'ok' ? `${tasks.length}件表示` : '未確認') : `${total}件`;
|
|
581
|
+
const taskNote = taskSection?.state === 'ok' ? (total === null ? '総数は未確認' : '取得済みの総数') : '取得状態を確認中';
|
|
582
|
+
const panel = makeElement(doc, 'section', { className: 'bb-pkw-dense-overview', attrs: { 'aria-label': 'プロジェクト概要' } });
|
|
583
|
+
const intro = makeElement(doc, 'div', { className: 'bb-pkw-dense-intro' });
|
|
584
|
+
intro.append(makeElement(doc, 'div', { className: 'bb-pkw-dense-eyebrow', text: '今日見る' }), makeElement(doc, 'h3', { text: cleanName(payload?.project?.name, 'プロジェクト') }), makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '量を把握して、必要な仕事と根拠へ進みます。' }));
|
|
585
|
+
panel.append(intro);
|
|
586
|
+
const metrics = makeElement(doc, 'div', { className: 'bb-pkw-dense-metrics', attrs: { 'aria-label': '量の指標' } });
|
|
587
|
+
metrics.append(
|
|
588
|
+
denseMetric(doc, '仕事', taskMetric, taskNote),
|
|
589
|
+
denseMetric(doc, '今日扱う仕事', taskSection?.state === 'ok' ? `${due.length}件` : '未確認', taskSection?.state === 'ok' ? '期限・更新が確認できる順' : 'タスクの状態を確認中'),
|
|
590
|
+
denseMetric(doc, '前回からの変化', updated.length > 0 ? `${updated.length}件` : '未確認', updated.length > 0 ? '更新時点がある記録' : '更新時点が未確認'),
|
|
591
|
+
denseMetric(doc, '関係者', Array.isArray(payload?.participants) && payload.participants.length > 0 ? `${payload.participants.length}人` : '未確認', 'Graphに記録された参加関係'),
|
|
592
|
+
);
|
|
593
|
+
panel.append(metrics);
|
|
594
|
+
|
|
595
|
+
const primaryGrid = makeElement(doc, 'div', { className: 'bb-pkw-dense-grid' });
|
|
596
|
+
const today = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '今日判断・対応すること' } });
|
|
597
|
+
const todayHead = makeElement(doc, 'div', { className: 'bb-pkw-dense-card-head' });
|
|
598
|
+
todayHead.append(makeElement(doc, 'h4', { text: '今日判断・対応すること' }));
|
|
599
|
+
if (typeof actions.openWorkList === 'function') todayHead.append(denseButton(doc, 'すべて見る', 'bb-pkw-dense-action', actions.openWorkList));
|
|
600
|
+
today.append(todayHead);
|
|
601
|
+
const todayNotice = denseContextStateNotice(doc, taskSection ?? (context?.state !== 'ok' ? context : null), 'タスクを読み取れませんでした。');
|
|
602
|
+
if (todayNotice) today.append(todayNotice);
|
|
603
|
+
if (!todayNotice && due.length === 0) today.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '表示できる仕事はありません。' }));
|
|
604
|
+
if (!todayNotice && due.length > 0) {
|
|
605
|
+
const list = makeElement(doc, 'ul', { className: 'bb-pkw-dense-list bb-pkw-dense-table' });
|
|
606
|
+
list.append(renderDenseTableHeader(doc));
|
|
607
|
+
for (const item of due) list.append(renderDenseItemRow(doc, item, { onOpen: actions.openWorkDetail, table: true }));
|
|
608
|
+
today.append(list);
|
|
609
|
+
}
|
|
610
|
+
primaryGrid.append(today);
|
|
611
|
+
|
|
612
|
+
const changes = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '前回からの変化' } });
|
|
613
|
+
const changesHead = makeElement(doc, 'div', { className: 'bb-pkw-dense-card-head' });
|
|
614
|
+
changesHead.append(makeElement(doc, 'h4', { text: '前回からの変化' }));
|
|
615
|
+
changes.append(changesHead);
|
|
616
|
+
if (updated.length === 0) changes.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '更新時点のある記録は未確認です。' }));
|
|
617
|
+
else {
|
|
618
|
+
const list = makeElement(doc, 'ul', { className: 'bb-pkw-dense-list bb-pkw-dense-table bb-pkw-dense-change-table' });
|
|
619
|
+
list.append(renderDenseTableHeader(doc));
|
|
620
|
+
for (const item of updated) list.append(renderDenseItemRow(doc, item, { kind: 'change', onOpen: records.includes(item) ? actions.openRecordDetail : actions.openWorkDetail, table: true }));
|
|
621
|
+
changes.append(list);
|
|
622
|
+
}
|
|
623
|
+
primaryGrid.append(changes);
|
|
624
|
+
panel.append(primaryGrid);
|
|
625
|
+
|
|
626
|
+
const supportingGrid = makeElement(doc, 'div', { className: 'bb-pkw-dense-grid bb-pkw-dense-supporting-grid' });
|
|
627
|
+
const recent = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '最近の記録' } });
|
|
628
|
+
const recentHead = makeElement(doc, 'div', { className: 'bb-pkw-dense-card-head' });
|
|
629
|
+
recentHead.append(makeElement(doc, 'h4', { text: '最近の記録' }), makeElement(doc, 'span', { className: 'bb-pkw-muted', text: recentRecords.length > 0 ? `${recentRecords.length}件を表示` : '未確認' }));
|
|
630
|
+
recent.append(recentHead);
|
|
631
|
+
if (recentRecords.length === 0) recent.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '最近の記録は未確認です。' }));
|
|
632
|
+
else {
|
|
633
|
+
const list = makeElement(doc, 'ul', { className: 'bb-pkw-dense-record-list' });
|
|
634
|
+
for (const item of recentRecords) list.append(renderDenseItemRow(doc, item, { kind: 'record', onOpen: actions.openRecordDetail }));
|
|
635
|
+
recent.append(list);
|
|
636
|
+
}
|
|
637
|
+
supportingGrid.append(recent);
|
|
638
|
+
|
|
639
|
+
const people = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '関係者' } });
|
|
640
|
+
const peopleHead = makeElement(doc, 'div', { className: 'bb-pkw-dense-card-head' });
|
|
641
|
+
peopleHead.append(makeElement(doc, 'h4', { text: '関係者' }), makeElement(doc, 'span', { className: 'bb-pkw-muted', text: participants.length > 0 ? `${participants.length}人を表示` : '未確認' }));
|
|
642
|
+
people.append(peopleHead);
|
|
643
|
+
if (participants.length === 0) people.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: 'Graphで確認できる関係者は未確認です。' }));
|
|
644
|
+
else {
|
|
645
|
+
const list = makeElement(doc, 'ul', { className: 'bb-pkw-dense-list' });
|
|
646
|
+
for (const person of participants) {
|
|
647
|
+
const row = makeElement(doc, 'li', { className: 'bb-pkw-dense-item' });
|
|
648
|
+
row.append(makeElement(doc, 'strong', { text: cleanName(person.name, person.title) }));
|
|
649
|
+
if (person.role) row.append(makeElement(doc, 'span', { className: 'bb-pkw-muted', text: person.role }));
|
|
650
|
+
list.append(row);
|
|
651
|
+
}
|
|
652
|
+
people.append(list);
|
|
653
|
+
}
|
|
654
|
+
supportingGrid.append(people);
|
|
655
|
+
panel.append(supportingGrid);
|
|
656
|
+
return panel;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function renderWorkList(doc, context, actions = {}) {
|
|
660
|
+
const section = contextTaskSection(context);
|
|
661
|
+
const allItems = contextTaskItems(context);
|
|
662
|
+
const panel = makeElement(doc, 'section', { className: 'bb-pkw-panel bb-pkw-route bb-pkw-work-list', attrs: { role: 'tabpanel', 'aria-label': '仕事一覧' } });
|
|
663
|
+
const heading = makeElement(doc, 'div', { className: 'bb-pkw-route-heading' });
|
|
664
|
+
heading.append(denseButton(doc, '← 概要', 'bb-pkw-back', actions.openOverview), makeElement(doc, 'div', { className: 'bb-pkw-route-title' }));
|
|
665
|
+
heading.children[1].append(makeElement(doc, 'h3', { text: '仕事一覧' }), makeElement(doc, 'p', { className: 'bb-pkw-muted', text: section?.state === 'ok' ? '状態・担当・期限で必要な行だけを確認します。' : '取得状態を確認しながら表示します。' }));
|
|
666
|
+
panel.append(heading);
|
|
667
|
+
const notice = denseContextStateNotice(doc, section ?? (context?.state !== 'ok' ? context : null), 'タスクを読み取れませんでした。');
|
|
668
|
+
if (notice) panel.append(notice);
|
|
669
|
+
const controls = makeElement(doc, 'div', { className: 'bb-pkw-work-controls', attrs: { 'aria-label': '仕事の絞り込み' } });
|
|
670
|
+
const search = makeElement(doc, 'input', { className: 'bb-pkw-work-search', attrs: { type: 'search', name: 'work-search', placeholder: '仕事を検索', 'aria-label': '仕事を検索' } });
|
|
671
|
+
const status = makeElement(doc, 'select', { className: 'bb-pkw-work-status', attrs: { name: 'work-status', 'aria-label': '状態で絞り込む' } });
|
|
672
|
+
status.append(makeElement(doc, 'option', { text: 'すべての状態', attrs: { value: '' } }));
|
|
673
|
+
for (const value of new Set(allItems.map((item) => item.status).filter(Boolean))) status.append(makeElement(doc, 'option', { text: value, attrs: { value } }));
|
|
674
|
+
const owner = makeElement(doc, 'select', { className: 'bb-pkw-work-owner', attrs: { name: 'work-owner', 'aria-label': '担当で絞り込む' } });
|
|
675
|
+
owner.append(makeElement(doc, 'option', { text: 'すべての担当', attrs: { value: '' } }));
|
|
676
|
+
for (const value of new Set(allItems.map((item) => item.owner ?? item.assignee).filter(Boolean))) owner.append(makeElement(doc, 'option', { text: value, attrs: { value } }));
|
|
677
|
+
const due = makeElement(doc, 'select', { className: 'bb-pkw-work-due', attrs: { name: 'work-due', 'aria-label': '期限で絞り込む' } });
|
|
678
|
+
due.append(
|
|
679
|
+
makeElement(doc, 'option', { text: 'すべての期限', attrs: { value: '' } }),
|
|
680
|
+
makeElement(doc, 'option', { text: '期限あり', attrs: { value: 'set' } }),
|
|
681
|
+
makeElement(doc, 'option', { text: '期限未確認', attrs: { value: 'missing' } }),
|
|
682
|
+
);
|
|
683
|
+
controls.append(search, status, owner, due);
|
|
684
|
+
panel.append(controls);
|
|
685
|
+
const count = makeElement(doc, 'p', { className: 'bb-pkw-work-count' });
|
|
686
|
+
const rows = makeElement(doc, 'div', { className: 'bb-pkw-work-rows', attrs: { role: 'list', 'aria-label': '仕事の行' } });
|
|
687
|
+
panel.append(count, rows);
|
|
688
|
+
const renderRows = () => {
|
|
689
|
+
const query = String(search.value ?? '').trim().toLocaleLowerCase();
|
|
690
|
+
const selectedStatus = String(status.value ?? '');
|
|
691
|
+
const selectedOwner = String(owner.value ?? '');
|
|
692
|
+
const selectedDue = String(due.value ?? '');
|
|
693
|
+
const visible = allItems.filter((item) => {
|
|
694
|
+
if (selectedStatus && item.status !== selectedStatus) return false;
|
|
695
|
+
if (selectedOwner && (item.owner ?? item.assignee) !== selectedOwner) return false;
|
|
696
|
+
if (selectedDue === 'set' && !item.dueAt) return false;
|
|
697
|
+
if (selectedDue === 'missing' && item.dueAt) return false;
|
|
698
|
+
if (!query) return true;
|
|
699
|
+
return [item.title, item.summary, item.owner, item.assignee, item.status].filter(Boolean).some((value) => String(value).toLocaleLowerCase().includes(query));
|
|
700
|
+
});
|
|
701
|
+
const total = explicitSectionTotal(section);
|
|
702
|
+
const display = visible.slice(0, 16);
|
|
703
|
+
count.textContent = `表示 ${display.length}件${display.length < visible.length ? `(条件一致 ${visible.length}件の先頭)` : ''} / 総数 ${total === null ? '未確認' : `${total}件`}`;
|
|
704
|
+
rows.replaceChildren();
|
|
705
|
+
if (display.length === 0) {
|
|
706
|
+
rows.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: section?.state === 'ok' ? '条件に一致する仕事はありません。' : '仕事の行は未確認です。' }));
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const header = makeElement(doc, 'div', { className: 'bb-pkw-work-row bb-pkw-work-row-header', attrs: { role: 'presentation' } });
|
|
710
|
+
header.append(makeElement(doc, 'span', { text: '仕事' }), makeElement(doc, 'span', { text: '状態' }), makeElement(doc, 'span', { text: '担当' }), makeElement(doc, 'span', { text: '期限' }), makeElement(doc, 'span', { text: '更新' }));
|
|
711
|
+
rows.append(header);
|
|
712
|
+
for (const item of display) {
|
|
713
|
+
const row = makeElement(doc, 'div', { className: 'bb-pkw-work-row', attrs: { role: 'listitem' } });
|
|
714
|
+
row.append(denseButton(doc, item.title, 'bb-pkw-dense-link', () => actions.openWorkDetail?.(item.id), { 'data-item-id': item.id }), makeElement(doc, 'span', { text: item.status ?? '未確認' }), makeElement(doc, 'span', { text: item.owner ?? item.assignee ?? '未確認' }), makeElement(doc, 'span', { text: denseDate(item.dueAt) }), makeElement(doc, 'span', { text: denseDate(item.updatedAt) }));
|
|
715
|
+
rows.append(row);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
search.addEventListener('input', renderRows);
|
|
719
|
+
status.addEventListener('change', renderRows);
|
|
720
|
+
owner.addEventListener('change', renderRows);
|
|
721
|
+
due.addEventListener('change', renderRows);
|
|
722
|
+
renderRows();
|
|
723
|
+
return panel;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function detailDefinition(doc, item) {
|
|
727
|
+
return workspaceDefinition(doc, [
|
|
728
|
+
['状態', item.status],
|
|
729
|
+
['担当', item.owner ?? item.assignee],
|
|
730
|
+
['期限', item.dueAt ? denseDate(item.dueAt) : null],
|
|
731
|
+
['更新', item.updatedAt ? denseDate(item.updatedAt) : null],
|
|
732
|
+
['発生', item.eventAt ? denseDate(item.eventAt) : null],
|
|
733
|
+
['出典', item.source ?? item.sourcePath ?? item.provenance],
|
|
734
|
+
]);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function renderWorkDetail(doc, item, context, actions = {}) {
|
|
738
|
+
const panel = makeElement(doc, 'section', { className: 'bb-pkw-panel bb-pkw-route bb-pkw-detail', attrs: { role: 'tabpanel', 'aria-label': '仕事詳細' } });
|
|
739
|
+
const heading = makeElement(doc, 'div', { className: 'bb-pkw-route-heading' });
|
|
740
|
+
heading.append(denseButton(doc, '← 仕事一覧', 'bb-pkw-back', actions.openWorkList), makeElement(doc, 'div', { className: 'bb-pkw-route-title' }));
|
|
741
|
+
heading.children[1].append(makeElement(doc, 'h3', { text: item?.title ?? '仕事詳細' }), item?.status ? badge(doc, item.status, 'accent') : null);
|
|
742
|
+
panel.append(heading);
|
|
743
|
+
if (!item) {
|
|
744
|
+
panel.append(workspaceNotice(doc, { label: '未確認', text: 'この仕事のIDに一致する記録を確認できません。', tone: 'warning' }));
|
|
745
|
+
return panel;
|
|
746
|
+
}
|
|
747
|
+
const columns = makeElement(doc, 'div', { className: 'bb-pkw-detail-columns' });
|
|
748
|
+
const main = makeElement(doc, 'div', { className: 'bb-pkw-detail-main' });
|
|
749
|
+
const bodyCard = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '仕事の内容' } });
|
|
750
|
+
bodyCard.append(makeElement(doc, 'h4', { text: '仕事の内容' }));
|
|
751
|
+
const body = firstText(item.body, item.description, item.summary);
|
|
752
|
+
bodyCard.append(makeElement(doc, 'p', { className: body ? 'bb-pkw-detail-body' : 'bb-pkw-muted', text: body ?? '本文は未接続です。要約以外の内容は未確認です。' }));
|
|
753
|
+
main.append(bodyCard);
|
|
754
|
+
const records = [item.sourceRecordId, item.relatedRecordId, ...(item.recordIds ?? [])].filter(Boolean);
|
|
755
|
+
const related = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '関連記録' } });
|
|
756
|
+
related.append(makeElement(doc, 'h4', { text: '関連記録' }));
|
|
757
|
+
if (records.length === 0) related.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '関連記録のIDは未接続です。関係を推測して追加していません。' }));
|
|
758
|
+
else {
|
|
759
|
+
const list = makeElement(doc, 'ul', { className: 'bb-pkw-dense-list' });
|
|
760
|
+
for (const id of records) {
|
|
761
|
+
const row = makeElement(doc, 'li');
|
|
762
|
+
row.append(denseButton(doc, id, 'bb-pkw-dense-link', () => actions.openRecordDetail?.(id), { 'data-record-id': id }));
|
|
763
|
+
list.append(row);
|
|
764
|
+
}
|
|
765
|
+
related.append(list);
|
|
766
|
+
}
|
|
767
|
+
main.append(related);
|
|
768
|
+
columns.append(main);
|
|
769
|
+
const side = makeElement(doc, 'aside', { className: 'bb-pkw-detail-side', attrs: { 'aria-label': '仕事の属性' } });
|
|
770
|
+
const sideCard = makeElement(doc, 'section', { className: 'bb-pkw-dense-card' });
|
|
771
|
+
sideCard.append(makeElement(doc, 'h4', { text: '属性' }), detailDefinition(doc, item));
|
|
772
|
+
if (Array.isArray(item.unknowns) && item.unknowns.length > 0) sideCard.append(makeElement(doc, 'h4', { text: '未確認' }), makeElement(doc, 'p', { className: 'bb-pkw-muted', text: item.unknowns.join(' · ') }));
|
|
773
|
+
side.append(sideCard);
|
|
774
|
+
columns.append(side);
|
|
775
|
+
panel.append(columns);
|
|
776
|
+
return panel;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function renderRecordDetail(doc, record, context, actions = {}) {
|
|
780
|
+
const panel = makeElement(doc, 'section', { className: 'bb-pkw-panel bb-pkw-route bb-pkw-detail', attrs: { role: 'tabpanel', 'aria-label': '記録詳細' } });
|
|
781
|
+
const heading = makeElement(doc, 'div', { className: 'bb-pkw-route-heading' });
|
|
782
|
+
heading.append(denseButton(doc, '← 概要', 'bb-pkw-back', actions.openOverview), makeElement(doc, 'div', { className: 'bb-pkw-route-title' }));
|
|
783
|
+
heading.children[1].append(makeElement(doc, 'h3', { text: cleanName(record?.title, record?.name) ?? '記録詳細' }), record?.kind || record?.type ? badge(doc, typeLabel(record.kind ?? record.type), 'accent') : null);
|
|
784
|
+
panel.append(heading);
|
|
785
|
+
if (!record) {
|
|
786
|
+
panel.append(workspaceNotice(doc, { label: '未確認', text: 'この記録のIDに一致する内容を確認できません。', tone: 'warning' }));
|
|
787
|
+
return panel;
|
|
788
|
+
}
|
|
789
|
+
const columns = makeElement(doc, 'div', { className: 'bb-pkw-detail-columns' });
|
|
790
|
+
const main = makeElement(doc, 'div', { className: 'bb-pkw-detail-main' });
|
|
791
|
+
const summary = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '記録の要点' } });
|
|
792
|
+
summary.append(makeElement(doc, 'h4', { text: '記録の要点' }), makeElement(doc, 'p', { className: record.body || record.summary ? 'bb-pkw-detail-body' : 'bb-pkw-muted', text: firstText(record.body, record.summary) ?? '本文・要約は未確認です。' }));
|
|
793
|
+
main.append(summary);
|
|
794
|
+
const related = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '関連する仕事' } });
|
|
795
|
+
related.append(makeElement(doc, 'h4', { text: '関連する仕事' }));
|
|
796
|
+
const tasks = contextTaskItems(context);
|
|
797
|
+
const relatedTask = tasks.find((task) => task.relatedTaskId === record.id || task.sourceRecordId === record.id || (task.recordIds ?? []).includes(record.id));
|
|
798
|
+
if (record.relatedTaskId) {
|
|
799
|
+
const task = tasks.find((candidate) => candidate.id === record.relatedTaskId);
|
|
800
|
+
if (task) related.append(denseButton(doc, task.title, 'bb-pkw-dense-link', () => actions.openWorkDetail?.(task.id), { 'data-task-id': task.id }));
|
|
801
|
+
else related.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: `仕事ID ${record.relatedTaskId} は未確認です。` }));
|
|
802
|
+
} else if (relatedTask) related.append(denseButton(doc, relatedTask.title, 'bb-pkw-dense-link', () => actions.openWorkDetail?.(relatedTask.id), { 'data-task-id': relatedTask.id }));
|
|
803
|
+
else related.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '関連する仕事のIDは未接続です。' }));
|
|
804
|
+
main.append(related);
|
|
805
|
+
columns.append(main);
|
|
806
|
+
const side = makeElement(doc, 'aside', { className: 'bb-pkw-detail-side', attrs: { 'aria-label': '記録の属性' } });
|
|
807
|
+
const sideCard = makeElement(doc, 'section', { className: 'bb-pkw-dense-card' });
|
|
808
|
+
sideCard.append(makeElement(doc, 'h4', { text: '出典と状態' }), detailDefinition(doc, record));
|
|
809
|
+
const unknowns = Array.isArray(record.unknowns) ? record.unknowns : [];
|
|
810
|
+
if (unknowns.length > 0) sideCard.append(makeElement(doc, 'h4', { text: '未確認' }), makeElement(doc, 'p', { className: 'bb-pkw-muted', text: unknowns.join(' · ') }));
|
|
811
|
+
side.append(sideCard);
|
|
812
|
+
columns.append(side);
|
|
813
|
+
panel.append(columns);
|
|
814
|
+
return panel;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function renderOverview(doc, detail, context, edges, entities, onSelect, onContext, actions = {}) {
|
|
431
818
|
const payload = projectDetailPayload(detail);
|
|
432
819
|
const project = payload?.project;
|
|
433
820
|
const panel = makeElement(doc, 'div', { className: 'bb-pkw-panel bb-pkw-overview', attrs: { role: 'tabpanel', 'aria-label': '概要' } });
|
|
821
|
+
panel.append(renderDenseOverview(doc, detail, context, entities, actions));
|
|
434
822
|
const columns = makeElement(doc, 'div', { className: 'bb-pkw-columns' });
|
|
435
823
|
const summary = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '目的と現在地' } });
|
|
436
824
|
summary.append(makeElement(doc, 'h3', { text: '目的と現在地' }), workspaceDefinition(doc, [
|
|
@@ -565,6 +953,8 @@ export function createProjectKnowledgeWorkspace({
|
|
|
565
953
|
let graphSearch = '';
|
|
566
954
|
let graphType = '';
|
|
567
955
|
let currentContext = context;
|
|
956
|
+
let activeView = 'overview';
|
|
957
|
+
let selectedContextId = null;
|
|
568
958
|
// Keep direct references as well as the DOM lookup. The local test harness
|
|
569
959
|
// intentionally has no querySelector implementation, and a host can replace
|
|
570
960
|
// the panel while a detail read is still in flight.
|
|
@@ -597,10 +987,53 @@ export function createProjectKnowledgeWorkspace({
|
|
|
597
987
|
|
|
598
988
|
function setTab(tab) {
|
|
599
989
|
activeTab = tab === 'graph' ? 'graph' : 'overview';
|
|
990
|
+
if (activeTab === 'overview') {
|
|
991
|
+
activeView = 'overview';
|
|
992
|
+
selectedContextId = null;
|
|
993
|
+
}
|
|
600
994
|
if (typeof onTabChange === 'function') onTabChange(activeTab);
|
|
601
995
|
render();
|
|
602
996
|
}
|
|
603
997
|
|
|
998
|
+
function findContextItem(id, kind = null) {
|
|
999
|
+
const target = cleanId(id);
|
|
1000
|
+
if (!target) return null;
|
|
1001
|
+
const candidates = kind === 'task' ? contextTaskItems(currentContext) : kind === 'record' ? contextRecordItems(currentContext) : [...contextTaskItems(currentContext), ...contextRecordItems(currentContext)];
|
|
1002
|
+
return candidates.find((item) => item.id === target) ?? null;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function openOverview() {
|
|
1006
|
+
if (destroyed) return;
|
|
1007
|
+
activeTab = 'overview';
|
|
1008
|
+
activeView = 'overview';
|
|
1009
|
+
selectedContextId = null;
|
|
1010
|
+
render();
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function openWorkList() {
|
|
1014
|
+
if (destroyed) return;
|
|
1015
|
+
activeTab = 'overview';
|
|
1016
|
+
activeView = 'work-list';
|
|
1017
|
+
selectedContextId = null;
|
|
1018
|
+
render();
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function openWorkDetail(id) {
|
|
1022
|
+
if (destroyed) return;
|
|
1023
|
+
activeTab = 'overview';
|
|
1024
|
+
activeView = 'work-detail';
|
|
1025
|
+
selectedContextId = cleanId(id);
|
|
1026
|
+
render();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function openRecordDetail(id) {
|
|
1030
|
+
if (destroyed) return;
|
|
1031
|
+
activeTab = 'overview';
|
|
1032
|
+
activeView = 'record-detail';
|
|
1033
|
+
selectedContextId = cleanId(id);
|
|
1034
|
+
render();
|
|
1035
|
+
}
|
|
1036
|
+
|
|
604
1037
|
function mergeEntityRead(read) {
|
|
605
1038
|
if (!isRecord(read?.entity)) return;
|
|
606
1039
|
addEntity(entities, read.entity);
|
|
@@ -813,6 +1246,10 @@ export function createProjectKnowledgeWorkspace({
|
|
|
813
1246
|
function replaceContextBlock(nextContext) {
|
|
814
1247
|
currentContext = nextContext;
|
|
815
1248
|
if (destroyed) return;
|
|
1249
|
+
if (activeTab === 'overview' && activeView !== 'overview') {
|
|
1250
|
+
render();
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
816
1253
|
const nextBlock = renderProjectContext(doc, currentContext);
|
|
817
1254
|
if (!nextBlock) {
|
|
818
1255
|
if (contextBlock?.parentNode && typeof contextBlock.parentNode.replaceChild === 'function') {
|
|
@@ -852,10 +1289,15 @@ export function createProjectKnowledgeWorkspace({
|
|
|
852
1289
|
panels.replaceChildren();
|
|
853
1290
|
contextBlock = null;
|
|
854
1291
|
contextAnchor = null;
|
|
855
|
-
const
|
|
1292
|
+
const actions = { openOverview, openWorkList, openWorkDetail, openRecordDetail };
|
|
1293
|
+
let overview;
|
|
1294
|
+
if (activeView === 'work-list') overview = renderWorkList(doc, currentContext, actions);
|
|
1295
|
+
else if (activeView === 'work-detail') overview = renderWorkDetail(doc, findContextItem(selectedContextId, 'task'), currentContext, actions);
|
|
1296
|
+
else if (activeView === 'record-detail') overview = renderRecordDetail(doc, findContextItem(selectedContextId, 'record') ?? entities.get(selectedContextId), currentContext, actions);
|
|
1297
|
+
else overview = renderOverview(doc, payload, currentContext, overviewEdges, overviewEntities, selectAndExplore, (node, anchor) => {
|
|
856
1298
|
contextBlock = node;
|
|
857
1299
|
contextAnchor = anchor;
|
|
858
|
-
});
|
|
1300
|
+
}, actions);
|
|
859
1301
|
overview.id = 'bb-pkw-panel-overview';
|
|
860
1302
|
overview.hidden = activeTab !== 'overview';
|
|
861
1303
|
overviewPanel = overview;
|