local-kanban 1.9.0 → 1.10.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,35 @@
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.10.0]
7
+
8
+ ### Fixed
9
+
10
+ - **The deploy skill list only offers deploy skills.** On the project settings page it listed
11
+ everything in your skills folder — on a real board that is ten entries when one of them is an
12
+ actual deploy skill, and the board's own skill sits among them, offered as a way to deploy your
13
+ project. The wizard already filtered this; the project page did not, because the rule lived in
14
+ the wizard's file. Now both ask the same function. A skill your project already points at stays
15
+ in the list even if it falls outside the filter, so saving never quietly clears the field.
16
+
17
+ - **A dropdown opens where there is room.** The custom select always opened downward with a fixed
18
+ height, so near the bottom of a dialog its list ran past the dialog and under the buttons. It now
19
+ measures the space first — against the dialog, not just the window — and flips up when it has to.
20
+ The rule already existed for the picker menus; there is one copy of it now instead of one and a
21
+ half.
22
+
23
+ - **The Add project form is not cut off.** Its body was capped at 62% of the window height while
24
+ the dialog itself used far less, so on a 800px-tall window the last field lost 37 pixels and the
25
+ hint under it never showed at all. The cap now follows the space actually available.
26
+
27
+ - **Picking the project folder starts with the folder, not with a list.** The step opened with
28
+ every unregistered folder in your projects directory — 31 entries on a real machine, eight of
29
+ them visible — and the path field and "Choose…" button were hidden behind an entry near the
30
+ bottom of that same list. The list now holds only the three modes (a folder, no folder, clone
31
+ from git), and the folder one — the default — shows the path field and the button straight away.
32
+ The system dialog no longer opens by itself just because you reached the step. Your project
33
+ folders are still one click away: the built-in browser opens on that very directory.
34
+
6
35
  ## [1.9.0]
7
36
 
8
37
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-kanban",
3
- "version": "1.9.0",
3
+ "version": "1.10.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': 'Архивировать',
@@ -758,6 +758,15 @@ export const state = {
758
758
 
759
759
  export function $(id) { return document.getElementById(id); }
760
760
 
761
+ export function popoverPlacement(anchor, { flipBelow = 200, floor = 120, gap = 10 } = {}) {
762
+ const r = anchor.getBoundingClientRect();
763
+ const box = anchor.closest?.('.modal')?.getBoundingClientRect();
764
+ const below = Math.min(window.innerHeight, box ? box.bottom : Infinity) - r.bottom - gap;
765
+ const above = r.top - Math.max(0, box ? box.top : 0) - gap;
766
+ const openUp = below < flipBelow && above > below;
767
+ return { openUp, maxHeight: Math.max(floor, (openUp ? above : below) - 5) };
768
+ }
769
+
761
770
  export const SETUP_PROMPT = {
762
771
  ru: `Запусти скилл kanban.
763
772
 
@@ -124,6 +124,12 @@ export async function renderProjectSettings(force) {
124
124
  renderDocs(p);
125
125
  }
126
126
 
127
+ export function deploySkills(info) {
128
+ const generic = info?.generic_deploy_skill || 'deploy';
129
+ const own = new Set(info?.own_skills || []);
130
+ return (info?.items || []).filter((s) => s.name === generic || s.used_by?.length || own.has(s.name));
131
+ }
132
+
127
133
  export function skillOptions(cur, list) {
128
134
  const names = list ? list.map((s) => s.name) : (cur ? [cur] : []);
129
135
  if (cur && !names.includes(cur)) names.unshift(cur);
@@ -145,7 +151,7 @@ async function loadSkillOptions(p) {
145
151
  try { info = await api('GET', '/api/skills', null, { quiet: true }); }
146
152
  catch { return; }
147
153
  if (!$('ps-skill') || $('projset').dataset.slug !== p.slug) return;
148
- buildSkillSelect(p, info.items || []);
154
+ buildSkillSelect(p, deploySkills(info));
149
155
  }
150
156
 
151
157
  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
 
@@ -442,17 +442,17 @@ function openProjectWizard() {
442
442
  if (folderSel) {
443
443
  const options = [
444
444
  ...(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…') },
445
+ { value: '__manual__', label: tr('a folder on this computer') },
447
446
  ...(type === 'localserver' ? [{ value: '__clone__', label: tr('clone from a git URL…') }] : []),
448
447
  ];
449
448
  const onFolderChange = (v) => {
450
449
  $('wiz-path-row').classList.toggle('hidden', v !== '__manual__');
451
450
  $('wiz-browser').classList.add('hidden');
452
451
  $('wiz-git').classList.toggle('hidden', v !== '__clone__');
453
- if (v === '__manual__') openFolderPicker();
454
452
  };
455
- buildSelect(folderSel, { value: type === 'local' ? '' : (options[0]?.value ?? ''), options, onChange: onFolderChange });
453
+ const start = type === 'local' ? '' : '__manual__';
454
+ buildSelect(folderSel, { value: start, options, onChange: onFolderChange });
455
+ onFolderChange(start);
456
456
  $('wiz-browse').onclick = openFolderPicker;
457
457
  }
458
458
  const skillHost = $('wiz-skill');
@@ -462,8 +462,7 @@ function openProjectWizard() {
462
462
  .then((info) => {
463
463
  if (!$('wiz-skill')) return;
464
464
  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));
465
+ const deployish = deploySkills(info);
467
466
  const has = deployish.some((s) => s.name === generic);
468
467
  buildSelect($('wiz-skill'), { value: has ? generic : '', options: skillOptions(generic, deployish) });
469
468
  })
@@ -498,8 +497,6 @@ function openProjectWizard() {
498
497
  btn.textContent = tr('cloning…');
499
498
  const r = await api('POST', '/api/projects/clone', { url, name: slug });
500
499
  body.path = r.path;
501
- } else if (mode) {
502
- body.path = `${state.folders.root}/${mode}`;
503
500
  }
504
501
  if (type === 'local') body.category = 'Local';
505
502
  }
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: 9px; max-height: calc(100vh - 190px); 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;