create-pmos 0.65.0 → 0.65.2
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 +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/guide/docs.md +59 -6
- package/workspace/guide/initiatives-screen.md +8 -0
- package/workspace/guide/today.md +11 -3
- package/workspace/lib/calendar-memory.js +53 -5
- package/workspace/lib/doc-library.js +131 -0
- package/workspace/lib/goals.js +25 -6
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +307 -0
- package/workspace/public/pm/dest-docs.js +144 -44
- package/workspace/public/pm/dest-today.js +4 -4
- package/workspace/public/pm/doc-graph.js +272 -0
- package/workspace/public/pm/initiative-sheet.js +24 -2
- package/workspace/public/pm/initiatives-copy.js +8 -0
- package/workspace/public/pm/today-agenda.js +29 -5
- package/workspace/public/pm/today-copy.js +11 -3
- package/workspace/public/pm/today-day.js +42 -18
- package/workspace/server.js +81 -6
- package/workspace/test-lists/docs-library-graph.json +5 -0
|
@@ -1784,6 +1784,17 @@
|
|
|
1784
1784
|
.in-derived {
|
|
1785
1785
|
margin-bottom: 16px;
|
|
1786
1786
|
}
|
|
1787
|
+
.in-files__bar {
|
|
1788
|
+
display: flex;
|
|
1789
|
+
flex-wrap: wrap;
|
|
1790
|
+
align-items: center;
|
|
1791
|
+
gap: 12px;
|
|
1792
|
+
margin-bottom: 12px;
|
|
1793
|
+
}
|
|
1794
|
+
.in-files__hint {
|
|
1795
|
+
font-size: 12px;
|
|
1796
|
+
color: var(--muted-foreground);
|
|
1797
|
+
}
|
|
1787
1798
|
.in-files__title,
|
|
1788
1799
|
.in-derived__title {
|
|
1789
1800
|
margin: 0;
|
|
@@ -4503,6 +4514,302 @@
|
|
|
4503
4514
|
}
|
|
4504
4515
|
}
|
|
4505
4516
|
|
|
4517
|
+
/* Docs › All files (docs-library-graph): folding categories, one line per file, the match marked, Add copy on hover. */
|
|
4518
|
+
.rp-docs-folds {
|
|
4519
|
+
display: flex;
|
|
4520
|
+
flex-direction: column;
|
|
4521
|
+
}
|
|
4522
|
+
.rp-docs-hint {
|
|
4523
|
+
margin: 0;
|
|
4524
|
+
font-size: 12px;
|
|
4525
|
+
color: var(--muted-foreground);
|
|
4526
|
+
}
|
|
4527
|
+
.rp-docs-mark {
|
|
4528
|
+
background: var(--accent);
|
|
4529
|
+
color: var(--foreground);
|
|
4530
|
+
}
|
|
4531
|
+
.rp-docs-path {
|
|
4532
|
+
font-family: var(--mono);
|
|
4533
|
+
}
|
|
4534
|
+
.rp-docs-showall {
|
|
4535
|
+
align-self: flex-start;
|
|
4536
|
+
margin-top: 4px;
|
|
4537
|
+
}
|
|
4538
|
+
.rp-docs-links {
|
|
4539
|
+
grid-column: 1 / -1;
|
|
4540
|
+
padding: 4px 0 16px 24px;
|
|
4541
|
+
}
|
|
4542
|
+
.rp-docs-detail > .rp-docs-links {
|
|
4543
|
+
padding-left: 0;
|
|
4544
|
+
}
|
|
4545
|
+
/* Add copy shows on hover or focus where the pointer can hover; without hover (a touch screen) it always shows. */
|
|
4546
|
+
@media (hover: hover) and (pointer: fine) {
|
|
4547
|
+
.rp-docs-addcopy {
|
|
4548
|
+
opacity: 0;
|
|
4549
|
+
}
|
|
4550
|
+
.rp-docs-row:hover .rp-docs-addcopy,
|
|
4551
|
+
.rp-docs-row:focus-within .rp-docs-addcopy {
|
|
4552
|
+
opacity: 1;
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
/* The links of one file: who links here on the left, what it links to on the right, lines behind real buttons. */
|
|
4557
|
+
.rp-graph {
|
|
4558
|
+
display: flex;
|
|
4559
|
+
flex-direction: column;
|
|
4560
|
+
align-items: flex-start;
|
|
4561
|
+
gap: 8px;
|
|
4562
|
+
min-width: 0;
|
|
4563
|
+
}
|
|
4564
|
+
.rp-graph__head {
|
|
4565
|
+
display: flex;
|
|
4566
|
+
flex-wrap: wrap;
|
|
4567
|
+
align-items: center;
|
|
4568
|
+
gap: 8px;
|
|
4569
|
+
width: 100%;
|
|
4570
|
+
}
|
|
4571
|
+
.rp-graph__title {
|
|
4572
|
+
flex: 1 1 240px;
|
|
4573
|
+
min-width: 0;
|
|
4574
|
+
font-size: 14px;
|
|
4575
|
+
}
|
|
4576
|
+
.rp-graph__pic {
|
|
4577
|
+
position: relative;
|
|
4578
|
+
width: 100%;
|
|
4579
|
+
max-width: 820px;
|
|
4580
|
+
aspect-ratio: var(--rp-ar);
|
|
4581
|
+
margin-top: 16px;
|
|
4582
|
+
}
|
|
4583
|
+
.rp-graph__lines {
|
|
4584
|
+
position: absolute;
|
|
4585
|
+
inset: 0;
|
|
4586
|
+
width: 100%;
|
|
4587
|
+
height: 100%;
|
|
4588
|
+
overflow: visible;
|
|
4589
|
+
}
|
|
4590
|
+
.rp-graph__edge {
|
|
4591
|
+
fill: none;
|
|
4592
|
+
stroke: var(--muted-foreground);
|
|
4593
|
+
stroke-width: 1;
|
|
4594
|
+
}
|
|
4595
|
+
.rp-graph__edge--cites {
|
|
4596
|
+
stroke-dasharray: 4 3;
|
|
4597
|
+
}
|
|
4598
|
+
.rp-graph__node,
|
|
4599
|
+
.rp-graph__centre,
|
|
4600
|
+
.rp-graph__more {
|
|
4601
|
+
position: absolute;
|
|
4602
|
+
left: calc(var(--rp-x) * 1%);
|
|
4603
|
+
top: calc(var(--rp-y) * 1%);
|
|
4604
|
+
width: calc(var(--rp-w) * 1%);
|
|
4605
|
+
transform: translateY(-50%);
|
|
4606
|
+
}
|
|
4607
|
+
.rp-graph__node,
|
|
4608
|
+
.rp-graph__item,
|
|
4609
|
+
.rp-net__node {
|
|
4610
|
+
letter-spacing: normal;
|
|
4611
|
+
text-transform: none;
|
|
4612
|
+
}
|
|
4613
|
+
.rp-graph__node {
|
|
4614
|
+
justify-content: flex-start;
|
|
4615
|
+
background: var(--background);
|
|
4616
|
+
font-size: 12px;
|
|
4617
|
+
}
|
|
4618
|
+
.rp-graph__node--in {
|
|
4619
|
+
justify-content: flex-end;
|
|
4620
|
+
}
|
|
4621
|
+
.rp-graph__node--cites {
|
|
4622
|
+
border-style: dashed;
|
|
4623
|
+
}
|
|
4624
|
+
.rp-graph__node:focus-visible {
|
|
4625
|
+
outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground));
|
|
4626
|
+
outline-offset: 2px;
|
|
4627
|
+
}
|
|
4628
|
+
.rp-graph__centre {
|
|
4629
|
+
padding: 6px 10px;
|
|
4630
|
+
border: 1px solid var(--foreground);
|
|
4631
|
+
background: var(--card);
|
|
4632
|
+
font-size: 12px;
|
|
4633
|
+
font-weight: 600;
|
|
4634
|
+
text-align: center;
|
|
4635
|
+
white-space: nowrap;
|
|
4636
|
+
overflow: hidden;
|
|
4637
|
+
text-overflow: ellipsis;
|
|
4638
|
+
}
|
|
4639
|
+
.rp-graph__more {
|
|
4640
|
+
font-size: 12px;
|
|
4641
|
+
color: var(--muted-foreground);
|
|
4642
|
+
}
|
|
4643
|
+
.rp-graph__more--in {
|
|
4644
|
+
text-align: right;
|
|
4645
|
+
}
|
|
4646
|
+
.rp-graph__cap {
|
|
4647
|
+
position: absolute;
|
|
4648
|
+
top: -20px;
|
|
4649
|
+
font-size: 11px;
|
|
4650
|
+
color: var(--muted-foreground);
|
|
4651
|
+
}
|
|
4652
|
+
.rp-graph__cap--out {
|
|
4653
|
+
right: 0;
|
|
4654
|
+
}
|
|
4655
|
+
.rp-graph__lists {
|
|
4656
|
+
display: grid;
|
|
4657
|
+
grid-template-columns: 1fr 1fr;
|
|
4658
|
+
gap: 16px;
|
|
4659
|
+
width: 100%;
|
|
4660
|
+
}
|
|
4661
|
+
.rp-graph__col,
|
|
4662
|
+
.rp-graph__group {
|
|
4663
|
+
display: flex;
|
|
4664
|
+
flex-direction: column;
|
|
4665
|
+
align-items: flex-start;
|
|
4666
|
+
gap: 2px;
|
|
4667
|
+
min-width: 0;
|
|
4668
|
+
}
|
|
4669
|
+
.rp-graph__col {
|
|
4670
|
+
gap: 12px;
|
|
4671
|
+
}
|
|
4672
|
+
.rp-graph__label {
|
|
4673
|
+
font-size: 12px;
|
|
4674
|
+
color: var(--muted-foreground);
|
|
4675
|
+
}
|
|
4676
|
+
.rp-graph__item {
|
|
4677
|
+
max-width: 100%;
|
|
4678
|
+
justify-content: flex-start;
|
|
4679
|
+
}
|
|
4680
|
+
.rp-graph__hint {
|
|
4681
|
+
margin: 0;
|
|
4682
|
+
font-size: 12px;
|
|
4683
|
+
color: var(--muted-foreground);
|
|
4684
|
+
}
|
|
4685
|
+
@media (max-width: 760px) {
|
|
4686
|
+
.rp-graph__lists {
|
|
4687
|
+
grid-template-columns: 1fr;
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
|
|
4691
|
+
/* An initiative's files as a network: filled dots are its files, hollow dots the outside files they link to. */
|
|
4692
|
+
.rp-net {
|
|
4693
|
+
display: flex;
|
|
4694
|
+
flex-direction: column;
|
|
4695
|
+
gap: 8px;
|
|
4696
|
+
}
|
|
4697
|
+
.rp-net__pic {
|
|
4698
|
+
position: relative;
|
|
4699
|
+
width: 100%;
|
|
4700
|
+
aspect-ratio: var(--rp-ar);
|
|
4701
|
+
border: 1px solid var(--border);
|
|
4702
|
+
}
|
|
4703
|
+
.rp-net__lines {
|
|
4704
|
+
position: absolute;
|
|
4705
|
+
inset: 0;
|
|
4706
|
+
width: 100%;
|
|
4707
|
+
height: 100%;
|
|
4708
|
+
}
|
|
4709
|
+
.rp-net__edge {
|
|
4710
|
+
stroke: var(--border);
|
|
4711
|
+
stroke-width: 1;
|
|
4712
|
+
}
|
|
4713
|
+
.rp-net__edge--in {
|
|
4714
|
+
stroke: var(--muted-foreground);
|
|
4715
|
+
}
|
|
4716
|
+
.rp-net__edge--cites {
|
|
4717
|
+
stroke-dasharray: 4 3;
|
|
4718
|
+
}
|
|
4719
|
+
.rp-net__edge--hot {
|
|
4720
|
+
stroke: var(--foreground);
|
|
4721
|
+
stroke-width: 2;
|
|
4722
|
+
}
|
|
4723
|
+
.rp-net__node {
|
|
4724
|
+
position: absolute;
|
|
4725
|
+
left: calc(var(--rp-x) * 1%);
|
|
4726
|
+
top: calc(var(--rp-y) * 1%);
|
|
4727
|
+
height: auto;
|
|
4728
|
+
min-height: 0;
|
|
4729
|
+
padding: 2px 4px 2px 0;
|
|
4730
|
+
transform: translate(-5px, -50%);
|
|
4731
|
+
font-size: 11px;
|
|
4732
|
+
}
|
|
4733
|
+
.rp-net__node--in {
|
|
4734
|
+
transform: translate(-7px, -50%);
|
|
4735
|
+
}
|
|
4736
|
+
.rp-net__node:focus-visible {
|
|
4737
|
+
outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground));
|
|
4738
|
+
outline-offset: 2px;
|
|
4739
|
+
}
|
|
4740
|
+
.rp-net__node--sel .rp-net__label {
|
|
4741
|
+
font-weight: 600;
|
|
4742
|
+
text-decoration: underline;
|
|
4743
|
+
text-underline-offset: 2px;
|
|
4744
|
+
}
|
|
4745
|
+
.rp-net__label {
|
|
4746
|
+
max-width: 140px;
|
|
4747
|
+
overflow: hidden;
|
|
4748
|
+
text-overflow: ellipsis;
|
|
4749
|
+
white-space: nowrap;
|
|
4750
|
+
color: var(--muted-foreground);
|
|
4751
|
+
}
|
|
4752
|
+
.rp-net__node--sel .rp-net__label,
|
|
4753
|
+
.rp-net__dot--in + .rp-net__label {
|
|
4754
|
+
color: var(--foreground);
|
|
4755
|
+
}
|
|
4756
|
+
.rp-net__dot {
|
|
4757
|
+
display: inline-block;
|
|
4758
|
+
flex: none;
|
|
4759
|
+
margin-right: 4px;
|
|
4760
|
+
vertical-align: middle;
|
|
4761
|
+
width: 10px;
|
|
4762
|
+
height: 10px;
|
|
4763
|
+
border: 1px solid var(--muted-foreground);
|
|
4764
|
+
border-radius: 50%;
|
|
4765
|
+
background: var(--background);
|
|
4766
|
+
}
|
|
4767
|
+
.rp-net__dot--in {
|
|
4768
|
+
width: 14px;
|
|
4769
|
+
height: 14px;
|
|
4770
|
+
border-color: var(--foreground);
|
|
4771
|
+
background: var(--foreground);
|
|
4772
|
+
}
|
|
4773
|
+
.rp-net__dash {
|
|
4774
|
+
display: inline-block;
|
|
4775
|
+
width: 18px;
|
|
4776
|
+
border-top: 1px dashed var(--muted-foreground);
|
|
4777
|
+
}
|
|
4778
|
+
.rp-net__legend {
|
|
4779
|
+
display: flex;
|
|
4780
|
+
flex-wrap: wrap;
|
|
4781
|
+
align-items: center;
|
|
4782
|
+
gap: 16px;
|
|
4783
|
+
font-size: 12px;
|
|
4784
|
+
color: var(--muted-foreground);
|
|
4785
|
+
}
|
|
4786
|
+
.rp-net__legend > span {
|
|
4787
|
+
display: inline-flex;
|
|
4788
|
+
align-items: center;
|
|
4789
|
+
gap: 6px;
|
|
4790
|
+
}
|
|
4791
|
+
.rp-net__sel {
|
|
4792
|
+
display: flex;
|
|
4793
|
+
flex-wrap: wrap;
|
|
4794
|
+
align-items: center;
|
|
4795
|
+
gap: 12px;
|
|
4796
|
+
padding: 8px 12px;
|
|
4797
|
+
border: 1px solid var(--border);
|
|
4798
|
+
}
|
|
4799
|
+
.rp-net__sel-text {
|
|
4800
|
+
display: flex;
|
|
4801
|
+
flex: 1 1 240px;
|
|
4802
|
+
flex-direction: column;
|
|
4803
|
+
min-width: 0;
|
|
4804
|
+
font-size: 14px;
|
|
4805
|
+
}
|
|
4806
|
+
.rp-net__path {
|
|
4807
|
+
font-family: var(--mono);
|
|
4808
|
+
font-size: 12px;
|
|
4809
|
+
color: var(--muted-foreground);
|
|
4810
|
+
overflow-wrap: anywhere;
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4506
4813
|
/* People (people-directory): what waits for the PM, the list beside the chosen person's page. */
|
|
4507
4814
|
.rp-people {
|
|
4508
4815
|
display: flex;
|
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
* only the body is rebuilt when its own data changed.
|
|
7
7
|
*
|
|
8
8
|
* What it shows, for the repository the tab names (or the open one):
|
|
9
|
-
* - Docs, a search box, Needs you · All, and when the copies were last compared, with a control
|
|
10
|
-
*
|
|
9
|
+
* - Docs, a search box, Needs you · With a copy · All files, and when the copies were last compared, with a control
|
|
10
|
+
* that compares again;
|
|
11
|
+
* - the copies grouped by category, ONE line per copy: a status dot, the title, a link that opens the copy, one
|
|
11
12
|
* sentence (which side is newer, and who edited it last) and at most one button (Use PMOS version, Use Confluence
|
|
12
13
|
* version, or Choose… when both changed);
|
|
13
14
|
* - a click on a line: both sides with where they live and their last edit, a Use … version button for each, the first
|
|
14
|
-
* difference the check saw, Compare again
|
|
15
|
-
* -
|
|
15
|
+
* difference the check saw, Compare again, Stop tracking this copy, and the file's links (doc-graph.js);
|
|
16
|
+
* - All files (docs-library-graph): every repository file under folding categories, each line naming its initiative;
|
|
17
|
+
* a category opens on its first 20 files. A press on a file shows the files it links to and the files that link to
|
|
18
|
+
* it. Add copy shows on hover or focus, and always on a touch screen;
|
|
19
|
+
* - search: every file whose title, path or initiative matches, grouped by category, with the match marked.
|
|
16
20
|
* The signal is decided on the server (lib/doc-sync.js) from what a check session read. Nothing here reads or writes
|
|
17
21
|
* Google Docs or Confluence. Use … version waits five seconds behind Undo, then asks the server to start ONE doc-sync
|
|
18
22
|
* session for that copy, which writes one side with the PM's own connectors (the PM's ruling, amended 2026-10-06).
|
|
@@ -21,13 +25,20 @@
|
|
|
21
25
|
|
|
22
26
|
import { el } from './dom.js';
|
|
23
27
|
import { button, row, field, toast, empty, tabs, ensureComponentsCss } from './primitives.js';
|
|
24
|
-
import { ensureSprite } from './icons.js';
|
|
28
|
+
import { ensureSprite, icon } from './icons.js';
|
|
25
29
|
import { pmGet, pmPost } from './http.js';
|
|
26
30
|
import { wsOf } from './data.js';
|
|
27
31
|
import { registerCommand, openDestination } from './registry.js';
|
|
32
|
+
import { getLibrary, fileGraph } from './doc-graph.js';
|
|
33
|
+
import { openDoc } from './panel-initiatives.js';
|
|
28
34
|
|
|
29
35
|
const LABEL = { google: 'Google Docs', confluence: 'Confluence' };
|
|
30
36
|
const UNDO_MS = 5000;
|
|
37
|
+
const PAGE = 20;
|
|
38
|
+
/* the library's categories, in display order (lib/doc-library.js CATEGORIES) */
|
|
39
|
+
const CATS = [['spec', 'Specs'], ['ini', 'Initiatives'], ['dec', 'Decisions'], ['res', 'Research & plans'], ['exp', 'Explainers'],
|
|
40
|
+
['eval', 'Evals'], ['know', 'Knowledge'], ['guide', 'Guides & rules'], ['proto', 'Prototypes']];
|
|
41
|
+
const CAT_NAME = Object.fromEntries(CATS);
|
|
31
42
|
|
|
32
43
|
export const COPY = Object.freeze({
|
|
33
44
|
title: 'Docs',
|
|
@@ -36,20 +47,25 @@ export const COPY = Object.freeze({
|
|
|
36
47
|
loading: 'Reading…',
|
|
37
48
|
readError: 'The documents could not be read.',
|
|
38
49
|
find: 'Find a document',
|
|
50
|
+
findIn: (n) => `Find in ${n.toLocaleString('en-US')} files`,
|
|
39
51
|
needs: 'Needs you',
|
|
40
|
-
|
|
52
|
+
copies: 'With a copy',
|
|
53
|
+
files: 'All files',
|
|
54
|
+
readingFiles: 'Reading the files…',
|
|
55
|
+
filesError: 'The file list could not be read.',
|
|
56
|
+
matches: (n, all) => `${n} of ${all.toLocaleString('en-US')} files match by title, path or initiative. Press Esc to clear.`,
|
|
57
|
+
showAll: (n, cat) => `Show all ${n} in ${cat}`,
|
|
58
|
+
needYou: (n) => `${n} need${n === 1 ? 's' : ''} you`,
|
|
41
59
|
compared: (iso) => 'Compared ' + ago(iso),
|
|
42
60
|
never: 'Not compared yet',
|
|
43
61
|
comparing: 'Comparing…',
|
|
44
62
|
compareAll: 'Compare every copy again',
|
|
45
|
-
none: 'No document has a copy yet.
|
|
63
|
+
none: 'No document has a copy yet. Open All files and press Add copy on a file.',
|
|
46
64
|
nothingNeeds: 'Nothing needs you. Every copy matches PMOS.',
|
|
47
65
|
noMatch: (q) => `No document matches “${q}”.`,
|
|
48
|
-
other: 'Other',
|
|
49
66
|
usePmos: 'Use PMOS version',
|
|
50
67
|
useCopy: (label) => `Use ${label} version`,
|
|
51
68
|
choose: 'Choose…',
|
|
52
|
-
noCopy: 'No copy',
|
|
53
69
|
addCopy: 'Add copy',
|
|
54
70
|
add: 'Add',
|
|
55
71
|
cancel: 'Cancel',
|
|
@@ -91,6 +107,23 @@ function when(iso) {
|
|
|
91
107
|
if (days > 1 && days < 7) return t.toLocaleDateString('en-US', { weekday: 'short' }) + ' ' + hm;
|
|
92
108
|
return t.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
93
109
|
}
|
|
110
|
+
/** A file's last edit from its YYYY-MM-DD date: "Oct 6", or "Oct 6, 2025" in another year. */
|
|
111
|
+
function day(d) {
|
|
112
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(d || ''));
|
|
113
|
+
if (!m) return '';
|
|
114
|
+
const t = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
115
|
+
return t.toLocaleDateString('en-US', { month: 'short', day: 'numeric', ...(t.getFullYear() !== new Date().getFullYear() ? { year: 'numeric' } : {}) });
|
|
116
|
+
}
|
|
117
|
+
/** The text with every match of q marked. */
|
|
118
|
+
function marked(text, q) {
|
|
119
|
+
const t = String(text || '');
|
|
120
|
+
if (!q) return t;
|
|
121
|
+
const out = []; const low = t.toLowerCase(); let at = 0;
|
|
122
|
+
for (let k = low.indexOf(q); k >= 0 && q; k = low.indexOf(q, at)) { out.push(t.slice(at, k), el('mark', { class: 'rp-docs-mark' }, t.slice(k, k + q.length))); at = k + q.length; }
|
|
123
|
+
out.push(t.slice(at));
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
const openFile = (p) => openDoc(null, p, 'FILE');
|
|
94
127
|
const edit = (by, at) => [by, at ? when(at) : null].filter(Boolean).join(', ');
|
|
95
128
|
const keyOf = (doc, link) => `${doc.path}|${link.kind}`;
|
|
96
129
|
/** Which kind a pasted address names. The server checks it again; this only picks the kind. */
|
|
@@ -134,7 +167,8 @@ export function mountDestination(host, ctx) {
|
|
|
134
167
|
if (!inst) {
|
|
135
168
|
inst = { host, root: el('div', { class: 'rp-page rp-docs', 'data-docs': '' }), head: null, body: el('div', { class: 'rp-docs-body' }),
|
|
136
169
|
ws: null, loaded: false, error: null, view: null, filter: 'needs', q: '', files: [], open: null, adding: null,
|
|
137
|
-
pending: new Map(), busy: false, seq: 0, fseq: 0, last: '', timer: null, ftimer: null
|
|
170
|
+
pending: new Map(), busy: false, seq: 0, fseq: 0, last: '', timer: null, ftimer: null,
|
|
171
|
+
lib: null, libError: null, openCats: new Set(), full: new Set(), graph: null };
|
|
138
172
|
insts.set(host, inst);
|
|
139
173
|
}
|
|
140
174
|
ensureComponentsCss();
|
|
@@ -144,7 +178,8 @@ export function mountDestination(host, ctx) {
|
|
|
144
178
|
const ws = repo ? wsOf(repo) : null;
|
|
145
179
|
if (ws !== inst.ws) {
|
|
146
180
|
stopPoll(inst);
|
|
147
|
-
Object.assign(inst, { ws, loaded: false, error: null, view: null, last: '', open: null, adding: null, files: [] });
|
|
181
|
+
Object.assign(inst, { ws, loaded: false, error: null, view: null, last: '', open: null, adding: null, files: [], lib: null, libError: null, graph: null });
|
|
182
|
+
inst.openCats.clear(); inst.full.clear();
|
|
148
183
|
if (ws) { ensureSprite(); refresh(inst); }
|
|
149
184
|
draw(inst);
|
|
150
185
|
return;
|
|
@@ -177,8 +212,9 @@ async function refresh(inst) {
|
|
|
177
212
|
if (!ws) return;
|
|
178
213
|
const seq = ++inst.seq;
|
|
179
214
|
inst.readAt = Date.now();
|
|
180
|
-
const v = await pmGet('/api/docs?ws=' + encodeURIComponent(ws));
|
|
215
|
+
const [v, lib] = await Promise.all([pmGet('/api/docs?ws=' + encodeURIComponent(ws)), getLibrary(ws)]);
|
|
181
216
|
if (seq !== inst.seq || ws !== inst.ws) return;
|
|
217
|
+
if (lib) { inst.lib = lib; inst.libError = null; } else if (!inst.lib) inst.libError = COPY.filesError;
|
|
182
218
|
inst.loaded = true;
|
|
183
219
|
if (!v || v.ok === false) { inst.error = (v && v.error) || COPY.readError; inst.view = null; }
|
|
184
220
|
else { inst.error = null; inst.view = v; }
|
|
@@ -190,7 +226,7 @@ async function refresh(inst) {
|
|
|
190
226
|
function searchFiles(inst) {
|
|
191
227
|
if (inst.ftimer) clearTimeout(inst.ftimer);
|
|
192
228
|
const q = inst.q.trim();
|
|
193
|
-
if (!q) { inst.files = []; return; }
|
|
229
|
+
if (!q || inst.lib) { inst.files = []; return; }
|
|
194
230
|
inst.ftimer = setTimeout(async () => {
|
|
195
231
|
const seq = ++inst.fseq;
|
|
196
232
|
const r = await pmGet('/api/docs/files?ws=' + encodeURIComponent(inst.ws) + '&q=' + encodeURIComponent(q));
|
|
@@ -252,7 +288,7 @@ async function addCopy(inst, path, target, err) {
|
|
|
252
288
|
|
|
253
289
|
/* ── lines ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
254
290
|
|
|
255
|
-
function copyLine(inst, doc, link) {
|
|
291
|
+
function copyLine(inst, doc, link, q = '') {
|
|
256
292
|
const key = keyOf(doc, link);
|
|
257
293
|
const L = link.label || LABEL[link.kind];
|
|
258
294
|
const st = stateOf(inst, doc, link);
|
|
@@ -270,7 +306,7 @@ function copyLine(inst, doc, link) {
|
|
|
270
306
|
? el('a', { class: 'rp-docs-open', href: link.url, target: '_blank', rel: 'noopener noreferrer', 'data-docs-open': link.kind }, L + ' ↗')
|
|
271
307
|
: el('span', { class: 'rp-docs-open' }, L);
|
|
272
308
|
const line = row({
|
|
273
|
-
title: doc.title,
|
|
309
|
+
title: marked(doc.title, q),
|
|
274
310
|
meta: [st.strong, st.rest].filter(Boolean),
|
|
275
311
|
lead: [el('span', { class: 'rp-docs-dot rp-docs-dot--' + st.dot, 'aria-hidden': 'true' })],
|
|
276
312
|
trail: [opener, act].filter(Boolean),
|
|
@@ -304,6 +340,7 @@ function details(inst, doc, link, st) {
|
|
|
304
340
|
side('PMOS', doc.path, edit(doc.repo && doc.repo.modified_by, doc.repo && doc.repo.modified_at), pNew, 'pmos'),
|
|
305
341
|
side(L, link.target, edit(link.remote_modified_by, link.remote_modified_at), cNew, 'copy'),
|
|
306
342
|
diff,
|
|
343
|
+
inst.lib && inst.lib.byPath.has(doc.path) ? el('div', { class: 'rp-docs-links' }, fileGraph(inst.lib, doc.path, { onOpen: openFile })) : null,
|
|
307
344
|
el('div', { class: 'rp-docs-more' },
|
|
308
345
|
button({ label: COPY.compareAgain, variant: 'ghost', size: 'sm', disabled: !!(inst.view && (inst.view.open || inst.view.session)) || inst.busy,
|
|
309
346
|
attrs: { 'data-docs-act': 'compare-one' }, onClick: () => compareAll(inst) }),
|
|
@@ -311,18 +348,28 @@ function details(inst, doc, link, st) {
|
|
|
311
348
|
onClick: () => stopTracking(inst, doc, link) })));
|
|
312
349
|
}
|
|
313
350
|
|
|
314
|
-
/**
|
|
315
|
-
|
|
316
|
-
|
|
351
|
+
/**
|
|
352
|
+
* A file with no copy: its title, its initiative's short name (or its path) and the date of its last edit. A press shows
|
|
353
|
+
* its links; Add copy opens a paste field in place.
|
|
354
|
+
*/
|
|
355
|
+
function fileLine(inst, f, q = '') {
|
|
356
|
+
const start = () => { inst.adding = f.path; inst.graph = null; draw(inst); };
|
|
317
357
|
const adding = inst.adding === f.path;
|
|
358
|
+
const open = inst.graph === f.path;
|
|
359
|
+
const toggle = () => { inst.graph = open ? null : f.path; draw(inst); };
|
|
360
|
+
const ini = inst.lib && f.initiative ? inst.lib.ini(f) : null;
|
|
318
361
|
const line = row({
|
|
319
|
-
title: f.title,
|
|
320
|
-
meta: [
|
|
362
|
+
title: marked(f.title, q),
|
|
363
|
+
meta: [ini ? el('span', { class: 'rp-docs-ini', title: ini.title }, marked(ini.short, q)) : el('span', { class: 'rp-docs-path' }, marked(f.path, q)), day(f.date)],
|
|
321
364
|
lead: [el('span', { class: 'rp-docs-dot rp-docs-dot--none', 'aria-hidden': 'true' })],
|
|
322
|
-
trail: adding ? [] : [button({ label: COPY.addCopy, variant: 'outline', size: 'sm', attrs: { 'data-docs-act': 'add-copy', 'data-docs-key': f.path }, onClick: start })],
|
|
365
|
+
trail: adding ? [] : [button({ label: COPY.addCopy, variant: 'outline', size: 'sm', attrs: { class: 'rp-docs-addcopy', 'data-docs-act': 'add-copy', 'data-docs-key': f.path }, onClick: start })],
|
|
366
|
+
onOpen: inst.lib ? toggle : null,
|
|
367
|
+
openLabel: `${f.title}: show the files it links to`,
|
|
368
|
+
openAttrs: { 'aria-expanded': open ? 'true' : 'false', title: f.path, 'data-docs-file-toggle': f.path },
|
|
323
369
|
attrs: { 'data-docs-file-line': f.path, title: f.path },
|
|
324
370
|
});
|
|
325
371
|
const wrap = el('div', { class: 'rp-docs-row', role: 'listitem', 'data-docs-file': f.path }, line);
|
|
372
|
+
if (open && inst.lib) wrap.appendChild(el('div', { class: 'rp-docs-links' }, fileGraph(inst.lib, f.path, { onOpen: openFile })));
|
|
326
373
|
if (!adding) return wrap;
|
|
327
374
|
const err = el('span', { class: 'rp-docs-add__err', role: 'alert' });
|
|
328
375
|
const inp = field({ label: `Copy of ${f.title}`, hideLabel: true, placeholder: COPY.paste, attrs: { 'data-docs-field': 'target' } });
|
|
@@ -344,57 +391,110 @@ function header(inst) {
|
|
|
344
391
|
if (inst.head) return inst.head;
|
|
345
392
|
const find = field({ label: COPY.find, hideLabel: true, placeholder: COPY.find, attrs: { 'data-docs-find': '' } });
|
|
346
393
|
const q = find.control;
|
|
347
|
-
|
|
394
|
+
inst.findControl = q;
|
|
395
|
+
q.addEventListener('input', () => { inst.q = q.value; inst.open = null; inst.adding = null; inst.graph = null; inst.full.clear(); searchFiles(inst); draw(inst); });
|
|
348
396
|
// The field restores its last committed text on Escape; here Escape clears the search instead.
|
|
349
|
-
q.addEventListener('keydown', (e) => { if (e.key === 'Escape') { q.resetCommitted(''); inst.q = ''; searchFiles(inst); draw(inst); } });
|
|
397
|
+
q.addEventListener('keydown', (e) => { if (e.key === 'Escape') { q.resetCommitted(''); inst.q = ''; inst.graph = null; searchFiles(inst); draw(inst); } });
|
|
350
398
|
inst.head = el('div', { class: 'rp-docs-top' }, el('h2', { class: 'rp-head__title' }, COPY.title), el('div', { class: 'rp-docs-find' }, find));
|
|
351
399
|
return inst.head;
|
|
352
400
|
}
|
|
353
401
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
402
|
+
const needsYou = (inst, d, l) => l.signal !== 'same' || !!l.syncing || inst.pending.has(keyOf(d, l));
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* One category: its title (a fold when `foldable`), then its lines — files with a copy first, then newest first; the
|
|
406
|
+
* first 20, then Show all. `entries` are { f, doc }: f the file, doc its copies (null for a file with none).
|
|
407
|
+
*/
|
|
408
|
+
function section(inst, key, entries, { q = '', foldable = false, page = true } = {}) {
|
|
409
|
+
const name = CAT_NAME[key] || key;
|
|
410
|
+
const open = !foldable || inst.openCats.has(key);
|
|
411
|
+
const copies = entries.reduce((n, e) => n + (e.doc ? e.doc.links.length : 0), 0);
|
|
412
|
+
const needs = entries.reduce((n, e) => n + (e.doc ? e.doc.links.filter((l) => needsYou(inst, e.doc, l)).length : 0), 0);
|
|
413
|
+
const toggle = () => { if (inst.openCats.has(key)) inst.openCats.delete(key); else inst.openCats.add(key); draw(inst); };
|
|
414
|
+
const head = foldable
|
|
415
|
+
? row({ title: name, meta: [String(entries.length), needs ? COPY.needYou(needs) : null], lead: [icon(open ? 'chevron-down' : 'chevron-right')],
|
|
416
|
+
onOpen: toggle, openLabel: `${name}, ${entries.length} files${needs ? ', ' + COPY.needYou(needs) : ''}`,
|
|
417
|
+
openAttrs: { 'aria-expanded': open ? 'true' : 'false', 'data-docs-cat-toggle': key }, attrs: { class: 'rp-docs-cat', 'data-docs-cat': key, 'data-docs-count': String(entries.length) } })
|
|
418
|
+
: el('h3', { class: 'rp-docs-group__title' }, `${name} · ${entries.length}`);
|
|
419
|
+
const box = el('section', { class: 'rp-docs-group', 'data-docs-group': key }, head);
|
|
420
|
+
if (!open) return box;
|
|
421
|
+
const sorted = entries.slice().sort((a, b) => (!!b.doc - !!a.doc) || String(b.f.date || '').localeCompare(String(a.f.date || '')) || a.f.path.localeCompare(b.f.path));
|
|
422
|
+
const all = !page || inst.full.has(key);
|
|
423
|
+
const shown = all ? sorted : sorted.slice(0, PAGE);
|
|
424
|
+
const list = el('div', { role: 'list' }, shown.flatMap((e) => (e.doc ? e.doc.links.map((l) => copyLine(inst, e.doc, l, q)) : [fileLine(inst, e.f, q)])));
|
|
425
|
+
box.appendChild(list);
|
|
426
|
+
if (shown.length < sorted.length) {
|
|
427
|
+
box.appendChild(button({ label: COPY.showAll(sorted.length, name), variant: 'ghost', size: 'sm', attrs: { class: 'rp-docs-showall', 'data-docs-act': 'show-all', 'data-docs-key': key },
|
|
428
|
+
onClick: () => { inst.full.add(key); draw(inst); } }));
|
|
360
429
|
}
|
|
361
|
-
return
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
430
|
+
return box;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Entries grouped by category, in the categories' order. */
|
|
434
|
+
function byCategory(entries) {
|
|
435
|
+
const by = new Map();
|
|
436
|
+
for (const e of entries) { const k = CAT_NAME[e.f.category] ? e.f.category : 'guide'; if (!by.has(k)) by.set(k, []); by.get(k).push(e); }
|
|
437
|
+
return CATS.map(([k]) => [k, by.get(k)]).filter(([, es]) => es && es.length);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Every file: the library's, with each one's copies, plus a file with a copy the library does not list yet. */
|
|
441
|
+
function allEntries(inst) {
|
|
442
|
+
const v = inst.view;
|
|
443
|
+
const docOf = new Map(v.docs.map((d) => [d.path, d]));
|
|
444
|
+
const out = inst.lib ? inst.lib.files.map((f) => ({ f, doc: docOf.get(f.path) || null })) : [];
|
|
445
|
+
const listed = new Set(out.map((e) => e.f.path));
|
|
446
|
+
for (const d of v.docs) if (!listed.has(d.path)) out.push({ f: { path: d.path, title: d.title, category: d.category, date: null, initiative: null }, doc: d });
|
|
447
|
+
return out;
|
|
365
448
|
}
|
|
366
449
|
|
|
367
450
|
function body(inst) {
|
|
368
451
|
const v = inst.view;
|
|
369
452
|
const q = inst.q.trim().toLowerCase();
|
|
453
|
+
const lib = inst.lib;
|
|
454
|
+
if (inst.findControl) inst.findControl.placeholder = lib ? COPY.findIn(lib.files.length) : COPY.find;
|
|
370
455
|
if (q) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
456
|
+
if (!lib) {
|
|
457
|
+
const hits = v.docs.filter((d) => [d.title, d.path, d.initiative ? d.initiative.title : ''].join(' ').toLowerCase().includes(q));
|
|
458
|
+
const rows = [...hits.flatMap((d) => d.links.map((l) => copyLine(inst, d, l, q))), ...inst.files.map((f) => fileLine(inst, f, q))];
|
|
459
|
+
return rows.length ? [el('div', { role: 'list', class: 'rp-docs-group', 'data-docs-results': '' }, rows)] : [el('p', { class: 'rp-section__empty' }, COPY.noMatch(inst.q.trim()))];
|
|
460
|
+
}
|
|
461
|
+
const hay = (e) => { const ini = e.f.initiative ? lib.ini(e.f) : null; return [e.f.title, e.f.path, ini ? ini.short + ' ' + ini.title : ''].join(' ').toLowerCase(); };
|
|
462
|
+
const hits = allEntries(inst).filter((e) => hay(e).includes(q));
|
|
463
|
+
if (!hits.length) return [el('p', { class: 'rp-section__empty' }, COPY.noMatch(inst.q.trim()))];
|
|
464
|
+
return [el('p', { class: 'rp-docs-hint', 'data-docs-results': String(hits.length) }, COPY.matches(hits.length, lib.files.length)),
|
|
465
|
+
...byCategory(hits).map(([k, es]) => section(inst, k, es, { q }))];
|
|
374
466
|
}
|
|
375
467
|
const all = v.docs.reduce((n, d) => n + d.links.length, 0);
|
|
376
|
-
const needs = v.docs.reduce((n, d) => n + d.links.filter((l) =>
|
|
468
|
+
const needs = v.docs.reduce((n, d) => n + d.links.filter((l) => needsYou(inst, d, l)).length, 0);
|
|
377
469
|
const running = !!(v.open || v.session) || inst.busy;
|
|
378
470
|
const out = [el('div', { class: 'rp-docs-views' },
|
|
379
|
-
tabs({ variant: 'line', items: [{ id: 'needs', label: COPY.needs, count: needs }, { id: '
|
|
380
|
-
|
|
471
|
+
tabs({ variant: 'line', items: [{ id: 'needs', label: COPY.needs, count: needs }, { id: 'copies', label: COPY.copies, count: all },
|
|
472
|
+
{ id: 'files', label: COPY.files, count: lib ? lib.files.length.toLocaleString('en-US') : null }],
|
|
473
|
+
value: inst.filter, ariaLabel: 'Filter', attrs: { 'data-docs-filter': '' }, onChange: (val) => { inst.filter = val; inst.graph = null; draw(inst); } }),
|
|
381
474
|
el('span', { class: 'rp-docs-when', 'data-docs-summary': '' },
|
|
382
475
|
running ? COPY.comparing : (v.last ? COPY.compared(v.last.finished_at) : COPY.never),
|
|
383
476
|
button({ label: '↻', variant: 'ghost', size: 'sm', disabled: running || !all,
|
|
384
477
|
attrs: { 'data-docs-act': 'check', 'aria-label': COPY.compareAll, title: COPY.compareAll }, onClick: () => compareAll(inst) })))];
|
|
478
|
+
if (inst.filter === 'files') {
|
|
479
|
+
if (!lib) { out.push(el('p', { class: 'rp-section__empty' + (inst.libError ? ' rp-section__empty--bad' : '') }, inst.libError || COPY.readingFiles)); return out; }
|
|
480
|
+
out.push(el('div', { class: 'rp-docs-folds' }, byCategory(allEntries(inst)).map(([k, es]) => section(inst, k, es, { foldable: true }))));
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
385
483
|
if (!v.docs.length) { out.push(el('p', { class: 'rp-section__empty' }, COPY.none)); return out; }
|
|
386
|
-
const
|
|
387
|
-
? v.docs.map((d) => ({ ...d, links: d.links.filter((l) =>
|
|
484
|
+
const docs = inst.filter === 'needs'
|
|
485
|
+
? v.docs.map((d) => ({ ...d, links: d.links.filter((l) => needsYou(inst, d, l)) })).filter((d) => d.links.length)
|
|
388
486
|
: v.docs;
|
|
389
|
-
if (!
|
|
390
|
-
|
|
487
|
+
if (!docs.length) { out.push(el('p', { class: 'rp-section__empty' }, COPY.nothingNeeds)); return out; }
|
|
488
|
+
const libFile = (d) => (lib && lib.byPath.has(d.path) ? lib.files[lib.byPath.get(d.path)] : { path: d.path, title: d.title, category: d.category, date: null });
|
|
489
|
+
out.push(...byCategory(docs.map((d) => ({ f: libFile(d), doc: d }))).map(([k, es]) => section(inst, k, es, { page: false })));
|
|
391
490
|
return out;
|
|
392
491
|
}
|
|
393
492
|
|
|
394
493
|
function draw(inst) {
|
|
395
494
|
const root = inst.root;
|
|
396
495
|
if (!inst.ws) { root.replaceChildren(empty({ text: COPY.noRepo })); inst.last = 'none'; return; }
|
|
397
|
-
const fp = JSON.stringify([inst.loaded, inst.error, inst.view, inst.filter, inst.busy, inst.q, inst.files, inst.open, inst.adding, [...inst.pending.keys()]
|
|
496
|
+
const fp = JSON.stringify([inst.loaded, inst.error, inst.view, inst.filter, inst.busy, inst.q, inst.files, inst.open, inst.adding, [...inst.pending.keys()],
|
|
497
|
+
inst.lib ? inst.lib.key : null, inst.libError, [...inst.openCats], [...inst.full], inst.graph]);
|
|
398
498
|
if (fp === inst.last && root.childNodes.length && root.contains(inst.body)) return;
|
|
399
499
|
inst.last = fp;
|
|
400
500
|
const head = header(inst);
|
|
@@ -143,14 +143,14 @@ function build(ctx0) {
|
|
|
143
143
|
const dayWs = dayRepo ? wsOf(dayRepo) : null;
|
|
144
144
|
const prep = onPhone && !(window.pmDeck && window.pmDeck.phoneView('today') === 'day') ? prepCard(dayWs) : null;
|
|
145
145
|
if (prep) { prep.setAttribute('role', 'listitem'); const none = list.querySelector('[data-today="empty"]'); if (none) none.setAttribute('data-today-id', 'empty'); list.prepend(prep); } // the empty note stays, as the deck's last card
|
|
146
|
-
// the open day takes the
|
|
147
|
-
// leaves it below, as before
|
|
148
|
-
const owed = { list, more, count: n };
|
|
146
|
+
// the open day takes the cards into its jobs (rows of Needs you; a pressed row shows its card), so Today shows one layout; a folded day
|
|
147
|
+
// leaves it below, as before. A phone does not draw the day, so its deck keeps every card
|
|
148
|
+
const owed = onPhone ? null : { list, more, count: n };
|
|
149
149
|
// one day for every repository: the day is the same everywhere; the Show switch narrows it to the repository Today
|
|
150
150
|
// was opened from (`here`) or shows all of it
|
|
151
151
|
const here = sc.repo ? wsOf(sc.repo) : null;
|
|
152
152
|
const day = daySection({ ws: dayWs, setState: ctx0.setState || setState, openIni: dayRepo ? (id) => openInitiative(dayRepo, id) : null, owed, here, all: sc.all });
|
|
153
|
-
const inDay = !!(day &&
|
|
153
|
+
const inDay = !!(day && owed && owed.taken);
|
|
154
154
|
return { children: inDay ? [head, day, health] : [head, day, health, list, more], sig: JSON.stringify([sc.all, sc.repo, n, ready, expanded, banner, onPhone, prep ? prep.outerHTML : null]) + daySig(dayWs) + nodes.map((x) => x.outerHTML).join('') };
|
|
155
155
|
}
|
|
156
156
|
|