nio-cat 0.9.6 → 0.9.9
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/README.md +1 -0
- package/bin/nio.js +6 -2
- package/package.json +1 -1
- package/src/agents/claude.js +6 -1
- package/src/agents/ollama.js +3 -2
- package/src/call.js +246 -0
- package/src/hub.js +23 -0
- package/src/notes.js +293 -0
- package/src/server.js +27 -3
- package/src/terminal.js +147 -0
- package/src/usage.js +79 -0
- package/web/app/app.js +334 -72
- package/web/app/call.css +80 -0
- package/web/app/call.js +435 -0
- package/web/app/demo.js +203 -10
- package/web/app/index.html +4 -2
- package/web/app/notes.css +225 -0
- package/web/app/notes.js +747 -0
- package/web/app/style.css +83 -0
- package/web/index.html +7 -2
- package/web/sw.js +2 -2
package/web/app/demo.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// text en directe, eines i aprovacions de mentida. No surt res del navegador i es reinicia en recarregar.
|
|
4
4
|
(() => {
|
|
5
5
|
const ROOT = '/Users/tu/projectes';
|
|
6
|
+
const DEMO_USAGE = { claude: { windows: [{ label: '5 h', used: 0.32, resetsAt: Math.floor(Date.now() / 1000) + 7200 }, { label: 'setmana', used: 0.57, resetsAt: Math.floor(Date.now() / 1000) + 3 * 86400 }], at: Date.now() }, codex: { windows: [{ label: '5 h', used: 0.85, resetsAt: Math.floor(Date.now() / 1000) + 3600 }, { label: 'setmana', used: 0.17, resetsAt: Math.floor(Date.now() / 1000) + 5 * 86400 }], at: Date.now() } };
|
|
6
7
|
const AGENTS = [
|
|
7
8
|
{ id: 'claude', name: 'Claude Code', beta: false, approvals: true, noModes: false, login: '', available: true, version: 'simulat' },
|
|
8
9
|
{ id: 'codex', name: 'Codex', beta: false, approvals: true, noModes: false, login: '', available: true, version: 'simulat' },
|
|
@@ -73,7 +74,7 @@
|
|
|
73
74
|
}
|
|
74
75
|
async function work(s, text) {
|
|
75
76
|
const run = { stopped: false }; runs.set(s.id, run);
|
|
76
|
-
const p = plan(text, s), tool = (name, input, output, error) => { const id = rid(); push(s, 'tool', { id, name, input }); return { id, done: () => push(s, 'tool_result', { id, name, output, error: !!error }) }; };
|
|
77
|
+
const p = plan(text, s), tool = (name, input, output, error) => { const id = rid(); push(s, 'tool', { id, name, input }); logTool(s, name, input); return { id, done: () => push(s, 'tool_result', { id, name, output, error: !!error }) }; };
|
|
77
78
|
const allowed = async (name, input) => s.mode === 'tot' || (await ask(s, name, input)) === 'allow';
|
|
78
79
|
try {
|
|
79
80
|
await sleep(500); await stream(s, p.intro, run); if (run.stopped) return;
|
|
@@ -94,8 +95,10 @@
|
|
|
94
95
|
runs.delete(s.id);
|
|
95
96
|
for (const a of approvals.values()) if (a.session === s.id && a.state === 'pending') decide(a.id, 'deny');
|
|
96
97
|
s.state = 'idle'; push(s, 'status', { state: 'idle', code: run.stopped ? 130 : 0 });
|
|
98
|
+
runEnded(s, run.stopped);
|
|
97
99
|
}
|
|
98
100
|
}
|
|
101
|
+
const logTool = (s, tool, input) => audit.unshift({ t: Date.now(), type: 'tool', session: s.id, agent: s.agent, title: s.title, tool, what: input.command || input.file_path || tool, mode: s.mode });
|
|
99
102
|
function create({ agent, cwd, title, mode }) {
|
|
100
103
|
if (!AGENTS.some((a) => a.id === agent)) throw err(400, `A la demo només hi ha ${AGENTS.map((a) => a.name).join(' i ')}`);
|
|
101
104
|
const s = { id: rid(), agent, cwd: cwd || `${ROOT}/web-botiga`, title: title || '', mode: ['llegir', 'preguntar', 'tot'].includes(mode) ? mode : 'preguntar', state: 'idle', created: Date.now(), updated: Date.now(), cost: 0, events: [] };
|
|
@@ -145,6 +148,161 @@
|
|
|
145
148
|
}
|
|
146
149
|
seed();
|
|
147
150
|
|
|
151
|
+
// ---------- Feines: cua, cadenes i programades. Cada feina obre una sessió i acaba quan l'agent acaba.
|
|
152
|
+
const jobs = [];
|
|
153
|
+
const jobOut = (j) => out({ type: 'job', job: { ...j } });
|
|
154
|
+
function runJob(j) {
|
|
155
|
+
const prev = j.after && jobs.find((x) => x.id === j.after);
|
|
156
|
+
const prompt = j.prompt.replace('{{anterior}}', prev?.result || '');
|
|
157
|
+
const s = create({ agent: j.agent, cwd: j.cwd, title: j.title, mode: j.mode });
|
|
158
|
+
Object.assign(j, { state: 'en curs', session: s.id, started: Date.now(), runAt: null });
|
|
159
|
+
jobOut(j); audit.unshift({ t: Date.now(), type: 'job', what: `Feina començada: ${j.title}`, agent: j.agent, job: j.id });
|
|
160
|
+
send(s, prompt);
|
|
161
|
+
}
|
|
162
|
+
function runEnded(s, stopped) {
|
|
163
|
+
const j = jobs.find((x) => x.session === s.id && x.state === 'en curs'); if (!j) return;
|
|
164
|
+
Object.assign(j, { state: stopped ? 'cancel·lada' : 'feta', finished: Date.now(), result: [...s.events].reverse().find((e) => e.kind === 'text')?.text || '' });
|
|
165
|
+
jobOut(j); audit.unshift({ t: Date.now(), type: 'job', what: `Feina ${stopped ? 'aturada' : 'feta'}: ${j.title}`, agent: j.agent, job: j.id });
|
|
166
|
+
if (!stopped) for (const n of jobs) if (n.after === j.id && n.state === 'pendent') runJob(n);
|
|
167
|
+
}
|
|
168
|
+
function schedule(j) {
|
|
169
|
+
if (j.state === 'pendent' && !j.after && (!j.runAt || j.runAt <= Date.now())) return runJob(j);
|
|
170
|
+
if (j.state === 'pendent' && j.runAt && j.runAt - Date.now() < 3600000) setTimeout(() => j.state === 'pendent' && runJob(j), j.runAt - Date.now());
|
|
171
|
+
}
|
|
172
|
+
function addJob(b) {
|
|
173
|
+
const prompt = String(b.prompt || '').trim(); if (!prompt) throw err(400, 'Explica què ha de fer la feina');
|
|
174
|
+
const agent = AGENTS.some((a) => a.id === b.agent) ? b.agent : 'claude';
|
|
175
|
+
if (b.after && !jobs.some((x) => x.id === b.after)) throw err(400, 'La feina anterior no existeix');
|
|
176
|
+
const at = b.at ? Number(new Date(b.at)) : null;
|
|
177
|
+
const j = { id: rid(), title: String(b.title || prompt.replace(/\s+/g, ' ').slice(0, 70)), prompt, agent, cwd: b.cwd || `${ROOT}/web-botiga`, mode: ['llegir', 'preguntar', 'tot'].includes(b.mode) ? b.mode : 'preguntar', rule: null, after: b.after || null, cron: b.cron || null, state: b.cron ? 'programada' : 'pendent', runAt: b.cron ? nextNine() : at, created: Date.now() };
|
|
178
|
+
jobs.unshift(j); jobOut(j);
|
|
179
|
+
audit.unshift({ t: Date.now(), type: 'job', what: `Feina ${j.cron ? 'programada' : 'a la cua'}: ${j.title}`, agent: j.agent, job: j.id });
|
|
180
|
+
setTimeout(() => schedule(j), 300);
|
|
181
|
+
return j;
|
|
182
|
+
}
|
|
183
|
+
// La propera vegada que toca una feina programada (a la demo, sempre el proper dia a les 9).
|
|
184
|
+
const nextNine = () => { const d = new Date(); d.setHours(9, 0, 0, 0); if (d <= new Date()) d.setDate(d.getDate() + 1); return +d; };
|
|
185
|
+
|
|
186
|
+
// ---------- Pantalla: un escriptori dibuixat aquí mateix, que respon als clics, al teclat i als arrossegaments.
|
|
187
|
+
const caps = { navegador: { installed: true, ready: true, enabled: true }, escriptori: { installed: true, ready: true, why: '', enabled: true, screenshot: true }, platform: 'darwin' };
|
|
188
|
+
const W = 1600, H = 1000;
|
|
189
|
+
const desk = {
|
|
190
|
+
ver: 1,
|
|
191
|
+
wins: [
|
|
192
|
+
{ id: 'code', x: 70, y: 70, w: 860, h: 540, title: 'styles.css — web-botiga' },
|
|
193
|
+
{ id: 'term', x: 720, y: 400, w: 780, h: 440, title: 'Terminal — api' },
|
|
194
|
+
],
|
|
195
|
+
term: ['$ npm test', '✓ 14 tests passen (0,8 s)', '$ git status --short', ' M src/styles.css'],
|
|
196
|
+
typed: '', mouse: null, ripple: 0, menu: null,
|
|
197
|
+
};
|
|
198
|
+
const bump = () => { desk.ver++; };
|
|
199
|
+
const hit = (x, y) => [...desk.wins].reverse().find((w) => x >= w.x && x <= w.x + w.w && y >= w.y && y <= w.y + w.h);
|
|
200
|
+
const front = (w) => { desk.wins = desk.wins.filter((x) => x !== w).concat(w); };
|
|
201
|
+
function input(ev) {
|
|
202
|
+
const px = (p) => [p[0] * W, p[1] * H];
|
|
203
|
+
if (ev.type === 'click' || ev.type === 'rclick') {
|
|
204
|
+
const [x, y] = px([ev.x, ev.y]); desk.mouse = [x, y]; desk.ripple = Date.now(); desk.menu = ev.type === 'rclick' ? [x, y, Date.now()] : null;
|
|
205
|
+
const w = hit(x, y); if (w) front(w);
|
|
206
|
+
} else if (ev.type === 'drag' && ev.path?.length > 1) {
|
|
207
|
+
const [x0, y0] = px(ev.path[0]), [x1, y1] = px(ev.path[ev.path.length - 1]), w = hit(x0, y0);
|
|
208
|
+
if (w && y0 - w.y < 36) { w.x = Math.max(-w.w + 80, Math.min(W - 80, w.x + x1 - x0)); w.y = Math.max(28, Math.min(H - 60, w.y + y1 - y0)); front(w); }
|
|
209
|
+
desk.mouse = [x1, y1];
|
|
210
|
+
} else if (ev.type === 'text') desk.typed = (desk.typed + ev.text).slice(-60);
|
|
211
|
+
else if (ev.type === 'key') {
|
|
212
|
+
if (ev.key === 'Backspace' || ev.key === 'BackSpace') desk.typed = desk.typed.slice(0, -1);
|
|
213
|
+
else if (ev.key === 'Enter' || ev.key === 'Return') { const cmd = desk.typed.trim(); desk.term.push(`$ ${cmd}`); if (cmd) desk.term.push(cmd === 'ls' ? 'api notes web-botiga' : cmd === 'pwd' ? ROOT : cmd === 'date' ? new Date().toString() : `demo: «${cmd.split(' ')[0]}» és una ordre simulada`); desk.typed = ''; desk.term = desk.term.slice(-12); }
|
|
214
|
+
}
|
|
215
|
+
bump();
|
|
216
|
+
return { ok: true };
|
|
217
|
+
}
|
|
218
|
+
const C = { bar: 'rgba(255,255,255,.72)', ink: '#272224', code: '#1f1d1c', term: '#141313' };
|
|
219
|
+
function rr(c, x, y, w, h, r) { c.beginPath(); c.roundRect ? c.roundRect(x, y, w, h, r) : c.rect(x, y, w, h); }
|
|
220
|
+
function paint(c) {
|
|
221
|
+
const g = c.createLinearGradient(0, 0, W, H); g.addColorStop(0, '#f6b896'); g.addColorStop(.55, '#e8a7b8'); g.addColorStop(1, '#8ea6dc');
|
|
222
|
+
c.fillStyle = g; c.fillRect(0, 0, W, H);
|
|
223
|
+
c.fillStyle = C.bar; c.fillRect(0, 0, W, 28);
|
|
224
|
+
c.fillStyle = C.ink; c.font = '600 14px -apple-system, Segoe UI, sans-serif'; c.textBaseline = 'middle';
|
|
225
|
+
c.fillText('nio', 18, 14); c.font = '14px -apple-system, Segoe UI, sans-serif';
|
|
226
|
+
let mx = 60; for (const t of ['Fitxer', 'Edita', 'Visualització', 'Finestra', 'Ajuda']) { c.fillText(t, mx, 14); mx += c.measureText(t).width + 24; }
|
|
227
|
+
const now = new Date(); c.textAlign = 'right';
|
|
228
|
+
c.fillText(now.toLocaleString('ca', { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }), W - 18, 14); c.textAlign = 'left';
|
|
229
|
+
for (const w of desk.wins) {
|
|
230
|
+
c.save(); c.shadowColor = 'rgba(0,0,0,.28)'; c.shadowBlur = 40; c.shadowOffsetY = 14;
|
|
231
|
+
c.fillStyle = w.id === 'code' ? C.code : C.term; rr(c, w.x, w.y, w.w, w.h, 12); c.fill(); c.restore();
|
|
232
|
+
c.save(); rr(c, w.x, w.y, w.w, w.h, 12); c.clip();
|
|
233
|
+
c.fillStyle = w.id === 'code' ? '#2b2826' : '#242120'; c.fillRect(w.x, w.y, w.w, 36);
|
|
234
|
+
['#ff5f57', '#febc2e', '#28c840'].forEach((col, i) => { c.fillStyle = col; c.beginPath(); c.arc(w.x + 20 + i * 20, w.y + 18, 6, 0, 7); c.fill(); });
|
|
235
|
+
c.fillStyle = '#bdb6ae'; c.font = '13px -apple-system, Segoe UI, sans-serif'; c.textAlign = 'center'; c.fillText(w.title, w.x + w.w / 2, w.y + 18); c.textAlign = 'left';
|
|
236
|
+
c.font = '16px ui-monospace, SF Mono, Menlo, monospace';
|
|
237
|
+
if (w.id === 'code') {
|
|
238
|
+
const lines = [['#7f8c98', '/* Colors de la botiga */'], ['#e8e2d8', ':root {'], ['#9cdcfe', ' --bg: #fff;'], ['#9cdcfe', ' --text: #1a1a1a;'], ['#e8e2d8', '}'], ['', ''], ['#c586c0', '@media (prefers-color-scheme: dark) {'], ['#9cdcfe', ' :root { --bg: #141414; --text: #eee; }'], ['#e8e2d8', '}'], ['', ''], ['#d7ba7d', '.boto-comprar {'], ['#9cdcfe', ' background: var(--accent);'], ['#9cdcfe', ' border-radius: 999px;'], ['#e8e2d8', '}']];
|
|
239
|
+
lines.forEach(([col, t], i) => { c.fillStyle = '#5c5652'; c.fillText(String(i + 1).padStart(2), w.x + 16, w.y + 66 + i * 26); c.fillStyle = col; c.fillText(t, w.x + 56, w.y + 66 + i * 26); });
|
|
240
|
+
} else {
|
|
241
|
+
const lines = [...desk.term, `$ ${desk.typed}`];
|
|
242
|
+
lines.slice(-14).forEach((t, i, a) => { c.fillStyle = t.startsWith('✓') ? '#7bd88f' : t.startsWith('$') ? '#f2ede6' : '#b9b2aa'; c.fillText(t, w.x + 20, w.y + 64 + i * 26); if (i === a.length - 1 && Math.floor(Date.now() / 500) % 2) { c.fillStyle = '#f2ede6'; c.fillRect(w.x + 20 + c.measureText(t).width + 2, w.y + 54 + i * 26, 9, 20); } });
|
|
243
|
+
}
|
|
244
|
+
c.restore();
|
|
245
|
+
}
|
|
246
|
+
// Dock
|
|
247
|
+
c.fillStyle = 'rgba(255,255,255,.45)'; rr(c, W / 2 - 230, H - 82, 460, 66, 20); c.fill();
|
|
248
|
+
['#f46a3b', '#2b5bd8', '#5b6551', '#e7e66a', '#272224', '#e2382b'].forEach((col, i) => { c.fillStyle = col; rr(c, W / 2 - 205 + i * 70, H - 70, 50, 50, 12); c.fill(); });
|
|
249
|
+
if (desk.menu && Date.now() - desk.menu[2] < 2500) {
|
|
250
|
+
const [x, y] = desk.menu; c.fillStyle = 'rgba(248,246,242,.97)'; rr(c, x, y, 210, 128, 10); c.fill();
|
|
251
|
+
c.fillStyle = C.ink; c.font = '15px -apple-system, Segoe UI, sans-serif'; ['Obre', 'Copia', 'Mostra informació', 'Mou a la paperera'].forEach((t, i) => c.fillText(t, x + 16, y + 22 + i * 28));
|
|
252
|
+
}
|
|
253
|
+
if (desk.mouse) {
|
|
254
|
+
const [x, y] = desk.mouse, age = Date.now() - desk.ripple;
|
|
255
|
+
if (age < 600) { c.strokeStyle = `rgba(244,106,59,${1 - age / 600})`; c.lineWidth = 3; c.beginPath(); c.arc(x, y, 10 + age / 15, 0, 7); c.stroke(); }
|
|
256
|
+
c.fillStyle = '#fff'; c.strokeStyle = '#000'; c.lineWidth = 1.5; c.beginPath(); c.moveTo(x, y); c.lineTo(x, y + 22); c.lineTo(x + 6, y + 17); c.lineTo(x + 11, y + 27); c.lineTo(x + 15, y + 25); c.lineTo(x + 10, y + 15); c.lineTo(x + 17, y + 15); c.closePath(); c.fill(); c.stroke();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Clau de l'estat visible: si no canvia, el panell rep un 204 i no es redibuixa res.
|
|
260
|
+
const deskHash = () => `${desk.ver}-${Math.floor(Date.now() / 500)}-${Date.now() - desk.ripple < 700 ? Date.now() : 0}`;
|
|
261
|
+
async function frame(w, q) {
|
|
262
|
+
const scale = Math.min(1, Math.max(.3, (Number(w) || W) / W));
|
|
263
|
+
const cv = typeof OffscreenCanvas !== 'undefined' ? new OffscreenCanvas(Math.round(W * scale), Math.round(H * scale)) : Object.assign(document.createElement('canvas'), { width: Math.round(W * scale), height: Math.round(H * scale) });
|
|
264
|
+
const c = cv.getContext('2d'); c.scale(scale, scale); paint(c);
|
|
265
|
+
const quality = Math.min(.92, Math.max(.3, (Number(q) || 70) / 100));
|
|
266
|
+
return cv.convertToBlob ? cv.convertToBlob({ type: 'image/jpeg', quality }) : new Promise((r) => cv.toBlob(r, 'image/jpeg', quality));
|
|
267
|
+
}
|
|
268
|
+
const shots = new Map();
|
|
269
|
+
let lastShot = null;
|
|
270
|
+
async function takeShot(from) {
|
|
271
|
+
const id = rid(); shots.set(id, await frame(1600, 80));
|
|
272
|
+
lastShot = { id, t: Date.now(), from: from || 'Aquest navegador' };
|
|
273
|
+
audit.unshift({ t: Date.now(), type: 'tool', agent: 'claude', title: 'Captura de pantalla', tool: 'Bash', what: 'agent-desktop screenshot', mode: 'preguntar' });
|
|
274
|
+
return lastShot;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---------- Activitat d'exemple: el que haurien fet els agents abans que obrissis la demo.
|
|
278
|
+
function seedExtra() {
|
|
279
|
+
const ago = (min) => Date.now() - min * 60000;
|
|
280
|
+
const s1 = [...sessions.values()].find((s) => s.agent === 'claude' && /fosc/.test(s.title));
|
|
281
|
+
const s3 = [...sessions.values()].find((s) => /tests per/.test(s.title));
|
|
282
|
+
jobs.push(
|
|
283
|
+
{ id: rid(), title: 'Revisa les dependències i obre un PR si n’hi ha de noves', prompt: 'Revisa les dependències de l’API i, si n’hi ha de noves, actualitza-les i obre un PR.', agent: 'codex', cwd: `${ROOT}/api`, mode: 'preguntar', rule: null, after: null, cron: '0 9 * * 1-5', state: 'programada', runAt: nextNine(), created: ago(60 * 48) },
|
|
284
|
+
{ id: rid(), title: 'Afegeix el mode fosc a la botiga', prompt: 'Afegeix el mode fosc a la botiga', agent: 'claude', cwd: `${ROOT}/web-botiga`, mode: 'preguntar', rule: null, after: null, cron: null, state: 'feta', session: s1?.id, created: ago(20), started: ago(18), finished: ago(12), result: 'Fet.' },
|
|
285
|
+
{ id: rid(), title: 'Escriu tests per a /usuaris', prompt: 'Escriu tests per a /usuaris', agent: 'claude', cwd: `${ROOT}/api`, mode: 'preguntar', rule: null, after: null, cron: null, state: 'feta', session: s3?.id, created: ago(60 * 27), started: ago(60 * 26 + 5), finished: ago(60 * 26), result: 'Fet.' },
|
|
286
|
+
);
|
|
287
|
+
audit.push(
|
|
288
|
+
{ t: ago(12), type: 'tool', agent: 'claude', title: 'Afegeix el mode fosc a la botiga', tool: 'Bash', what: 'npm run build', mode: 'preguntar' },
|
|
289
|
+
{ t: ago(12.5), type: 'approval', agent: 'claude', title: 'Afegeix el mode fosc a la botiga', tool: 'Edit', what: `${ROOT}/web-botiga/src/styles.css`, why: 'Afegeixo els colors del mode fosc', decision: 'allowed', by: 'iPhone', asked: ago(13) },
|
|
290
|
+
{ t: ago(13), type: 'tool', agent: 'claude', title: 'Afegeix el mode fosc a la botiga', tool: 'Read', what: 'src/styles.css', mode: 'preguntar' },
|
|
291
|
+
{ t: ago(35), type: 'tool', agent: 'claude', title: 'Comprova la botiga al navegador', tool: 'Bash', what: 'agent-browser open http://localhost:4321', mode: 'preguntar' },
|
|
292
|
+
{ t: ago(34), type: 'tool', agent: 'claude', title: 'Comprova la botiga al navegador', tool: 'Bash', what: 'agent-desktop screenshot', mode: 'preguntar' },
|
|
293
|
+
{ t: ago(60 * 5), type: 'device', what: 'iPhone s’ha connectat amb un codi', by: 'iPhone' },
|
|
294
|
+
{ t: ago(60 * 26), type: 'job', what: 'Feina feta: Escriu tests per a /usuaris', agent: 'claude' },
|
|
295
|
+
);
|
|
296
|
+
lastShot = { id: 'inicial', t: ago(34), from: 'Claude Code' };
|
|
297
|
+
}
|
|
298
|
+
seedExtra();
|
|
299
|
+
|
|
300
|
+
const now = Date.now(), NOTES = [
|
|
301
|
+
{ id: 'n1', title: 'Reunió de dimarts', text: 'Parlar amb la Marta del pressupost.\nRevisar el contracte del fuster abans de divendres.', items: null, color: 'lemon', pinned: true, archived: false, trashed: 0, labels: ['feina'], audio: [], created: now - 9e5, updated: now - 9e5 },
|
|
302
|
+
{ id: 'n2', title: 'Compra', text: '', items: [{ id: 'i1', text: 'llet', done: false }, { id: 'i2', text: 'pa de pagès', done: true }, { id: 'i3', text: 'pintura blanca', done: false }], color: 'sky', pinned: false, archived: false, trashed: 0, labels: ['casa'], audio: [], created: now - 8e5, updated: now - 8e5 },
|
|
303
|
+
{ id: 'n3', title: 'Idees per al logo', text: 'El bot que es converteix en la O. Animació suau, fons crema.', items: null, color: 'lilac', pinned: false, archived: false, trashed: 0, labels: ['nio'], audio: [], created: now - 7e5, updated: now - 7e5 },
|
|
304
|
+
];
|
|
305
|
+
const noteId = () => Math.random().toString(36).slice(2, 10);
|
|
148
306
|
const MEMORY = {
|
|
149
307
|
'preferencies.md': '# Preferències\n\n- Respon en català.\n- Fes commits petits i explica’ls bé.\n',
|
|
150
308
|
'projectes/web-botiga.md': '# web-botiga\n\nBotiga en línia feta amb Astro. Els colors són a src/styles.css.\n',
|
|
@@ -154,8 +312,12 @@
|
|
|
154
312
|
async function route(method, p, q, body) {
|
|
155
313
|
let m;
|
|
156
314
|
if (method === 'GET') {
|
|
157
|
-
if (p === '/api/info') return { name: 'nio', version: 'demo', host: 'Ordinador de demo', platform: 'darwin', user: 'tu', publicUrl: null, agents: AGENTS, roots: [ROOT], vapidKey: null, id: 'demo', device: { id: 'demo', name: 'Aquest navegador', primary: true }, demo: true, control:
|
|
158
|
-
if (p === '/api/
|
|
315
|
+
if (p === '/api/info') return { name: 'nio', version: 'demo', host: 'Ordinador de demo', platform: 'darwin', user: 'tu', publicUrl: null, agents: AGENTS, roots: [ROOT], vapidKey: null, id: 'demo', device: { id: 'demo', name: 'Aquest navegador', primary: true }, demo: true, control: caps };
|
|
316
|
+
if (p === '/api/usage') return DEMO_USAGE;
|
|
317
|
+
if (p === '/api/claude-sessions') return { sessions: [{ sessionId: 'demo-terminal', pid: 1, cwd: `${ROOT}/web-botiga`, kind: 'interactive', status: 'busy', name: '', title: 'Arregla el pagament amb targeta', startedAt: Date.now() - 40 * 60000, updated: Date.now() - 2 * 60000, ask: 'Els pagaments amb targeta fallen quan el client canvia de moneda. Mira-ho i arregla-ho.', last: '', terminal: { app: 'iTerm', tmux: false, screen: false, ssh: false }, transcript: true }] };
|
|
318
|
+
if (p === '/api/notes') return { notes: NOTES };
|
|
319
|
+
if (p === '/api/notes/engines') return { engines: [{ id: 'ollama:qwen3:4b', label: 'qwen3:4b · 4.0B', local: true, group: 'Local (Ollama)', params: 4 }] };
|
|
320
|
+
if (p === '/api/sessions') return list();
|
|
159
321
|
if (p === '/api/approvals') return pending();
|
|
160
322
|
if ((m = p.match(/^\/api\/sessions\/([\w-]+)$/))) { const s = sessions.get(m[1]); if (!s) throw err(404, 'Aquesta sessió no existeix'); const since = Number(q.get('since') || 0); return { session: summary(s), events: s.events.filter((e) => e.seq > since) }; }
|
|
161
323
|
if (/^\/api\/sessions\/[\w-]+\/commands$/.test(p)) return [];
|
|
@@ -165,13 +327,13 @@
|
|
|
165
327
|
if (p === '/api/memory') return { dir: '~/.nio/memory', files: Object.entries(MEMORY).map(([path, c]) => ({ path, size: c.length, updated: Date.now() - 86400000 })) };
|
|
166
328
|
if ((m = p.match(/^\/api\/memory\/f\/(.+)$/))) { const path = decodeURIComponent(m[1]); if (!(path in MEMORY)) throw err(404, 'Aquest fitxer no existeix'); return { path, content: MEMORY[path], updated: Date.now() - 86400000 }; }
|
|
167
329
|
if (p === '/api/memory/search') { const t = (q.get('q') || '').toLowerCase(), r = []; for (const [path, c] of Object.entries(MEMORY)) c.split('\n').forEach((l, i) => l.toLowerCase().includes(t) && t && r.push({ path, line: i + 1, text: l })); return r; }
|
|
168
|
-
if (p === '/api/jobs') return { jobs:
|
|
169
|
-
if (p === '/api/audit') return audit.slice(0, 300);
|
|
170
|
-
if (p === '/api/devices') return [{ id: 'demo', name: 'Aquest navegador', primary: true, created: null, lastSeen: Date.now(), current: true }];
|
|
330
|
+
if (p === '/api/jobs') return { jobs: jobs.map((j) => ({ ...j })), routing: [], max: 2 };
|
|
331
|
+
if (p === '/api/audit') return audit.filter((e) => !q.get('type') || e.type === q.get('type')).sort((a, b) => b.t - a.t).slice(0, 300);
|
|
332
|
+
if (p === '/api/devices') return [{ id: 'demo', name: 'Aquest navegador', primary: true, created: null, lastSeen: Date.now(), current: true }, { id: 'iphone', name: 'iPhone', primary: false, created: Date.now() - 3 * 86400000, lastSeen: Date.now() - 5 * 3600000 }];
|
|
171
333
|
if (p === '/api/voice') return { providers: {}, stt: null, tts: null, speak: false, lang: 'ca', voice: {}, demo: true };
|
|
172
|
-
if (p === '/api/control') return
|
|
173
|
-
if (p === '/api/screen') return { last:
|
|
174
|
-
if (p === '/api/desktop/status') return { w:
|
|
334
|
+
if (p === '/api/control') return caps;
|
|
335
|
+
if (p === '/api/screen') return { last: lastShot, enabled: caps.escriptori.enabled || caps.navegador.enabled };
|
|
336
|
+
if (p === '/api/desktop/status') return { w: W, h: H, screen: true, accessibility: true, platform: 'darwin', input: true, capture: true, enabled: caps.escriptori.enabled };
|
|
175
337
|
if (p === '/api/telegram') return { configured: false, bot: null, running: false, allowed: [], pending: [], agent: null, cwd: ROOT, mode: 'preguntar', notify: true, voice: false };
|
|
176
338
|
if (p === '/api/ollama') return { available: false, host: '', models: [], running: [], pulling: [] };
|
|
177
339
|
if (p === '/api/ollama/models' || p === '/api/notices' || p === '/api/ssh/hosts') return [];
|
|
@@ -182,13 +344,33 @@
|
|
|
182
344
|
if ((m = p.match(/^\/api\/sessions\/([\w-]+)\/message$/))) { const s = sessions.get(m[1]); if (!s) throw err(404, 'Aquesta sessió no existeix'); return send(s, body.text); }
|
|
183
345
|
if ((m = p.match(/^\/api\/sessions\/([\w-]+)\/stop$/))) { const r = runs.get(m[1]), s = sessions.get(m[1]); if (!r) return { stopped: false }; r.stopped = true; for (const a of approvals.values()) if (a.session === m[1] && a.state === 'pending') decide(a.id, 'deny'); if (s) push(s, 'info', { text: 'Has aturat l’agent' }); return { stopped: true }; }
|
|
184
346
|
if ((m = p.match(/^\/api\/approvals\/([\w-]+)$/))) { const a = decide(m[1], body.decision === 'allow' ? 'allow' : 'deny'); if (!a) throw err(404, 'Aquesta petició ja s’ha resolt'); return a; }
|
|
347
|
+
if (p === '/api/notes') { const t = Date.now(), n = { id: noteId(), title: '', text: '', items: null, color: '', pinned: false, archived: false, trashed: 0, labels: [], audio: [], created: t, updated: t, ...body }; NOTES.unshift(n); return n; }
|
|
348
|
+
if (p === '/api/notes/ai') { const n = NOTES.find((x) => x.id === body.id), t = (n?.text || '').split('\n').filter(Boolean); return { engine: 'ollama:qwen3:4b', local: true, action: body.action, result: { text: t.map((x) => `- ${x}`).join('\n') || '- (res a resumir)', items: t, title: n?.title || 'Nota', labels: ['demo'] } }; }
|
|
349
|
+
if (p === '/api/jobs') return addJob(body);
|
|
350
|
+
if ((m = p.match(/^\/api\/jobs\/([\w-]+)\/(run|cancel)$/))) {
|
|
351
|
+
const j = jobs.find((x) => x.id === m[1]); if (!j) throw err(404, 'Aquesta feina no existeix');
|
|
352
|
+
if (m[2] === 'run') { if (j.cron) { const c = { ...j, id: rid(), cron: null, state: 'pendent', created: Date.now() }; jobs.unshift(c); runJob(c); } else runJob(j); return j; }
|
|
353
|
+
if (j.state === 'en curs') { const r = runs.get(j.session); if (r) r.stopped = true; }
|
|
354
|
+
Object.assign(j, { state: 'cancel·lada', finished: Date.now() }); jobOut(j); return j;
|
|
355
|
+
}
|
|
356
|
+
if (p === '/api/screen') return takeShot('Aquest navegador');
|
|
357
|
+
if (p === '/api/desktop/session') return { ok: true };
|
|
358
|
+
if (p === '/api/desktop/input') { if (!caps.escriptori.enabled) throw err(400, 'Activa primer «Apps de l’escriptori».'); return input(body); }
|
|
359
|
+
if (p === '/api/panic') { let n = 0; for (const [id, r] of runs) { r.stopped = true; n++; const s = sessions.get(id); if (s) push(s, 'info', { text: 'Aturat des de l’aturada d’emergència' }); } for (const a of approvals.values()) if (a.state === 'pending') decide(a.id, 'deny'); return { stopped: n }; }
|
|
185
360
|
if (p === '/api/voice/stt') throw err(400, 'A la demo no hi ha micròfon. Amb nio instal·lat, dictes amb Whisper, gratis, a l’ordinador.');
|
|
186
361
|
}
|
|
362
|
+
if ((m = p.match(/^\/api\/notes\/([\w-]+)$/))) {
|
|
363
|
+
const n = NOTES.find((x) => x.id === m[1]); if (!n) throw err(404, 'Aquesta nota no existeix');
|
|
364
|
+
if (method === 'PATCH') { Object.assign(n, body, { updated: Date.now() }); if ('trashed' in body) n.trashed = body.trashed ? Date.now() : 0; return n; }
|
|
365
|
+
if (method === 'DELETE') { if (n.trashed || q.get('forever') === '1') NOTES.splice(NOTES.indexOf(n), 1); else n.trashed = Date.now(); return { ok: true }; }
|
|
366
|
+
}
|
|
187
367
|
if ((m = p.match(/^\/api\/sessions\/([\w-]+)$/))) {
|
|
188
368
|
const s = sessions.get(m[1]); if (!s) throw err(404, 'Aquesta sessió no existeix');
|
|
189
369
|
if (method === 'PATCH') { if (['llegir', 'preguntar', 'tot'].includes(body.mode)) s.mode = body.mode; if (typeof body.title === 'string') s.title = body.title.slice(0, 120); touch(s); return summary(s); }
|
|
190
370
|
if (method === 'DELETE') { runs.get(s.id) && (runs.get(s.id).stopped = true); sessions.delete(s.id); out({ type: 'removed', session: s.id }); return { ok: true }; }
|
|
191
371
|
}
|
|
372
|
+
if (method === 'DELETE' && (m = p.match(/^\/api\/jobs\/([\w-]+)$/))) { const i = jobs.findIndex((x) => x.id === m[1]); if (i >= 0) jobs.splice(i, 1); out({ type: 'job', removed: m[1] }); return { ok: true }; }
|
|
373
|
+
if (method === 'PUT' && p === '/api/control') { for (const k of ['navegador', 'escriptori']) if (typeof body[k] === 'boolean') caps[k].enabled = body[k]; return { ...caps, notes: [] }; }
|
|
192
374
|
if ((m = p.match(/^\/api\/memory\/f\/(.+)$/))) {
|
|
193
375
|
if (method === 'PUT') { MEMORY[decodeURIComponent(m[1])] = String(body.content ?? ''); return { ok: true }; }
|
|
194
376
|
if (method === 'DELETE') { delete MEMORY[decodeURIComponent(m[1])]; return { ok: true }; }
|
|
@@ -205,12 +387,23 @@
|
|
|
205
387
|
if (u.pathname === '/api/stream') {
|
|
206
388
|
let ctl;
|
|
207
389
|
const body = new ReadableStream({
|
|
208
|
-
start(c) { ctl = c; streams.add(c); c.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ type: 'hello', sessions: list(), approvals: pending() })}\n\n`)); },
|
|
390
|
+
start(c) { ctl = c; streams.add(c); c.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ type: 'hello', sessions: list(), approvals: pending(), usage: DEMO_USAGE })}\n\n`)); },
|
|
209
391
|
cancel() { streams.delete(ctl); },
|
|
210
392
|
});
|
|
211
393
|
opts.signal?.addEventListener('abort', () => { streams.delete(ctl); try { ctl.close(); } catch { /* ja tancat */ } });
|
|
212
394
|
return new Response(body, { status: 200, headers: { 'content-type': 'text/event-stream' } });
|
|
213
395
|
}
|
|
396
|
+
if (u.pathname === '/api/desktop/frame') {
|
|
397
|
+
if (!caps.escriptori.enabled) return json(400, { error: 'Activa primer «Apps de l’escriptori».' });
|
|
398
|
+
const h = deskHash(), hd = { 'x-hash': h, 'x-screen-w': String(W), 'x-screen-h': String(H), 'x-blank': '0' };
|
|
399
|
+
if (u.searchParams.get('h') === h) return new Response(null, { status: 204, headers: hd });
|
|
400
|
+
return new Response(await frame(u.searchParams.get('w'), u.searchParams.get('q')), { status: 200, headers: { ...hd, 'content-type': 'image/jpeg' } });
|
|
401
|
+
}
|
|
402
|
+
let sm;
|
|
403
|
+
if ((sm = u.pathname.match(/^\/api\/screen\/shot\/([\w-]+)$/))) {
|
|
404
|
+
if (!shots.has(sm[1])) shots.set(sm[1], await frame(1600, 80));
|
|
405
|
+
return new Response(shots.get(sm[1]), { status: 200, headers: { 'content-type': 'image/jpeg' } });
|
|
406
|
+
}
|
|
214
407
|
await sleep(60);
|
|
215
408
|
let body = {};
|
|
216
409
|
try { body = typeof opts.body === 'string' ? JSON.parse(opts.body) : {}; } catch { /* àudio o res */ }
|
package/web/app/index.html
CHANGED
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
<meta property="og:url" content="https://www.nio.cat/app">
|
|
18
18
|
<meta property="og:title" content="nio // Panell">
|
|
19
19
|
<meta property="og:description" content="Dona feina als agents d’IA del teu ordinador des del mòbil, segueix-los en directe i aprova cada pas.">
|
|
20
|
-
<meta property="og:image" content="https://www.nio.cat/og.
|
|
20
|
+
<meta property="og:image" content="https://www.nio.cat/og.jpg">
|
|
21
21
|
<meta property="og:image:width" content="1200">
|
|
22
22
|
<meta property="og:image:height" content="630">
|
|
23
23
|
<meta property="og:image:alt" content="nio">
|
|
24
24
|
<meta name="twitter:card" content="summary_large_image">
|
|
25
25
|
<meta name="twitter:title" content="nio // Panell">
|
|
26
26
|
<meta name="twitter:description" content="Dona feina als agents d’IA del teu ordinador des del mòbil, segueix-los en directe i aprova cada pas.">
|
|
27
|
-
<meta name="twitter:image" content="https://www.nio.cat/og.
|
|
27
|
+
<meta name="twitter:image" content="https://www.nio.cat/og.jpg">
|
|
28
28
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
29
29
|
<meta name="format-detection" content="telephone=no">
|
|
30
30
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
<link rel="stylesheet" href="/bot.css">
|
|
36
36
|
<link rel="stylesheet" href="/board.css">
|
|
37
37
|
<link rel="stylesheet" href="/app/style.css">
|
|
38
|
+
<link rel="stylesheet" href="/app/notes.css">
|
|
39
|
+
<link rel="stylesheet" href="/app/call.css">
|
|
38
40
|
</head>
|
|
39
41
|
<body>
|
|
40
42
|
<header class="mbar">
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/* nio · Notes */
|
|
2
|
+
.page:has(.nt) { max-width: 1240px; }
|
|
3
|
+
.nt-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; }
|
|
4
|
+
.nt-title h1 { font-size: 30px; font-weight: 500; letter-spacing: -.05em; line-height: 1.1; }
|
|
5
|
+
.nt-title p { margin-top: 4px; color: var(--fg-muted); }
|
|
6
|
+
.nt-bar { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
|
|
7
|
+
.nt-search { flex: 1; position: relative; display: flex; align-items: center; }
|
|
8
|
+
.nt-search .ico { position: absolute; left: 13px; width: 17px; height: 17px; color: var(--fg-muted); pointer-events: none; }
|
|
9
|
+
.nt-search input { width: 100%; height: 42px; padding: 0 14px 0 38px; border: 1px solid var(--border); border-radius: 12px; background: var(--white); font: inherit; color: var(--fg); transition: border-color .2s, box-shadow .2s; }
|
|
10
|
+
.nt-search input:focus { outline: none; border-color: var(--fg); box-shadow: 0 0 0 3px color-mix(in srgb, var(--fg) 10%, transparent); }
|
|
11
|
+
.nt-new { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; margin: 0 0 20px; padding: 8px; border: 1px solid var(--border); border-radius: 14px; background: var(--white); color: var(--fg-muted); cursor: default; box-shadow: 0 1px 2px rgba(39,34,36,.05), 0 6px 20px -14px rgba(39,34,36,.35); transition: box-shadow .25s var(--ease), transform .25s var(--ease), border-color .2s; }
|
|
12
|
+
.nt-new:hover { border-color: color-mix(in srgb, var(--fg) 30%, var(--border)); box-shadow: 0 1px 2px rgba(39,34,36,.06), 0 10px 28px -14px rgba(39,34,36,.45); }
|
|
13
|
+
.nt-new:active { transform: scale(.995); }
|
|
14
|
+
.nt-write { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; min-height: 44px; padding: 0 12px; border: 0; border-radius: 8px; background: transparent; text-align: left; font: inherit; color: var(--fg-muted); cursor: pointer; }
|
|
15
|
+
.nt-new-b { display: flex; gap: 2px; }
|
|
16
|
+
.nt-new-b button, .nt-ib { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-width: 40px; height: 40px; padding: 0 8px; border: 0; border-radius: 10px; background: transparent; color: var(--fg-muted); cursor: pointer; font: inherit; font-size: 13px; font-weight: 500; transition: background .15s, color .15s, transform .15s var(--ease); }
|
|
17
|
+
.nt-new-b button:hover, .nt-ib:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); color: var(--fg); }
|
|
18
|
+
.nt-ib:active, .nt-new-b button:active { transform: scale(.92); }
|
|
19
|
+
.nt-ib.on { color: var(--fg); background: color-mix(in srgb, var(--fg) 10%, transparent); }
|
|
20
|
+
.nt-ib.sm { min-width: 30px; height: 30px; }
|
|
21
|
+
.nt .ico, .nt-ed .ico, .nt-pop .ico { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; flex: none; }
|
|
22
|
+
.nt-ib.on .ico { fill: currentColor; fill-opacity: .15; }
|
|
23
|
+
|
|
24
|
+
.nt-filters { display: grid; overflow: visible; gap: 10px; margin-bottom: 0; padding: 2px 2px 12px; }
|
|
25
|
+
.nt-filter-views, .nt-filter-labels { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
26
|
+
.nt-filter-labels > span, .nt-results { font-size: 12px; color: var(--fg-muted); }
|
|
27
|
+
.nt-results { margin: 2px 2px 16px; overflow-wrap: anywhere; }
|
|
28
|
+
.nt-chip { display: inline-flex; align-items: center; gap: 6px; flex: none; min-height: 36px; height: auto; white-space: normal; overflow-wrap: anywhere; max-width: 100%; padding: 0 13px; border: 1px solid var(--border); border-radius: 999px; background: transparent; color: var(--fg-muted); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: background .18s, color .18s, border-color .18s, transform .18s var(--ease); }
|
|
29
|
+
.nt-chip .ico { width: 15px; height: 15px; }
|
|
30
|
+
.nt-chip:hover { border-color: var(--fg); color: var(--fg); }
|
|
31
|
+
.nt-chip[aria-current="true"] { background: var(--fg); color: var(--bg); border-color: var(--fg); }
|
|
32
|
+
.nt-chip:active { transform: scale(.96); }
|
|
33
|
+
|
|
34
|
+
.nt-sec { margin: 8px 2px 10px; font-size: 11px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: var(--fg-muted); }
|
|
35
|
+
.nt-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); gap: 16px; align-items: start; margin-bottom: 28px; }
|
|
36
|
+
.nt[data-layout="list"] .nt-grid { grid-template-columns: 1fr; max-width: 800px; margin-inline: auto; }
|
|
37
|
+
.nt[data-layout="list"] .nt-txt { -webkit-line-clamp: 4; }
|
|
38
|
+
.nt-grid > .nt-card { margin-bottom: 0; min-width: 0; }
|
|
39
|
+
.nt-card { position: relative; display: block; break-inside: avoid; margin: 0 0 14px; padding: 14px 15px 12px; border: 1px solid color-mix(in srgb, var(--fg) 13%, transparent); border-radius: 14px; background: var(--nc, var(--white)); cursor: pointer; transition: box-shadow .25s var(--ease), transform .25s var(--ease), opacity .2s; }
|
|
40
|
+
.nt-card:hover { box-shadow: 0 10px 28px -16px rgba(39,34,36,.5); transform: translateY(-2px); }
|
|
41
|
+
.nt-card:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
|
|
42
|
+
.nt-card h3 { margin: 0 28px 6px 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; line-height: 1.3; overflow-wrap: anywhere; }
|
|
43
|
+
.nt-txt { font-size: 14px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 10; -webkit-box-orient: vertical; overflow: hidden; }
|
|
44
|
+
.nt-txt.muted { color: var(--fg-muted); }
|
|
45
|
+
.nt-its { display: grid; gap: 3px; margin-top: 4px; }
|
|
46
|
+
.nt-it { display: flex; align-items: flex-start; gap: 8px; font-size: 14px; line-height: 1.4; }
|
|
47
|
+
.nt-it input { margin: 3px 0 0; accent-color: var(--fg); flex: none; cursor: pointer; width: 15px; height: 15px; }
|
|
48
|
+
.nt-it span { overflow-wrap: anywhere; }
|
|
49
|
+
.nt-it.done span { text-decoration: line-through; color: var(--fg-muted); }
|
|
50
|
+
.nt-more { font-size: 12px; color: var(--fg-muted); margin-left: 23px; }
|
|
51
|
+
.nt-pin { position: absolute; top: 8px; right: 8px; width: 30px; height: 30px; display: grid; place-items: center; border: 0; border-radius: 50%; background: transparent; color: var(--fg-muted); cursor: pointer; opacity: .65; transition: opacity .18s, background .15s; }
|
|
52
|
+
.nt-pin .ico { width: 17px; height: 17px; }
|
|
53
|
+
.nt-card:hover .nt-pin, .nt-pin.on, .nt-pin:focus-visible { opacity: 1; }
|
|
54
|
+
.nt-pin.on { color: var(--fg); } .nt-pin.on .ico { fill: currentColor; fill-opacity: .2; }
|
|
55
|
+
.nt-pin:hover { background: color-mix(in srgb, var(--fg) 10%, transparent); }
|
|
56
|
+
.nt-badge { display: inline-flex; align-items: center; gap: 5px; margin-top: 8px; font-size: 12px; font-weight: 500; color: var(--fg-muted); }
|
|
57
|
+
.nt-badge .ico { width: 14px; height: 14px; } .nt-badge.err { color: var(--red); }
|
|
58
|
+
.nt-labels { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
|
59
|
+
.nt-label { display: inline-flex; align-items: center; gap: 2px; height: auto; min-height: 22px; max-width: 100%; overflow-wrap: anywhere; padding: 0 9px; border-radius: 999px; background: color-mix(in srgb, var(--fg) 9%, transparent); font-size: 12px; font-weight: 500; }
|
|
60
|
+
.nt-label button { display: grid; place-items: center; width: 16px; height: 16px; margin: 0 -5px 0 2px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: inherit; cursor: pointer; }
|
|
61
|
+
.nt-label button .ico { width: 11px; height: 11px; }
|
|
62
|
+
.nt-acts { display: flex; gap: 2px; justify-content: flex-end; height: 36px; margin-top: 12px; }
|
|
63
|
+
.nt-acts button { width: 32px; height: 32px; display: grid; place-items: center; border: 0; border-radius: 9px; background: transparent; color: var(--fg-muted); cursor: pointer; }
|
|
64
|
+
.nt-acts button:hover { background: color-mix(in srgb, var(--fg) 10%, transparent); color: var(--fg); }
|
|
65
|
+
.nt-acts .ico { width: 17px; height: 17px; }
|
|
66
|
+
@media (hover: none) { .nt-pin { opacity: 1; } }
|
|
67
|
+
.nt-empty { padding: 56px 12px; text-align: center; color: var(--fg-muted); }
|
|
68
|
+
.nt-trash-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; color: var(--fg-muted); font-size: 13px; }
|
|
69
|
+
.nt-skel { display: block; height: 110px; margin-bottom: 14px; border-radius: 14px; background: linear-gradient(100deg, var(--bg-2) 30%, color-mix(in srgb, var(--bg-2) 55%, var(--white)) 50%, var(--bg-2) 70%) 0 0 / 220% 100%; animation: nt-sh 1.3s linear infinite; break-inside: avoid; }
|
|
70
|
+
.nt-skel.tall { height: 170px; }
|
|
71
|
+
@keyframes nt-sh { to { background-position: -120% 0; } }
|
|
72
|
+
.nt-undo { margin-left: 8px; padding: 0 2px; border: 0; background: none; color: var(--lemon); font: inherit; font-weight: 600; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
|
|
73
|
+
|
|
74
|
+
/* colors (targetes i editor) */
|
|
75
|
+
[data-color="lemon"] { --nc: #f1efa4; } [data-color="peach"] { --nc: #f8d9c8; } [data-color="moss"] { --nc: #d6dccb; }
|
|
76
|
+
[data-color="sky"] { --nc: #d3e3ea; } [data-color="lilac"] { --nc: #e0d9ec; } [data-color="rose"] { --nc: #f1d5d1; } [data-color="sand"] { --nc: #e7ddc9; }
|
|
77
|
+
[data-color=""] { --nc: var(--white); }
|
|
78
|
+
|
|
79
|
+
/* editor */
|
|
80
|
+
.nt-ov { position: fixed; inset: 0; z-index: 70; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(39,34,36,.32); backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); overscroll-behavior: contain; }
|
|
81
|
+
body:has(.nt-ov) { overflow: hidden; }
|
|
82
|
+
.nt-ed { position: relative; display: flex; flex-direction: column; width: min(800px, 100%); min-height: min(480px, 80dvh); border-radius: 18px; background: var(--nc, var(--white)); border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent); box-shadow: 0 30px 80px -24px rgba(39,34,36,.6); transition: background .25s; }
|
|
83
|
+
.nt-ed-nav { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px 0; }
|
|
84
|
+
.nt-ed-nav > span { font-size: 12px; color: var(--fg-muted); }
|
|
85
|
+
.nt-back { color: var(--fg); }
|
|
86
|
+
.nt-ed-head { display: flex; align-items: center; gap: 6px; padding: 12px 28px 8px; }
|
|
87
|
+
.nt-ed input.nt-ed-title { flex: 1; width: auto; padding: 0; border-radius: 0; box-shadow: none; min-width: 0; height: 40px; border: 0; background: transparent; font: inherit; font-size: 20px; font-weight: 600; letter-spacing: -.02em; color: var(--fg); outline: none; }
|
|
88
|
+
.nt-ed-title::placeholder, .nt-ed-text::placeholder, .nt-item input[type=text]::placeholder { color: color-mix(in srgb, var(--fg) 40%, transparent); }
|
|
89
|
+
.nt-ed-body { flex: 1; min-height: 80px; overflow-y: auto; padding: 4px 28px 24px; overscroll-behavior: contain; }
|
|
90
|
+
.nt-ed-text { display: block; width: 100%; min-height: 220px; max-height: 52vh; padding: 0; border: 0; background: transparent; font: inherit; font-size: 16px; line-height: 1.6; color: var(--fg); resize: none; outline: none; overflow-y: auto; }
|
|
91
|
+
.nt-ed-text.nt-desc { min-height: 0; margin-bottom: 10px; }
|
|
92
|
+
.nt-item { display: flex; align-items: center; gap: 10px; padding: 3px 0; }
|
|
93
|
+
.nt-item input[type=checkbox] { width: 17px; height: 17px; accent-color: var(--fg); flex: none; cursor: pointer; }
|
|
94
|
+
.nt-item input[type=text] { flex: 1; min-width: 0; height: 30px; padding: 0; border: 0; background: transparent; font: inherit; font-size: 15px; color: var(--fg); outline: none; }
|
|
95
|
+
.nt-item.done input[type=text] { text-decoration: line-through; color: var(--fg-muted); }
|
|
96
|
+
.nt-x { width: 26px; height: 26px; display: grid; place-items: center; border: 0; border-radius: 50%; background: transparent; color: var(--fg-muted); cursor: pointer; opacity: 0; transition: opacity .15s; }
|
|
97
|
+
.nt-item:hover .nt-x, .nt-x:focus-visible { opacity: 1; } .nt-x .ico { width: 14px; height: 14px; }
|
|
98
|
+
@media (hover: none) { .nt-x { opacity: .7; } }
|
|
99
|
+
.nt-add { display: flex; align-items: center; gap: 10px; margin: 2px 0 6px; padding: 4px 0; border: 0; background: transparent; color: var(--fg-muted); font: inherit; font-size: 14px; cursor: pointer; }
|
|
100
|
+
.nt-add .ico { width: 17px; height: 17px; } .nt-add:hover { color: var(--fg); }
|
|
101
|
+
.nt-donehd { margin: 10px 0 4px; font-size: 12px; font-weight: 600; color: var(--fg-muted); }
|
|
102
|
+
.nt-audio { display: grid; gap: 6px; padding: 0 20px 8px; }
|
|
103
|
+
.nt-au { display: flex; align-items: center; gap: 8px; padding: 6px 10px 6px 6px; border-radius: 12px; background: color-mix(in srgb, var(--fg) 6%, transparent); font-size: 13px; }
|
|
104
|
+
.nt-au .nt-ib { background: var(--fg); color: var(--bg); } .nt-au .nt-ib .ico { width: 14px; height: 14px; fill: currentColor; }
|
|
105
|
+
.nt-au-d { font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
106
|
+
.nt-au-s { flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; color: var(--fg-muted); }
|
|
107
|
+
.nt-au-s .ico { width: 14px; height: 14px; color: var(--moss); }
|
|
108
|
+
.nt-au[data-st="error"] .nt-au-s { color: var(--red); }
|
|
109
|
+
.nt-spin { width: 13px; height: 13px; border: 2px solid color-mix(in srgb, var(--fg) 20%, transparent); border-top-color: var(--fg); border-radius: 50%; animation: nt-rot .7s linear infinite; flex: none; }
|
|
110
|
+
@keyframes nt-rot { to { transform: rotate(360deg); } }
|
|
111
|
+
.nt-ed-labels { padding: 0 20px 6px; margin: 0; }
|
|
112
|
+
.nt-ed-nav, .nt-ed-head, .nt-ed-foot, .nt-audio, .nt-ed-labels, .nt-rec { flex-shrink: 0; }
|
|
113
|
+
.nt-save { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 4px 8px; min-width: 0; font-size: 12px; color: var(--fg-muted); text-align: right; }
|
|
114
|
+
.nt-save .err { color: var(--red); }
|
|
115
|
+
.nt-save button[hidden] { display: none; }
|
|
116
|
+
.nt-warning { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-bottom: 16px; padding: 12px; border: 1px solid var(--border); border-radius: 12px; background: var(--bg-2); font-size: 13px; }
|
|
117
|
+
.nt :is(button, input):focus-visible, .nt-ed :is(button, input, textarea):focus-visible { outline: 2px solid var(--fg); outline-offset: 3px; }
|
|
118
|
+
/* Dins la nota s'escriu com en un full: cap requadre al voltant del text, només el cursor. */
|
|
119
|
+
.nt-ed :is(.nt-ed-title, .nt-ed-text, .nt-item input[type=text]):is(:focus, :focus-visible), .nt-search input:focus-visible { outline: none; }
|
|
120
|
+
.nt-ed :is(.nt-ed-title, .nt-ed-text, .nt-item input[type=text]) { -webkit-tap-highlight-color: transparent; }
|
|
121
|
+
.nt-pop { overflow-y: auto; overscroll-behavior: contain; }
|
|
122
|
+
.nt-ed-foot { border-top: 1px solid color-mix(in srgb, var(--fg) 10%, transparent); padding: 12px 16px; }
|
|
123
|
+
.nt-tb { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; }
|
|
124
|
+
.nt-edit-tools, .nt-note-tools { display: flex; align-items: center; gap: 2px; min-width: 0; }
|
|
125
|
+
.nt-note-tools { margin-left: auto; }
|
|
126
|
+
.nt-ai-b { padding: 0 11px; } .nt-ai-b span { display: inline; }
|
|
127
|
+
.nt-done { height: 36px; padding: 0 16px; margin-left: 6px; }
|
|
128
|
+
.nt-rec { display: flex; align-items: center; gap: 10px; margin: 0 14px 12px; padding: 10px 12px; border-radius: 14px; background: color-mix(in srgb, var(--red) 10%, var(--white)); }
|
|
129
|
+
.nt-rec[hidden], .nt-ai[hidden], .nt-ed-foot[hidden], .nt-audio[hidden], .nt-ed-labels[hidden] { display: none; }
|
|
130
|
+
.nt-rec-dot { width: 12px; height: 12px; border-radius: 50%; background: var(--red); animation: nt-pulse 1.1s ease-in-out infinite; flex: none; }
|
|
131
|
+
@keyframes nt-pulse { 0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 color-mix(in srgb, var(--red) 45%, transparent); } 50% { transform: scale(1.15); box-shadow: 0 0 0 8px transparent; } }
|
|
132
|
+
.nt-rec-t { font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
133
|
+
.nt-rec-l { flex: 1; color: var(--fg-muted); font-size: 13px; }
|
|
134
|
+
.nt .btn.sm, .nt-ed .btn.sm, .nt-pop .btn.sm { height: 30px; padding: 0 12px; font-size: 13px; }
|
|
135
|
+
|
|
136
|
+
/* Panell «Més» (mòbil): llista de text, sense icones; es tanca lliscant cap avall o tocant fora. */
|
|
137
|
+
.nt-more-btn { display: none; }
|
|
138
|
+
.nt-more-scrim { position: absolute; inset: 0; z-index: 5; background: rgba(39,34,36,.28); transition: opacity .25s; }
|
|
139
|
+
.nt-more-scrim[hidden] { display: none; }
|
|
140
|
+
.nt-more-sheet { position: absolute; bottom: 0; left: 0; right: 0; z-index: 6; display: grid; gap: 2px; padding: 6px 12px calc(12px + var(--safe-b)); border-radius: 18px 18px 0 0; background: var(--white); box-shadow: 0 -8px 30px rgba(39,34,36,.14); touch-action: none; transition: transform .3s cubic-bezier(.22,1,.36,1); }
|
|
141
|
+
.nt-more-sheet[hidden] { display: none; }
|
|
142
|
+
.nt-more-grab { width: 40px; height: 5px; margin: 4px auto 10px; border-radius: 5px; background: var(--border); }
|
|
143
|
+
.nt-more-sheet button { display: block; width: 100%; min-height: 52px; padding: 0 14px; border: 0; border-radius: 12px; background: transparent; text-align: left; font: inherit; font-size: 16px; font-weight: 500; color: var(--fg); cursor: pointer; transition: background .15s; }
|
|
144
|
+
.nt-more-sheet button:active { background: color-mix(in srgb, var(--fg) 8%, transparent); }
|
|
145
|
+
.nt-more-sheet button.danger { color: var(--red); }
|
|
146
|
+
.nt-more-sheet button.muted { color: var(--fg-muted); text-align: center; }
|
|
147
|
+
|
|
148
|
+
/* finestretes */
|
|
149
|
+
.nt-pop { position: absolute; z-index: 5; min-width: 220px; max-width: min(340px, calc(100% - 16px)); padding: 8px; border: 1px solid var(--border); border-radius: 14px; background: var(--white); box-shadow: 0 18px 50px -14px rgba(39,34,36,.5); animation: nt-pop .18s var(--ease); transform-origin: bottom left; }
|
|
150
|
+
@keyframes nt-pop { from { opacity: 0; transform: translateY(6px) scale(.97); } }
|
|
151
|
+
.nt-pad { display: block; padding: 6px 8px; }
|
|
152
|
+
.nt-colors { display: flex; gap: 8px; padding: 6px; flex-wrap: wrap; }
|
|
153
|
+
.nt-dot { width: 28px; height: 28px; border-radius: 50%; border: 1.5px solid color-mix(in srgb, var(--fg) 22%, transparent); background: var(--nc, var(--white)); cursor: pointer; transition: transform .15s var(--ease); }
|
|
154
|
+
.nt-dot:hover { transform: scale(1.12); } .nt-dot[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--white), 0 0 0 3.5px var(--fg); }
|
|
155
|
+
.nt-lf { display: flex; gap: 6px; padding: 4px; }
|
|
156
|
+
.nt-lf input { flex: 1; min-width: 0; height: 30px; padding: 0 10px; border: 1px solid var(--border); border-radius: 9px; background: var(--white); font: inherit; font-size: 13px; color: var(--fg); }
|
|
157
|
+
.nt-lsug { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 4px 2px; }
|
|
158
|
+
.nt-sel { width: 100%; height: 34px; margin-top: 4px; padding: 0 8px; border: 1px solid var(--border); border-radius: 9px; background: var(--white); font: inherit; font-size: 13px; color: var(--fg); }
|
|
159
|
+
.nt-acts-list { display: grid; gap: 2px; padding: 4px 0; }
|
|
160
|
+
.nt-acts-list button { display: grid; gap: 1px; width: 100%; padding: 8px 10px; border: 0; border-radius: 10px; background: transparent; text-align: left; font: inherit; color: var(--fg); cursor: pointer; transition: background .15s; }
|
|
161
|
+
.nt-acts-list button:hover:not(:disabled) { background: color-mix(in srgb, var(--fg) 7%, transparent); }
|
|
162
|
+
.nt-acts-list button:disabled { opacity: .45; cursor: default; }
|
|
163
|
+
.nt-acts-list b { font-size: 14px; font-weight: 600; } .nt-acts-list span { font-size: 12px; color: var(--fg-muted); }
|
|
164
|
+
.nt-note { padding: 4px 8px 2px; }
|
|
165
|
+
.nt-steps { margin: 0; padding-left: 20px; display: grid; gap: 6px; } .nt-steps .ico { width: 15px; height: 15px; vertical-align: -3px; }
|
|
166
|
+
|
|
167
|
+
/* panell de l'assistent dins l'editor */
|
|
168
|
+
.nt-ai { margin: 0 14px 10px; padding: 12px 14px; border-radius: 14px; background: rgba(255,255,255,.62); border: 1px solid color-mix(in srgb, var(--fg) 10%, transparent); animation: nt-pop .22s var(--ease); max-height: 38vh; overflow: auto; }
|
|
169
|
+
.nt-ai-run, .nt-ai-err { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--fg-muted); } .nt-ai-err { color: var(--red); justify-content: space-between; }
|
|
170
|
+
.nt-ai-h { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 8px; font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }
|
|
171
|
+
.nt-ai-h span { display: inline-flex; align-items: center; gap: 6px; } .nt-ai-h .ico { width: 15px; height: 15px; } .nt-ai-h small { font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--fg-muted); }
|
|
172
|
+
.nt-ai-b { font-size: 14px; line-height: 1.5; } .nt-ai-b pre { margin: 6px 0 0; white-space: pre-wrap; font: inherit; } .nt-ai-b ul { margin: 0; padding-left: 18px; } .nt-ai-b p { margin: 0 0 6px; }
|
|
173
|
+
.nt-ai-f { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
|
|
174
|
+
|
|
175
|
+
@media (max-width: 700px) {
|
|
176
|
+
.page:has(.nt) { padding-inline: 14px; }
|
|
177
|
+
.nt-title h1 { font-size: 26px; } .nt-title p { display: none; }
|
|
178
|
+
.nt-top { margin-bottom: 12px; } .nt-tools .btn span { display: none; } .nt-tools .btn { padding: 0 11px; }
|
|
179
|
+
.nt-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } .nt-card { margin-bottom: 10px; padding: 12px; }
|
|
180
|
+
.nt-ov { padding: 0; align-items: flex-end; top: var(--vvt, 0px); bottom: auto; height: var(--vvh, 100dvh); background: var(--bg); backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
181
|
+
.nt-ed { width: 100%; min-height: 0; height: 100%; max-height: 100%; border-radius: 0; border: 0; padding-top: var(--safe-t); padding-bottom: var(--safe-b); }
|
|
182
|
+
.nt-ed-nav { padding-inline: 12px; } .nt-ed-nav > span { font-size: 11px; }
|
|
183
|
+
.nt-ed-head { padding: 10px 16px 8px; }
|
|
184
|
+
.nt-ed-body { padding: 4px 16px 16px; }
|
|
185
|
+
.nt-ed-foot { padding: 8px; }
|
|
186
|
+
.nt-ed-title { font-size: 19px; }
|
|
187
|
+
.nt-ai-b span { display: none; } .nt-ai-b { padding: 0 9px; }
|
|
188
|
+
.nt-new-b button, .nt-ib, .nt-acts button { min-width: 44px; height: 44px; }
|
|
189
|
+
.nt-acts { height: 44px; }
|
|
190
|
+
.nt-chip { min-height: 44px; }
|
|
191
|
+
.nt-tb { gap: 0; }
|
|
192
|
+
.nt-done { height: 44px; }
|
|
193
|
+
.nt-search input { height: 44px; font-size: 16px; }
|
|
194
|
+
.nt-filter-views { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
|
|
195
|
+
.nt-filter-views .nt-chip { padding: 8px 4px; justify-content: center; gap: 4px; font-size: 12px; }
|
|
196
|
+
.nt-filter-views .ico { width: 14px; height: 14px; }
|
|
197
|
+
.nt-edit-tools { width: 100%; justify-content: space-around; }
|
|
198
|
+
.nt-tb .nt-ib .ico { width: 22px; height: 22px; }
|
|
199
|
+
.nt-more-btn { display: inline-flex; }
|
|
200
|
+
.nt-note-tools { display: none; }
|
|
201
|
+
.nt-pin { width: 44px; height: 44px; top: 2px; right: 2px; }
|
|
202
|
+
.nt-card h3 { margin-right: 32px; }
|
|
203
|
+
.nt-item { min-height: 44px; }
|
|
204
|
+
.nt-x { width: 44px; height: 44px; opacity: .7; }
|
|
205
|
+
.nt-add { min-height: 44px; }
|
|
206
|
+
.nt-dot { width: 36px; height: 36px; }
|
|
207
|
+
.nt-colors { gap: 10px; }
|
|
208
|
+
.nt-lf input, .nt-sel { min-height: 44px; font-size: 16px; }
|
|
209
|
+
.nt-save { font-size: 11px; }
|
|
210
|
+
}
|
|
211
|
+
@media (max-width: 380px) { .nt-grid { grid-template-columns: 1fr; } }
|
|
212
|
+
/* Lliscar per arxivar (tàctil): el navegador només desplaça en vertical; l'horitzontal és nostre. */
|
|
213
|
+
@media (hover: none) { .nt-card { touch-action: pan-y; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; } .nt-card:hover { transform: none; box-shadow: none; } }
|
|
214
|
+
.nt-card.swiping { z-index: 2; box-shadow: 0 14px 34px -16px rgba(39,34,36,.55); cursor: grabbing; will-change: transform; }
|
|
215
|
+
/* Al mòbil, la llista de notes sense icones: només el contingut. Arxivar és lliscar; la resta, dins la nota. */
|
|
216
|
+
@media (max-width: 700px) {
|
|
217
|
+
.nt-card .nt-acts, .nt-card .nt-pin, .nt-chip .ico, .nt-badge .ico, .nt-filter-views .ico { display: none; }
|
|
218
|
+
.nt-card h3 { margin-right: 0; }
|
|
219
|
+
.nt-badge { margin-top: 6px; }
|
|
220
|
+
}
|
|
221
|
+
@media (min-width: 701px) {
|
|
222
|
+
.nt-ov { top: var(--vvt, 0px); bottom: auto; height: var(--vvh, 100dvh); }
|
|
223
|
+
.nt-ed { min-height: min(480px, calc(var(--vvh, 100dvh) - 48px)); max-height: min(820px, calc(var(--vvh, 100dvh) - 48px)); }
|
|
224
|
+
}
|
|
225
|
+
@media (prefers-reduced-motion: reduce) { .nt-card, .nt-new, .nt-pop, .nt-ai { transition: none; animation: none; } .nt-rec-dot, .nt-spin, .nt-skel { animation: none; } }
|