nio-cat 0.5.1 → 0.8.0

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/web/app/style.css CHANGED
@@ -314,7 +314,7 @@ body.board-mode .view-enter { animation: none; }
314
314
 
315
315
  @media (prefers-reduced-motion: no-preference) {
316
316
  .view-enter { animation: view-in .25s var(--ease); }
317
- @keyframes view-in { from { opacity: 0; transform: translateY(10px); } }
317
+ @keyframes view-in { from { opacity: 0; } }
318
318
  }
319
319
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } }
320
320
 
@@ -348,12 +348,13 @@ body.board-mode .view-enter { animation: none; }
348
348
  .paste .pair-form { flex-direction: column; }
349
349
  .side nav .grp { padding: 16px 12px 4px; }
350
350
  .top h1 { font-size: 26px; }
351
- .chat { height: calc(100dvh - 56px - var(--safe-t)); }
352
- body.chat-mode { overflow: hidden; }
353
- body.chat-mode .shell { min-height: 0; }
354
- body.chat-mode .mbar { position: fixed; left: 0; right: 0; transform: translateY(var(--vvt, 0px)); }
355
- body.chat-mode .chat { position: fixed; left: 0; right: 0; top: calc(56px + var(--safe-t) + var(--vvt, 0px)); height: calc(var(--vvh, 100dvh) - 56px - var(--safe-t)); }
356
- body.chat-mode.kb .composer { padding-bottom: 10px; }
351
+ /* Al mòbil només fa scroll el document: res de contenidors amb scroll propi. */
352
+ .chat { height: auto; min-height: calc(100dvh - 56px - var(--safe-t)); }
353
+ .mode-bar { position: sticky; top: calc(56px + var(--safe-t)); z-index: 20; background: color-mix(in srgb, var(--bg) 82%, transparent); -webkit-backdrop-filter: blur(18px) saturate(160%); backdrop-filter: blur(18px) saturate(160%); }
354
+ .feed { flex: 1 0 auto; overflow: visible; overscroll-behavior: auto; }
355
+ .composer { position: sticky; bottom: var(--kb, 0px); z-index: 20; background: var(--bg); }
356
+ body.kb .composer { padding-bottom: 10px; }
357
+ .tool pre, .approval pre, .ev-logs pre, .proj-list { max-height: none; overflow-y: visible; }
357
358
  .composer textarea, input[type="text"], input[type="password"], input[type="url"], input[type="search"], select, textarea.field { font-size: 16px; }
358
359
  .chat-head { display: none; }
359
360
  .mode-bar { padding: 10px 16px; }
@@ -569,3 +570,63 @@ dialog.sheet.wiz.compact .sheet-in, .compact .wiz-body { flex: 0 1 auto; }
569
570
  .new-fab { position: fixed; right: 16px; bottom: calc(72px + var(--safe-b)); z-index: 25; height: 48px; padding: 0 18px 0 14px; border-radius: 16px; box-shadow: 0 14px 30px -14px color-mix(in srgb, var(--fg) 70%, transparent); }
570
571
  body:not(.no-tabs):not(.kb) .main:has(.new-fab) { padding-bottom: calc(130px + var(--safe-b)); }
571
572
  }
