local-kanban 1.9.0 → 1.11.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 +41 -0
- package/package.json +1 -1
- package/public/js/board.js +4 -1
- package/public/js/chaos.js +3 -6
- package/public/js/core.js +18 -1
- package/public/js/project.js +19 -7
- package/public/js/sidebar.js +10 -11
- package/public/style.css +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,47 @@
|
|
|
3
3
|
Notable changes, newest first. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
4
4
|
and the project uses [semantic versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [1.11.0]
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **The server fields say which side they are about.** "SSH host" and "Server path" gave no hint of
|
|
11
|
+
whether they meant this computer or the machine you deploy to — a fair question, and the owner of
|
|
12
|
+
the board asked it. A line above the group now answers it, and the ssh field says the part you
|
|
13
|
+
cannot guess: the user, port and key come from your own `~/.ssh/config`, and the board stores no
|
|
14
|
+
secrets. Leaving them empty is a normal state — the project simply stays local. On a Russian board
|
|
15
|
+
the ssh placeholder used to stay in English, and the server path field on the project page had no
|
|
16
|
+
example at all.
|
|
17
|
+
|
|
18
|
+
## [1.10.0]
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **The deploy skill list only offers deploy skills.** On the project settings page it listed
|
|
23
|
+
everything in your skills folder — on a real board that is ten entries when one of them is an
|
|
24
|
+
actual deploy skill, and the board's own skill sits among them, offered as a way to deploy your
|
|
25
|
+
project. The wizard already filtered this; the project page did not, because the rule lived in
|
|
26
|
+
the wizard's file. Now both ask the same function. A skill your project already points at stays
|
|
27
|
+
in the list even if it falls outside the filter, so saving never quietly clears the field.
|
|
28
|
+
|
|
29
|
+
- **A dropdown opens where there is room.** The custom select always opened downward with a fixed
|
|
30
|
+
height, so near the bottom of a dialog its list ran past the dialog and under the buttons. It now
|
|
31
|
+
measures the space first — against the dialog, not just the window — and flips up when it has to.
|
|
32
|
+
The rule already existed for the picker menus; there is one copy of it now instead of one and a
|
|
33
|
+
half.
|
|
34
|
+
|
|
35
|
+
- **The Add project form is not cut off.** Its body was capped at 62% of the window height while
|
|
36
|
+
the dialog itself used far less, so on a 800px-tall window the last field lost 37 pixels and the
|
|
37
|
+
hint under it never showed at all. The cap now follows the space actually available.
|
|
38
|
+
|
|
39
|
+
- **Picking the project folder starts with the folder, not with a list.** The step opened with
|
|
40
|
+
every unregistered folder in your projects directory — 31 entries on a real machine, eight of
|
|
41
|
+
them visible — and the path field and "Choose…" button were hidden behind an entry near the
|
|
42
|
+
bottom of that same list. The list now holds only the three modes (a folder, no folder, clone
|
|
43
|
+
from git), and the folder one — the default — shows the path field and the button straight away.
|
|
44
|
+
The system dialog no longer opens by itself just because you reached the step. Your project
|
|
45
|
+
folders are still one click away: the built-in browser opens on that very directory.
|
|
46
|
+
|
|
6
47
|
## [1.9.0]
|
|
7
48
|
|
|
8
49
|
### Added
|
package/package.json
CHANGED
package/public/js/board.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { $, ALL, ALL_STATUSES, DONE_LIMIT,
|
|
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
4
|
import { getSetting, plural } from './settings.js';
|
|
5
5
|
import { styledAlert, styledConfirm } from './sidebar.js';
|
|
@@ -676,6 +676,9 @@ export function buildSelect(host, opts) {
|
|
|
676
676
|
if (host.classList.contains('open')) { close(); return; }
|
|
677
677
|
document.querySelectorAll('.kbsel.open').forEach((el) => { if (el !== host) el.classList.remove('open'); });
|
|
678
678
|
host.classList.add('open');
|
|
679
|
+
const place = popoverPlacement(host.querySelector('.kbsel-btn'));
|
|
680
|
+
list.classList.toggle('up', place.openUp);
|
|
681
|
+
list.style.maxHeight = `${place.maxHeight}px`;
|
|
679
682
|
if (opts.search) { const si = host.querySelector('.kbsel-search-input'); si.value = ''; filterOpts(''); setTimeout(() => si.focus(), 0); }
|
|
680
683
|
setTimeout(() => document.addEventListener('click', outside), 0);
|
|
681
684
|
};
|
package/public/js/chaos.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { buildSelect, selVal } from './board.js';
|
|
3
|
-
import { $,
|
|
3
|
+
import { $, api, esc, ic, LABEL_COLORS, LABEL_SELECTABLE, popoverPlacement, PRI_ICON, PRI_LEVELS, state, tr } from './core.js';
|
|
4
4
|
import { openDrawer, openLightbox, uploadAttachment } from './drawer.js';
|
|
5
5
|
import { loadProjects } from './sidebar.js';
|
|
6
6
|
import { autoGrow, refresh, titleOr } from './sse.js';
|
|
@@ -159,11 +159,8 @@ function chaosPopover(anchor, items, onPick) {
|
|
|
159
159
|
pop.innerHTML = items.map((o) => `<div class="chaos-pop-opt${o.active ? ' active' : ''}" data-v="${esc(String(o.v))}">${o.html}</div>`).join('');
|
|
160
160
|
document.body.appendChild(pop);
|
|
161
161
|
const r = anchor.getBoundingClientRect();
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
const above = r.top - margin;
|
|
165
|
-
const openUp = below < 200 && above > below;
|
|
166
|
-
pop.style.maxHeight = `${Math.max(120, (openUp ? above : below) - 5)}px`;
|
|
162
|
+
const { openUp, maxHeight } = popoverPlacement(anchor);
|
|
163
|
+
pop.style.maxHeight = `${maxHeight}px`;
|
|
167
164
|
let left = r.left;
|
|
168
165
|
if (left + pop.offsetWidth > innerWidth - 8) left = innerWidth - pop.offsetWidth - 8;
|
|
169
166
|
const top = openUp ? (r.top - pop.offsetHeight - 5) : (r.bottom + 5);
|
package/public/js/core.js
CHANGED
|
@@ -83,7 +83,7 @@ const I18N_RU = {
|
|
|
83
83
|
'All projects': 'Все проекты',
|
|
84
84
|
'all self-healed': 'всё починилось само',
|
|
85
85
|
'All set!': 'Готово!',
|
|
86
|
-
'
|
|
86
|
+
'a folder on this computer': 'папка на этом компьютере',
|
|
87
87
|
'Appearance': 'Оформление',
|
|
88
88
|
'applies after page reload': 'применится после перезагрузки страницы',
|
|
89
89
|
'Archive': 'Архивировать',
|
|
@@ -413,6 +413,14 @@ const I18N_RU = {
|
|
|
413
413
|
'the skill is not installed — the file will be created on save': 'скилл не установлен — файл появится при сохранении',
|
|
414
414
|
'the update did not go through': 'обновиться не удалось',
|
|
415
415
|
'Run it yourself': 'Выполни сам',
|
|
416
|
+
'Below is about the SERVER this project is deployed to, not about this computer. Empty means the project stays local.': 'Ниже — про СЕРВЕР, на который выкатывается проект, а не про этот компьютер. Пусто — проект остаётся локальным.',
|
|
417
|
+
'user, port and key come from your ~/.ssh/config — the board keeps no secrets': 'пользователь, порт и ключ берутся из твоего ~/.ssh/config — секретов доска не хранит',
|
|
418
|
+
'“Local folder” is on this computer. The rest describes the server this project is deployed to — leave it empty and the project simply stays local.': '«Папка на компьютере» — это здесь, на твоей машине. Остальное про сервер, на который выкатывается проект: оставь пустым — и проект просто останется локальным.',
|
|
419
|
+
'the board runs ssh <host> — user, port and key come from your ~/.ssh/config, no secrets are kept here': 'доска выполняет ssh <хост> — пользователь, порт и ключ берутся из твоего ~/.ssh/config, секретов доска не хранит',
|
|
420
|
+
'where the project lies ON THE SERVER — the deploy pulls into this folder': 'где проект лежит НА СЕРВЕРЕ — именно в эту папку деплой делает git pull',
|
|
421
|
+
'where the code lies on this computer — Claude edits and commits it here': 'где код лежит на этом компьютере — здесь Claude его правит и коммитит',
|
|
422
|
+
'restarted on the server after a deploy; empty means there is nothing to restart': 'что перезапустить на сервере после выкатки; пусто — перезапускать нечего',
|
|
423
|
+
'opened after a deploy to check the site answers': 'открывается после выкатки, чтобы убедиться, что сайт отвечает',
|
|
416
424
|
'The same in the README, in more detail': 'То же самое в README, подробнее',
|
|
417
425
|
'the board is not allowed to do this — the command is the same one it just tried': 'у доски нет на это прав — команда та же, что она только что пробовала',
|
|
418
426
|
'Your tasks are not touched: they live in the data directory, apart from the code.': 'Задачи не пострадают: они лежат в папке данных, отдельно от кода.',
|
|
@@ -758,6 +766,15 @@ export const state = {
|
|
|
758
766
|
|
|
759
767
|
export function $(id) { return document.getElementById(id); }
|
|
760
768
|
|
|
769
|
+
export function popoverPlacement(anchor, { flipBelow = 200, floor = 120, gap = 10 } = {}) {
|
|
770
|
+
const r = anchor.getBoundingClientRect();
|
|
771
|
+
const box = anchor.closest?.('.modal')?.getBoundingClientRect();
|
|
772
|
+
const below = Math.min(window.innerHeight, box ? box.bottom : Infinity) - r.bottom - gap;
|
|
773
|
+
const above = r.top - Math.max(0, box ? box.top : 0) - gap;
|
|
774
|
+
const openUp = below < flipBelow && above > below;
|
|
775
|
+
return { openUp, maxHeight: Math.max(floor, (openUp ? above : below) - 5) };
|
|
776
|
+
}
|
|
777
|
+
|
|
761
778
|
export const SETUP_PROMPT = {
|
|
762
779
|
ru: `Запусти скилл kanban.
|
|
763
780
|
|
package/public/js/project.js
CHANGED
|
@@ -32,7 +32,7 @@ export async function renderProjectSettings(force) {
|
|
|
32
32
|
if (!host) return;
|
|
33
33
|
const p = currentProject();
|
|
34
34
|
if (!p) {
|
|
35
|
-
|
|
35
|
+
host.innerHTML = `<div class="ps-wrap"><div class="muted">${tr('This project is gone')}</div></div>`;
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
if (!force && host.dataset.slug === p.slug) return;
|
|
@@ -65,12 +65,18 @@ export async function renderProjectSettings(force) {
|
|
|
65
65
|
|
|
66
66
|
<div class="panel ps-block">
|
|
67
67
|
<div class="panel-h">${tr('Paths & deploy')}</div>
|
|
68
|
+
<div class="kbh-note muted">${tr('“Local folder” is on this computer. The rest describes the server this project is deployed to — leave it empty and the project simply stays local.')}</div>
|
|
68
69
|
<div class="ps-grid">
|
|
69
|
-
<label class="pp-field">${tr('Local folder')}<input id="ps-path" type="text" placeholder="${tr('/path/to/the/folder — may be empty')}"
|
|
70
|
-
|
|
71
|
-
<label class="pp-field">${tr('
|
|
72
|
-
|
|
73
|
-
<label class="pp-field">${tr('
|
|
70
|
+
<label class="pp-field">${tr('Local folder')}<input id="ps-path" type="text" placeholder="${tr('/path/to/the/folder — may be empty')}">
|
|
71
|
+
<small class="ps-hint">${tr('where the code lies on this computer — Claude edits and commits it here')}</small></label>
|
|
72
|
+
<label class="pp-field">${tr('Domain')}<input id="ps-domain" type="text" placeholder="example.com">
|
|
73
|
+
<small class="ps-hint">${tr('opened after a deploy to check the site answers')}</small></label>
|
|
74
|
+
<label class="pp-field">${tr('SSH host')}<input id="ps-server" type="text" placeholder="${tr('ssh alias or address')}">
|
|
75
|
+
<small class="ps-hint">${tr('the board runs ssh <host> — user, port and key come from your ~/.ssh/config, no secrets are kept here')}</small></label>
|
|
76
|
+
<label class="pp-field">${tr('Server path')}<input id="ps-spath" type="text" placeholder="/var/www/my-project">
|
|
77
|
+
<small class="ps-hint">${tr('where the project lies ON THE SERVER — the deploy pulls into this folder')}</small></label>
|
|
78
|
+
<label class="pp-field">${tr('pm2 processes (comma-separated)')}<input id="ps-pm2" type="text" placeholder="my-api, my-web">
|
|
79
|
+
<small class="ps-hint">${tr('restarted on the server after a deploy; empty means there is nothing to restart')}</small></label>
|
|
74
80
|
<div class="pp-field">${tr('Deploy skill')}
|
|
75
81
|
<div class="kbsel" id="ps-skill"></div>
|
|
76
82
|
<small class="ps-hint" id="ps-skill-hint">${tr('the skill Claude follows to ship this project; “deploy” is the shared one. A new one is created in Settings → Skills.')}</small>
|
|
@@ -124,6 +130,12 @@ export async function renderProjectSettings(force) {
|
|
|
124
130
|
renderDocs(p);
|
|
125
131
|
}
|
|
126
132
|
|
|
133
|
+
export function deploySkills(info) {
|
|
134
|
+
const generic = info?.generic_deploy_skill || 'deploy';
|
|
135
|
+
const own = new Set(info?.own_skills || []);
|
|
136
|
+
return (info?.items || []).filter((s) => s.name === generic || s.used_by?.length || own.has(s.name));
|
|
137
|
+
}
|
|
138
|
+
|
|
127
139
|
export function skillOptions(cur, list) {
|
|
128
140
|
const names = list ? list.map((s) => s.name) : (cur ? [cur] : []);
|
|
129
141
|
if (cur && !names.includes(cur)) names.unshift(cur);
|
|
@@ -145,7 +157,7 @@ async function loadSkillOptions(p) {
|
|
|
145
157
|
try { info = await api('GET', '/api/skills', null, { quiet: true }); }
|
|
146
158
|
catch { return; }
|
|
147
159
|
if (!$('ps-skill') || $('projset').dataset.slug !== p.slug) return;
|
|
148
|
-
buildSkillSelect(p, info
|
|
160
|
+
buildSkillSelect(p, deploySkills(info));
|
|
149
161
|
}
|
|
150
162
|
|
|
151
163
|
async function renderDocs(p) {
|
package/public/js/sidebar.js
CHANGED
|
@@ -6,7 +6,7 @@ import { renderChaos } from './chaos.js';
|
|
|
6
6
|
import { $, ALL, CALENDAR, CHAOS, DASH, HIDDEN_SECTIONS, HORIZON, LANG, SETTINGS, SIDEBAR_SECTIONS, api, esc, ic, seg, setupPrompt, state, tr } from './core.js';
|
|
7
7
|
import { renderDashboard } from './dash.js';
|
|
8
8
|
import { renderCalendar, renderHorizon } from './horizon.js';
|
|
9
|
-
import { openProjectSettings, renderProjectSettings, skillOptions } from './project.js';
|
|
9
|
+
import { openProjectSettings, renderProjectSettings, skillOptions, deploySkills } from './project.js';
|
|
10
10
|
import { openSettingsModal, plural, setSetting, syncLangToServer } from './settings.js';
|
|
11
11
|
import { copyText, refresh, showView } from './sse.js';
|
|
12
12
|
|
|
@@ -420,10 +420,12 @@ function openProjectWizard() {
|
|
|
420
420
|
<button type="button" class="btn-ghost" id="wiz-browse">${tr('Choose…')}</button>
|
|
421
421
|
</div>
|
|
422
422
|
<div id="wiz-browser" class="wiz-browser hidden"></div>
|
|
423
|
-
<input id="wiz-git" type="text" class="hidden" placeholder="git@github.com:me/repo.git
|
|
423
|
+
<input id="wiz-git" type="text" class="hidden" placeholder="git@github.com:me/repo.git"></div>`;
|
|
424
424
|
}
|
|
425
425
|
if (withServer) {
|
|
426
|
-
html += `<
|
|
426
|
+
html += `<div class="kbh-note muted">${tr('Below is about the SERVER this project is deployed to, not about this computer. Empty means the project stays local.')}</div>`
|
|
427
|
+
+ `<label class="pp-field">${tr('SSH host')}<input id="wiz-server" type="text" placeholder="${tr('ssh alias or address')}">`
|
|
428
|
+
+ `<small class="pp-hint">${tr('user, port and key come from your ~/.ssh/config — the board keeps no secrets')}</small></label>`
|
|
427
429
|
+ `<label class="pp-field">${tr('Server path')}<input id="wiz-spath" type="text" placeholder="/var/www/my-project"></label>`
|
|
428
430
|
+ `<label class="pp-field">${tr('pm2 processes (comma-separated)')}<input id="wiz-pm2" type="text" placeholder="my-api, my-web"></label>`
|
|
429
431
|
+ `<label class="pp-field">${tr('Domain (to verify after deploy)')}<input id="wiz-domain" type="text" placeholder="example.com"></label>`
|
|
@@ -442,17 +444,17 @@ function openProjectWizard() {
|
|
|
442
444
|
if (folderSel) {
|
|
443
445
|
const options = [
|
|
444
446
|
...(type === 'local' ? [{ value: '', label: tr('no folder — just a task list') }] : []),
|
|
445
|
-
|
|
446
|
-
{ value: '__manual__', label: tr('another folder on this computer…') },
|
|
447
|
+
{ value: '__manual__', label: tr('a folder on this computer') },
|
|
447
448
|
...(type === 'localserver' ? [{ value: '__clone__', label: tr('clone from a git URL…') }] : []),
|
|
448
449
|
];
|
|
449
450
|
const onFolderChange = (v) => {
|
|
450
451
|
$('wiz-path-row').classList.toggle('hidden', v !== '__manual__');
|
|
451
452
|
$('wiz-browser').classList.add('hidden');
|
|
452
453
|
$('wiz-git').classList.toggle('hidden', v !== '__clone__');
|
|
453
|
-
if (v === '__manual__') openFolderPicker();
|
|
454
454
|
};
|
|
455
|
-
|
|
455
|
+
const start = type === 'local' ? '' : '__manual__';
|
|
456
|
+
buildSelect(folderSel, { value: start, options, onChange: onFolderChange });
|
|
457
|
+
onFolderChange(start);
|
|
456
458
|
$('wiz-browse').onclick = openFolderPicker;
|
|
457
459
|
}
|
|
458
460
|
const skillHost = $('wiz-skill');
|
|
@@ -462,8 +464,7 @@ function openProjectWizard() {
|
|
|
462
464
|
.then((info) => {
|
|
463
465
|
if (!$('wiz-skill')) return;
|
|
464
466
|
const generic = info.generic_deploy_skill || GENERIC_DEPLOY;
|
|
465
|
-
const
|
|
466
|
-
const deployish = (info.items || []).filter((s) => s.name === generic || s.used_by?.length || own.has(s.name));
|
|
467
|
+
const deployish = deploySkills(info);
|
|
467
468
|
const has = deployish.some((s) => s.name === generic);
|
|
468
469
|
buildSelect($('wiz-skill'), { value: has ? generic : '', options: skillOptions(generic, deployish) });
|
|
469
470
|
})
|
|
@@ -498,8 +499,6 @@ function openProjectWizard() {
|
|
|
498
499
|
btn.textContent = tr('cloning…');
|
|
499
500
|
const r = await api('POST', '/api/projects/clone', { url, name: slug });
|
|
500
501
|
body.path = r.path;
|
|
501
|
-
} else if (mode) {
|
|
502
|
-
body.path = `${state.folders.root}/${mode}`;
|
|
503
502
|
}
|
|
504
503
|
if (type === 'local') body.category = 'Local';
|
|
505
504
|
}
|
package/public/style.css
CHANGED
|
@@ -614,7 +614,7 @@ body.dragging-board .col-add-ghost { display: none; }
|
|
|
614
614
|
.modal-actions { display: flex; gap: 8px; }
|
|
615
615
|
|
|
616
616
|
.modal.wiz { width: 400px; max-width: calc(100vw - 32px); }
|
|
617
|
-
.wiz-body { display: flex; flex-direction: column; gap:
|
|
617
|
+
.wiz-body { display: flex; flex-direction: column; gap: 8px; max-height: calc(100vh - 170px); overflow-y: auto; }
|
|
618
618
|
.wiz-type {
|
|
619
619
|
display: flex; flex-direction: column; gap: 3px; text-align: left; padding: 10px 12px;
|
|
620
620
|
border: 1px solid var(--border); border-radius: 9px; background: var(--panel); cursor: pointer; font: inherit; color: var(--text);
|
|
@@ -1037,6 +1037,7 @@ body.dragging-board .col-add-ghost { display: none; }
|
|
|
1037
1037
|
box-shadow: 0 10px 28px var(--shadow); padding: 4px; max-height: 280px; overflow-y: auto;
|
|
1038
1038
|
}
|
|
1039
1039
|
.kbsel.open .kbsel-list { display: block; }
|
|
1040
|
+
.kbsel-list.up { top: auto; bottom: calc(100% + 4px); }
|
|
1040
1041
|
.kbsel-search { position: sticky; top: -4px; background: var(--panel); padding: 2px 2px 5px; margin: -4px -4px 2px; z-index: 1; }
|
|
1041
1042
|
.kbsel-search-input {
|
|
1042
1043
|
width: 100%; box-sizing: border-box; padding: 6px 9px; font: inherit; font-size: 12.5px;
|