clavarium-binder 0.1.3-next.6 → 0.1.3
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/package.json +1 -1
- package/server/binder.mjs +1 -1
- package/ui/binder-ui.css +6 -0
- package/ui/binder-ui.js +61 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clavarium-binder",
|
|
3
|
-
"version": "0.1.3
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Clavarium Binder: lists of records kept together by people and AI agents, each with its id (a command and an MCP server for agents, a keeper and a screen for the application). No dependency.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/clavarium/clavarium-binder#readme",
|
package/server/binder.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
23
23
|
import { randomUUID } from 'node:crypto';
|
|
24
24
|
import { createInterface } from 'node:readline';
|
|
25
25
|
|
|
26
|
-
export const VERSION = '0.1.3
|
|
26
|
+
export const VERSION = '0.1.3';
|
|
27
27
|
export const FOLDER = '.binder';
|
|
28
28
|
/* How long an ask waits for its answer. The keeper answers within a second or two. */
|
|
29
29
|
export const WAIT_MS = 20_000;
|
package/ui/binder-ui.css
CHANGED
|
@@ -226,3 +226,9 @@ button.binder-who:hover span:not(.binder-dot) { text-decoration: underline; text
|
|
|
226
226
|
.binder-shot-open img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
227
227
|
.binder-shot figcaption { display: flex; align-items: center; gap: .3rem; min-width: 0; font-size: .82em; color: var(--binder-dim); }
|
|
228
228
|
.binder-shot figcaption .binder-att-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1; }
|
|
229
|
+
|
|
230
|
+
/* Drag a card between columns. */
|
|
231
|
+
.binder-drag-clone { position: fixed; z-index: 1000; pointer-events: none; opacity: .92; box-shadow: 0 10px 30px rgb(0 0 0 / .3); transform: rotate(1.5deg); }
|
|
232
|
+
.binder-card.binder-drag-src { opacity: .35; }
|
|
233
|
+
.binder-col[data-over] { outline: 2px dashed var(--binder-action); outline-offset: -2px; background: color-mix(in srgb, var(--binder-action) 10%, var(--binder-panel)); }
|
|
234
|
+
.binder-cols .binder-card { touch-action: pan-y; }
|
package/ui/binder-ui.js
CHANGED
|
@@ -275,12 +275,71 @@ export function mount(root, { call, words = {}, lang = 'en', people = () => [],
|
|
|
275
275
|
return h('div', { class: 'binder-rows' }, records.map((r) => h('div', { class: 'binder-row', 'data-record': r.id, tabindex: '0', onclick: () => open(r), onkeydown: (e) => { if (e.key === 'Enter') open(r); } },
|
|
276
276
|
tick(l, r), h('div', { class: 'binder-what' }, h('b', {}, r.title), factLine(l, r), sub(r), meta(l, r)), pill(l, r) ?? h('span'))));
|
|
277
277
|
}
|
|
278
|
+
/* Drag a card from one column to another to change its state. Works with a finger (press and hold)
|
|
279
|
+
and with a mouse; a plain tap/click still opens the card. The server still guards a gate. */
|
|
280
|
+
function columnsDnd(colsEl, l) {
|
|
281
|
+
if (l.access !== 'write') return colsEl;
|
|
282
|
+
let d = null; let swallow = false;
|
|
283
|
+
const clear = () => {
|
|
284
|
+
if (!d) return;
|
|
285
|
+
clearTimeout(d.hold); d.clone?.remove();
|
|
286
|
+
d.card?.classList.remove('binder-drag-src');
|
|
287
|
+
for (const c of colsEl.querySelectorAll('.binder-col[data-over]')) c.removeAttribute('data-over');
|
|
288
|
+
try { colsEl.releasePointerCapture(d.id); } catch { /* gone */ }
|
|
289
|
+
d = null;
|
|
290
|
+
};
|
|
291
|
+
const start = (e) => {
|
|
292
|
+
const r = d.card.getBoundingClientRect();
|
|
293
|
+
d.offX = e.clientX - r.left; d.offY = e.clientY - r.top; d.moved = true;
|
|
294
|
+
try { colsEl.setPointerCapture(d.id); } catch { /* ok */ }
|
|
295
|
+
const clone = d.card.cloneNode(true); clone.className = 'binder-card binder-drag-clone'; clone.style.width = `${r.width}px`;
|
|
296
|
+
document.body.append(clone); d.clone = clone; d.card.classList.add('binder-drag-src');
|
|
297
|
+
};
|
|
298
|
+
const over = (e) => {
|
|
299
|
+
d.clone.style.left = `${e.clientX - d.offX}px`; d.clone.style.top = `${e.clientY - d.offY}px`;
|
|
300
|
+
d.clone.style.visibility = 'hidden';
|
|
301
|
+
const under = document.elementFromPoint?.(e.clientX, e.clientY);
|
|
302
|
+
d.clone.style.visibility = '';
|
|
303
|
+
const col = under instanceof Element ? under.closest('.binder-col') : null;
|
|
304
|
+
for (const c of colsEl.querySelectorAll('.binder-col[data-over]')) if (c !== col) c.removeAttribute('data-over');
|
|
305
|
+
if (col && col.dataset.state !== d.from) col.setAttribute('data-over', '');
|
|
306
|
+
};
|
|
307
|
+
colsEl.addEventListener('click', (e) => { if (swallow) { e.stopPropagation(); e.preventDefault(); swallow = false; } }, true);
|
|
308
|
+
colsEl.addEventListener('pointerdown', (e) => {
|
|
309
|
+
const card = e.target instanceof Element ? e.target.closest('.binder-card') : null;
|
|
310
|
+
if (!card || e.target.closest('button, a, input, select, label, .binder-pill, .binder-state')) return;
|
|
311
|
+
const col = card.closest('.binder-col');
|
|
312
|
+
d = { card, record: card.dataset.record, from: col?.dataset.state, x: e.clientX, y: e.clientY, id: e.pointerId, moved: false, ready: e.pointerType === 'mouse', clone: null, hold: null };
|
|
313
|
+
if (e.pointerType !== 'mouse') d.hold = setTimeout(() => { if (d) d.ready = true; }, 180);
|
|
314
|
+
});
|
|
315
|
+
colsEl.addEventListener('pointermove', (e) => {
|
|
316
|
+
if (!d || e.pointerId !== d.id) return;
|
|
317
|
+
const far = Math.hypot(e.clientX - d.x, e.clientY - d.y) > 10;
|
|
318
|
+
if (!d.moved) {
|
|
319
|
+
if (!d.ready) { if (far) clear(); return; } // a finger that moved first is a scroll
|
|
320
|
+
if (!far && e.pointerType === 'mouse') return;
|
|
321
|
+
start(e);
|
|
322
|
+
}
|
|
323
|
+
e.preventDefault(); over(e);
|
|
324
|
+
});
|
|
325
|
+
const up = (e) => {
|
|
326
|
+
if (!d || e.pointerId !== d.id) return;
|
|
327
|
+
if (d.moved) {
|
|
328
|
+
const target = colsEl.querySelector('.binder-col[data-over]');
|
|
329
|
+
const state = target?.dataset.state; const record = d.record; swallow = true; clear();
|
|
330
|
+
if (state && record) void soft(ask('move', { id: record, state })).then(refresh);
|
|
331
|
+
} else clear();
|
|
332
|
+
};
|
|
333
|
+
colsEl.addEventListener('pointerup', up);
|
|
334
|
+
colsEl.addEventListener('pointercancel', () => { if (d?.moved) swallow = true; clear(); });
|
|
335
|
+
return colsEl;
|
|
336
|
+
}
|
|
278
337
|
function asColumns(l, records) {
|
|
279
|
-
return h('div', { class: 'binder-cols' }, l.states.map((s) => {
|
|
338
|
+
return columnsDnd(h('div', { class: 'binder-cols' }, l.states.map((s) => {
|
|
280
339
|
const here = records.filter((r) => r.state === s.id);
|
|
281
340
|
return h('section', { class: 'binder-col', 'data-state': s.id }, h('h3', {}, sn(s), h('span', {}, String(here.length))),
|
|
282
341
|
here.map((r) => h('div', { class: 'binder-card', 'data-record': r.id, tabindex: '0', onclick: () => open(r), onkeydown: (e) => { if (e.key === 'Enter') open(r); } }, h('b', {}, r.title), factLine(l, r), sub(r), meta(l, r))));
|
|
283
|
-
}));
|
|
342
|
+
})), l);
|
|
284
343
|
}
|
|
285
344
|
/* A card without a picture: a band of a soft colour of its own (from its title), not a large letter alone. */
|
|
286
345
|
const band = (t) => {
|