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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clavarium-binder",
3
- "version": "0.1.3-next.6",
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-next.6';
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) => {