clavarium-binder 0.1.2 → 0.1.3-next.2

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/docs/protocol.md CHANGED
@@ -78,9 +78,17 @@ An agent at the limit is refused, and told the number.
78
78
  const k = keeper(storeDir, { sides: () => [{ dir, who }] });
79
79
  setInterval(() => k.pump(), 1000); // answer the agents
80
80
  k.act(op, args, person); // a person's change: { ok, text, data }
81
- k.due(); // records whose due time came: tell their owners
81
+ k.due(); // records whose due time came: tell their owners (below)
82
82
  k.store.onChange((line) => ...); // every change, as the log says it
83
83
 
84
+ What is due: `k.due()` gives `[{ record, list }]`, each record whose due time
85
+ came -- a date alone at 09:00, a date and time at that time, by the keeper's
86
+ clock (`now`) in its local time -- not archived, in a list not archived, in a
87
+ state not done. Each is given once per due time: the store keeps it
88
+ (`told`, in state.json), so a restart does not give it again; a due date
89
+ changed gives it again when the new time comes. Call it every minute or so,
90
+ and tell its `owners` as the application does.
91
+
84
92
  And the screen: `mount(element, { call, words, lang, people, picture, openAttached })`
85
93
  from `clavarium-binder/ui`, with `ui/binder-ui.css` and `ui/texts/*.json`.
86
94
  It gives back `refresh()`, `destroy()`, and `open(id)`: one record shown
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clavarium-binder",
3
- "version": "0.1.2",
3
+ "version": "0.1.3-next.2",
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.2';
26
+ export const VERSION = '0.1.3-next.2';
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
@@ -37,7 +37,8 @@
37
37
  .binder-owners { display: flex; gap: .35rem; flex-wrap: wrap; }
38
38
  .binder-pop { position: relative; }
39
39
  .binder-dots { font-weight: 700; letter-spacing: .05em; padding: .4em .75em; line-height: 1.15; }
40
- .binder-menu-end { left: auto; right: 0; }
40
+ /* Opened from the end of a row: it opens leftwards (the rule after .binder-menu once put it back at left: 0, past the screen). */
41
+ .binder-menu.binder-menu-end { left: auto; right: 0; }
41
42
  .binder-primary { background: var(--binder-action); color: var(--binder-on-action) !important; border: 1px solid var(--binder-action); border-radius: 8px; padding: .45em 1em; font-weight: 600; }
42
43
  .binder-ghost { background: var(--binder-panel); border: 1px solid var(--binder-line); border-radius: 8px; padding: .45em .9em; }
43
44
  .binder-danger { background: var(--binder-danger); color: var(--binder-bg) !important; border: 0; border-radius: 8px; padding: .45em .9em; font-weight: 600; }
@@ -67,7 +68,7 @@
67
68
  .binder-pill[data-gate] { color: var(--binder-ask); border-color: var(--binder-ask); }
68
69
 
69
70
  /* As columns, one per state. */
70
- .binder-cols { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(14rem, 1fr); gap: .8rem; overflow-x: auto; min-height: 0; padding-bottom: .3rem; }
71
+ .binder-cols { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(11.5rem, 1fr); gap: .8rem; overflow-x: auto; min-height: 0; padding-bottom: .3rem; }
71
72
  .binder-col { background: var(--binder-panel); border: 1px solid var(--binder-line); border-radius: var(--binder-radius); padding: .6rem; display: flex; flex-direction: column; gap: .5rem; }
72
73
  .binder-col h3 { font-size: .75rem; letter-spacing: .06em; text-transform: uppercase; color: var(--binder-dim); display: flex; justify-content: space-between; }
73
74
  .binder-card { background: var(--binder-bg); border: 1px solid var(--binder-line); border-radius: 9px; padding: .6rem .7rem; display: flex; flex-direction: column; gap: .3rem; cursor: pointer; }
@@ -106,7 +107,7 @@
106
107
  .binder-state[data-done] { background: color-mix(in srgb, var(--binder-ok) 16%, transparent); border-color: transparent; }
107
108
  .binder-state:disabled { opacity: 1; }
108
109
  .binder-caret { font-size: .8em; color: var(--binder-dim); }
