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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-kanban",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "license": "MIT",
5
5
  "author": "Dannylamao",
6
6
  "repository": {
@@ -1,5 +1,5 @@
1
1
 
2
- import { $, ALL, ALL_STATUSES, DONE_LIMIT, LABEL_COLORS, LABEL_SELECTABLE, PRI_ICON, PRI_LEVELS, api, esc, ic, noteUnsaved, seg, state, tr } from './core.js';
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
  };
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { buildSelect, selVal } from './board.js';
3
- import { $, LABEL_COLORS, LABEL_SELECTABLE, PRI_ICON, PRI_LEVELS, api, esc, ic, state, tr } from './core.js';
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 margin = 10;
163
- const below = innerHeight - r.bottom - margin;
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
- 'another folder on this computer…': 'другая папка на этом компьютере…',
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 &lt;host&gt; — user, port and key come from your ~/.ssh/config, no secrets are kept here': 'доска выполняет ssh &lt;хост&gt; — пользователь, порт и ключ берутся из твоего ~/.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
 
@@ -32,7 +32,7 @@ export async function renderProjectSettings(force) {
32
32
  if (!host) return;
33
33
  const p = currentProject();
34
34
  if (!p) {
35
- host.innerHTML = `<div class="ps-wrap"><div class="muted">${tr('This project is gone')}</div></div>`;
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')}"></label>
70
- <label class="pp-field">${tr('Domain')}<input id="ps-domain" type="text"></label>
71
- <label class="pp-field">${tr('SSH host')}<input id="ps-server" type="text" placeholder="${tr('ssh alias or address')}"></label>
72
- <label class="pp-field">${tr('Server path')}<input id="ps-spath" type="text"></label>
73
- <label class="pp-field">${tr('pm2 processes (comma-separated)')}<input id="ps-pm2" type="text"></label>
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 &lt;host&gt; — 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.items || []);
160
+ buildSkillSelect(p, deploySkills(info));
149
161
  }
150
162
 
151
163
  async function renderDocs(p) {
@@ -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 or https://…"></div>`;
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 += `<label class="pp-field">${tr('SSH host')}<input id="wiz-server" type="text" placeholder="host for ssh (keys in ~/.ssh/config)"></label>`
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
- ...folders.map((f) => ({ value: f, label: `${tr('folder')} ${f} (${state.folders.root || '~'})` })),
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
- buildSelect(folderSel, { value: type === 'local' ? '' : (options[0]?.value ?? ''), options, onChange: onFolderChange });
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 own = new Set(info.own_skills || []);
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: 9px; max-height: 62vh; overflow-y: auto; }
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;