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 +9 -1
- package/package.json +1 -1
- package/server/binder.mjs +1 -1
- package/ui/binder-ui.css +11 -3
- package/ui/binder-ui.js +25 -9
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
|
|
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(
|
|
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
|
-
* "* "),
|
|
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
|
-
|
|
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
|
|
154
|
-
export function
|
|
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) =>
|
|
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
|
|