create-pmos 0.76.0 → 0.77.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/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/layout.md +10 -11
- package/workspace/lib/mobile-nav.js +9 -2
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/index.html +14 -20
- package/workspace/public/pm/components.css +12 -8
- package/workspace/public/pm/mobile.css +77 -93
- package/workspace/public/pm/mobile.js +6 -2
- package/workspace/public/pm/phone-deck.js +67 -143
- package/workspace/public/vendor/lucide-sprite.js +9 -2
- package/workspace/third-party/README.md +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.77.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T10:24:50Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T10:24:50Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T10:24:50Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: layout"
|
|
4
|
-
description: "The frame: one top bar, the place on screen, the detail and the agent dock; the phone tabs and
|
|
4
|
+
description: "The frame: one top bar, the place on screen, the detail and the agent dock; the phone tabs and lists; the keys; and the older layout."
|
|
5
5
|
timestamp: 2026-10-07
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -119,20 +119,19 @@ A key that you type in a field, or in a terminal, goes to the field or the termi
|
|
|
119
119
|
|
|
120
120
|
## On a phone
|
|
121
121
|
|
|
122
|
-
A phone shows one panel at a time. The tabs at the bottom are **Agenda, Initiatives, Board,
|
|
122
|
+
A phone shows one panel at a time. The tabs at the bottom are **Agenda, Initiatives, Board, Docs and Agent**. Each tab has an icon and a short name. The Agenda tab shows how many things need you. Agent shows the dock, with your terminals. Reports is in the Board tab.
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
Where a tab has a list, the list goes from top to bottom. Only the list scrolls. The bar at the top, the line above the list and the tabs at the bottom stay in place. The line above the list shows how many items the list has, for example "5 need you".
|
|
125
125
|
|
|
126
|
-
Some tabs have a second view.
|
|
126
|
+
Some tabs have a second view. Use the two buttons at the right of the line above the list to change the view:
|
|
127
127
|
|
|
128
|
-
| Tab | The
|
|
128
|
+
| Tab | The list | The other view | Not on a phone |
|
|
129
129
|
| --- | --- | --- | --- |
|
|
130
|
-
| Agenda | **
|
|
131
|
-
| Initiatives | **
|
|
132
|
-
| Board |
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
| Agent | No deck. Talk to a session. | None. | The scratchpad bar when no terminal is in front. |
|
|
130
|
+
| Agenda | **Needs you**: one card for each thing that needs you. When your day is not prepared yet, the first card is **Prepare your day**. Press **Prepare my day** to start it, or **See the day** to open My day. | **My day**: your calendar, your goals and the jobs the agent does for the day. | The Repository / All repositories switch (use Repository in the bar). |
|
|
131
|
+
| Initiatives | **Roadmap**: the initiatives in Now, then Next, then Later. | **All**: the list of every initiative. Press one to open it. | The title, New initiative, the count line and the drag grips. |
|
|
132
|
+
| Board | **Board**: the items in the column you choose. Choose the initiative in the menu at the top, and the column with the column buttons. | **Reports**: read a report, or draft one. The Board tab stays lit. | The item count, the report title and Report schedules. |
|
|
133
|
+
| Docs | **Out of sync**: one row for each document that needs you. | **All**: every document, in its groups. | The title. |
|
|
134
|
+
| Agent | Talk to a session. | None. | The scratchpad bar when no terminal is in front. |
|
|
136
135
|
|
|
137
136
|
When you open a terminal, the terminal fills the screen. The tabs at the bottom are not shown, so the output has the most space. The control keys stay at the bottom. Press Back to return to the tabs.
|
|
138
137
|
|
|
@@ -72,6 +72,13 @@ const DETAIL_TITLE = {
|
|
|
72
72
|
* is not a tab: it is pushed on top of the current tab as a `dest` screen with id `settings`, so Back returns.
|
|
73
73
|
* The old cockpit's list modes still map to a tab (MOBILE_DEST), for the old shell (fixture mode, ?layout=legacy). */
|
|
74
74
|
const MOBILE_DESTS = ['today', 'initiatives', 'board', 'reports', 'docs', 'agent'];
|
|
75
|
+
/* The bottom bar draws FIVE of them (workspace-phone-scroll-layout, 2026-10-07): an icon and a short word each, the
|
|
76
|
+
* pattern most phone apps use. Reports is still a destination with its own root, but it has no button of its own: it is
|
|
77
|
+
* the second view of the Board tab, switched from the head above the list. MOBILE_TAB_OF says which button a
|
|
78
|
+
* destination lights; a destination absent from it lights its own. */
|
|
79
|
+
const MOBILE_TABS = ['today', 'initiatives', 'board', 'docs', 'agent'];
|
|
80
|
+
const MOBILE_TAB_OF = { reports: 'board' };
|
|
81
|
+
function mobTabOf(dest) { return MOBILE_TAB_OF[dest] || dest; }
|
|
75
82
|
// What each tab and Settings is called. The same words as the desktop bar (public/pm/frame-copy.js COPY.nav), which
|
|
76
83
|
// MOBNAV-P7 holds equal.
|
|
77
84
|
const DEST_TITLE = { today: 'Agenda', initiatives: 'Initiatives', board: 'Board', reports: 'Reports', docs: 'Docs', agent: 'Agent', settings: 'Settings' };
|
|
@@ -154,9 +161,9 @@ function mobMigrateView(v) {
|
|
|
154
161
|
|
|
155
162
|
return { push: mobPush, pop: mobPop, switchDest: mobSwitchDest, init: mobNavInit,
|
|
156
163
|
current: mobCurrentOf, titleOf: mobTitleOf, columnOf: mobColumnOf, panelOf: mobPanelOf,
|
|
157
|
-
destForMode: mobDestForMode,
|
|
164
|
+
destForMode: mobDestForMode, tabOf: mobTabOf,
|
|
158
165
|
MODE_TITLE,
|
|
159
166
|
migrateView: mobMigrateView, screen: mobScreen, root: mobRootOf,
|
|
160
|
-
DESTS: MOBILE_DESTS, DEST_FOR_MODE: MOBILE_DEST, ROOTS: MOBILE_ROOT,
|
|
167
|
+
DESTS: MOBILE_DESTS, TABS: MOBILE_TABS, TAB_OF: MOBILE_TAB_OF, DEST_FOR_MODE: MOBILE_DEST, ROOTS: MOBILE_ROOT,
|
|
161
168
|
PANEL_OF_KIND: MOBILE_PANEL, DETAIL_TITLE, DEST_TITLE, LEGACY_DEST };
|
|
162
169
|
});
|
package/workspace/package.json
CHANGED
|
@@ -114,9 +114,9 @@
|
|
|
114
114
|
<button class="mob-sync" id="mobSync" type="button" hidden>Sync</button>
|
|
115
115
|
<button class="mob-set" id="mobSettings" type="button" hidden aria-current="false" aria-label="Settings"><svg class="pmc-icon" width="18" height="18" aria-hidden="true" focusable="false" data-icon="settings"><use href="#lucide-settings"></use></svg></button>
|
|
116
116
|
</header>
|
|
117
|
-
<!-- The
|
|
118
|
-
second view, the switch to it (
|
|
119
|
-
|
|
117
|
+
<!-- The list head (pm/phone-deck.js): at a tab's root, how many things the list holds and, where the tab has a
|
|
118
|
+
second view, the switch to it (Agenda: Needs you or My day; Initiatives: Roadmap or All; Board: Board or Reports;
|
|
119
|
+
Docs: Out of sync or All). The list under it scrolls on its own; the head and the tabs stay put. -->
|
|
120
120
|
<div class="mob-deck" id="mobDeck" hidden>
|
|
121
121
|
<span class="mob-deck__where" id="mobDeckWhere" aria-live="polite"></span>
|
|
122
122
|
<span class="mob-deck__views" id="mobDeckViews" role="group" aria-label="View"></span>
|
|
@@ -124,11 +124,12 @@
|
|
|
124
124
|
</div>
|
|
125
125
|
|
|
126
126
|
<!-- The bottom block: a terminal control-key accessory bar (terminal screen only — phones have no
|
|
127
|
-
Esc/Tab/Ctrl/arrows)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
Esc/Tab/Ctrl/arrows) and the five tabs (workspace-phone-scroll-layout): Agenda · Initiatives · Board · Docs · Agent,
|
|
128
|
+
each an icon over a short word. Reports has no tab of its own: it is the Board tab's second view, switched from the
|
|
129
|
+
head above the list, and Board stays lit while it is shown. Agent is the dock with its live terminals. On a terminal
|
|
130
|
+
the tabs are not drawn: the session takes the screen, and Back returns. The nav is a <nav> of buttons carrying
|
|
131
|
+
aria-current, NOT a tablist: there is no tabpanel to point at. Selecting a tab resets it to its root; depth is pushed
|
|
132
|
+
on top of it and #mobBack pops one screen. -->
|
|
132
133
|
<div class="mobmenu" id="mobMenu" hidden>
|
|
133
134
|
<div class="ctrlbar" id="ctrlBar" role="toolbar" aria-label="Terminal control keys">
|
|
134
135
|
<button type="button" data-seq="esc" aria-label="Escape"><span class="lbl">esc</span></button>
|
|
@@ -139,19 +140,12 @@
|
|
|
139
140
|
<button type="button" data-seq="down" aria-label="Down arrow"><svg class="pmc-icon" width="16" height="16" aria-hidden="true" focusable="false" data-icon="arrow-down"><use href="#lucide-arrow-down"></use></svg></button>
|
|
140
141
|
<button type="button" data-seq="right" aria-label="Right arrow"><svg class="pmc-icon" width="16" height="16" aria-hidden="true" focusable="false" data-icon="arrow-right"><use href="#lucide-arrow-right"></use></svg></button>
|
|
141
142
|
</div>
|
|
142
|
-
<!-- The deck's pager: the previous card, where this one is in the deck, the next card. A swipe does the same. -->
|
|
143
|
-
<div class="mob-pager" id="mobPager" hidden>
|
|
144
|
-
<button type="button" class="mob-pager__b" id="mobPrev" aria-label="Previous card"><svg class="pmc-icon" width="18" height="18" aria-hidden="true" focusable="false" data-icon="chevron-left"><use href="#lucide-chevron-left"></use></svg></button>
|
|
145
|
-
<span class="mob-pager__track" id="mobPagerTrack" aria-hidden="true"><span class="mob-pager__fill" id="mobPagerFill"></span></span>
|
|
146
|
-
<button type="button" class="mob-pager__b" id="mobNext" aria-label="Next card"><svg class="pmc-icon" width="18" height="18" aria-hidden="true" focusable="false" data-icon="chevron-right"><use href="#lucide-chevron-right"></use></svg></button>
|
|
147
|
-
</div>
|
|
148
143
|
<nav class="mobnav" id="mobNav" aria-label="Destinations">
|
|
149
|
-
<button type="button" class="mobnav-b" data-dest="today" aria-current="page"><span class="l">Agenda</span><span class="mobnav-n" id="mobTodayCount" hidden></span></button>
|
|
150
|
-
<button type="button" class="mobnav-b" data-dest="initiatives" aria-current="false"><span class="l">Initiatives</span></button>
|
|
151
|
-
<button type="button" class="mobnav-b" data-dest="board" aria-current="false"><span class="l">Board</span></button>
|
|
152
|
-
<button type="button" class="mobnav-b" data-dest="
|
|
153
|
-
<button type="button" class="mobnav-b" data-dest="
|
|
154
|
-
<button type="button" class="mobnav-b" data-dest="agent" aria-current="false"><span class="l">Agent</span></button>
|
|
144
|
+
<button type="button" class="mobnav-b" data-dest="today" aria-current="page"><svg class="pmc-icon mobnav-i" width="22" height="22" aria-hidden="true" focusable="false" data-icon="house"><use href="#lucide-house"></use></svg><span class="l">Agenda</span><span class="mobnav-n" id="mobTodayCount" hidden></span></button>
|
|
145
|
+
<button type="button" class="mobnav-b" data-dest="initiatives" aria-current="false"><svg class="pmc-icon mobnav-i" width="22" height="22" aria-hidden="true" focusable="false" data-icon="target"><use href="#lucide-target"></use></svg><span class="l">Initiatives</span></button>
|
|
146
|
+
<button type="button" class="mobnav-b" data-dest="board" aria-current="false"><svg class="pmc-icon mobnav-i" width="22" height="22" aria-hidden="true" focusable="false" data-icon="square-kanban"><use href="#lucide-square-kanban"></use></svg><span class="l">Board</span></button>
|
|
147
|
+
<button type="button" class="mobnav-b" data-dest="docs" aria-current="false"><svg class="pmc-icon mobnav-i" width="22" height="22" aria-hidden="true" focusable="false" data-icon="file-text"><use href="#lucide-file-text"></use></svg><span class="l">Docs</span></button>
|
|
148
|
+
<button type="button" class="mobnav-b" data-dest="agent" aria-current="false"><svg class="pmc-icon mobnav-i" width="22" height="22" aria-hidden="true" focusable="false" data-icon="square-terminal"><use href="#lucide-square-terminal"></use></svg><span class="l">Agent</span></button>
|
|
155
149
|
</nav>
|
|
156
150
|
</div>
|
|
157
151
|
|
|
@@ -504,14 +504,18 @@
|
|
|
504
504
|
.pmc-card--muted .pmc-card__title {
|
|
505
505
|
color: var(--muted-foreground);
|
|
506
506
|
}
|
|
507
|
-
/* card
|
|
508
|
-
*
|
|
509
|
-
|
|
510
|
-
.pmc-card
|
|
511
|
-
.pmc-card
|
|
512
|
-
.pmc-card
|
|
513
|
-
.pmc-card
|
|
514
|
-
.pmc-card
|
|
507
|
+
/* card on a phone (≤760px), in the Initiatives and Board places only: a row in one vertical list (workspace-phone-scroll-layout). One open target as tall as what
|
|
508
|
+
* it says, with a 64px floor so short and long cards line up; the title and its line under it at reading size. */
|
|
509
|
+
@media (max-width: 760px) {
|
|
510
|
+
.pmc-card:where([data-fr-host="initiatives"] *) { min-height: 64px; padding: 12px 14px; justify-content: center; }
|
|
511
|
+
.pmc-card:where([data-fr-host="initiatives"] *) .pmc-card__title { font-size: 16px; line-height: 1.3; font-weight: 600; }
|
|
512
|
+
.pmc-card:where([data-fr-host="initiatives"] *) .pmc-card__meta { font-size: 14px; line-height: 1.4; }
|
|
513
|
+
.pmc-card:where([data-fr-host="initiatives"] *) .pmc-card__open { gap: 4px; }
|
|
514
|
+
.pmc-card:where([data-fr-host="board"] *) { min-height: 64px; padding: 12px 14px; justify-content: center; }
|
|
515
|
+
.pmc-card:where([data-fr-host="board"] *) .pmc-card__title { font-size: 16px; line-height: 1.3; font-weight: 600; }
|
|
516
|
+
.pmc-card:where([data-fr-host="board"] *) .pmc-card__meta { font-size: 14px; line-height: 1.4; }
|
|
517
|
+
.pmc-card:where([data-fr-host="board"] *) .pmc-card__open { gap: 4px; }
|
|
518
|
+
}
|
|
515
519
|
.pmc-card:focus-visible,
|
|
516
520
|
.pmc-card:has(.pmc-card__open:focus-visible) {
|
|
517
521
|
outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground));
|
|
@@ -202,43 +202,57 @@
|
|
|
202
202
|
}
|
|
203
203
|
.ctrlbar .lbl { font-size: 11px; letter-spacing: .04em; }
|
|
204
204
|
|
|
205
|
-
/*
|
|
206
|
-
|
|
205
|
+
/* Five tabs, one per column: an icon over a short word, the bar most phone apps draw (workspace-phone-scroll-layout).
|
|
206
|
+
* The tab on screen is drawn in the foreground ink, with a heavier icon stroke and a 2px rule along its top edge. The
|
|
207
|
+
* Agenda tab carries the count of what needs the PM beside its icon. */
|
|
208
|
+
.mobnav { display: grid; grid-template-columns: repeat(5, 1fr); min-width: 0; }
|
|
207
209
|
.mobnav-b {
|
|
210
|
+
position: relative;
|
|
208
211
|
display: flex;
|
|
209
212
|
flex-direction: column;
|
|
210
213
|
align-items: center;
|
|
211
214
|
justify-content: center;
|
|
212
|
-
gap:
|
|
215
|
+
gap: 4px;
|
|
213
216
|
min-width: 0;
|
|
214
|
-
min-height:
|
|
215
|
-
padding:
|
|
217
|
+
min-height: 60px;
|
|
218
|
+
padding: 6px 2px 4px;
|
|
216
219
|
color: var(--muted-foreground);
|
|
220
|
+
font-family: var(--sans);
|
|
217
221
|
}
|
|
218
|
-
.mobnav-b .
|
|
219
|
-
.mobnav-b .l { font-size:
|
|
222
|
+
.mobnav-b .mobnav-i { width: 22px; height: 22px; stroke-width: 1.75; }
|
|
223
|
+
.mobnav-b .l { font-size: 11px; line-height: 1; font-weight: 500; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
220
224
|
.mobnav-b[aria-current="page"] { color: var(--foreground); }
|
|
221
|
-
|
|
222
|
-
.mobnav-b {
|
|
223
|
-
.mobnav-
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
.mobnav-b[aria-current="page"] .mobnav-i { stroke-width: 2.25; }
|
|
226
|
+
.mobnav-b[aria-current="page"]::before { content: ""; position: absolute; top: 0; left: 25%; right: 25%; height: 2px; background: var(--foreground); }
|
|
227
|
+
.mobnav-n {
|
|
228
|
+
position: absolute;
|
|
229
|
+
top: 6px;
|
|
230
|
+
left: calc(50% + 8px);
|
|
231
|
+
min-width: 18px;
|
|
232
|
+
padding: 2px 4px;
|
|
233
|
+
font-family: var(--mono);
|
|
234
|
+
font-size: 11px;
|
|
235
|
+
line-height: 1;
|
|
236
|
+
text-align: center;
|
|
237
|
+
background: var(--destructive);
|
|
238
|
+
color: var(--background);
|
|
239
|
+
}
|
|
226
240
|
|
|
227
241
|
/* ── one job per screen, one card at a time ────────────────────────────────────────────────────
|
|
228
|
-
* A phone is not a small desktop. At a tab's root the tab's
|
|
229
|
-
* (pm/phone-deck.js)
|
|
230
|
-
* work are not drawn. Nothing below removes a capability: each hidden control is a desktop one, or the
|
|
242
|
+
* A phone is not a small desktop. At a tab's root the tab's list runs top to bottom and only the list scrolls; the
|
|
243
|
+
* head above it (pm/phone-deck.js) says how many things it holds and switches the tab's second view. The controls that
|
|
244
|
+
* serve desk work are not drawn. Nothing below removes a capability: each hidden control is a desktop one, or the head's
|
|
231
245
|
* view switch, the bar's Repository menu or ⌘K (the bar's search) reaches the same act. The nodes stay in the DOM,
|
|
232
246
|
* so the renderers are the desktop's, unchanged (see pm/mobile.js, "ONE renderer, two shells").
|
|
233
247
|
*
|
|
234
|
-
* Today the Today cards. The count is in the
|
|
235
|
-
* All repositories).
|
|
248
|
+
* Today the Today cards. The count is in the head; the scope is the bar's Repository menu (it has
|
|
249
|
+
* All repositories). My day, the head's second view, is the day section: goals, calendar, jobs.
|
|
236
250
|
* Initiatives the Roadmap's cards. No title, view switch, New initiative, count line, hint or drag grips: the
|
|
237
|
-
*
|
|
251
|
+
* head's All is the List, ⌘K has New initiative, and a card's ⋯ moves it.
|
|
238
252
|
* Board the picked initiative's cards in the column on screen: Active or Mine, the initiative picker and
|
|
239
253
|
* the column switch stay. No count, and not a second "no item" note above the column's own.
|
|
240
254
|
* Reports read a report, or draft one: no title (the tab names it) and no schedule set-up.
|
|
241
|
-
* Docs the copies that need you, one
|
|
255
|
+
* Docs the copies that need you, one row each. All, the head's second view, lists every copy and
|
|
242
256
|
* has the search.
|
|
243
257
|
* An initiative (the sheet) act on its next step: the stepper and the waiting line lead. The Breakdown tab shows
|
|
244
258
|
* the Tree only (the Board tab is the board) and not the starter list, which is authoring. */
|
|
@@ -261,95 +275,65 @@
|
|
|
261
275
|
[data-in-sheet] .bd-head > [data-bd-view],
|
|
262
276
|
[data-in-sheet] [data-bd-starters] { display: none !important; }
|
|
263
277
|
|
|
264
|
-
/* ── the
|
|
265
|
-
|
|
278
|
+
/* ── the list head (pm/phone-deck.js) ───────────────────────────────────────────────────────────
|
|
279
|
+
* One line above the list: how many things it holds, then the tab's two views as a segmented switch, each button an
|
|
280
|
+
* icon and a word. It sits in the phone's top block, so it stays put while the list scrolls under it. */
|
|
266
281
|
.mob-deck {
|
|
267
282
|
display: flex;
|
|
268
283
|
align-items: center;
|
|
269
284
|
justify-content: space-between;
|
|
270
|
-
gap:
|
|
271
|
-
min-height:
|
|
272
|
-
padding: 0
|
|
285
|
+
gap: 12px;
|
|
286
|
+
min-height: 52px;
|
|
287
|
+
padding: 0 12px 0 16px;
|
|
273
288
|
background: var(--background);
|
|
274
289
|
border-bottom: 1px solid var(--border);
|
|
275
|
-
font-family: var(--
|
|
276
|
-
font-size:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
color: var(--muted-foreground);
|
|
290
|
+
font-family: var(--sans);
|
|
291
|
+
font-size: 14px;
|
|
292
|
+
font-weight: 600;
|
|
293
|
+
color: var(--foreground);
|
|
280
294
|
}
|
|
281
295
|
.mob-deck__where { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
282
|
-
.mob-deck__views { display: inline-flex; flex: none;
|
|
296
|
+
.mob-deck__views { display: inline-flex; flex: none; gap: 2px; padding: 2px; background: var(--muted); }
|
|
283
297
|
.mob-deck__view {
|
|
298
|
+
display: inline-flex;
|
|
299
|
+
align-items: center;
|
|
300
|
+
gap: 6px;
|
|
284
301
|
min-width: 44px;
|
|
285
302
|
min-height: 44px;
|
|
286
303
|
padding: 0 12px;
|
|
287
|
-
font:
|
|
288
|
-
|
|
289
|
-
|
|
304
|
+
font-family: var(--sans);
|
|
305
|
+
font-size: 14px;
|
|
306
|
+
font-weight: 500;
|
|
290
307
|
color: var(--muted-foreground);
|
|
291
308
|
}
|
|
292
|
-
.mob-deck__view
|
|
293
|
-
|
|
294
|
-
.mob-pager { display: grid; grid-template-columns: 48px minmax(0, 1fr) 48px; align-items: center; border-bottom: 1px solid var(--border); }
|
|
295
|
-
.mob-pager__b { display: grid; place-items: center; min-width: 48px; min-height: 44px; color: var(--foreground); }
|
|
296
|
-
.mob-pager__b:disabled { opacity: .3; }
|
|
297
|
-
.mob-pager__track { height: 3px; background: var(--border); }
|
|
298
|
-
.mob-pager__fill { display: block; height: 100%; width: var(--fr-deck-fill, 0); background: var(--foreground); transition: width .2s ease-out; }
|
|
309
|
+
.mob-deck__view .mob-deck__i { width: 16px; height: 16px; }
|
|
310
|
+
.mob-deck__view[aria-pressed="true"] { background: var(--card); color: var(--foreground); }
|
|
299
311
|
|
|
300
|
-
/*
|
|
301
|
-
*
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
body[data-ph-view="cards"] .ph-deck .rp-docs-group,
|
|
320
|
-
body[data-ph-view="cards"] .ph-deck .rp-docs-group > [role="list"] { display: contents !important; }
|
|
321
|
-
body[data-ph-view="cards"] .ph-deck .in-col__head,
|
|
322
|
-
body[data-ph-view="cards"] .ph-deck .in-col__empty,
|
|
323
|
-
body[data-ph-view="cards"] .ph-deck .in-col > :not(.in-list),
|
|
324
|
-
body[data-ph-view="cards"] .ph-deck .in-list--dormant { display: none !important; }
|
|
325
|
-
body[data-ph-view="cards"] .ph-deck > .rp-section__empty { flex: 0 0 100%; }
|
|
326
|
-
body[data-ph-view="cards"] .ph-card {
|
|
327
|
-
flex: 0 0 100% !important;
|
|
328
|
-
width: 100% !important;
|
|
329
|
-
max-width: none !important;
|
|
330
|
-
height: var(--fr-deck-h, 60vh);
|
|
331
|
-
margin: 0 !important;
|
|
332
|
-
overflow-y: auto;
|
|
333
|
-
scroll-snap-align: start;
|
|
334
|
-
scroll-snap-stop: always;
|
|
335
|
-
display: flex !important;
|
|
336
|
-
flex-direction: column;
|
|
337
|
-
background: var(--card);
|
|
338
|
-
border: 1px solid var(--border);
|
|
312
|
+
/* ── the list: one scroller, rows sized to what they hold ───────────────────────────────────────
|
|
313
|
+
* The place is the only thing that scrolls. It does not hand a drag at its top or bottom to the page, so the bars never
|
|
314
|
+
* move and nothing empty is dragged along. Rows take the height of what they say, with one floor, so a short row and a
|
|
315
|
+
* long one line up as a column. */
|
|
316
|
+
[data-fr="main"] > .fr-dest { overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
|
|
317
|
+
html, body { overscroll-behavior: none; }
|
|
318
|
+
[data-dest="today"] .td-board { gap: 8px; padding: 12px; }
|
|
319
|
+
[data-dest="today"] .td-card { min-height: 64px; padding: 14px; gap: 10px; }
|
|
320
|
+
[data-dest="today"] .td-card--host { padding: 0; }
|
|
321
|
+
[data-dest="today"] .td-card__top { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
|
|
322
|
+
[data-dest="today"] .td-card__cause { font-size: 16px; line-height: 1.3; font-weight: 600; align-items: flex-start; }
|
|
323
|
+
[data-dest="today"] .td-card__evidence {
|
|
324
|
+
padding-left: 0;
|
|
325
|
+
font-size: 14px;
|
|
326
|
+
line-height: 1.4;
|
|
327
|
+
display: -webkit-box;
|
|
328
|
+
-webkit-line-clamp: 2;
|
|
329
|
+
-webkit-box-orient: vertical;
|
|
330
|
+
overflow: hidden;
|
|
339
331
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
body[data-ph-view="cards"] .ph-card .td-card__evidence { white-space: normal; overflow: visible; font-size: 14px; line-height: 1.45; }
|
|
346
|
-
body[data-ph-view="cards"] .ph-card .td-card__acts { display: grid !important; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; width: 100%; margin: 0; }
|
|
347
|
-
body[data-ph-view="cards"] .ph-card .td-card__acts > :first-child { width: 100%; }
|
|
348
|
-
body[data-ph-view="cards"] .ph-card .td-card__acts > * { min-height: 48px; justify-content: center; }
|
|
349
|
-
body[data-ph-view="cards"] .ph-card.td-card--prep .td-card__acts { grid-template-columns: minmax(0, 1fr); }
|
|
350
|
-
/* An initiative or a Board item: a .pmc-card primitive, so its deck size lives in components.css (.pmc-card.ph-card). */
|
|
351
|
-
/* A copy that differs: the row's sentence and its one button. */
|
|
352
|
-
body[data-ph-view="cards"] .ph-card.rp-docs-row { padding: 6px 4px; }
|
|
332
|
+
[data-dest="today"] .td-card__acts { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; width: 100%; margin: 0; }
|
|
333
|
+
[data-dest="today"] .td-card__acts > * { min-height: 44px; justify-content: center; }
|
|
334
|
+
[data-dest="today"] .td-card--prep .td-card__acts { grid-template-columns: minmax(0, 1fr); }
|
|
335
|
+
/* An initiative or a Board item is a .pmc-card primitive: its phone size lives in components.css's primitives section. */
|
|
336
|
+
[data-dest="docs"] .rp-docs-row { min-height: 56px; }
|
|
353
337
|
|
|
354
338
|
/* ── the terminal takes the screen ────────────────────────────────────────────────────────────
|
|
355
339
|
* The PM's word: entering a terminal switches the whole screen to it, because it is mostly output. The tabs are not
|
|
@@ -363,7 +347,7 @@
|
|
|
363
347
|
* the thing this workspace refuses to ship. */
|
|
364
348
|
.mob[hidden], .mobmenu[hidden], .mob-back[hidden], .mob-brand[hidden] { display: none !important; }
|
|
365
349
|
.mob-repo[hidden], .mob-set[hidden], .mob-sync[hidden], .mob-theme[hidden], .mobnav-n[hidden] { display: none !important; }
|
|
366
|
-
.mob-deck[hidden], .mob-
|
|
350
|
+
.mob-deck[hidden], .mob-deck__views[hidden] { display: none !important; }
|
|
367
351
|
|
|
368
352
|
/* ── a touch screen: the visible height, no page bounce, no zoom on focus ─────────────────────
|
|
369
353
|
* pm/mobile.js sets `pm-vv` and `--fr-vvh` (the visual viewport's height) on a touch screen. The
|
|
@@ -387,7 +387,9 @@ export function applyMobile() {
|
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
const buttons = menu ? menu.querySelectorAll('#mobNav [data-dest]') : [];
|
|
390
|
-
|
|
390
|
+
// Reports has no tab of its own: it is the Board tab's second view, so Board stays lit (lib/mobile-nav.js TAB_OF).
|
|
391
|
+
const lit = typeof NAV.tabOf === 'function' ? NAV.tabOf(nav.dest) : nav.dest;
|
|
392
|
+
for (const b of buttons) b.setAttribute('aria-current', b.dataset.dest === lit ? 'page' : 'false');
|
|
391
393
|
|
|
392
394
|
if (s.kind !== 'terminal' && s.kind !== 'agent' && ctrlArmed) setCtrlArmed(false);
|
|
393
395
|
|
|
@@ -544,8 +546,9 @@ export function mountMobile(context) {
|
|
|
544
546
|
document.addEventListener('click', onChromeClick);
|
|
545
547
|
|
|
546
548
|
if (window.visualViewport) {
|
|
549
|
+
// resize only. A visualViewport `scroll` fires on every pan of the page, and answering it with scrollTo(0, 0) and a
|
|
550
|
+
// refit undid the PM's own scroll in one jump (workspace-phone-scroll-layout). The height changes only on a resize.
|
|
547
551
|
window.visualViewport.addEventListener('resize', fitViewport);
|
|
548
|
-
window.visualViewport.addEventListener('scroll', fitViewport);
|
|
549
552
|
}
|
|
550
553
|
if (TOUCH.addEventListener) TOUCH.addEventListener('change', fitViewport);
|
|
551
554
|
window.addEventListener('orientationchange', () => setTimeout(() => { fitViewport(); refit(); }, 250));
|
|
@@ -567,6 +570,7 @@ export function mountMobile(context) {
|
|
|
567
570
|
|
|
568
571
|
mountDeck({
|
|
569
572
|
rerender: () => setState({}),
|
|
573
|
+
go: (dest) => setDest(dest),
|
|
570
574
|
apply: () => { if (isMobile() && nav) applyDeck(frameOn(), NAV.current(nav)); },
|
|
571
575
|
place: () => ctx.bodyGrid.querySelector('[data-fr="main"]'),
|
|
572
576
|
});
|
|
@@ -1,51 +1,57 @@
|
|
|
1
|
-
/* pm/phone-deck.js — a phone tab
|
|
1
|
+
/* pm/phone-deck.js — the head above a phone tab's list (initiative workspace-phone-scroll-layout).
|
|
2
2
|
*
|
|
3
|
-
* At a tab's root on a phone (≤760px), the
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* At a tab's root on a phone (≤760px), the tab's place draws its own list, top to bottom, and only that list scrolls. This
|
|
4
|
+
* module draws the line above it (#mobDeck): how many things the list holds and, where the tab has a second view, a
|
|
5
|
+
* two-button switch to it, each button an icon and a short word.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* WHY NOT A DECK ANY MORE. Until 2026-10-07 this module turned the list into a deck of full-screen cards, side by side with
|
|
8
|
+
* scroll snapping (initiative workspace-phone-one-job). The PM reported that scrolling down a card "pages up" by itself,
|
|
9
|
+
* and that the empty space scrolls with the content. Both came from the deck: each card was a fixed-height box with its own
|
|
10
|
+
* vertical scroller inside a horizontal snap scroller, the deck re-snapped to "the card on screen" on every window resize
|
|
11
|
+
* (and a soft keyboard or Safari's toolbar resizes the window), and a short card left empty space that still scrolled. The
|
|
12
|
+
* PM chose one vertical list from a clickable mockup (its link is in the initiative record). Nothing here
|
|
13
|
+
* moves a scroll position.
|
|
10
14
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Initiatives the Roadmap's cards, Now then Next then Later. Second view: All, the List.
|
|
14
|
-
* Board the cards of the column on screen; the initiative picker and the column switch stay above them.
|
|
15
|
-
* Docs the copies that need you. Second view: All, every copy.
|
|
16
|
-
* Reports and Agent have no deck: the report reads as a page, and Agent is the dock with its sessions.
|
|
15
|
+
* ONE RENDERER. Nothing here draws a row. The destinations draw exactly what they draw on a desktop (pm/mobile.js, "ONE
|
|
16
|
+
* renderer, two shells"); pm/mobile.css only sizes them for a thumb.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Agenda Needs you (the Today cards) or My day (today-day.js: the goals, the calendar and the jobs).
|
|
19
|
+
* Initiatives Roadmap (Now, then Next, then Later) or All (the List).
|
|
20
|
+
* Board Board or Reports. Reports is a destination of its own with no tab (lib/mobile-nav.js TAB_OF), so this
|
|
21
|
+
* switch changes the destination, and the Board tab stays lit.
|
|
22
|
+
* Docs Out of sync (the copies that need you) or All (every copy).
|
|
23
|
+
* Agent has no head: it is the dock with its sessions.
|
|
24
|
+
*
|
|
25
|
+
* The view a tab shows lives in memory for this page only, so a phone never changes what a desktop opens on.
|
|
21
26
|
*/
|
|
22
27
|
|
|
23
28
|
import { setDayForced } from './today-day.js';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
where: (
|
|
30
|
-
|
|
31
|
-
where: (
|
|
32
|
-
|
|
33
|
-
where: (
|
|
29
|
+
import { icon } from './icons.js';
|
|
30
|
+
|
|
31
|
+
/* views: [id, word, icon]. A view whose id is a destination (Board's) switches the destination instead of the view. */
|
|
32
|
+
const HEADS = {
|
|
33
|
+
today: { views: [['cards', 'Needs you', 'circle-alert'], ['day', 'My day', 'calendar']], scroller: '[data-today="needs"]', item: '[data-today-id]',
|
|
34
|
+
where: (n) => (n ? `${n} need${n === 1 ? 's' : ''} you` : 'Nothing needs you'), second: 'Your day' },
|
|
35
|
+
initiatives: { views: [['cards', 'Roadmap', 'target'], ['all', 'All', 'list']], scroller: '[data-in-cols]', item: '[data-in-card], [data-in-unrecorded-card]',
|
|
36
|
+
where: (n) => (n ? `${n} initiative${n === 1 ? '' : 's'}` : 'No initiatives'), second: 'All initiatives' },
|
|
37
|
+
board: { views: [['board', 'Board', 'square-kanban'], ['reports', 'Reports', 'chart-column']], dests: true, scroller: '.bd-col--shown .bd-col__list', item: '[data-item]',
|
|
38
|
+
where: (n) => (n ? `${n} item${n === 1 ? '' : 's'}` : 'No item here') },
|
|
39
|
+
reports: { views: [['board', 'Board', 'square-kanban'], ['reports', 'Reports', 'chart-column']], dests: true,
|
|
40
|
+
where: () => 'Reports' },
|
|
41
|
+
docs: { views: [['cards', 'Out of sync', 'circle-alert'], ['all', 'All', 'list']], scroller: '.rp-docs-body', item: '.rp-docs-row',
|
|
42
|
+
where: (n) => (n ? `${n} differ` : 'Every copy is the same'), second: 'Every copy' },
|
|
34
43
|
};
|
|
35
|
-
const label = (h) => h.charAt(0).toUpperCase() + h.slice(1);
|
|
36
44
|
|
|
37
45
|
const view = { today: 'cards', initiatives: 'cards', docs: 'cards' };
|
|
38
|
-
|
|
39
|
-
let live = null; // { dest, scroller } the deck being shown
|
|
46
|
+
let shown = null; // the destination whose head is drawn
|
|
40
47
|
let wasOn = false;
|
|
41
|
-
let ctx = null; // { rerender, place: () => the place panel }
|
|
48
|
+
let ctx = null; // { rerender, apply, go(dest), place: () => the place panel }
|
|
42
49
|
let observer = null;
|
|
43
50
|
let queued = false;
|
|
44
51
|
|
|
45
52
|
const $ = (id) => document.getElementById(id);
|
|
46
53
|
|
|
47
|
-
/** The view a tab shows
|
|
48
|
-
* All filter, however it was reached: the switch, ⌘K or the desktop's saved choice); Today's lives here. */
|
|
54
|
+
/** The view a tab shows. Initiatives and Docs read their own switch, so a change made in the place itself is honoured. */
|
|
49
55
|
export function phoneView(dest) {
|
|
50
56
|
if (dest === 'initiatives') {
|
|
51
57
|
const body = document.querySelector('[data-dest="initiatives"] [data-in-body]');
|
|
@@ -55,12 +61,13 @@ export function phoneView(dest) {
|
|
|
55
61
|
const on = document.querySelector('[data-dest="docs"] [data-docs-filter] [aria-selected="true"]');
|
|
56
62
|
return on && on.getAttribute('data-tab') === 'all' ? 'all' : 'cards';
|
|
57
63
|
}
|
|
64
|
+
if (dest === 'board' || dest === 'reports') return dest;
|
|
58
65
|
return view[dest] || 'cards';
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
/* Put the destination in the view the PM chose. Only in-memory state is touched: the desktop's saved choices (the
|
|
62
|
-
* Initiatives view in localStorage, the day's fold) are left as they are. */
|
|
63
68
|
function setView(dest, v) {
|
|
69
|
+
const cfg = HEADS[dest];
|
|
70
|
+
if (cfg && cfg.dests) { if (v !== dest && ctx && ctx.go) ctx.go(v); return; }
|
|
64
71
|
if (phoneView(dest) === v) return;
|
|
65
72
|
view[dest] = v;
|
|
66
73
|
if (dest === 'today') setDayForced(v === 'day');
|
|
@@ -73,143 +80,73 @@ function setView(dest, v) {
|
|
|
73
80
|
refresh();
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
function count(dest, cfg) {
|
|
84
|
+
if (!cfg.scroller) return 0;
|
|
85
|
+
const host = document.querySelector(`[data-fr-host="${dest}"]`);
|
|
86
|
+
const scroller = host && host.querySelector(cfg.scroller);
|
|
87
|
+
return scroller ? [...scroller.querySelectorAll(cfg.item)].filter((n) => !n.closest('[hidden]')).length : 0;
|
|
88
|
+
}
|
|
79
89
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const i = scroller ? Math.min(indexNow(scroller), Math.max(0, n - 1)) : 0;
|
|
90
|
+
function paint(dest, cfg) {
|
|
91
|
+
const v = phoneView(dest);
|
|
92
|
+
const first = !cfg.views || cfg.dests || v === cfg.views[0][0];
|
|
93
|
+
const text = first ? cfg.where(count(dest, cfg)) : (cfg.second || '');
|
|
85
94
|
const where = $('mobDeckWhere');
|
|
86
|
-
|
|
87
|
-
if (where) where.textContent = deck ? cfg.where(i, n, list[i]) : (cfg.second || '');
|
|
88
|
-
const pager = $('mobPager');
|
|
89
|
-
if (pager) pager.hidden = !deck || n < 2;
|
|
90
|
-
const prev = $('mobPrev'); const next = $('mobNext'); const fill = $('mobPagerFill');
|
|
91
|
-
if (prev) prev.disabled = i <= 0;
|
|
92
|
-
if (next) next.disabled = i >= n - 1;
|
|
93
|
-
if (fill) fill.style.setProperty('--fr-deck-fill', n ? ((i + 1) / n * 100).toFixed(2) + '%' : '0');
|
|
94
|
-
list.forEach((node, k) => { const v = k === i ? 'true' : 'false'; if (node.getAttribute('data-ph-on') !== v) node.setAttribute('data-ph-on', v); });
|
|
95
|
-
if (n) at[dest] = { key: keyOf(cfg, list[i], i), i };
|
|
95
|
+
if (where && where.textContent !== text) where.textContent = text;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function views(dest, cfg) {
|
|
99
99
|
const box = $('mobDeckViews');
|
|
100
100
|
if (!box) return;
|
|
101
|
-
const
|
|
101
|
+
const list = cfg.views || [];
|
|
102
|
+
const want = list.map(([id, l]) => id + ':' + l + ':' + (phoneView(dest) === id)).join('|');
|
|
102
103
|
if (box.dataset.sig === dest + want) return;
|
|
103
104
|
box.dataset.sig = dest + want;
|
|
104
|
-
box.replaceChildren(...
|
|
105
|
+
box.replaceChildren(...list.map(([id, l, ic]) => {
|
|
105
106
|
const b = document.createElement('button');
|
|
106
107
|
b.type = 'button';
|
|
107
108
|
b.className = 'mob-deck__view';
|
|
108
|
-
b.
|
|
109
|
+
const g = icon(ic); g.classList.add('mob-deck__i'); b.append(g, document.createTextNode(l));
|
|
109
110
|
b.setAttribute('aria-pressed', phoneView(dest) === id ? 'true' : 'false');
|
|
110
111
|
b.dataset.view = id;
|
|
111
112
|
b.dataset.phViewTo = dest;
|
|
112
113
|
return b;
|
|
113
114
|
}));
|
|
114
|
-
box.hidden = !
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/* The height a card gets: the screen between the top of the deck and the pager, so a card fills it and its own text
|
|
118
|
-
* scrolls inside it when it is longer. */
|
|
119
|
-
function fit(scroller) {
|
|
120
|
-
const top = scroller.getBoundingClientRect().top;
|
|
121
|
-
const menu = document.getElementById('mobMenu');
|
|
122
|
-
const bottom = menu ? menu.getBoundingClientRect().top : window.innerHeight;
|
|
123
|
-
const h = Math.max(220, Math.floor(bottom - top - 12));
|
|
124
|
-
if (scroller.style.getPropertyValue('--fr-deck-h') !== h + 'px') scroller.style.setProperty('--fr-deck-h', h + 'px');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function attach(dest, cfg, scroller) {
|
|
128
|
-
if (scroller.dataset.phDeck === dest) return false;
|
|
129
|
-
scroller.dataset.phDeck = dest;
|
|
130
|
-
let t = null;
|
|
131
|
-
scroller.addEventListener('scroll', () => {
|
|
132
|
-
if (t) cancelAnimationFrame(t);
|
|
133
|
-
t = requestAnimationFrame(() => { if (live && live.scroller === scroller) paint(dest, cfg, scroller); });
|
|
134
|
-
}, { passive: true });
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/* Bring the remembered card back on screen after the list was drawn again. */
|
|
139
|
-
function restore(dest, cfg, scroller) {
|
|
140
|
-
const list = items(cfg, scroller);
|
|
141
|
-
const was = at[dest];
|
|
142
|
-
// the first card stays first: a card that comes in at the front (the day's Prepare card, a new cause) is the one to see
|
|
143
|
-
let k = !was || was.i === 0 ? 0 : list.findIndex((n, j) => keyOf(cfg, n, j) === was.key);
|
|
144
|
-
if (k < 0) k = Math.min(was.i, Math.max(0, list.length - 1));
|
|
145
|
-
const left = k * scroller.clientWidth;
|
|
146
|
-
if (Math.abs(scroller.scrollLeft - left) > 2) scroller.scrollTo({ left, behavior: 'instant' });
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function step(by) {
|
|
150
|
-
if (!live) return;
|
|
151
|
-
const { scroller } = live;
|
|
152
|
-
const w = scroller.clientWidth || 1;
|
|
153
|
-
scroller.scrollTo({ left: (indexNow(scroller) + by) * w, behavior: 'smooth' });
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/* The deck's marks come off every node, so a primitive's deck size (components.css .pmc-card.ph-card) applies only
|
|
157
|
-
* while a deck is on screen. */
|
|
158
|
-
function strip() {
|
|
159
|
-
for (const n of document.querySelectorAll('.ph-deck')) n.classList.remove('ph-deck');
|
|
160
|
-
for (const n of document.querySelectorAll('.ph-card')) { n.classList.remove('ph-card'); n.removeAttribute('data-ph-on'); }
|
|
115
|
+
box.hidden = !list.length;
|
|
161
116
|
}
|
|
162
117
|
|
|
163
118
|
function off() {
|
|
164
|
-
strip();
|
|
165
119
|
const shell = $('mobDeck'); if (shell) shell.hidden = true;
|
|
166
|
-
const pager = $('mobPager'); if (pager) pager.hidden = true;
|
|
167
120
|
delete document.body.dataset.phDeck;
|
|
168
121
|
delete document.body.dataset.phView;
|
|
169
|
-
|
|
122
|
+
shown = null;
|
|
170
123
|
}
|
|
171
124
|
|
|
172
|
-
/**
|
|
173
|
-
* Draw the deck for the screen on show. `screen` is the reducer's current screen; `on` is false above the breakpoint.
|
|
174
|
-
* Called by pm/mobile.js at the end of every shell render, and again whenever the place's own list is drawn again.
|
|
175
|
-
*/
|
|
125
|
+
/** Called by pm/mobile.js on every phone render, with whether the frame is up and the screen on top. */
|
|
176
126
|
export function applyDeck(on, screen) {
|
|
177
|
-
// Above the breakpoint nothing here runs: the desktop's own fold applies, the marks come off once, and the place is
|
|
178
|
-
// not watched (a subtree observer on the desktop's place, terminals included, costs every render).
|
|
179
127
|
if (!on) { if (wasOn) { wasOn = false; setDayForced(null); off(); if (observer) { observer.disconnect(); observer = null; } } return; }
|
|
180
128
|
wasOn = true;
|
|
181
129
|
watch();
|
|
182
130
|
const dest = screen && screen.kind === 'dest' ? screen.id : null;
|
|
183
|
-
const cfg = dest ?
|
|
131
|
+
const cfg = dest ? HEADS[dest] : null;
|
|
184
132
|
if (!cfg) { off(); return; }
|
|
185
133
|
if (dest === 'today') setDayForced(view.today === 'day');
|
|
186
134
|
document.body.dataset.phDeck = dest;
|
|
187
135
|
document.body.dataset.phView = phoneView(dest);
|
|
188
136
|
const shell = $('mobDeck'); if (shell) shell.hidden = false;
|
|
137
|
+
shown = dest;
|
|
189
138
|
views(dest, cfg);
|
|
190
|
-
|
|
191
|
-
const scroller = phoneView(dest) === 'cards' && host ? host.querySelector(cfg.scroller) : null;
|
|
192
|
-
if (!scroller) { strip(); live = null; paint(dest, cfg, null); return; }
|
|
193
|
-
scroller.classList.add('ph-deck');
|
|
194
|
-
for (const n of items(cfg, scroller)) n.classList.add('ph-card');
|
|
195
|
-
const fresh = attach(dest, cfg, scroller) || !live || live.scroller !== scroller;
|
|
196
|
-
live = { dest, scroller };
|
|
197
|
-
fit(scroller);
|
|
198
|
-
if (fresh || scroller.dataset.phCount !== String(items(cfg, scroller).length)) restore(dest, cfg, scroller);
|
|
199
|
-
scroller.dataset.phCount = String(items(cfg, scroller).length);
|
|
200
|
-
paint(dest, cfg, scroller);
|
|
139
|
+
paint(dest, cfg);
|
|
201
140
|
}
|
|
202
141
|
|
|
203
|
-
/* A destination that draws on its own (the Roadmap's read, the Board's poll) changes the place without a shell render:
|
|
204
|
-
* draw the deck again on the next frame. Only child lists are watched, and the deck writes attributes, so it cannot
|
|
205
|
-
* wake itself. */
|
|
206
142
|
function refresh() {
|
|
207
143
|
if (queued) return;
|
|
208
144
|
queued = true;
|
|
209
145
|
requestAnimationFrame(() => { queued = false; if (ctx && ctx.apply) ctx.apply(); });
|
|
210
146
|
}
|
|
211
147
|
|
|
212
|
-
/* The place
|
|
148
|
+
/* The place rebuilds its list on a poll; the count in the head follows it. A recount reads the DOM and writes one text
|
|
149
|
+
* node, and never touches a scroll position. */
|
|
213
150
|
function watch() {
|
|
214
151
|
if (observer || !ctx || !ctx.place || !('MutationObserver' in window)) return;
|
|
215
152
|
const place = ctx.place();
|
|
@@ -218,26 +155,13 @@ function watch() {
|
|
|
218
155
|
observer.observe(place, { childList: true, subtree: true });
|
|
219
156
|
}
|
|
220
157
|
|
|
221
|
-
/** Wire the pager once, at boot. `context` is { rerender, apply, place }: apply() re-runs applyDeck with the current screen. */
|
|
222
158
|
export function mountDeck(context) {
|
|
223
159
|
ctx = context || null;
|
|
224
|
-
const prev = $('mobPrev'); const next = $('mobNext');
|
|
225
|
-
// one listener for the deck's own buttons (the view switch, the pager), read from their data attributes
|
|
226
160
|
document.addEventListener('click', (e) => {
|
|
227
|
-
const t = e.target instanceof Element ? e.target.closest('#mobDeckViews [data-ph-view-to]
|
|
161
|
+
const t = e.target instanceof Element ? e.target.closest('#mobDeckViews [data-ph-view-to]') : null;
|
|
228
162
|
if (!t || t.disabled) return;
|
|
229
|
-
|
|
230
|
-
else if (t.id === 'mobNext') step(1);
|
|
231
|
-
else setView(t.dataset.phViewTo, t.dataset.view);
|
|
232
|
-
});
|
|
233
|
-
window.addEventListener('resize', () => { if (live) { fit(live.scroller); restore(live.dest, DECKS[live.dest], live.scroller); } });
|
|
234
|
-
document.addEventListener('keydown', (e) => {
|
|
235
|
-
if (!live || e.altKey || e.metaKey || e.ctrlKey) return;
|
|
236
|
-
const t = e.target;
|
|
237
|
-
if (t && t.closest && t.closest('input, textarea, select, [contenteditable="true"], .xterm')) return;
|
|
238
|
-
if (e.key === 'ArrowRight') { e.preventDefault(); step(1); } else if (e.key === 'ArrowLeft') { e.preventDefault(); step(-1); }
|
|
163
|
+
setView(t.dataset.phViewTo, t.dataset.view);
|
|
239
164
|
});
|
|
240
165
|
}
|
|
241
166
|
|
|
242
|
-
|
|
243
|
-
window.pmDeck = { phoneView, setView, live: () => (live ? { dest: live.dest, index: indexNow(live.scroller), count: items(DECKS[live.dest], live.scroller).length } : null) };
|
|
167
|
+
window.pmDeck = { phoneView, setView, live: () => (shown ? { dest: shown, view: phoneView(shown), count: HEADS[shown] ? count(shown, HEADS[shown]) : 0 } : null) };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* lucide-sprite.js — GENERATED by workspace/third-party/build-lucide-sprite.js from lucide-static 1.52.0.
|
|
2
|
-
* Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right, panel-top, panel-left, maximize-2, minimize-2, git-branch, clock, bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud, share-2.
|
|
2
|
+
* Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right, panel-top, panel-left, maximize-2, minimize-2, git-branch, clock, bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud, share-2, house, target, square-kanban, file-text, square-terminal, calendar, chart-column.
|
|
3
3
|
*
|
|
4
4
|
* ISC License
|
|
5
5
|
*
|
|
@@ -94,5 +94,12 @@ export const SPRITE = '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true
|
|
|
94
94
|
+ '<symbol id="lucide-git-compare" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="18" r="3" /><circle cx="6" cy="6" r="3" /><path d="M13 6h3a2 2 0 0 1 2 2v7" /><path d="M11 18H8a2 2 0 0 1-2-2V9" /></symbol>'
|
|
95
95
|
+ '<symbol id="lucide-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>'
|
|
96
96
|
+ '<symbol id="lucide-share-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /><line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>'
|
|
97
|
+
+ '<symbol id="lucide-house" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" /><path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /></symbol>'
|
|
98
|
+
+ '<symbol id="lucide-target" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" /></symbol>'
|
|
99
|
+
+ '<symbol id="lucide-square-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M8 7v7" /><path d="M12 7v4" /><path d="M16 7v9" /></symbol>'
|
|
100
|
+
+ '<symbol id="lucide-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /><path d="M14 2v5a1 1 0 0 0 1 1h5" /><path d="M10 9H8" /><path d="M16 13H8" /><path d="M16 17H8" /></symbol>'
|
|
101
|
+
+ '<symbol id="lucide-square-terminal" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 11 2-2-2-2" /><path d="M11 13h4" /><rect width="18" height="18" x="3" y="3" rx="2" ry="2" /></symbol>'
|
|
102
|
+
+ '<symbol id="lucide-calendar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v3" /><path d="M16 2v3" /><rect x="3" y="3" width="18" height="18" rx="2" /><path d="M3 9h18" /></symbol>'
|
|
103
|
+
+ '<symbol id="lucide-chart-column" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v16a2 2 0 0 0 2 2h16" /><path d="M18 17V9" /><path d="M13 17V5" /><path d="M8 17v-3" /></symbol>'
|
|
97
104
|
+ '</svg>';
|
|
98
|
-
export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right","panel-top","panel-left","maximize-2","minimize-2","git-branch","clock","bold","italic","code","link","type","heading","list","list-checks","text-quote","file-code","git-compare","cloud","share-2"];
|
|
105
|
+
export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right","panel-top","panel-left","maximize-2","minimize-2","git-branch","clock","bold","italic","code","link","type","heading","list","list-checks","text-quote","file-code","git-compare","cloud","share-2","house","target","square-kanban","file-text","square-terminal","calendar","chart-column"];
|
|
@@ -12,7 +12,7 @@ The workspace runs with no network (D62). Two third-party browser files are comm
|
|
|
12
12
|
| Package | Version | Licence | Tarball | Tarball sha256 | Shipped file |
|
|
13
13
|
|---|---|---|---|---|---|
|
|
14
14
|
| sortablejs | 1.15.7 | MIT | https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz | `66cbfc8fa18860783584fe0c346baf6cbf3399da72d23d21053425ba232abed4` | `public/vendor/Sortable.min.js`: an unmodified copy of `package/Sortable.min.js` (45 478 bytes, sha256 `bf4241bc73fef7f11c59a283a69fe8051cdd31c6d8ff5a2b9ba219e7831fcf76`; npm integrity `sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==`) |
|
|
15
|
-
| lucide-static | 1.52.0 | ISC, plus the Feather MIT notice | https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz | `4d2aa079171e4eba4c704844b8b0c9edb6e4cfb2fb8bd86f7b14353994f04a87` | `public/vendor/lucide-sprite.js`: generated by `build-lucide-sprite.js` (
|
|
15
|
+
| lucide-static | 1.52.0 | ISC, plus the Feather MIT notice | https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz | `4d2aa079171e4eba4c704844b8b0c9edb6e4cfb2fb8bd86f7b14353994f04a87` | `public/vendor/lucide-sprite.js`: generated by `build-lucide-sprite.js` (19 585 bytes, sha256 `405c8046d0954e3c942a4897eab7dcaca4cd3e2632b40788e4dbd4d9e5475461`). It holds 55 icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, then the eight the frame's chrome draws (Run 3): settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, then globe, columns-2, arrow-up-down and arrow-left-right for the dock strip's web, split and orientation chips (Run 3, C6), then panel-top, panel-left, maximize-2, minimize-2, git-branch and clock for the dock's icon controls and session rows (0.54.2), then bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud and share-2 for the document tab's in-place editor, its Confluence copy and its connections, then house, target, square-kanban, file-text, square-terminal, calendar and chart-column for the phone's tab bar and view switches |
|
|
16
16
|
|
|
17
17
|
The licences are in this folder: [sortablejs-1.15.7.LICENSE](sortablejs-1.15.7.LICENSE) and [lucide-static-1.52.0.LICENSE](lucide-static-1.52.0.LICENSE). The sprite also carries the lucide licence text as its first comment.
|
|
18
18
|
|