573
+
574
+ .skel { display: block; height: 14px; border-radius: 8px; background: linear-gradient(90deg, var(--border) 25%, color-mix(in srgb, var(--border) 45%, var(--white)) 50%, var(--border) 75%) 0 0 / 200% 100%; animation: skel 1.3s linear infinite; }
575
+ .skel.tall { height: 22px; }
576
+ .skel.r { margin-left: auto; }
577
+ .skel-card { display: grid; gap: 12px; padding: 18px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--white); }
578
+ .skel-feed { display: grid; gap: 14px; padding: 8px 16px; max-width: 760px; margin: 0 auto; width: 100%; box-sizing: border-box; }
579
+ @keyframes skel { to { background-position: -200% 0; } }
580
+ .chip.fav { border-color: color-mix(in srgb, var(--lemon) 70%, var(--border)); }
581
+ .chip[data-fav][aria-pressed="true"] { background: var(--lemon); border-color: var(--lemon); }
582
+ .manage-bar { position: sticky; top: 0; z-index: 5; display: grid; gap: 10px; margin: 0 0 12px; padding: 12px; border: 1px solid var(--border); border-radius: 14px; background: var(--bg); }
583
+ .manage-bar .row { flex-wrap: wrap; }
584
+ .session-card.pick-mode { cursor: pointer; position: relative; padding-left: 46px; user-select: none; }
585
+ .session-card.pick-mode::before { content: ""; position: absolute; left: 14px; top: 16px; width: 20px; height: 20px; border-radius: 6px; border: 1.5px solid var(--border); background: var(--white); }
586
+ .session-card.picked { border-color: var(--fg); }
587
+ .session-card.picked::before { background: var(--fg); border-color: var(--fg); }
588
+ .session-card.picked::after { content: ""; position: absolute; left: 21px; top: 20px; width: 6px; height: 11px; border: solid var(--bg); border-width: 0 2px 2px 0; transform: rotate(45deg); }
589
+
590
+ .mode-bar .chip { height: 30px; gap: 7px; padding: 0 12px 0 10px; }
591
+ .mode-bar .chip > .ico, .mode-bar .chip > .agent-tag .ico { width: 15px; height: 15px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
592
+ .mode-bar .chip > span:not(.agent-tag) { line-height: 1; }
593
+ .mode-bar .chip .abot { --s: 18px; display: inline-grid; place-items: center; }
594
+ .mode-bar .chip .agent-tag { gap: 7px; line-height: 1; }
595
+
596
+ /* lliscar entre pàgines: es mou només el contingut; barres i botons fixos no es toquen */
597
+ @media (max-width: 820px) {
598
+ .main { overflow-x: clip; }
599
+ #view.swiping .page, #view.swipe-anim .page { position: relative; left: var(--sx, 0); opacity: var(--so, 1); will-change: left, opacity; }
600
+ #view.swipe-anim .page { transition: left .18s var(--ease), opacity .18s ease-out; }
601
+ }
602
+
603
+ /* ---------- Motiu i captura a les aprovacions ---------- */
604
+ .approval .why { display: grid; gap: 2px; padding: 10px 12px; border-radius: 8px; background: color-mix(in srgb, var(--lemon) 22%, var(--white)); }
605
+ .approval .why p { font-size: 14px; white-space: pre-wrap; overflow-wrap: anywhere; }
606
+ .approval .shot, .screen-img { width: 100%; border-radius: 10px; border: 1px solid var(--border); background: var(--bg-2); display: block; }
607
+
608
+ /* ---------- Activitat ---------- */
609
+ .act-filter { margin-bottom: 16px; overflow-x: auto; scrollbar-width: none; }
610
+ .act-filter button { padding: 0 12px; }
611
+ .act { display: grid; }
612
+ .act-day { font-size: 12px; font-weight: 600; color: var(--fg-muted); margin: 18px 0 6px; text-transform: capitalize; }
613
+ .act-day:first-child { margin-top: 0; }
614
+ .act-row { display: flex; gap: 12px; padding: 10px 0; border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent); }
615
+ .act-t { flex: none; width: 44px; font-size: 12px; color: var(--fg-muted); padding-top: 2px; }
616
+ .act-row .row { gap: 8px; }
617
+ .act-what { overflow-wrap: anywhere; color: var(--fg); margin-top: 2px; }
618
+ .act-row .verdict { font-size: 12px; }
619
+
620
+ /* ---------- Feines ---------- */
621
+ .job .title { font-size: 16px; font-weight: 500; letter-spacing: -.02em; margin-top: 8px; }
622
+ .job .meta { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; font-size: 12px; color: var(--fg-muted); }
623
+ .chip.ok { color: var(--moss); border-color: color-mix(in srgb, var(--moss) 40%, transparent); }
624
+ .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
625
+ .check { gap: 10px; font-size: 14px; }
626
+ .check input { width: 18px; height: 18px; accent-color: var(--fg); }
627
+ @media (max-width: 520px) { .grid-2 { grid-template-columns: 1fr; } }
628
+
629
+ /* ---------- Aturada d'emergència (sempre a la vista amb el control actiu) ---------- */
630
+ .panic { position: fixed; z-index: 35; right: 16px; top: calc(66px + var(--safe-t)); display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 12px 0 9px; border: 0; border-radius: 999px; background: var(--red); color: #fff; font: 600 13px var(--font); box-shadow: 0 8px 20px -10px color-mix(in srgb, var(--red) 80%, transparent); }
631
+ .panic .ico { width: 17px; height: 17px; }
632
+ @media (min-width: 821px) { .panic { top: 16px; right: 20px; } }
package/web/board.css CHANGED
@@ -104,9 +104,10 @@
104
104
  .bd-chat .m.nio b { font-weight: 600; }
105
105
  .bd-chat .mav { --s: 24px; --bw: .15; margin-bottom: 4px; }
106
106
  @keyframes bd-msg { from { opacity: 0; transform: translateY(6px); } }
107
- .bd-chat .chips { display: flex; gap: 6px; overflow-x: auto; padding: 0 16px 10px; scrollbar-width: none; }
107
+ .bd-chat .chips { flex: none; display: flex; gap: 6px; overflow-x: auto; overflow-y: hidden; padding: 4px 16px 10px; scrollbar-width: none; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; scroll-snap-type: x proximity; }
108
+ .bd-chat header, .bd-chat .brain, .bd-chat .cin { flex: none; }
108
109
  .bd-chat .chips::-webkit-scrollbar { display: none; }
109
- .bd-chat .chip-s { flex: none; max-width: 260px; box-sizing: border-box; height: 32px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 12px; line-height: 30px; text-align: center; border: 1px solid var(--border); border-radius: 999px; background: var(--white); font-size: 12px; font-weight: 500; cursor: pointer; }
110
+ .bd-chat .chip-s { flex: none; scroll-snap-align: start; max-width: 260px; box-sizing: border-box; height: 32px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 12px; line-height: 30px; text-align: center; border: 1px solid var(--border); border-radius: 999px; background: var(--white); font-size: 12px; font-weight: 500; cursor: pointer; }
110
111
  .bd-chat .chip-s:hover { border-color: var(--fg); }
111
112
  .bd-chat .cin { display: flex; gap: 8px; align-items: flex-end; padding: 12px 14px calc(14px + env(safe-area-inset-bottom)); border-top: 1px solid var(--border); }
112
113
  .bd-chat .cin textarea { flex: 1; min-height: 42px; max-height: 140px; padding: 10px 12px; border: 1px solid var(--border); border-radius: 12px; background: var(--white); resize: none; font: inherit; font-size: 14px; line-height: 1.4; outline: none; }
package/web/board.js CHANGED
@@ -740,21 +740,21 @@ function orgChart(spec) {
740
740
  return pairs.length ? buildGraph(pairs, [], 'v') : [];
741
741
  }
742
742
  const TEMPLATES = {
743
- agent: { say: 'Aquí tens com treballa un agent de Nio, de la dada a l’acció.', edges: [['Banc i ERP', 'Lectura', ''], ['Lectura', 'Regles', ''], ['Regles', 'Supera el llindar?', ''], ['Supera el llindar?', 'Aprovació humana', 'sí'], ['Supera el llindar?', 'Acció automàtica', 'no'], ['Aprovació humana', 'Registre', ''], ['Acció automàtica', 'Registre', '']] },
744
- aprovacio: { say: 'Flux d’aprovació: el que supera el llindar passa per una persona.', edges: [['Inici', 'Tasca detectada', ''], ['Tasca detectada', 'Import > llindar?', ''], ['Import > llindar?', 'Demana aprovació', 'sí'], ['Import > llindar?', 'Executa', 'no'], ['Demana aprovació', 'Aprovada?', ''], ['Aprovada?', 'Executa', 'sí'], ['Aprovada?', 'Arxiva amb motiu', 'no'], ['Executa', 'Fi', ''], ['Arxiva amb motiu', 'Fi', '']] },
745
- conciliacio: { say: 'Conciliació bancària, pas a pas.', edges: [['Extracte bancari', 'Creuar amb factures', ''], ['Factures ERP', 'Creuar amb factures', ''], ['Creuar amb factures', 'Quadra?', ''], ['Quadra?', 'Marcar conciliat', 'sí'], ['Quadra?', 'Excepció per revisar', 'no'], ['Excepció per revisar', 'Aprovació', '']] }
743
+ agent: { say: 'Aquí tens com treballa un agent amb nio, del mòbil a l’ordinador.', edges: [['Mòbil', 'nio', 'instrucció'], ['nio', 'Claude Code', ''], ['Claude Code', 'Acció de risc?', ''], ['Acció de risc?', 'Aprovació al mòbil', 'sí'], ['Acció de risc?', 'Fa la feina', 'no'], ['Aprovació al mòbil', 'Fa la feina', 'aprovada'], ['Fa la feina', 'Registre', '']] },
744
+ aprovacio: { say: 'Flux d’aprovació: el que és arriscat passa per tu.', edges: [['Inici', 'L’agent vol actuar', ''], ['L’agent vol actuar', 'Només llegeix?', ''], ['Només llegeix?', 'Executa', 'sí'], ['Només llegeix?', 'Demana permís', 'no'], ['Demana permís', 'Aprovada?', ''], ['Aprovada?', 'Executa', 'sí'], ['Aprovada?', 'Ho deixa estar', 'no'], ['Executa', 'Fi', ''], ['Ho deixa estar', 'Fi', '']] },
745
+ revisio: { say: 'Una feina encadenada: un agent fa, l’altre revisa.', edges: [['Issue nova', 'Claude Code arregla', ''], ['Claude Code arregla', 'Tests', ''], ['Tests', 'Passen?', ''], ['Passen?', 'Codex revisa', 'sí'], ['Passen?', 'Claude Code torna-hi', 'no'], ['Claude Code torna-hi', 'Tests', ''], ['Codex revisa', 'Pull request', '']] }
746
746
  };
747
747
 
748
748
  /* ---------- Xat ---------- */
749
- const SUGG = ['Flux: Comanda -> Validació -> Preparació -> Enviament', 'Mapa mental de Nio: agents, connexions, aprovacions, registre', 'Organigrama: Direcció > Finances, Operacions; Finances > Comptabilitat', 'Plantilla d’aprovació', 'Ajuda'];
749
+ const SUGG = ['Flux: Idea -> Prototip -> Proves -> Publicació', 'Mapa mental de nio: agents, ordinadors, aprovacions, memòria', 'Arbre: Web > Portada, App; App > Sessions, Feines', 'Plantilla d’aprovació', 'Ajuda'];
750
750
  const HELP = `Puc dibuixar i editar per tu. Prova, per exemple:
751
751
  • <b>Flux: A -> B -> C</b> (una fletxa per línia, amb <b>: etiqueta</b> si vols)
752
752
  • <b>Mapa mental de X: a, b, c</b>
753
- • <b>Organigrama: Direcció > Finances, Operacions</b>
754
- • <b>Afegeix un rombe “Quadra?”</b> · <b>Connecta Banc amb ERP</b>
755
- • <b>Pinta Banc de verd</b> · <b>Reanomena Banc a Caixa</b>
756
- • <b>Esborra ERP</b> · <b>Ordena-ho</b> · <b>Centra</b> · <b>Desfés</b>
757
- • <b>Plantilla agent</b>, <b>Plantilla d’aprovació</b>, <b>Plantilla conciliació</b>
753
+ • <b>Organigrama: Web > Portada, App</b>
754
+ • <b>Afegeix un rombe “Passen?”</b> · <b>Connecta Tests amb Deploy</b>
755
+ • <b>Pinta Tests de verd</b> · <b>Reanomena Tests a Proves</b>
756
+ • <b>Esborra Deploy</b> · <b>Ordena-ho</b> · <b>Centra</b> · <b>Desfés</b>
757
+ • <b>Plantilla agent</b>, <b>Plantilla d’aprovació</b>, <b>Plantilla revisió</b>
758
758
  • <b>Exporta PNG</b>, <b>SVG</b> o <b>JSON</b>`;
759
759
  function say(html, me) {
760
760
  B.chat.push({ me: !!me, html, t: Date.now() }); if (B.chat.length > 200) B.chat.splice(0, B.chat.length - 200);
@@ -792,7 +792,7 @@ function ruleAct(text) {
792
792
  }
793
793
  else if (/^exporta/.test(n)) { const k = /svg/.test(n) ? 'svg' : /json/.test(n) ? 'json' : 'png'; reply = exportAs(k) ? `T’he descarregat la pissarra en ${k.toUpperCase()}.` : 'Encara no hi ha res per exportar.'; }
794
794
  else if (/plantilla|com treballa un agent|flux d.?un agent/.test(n)) {
795
- const k = /aprova/.test(n) ? 'aprovacio' : /concilia/.test(n) ? 'conciliacio' : 'agent';
795
+ const k = /aprova/.test(n) ? 'aprovacio' : /revisi|revisa/.test(n) ? 'revisio' : 'agent';
796
796
  record(); const t = TEMPLATES[k], list = buildGraph(t.edges, [], 'h'); done(t.say, list);
797
797
  }
798
798
  else if (/^(mapa mental|mapa conceptual|mindmap)/.test(n)) {
@@ -952,7 +952,8 @@ function drawBrains() {
952
952
  const sel_ = R.root.querySelector('#bdBrain'); if (!sel_) return;
953
953
  const list = window.NioBrains?.list() || [{ id: 'rules', label: 'Ordres ràpides' }];
954
954
  const cur = list.some((b) => b.id === B.opts.brain) ? B.opts.brain : 'rules'; // la preferència es manté encara que ara no hi sigui
955
- sel_.innerHTML = list.map((b) => `<option value="${esc(b.id)}" ${b.id === cur ? 'selected' : ''}>${esc(b.label)}</option>`).join('');
955
+ const html = list.map((b) => `<option value="${esc(b.id)}" ${b.id === cur ? 'selected' : ''}>${esc(b.label)}</option>`).join('');
956
+ if (sel_.dataset.sig !== html) { sel_.dataset.sig = html; sel_.innerHTML = html; }
956
957
  }
957
958
 
958
959
  /* ---------- Panell d'estil ---------- */
@@ -1034,7 +1035,7 @@ function openChat(open, focus = true, persist = true) {
1034
1035
  R.root.classList.toggle('chat-open', open);
1035
1036
  R.chat.inert = !open;
1036
1037
  const t = R.root.querySelector('[data-bd=chat]'); t.setAttribute('aria-pressed', open);
1037
- if (open && focus) setTimeout(() => R.input.focus(), reduce ? 0 : 280);
1038
+ if (open && focus && !matchMedia('(pointer: coarse)').matches) setTimeout(() => R.input.focus(), reduce ? 0 : 280);
1038
1039
  if (!open && wasOpen && R.chat.contains(document.activeElement)) t.focus({ preventScroll: true });
1039
1040
  }
1040
1041
 
@@ -1045,7 +1046,7 @@ function template() {
1045
1046
  <div class="bd-stage" id="bdStage" data-tool="select">
1046
1047
  <svg class="bd-svg" id="bdSvg" role="img" aria-label="Pissarra"><g id="bdWorld"><g id="bdEls"></g><g id="bdSel" class="bd-selg"></g></g></svg>
1047
1048
  <textarea class="bd-edit" id="bdEdit" hidden spellcheck="false" aria-label="Edita el text"></textarea>
1048
- <div class="bd-empty" id="bdEmpty"><b>Pissarra en blanc</b><span>Dibuixa amb les eines de dalt o demana-ho a en Nio: «Flux: Banc -> ERP -> Aprovació».</span></div>
1049
+ <div class="bd-empty" id="bdEmpty"><b>Pissarra en blanc</b><span>Dibuixa amb les eines de dalt o demana-ho a en Nio: «Flux: Idea -> Prototip -> Proves».</span></div>
1049
1050
 
1050
1051
  <div class="bd-bar bd-tools" id="bdTools" role="toolbar" aria-label="Eines">
1051
1052
  ${TOOLS.map(t => `<button type="button" class="tb" data-tool="${t[0]}" aria-pressed="${t[0] === tool}" title="${t[1]} — ${t[2]}${t[3] ? ' o ' + t[3] : ''}">${svgI(t[4])}${t[3] ? `<small>${t[3]}</small>` : ''}</button>`).join('')}
@@ -1175,7 +1176,7 @@ function mount(root) {
1175
1176
  on(R.input, 'keydown', e => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); send(); } if (e.key === 'Escape') { e.stopPropagation(); openChat(false); } });
1176
1177
  on(R.input, 'input', () => { R.input.style.height = ''; R.input.style.height = Math.min(140, R.input.scrollHeight) + 'px'; });
1177
1178
  on(R.root.querySelector('#bdBrain'), 'change', (e) => { B.opts.brain = e.target.value; save(); say(B.opts.brain === 'rules' ? 'Faig servir les ordres ràpides. Escriu <b>ajuda</b> per veure-les.' : `Ara t’entendré amb <b>${esc(e.target.selectedOptions[0].textContent)}</b>. Demana’m el diagrama com vulguis.`); });
1178
- on(R.root.querySelector('#bdBrain'), 'focus', drawBrains);
1179
+ on(R.root.querySelector('#bdBrain'), 'focus', () => { drawBrains(); window.NioBrains?.refresh?.(); });
1179
1180
  on(R.root.querySelector('#bdImport'), 'change', e => { const f = e.target.files[0]; if (f) importJSON(f); e.target.value = ''; });
1180
1181
  on(document, 'click', e => { if (R && !e.target.closest('.bd-menuwrap')) { const m = R.root.querySelector('#bdMenu'); if (m) m.hidden = true; } });
1181
1182
  on(window, 'resize', () => { applyView(); drawSel(); });
package/web/index.html CHANGED
@@ -3,23 +3,23 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>nio // agents al mòbil</title>
7
- <meta name="description" content="Dona feina als agents d’IA del teu ordinador des del mòbil, segueix-los en directe i aprova cada pas.">
6
+ <title>nio // els teus agents, des del mòbil</title>
7
+ <meta name="description" content="Claude Code i Codex als teus ordinadors, controlats des del mòbil. Personal, gratuït i amb les teves claus.">
8
8
  <link rel="canonical" href="https://www.nio.cat/">
9
9
  <meta name="theme-color" content="#f0ede7">
10
10
  <meta property="og:type" content="website">
11
11
  <meta property="og:site_name" content="nio">
12
12
  <meta property="og:locale" content="ca_ES">
13
13
  <meta property="og:url" content="https://www.nio.cat/">
14
- <meta property="og:title" content="nio // agents al mòbil">
15
- <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.">
14
+ <meta property="og:title" content="nio // els teus agents, des del mòbil">
15
+ <meta property="og:description" content="Claude Code i Codex als teus ordinadors, controlats des del mòbil. Personal, gratuït i amb les teves claus.">
16
16
  <meta property="og:image" content="https://www.nio.cat/og.png">
17
17
  <meta property="og:image:width" content="1200">
18
18
  <meta property="og:image:height" content="630">
19
19
  <meta property="og:image:alt" content="nio">
20
20
  <meta name="twitter:card" content="summary_large_image">
21
- <meta name="twitter:title" content="nio // agents al mòbil">
22
- <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.">
21
+ <meta name="twitter:title" content="nio // els teus agents, des del mòbil">
22
+ <meta name="twitter:description" content="Claude Code i Codex als teus ordinadors, controlats des del mòbil. Personal, gratuït i amb les teves claus.">
23
23
  <meta name="twitter:image" content="https://www.nio.cat/og.png">
24
24
  <meta name="format-detection" content="telephone=no">
25
25
  <link rel="icon" href="/favicon.svg" type="image/svg+xml">
@@ -64,6 +64,7 @@ section p { text-wrap: pretty; }
64
64
  section p + p, section p + ul, section ul + p, section p + .cmd, section .cmd + p { margin-top: 1em; }
65
65
  main a { text-decoration-thickness: 1px; text-underline-offset: 4px; text-decoration-color: var(--accent); }
66
66
  main a:hover { color: var(--accent); }
67
+ main ul.list { padding-left: 1.2em; display: grid; gap: .5em; }
67
68
  main code { font: 500 .88em ui-monospace, "SF Mono", Menlo, monospace; padding: 2px 7px; border-radius: 7px; background: #e6e1d8; }
68
69
  main .cmd { display: flex; align-items: center; gap: 4px; padding: 6px 6px 6px 18px; border-radius: 12px; background: #e6e1d8; }
69
70
  main .cmd code { flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; padding: 8px 0; background: none; border-radius: 0; font-size: 16px; scrollbar-width: none; }
@@ -102,26 +103,43 @@ main strong { font-weight: 600; }
102
103
  </header>
103
104
 
104
105
  <main class="page">
105
- <h1>Els agents d’IA del teu ordinador, al mòbil.</h1>
106
- <p class="lead">Dona feina a Claude Code, Codex, Grok, Antigravity o Ollama des del mòbil. Treballen al teu ordinador, amb els teus comptes. Tu els segueixes en directe i aproves el que facin. És gratuït.</p>
106
+ <h1>Els teus agents, a les teves màquines, des del mòbil.</h1>
107
+ <p class="lead">nio és un comandament per a Claude Code i Codex. Treballen als teus ordinadors (el portàtil, el mini PC de casa, un servidor), amb els teus comptes i les teves claus. Tu els dones feina des del mòbil, els segueixes en directe i aproves el que és arriscat. És personal i gratuït.</p>
107
108
 
108
109
  <section aria-labelledby="com">
109
110
  <h2 id="com">Com funciona</h2>
110
- <p><strong>1. Al teu ordinador.</strong> Obre el terminal a la carpeta dels teus projectes i executa:</p>
111
- <div class="cmd"><code id="cmd">npx nio-cat</code><button class="copy" type="button" data-copy="npx nio-cat" aria-label="Copia l’ordre"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3"/></svg><svg class="ok" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></button></div>
112
- <p>S’obre nio.cat ja connectat. Només cal <a href="https://nodejs.org">Node.js</a>.</p>
113
- <p><strong>2. Amb un servidor (opcional).</strong> A nio.cat, <em>Ordinadors → Afegeix ordinador → Servidor (SSH)</em>, escrius <code>usuari@servidor</code> i nio hi engega Claude Code i la resta. No cal obrir ports.</p>
114
- <p><strong>3. Al mòbil.</strong> A l’ordinador, prem <em>Continua al mòbil</em>. Al mòbil, obre <a href="/app">nio.cat/app</a>, escaneja el QR o enganxa el codi, i ja ho controles. Des de qualsevol xarxa amb <code>cloudflared</code>; sense ell, per la mateixa wifi. Cap dada no passa per servidors nostres.</p>
111
+ <p><strong>1. A cada ordinador</strong> (Linux o Mac), una sola ordre:</p>
112
+ <div class="cmd"><code id="cmd">curl -fsSL https://www.nio.cat/install.sh | sh</code><button class="copy" type="button" data-copy="curl -fsSL https://www.nio.cat/install.sh | sh" aria-label="Copia l’ordre"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3"/></svg><svg class="ok" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></button></div>
113
+ <p>Instal·la nio, el deixa engegat i et mostra un codi QR. Si només el vols provar: <code>npx nio-cat</code>.</p>
114
+ <p><strong>2. Al mòbil</strong>, obre <a href="/app">nio.cat/app</a> i escaneja el QR. El codi serveix un sol cop i el mòbil en rep una clau pròpia, que pots revocar quan vulguis. Afegeix-lo a la pantalla d’inici i tindràs avisos.</p>
115
+ <p><strong>3. Dona-li feina.</strong> Tria l’ordinador, l’agent i la carpeta, i explica què vols. Si l’ordinador es reinicia, el mòbil el torna a trobar sol.</p>
115
116
  </section>
116
117
 
117
- <section aria-labelledby="limits">
118
- <h2 id="limits">Tu decideixes</h2>
119
- <p>Cada sessió té el seu nivell de confiança: <strong>només llegir</strong>, <strong>demana permís</strong> abans d’editar o executar, o <strong>via lliure</strong>. Quan l’agent et necessita, t’arriba un avís i ho resols amb un toc.</p>
118
+ <section aria-labelledby="que">
119
+ <h2 id="que">Què hi pots fer</h2>
120
+ <ul class="list">
121
+ <li><strong>Sessions</strong> amb Claude Code i Codex, en directe, a qualsevol dels teus ordinadors.</li>
122
+ <li><strong>Aprovacions</strong>: abans d’editar, executar o clicar res, te’n demana permís amb el motiu. Tot queda registrat.</li>
123
+ <li><strong>Feines</strong> que es fan soles: ara, més tard o cada dia, i encadenades («quan Claude Code acabi, que Codex ho revisi»).</li>
124
+ <li><strong>Memòria compartida</strong>: el que recorda un agent, ho sap l’altre. Fitxers markdown al teu ordinador.</li>
125
+ <li><strong>Telegram i veu</strong>: parla-hi amb el teu bot o amb notes de veu, i aprova amb un botó.</li>
126
+ <li><strong>Navegador i escriptori</strong>: els agents poden mirar webs i apps; per actuar, sempre et demanen permís amb una captura.</li>
127
+ </ul>
128
+ </section>
129
+
130
+ <section aria-labelledby="teu">
131
+ <h2 id="teu">Tot és teu</h2>
132
+ <p>Fas servir els teus agents i les teves claus (Anthropic, OpenAI, ElevenLabs, el bot de Telegram). Les converses, la memòria i les claus es queden als teus ordinadors. nio.cat només serveix l’app i guarda, signada, l’adreça on trobar cada ordinador. Més detalls a <a href="/privadesa">privadesa</a>.</p>
133
+ </section>
134
+
135
+ <section aria-labelledby="prova">
136
+ <h2 id="prova">Prova-ho</h2>
137
+ <p>Obre <a href="/app">el panell</a>. Si encara no tens cap ordinador connectat, t’ensenya com fer-ho pas a pas.</p>
120
138
  </section>
121
139
  </main>
122
140
 
123
141
  <footer class="footer">
124
- <div class="page">© 2026 nio // un projecte de <a href="https://f.cat">f.cat</a></div>
142
+ <div class="page">© 2026 nio // un projecte de <a href="https://f.cat">f.cat</a> · <a href="mailto:hola@nio.cat">hola@nio.cat</a> · <a href="/privadesa">Privadesa</a></div>
125
143
  </footer>
126
144
  <script src="/bot.js" defer></script>
127
145
  <script>
@@ -0,0 +1,148 @@
1
+ <!doctype html>
2
+ <html lang="ca">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>nio // privadesa</title>
7
+ <meta name="description" content="Què passa per nio.cat i què no hi passa mai.">
8
+ <link rel="canonical" href="https://www.nio.cat/privadesa">
9
+ <meta name="theme-color" content="#f0ede7">
10
+ <meta property="og:type" content="website">
11
+ <meta property="og:site_name" content="nio">
12
+ <meta property="og:locale" content="ca_ES">
13
+ <meta property="og:url" content="https://www.nio.cat/privadesa">
14
+ <meta property="og:title" content="nio // privadesa">
15
+ <meta property="og:description" content="Què passa per nio.cat i què no hi passa mai.">
16
+ <meta property="og:image" content="https://www.nio.cat/og.png">
17
+ <meta property="og:image:width" content="1200">
18
+ <meta property="og:image:height" content="630">
19
+ <meta property="og:image:alt" content="nio">
20
+ <meta name="twitter:card" content="summary_large_image">
21
+ <meta name="twitter:title" content="nio // privadesa">
22
+ <meta name="twitter:description" content="Què passa per nio.cat i què no hi passa mai.">
23
+ <meta name="twitter:image" content="https://www.nio.cat/og.png">
24
+ <meta name="format-detection" content="telephone=no">
25
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml">
26
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png">
27
+ <link rel="manifest" href="/manifest.webmanifest">
28
+ <link rel="preconnect" href="https://fonts.googleapis.com">
29
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
30
+ <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600&display=swap" rel="stylesheet">
31
+ <link rel="stylesheet" href="/bot.css">
32
+ <style>
33
+ :root {
34
+ --bg: #f0ede7;
35
+ --fg: #272224;
36
+ --fg-muted: #69615c;
37
+ --accent: #f46a3b;
38
+ --border: #cec7bd;
39
+ --lemon: #e7e66a;
40
+ --red: #e2382b;
41
+ --font: "Manrope", system-ui, sans-serif;
42
+ }
43
+ * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
44
+ html { -webkit-text-size-adjust: 100%; }
45
+ body { background: var(--bg); color: var(--fg); font: 400 19px/1.6 var(--font); letter-spacing: -.02em; -webkit-font-smoothing: antialiased; }
46
+ a { color: inherit; }
47
+ ::selection { background: var(--lemon); }
48
+ :focus-visible { outline: 2px solid var(--fg); outline-offset: 3px; border-radius: 2px; }
49
+
50
+ .page { width: min(640px, 100% - 48px); margin: 0 auto; }
51
+ .head { padding: 56px 0 0; display: flex; justify-content: space-between; align-items: baseline; }
52
+ .head .enter { display: inline-flex; align-items: center; gap: 8px; height: 34px; padding: 0 14px 0 12px; border: 1px solid var(--border); border-radius: 999px; font-size: 14px; font-weight: 500; color: var(--fg); text-decoration: none; transition: border-color .2s, background-color .2s; }
53
+ .head .enter svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
54
+ .head .enter:hover { border-color: var(--fg); background: color-mix(in srgb, var(--fg) 4%, transparent); }
55
+ .mark { font-size: 28px; font-weight: 600; letter-spacing: -.07em; line-height: 1; text-decoration: none; }
56
+
57
+ main { padding: 96px 0 96px; }
58
+ h1 { font-size: clamp(36px, 6vw, 52px); font-weight: 500; line-height: 1.06; letter-spacing: -.055em; text-wrap: balance; }
59
+ .lead { margin-top: 28px; font-size: 21px; line-height: 1.5; color: var(--fg-muted); text-wrap: pretty; }
60
+
61
+ section { margin-top: 56px; }
62
+ h2 { font-size: clamp(26px, 3.4vw, 32px); font-weight: 500; line-height: 1.15; letter-spacing: -.045em; margin-bottom: 18px; }
63
+ section p { text-wrap: pretty; }
64
+ section p + p, section p + ul, section ul + p, section p + .cmd, section .cmd + p { margin-top: 1em; }
65
+ main a { text-decoration-thickness: 1px; text-underline-offset: 4px; text-decoration-color: var(--accent); }
66
+ main a:hover { color: var(--accent); }
67
+ main ul.list { padding-left: 1.2em; display: grid; gap: .5em; }
68
+ main code { font: 500 .88em ui-monospace, "SF Mono", Menlo, monospace; padding: 2px 7px; border-radius: 7px; background: #e6e1d8; }
69
+ main .cmd { display: flex; align-items: center; gap: 4px; padding: 6px 6px 6px 18px; border-radius: 12px; background: #e6e1d8; }
70
+ main .cmd code { flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; padding: 8px 0; background: none; border-radius: 0; font-size: 16px; scrollbar-width: none; }
71
+ main .cmd code::-webkit-scrollbar { display: none; }
72
+ .copy { flex: none; display: inline-grid; place-items: center; width: 38px; height: 38px; padding: 0; border: 0; border-radius: 9px; background: none; color: var(--fg-muted); cursor: pointer; transition: color .2s, background-color .2s, transform .12s; }
73
+ .copy:hover { color: var(--fg); }
74
+ .copy:active { transform: scale(.97); }
75
+ .copy svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
76
+ .copy .ok { display: none; }
77
+ .copy.done .ok { display: block; color: #5b6551; animation: copy-ok .2s cubic-bezier(.22,1,.36,1); }
78
+ @keyframes copy-ok { from { opacity: 0; transform: scale(.6); } }
79
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none; transition: none; } }
80
+ .copy.done .ic { display: none; }
81
+ main strong { font-weight: 600; }
82
+
83
+ .footer .page { padding: 40px 0 56px; text-align: center; font-size: 13px; color: var(--fg-muted); }
84
+ .footer a { color: var(--fg); text-decoration: underline; text-decoration-color: var(--red); text-decoration-thickness: 1px; text-underline-offset: 3px; }
85
+ .footer a:hover { color: var(--red); }
86
+
87
+ @media (max-width: 620px) {
88
+ body { font-size: 17px; }
89
+ .page { width: calc(100% - 40px); }
90
+ .head { padding-top: 32px; }
91
+ main { padding: 80px 0; }
92
+ .lead { font-size: 19px; }
93
+ section { margin-top: 64px; }
94
+ main .cmd { padding-left: 14px; }
95
+ main .cmd code { font-size: 14px; }
96
+ }
97
+ </style>
98
+ </head>
99
+ <body>
100
+ <header class="page head">
101
+ <a class="mark word" href="/" aria-label="Nio, inici">ni<span class="bot" data-mood="idle" aria-hidden="true"><span class="eyes"><i></i><i></i></span></span></a>
102
+ <a class="enter" href="/app"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4"/><path d="M9 16l4-4-4-4"/><path d="M13 12H4"/></svg>Entra al panell</a>
103
+ </header>
104
+
105
+ <main class="page">
106
+ <h1>Privadesa</h1>
107
+ <p class="lead">nio està fet perquè la teva feina no surti dels teus ordinadors. Aquí tens, sense lletra petita, què passa per nio.cat i què no.</p>
108
+
109
+ <section aria-labelledby="passa">
110
+ <h2 id="passa">Què passa per nio.cat</h2>
111
+ <ul class="list">
112
+ <li><strong>L’app.</strong> nio.cat serveix els fitxers de l’app web (com qualsevol web). No té comptes ni inicis de sessió.</li>
113
+ <li><strong>L’adreça de cada ordinador.</strong> Perquè el mòbil el trobi si l’adreça del túnel canvia, cada ordinador hi desa la seva adreça actual, signada amb una clau que només té ell. Només és «aquest ordinador → aquesta adreça». Caduca als 30 dies si no es renova, i es desactiva amb <code>nio directory off</code>.</li>
114
+ <li><strong>Res més.</strong> L’aparellament és directe entre el mòbil i l’ordinador: el codi va dins de l’enllaç, en una part que el navegador no envia a cap servidor.</li>
115
+ </ul>
116
+ </section>
117
+
118
+ <section aria-labelledby="no">
119
+ <h2 id="no">Què no hi passa mai</h2>
120
+ <ul class="list">
121
+ <li>Les teves converses amb els agents, els fitxers, la memòria i el registre d’accions: es queden a cada ordinador (<code>~/.nio</code>).</li>
122
+ <li>Les claus d’accés dels teus dispositius ni les claus d’API (Anthropic, OpenAI, ElevenLabs, Telegram): viuen als teus ordinadors.</li>
123
+ <li>L’àudio de la veu: va del mòbil al teu ordinador i d’allà al proveïdor que hagis triat.</li>
124
+ </ul>
125
+ </section>
126
+
127
+ <section aria-labelledby="altres">
128
+ <h2 id="altres">Altres serveis que hi intervenen</h2>
129
+ <ul class="list">
130
+ <li><strong>El túnel</strong> (Cloudflare, si el fas servir) porta el trànsit xifrat entre el mòbil i l’ordinador.</li>
131
+ <li><strong>Els avisos</strong> passen pel servei de notificacions del mòbil (Apple o Google), xifrats de punta a punta: ells no en veuen el text.</li>
132
+ <li><strong>Els proveïdors que tu triïs</strong> (Anthropic, OpenAI, ElevenLabs, Telegram) reben el que els envien els teus agents, amb les teves claus i segons les seves condicions.</li>
133
+ </ul>
134
+ </section>
135
+
136
+ <section aria-labelledby="control">
137
+ <h2 id="control">Tu tens el control</h2>
138
+ <p>Pots veure i revocar els dispositius amb accés des de l’app (Ordinadors), o tots alhora amb <code>nio reset-token</code>. Per esborrar-ho tot d’un ordinador, esborra la carpeta <code>~/.nio</code>.</p>
139
+ <p>Dubtes: <a href="mailto:hola@nio.cat">hola@nio.cat</a>.</p>
140
+ </section>
141
+ </main>
142
+
143
+ <footer class="footer">
144
+ <div class="page">© 2026 nio // un projecte de <a href="https://f.cat">f.cat</a> · <a href="mailto:hola@nio.cat">hola@nio.cat</a> · <a href="/privadesa">Privadesa</a></div>
145
+ </footer>
146
+ <script src="/bot.js" defer></script>
147
+ </body>
148
+ </html>
package/web/sw.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // nio · service worker: obre l'app encara que no hi hagi connexió i mostra els avisos.
2
- const SHELL = 'nio-shell-v15';
2
+ const SHELL = 'nio-shell-v20';
3
3
  const FILES = ['/app', '/app/style.css', '/app/app.js', '/bot.css', '/bot.js', '/board.css', '/board.js', '/favicon.svg', '/manifest.webmanifest'];
4
4
 
5
5
  self.addEventListener('install', (e) => {
@@ -37,16 +37,20 @@ self.addEventListener('message', (e) => {
37
37
  }
38
38
  });
39
39
 
40
- // Els avisos arriben buits: el detall es demana directament a l'ordinador.
40
+ // Normalment l'avís arriba xifrat de punta a punta amb el text dins (RFC 8291).
41
+ // Si arriba buit (nio antic), en demanem el detall a l'ordinador.
41
42
  self.addEventListener('push', (e) => {
42
43
  e.waitUntil((async () => {
43
- let title = 'nio', body = 'Un agent et necessita', session = null;
44
- try {
45
- const cfg = await (await (await caches.open('nio-cfg')).match('/cfg')).json();
46
- const [n] = await (await fetch(`${cfg.url}/api/notices`, { headers: { Authorization: `Bearer ${cfg.token}` } })).json();
47
- if (n) { title = n.title; body = n.body; session = n.session; }
48
- } catch { /* sense detall: avís genèric */ }
49
- await self.registration.showNotification(title, { body, icon: '/icon-192.png', badge: '/icon-192.png', tag: session || 'nio', renotify: true, data: { session } });
44
+ let n = null;
45
+ try { n = e.data ? e.data.json() : null; } catch { n = null; }
46
+ if (!n) {
47
+ try {
48
+ const cfg = await (await (await caches.open('nio-cfg')).match('/cfg')).json();
49
+ [n] = await (await fetch(`${cfg.url}/api/notices`, { headers: { Authorization: `Bearer ${cfg.token}` } })).json();
50
+ } catch { /* sense detall: avís genèric */ }
51
+ }
52
+ const title = n?.title || 'nio', body = n?.body || 'Un agent et necessita', session = n?.session || null;
53
+ await self.registration.showNotification(title, { body, icon: '/icon-192.png', badge: '/badge.png', tag: session || n?.job || 'nio', renotify: true, data: { session, job: n?.job || null } });
50
54
  })());
51
55
  });
52
56