109
- .binder-menu { position: absolute; z-index: 5; top: calc(100% + .35rem); left: 0; min-width: 12rem; display: flex; flex-direction: column; padding: .3rem; background: var(--binder-bg);
110
+ .binder-menu { position: absolute; z-index: 5; top: calc(100% + .35rem); left: 0; min-width: 12rem; max-width: calc(100vw - 2rem); display: flex; flex-direction: column; padding: .3rem; background: var(--binder-bg);
110
111
  border: 1px solid var(--binder-line); border-radius: 12px; box-shadow: 0 8px 28px rgb(0 0 0 / .18); }
111
112
  .binder-menu button { border: 0; background: transparent; text-align: left; padding: .6em .7em; border-radius: 8px; display: flex; gap: .5em; align-items: center; }
112
113
  .binder-menu button:hover, .binder-menu button:focus-visible { background: var(--binder-panel); outline: none; }
@@ -177,3 +178,10 @@ button.binder-who:hover span:not(.binder-dot) { text-decoration: underline; text
177
178
  .binder-seg button { padding: .4em .6em; }
178
179
  .binder-bar .binder-primary { padding: .45em .75em; }
179
180
  }
181
+
182
+ /* A description's boxes: ticked here, kept in its text. */
183
+ .binder-rich ul.binder-tasks { list-style: none; padding-left: 0; }
184
+ .binder-task label { display: flex; gap: .55em; align-items: flex-start; cursor: pointer; }
185
+ .binder-task input { width: 1.15em; height: 1.15em; margin: .2em 0 0; flex: none; accent-color: var(--binder-ok); }
186
+ .binder-task input:disabled { cursor: default; }
187
+ .binder-task[data-done="true"] > label > span { color: var(--binder-dim); text-decoration: line-through; }
package/ui/binder-ui.js CHANGED
@@ -92,9 +92,10 @@ export function stateShown(l, r) {
92
92
 
93
93
  /* A DESCRIPTION, READ: a few lines of Markdown-ish text, as blocks a screen can
94
94
  * draw element by element. Paragraphs (a blank line between), lists ("- " or
95
- * "* "), **bold**, links ([text](https://...) or a bare http(s) address).
95
+ * "* "), a list item that starts with [ ] or [x] (a box, ticked or not), **bold**,
96
+ * links ([text](https://...) or a bare http(s) address).
96
97
  * Nothing else is understood: whatever else it holds stays text.
97
- * [{ type: 'p', lines: [parts] }, { type: 'list', items: [parts] }]
98
+ * [{ type: 'p', lines: [parts] }, { type: 'list', items: [parts], checks: [null | { on, line }] }]
98
99
  * parts: [{ text }, { bold }, { link, text }]
99
100
  */
100
101
  const INLINE = /\*\*([^*\n]+?)\*\*|\[([^\]\n]{1,300})\]\((https?:\/\/[^\s()<>]+)\)|(https?:\/\/[^\s<>"]+)/gi;
@@ -119,13 +120,15 @@ export function inline(text) {
119
120
  export function blocks(text) {
120
121
  const out = [];
121
122
  let para = null; let list = null;
122
- for (const raw of String(text ?? '').replace(/\r/g, '').split('\n')) {
123
+ for (const [at, raw] of String(text ?? '').replace(/\r/g, '').split('\n').entries()) {
123
124
  const line = raw.trim();
124
125
  const item = /^[-*]\s+(.*)$/.exec(line);
125
126
  if (!line) { para = null; list = null; } else if (item) {
126
127
  para = null;
127
- if (!list) out.push(list = { type: 'list', items: [] });
128
- list.items.push(inline(item[1]));
128
+ if (!list) out.push(list = { type: 'list', items: [], checks: [] });
129
+ const box = /^\[([ xX])\]\s+(.*)$/.exec(item[1]);
130
+ list.items.push(inline(box ? box[2] : item[1]));
131
+ list.checks.push(box ? { on: box[1] !== ' ', line: at } : null);
129
132
  } else {
130
133
  list = null;
131
134
  if (!para) out.push(para = { type: 'p', lines: [] });
@@ -150,12 +153,25 @@ function h(tag, props = {}, ...kids) {
150
153
  return el;
151
154
  }
152
155
 
153
- /* A description drawn: text nodes, <strong>, and <a> whose address starts with http(s)://. */
154
- export function rich(text) {
156
+ /* A description's box ticked or unticked: the same text, that one line changed. */
157
+ export function toggled(text, line) {
158
+ const lines = String(text ?? '').replace(/\r/g, '').split('\n');
159
+ lines[line] = lines[line].replace(/^(\s*[-*]\s+)\[([ xX])\]/, (_, lead, mark) => `${lead}[${mark === ' ' ? 'x' : ' '}]`);
160
+ return lines.join('\n');
161
+ }
162
+ /* A description drawn: text nodes, <strong>, and <a> whose address starts with http(s)://.
163
+ `tick(text)`: given, its boxes can be ticked (the whole new text, to save); else they are only shown. */
164
+ export function rich(text, tick = null) {
155
165
  const parts = (ps) => ps.map((x) => (x.bold !== undefined ? h('strong', {}, x.bold)
156
166
  : x.link && /^https?:\/\//i.test(x.link) ? h('a', { href: x.link, target: '_blank', rel: 'noopener noreferrer' }, x.text) : x.text ?? ''));
167
+ const item = (it, check) => {
168
+ if (!check) return h('li', {}, parts(it));
169
+ const box = h('input', { type: 'checkbox', class: 'binder-check-item', ...(check.on ? { checked: true } : {}), ...(tick ? {} : { disabled: true }),
170
+ onchange: () => tick?.(toggled(text, check.line)) });
171
+ return h('li', { class: 'binder-task', 'data-done': check.on ? 'true' : null }, h('label', {}, box, h('span', {}, parts(it))));
172
+ };
157
173
  return h('div', { class: 'binder-rich' }, blocks(text).map((b) => (b.type === 'list'
158
- ? h('ul', {}, b.items.map((it) => h('li', {}, parts(it))))
174
+ ? h('ul', b.checks.some(Boolean) ? { class: 'binder-tasks' } : {}, b.items.map((it, i) => item(it, b.checks[i])))
159
175
  : h('p', {}, b.lines.flatMap((l, i) => (i ? [h('br'), ...parts(l)] : parts(l)))))));
160
176
  }
161
177
 
@@ -326,7 +342,7 @@ export function mount(root, { call, words = {}, lang = 'en', people = () => [],
326
342
  h('button', { type: 'button', class: 'binder-primary binder-small', 'data-act': 'save-description', onclick: () => { delete ui.draft.description; done(ask('edit', { id: r.id, description: text.value })); } }, w('save')),
327
343
  h('button', { type: 'button', class: 'binder-text-btn', onclick: () => { delete ui.draft.description; set(null); } }, w('cancel'))));
328
344
  } else {
329
- about = r.description ? h('div', { class: 'binder-desc' }, rich(r.description),
345
+ about = r.description ? h('div', { class: 'binder-desc' }, rich(r.description, can ? (text) => done(ask('edit', { id: r.id, description: text })) : null),
330
346
  can ? h('button', { type: 'button', class: 'binder-text-btn', 'data-act': 'edit-description', onclick: () => set('description') }, w('edit')) : null) : null;
331
347
  }
332
348