claude-code-kanban 7.12.1 → 7.13.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/hub-app.json +1 -1
- package/package.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/claude-code-kanban/skills/visualize/SKILL.md +3 -2
- package/plugin/plugins/claude-code-kanban/skills/visualize/template.html +26 -8
- package/public/app.js +296 -58
- package/public/style.css +105 -1
package/hub-app.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"icon": "columns",
|
|
6
6
|
"run": { "entry": "server.js", "defaultPort": 3541 },
|
|
7
7
|
"publishes": ["session.changed"],
|
|
8
|
-
"keys": { "keeps": ["ctrl+alt+n", "ctrl+alt+r", "ctrl+alt+s", "ctrl+shift+d", "ctrl+shift+k", "ctrl+shift+l", "ctrl+shift+m", "ctrl+shift+p", "ctrl+shift+z"] },
|
|
8
|
+
"keys": { "keeps": ["ctrl+alt+1", "ctrl+alt+2", "ctrl+alt+3", "ctrl+alt+4", "ctrl+alt+5", "ctrl+alt+6", "ctrl+alt+7", "ctrl+alt+8", "ctrl+alt+9", "ctrl+alt+n", "ctrl+alt+r", "ctrl+alt+s", "ctrl+shift+d", "ctrl+shift+k", "ctrl+shift+l", "ctrl+shift+m", "ctrl+shift+p", "ctrl+shift+z"] },
|
|
9
9
|
"plugin": {
|
|
10
10
|
"marketplace": "plugin",
|
|
11
11
|
"name": "claude-code-kanban",
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-kanban",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.9",
|
|
4
4
|
"description": "claude-code-kanban dashboard integration: agent activity tracking, context and cost tracking, board moves and reviews delivered to the session, and skills to drive the board from a session"
|
|
5
5
|
}
|
|
@@ -17,7 +17,7 @@ The page is a document to read: its text and static exhibits are in the HTML, an
|
|
|
17
17
|
1. `.kicker`, then `<title>` and `<h1>`: a name of 2 to 5 words. `.sub`: the question, in one line.
|
|
18
18
|
2. `.lead`: the answer in 1 or 2 sentences, the pick in `<b>`.
|
|
19
19
|
3. Sections. Each `<h2>` is a claim that can be false ("A breaker alone trips again on release", not "Breaker"), with one exhibit and at most 2 short paragraphs under it.
|
|
20
|
-
4. Close with what to do next and `.sources
|
|
20
|
+
4. Close with what to do next and `.sources`, one `li` per source. Each `[n]` in the text is an `a.cite` to its row.
|
|
21
21
|
3. **Invent each exhibit.** Ask what form makes this claim obvious at a glance, then build that, the template's blocks or something new: a slope chart, an annotated timeline, small multiples, a cutaway, a ranked strip, a map. The Blocks table is a palette of ready parts, not a menu to choose from. Before you draw an SVG, read the matching guide in the `show` skill — [chart](../show/references/chart.md), [diagram](../show/references/diagram.md) and its type file, [mockup](../show/references/mockup.md) — for patterns: shapes, arrows and labels. Their sizes and directions are for cards; on a page, the Principles below set them. Label masks fill with `--color-bg-subtle`, the figure's background. When the source or the request has a value the reader would want to change, or states one after another, the exhibit can be interactive: a slider or toggle that computes the figure again, a stepper through states, or a model that runs over time. Use the simplest one that proves the claim, and follow [Interactive exhibits](#interactive-exhibits). Never add a control on an input you made up: a claim that rests on fixed facts gets a static exhibit.
|
|
22
22
|
4. **Check.** Render the page in light and in dark, and look at each render, not the source. Done when no text is clipped, no arrow crosses a label, `scrollWidth` equals `clientWidth`, and, when the topic comes from a source such as a file or a conversation, every section and number of it is named where it landed on the page or reported to the user as left out. For an interactive exhibit, also: no console errors, and the numbers the text states are the ones the exhibit shows at its default settings.
|
|
23
23
|
5. **Show.** When `$CCK_URL` is set, the session runs in the cck board: run `claude-code-kanban pane add <page> --title '<h1>'` and tell the user the page waits in that tab. Else give the user the page path.
|
|
@@ -62,6 +62,7 @@ Ready parts in the template's CSS; its body has one of each.
|
|
|
62
62
|
| Callout | `div.callout` | A warning |
|
|
63
63
|
| Steps | `ol.steps` | What to do next |
|
|
64
64
|
| Tag, dot, legend | `span.tag`, `span.dot`, `.legend > span > i` | Labels and keys |
|
|
65
|
-
| Sources |
|
|
65
|
+
| Sources | `.sources > ol > li#src-n > code + span`, then `p` | Where each claim comes from: one source per `li`, numbered in order (CSS prints `[n]`), the path or `a` link, and in `span` what it backs; a closing line, such as how estimates are marked, goes in the `p` |
|
|
66
|
+
| Citation | `a.cite[href="#src-n"]` holding `[n]` | An in-text marker that jumps to its source row |
|
|
66
67
|
|
|
67
68
|
`data-tone` takes `accent`, `info`, `success`, `warning`, `danger` or `muted` on any block, legend keys included.
|
|
@@ -111,7 +111,8 @@ td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
|
111
111
|
td.c { text-align: center; white-space: nowrap; }
|
|
112
112
|
td.yes { --tone: var(--color-success); } td.no { --tone: var(--color-danger); } td.part { --tone: var(--color-warning); }
|
|
113
113
|
td.yes, td.no, td.part { color: var(--tone); font-weight: 600; background: color-mix(in srgb, var(--tone) 13%, transparent); }
|
|
114
|
-
td.yes::before { content: "
|
|
114
|
+
td.yes::before, td.part::before, td.no::before { content: ""; display: inline-block; width: 8px; height: 8px; border: 1.5px solid; border-radius: 50%; margin-right: 6px; vertical-align: 1px; }
|
|
115
|
+
td.yes::before { background: currentColor; } td.part::before { background: linear-gradient(90deg, currentColor 50%, transparent 50%); }
|
|
115
116
|
tr[data-tone] > td:first-child { box-shadow: inset 3px 0 0 var(--tone); background: color-mix(in srgb, var(--tone) 10%, transparent); }
|
|
116
117
|
|
|
117
118
|
figure { margin: 0 0 20px; padding: 24px; border: 1px solid var(--color-border); border-radius: 14px; background: var(--color-bg-subtle) radial-gradient(color-mix(in srgb, var(--color-text) 9%, transparent) 1px, transparent 1.5px) 0 0 / 18px 18px; }
|
|
@@ -131,9 +132,18 @@ ol.steps > li { counter-increment: step; position: relative; padding: 2px 0 20px
|
|
|
131
132
|
ol.steps > li::before { content: counter(step); position: absolute; left: 0; top: 0; width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center; font: 600 13px var(--font-mono); color: var(--color-bg); background: var(--color-accent); }
|
|
132
133
|
ol.steps > li:not(:last-child)::after { content: ""; position: absolute; left: 15px; top: 36px; bottom: 4px; width: 1px; background: var(--color-border); }
|
|
133
134
|
|
|
134
|
-
.sources { margin-top: 88px; padding-top: 16px; border-top: 1px solid var(--color-border); font:
|
|
135
|
-
.sources
|
|
136
|
-
.sources
|
|
135
|
+
.sources { margin-top: 88px; padding-top: 16px; border-top: 1px solid var(--color-border); font: 13px/20px var(--font-sans); color: var(--color-text-muted); }
|
|
136
|
+
.sources::before { content: "Sources"; display: block; font: 500 12px/1.4 var(--font-mono); letter-spacing: .12em; text-transform: uppercase; margin: 0 0 10px; }
|
|
137
|
+
.sources ol { list-style: none; counter-reset: src; padding: 0; margin: 0; }
|
|
138
|
+
.sources li { counter-increment: src; position: relative; max-width: none; margin: 0; padding: 4px 8px 4px calc(5ch + 8px); border-radius: 6px; scroll-margin-top: 24px; }
|
|
139
|
+
.sources li::before { content: "[" counter(src) "]"; position: absolute; left: 8px; font: 12.5px/20px var(--font-mono); }
|
|
140
|
+
.sources li:target { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }
|
|
141
|
+
.sources li > span { margin-left: 10px; }
|
|
142
|
+
.sources code { font-size: 12.5px; color: var(--color-text); background: none; border: 0; padding: 0; overflow-wrap: anywhere; }
|
|
143
|
+
.sources a { overflow-wrap: anywhere; }
|
|
144
|
+
.sources > p { max-width: none; margin: 12px 0 0; padding-left: 8px; }
|
|
145
|
+
a.cite { font: 500 .8em var(--font-mono); text-decoration: none; white-space: nowrap; }
|
|
146
|
+
a.cite:hover { text-decoration: underline; }
|
|
137
147
|
|
|
138
148
|
@media (max-width: 600px) {
|
|
139
149
|
main { padding-top: 36px; }
|
|
@@ -160,7 +170,7 @@ ol.steps > li:not(:last-child)::after { content: ""; position: absolute; left: 1
|
|
|
160
170
|
</div>
|
|
161
171
|
|
|
162
172
|
<div class="kpis">
|
|
163
|
-
<div class="kpi"><span>Errors in 7 days</span><b>133.5K</b><small>26% of minutes [1]</small></div>
|
|
173
|
+
<div class="kpi"><span>Errors in 7 days</span><b>133.5K</b><small>26% of minutes <a class="cite" href="#src-1">[1]</a></small></div>
|
|
164
174
|
<div class="kpi" data-tone="danger"><span>Peak after release</span><b>30,000</b><small>RU per minute, estimate</small></div>
|
|
165
175
|
</div>
|
|
166
176
|
|
|
@@ -180,7 +190,7 @@ ol.steps > li:not(:last-child)::after { content: ""; position: absolute; left: 1
|
|
|
180
190
|
<div class="card" data-tone="accent">
|
|
181
191
|
<h3><span class="dot" data-tone="accent">1</span>Option A<span class="tag" data-tone="accent">pick</span></h3>
|
|
182
192
|
<p class="verdict">The verdict, in one line.</p>
|
|
183
|
-
<p>What it is, in one sentence
|
|
193
|
+
<p>What it is, in one sentence. <a class="cite" href="#src-2">[2]</a></p>
|
|
184
194
|
<pre>on 429: wait, then retry</pre>
|
|
185
195
|
<ul><li class="pro">A strong point.</li><li class="con">A weak point.</li></ul>
|
|
186
196
|
</div>
|
|
@@ -194,7 +204,8 @@ ol.steps > li:not(:last-child)::after { content: ""; position: absolute; left: 1
|
|
|
194
204
|
<thead><tr><th>Option</th><th>Global</th><th>Lines</th></tr></thead>
|
|
195
205
|
<tbody>
|
|
196
206
|
<tr data-tone="accent"><td><span class="dot" data-tone="accent">1</span>Option A</td><td class="c yes">yes</td><td class="num">1,229</td></tr>
|
|
197
|
-
<tr><td><span class="dot">2</span>Option B</td><td class="c
|
|
207
|
+
<tr><td><span class="dot">2</span>Option B</td><td class="c part">partial</td><td class="num">640</td></tr>
|
|
208
|
+
<tr><td><span class="dot">3</span>Option C</td><td class="c no">no</td><td class="num">300</td></tr>
|
|
198
209
|
</tbody>
|
|
199
210
|
</table></div>
|
|
200
211
|
<ul class="points">
|
|
@@ -209,7 +220,14 @@ ol.steps > li:not(:last-child)::after { content: ""; position: absolute; left: 1
|
|
|
209
220
|
<ol class="steps"><li>First action.</li><li>Second action.</li></ol>
|
|
210
221
|
</section>
|
|
211
222
|
|
|
212
|
-
<div class="sources"
|
|
223
|
+
<div class="sources">
|
|
224
|
+
<ol>
|
|
225
|
+
<li id="src-1"><code>logs/errors-7d.csv</code><span>error count and share of minutes</span></li>
|
|
226
|
+
<li id="src-2"><code>services/api/src/middleware/rate-limit/retry-after-handler.ts:42</code><span>the retry on 429</span></li>
|
|
227
|
+
<li id="src-3"><a href="https://example.com">Title of the page</a><span>line counts in the table</span></li>
|
|
228
|
+
</ol>
|
|
229
|
+
<p>Estimates are marked as estimates.</p>
|
|
230
|
+
</div>
|
|
213
231
|
</main>
|
|
214
232
|
</body>
|
|
215
233
|
</html>
|
package/public/app.js
CHANGED
|
@@ -3918,7 +3918,7 @@ function renderSessions() {
|
|
|
3918
3918
|
const renderSessionCard = (session) => {
|
|
3919
3919
|
if (session.placeholder) return renderPlaceholderCard(session, childDepth.get(session.id));
|
|
3920
3920
|
const total = session.taskCount;
|
|
3921
|
-
const
|
|
3921
|
+
const pct = (n) => Math.round((n / total) * 100);
|
|
3922
3922
|
const isActive = session.id === currentSessionId && viewMode === 'session';
|
|
3923
3923
|
const isLive = isSessionLive(session);
|
|
3924
3924
|
const sessionName = sessionDisplayName(session);
|
|
@@ -3951,7 +3951,7 @@ function renderSessions() {
|
|
|
3951
3951
|
const sid = escAttrJs(session.id);
|
|
3952
3952
|
const progressHtml =
|
|
3953
3953
|
total > 0
|
|
3954
|
-
? `<div class="session-progress" title="${escapeHtml(session.
|
|
3954
|
+
? `<div class="session-progress" title="${escapeHtml(`${session.pending} pending, ${session.inProgress} in progress, ${session.completed} completed`)}"><div class="progress-bar"><div class="progress-fill done" style="width: ${pct(session.completed)}%"></div>${session.inProgress > 0 ? `<div class="progress-fill wip" style="width: ${pct(session.inProgress)}%"></div>` : ''}</div><span class="progress-text">${session.completed + session.inProgress}/${total}</span></div>`
|
|
3955
3955
|
: '';
|
|
3956
3956
|
const metricsHtml = progressHtml + (showCtx ? renderContextBar(session) : '');
|
|
3957
3957
|
return `
|
|
@@ -6391,6 +6391,12 @@ const SHORTCUT_TABS = [
|
|
|
6391
6391
|
title: 'Tabs',
|
|
6392
6392
|
rows: [
|
|
6393
6393
|
{ keys: ['[', ']'], label: 'Previous / next tab (a split counts as one)' },
|
|
6394
|
+
{
|
|
6395
|
+
keys: ['Ctrl', 'Alt', '1…9'],
|
|
6396
|
+
combo: true,
|
|
6397
|
+
yields: 'ctrl+alt+1',
|
|
6398
|
+
label: 'Go to tab or group N (the board is 1)',
|
|
6399
|
+
},
|
|
6394
6400
|
{ keys: ['Alt', '[ / ]'], combo: true, label: 'Change the tab in the focused side of a split' },
|
|
6395
6401
|
{ keys: ['|'], label: 'Split with the tab before it / unsplit (or drag a tab onto a side)' },
|
|
6396
6402
|
{ keys: ['{', '}'], label: 'Focus left / right side of a split' },
|
|
@@ -6542,6 +6548,8 @@ function termKbd() {
|
|
|
6542
6548
|
function helpKeys(row, mac = IS_MAC, keyLabel = hub.keyLabel) {
|
|
6543
6549
|
const live = row.action && keyLabel(row.action);
|
|
6544
6550
|
if (live) return live;
|
|
6551
|
+
// `yields`: the row's key gives way when the hub binds that combo.
|
|
6552
|
+
if (row.yields && hub.forwardCombos().includes(row.yields)) return [];
|
|
6545
6553
|
if (!mac) return row.keys;
|
|
6546
6554
|
if (row.hubMod) return ['⌃', '⌥', ...row.keys.filter((k) => k !== 'Ctrl' && k !== 'Alt')];
|
|
6547
6555
|
return row.keys.map((k) => MAC_KEYS[k] || k);
|
|
@@ -7825,6 +7833,12 @@ document.addEventListener('keydown', (e) => {
|
|
|
7825
7833
|
}
|
|
7826
7834
|
return;
|
|
7827
7835
|
}
|
|
7836
|
+
// The hub binds Ctrl+Alt+digit on macOS for its app keys.
|
|
7837
|
+
if (hubModDown(e) && /^Digit[1-9]$/.test(e.code) && !hub.forwards(e) && selectPaneByNumber(Number(e.code.slice(5)))) {
|
|
7838
|
+
e.preventDefault();
|
|
7839
|
+
focusActivePane();
|
|
7840
|
+
return;
|
|
7841
|
+
}
|
|
7828
7842
|
if (altOnly && e.code === 'KeyW' && closeFocusedPane()) {
|
|
7829
7843
|
e.preventDefault();
|
|
7830
7844
|
return;
|
|
@@ -8631,6 +8645,7 @@ const PANE_CLAIMS = [
|
|
|
8631
8645
|
...['|', '{', '}', 'F10'].map((key) => ({ shiftKey: true, key, code: '' })),
|
|
8632
8646
|
...['BracketLeft', 'BracketRight', 'KeyW'].map((code) => ({ altKey: true, key: 'Unidentified', code })),
|
|
8633
8647
|
...['KeyL', 'KeyM', 'KeyP', 'KeyS', 'KeyZ'].map((code) => ({ shiftKey: true, key: 'Unidentified', code })),
|
|
8648
|
+
...'123456789'.split('').map((n) => ({ ctrlKey: true, altKey: true, key: 'Unidentified', code: `Digit${n}` })),
|
|
8634
8649
|
];
|
|
8635
8650
|
|
|
8636
8651
|
// Ctrl+D and Alt+Enter act on the show card (showCardKey); other pages keep them. Alt+W and Esc are in PANE_CLAIMS.
|
|
@@ -13237,6 +13252,7 @@ function setActivePaneId(sid, id) {
|
|
|
13237
13252
|
if (getActivePaneId(sid) === id) return;
|
|
13238
13253
|
paneActive = savePaneState(PANE_ACTIVE_KEY, paneActive, sid, id === 'board' ? null : id);
|
|
13239
13254
|
notePairFocus(sid, id);
|
|
13255
|
+
noteGroupFocus(sid, id);
|
|
13240
13256
|
}
|
|
13241
13257
|
|
|
13242
13258
|
// {[sessionId]: [{l, r, f}]}; sides are pane ids or 'board', `f` the side focused
|
|
@@ -13297,18 +13313,75 @@ function neighborPair(sid, id, pairs) {
|
|
|
13297
13313
|
return i > 0 ? { l: free[i - 1], r: id } : { l: id, r: free[1] };
|
|
13298
13314
|
}
|
|
13299
13315
|
|
|
13300
|
-
//
|
|
13301
|
-
|
|
13316
|
+
// {[sessionId]: [{id, name, color, ids, closed, last}]}; `ids` are pane ids, `last` the one shown
|
|
13317
|
+
// last. Like pairs, groups live in this browser tab only. A group shows at the place of its first
|
|
13318
|
+
// tab, so grouping leaves the server's order alone; a drag saves the order on screen.
|
|
13319
|
+
const PANE_GROUPS_KEY = 'pane-groups';
|
|
13320
|
+
const PANE_GROUP_COLORS = ['blue', 'purple', 'teal', 'orange', 'yellow', 'pink', 'green', 'red'];
|
|
13321
|
+
let paneGroups = loadPaneState(PANE_GROUPS_KEY);
|
|
13322
|
+
|
|
13323
|
+
// Tabs that are gone, or in an earlier group, drop out, and so do groups left empty.
|
|
13324
|
+
function getPaneGroups(sid) {
|
|
13325
|
+
if (!paneGroups[sid]) return [];
|
|
13326
|
+
const live = new Set(paneLayout(sid).panes.map((p) => p.id));
|
|
13327
|
+
const taken = new Set();
|
|
13328
|
+
const groups = [];
|
|
13329
|
+
for (const g of paneGroups[sid]) {
|
|
13330
|
+
const ids = g.ids.filter((id) => live.has(id) && !taken.has(id));
|
|
13331
|
+
for (const id of ids) taken.add(id);
|
|
13332
|
+
const color = PANE_GROUP_COLORS.includes(g.color) ? g.color : PANE_GROUP_COLORS[0];
|
|
13333
|
+
if (ids.length) groups.push({ id: g.id, name: g.name || '', color, ids, closed: !!g.closed, last: g.last });
|
|
13334
|
+
}
|
|
13335
|
+
return groups;
|
|
13336
|
+
}
|
|
13337
|
+
|
|
13338
|
+
function setPaneGroups(sid, groups) {
|
|
13339
|
+
paneGroups = savePaneState(PANE_GROUPS_KEY, paneGroups, sid, groups.length ? groups : null);
|
|
13340
|
+
}
|
|
13341
|
+
|
|
13342
|
+
// A pair is in the group of either side, and the board is in none.
|
|
13343
|
+
function groupOfTabs(groups, ids) {
|
|
13344
|
+
return ids.includes('board') ? null : groups.find((g) => ids.some((t) => g.ids.includes(t))) || null;
|
|
13345
|
+
}
|
|
13346
|
+
|
|
13347
|
+
function noteGroupFocus(sid, id) {
|
|
13348
|
+
const pair = pairOf(sid, id);
|
|
13349
|
+
const group = groupOfTabs(getPaneGroups(sid), pair ? [pair.l, pair.r] : [id]);
|
|
13350
|
+
if (group && (group.closed || group.last !== id)) updatePaneGroup(sid, group.id, { closed: false, last: id });
|
|
13351
|
+
}
|
|
13352
|
+
|
|
13353
|
+
// The strip as the number keys count it, {group, items}: a group is one stop, open or collapsed.
|
|
13354
|
+
// An item is {ids, pair, group}: a lone tab, or a pair at the place of its first tab. A group's
|
|
13355
|
+
// items follow its first one.
|
|
13356
|
+
function paneStripStops(sid) {
|
|
13302
13357
|
const pairs = getPanePairs(sid);
|
|
13303
|
-
const
|
|
13358
|
+
const groups = getPaneGroups(sid);
|
|
13359
|
+
const stops = [];
|
|
13360
|
+
const groupStops = new Map();
|
|
13361
|
+
const seen = new Set();
|
|
13304
13362
|
for (const id of paneTabIds(sid)) {
|
|
13305
13363
|
const pair = pairs.find((p) => splitSideOf(p, id)) || null;
|
|
13306
|
-
if (
|
|
13307
|
-
|
|
13364
|
+
if (pair && seen.has(pair)) continue;
|
|
13365
|
+
if (pair) seen.add(pair);
|
|
13366
|
+
const ids = pair ? [pair.l, pair.r] : [id];
|
|
13367
|
+
const group = groupOfTabs(groups, ids);
|
|
13368
|
+
const item = { ids, pair, group };
|
|
13369
|
+
const stop = group && groupStops.get(group);
|
|
13370
|
+
if (stop) stop.items.push(item);
|
|
13371
|
+
else {
|
|
13372
|
+
stops.push({ group, items: [item] });
|
|
13373
|
+
if (group) groupStops.set(group, stops.at(-1));
|
|
13374
|
+
}
|
|
13308
13375
|
}
|
|
13309
|
-
return
|
|
13376
|
+
return stops;
|
|
13310
13377
|
}
|
|
13311
13378
|
|
|
13379
|
+
const paneStripItems = (sid) => paneStripStops(sid).flatMap((s) => s.items);
|
|
13380
|
+
|
|
13381
|
+
// The items a click or a key can reach: a collapsed group hides its tabs.
|
|
13382
|
+
const isOpenItem = (it) => !it.group?.closed;
|
|
13383
|
+
const openStripItems = (sid) => paneStripItems(sid).filter(isOpenItem);
|
|
13384
|
+
|
|
13312
13385
|
function unpairPanes(sid, pair, keep) {
|
|
13313
13386
|
setPanePairs(sid, withoutTabs(getPanePairs(sid), pair.l));
|
|
13314
13387
|
if (keep) setActivePaneId(sid, keep);
|
|
@@ -13502,7 +13575,7 @@ function paneMenuLabel(pane, label) {
|
|
|
13502
13575
|
|
|
13503
13576
|
function renderPaneTabs(sid, layout, split) {
|
|
13504
13577
|
// A rebuild mid-drag removes the dragged tab, and then no dragend fires. Mid-rename it drops the input.
|
|
13505
|
-
if (paneDragId ||
|
|
13578
|
+
if (paneDragId || stripRename) return;
|
|
13506
13579
|
const active = getActivePaneId(sid);
|
|
13507
13580
|
const state = (id) => (active === id ? ' on' : split && splitSideOf(split, id) ? ' shown' : '');
|
|
13508
13581
|
const tab = (id, cls, inner, tip) =>
|
|
@@ -13531,9 +13604,17 @@ function renderPaneTabs(sid, layout, split) {
|
|
|
13531
13604
|
pair
|
|
13532
13605
|
? `<div class="pane-pair${pair === split ? ' on' : ''}">${ids.map(tabHtml).join('<span class="pane-pair-sep"></span>')}</div>`
|
|
13533
13606
|
: tabHtml(ids[0]);
|
|
13534
|
-
const
|
|
13535
|
-
|
|
13536
|
-
|
|
13607
|
+
const groupHtml = (g, items) => {
|
|
13608
|
+
const n = items.flatMap((it) => it.ids).length;
|
|
13609
|
+
const tip = g.closed ? `Expand group (${n} tabs)` : 'Collapse group';
|
|
13610
|
+
const label =
|
|
13611
|
+
(g.name ? `<span>${escapeHtml(g.name)}</span>` : '') +
|
|
13612
|
+
(g.closed ? `<span class="pane-group-count">${n}</span>` : '');
|
|
13613
|
+
return `<div class="pane-group${g.closed ? ' closed' : ''}" style="${paneGroupStyle(g.color)}"><button class="pane-group-chip" data-group="${escapeHtml(g.id)}" title="${escapeHtml(tip)}">${label}</button>${g.closed ? '' : items.map(itemHtml).join('')}</div>`;
|
|
13614
|
+
};
|
|
13615
|
+
const [first, ...rest] = paneStripStops(sid);
|
|
13616
|
+
const pin = itemHtml(first.items[0]) + (rest.length ? '<span class="pane-sep"></span>' : '');
|
|
13617
|
+
const html = rest.map(({ group, items }) => (group ? groupHtml(group, items) : itemHtml(items[0]))).join('');
|
|
13537
13618
|
// syncPanes runs on every task refresh and search keystroke; skip the rebuild and the reflows.
|
|
13538
13619
|
if (pin + html === paneTabsHtml) return;
|
|
13539
13620
|
paneTabsHtml = pin + html;
|
|
@@ -13901,17 +13982,90 @@ function cyclePane(step) {
|
|
|
13901
13982
|
return true;
|
|
13902
13983
|
}
|
|
13903
13984
|
|
|
13985
|
+
function selectStripItem({ ids, pair }) {
|
|
13986
|
+
selectPane(pair ? pair[pair.f || 'l'] : ids[0]);
|
|
13987
|
+
}
|
|
13988
|
+
|
|
13989
|
+
function selectPaneByNumber(n) {
|
|
13990
|
+
const sid = paneSessionId();
|
|
13991
|
+
if (!sid || wantsTerminal()) return false;
|
|
13992
|
+
const stop = paneStripStops(sid)[n - 1];
|
|
13993
|
+
if (stop) selectStripItem(stop.items.find((it) => it.ids.includes(stop.group?.last)) || stop.items[0]);
|
|
13994
|
+
return true;
|
|
13995
|
+
}
|
|
13996
|
+
|
|
13904
13997
|
function cyclePaneStrip(step) {
|
|
13905
13998
|
const sid = paneSessionId();
|
|
13906
13999
|
if (!sid || wantsTerminal()) return false;
|
|
13907
14000
|
const active = getActivePaneId(sid);
|
|
13908
|
-
const items =
|
|
14001
|
+
const items = openStripItems(sid);
|
|
13909
14002
|
const i = items.findIndex((it) => it.ids.includes(active));
|
|
13910
|
-
|
|
13911
|
-
selectPane(pair ? pair[pair.f || 'l'] : ids[0]);
|
|
14003
|
+
selectStripItem(items[(i + step + items.length) % items.length]);
|
|
13912
14004
|
return true;
|
|
13913
14005
|
}
|
|
13914
14006
|
|
|
14007
|
+
function updatePaneGroup(sid, groupId, change) {
|
|
14008
|
+
const groups = getPaneGroups(sid);
|
|
14009
|
+
const group = groups.find((g) => g.id === groupId);
|
|
14010
|
+
if (!group) return;
|
|
14011
|
+
Object.assign(group, change);
|
|
14012
|
+
setPaneGroups(sid, groups);
|
|
14013
|
+
}
|
|
14014
|
+
|
|
14015
|
+
// `to` is a group id, 'new', or null to take the tab out of its group. A split moves as one.
|
|
14016
|
+
function groupPaneTab(id, to) {
|
|
14017
|
+
const sid = paneSessionId();
|
|
14018
|
+
const item = sid && paneStripItems(sid).find((it) => it.ids.includes(id));
|
|
14019
|
+
if (!item || item.ids.includes('board')) return;
|
|
14020
|
+
const groups = getPaneGroups(sid);
|
|
14021
|
+
for (const g of groups) g.ids = g.ids.filter((t) => !item.ids.includes(t));
|
|
14022
|
+
let target = groups.find((g) => g.id === to);
|
|
14023
|
+
if (to === 'new') {
|
|
14024
|
+
const used = new Set(groups.filter((g) => g.ids.length).map((g) => g.color));
|
|
14025
|
+
const color =
|
|
14026
|
+
PANE_GROUP_COLORS.find((c) => !used.has(c)) || PANE_GROUP_COLORS[groups.length % PANE_GROUP_COLORS.length];
|
|
14027
|
+
target = { id: `g${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`, name: '', color, ids: [] };
|
|
14028
|
+
groups.push(target);
|
|
14029
|
+
}
|
|
14030
|
+
if (target) Object.assign(target, { ids: [...target.ids, ...item.ids], closed: false });
|
|
14031
|
+
setPaneGroups(sid, groups);
|
|
14032
|
+
syncPanes();
|
|
14033
|
+
if (to === 'new') beginPaneGroupRename(target.id);
|
|
14034
|
+
}
|
|
14035
|
+
|
|
14036
|
+
// A collapsed group cannot hold the tab on screen, so collapsing it moves to the next open item.
|
|
14037
|
+
function togglePaneGroup(groupId) {
|
|
14038
|
+
const sid = paneSessionId();
|
|
14039
|
+
const group = sid && getPaneGroups(sid).find((g) => g.id === groupId);
|
|
14040
|
+
if (!group) return;
|
|
14041
|
+
const closing = !group.closed;
|
|
14042
|
+
updatePaneGroup(sid, groupId, { closed: closing });
|
|
14043
|
+
const items = paneStripItems(sid);
|
|
14044
|
+
const i = items.findIndex((it) => it.ids.includes(getActivePaneId(sid)));
|
|
14045
|
+
if (!closing || items[i]?.group?.id !== groupId) {
|
|
14046
|
+
syncPanes();
|
|
14047
|
+
return;
|
|
14048
|
+
}
|
|
14049
|
+
selectStripItem(items.slice(i).find(isOpenItem) || items.slice(0, i).findLast(isOpenItem));
|
|
14050
|
+
}
|
|
14051
|
+
|
|
14052
|
+
function ungroupPanes(groupId) {
|
|
14053
|
+
const sid = paneSessionId();
|
|
14054
|
+
if (!sid) return;
|
|
14055
|
+
setPaneGroups(
|
|
14056
|
+
sid,
|
|
14057
|
+
getPaneGroups(sid).filter((g) => g.id !== groupId),
|
|
14058
|
+
);
|
|
14059
|
+
syncPanes();
|
|
14060
|
+
}
|
|
14061
|
+
|
|
14062
|
+
function colorPaneGroup(groupId, color) {
|
|
14063
|
+
const sid = paneSessionId();
|
|
14064
|
+
if (!sid) return;
|
|
14065
|
+
updatePaneGroup(sid, groupId, { color });
|
|
14066
|
+
syncPanes();
|
|
14067
|
+
}
|
|
14068
|
+
|
|
13915
14069
|
function isMenuKey(e) {
|
|
13916
14070
|
return e.key === 'ContextMenu' || (e.key === 'F10' && e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey);
|
|
13917
14071
|
}
|
|
@@ -14057,44 +14211,105 @@ async function deleteBoardTasks(onlyCompleted) {
|
|
|
14057
14211
|
await refreshCurrentView();
|
|
14058
14212
|
}
|
|
14059
14213
|
|
|
14060
|
-
|
|
14214
|
+
// The save function of the tab or group name being edited.
|
|
14215
|
+
let stripRename = null;
|
|
14216
|
+
|
|
14217
|
+
function beginStripRename(el, { className, value, label, maxLength, save }) {
|
|
14218
|
+
stripRename = save;
|
|
14219
|
+
const input = document.createElement('input');
|
|
14220
|
+
input.className = `strip-rename ${className}`;
|
|
14221
|
+
input.value = value;
|
|
14222
|
+
if (maxLength) input.maxLength = maxLength;
|
|
14223
|
+
input.setAttribute('aria-label', label);
|
|
14224
|
+
el.replaceWith(input);
|
|
14225
|
+
input.focus();
|
|
14226
|
+
input.select();
|
|
14227
|
+
}
|
|
14228
|
+
|
|
14229
|
+
async function endStripRename(input, save) {
|
|
14230
|
+
const onSave = stripRename;
|
|
14231
|
+
if (!onSave) return;
|
|
14232
|
+
stripRename = null;
|
|
14233
|
+
paneTabsHtml = '';
|
|
14234
|
+
if (save) await onSave(input.value.trim());
|
|
14235
|
+
syncPanes();
|
|
14236
|
+
}
|
|
14061
14237
|
|
|
14062
14238
|
function beginPaneRename(id) {
|
|
14063
14239
|
const pane = paneById(id);
|
|
14064
14240
|
const tab = paneStrip.querySelector(`.pane-tab[data-pane="${CSS.escape(id)}"]`);
|
|
14065
14241
|
const title = tab?.querySelector('.pane-title');
|
|
14066
14242
|
if (!pane || !title) return;
|
|
14067
|
-
paneRenameId = id;
|
|
14068
14243
|
tab.draggable = false;
|
|
14069
|
-
|
|
14070
|
-
|
|
14071
|
-
|
|
14072
|
-
|
|
14073
|
-
|
|
14074
|
-
|
|
14075
|
-
|
|
14244
|
+
beginStripRename(title, {
|
|
14245
|
+
className: 'pane-title-input',
|
|
14246
|
+
value: pane.title,
|
|
14247
|
+
label: 'Pane title',
|
|
14248
|
+
save: async (text) => {
|
|
14249
|
+
const sid = paneSessionId();
|
|
14250
|
+
if (!sid || !paneById(id) || text === paneById(id).title) return;
|
|
14251
|
+
const res = await paneRequest('PATCH', apiPath`/api/panes/${sid}/${id}`, { title: text });
|
|
14252
|
+
if (res.error) showToast(res.error, 'error');
|
|
14253
|
+
else applyPaneLayout(sid, res.layout);
|
|
14254
|
+
},
|
|
14255
|
+
});
|
|
14076
14256
|
}
|
|
14077
14257
|
|
|
14078
|
-
|
|
14079
|
-
|
|
14080
|
-
|
|
14081
|
-
paneRenameId = null;
|
|
14082
|
-
paneTabsHtml = '';
|
|
14258
|
+
const PANE_GROUP_NAME_MAX = 40;
|
|
14259
|
+
|
|
14260
|
+
function beginPaneGroupRename(groupId) {
|
|
14083
14261
|
const sid = paneSessionId();
|
|
14084
|
-
const
|
|
14085
|
-
const
|
|
14086
|
-
if (
|
|
14087
|
-
|
|
14088
|
-
|
|
14089
|
-
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
|
|
14262
|
+
const group = sid && getPaneGroups(sid).find((g) => g.id === groupId);
|
|
14263
|
+
const chip = paneStrip.querySelector(`[data-group="${CSS.escape(groupId)}"]`);
|
|
14264
|
+
if (!group || !chip) return;
|
|
14265
|
+
beginStripRename(chip, {
|
|
14266
|
+
className: 'pane-group-input',
|
|
14267
|
+
value: group.name,
|
|
14268
|
+
label: 'Group name',
|
|
14269
|
+
maxLength: PANE_GROUP_NAME_MAX,
|
|
14270
|
+
save: (name) => updatePaneGroup(sid, groupId, { name: name.slice(0, PANE_GROUP_NAME_MAX) }),
|
|
14271
|
+
});
|
|
14272
|
+
}
|
|
14273
|
+
|
|
14274
|
+
const paneMenuItem = (act, label, arg) =>
|
|
14275
|
+
`<button class="pane-menu-item" role="menuitem" data-pane-act="${escapeHtml(act)}"${arg == null ? '' : ` data-arg="${escapeHtml(arg)}"`}>${label}</button>`;
|
|
14276
|
+
const paneGroupStyle = (color) => escapeHtml(`--g: var(--owner-${color})`);
|
|
14277
|
+
const paneGroupDot = (g) => `<span class="pane-menu-dot" style="${paneGroupStyle(g.color)}"></span>`;
|
|
14278
|
+
|
|
14279
|
+
function paneGroupMenuItems(id) {
|
|
14280
|
+
const sid = paneSessionId();
|
|
14281
|
+
const item = paneStripItems(sid).find((it) => it.ids.includes(id));
|
|
14282
|
+
if (!item || item.ids.includes('board')) return '';
|
|
14283
|
+
const others = getPaneGroups(sid).filter((g) => g.id !== item.group?.id);
|
|
14284
|
+
return (
|
|
14285
|
+
paneMenuItem('group', 'Add to new group', 'new') +
|
|
14286
|
+
others
|
|
14287
|
+
.map((g) => paneMenuItem('group', `Add to ${paneGroupDot(g)}${escapeHtml(g.name || 'unnamed group')}`, g.id))
|
|
14288
|
+
.join('') +
|
|
14289
|
+
(item.group ? paneMenuItem('group', 'Remove from group', '') : '')
|
|
14290
|
+
);
|
|
14094
14291
|
}
|
|
14095
14292
|
|
|
14096
|
-
|
|
14097
|
-
|
|
14293
|
+
function openPaneGroupMenu(x, y, groupId) {
|
|
14294
|
+
const sid = paneSessionId();
|
|
14295
|
+
const group = sid && getPaneGroups(sid).find((g) => g.id === groupId);
|
|
14296
|
+
if (!group) return;
|
|
14297
|
+
const swatches = PANE_GROUP_COLORS.map(
|
|
14298
|
+
(c) =>
|
|
14299
|
+
`<button class="pane-menu-item pane-menu-swatch${c === group.color ? ' on' : ''}" role="menuitemradio" aria-checked="${escapeHtml(String(c === group.color))}" aria-label="${escapeHtml(c)}" title="${escapeHtml(c)}" data-pane-act="groupColor" data-arg="${escapeHtml(c)}" style="${paneGroupStyle(c)}"></button>`,
|
|
14300
|
+
).join('');
|
|
14301
|
+
showPaneMenu(
|
|
14302
|
+
x,
|
|
14303
|
+
y,
|
|
14304
|
+
group.id,
|
|
14305
|
+
'Group actions',
|
|
14306
|
+
`<div class="pane-menu-label">${escapeHtml(group.name || 'Unnamed group')}</div>` +
|
|
14307
|
+
paneMenuItem('groupRename', 'Rename') +
|
|
14308
|
+
paneMenuItem('groupToggle', group.closed ? 'Expand' : 'Collapse') +
|
|
14309
|
+
`<div class="pane-menu-swatches">${swatches}</div>` +
|
|
14310
|
+
paneMenuItem('ungroup', 'Ungroup'),
|
|
14311
|
+
);
|
|
14312
|
+
}
|
|
14098
14313
|
|
|
14099
14314
|
function openPaneMenu(x, y, id) {
|
|
14100
14315
|
if (id === 'board') {
|
|
@@ -14127,6 +14342,7 @@ function openPaneMenu(x, y, id) {
|
|
|
14127
14342
|
: paneMenuItem('link', paneIsLinked(pane) ? 'Remove from linked documents' : 'Add to linked documents')) +
|
|
14128
14343
|
paneMenuItem('rename', 'Rename') +
|
|
14129
14344
|
paneMenuItem('reload', 'Reload') +
|
|
14345
|
+
paneGroupMenuItems(pane.id) +
|
|
14130
14346
|
paneMenuItem('close', 'Close pane'),
|
|
14131
14347
|
);
|
|
14132
14348
|
return true;
|
|
@@ -14169,6 +14385,11 @@ const PANE_MENU_ACTIONS = {
|
|
|
14169
14385
|
rename: beginPaneRename,
|
|
14170
14386
|
reload: reloadPane,
|
|
14171
14387
|
close: closePane,
|
|
14388
|
+
group: (id, to) => groupPaneTab(id, to || null),
|
|
14389
|
+
groupRename: beginPaneGroupRename,
|
|
14390
|
+
groupToggle: togglePaneGroup,
|
|
14391
|
+
groupColor: colorPaneGroup,
|
|
14392
|
+
ungroup: ungroupPanes,
|
|
14172
14393
|
deleteCompleted: () => deleteBoardTasks(true),
|
|
14173
14394
|
deleteAll: () => deleteBoardTasks(false),
|
|
14174
14395
|
};
|
|
@@ -14358,18 +14579,23 @@ function closePanePop() {
|
|
|
14358
14579
|
}
|
|
14359
14580
|
|
|
14360
14581
|
paneStrip.addEventListener('keydown', (e) => {
|
|
14361
|
-
const input = e.target.closest?.('.
|
|
14582
|
+
const input = e.target.closest?.('.strip-rename');
|
|
14362
14583
|
if (input && (e.key === 'Enter' || e.key === 'Escape')) {
|
|
14363
14584
|
e.preventDefault();
|
|
14364
|
-
|
|
14585
|
+
endStripRename(input, e.key === 'Enter');
|
|
14365
14586
|
}
|
|
14366
14587
|
});
|
|
14367
14588
|
paneStrip.addEventListener('focusout', (e) => {
|
|
14368
|
-
const input = e.target.closest?.('.
|
|
14369
|
-
if (input)
|
|
14589
|
+
const input = e.target.closest?.('.strip-rename');
|
|
14590
|
+
if (input) endStripRename(input, true);
|
|
14370
14591
|
});
|
|
14371
14592
|
paneStrip.addEventListener('click', (e) => {
|
|
14372
|
-
if (e.target.closest('.
|
|
14593
|
+
if (e.target.closest('.strip-rename')) return;
|
|
14594
|
+
const chip = e.target.closest('[data-group]');
|
|
14595
|
+
if (chip) {
|
|
14596
|
+
togglePaneGroup(chip.dataset.group);
|
|
14597
|
+
return;
|
|
14598
|
+
}
|
|
14373
14599
|
const close = e.target.closest('[data-close]');
|
|
14374
14600
|
if (close) {
|
|
14375
14601
|
closePane(close.dataset.close);
|
|
@@ -14385,16 +14611,19 @@ paneStrip.addEventListener('auxclick', (e) => {
|
|
|
14385
14611
|
if (e.button === 1 && tab && tab.dataset.pane !== 'board') closePane(tab.dataset.pane);
|
|
14386
14612
|
});
|
|
14387
14613
|
paneStrip.addEventListener('contextmenu', (e) => {
|
|
14614
|
+
const chip = e.target.closest('[data-group]');
|
|
14388
14615
|
const tab = e.target.closest('.pane-tab');
|
|
14389
|
-
if (!tab) return;
|
|
14616
|
+
if (!chip && !tab) return;
|
|
14390
14617
|
e.preventDefault();
|
|
14391
|
-
|
|
14618
|
+
if (chip) openPaneGroupMenu(e.clientX, e.clientY, chip.dataset.group);
|
|
14619
|
+
else openPaneMenu(e.clientX, e.clientY, tab.dataset.pane);
|
|
14392
14620
|
});
|
|
14393
14621
|
document.addEventListener('click', (e) => {
|
|
14394
14622
|
const menu = document.getElementById('pane-menu');
|
|
14395
14623
|
if (!menu) return;
|
|
14396
|
-
const
|
|
14397
|
-
|
|
14624
|
+
const item = e.target.closest('.pane-menu-item');
|
|
14625
|
+
const act = item?.dataset.paneAct;
|
|
14626
|
+
if (act) PANE_MENU_ACTIONS[act](menu.dataset.pane, item.dataset.arg);
|
|
14398
14627
|
if (act) closePaneMenu();
|
|
14399
14628
|
else if (!menu.contains(e.target)) closePaneMenu(true);
|
|
14400
14629
|
});
|
|
@@ -14470,15 +14699,18 @@ paneStrip.addEventListener('dragstart', (e) => {
|
|
|
14470
14699
|
});
|
|
14471
14700
|
|
|
14472
14701
|
// A split shows as one strip item, so a drop moves the dragged tab's whole item; the first item
|
|
14473
|
-
// holds the board and stays put.
|
|
14702
|
+
// holds the board and stays put. A moved item joins the group it lands in, and a drop on a
|
|
14703
|
+
// group's chip adds it to that group. Elsewhere on the strip, a split tab's drop ends its split.
|
|
14474
14704
|
function paneDropAction(e) {
|
|
14475
14705
|
const sid = paneSessionId();
|
|
14476
14706
|
const tab = e.target.closest?.('.pane-tab');
|
|
14707
|
+
const chip = e.target.closest?.('[data-group]');
|
|
14477
14708
|
if (!sid || !paneDragId) return null;
|
|
14478
|
-
if (tab) {
|
|
14709
|
+
if (chip || tab) {
|
|
14479
14710
|
const [fixed, ...movable] = paneStripItems(sid);
|
|
14480
14711
|
const itemOf = (id) => movable.find((it) => it.ids.includes(id));
|
|
14481
14712
|
const from = itemOf(paneDragId);
|
|
14713
|
+
if (chip) return from ? { kind: 'group', sid, el: chip, group: chip.dataset.group } : null;
|
|
14482
14714
|
const to = itemOf(tab.dataset.pane);
|
|
14483
14715
|
if (from && to && from !== to) {
|
|
14484
14716
|
const el = to.pair ? tab.closest('.pane-pair') : tab;
|
|
@@ -14492,9 +14724,11 @@ function paneDropAction(e) {
|
|
|
14492
14724
|
|
|
14493
14725
|
function markPaneDrop(action) {
|
|
14494
14726
|
paneStrip.classList.toggle('drop-hot', action?.kind === 'unpair');
|
|
14495
|
-
for (const el of paneStrip.querySelectorAll('.pane-tab, .pane-pair')) {
|
|
14496
|
-
|
|
14497
|
-
el.classList.toggle('drop-
|
|
14727
|
+
for (const el of paneStrip.querySelectorAll('.pane-tab, .pane-pair, .pane-group-chip')) {
|
|
14728
|
+
const on = el === action?.el;
|
|
14729
|
+
el.classList.toggle('drop-in', on && action.kind === 'group');
|
|
14730
|
+
el.classList.toggle('drop-before', on && action.kind === 'move' && !action.after);
|
|
14731
|
+
el.classList.toggle('drop-after', on && action.kind === 'move' && action.after);
|
|
14498
14732
|
}
|
|
14499
14733
|
}
|
|
14500
14734
|
|
|
@@ -14521,11 +14755,15 @@ paneStrip.addEventListener('drop', (e) => {
|
|
|
14521
14755
|
if (!action) return;
|
|
14522
14756
|
e.preventDefault();
|
|
14523
14757
|
const { sid, pair } = action;
|
|
14758
|
+
const id = paneDragId;
|
|
14524
14759
|
const shown = pair && !!splitSideOf(pair, getActivePaneId(sid));
|
|
14525
|
-
const keep = shown ? pair[otherSide(splitSideOf(pair,
|
|
14760
|
+
const keep = shown ? pair[otherSide(splitSideOf(pair, id))] : null;
|
|
14526
14761
|
endPaneDrag();
|
|
14527
|
-
if (action.kind === '
|
|
14528
|
-
else
|
|
14762
|
+
if (action.kind === 'group') groupPaneTab(id, action.group);
|
|
14763
|
+
else if (action.kind === 'move') {
|
|
14764
|
+
if (action.from.group?.id !== action.to.group?.id) groupPaneTab(id, action.to.group?.id ?? null);
|
|
14765
|
+
movePaneItem(action);
|
|
14766
|
+
} else unpairPanes(sid, pair, keep);
|
|
14529
14767
|
});
|
|
14530
14768
|
paneDrop.addEventListener('dragover', (e) => {
|
|
14531
14769
|
if (!paneDragId) return;
|
package/public/style.css
CHANGED
|
@@ -1204,7 +1204,6 @@ body.booting .boot-splash {
|
|
|
1204
1204
|
|
|
1205
1205
|
/* The live-workflow bar takes the muted fill too: its card already carries a pulsing
|
|
1206
1206
|
accent dot, and a second accent bar under it shouts. */
|
|
1207
|
-
.session-item:not(.active) .progress-fill,
|
|
1208
1207
|
.wf-live-bar .progress-fill {
|
|
1209
1208
|
background: color-mix(in srgb, var(--text-tertiary) 55%, transparent);
|
|
1210
1209
|
}
|
|
@@ -1216,6 +1215,14 @@ body.booting .boot-splash {
|
|
|
1216
1215
|
font-variant-numeric: tabular-nums;
|
|
1217
1216
|
}
|
|
1218
1217
|
|
|
1218
|
+
.session-progress .progress-bar {
|
|
1219
|
+
display: flex;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
.progress-fill.done {
|
|
1223
|
+
background: var(--success);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1219
1226
|
.context-bar-labels {
|
|
1220
1227
|
display: flex;
|
|
1221
1228
|
justify-content: space-between;
|
|
@@ -4489,6 +4496,75 @@ body.show-resizing iframe {
|
|
|
4489
4496
|
background: var(--border);
|
|
4490
4497
|
}
|
|
4491
4498
|
|
|
4499
|
+
.pane-group {
|
|
4500
|
+
position: relative;
|
|
4501
|
+
display: flex;
|
|
4502
|
+
gap: 2px;
|
|
4503
|
+
min-width: 0;
|
|
4504
|
+
}
|
|
4505
|
+
|
|
4506
|
+
/* Over the tabs, so a tab's own background does not hide it. */
|
|
4507
|
+
.pane-group:not(.closed)::after {
|
|
4508
|
+
content: '';
|
|
4509
|
+
position: absolute;
|
|
4510
|
+
inset: auto 0 1px;
|
|
4511
|
+
z-index: 1;
|
|
4512
|
+
height: 2px;
|
|
4513
|
+
background: var(--g);
|
|
4514
|
+
pointer-events: none;
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
.pane-group-chip,
|
|
4518
|
+
.pane-group-input {
|
|
4519
|
+
flex: none;
|
|
4520
|
+
align-self: center;
|
|
4521
|
+
height: 18px;
|
|
4522
|
+
margin: 0 2px;
|
|
4523
|
+
padding: 0 8px;
|
|
4524
|
+
border: 0;
|
|
4525
|
+
border-radius: 6px;
|
|
4526
|
+
font-family: var(--font-mono);
|
|
4527
|
+
font-size: 11px;
|
|
4528
|
+
font-weight: 600;
|
|
4529
|
+
letter-spacing: 0.3px;
|
|
4530
|
+
}
|
|
4531
|
+
|
|
4532
|
+
.pane-group-chip {
|
|
4533
|
+
min-width: 14px;
|
|
4534
|
+
max-width: 140px;
|
|
4535
|
+
overflow: hidden;
|
|
4536
|
+
text-overflow: ellipsis;
|
|
4537
|
+
white-space: nowrap;
|
|
4538
|
+
background: var(--g);
|
|
4539
|
+
color: var(--bg-deep);
|
|
4540
|
+
cursor: pointer;
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
.pane-group-chip:hover {
|
|
4544
|
+
filter: brightness(1.12);
|
|
4545
|
+
}
|
|
4546
|
+
|
|
4547
|
+
.pane-group-count {
|
|
4548
|
+
font-weight: 400;
|
|
4549
|
+
opacity: 0.75;
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
.pane-group-count:not(:first-child) {
|
|
4553
|
+
margin-left: 6px;
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
.pane-group-chip.drop-in {
|
|
4557
|
+
box-shadow: 0 0 0 2px var(--accent);
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
.pane-group-input {
|
|
4561
|
+
width: 110px;
|
|
4562
|
+
color: var(--text-primary);
|
|
4563
|
+
background: var(--bg-deep);
|
|
4564
|
+
box-shadow: inset 0 0 0 1px var(--g);
|
|
4565
|
+
outline: none;
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4492
4568
|
.pane-strip.drop-hot {
|
|
4493
4569
|
background: var(--accent-dim);
|
|
4494
4570
|
}
|
|
@@ -10441,4 +10517,32 @@ pre.mermaid[data-mermaid-error]::before {
|
|
|
10441
10517
|
color: var(--text-primary);
|
|
10442
10518
|
font-weight: 600;
|
|
10443
10519
|
}
|
|
10520
|
+
.pane-menu-dot {
|
|
10521
|
+
flex: none;
|
|
10522
|
+
width: 8px;
|
|
10523
|
+
height: 8px;
|
|
10524
|
+
border-radius: 50%;
|
|
10525
|
+
background: var(--g);
|
|
10526
|
+
}
|
|
10527
|
+
.pane-menu-swatches {
|
|
10528
|
+
display: flex;
|
|
10529
|
+
gap: 4px;
|
|
10530
|
+
padding: 4px 8px;
|
|
10531
|
+
}
|
|
10532
|
+
.pane-menu-item.pane-menu-swatch {
|
|
10533
|
+
flex: none;
|
|
10534
|
+
width: 16px;
|
|
10535
|
+
height: 16px;
|
|
10536
|
+
padding: 0;
|
|
10537
|
+
border-radius: 50%;
|
|
10538
|
+
background: var(--g);
|
|
10539
|
+
}
|
|
10540
|
+
.pane-menu-item.pane-menu-swatch:hover,
|
|
10541
|
+
.pane-menu-item.pane-menu-swatch:focus-visible,
|
|
10542
|
+
.pane-menu-swatch.on {
|
|
10543
|
+
background: var(--g);
|
|
10544
|
+
box-shadow:
|
|
10545
|
+
0 0 0 2px var(--bg-elevated),
|
|
10546
|
+
0 0 0 3px var(--text-primary);
|
|
10547
|
+
}
|
|
10444
10548
|
/* #endregion */
|