local-kanban 1.14.1 → 1.15.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/CHANGELOG.md +18 -0
- package/bin/kb +4 -2
- package/package.json +1 -1
- package/public/js/board.js +92 -25
- package/public/js/core.js +6 -0
- package/public/js/drawer.js +4 -3
- package/public/js/init.js +1 -1
- package/public/js/order.js +37 -0
- package/public/style.css +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,24 @@ version it goes out as and opens an empty one again. Released sections are histo
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.15.0]
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **A column can be sorted by task number.** Every column with more than one card has a sort button
|
|
16
|
+
in its header: by priority and your own order (as before), by number with the newest first, or by
|
|
17
|
+
number with the oldest first. Pinned cards stay on top either way. The choice is remembered per
|
|
18
|
+
board and column in this browser and changes only what you see: the order `kb take` follows stays
|
|
19
|
+
the same. While a column is sorted by number its cards cannot be reordered by hand, and a card
|
|
20
|
+
dragged in from another column lands where its number puts it.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`kb link` refuses a link type it does not know, whatever the word.** `kb link A B constructor` (or
|
|
25
|
+
`toString`) was taken for a valid type, because every object answers for those names, and the
|
|
26
|
+
board quietly made the link `related`. Now it is refused with exit code 2 before anything is
|
|
27
|
+
written, like any other unknown word.
|
|
28
|
+
|
|
11
29
|
## [1.14.1]
|
|
12
30
|
|
|
13
31
|
### Changed
|
package/bin/kb
CHANGED
|
@@ -309,10 +309,12 @@ const commands = {
|
|
|
309
309
|
|
|
310
310
|
// kb link A B [child|parent|related] — `child`: B becomes a sub-task of A, `parent`: B becomes
|
|
311
311
|
// A's parent. Without a third word the link is plain `related`, as it always was. A pair holds
|
|
312
|
-
// one link, so linking again with another word re-types it.
|
|
312
|
+
// one link, so linking again with another word re-types it. The word is looked up as an OWN key:
|
|
313
|
+
// `REL[rel]` also answers for `constructor`, `toString` or `__proto__` (they come from the
|
|
314
|
+
// prototype), and such a word went to the board, which quietly made the link `related`.
|
|
313
315
|
async link() {
|
|
314
316
|
const [id, other, rel = 'related'] = args;
|
|
315
|
-
if (!id || !other || !REL
|
|
317
|
+
if (!id || !other || !Object.hasOwn(REL, rel)) die('kb link <key> <key> [child|parent|related] (child: the second task becomes a sub-task of the first)');
|
|
316
318
|
const links = await api('POST', `/api/tasks/${id}/links`, { key: other, rel });
|
|
317
319
|
console.log(linksLine(id, links));
|
|
318
320
|
},
|
package/package.json
CHANGED
package/public/js/board.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
import { $, ALL, ALL_STATUSES, api, DONE_LIMIT, esc, ic, LABEL_COLORS, LABEL_SELECTABLE, noteUnsaved, popoverPlacement, PRI_ICON, PRI_LEVELS, seg, state, tr } from './core.js';
|
|
3
3
|
import { markKbCursor, openCardMenu, openDrawer, openDrawerNew, setKbCursor, statusOptionList } from './drawer.js';
|
|
4
|
+
import { columnComparator, DEFAULT_SORT, orderColumn, slotBefore, sortMode } from './order.js';
|
|
4
5
|
import { getSetting, plural } from './settings.js';
|
|
5
|
-
import { styledAlert, styledConfirm } from './sidebar.js';
|
|
6
|
+
import { popLayer, pushLayer, styledAlert, styledConfirm } from './sidebar.js';
|
|
6
7
|
import { copyBacklogPrompt, copyTodoPrompt, refresh } from './sse.js';
|
|
7
8
|
|
|
8
9
|
function matchesSearch(t) {
|
|
@@ -38,19 +39,22 @@ export function renderBoard() {
|
|
|
38
39
|
const columns = ALL_STATUSES.map(([k]) => k)
|
|
39
40
|
.filter((k) => k !== 'cancelled' || state.tasks.some((t) => t.status === 'cancelled'));
|
|
40
41
|
for (const key of columns) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0) || b.priority - a.priority || a.position - b.position || a.id - b.id);
|
|
44
|
-
if (key === 'done' || key === 'cancelled') {
|
|
45
|
-
tasks = tasks.sort((a, b) => b.id - a.id).slice(0, DONE_LIMIT)
|
|
46
|
-
.sort((a, b) => b.priority - a.priority || b.id - a.id);
|
|
47
|
-
}
|
|
42
|
+
const mode = colSort(key);
|
|
43
|
+
const tasks = orderColumn(state.tasks.filter((t) => t.status === key && matchesSearch(t)), mode, colOrderOpts(key));
|
|
48
44
|
|
|
49
45
|
const col = document.createElement('div');
|
|
50
46
|
col.className = 'col';
|
|
51
47
|
const head = document.createElement('div');
|
|
52
48
|
head.className = 'col-head';
|
|
53
49
|
head.innerHTML = `<span class="dot s-${key}"></span>${labels[key]} <span class="n">${tasks.length}</span>`;
|
|
50
|
+
if (tasks.length > 1 || mode !== DEFAULT_SORT) {
|
|
51
|
+
const sort = document.createElement('button');
|
|
52
|
+
sort.className = `btn-icon col-sort${mode !== DEFAULT_SORT ? ' on' : ''}`;
|
|
53
|
+
sort.innerHTML = ic('sort', 13);
|
|
54
|
+
sort.title = mode === DEFAULT_SORT ? tr('Sort the column') : tr(SORT_LABELS[mode]);
|
|
55
|
+
sort.onclick = (e) => { e.stopPropagation(); openColSortMenu(sort, key); };
|
|
56
|
+
head.appendChild(sort);
|
|
57
|
+
}
|
|
54
58
|
if (CREATABLE.has(key)) {
|
|
55
59
|
const add = document.createElement('button');
|
|
56
60
|
add.className = 'btn-icon col-add';
|
|
@@ -88,7 +92,7 @@ export function renderBoard() {
|
|
|
88
92
|
e.preventDefault();
|
|
89
93
|
body.classList.add('dragover');
|
|
90
94
|
if (dragId == null) return;
|
|
91
|
-
const after = dragAfterCard(body, e.clientY, dragId);
|
|
95
|
+
const after = colSort(key) === DEFAULT_SORT ? dragAfterCard(body, e.clientY, dragId) : sortedSlotCard(body, key, dt);
|
|
92
96
|
if (dropPh.parentNode !== body || dropPh.nextSibling !== after) body.insertBefore(dropPh, after);
|
|
93
97
|
};
|
|
94
98
|
body.ondragleave = (e) => { if (!body.contains(e.relatedTarget)) body.classList.remove('dragover'); };
|
|
@@ -470,6 +474,66 @@ export async function multiAction(act, btn) {
|
|
|
470
474
|
}
|
|
471
475
|
}
|
|
472
476
|
|
|
477
|
+
const SORT_KEY = 'kb.ui.colSort';
|
|
478
|
+
const SORT_LABELS = {
|
|
479
|
+
manual: 'By priority, then by hand',
|
|
480
|
+
'num-desc': 'By number, newest first',
|
|
481
|
+
'num-asc': 'By number, oldest first',
|
|
482
|
+
};
|
|
483
|
+
function readColSorts() {
|
|
484
|
+
try {
|
|
485
|
+
const v = JSON.parse(localStorage.getItem(SORT_KEY));
|
|
486
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
487
|
+
} catch { return {}; }
|
|
488
|
+
}
|
|
489
|
+
export function colSort(status) { return sortMode(readColSorts()[`${state.slug}:${status}`]); }
|
|
490
|
+
function setColSort(status, mode) {
|
|
491
|
+
const all = readColSorts();
|
|
492
|
+
const k = `${state.slug}:${status}`;
|
|
493
|
+
if (sortMode(mode) === DEFAULT_SORT) delete all[k]; else all[k] = mode;
|
|
494
|
+
try { localStorage.setItem(SORT_KEY, JSON.stringify(all)); } catch { }
|
|
495
|
+
renderBoard();
|
|
496
|
+
}
|
|
497
|
+
function colOrderOpts(status) {
|
|
498
|
+
return { mixed: state.slug === ALL, closed: status === 'done' || status === 'cancelled', limit: DONE_LIMIT };
|
|
499
|
+
}
|
|
500
|
+
function sortedSlotCard(body, status, t) {
|
|
501
|
+
const id = slotBefore(state.tasks.filter((x) => x.status === status && matchesSearch(x)), t, colSort(status), colOrderOpts(status));
|
|
502
|
+
return id == null ? null : body.querySelector(`.card[data-id="${id}"]`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function closeColSortMenu() {
|
|
506
|
+
popLayer('col-sort-menu');
|
|
507
|
+
$('col-sort-menu')?.remove();
|
|
508
|
+
document.removeEventListener('click', colSortOutside);
|
|
509
|
+
}
|
|
510
|
+
function colSortOutside(e) { if (!e.target.closest('#col-sort-menu')) closeColSortMenu(); }
|
|
511
|
+
function openColSortMenu(anchor, status) {
|
|
512
|
+
const was = $('col-sort-menu')?.dataset.status;
|
|
513
|
+
closeColSortMenu();
|
|
514
|
+
if (was === status) return;
|
|
515
|
+
pushLayer('col-sort-menu', closeColSortMenu);
|
|
516
|
+
const menu = document.createElement('div');
|
|
517
|
+
menu.className = 'drawer-menu-pop col-sort-menu';
|
|
518
|
+
menu.id = 'col-sort-menu';
|
|
519
|
+
menu.dataset.status = status;
|
|
520
|
+
const cur = colSort(status);
|
|
521
|
+
menu.innerHTML = Object.entries(SORT_LABELS).map(([v, label]) =>
|
|
522
|
+
`<div class="dm-item${v === cur ? ' on' : ''}" data-v="${v}">${esc(tr(label))}${v === cur ? '<span class="dm-chev">✓</span>' : ''}</div>`).join('')
|
|
523
|
+
+ `<div class="dm-sep"></div><div class="dm-hint">${esc(tr('Sorted by number, cards cannot be reordered by hand'))}</div>`;
|
|
524
|
+
document.body.appendChild(menu);
|
|
525
|
+
const r = anchor.getBoundingClientRect();
|
|
526
|
+
let left = r.left;
|
|
527
|
+
if (left + menu.offsetWidth > window.innerWidth - 8) left = window.innerWidth - menu.offsetWidth - 8;
|
|
528
|
+
menu.style.left = `${Math.max(8, left)}px`;
|
|
529
|
+
menu.style.top = `${r.bottom + 4}px`;
|
|
530
|
+
menu.querySelectorAll('.dm-item').forEach((el) => {
|
|
531
|
+
el.onclick = () => { closeColSortMenu(); if (el.dataset.v !== cur) setColSort(status, el.dataset.v); };
|
|
532
|
+
});
|
|
533
|
+
requestAnimationFrame(() => menu.classList.add('open'));
|
|
534
|
+
document.addEventListener('click', colSortOutside);
|
|
535
|
+
}
|
|
536
|
+
|
|
473
537
|
function dragAfterCard(body, y, draggedId) {
|
|
474
538
|
const cards = [...body.querySelectorAll('.card')].filter((c) => Number(c.dataset.id) !== draggedId);
|
|
475
539
|
return cards.find((c) => y < c.getBoundingClientRect().top + c.offsetHeight / 2) || null;
|
|
@@ -587,16 +651,19 @@ async function onDrop(e, status, body) {
|
|
|
587
651
|
if (dragGroupIds && dragGroupIds.length > 1) return onDropGroup(e, status, body);
|
|
588
652
|
if (moveBlocked(t.status, status, { manual: taskIsNoclaude(t) })) return;
|
|
589
653
|
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
let
|
|
593
|
-
if (
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
.
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
654
|
+
const fixedOrder = colSort(status) !== DEFAULT_SORT;
|
|
655
|
+
if (fixedOrder && t.status === status) { dropPh.remove(); return; }
|
|
656
|
+
let patch = { status };
|
|
657
|
+
if (!fixedOrder) {
|
|
658
|
+
const cards = [...body.querySelectorAll('.card')].filter((c) => Number(c.dataset.id) !== id);
|
|
659
|
+
const y = e.clientY;
|
|
660
|
+
let idx = cards.findIndex((c) => y < c.getBoundingClientRect().top + c.offsetHeight / 2);
|
|
661
|
+
if (idx === -1) idx = cards.length;
|
|
662
|
+
const colTasks = state.tasks.filter((x) => x.status === status && x.id !== id).sort(columnComparator(DEFAULT_SORT));
|
|
663
|
+
const prev = colTasks[idx - 1]?.position ?? 0;
|
|
664
|
+
const next = colTasks[idx]?.position ?? prev + 2;
|
|
665
|
+
patch = { status, position: (prev + next) / 2 };
|
|
666
|
+
}
|
|
600
667
|
|
|
601
668
|
const card = $('board').querySelector(`.card[data-id="${id}"]`);
|
|
602
669
|
if (card) {
|
|
@@ -607,10 +674,10 @@ async function onDrop(e, status, body) {
|
|
|
607
674
|
}
|
|
608
675
|
dropPh.remove();
|
|
609
676
|
try {
|
|
610
|
-
await api('PATCH', `/api/tasks/${seg(id)}`,
|
|
677
|
+
await api('PATCH', `/api/tasks/${seg(id)}`, patch);
|
|
611
678
|
} catch (e) {
|
|
612
679
|
renderBoard();
|
|
613
|
-
if (e?.offline) noteUnsaved(() => api('PATCH', `/api/tasks/${seg(id)}`,
|
|
680
|
+
if (e?.offline) noteUnsaved(() => api('PATCH', `/api/tasks/${seg(id)}`, patch).then(refresh));
|
|
614
681
|
return;
|
|
615
682
|
}
|
|
616
683
|
await refresh();
|
|
@@ -637,16 +704,16 @@ async function onDropGroup(e, status, body) {
|
|
|
637
704
|
const y = e.clientY;
|
|
638
705
|
let idx = cards.findIndex((c) => y < c.getBoundingClientRect().top + c.offsetHeight / 2);
|
|
639
706
|
if (idx === -1) idx = cards.length;
|
|
640
|
-
const colTasks = state.tasks
|
|
641
|
-
.filter((x) => x.status === status && !draggedSet.has(x.id))
|
|
642
|
-
.sort((a, b) => b.priority - a.priority || a.position - b.position || a.id - b.id);
|
|
707
|
+
const colTasks = state.tasks.filter((x) => x.status === status && !draggedSet.has(x.id)).sort(columnComparator(DEFAULT_SORT));
|
|
643
708
|
const prev = colTasks[idx - 1]?.position ?? 0;
|
|
644
709
|
const next = colTasks[idx]?.position ?? prev + movable.length + 1;
|
|
645
710
|
const step = (next - prev) / (movable.length + 1);
|
|
711
|
+
const fixedOrder = colSort(status) !== DEFAULT_SORT;
|
|
646
712
|
|
|
647
713
|
dropPh.remove();
|
|
648
714
|
for (let i = 0; i < movable.length; i++) {
|
|
649
|
-
|
|
715
|
+
if (fixedOrder && movable[i].status === status) continue;
|
|
716
|
+
await api('PATCH', `/api/tasks/${seg(movable[i].id)}`, fixedOrder ? { status } : { status, position: prev + step * (i + 1) });
|
|
650
717
|
}
|
|
651
718
|
clearSelection();
|
|
652
719
|
await refresh();
|
package/public/js/core.js
CHANGED
|
@@ -13,6 +13,7 @@ const ICONS = {
|
|
|
13
13
|
warn: 'M8 2.6 14.4 13.4H1.6L8 2.6ZM8 6.6v3M8 11.4v.1',
|
|
14
14
|
trash: 'M2.8 4.4h10.4M6.4 4.4V2.9h3.2v1.5M4.2 4.4l.6 8.7a1 1 0 0 0 1 .9h4.4a1 1 0 0 0 1-.9l.6-8.7M6.7 6.9v4.6M9.3 6.9v4.6',
|
|
15
15
|
plus: 'M8 3.4v9.2M3.4 8h9.2',
|
|
16
|
+
sort: 'M4.8 2.8v10.4M4.8 13.2 2.4 10.8M4.8 13.2l2.4-2.4M11.2 13.2V2.8M11.2 2.8 8.8 5.2M11.2 2.8l2.4 2.4',
|
|
16
17
|
download: 'M8 2.6v7.8M5 7.4 8 10.4l3-3M2.8 12.6v.8a1 1 0 0 0 1 1h8.4a1 1 0 0 0 1-1v-.8',
|
|
17
18
|
duplicate: 'M3.4 10.6H2.6a1 1 0 0 1-1-1V2.6a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v.8M6.4 5.4h7a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1Z',
|
|
18
19
|
person: 'M8 8.4a2.6 2.6 0 1 0 0-5.2 2.6 2.6 0 0 0 0 5.2ZM3.2 14a4.8 4.8 0 0 1 9.6 0',
|
|
@@ -111,6 +112,9 @@ const I18N_RU = {
|
|
|
111
112
|
'built-in section — cannot be deleted': 'служебный раздел — удалить нельзя',
|
|
112
113
|
'built-in section — the name cannot be changed': 'служебный раздел — имя менять нельзя',
|
|
113
114
|
'Busiest around': 'Жарче всего около',
|
|
115
|
+
'By number, newest first': 'По номеру: сначала новые',
|
|
116
|
+
'By number, oldest first': 'По номеру: сначала старые',
|
|
117
|
+
'By priority, then by hand': 'По приоритету, затем вручную',
|
|
114
118
|
'Calendar': 'Календарь',
|
|
115
119
|
'Cancel': 'Отмена',
|
|
116
120
|
'Cancel task': 'Отменить задачу',
|
|
@@ -582,6 +586,8 @@ const I18N_RU = {
|
|
|
582
586
|
'Setup via Claude': 'Настройка через Claude',
|
|
583
587
|
'Skip': 'Пропустить',
|
|
584
588
|
'Skip — work locally': 'Пропустить — работать локально',
|
|
589
|
+
'Sort the column': 'Сортировка колонки',
|
|
590
|
+
'Sorted by number, cards cannot be reordered by hand': 'При сортировке по номеру карточки вручную не переставить',
|
|
585
591
|
'Sound diagnostics': 'Диагностика звука',
|
|
586
592
|
'Sound for Done': 'Звук на «Готово»',
|
|
587
593
|
'Sound for Review': 'Звук на «Проверяю»',
|
package/public/js/drawer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
|
-
import { WORKING, buildSelect, canMoveTo, clearSelection, labelChip, moveBlocked, scheduleDrawLinks, selVal, selectedIds, setScrollToNewCardId, taskIsNoclaude } from './board.js';
|
|
2
|
+
import { WORKING, buildSelect, canMoveTo, clearSelection, colSort, labelChip, moveBlocked, scheduleDrawLinks, selVal, selectedIds, setScrollToNewCardId, taskIsNoclaude } from './board.js';
|
|
3
3
|
import { $, ALL, ALL_STATUSES, DASH, LABEL_COLORS, LABEL_SELECTABLE, PRI_LEVELS, SETTINGS, TITLE_LABEL_WORDS, api, apiBlob, esc, ghSyncOn, ic, noteUnsaved, seg, state, tr } from './core.js';
|
|
4
|
+
import { columnComparator, DEFAULT_SORT } from './order.js';
|
|
4
5
|
import { getSetting } from './settings.js';
|
|
5
6
|
import { currentProject, overlayLayer, popLayer, pushLayer, selectProject, styledAlert, styledConfirm } from './sidebar.js';
|
|
6
7
|
import { autoGrow, copyText, refresh, relTime, renderTimeline } from './sse.js';
|
|
@@ -393,12 +394,12 @@ function syncDrawerToCursor() {
|
|
|
393
394
|
if (t && t.key !== state.drawerKey) openDrawer(t.key);
|
|
394
395
|
}, 150);
|
|
395
396
|
}
|
|
396
|
-
const kbBoardSort = (a, b) => b.priority - a.priority || a.position - b.position || a.id - b.id;
|
|
397
397
|
export async function kbMoveCard(dr, dc) {
|
|
398
398
|
const t = state.tasks.find((x) => x.id === state.kbCursor);
|
|
399
399
|
if (!t) return;
|
|
400
400
|
if (dr) {
|
|
401
|
-
|
|
401
|
+
if (colSort(t.status) !== DEFAULT_SORT) return;
|
|
402
|
+
const sorted = state.tasks.filter((x) => x.status === t.status).sort(columnComparator(DEFAULT_SORT));
|
|
402
403
|
const ri = sorted.findIndex((x) => x.id === t.id);
|
|
403
404
|
const ti = Math.max(0, Math.min(sorted.length - 1, ri + dr));
|
|
404
405
|
if (ti === ri) return;
|
package/public/js/init.js
CHANGED
|
@@ -58,7 +58,7 @@ $('search').addEventListener('input', (e) => {
|
|
|
58
58
|
window.addEventListener('resize', scheduleDrawLinks);
|
|
59
59
|
$('board').addEventListener('scroll', scheduleDrawLinks, true);
|
|
60
60
|
|
|
61
|
-
const POPUP_SELECTOR = '.modal-overlay, #card-menu, #drawer-menu-pop, #return-pop, .lbl-picker, #status-picker, .lightbox, .kbsel.open';
|
|
61
|
+
const POPUP_SELECTOR = '.modal-overlay, #card-menu, #col-sort-menu, #drawer-menu-pop, #return-pop, .lbl-picker, #status-picker, .lightbox, .kbsel.open';
|
|
62
62
|
|
|
63
63
|
function escClosePopup() {
|
|
64
64
|
if (document.querySelector('.kbsel.open')) { closeOpenSelect(); return true; }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
export const SORT_MODES = ['manual', 'num-desc', 'num-asc'];
|
|
3
|
+
export const DEFAULT_SORT = 'manual';
|
|
4
|
+
|
|
5
|
+
export function sortMode(v) { return SORT_MODES.includes(v) ? v : DEFAULT_SORT; }
|
|
6
|
+
|
|
7
|
+
export function taskNumber(t) {
|
|
8
|
+
const n = Number(String(t.key || '').split('-').pop());
|
|
9
|
+
return Number.isFinite(n) ? n : 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const pinnedFirst = (a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0);
|
|
13
|
+
|
|
14
|
+
export function columnComparator(mode, { mixed = false, closed = false } = {}) {
|
|
15
|
+
const m = sortMode(mode);
|
|
16
|
+
const pin = closed ? () => 0 : pinnedFirst;
|
|
17
|
+
if (m === DEFAULT_SORT) {
|
|
18
|
+
return closed
|
|
19
|
+
? (a, b) => b.priority - a.priority || b.id - a.id
|
|
20
|
+
: (a, b) => pin(a, b) || b.priority - a.priority || a.position - b.position || a.id - b.id;
|
|
21
|
+
}
|
|
22
|
+
const num = mixed ? (t) => t.id : taskNumber;
|
|
23
|
+
const dir = m === 'num-asc' ? 1 : -1;
|
|
24
|
+
return (a, b) => pin(a, b) || dir * (num(a) - num(b)) || dir * (a.id - b.id);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function orderColumn(tasks, mode, { mixed = false, closed = false, limit = Infinity } = {}) {
|
|
28
|
+
let list = [...tasks];
|
|
29
|
+
if (closed) list = list.sort((a, b) => b.id - a.id).slice(0, limit);
|
|
30
|
+
return list.sort(columnComparator(mode, { mixed, closed }));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function slotBefore(tasks, task, mode, opts = {}) {
|
|
34
|
+
const cmp = columnComparator(mode, opts);
|
|
35
|
+
const next = orderColumn(tasks.filter((x) => x.id !== task.id), mode, opts).find((x) => cmp(task, x) < 0);
|
|
36
|
+
return next ? next.id : null;
|
|
37
|
+
}
|
package/public/style.css
CHANGED
|
@@ -229,7 +229,14 @@ html.booted #horizon, html.booted #calendar, html.booted #chaos { animation: boo
|
|
|
229
229
|
}
|
|
230
230
|
.col-head .dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
|
|
231
231
|
.col-head .n { color: var(--muted); font-weight: 400; }
|
|
232
|
-
.col-
|
|
232
|
+
.col-head .n + .btn-icon { margin-left: auto; }
|
|
233
|
+
.col-sort { font-size: 12px; opacity: .65; }
|
|
234
|
+
.col-sort:hover { opacity: 1; }
|
|
235
|
+
.col-sort.on { opacity: 1; color: var(--accent); }
|
|
236
|
+
.col-sort-menu { transform-origin: top left; min-width: 214px; }
|
|
237
|
+
.col-sort-menu .dm-item.on { color: var(--accent); }
|
|
238
|
+
.col-sort-menu .dm-chev { color: var(--accent); }
|
|
239
|
+
.dm-hint { width: 0; min-width: 100%; box-sizing: border-box; padding: 3px 10px 6px; font-size: 11.5px; line-height: 1.35; color: var(--muted); }
|
|
233
240
|
.col-copy { font-size: 12px; opacity: .65; }
|
|
234
241
|
.col-copy:hover { opacity: 1; }
|
|
235
242
|
.col-copy.copied { opacity: 1; color: var(--done); font-size: 14px; }
|