nio-cat 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,6 +65,7 @@
65
65
  <div class="tour" id="tour" hidden>
66
66
  <div class="tour-spot" id="tourSpot"></div>
67
67
  <div class="tour-card" id="tourCard" role="dialog" aria-modal="false" aria-labelledby="tourT"></div>
68
+ <span class="tour-tap" id="tourTap" aria-hidden="true"></span>
68
69
  </div>
69
70
  <nav class="tabbar" id="tabbar" aria-label="Navegació"></nav>
70
71
  <div class="toasts" id="toasts" popover="manual" aria-live="polite"></div>
package/web/app/style.css CHANGED
@@ -66,7 +66,7 @@ button { cursor: pointer; }
66
66
  kbd { font: 600 11px var(--font); padding: 1px 6px; border: 1px solid var(--border); border-radius: 6px; background: var(--white); color: var(--fg-muted); }
67
67
  .main { min-width: 0; display: flex; flex-direction: column; }
68
68
  .page { padding: 32px 40px 96px; max-width: 1080px; width: 100%; }
69
- .top { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 24px; }
69
+ .top { display: flex; flex-direction: column; align-items: flex-start; gap: 14px; margin-bottom: 24px; }
70
70
  .top h1 { font-size: 30px; font-weight: 500; letter-spacing: -.05em; line-height: 1.1; }
71
71
  .top p { margin-top: 4px; color: var(--fg-muted); }
72
72
  .mbar, .scrim { display: none; }
@@ -186,7 +186,7 @@ button.chip:hover { border-color: var(--fg); }
186
186
  /* ---------- Sessions ---------- */
187
187
  .session-card .title { font-size: 16px; font-weight: 500; letter-spacing: -.02em; margin-top: 8px; }
188
188
  .session-card .last.err { color: var(--red); }
189
- .session-card .last { color: var(--fg-muted); font-size: 14px; margin-top: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
189
+ .session-card .last { color: var(--fg-muted); font-size: 14px; margin-top: 4px; line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
190
190
  .session-card .meta { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; font-size: 12px; color: var(--fg-muted); }
191
191
  .fab { position: fixed; right: 24px; bottom: calc(24px + var(--safe-b)); z-index: 15; display: inline-flex; align-items: center; gap: 8px; height: 50px; padding: 0 20px 0 16px; border: 0; border-radius: 16px; background: var(--fg); color: var(--bg); font-weight: 600; box-shadow: 0 14px 30px -14px color-mix(in srgb, var(--fg) 70%, transparent); transition: transform .15s; }
192
192
  .fab:active { transform: scale(.97); }
@@ -304,6 +304,23 @@ dialog.sheet[open] { animation: pop .22s var(--ease); }
304
304
  .tour-card h3 { font-size: 19px; font-weight: 500; letter-spacing: -.035em; line-height: 1.2; }
305
305
  .tour-card.center h3 { font-size: 26px; letter-spacing: -.045em; }
306
306
  .tour-card p { font-size: 14px; color: var(--fg-muted); }
307
+ .tour-card { transition: left .35s var(--ease), top .35s var(--ease), opacity .25s; }
308
+ .tour-card.swap { opacity: .0; pointer-events: none; }
309
+ .tour-spot.fade { box-shadow: 0 0 0 9999px color-mix(in srgb, var(--fg) 42%, transparent); }
310
+ .tour-dots { display: flex; gap: 5px; width: 100%; }
311
+ .tour-card.center .tour-dots { display: none; }
312
+ .tour-dots i { flex: 1; height: 3px; border-radius: 2px; background: color-mix(in srgb, var(--fg) 14%, transparent); transition: background .3s; }
313
+ .tour-dots i.done { background: color-mix(in srgb, var(--fg) 45%, transparent); }
314
+ .tour-dots i.on { background: var(--fg); }
315
+ .tour-tap { position: fixed; left: 0; top: 0; width: 28px; height: 28px; margin: -14px 0 0 -14px; border-radius: 50%; background: rgba(255,255,255,.95); box-shadow: 0 0 0 2px var(--fg), 0 6px 16px rgba(0,0,0,.3); opacity: 0; pointer-events: none; z-index: 2; transition: translate .6s var(--ease), opacity .25s, scale .15s; }
316
+ .tour-tap.on { opacity: 1; }
317
+ .tour-tap.press { scale: .76; }
318
+ .tour-tap.nomove { transition: none; }
319
+ .tour-tap::after { content: ''; position: absolute; inset: -2px; border-radius: 50%; border: 2px solid var(--fg); opacity: 0; }
320
+ .tour-tap.press::after { animation: tourRipple .5s ease-out; }
321
+ @keyframes tourRipple { from { opacity: .7; transform: scale(1); } to { opacity: 0; transform: scale(2.8); } }
322
+ .tour-pressed { scale: .97; transition: scale .15s; }
323
+ @media (prefers-reduced-motion: reduce) { .tour-tap, .tour-card, .tour-spot { transition: none !important; } .tour-tap.press::after { animation: none; } }
307
324
  .tour-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; padding-top: 4px; }
308
325
  .tour-card.center .tour-foot { justify-content: center; }
309
326
 
@@ -395,7 +412,11 @@ body.board-mode .view-enter { animation: none; }
395
412
  .choice .body { min-width: 0; }
396
413
  @media (max-width: 820px) { .choice-grid { grid-template-columns: 1fr; } }
397
414
  .fs-bar { display: flex; align-items: center; gap: 8px; min-width: 0; }
398
- .fs-path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left; }
415
+ .crumbs { flex: 1; min-width: 0; display: flex; align-items: center; gap: 2px; overflow: hidden; white-space: nowrap; }
416
+ .crumb { background: none; border: 0; padding: 6px 6px; margin: 0; border-radius: 8px; color: var(--muted, #8a8f98); font: inherit; cursor: pointer; flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
417
+ .crumb:hover { background: color-mix(in srgb, currentColor 10%, transparent); color: inherit; }
418
+ .crumb.cur { color: inherit; font-weight: 600; cursor: default; }
419
+ .crumbs .sl { opacity: .4; flex: none; }
399
420
  .fs-list { max-height: 240px; overflow: auto; }
400
421
 
401
422
  /* Assistent «Nova sessió»: drawer fix al telèfon, capçalera i peu fixos, cos amb scroll */
@@ -477,6 +498,14 @@ video.scan { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 14p
477
498
  .help-body { display: grid; gap: 8px; padding: 0 0 14px; font-size: 14px; color: var(--fg-muted); }
478
499
  .help-body ul { padding-left: 18px; display: grid; gap: 4px; }
479
500
  .help-body b { color: var(--fg); font-weight: 600; }
501
+ .help-body .mono { padding: 2px 7px; border-radius: 6px; background: var(--code); color: var(--fg); white-space: nowrap; }
502
+ .help-body .cmdb { margin: 2px 0; padding: 12px 14px; border-radius: 10px; background: var(--code); color: var(--fg); font: 14px/1.4 var(--mono); overflow-x: auto; }
503
+ .help-body .cmdlist { display: grid; gap: 6px; margin: 2px 0; }
504
+ .help-body .cmdlist > div { display: grid; grid-template-columns: minmax(0, 15rem) minmax(0, 1fr); gap: 4px 14px; align-items: baseline; padding: 8px 10px; border-radius: 10px; background: color-mix(in srgb, var(--code) 45%, transparent); }
505
+ .help-body .cmdlist .mono { justify-self: start; white-space: normal; overflow-wrap: anywhere; }
506
+ .help-grp { margin: 22px 0 2px; font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--fg-muted); }
507
+ .help-grp:first-child { margin-top: 0; }
508
+ @media (max-width: 620px) { .help-body .cmdlist > div { grid-template-columns: 1fr; } }
480
509
  .empty .stepper { text-align: left; max-width: 640px; margin: 16px auto 18px; }
481
510
  a.kbd { text-decoration: none; color: inherit; }
482
511
 
@@ -642,7 +671,7 @@ dialog.sheet.wiz.compact .sheet-in, .compact .wiz-body { flex: 0 1 auto; }
642
671
  .section-title + .mem-list { margin-bottom: 8px; }
643
672
 
644
673
  /* Capçalera d'una secció dins d'una pàgina */
645
- .sec-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin: 36px 0 14px; }
674
+ .sec-head { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; margin: 36px 0 14px; }
646
675
  .sec-head .sec { margin: 0; }
647
676
  .sec-head p { margin-top: 4px; max-width: 640px; }
648
677
 
@@ -668,3 +697,18 @@ html.rd-open, html.rd-open body { overflow: hidden; }
668
697
  .rd-keys[hidden] { display: none; }
669
698
  .rd-type { position: fixed; left: 0; top: 0; width: 1px; height: 1px; opacity: 0; border: 0; padding: 0; resize: none; font-size: 16px; pointer-events: none; }
670
699
  @media (max-width: 560px) { .rd-bar { padding: 8px 10px; gap: 8px; } .rd-stat, #rdFull { display: none; } .rd-seg button { padding: 9px 11px; } }
700
+
701
+ .rd-stage img { transform-origin: center; will-change: transform; }
702
+ .rd-tools { position: absolute; left: 50%; bottom: calc(14px + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%); display: flex; align-items: center; gap: 6px; padding: 6px; border-radius: 14px; background: rgba(22, 22, 23, .86); border: 1px solid #36363a; backdrop-filter: blur(8px); z-index: 2; max-width: calc(100% - 20px); }
703
+ .rd-tools .rd-btn { padding: 10px 12px; min-width: 40px; }
704
+ .rd-pct { min-width: 56px !important; font-variant-numeric: tabular-nums; }
705
+ .rd-sep { width: 1px; align-self: stretch; background: #36363a; }
706
+ .rd-msg { bottom: 84px; }
707
+ @media (max-width: 560px) { .rd-tools { gap: 4px; padding: 5px; } .rd-tools .rd-btn { padding: 12px 10px; min-width: 44px; font-size: 14px; } .rd-pct { min-width: 52px !important; } }
708
+
709
+ .session-card { padding: 14px 16px; }
710
+ .session-card .meta { margin-top: 8px; }
711
+ #jobWhen { flex-wrap: wrap; }
712
+ #jobWhen button { flex: 1 0 auto; padding: 0 12px; }
713
+ #jobTimes .chip[aria-pressed="true"] { background: var(--fg); color: var(--bg); border-color: var(--fg); }
714
+ #jobMinsU { flex: 1; }
package/web/board.css CHANGED
@@ -190,3 +190,53 @@
190
190
  .bd-chat header { padding-top: 22px; }
191
191
  .bd-chat .cin textarea { font-size: 16px; }
192
192
  }
193
+
194
+ /* Tàctil: botons còmodes, sense menú de pressió llarga ni selecció */
195
+ .bd-stage, .bd-svg { -webkit-touch-callout: none; }
196
+ @media (pointer: coarse) {
197
+ .bd .ib, .bd .pct { height: 40px; }
198
+ .bd .ib { width: 40px; }
199
+ .bd .pct { min-width: 52px; font-size: 13px; }
200
+ .bd .tb { width: 44px; height: 44px; }
201
+ .bd .chat-t { height: 44px; }
202
+ .bd-menu { top: 50px; min-width: 250px; }
203
+ .bd-menu button, .bd-menu label { padding: 13px 12px; font-size: 15px; }
204
+ .bd-style .sw-c { width: 34px; height: 34px; }
205
+ .bd-style .seg2 button { height: 36px; }
206
+ .bd-chat .chip-s { height: 38px; line-height: 36px; font-size: 13px; }
207
+ .bd-chat .send { width: 46px; height: 46px; }
208
+ }
209
+ @media (pointer: coarse) and (max-width: 820px) {
210
+ .bd-zoom [data-bd="zin"], .bd-zoom [data-bd="zout"] { display: none; }
211
+ .bd-tools { gap: 0; scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch; }
212
+ .bd .tb { scroll-snap-align: center; }
213
+ }
214
+ @container stage (max-width: 440px) {
215
+ .bd .tb { width: 40px; height: 44px; }
216
+ .bd-zoom { max-width: calc(100% - 128px); }
217
+ }
218
+ @container stage (max-width: 400px) {
219
+ .bd-tools .tb { min-width: 34px; height: 44px; }
220
+ }
221
+
222
+ /* Teclat obert: el xat puja per damunt del teclat */
223
+ @media (max-width: 820px) {
224
+ .bd-chat { bottom: var(--kb, 0px); transition: transform .34s var(--bd-ease), bottom .2s, visibility 0s linear .34s; }
225
+ .chat-open .bd-chat { transition: transform .34s var(--bd-ease), bottom .2s, visibility 0s; }
226
+ .bd-chat.dragging { transition: none !important; }
227
+ }
228
+ @container stage (max-width: 720px) {
229
+ .bd-chat { bottom: var(--kb, 0px); height: min(78%, 600px, calc(100% - var(--kb, 0px) - 8px)); }
230
+ }
231
+
232
+ /* Mòbil en horitzontal: poc d'alçada */
233
+ @media (max-height: 480px) and (max-width: 940px) {
234
+ .bd-chat { height: min(92%, 600px); }
235
+ .bd-tools { bottom: calc(6px + env(safe-area-inset-bottom)); left: auto; right: auto; max-width: calc(100% - 20px); }
236
+ .bd-tools { left: 50%; transform: translateX(-50%); }
237
+ .bd .tb { height: 40px; }
238
+ .style-open .bd-style:not([hidden]) { bottom: calc(58px + env(safe-area-inset-bottom)); max-height: calc(100% - 130px); overflow-y: auto; grid-template-columns: repeat(2, 1fr); }
239
+ .bd-help-in { padding: 14px; }
240
+ .bd-empty { display: none; }
241
+ }
242
+
package/web/board.js CHANGED
@@ -42,6 +42,7 @@ const uid = () => Math.random().toString(36).slice(2, 10);
42
42
  const norm = s => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/·/g, '').toLowerCase().trim();
43
43
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
44
44
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
45
+ const coarse = matchMedia('(pointer: coarse)').matches;
45
46
 
46
47
  /* ---------- Estat ---------- */
47
48
  function fresh() {
@@ -311,13 +312,13 @@ function drawSel() {
311
312
  if (list.length) {
312
313
  const p = 6 / z;
313
314
  if (list.length === 1 && isLinear(list[0])) {
314
- const [a, b] = ends(list[0]), r = 6 / z;
315
+ const [a, b] = ends(list[0]), r = (coarse ? 11 : 6) / z;
315
316
  s += `<circle class="hdl" data-ep="0" cx="${a[0]}" cy="${a[1]}" r="${r}" stroke-width="${1.5 / z}"/><circle class="hdl" data-ep="1" cx="${b[0]}" cy="${b[1]}" r="${r}" stroke-width="${1.5 / z}"/>`;
316
317
  } else {
317
318
  const b = union(list);
318
319
  s += `<rect class="selbox" x="${b.x - p}" y="${b.y - p}" width="${b.w + p * 2}" height="${b.h + p * 2}" stroke-width="${1.2 / z}" stroke-dasharray="${4 / z} ${3 / z}"/>`;
319
320
  if (list.length === 1 && list[0].type !== 'draw') {
320
- const H = 9 / z, bx = b.x - p, by = b.y - p, bw = b.w + p * 2, bh = b.h + p * 2;
321
+ const H = (coarse ? 17 : 9) / z, bx = b.x - p, by = b.y - p, bw = b.w + p * 2, bh = b.h + p * 2;
321
322
  const pos = { nw: [bx, by], n: [bx + bw / 2, by], ne: [bx + bw, by], e: [bx + bw, by + bh / 2], se: [bx + bw, by + bh], s: [bx + bw / 2, by + bh], sw: [bx, by + bh], w: [bx, by + bh / 2] };
322
323
  HANDLES.forEach(k => { if (list[0].type === 'text' && (k === 'n' || k === 's' || k === 'e' || k === 'w')) return; s += `<rect class="hdl" data-h="${k}" x="${pos[k][0] - H / 2}" y="${pos[k][1] - H / 2}" width="${H}" height="${H}" rx="${2 / z}" stroke-width="${1.5 / z}"/>`; });
323
324
  }
@@ -395,7 +396,7 @@ function onDown(e) {
395
396
  if (editing) { endEdit(); if (tool === 'select') return; }
396
397
  R.svg.setPointerCapture(e.pointerId);
397
398
  pointers.set(e.pointerId, [e.clientX, e.clientY]);
398
- if (pointers.size === 2) { const [a, b] = [...pointers.values()]; drag = { mode: 'pinch', d0: Math.hypot(a[0] - b[0], a[1] - b[1]), z0: B.view.z, v0: { ...B.view }, m0: [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2] }; marquee = null; return; }
399
+ if (pointers.size === 2) { abortDrag(); const [a, b] = [...pointers.values()]; drag = { mode: 'pinch', d0: Math.hypot(a[0] - b[0], a[1] - b[1]), z0: B.view.z, v0: { ...B.view }, m0: [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2] }; marquee = null; return; }
399
400
  const [wx, wy] = toWorld(e.clientX, e.clientY);
400
401
  const hitG = e.target.closest('[data-id]'), hit = hitG ? byId(hitG.dataset.id) : null;
401
402
  const touch = e.pointerType === 'touch';
@@ -439,6 +440,13 @@ function onDown(e) {
439
440
  drag = { mode: 'draw', el, last: [wx, wy] };
440
441
  }
441
442
  }
443
+ function abortDrag() {
444
+ const d = drag; if (!d) return;
445
+ if (d.mode === 'create' || d.mode === 'line' || d.mode === 'draw') { B.els = B.els.filter(x => x !== d.el); nodes.get(d.el.id)?.remove(); nodes.delete(d.el.id); hist.pop(); }
446
+ else if (d.mode === 'move' && d.rec) { Object.entries(d.orig).forEach(([id, o]) => { const el = byId(id); if (el) { el.x = o.x; el.y = o.y; } }); hist.pop(); full(); }
447
+ else if (d.mode === 'resize' && d.rec) { Object.assign(d.el, d.o); hist.pop(); full(); }
448
+ marquee = null; R.stage.classList.remove('panning'); drag = null; R.empty.hidden = !!B.els.length; drawSel();
449
+ }
442
450
  function snapshotSel() { const m = {}; sel.forEach(id => { const el = byId(id); if (el) m[id] = { x: el.x, y: el.y }; }); return m; }
443
451
  function onMove(e) {
444
452
  if (!R) return;
@@ -559,8 +567,18 @@ function onWheel(e) {
559
567
  if (!R) return;
560
568
  e.preventDefault();
561
569
  const r = R.svg.getBoundingClientRect();
562
- if (e.ctrlKey || e.metaKey) zoomAt(B.view.z * Math.exp(-e.deltaY * .0028), e.clientX - r.left, e.clientY - r.top);
563
- else { B.view.x -= e.shiftKey && !e.deltaX ? e.deltaY : e.deltaX; B.view.y -= e.shiftKey && !e.deltaX ? 0 : e.deltaY; applyView(); drawSel(); save(); }
570
+ const k = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? r.height : 1, dx = e.deltaX * k, dy = e.deltaY * k;
571
+ if (e.ctrlKey || e.metaKey) zoomAt(B.view.z * Math.exp(-clamp(dy, -40, 40) * .01), e.clientX - r.left, e.clientY - r.top);
572
+ else { B.view.x -= e.shiftKey && !dx ? dy : dx; B.view.y -= e.shiftKey && !dx ? 0 : dy; applyView(); drawSel(); save(); }
573
+ }
574
+ let gz = null;
575
+ function onGesture(e) {
576
+ if (!R) return;
577
+ e.preventDefault();
578
+ const r = R.svg.getBoundingClientRect();
579
+ if (e.type === 'gesturestart') { gz = { z0: B.view.z }; return; }
580
+ if (e.type === 'gestureend') { gz = null; return; }
581
+ if (gz) zoomAt(gz.z0 * e.scale, e.clientX - r.left, e.clientY - r.top);
564
582
  }
565
583
 
566
584
  /* ---------- Accions ---------- */
@@ -1169,6 +1187,7 @@ function mount(root) {
1169
1187
  if (first) { const r = R.svg.getBoundingClientRect(); B.view = { x: r.width / 2, y: r.height / 2, z: 1 }; }
1170
1188
  on(R.svg, 'pointerdown', onDown); on(window, 'pointermove', onMove); on(window, 'pointerup', onUp); on(window, 'pointercancel', onUp);
1171
1189
  on(R.svg, 'wheel', onWheel, { passive: false });
1190
+ ['gesturestart', 'gesturechange', 'gestureend'].forEach(t => on(R.svg, t, onGesture, { passive: false }));
1172
1191
  on(window, 'keydown', onKey); on(window, 'keyup', onKeyUp);
1173
1192
  on(root, 'click', onClick);
1174
1193
  on(R.editor, 'input', placeEditor); on(R.editor, 'blur', () => setTimeout(endEdit, 0));
@@ -1180,6 +1199,11 @@ function mount(root) {
1180
1199
  on(R.root.querySelector('#bdImport'), 'change', e => { const f = e.target.files[0]; if (f) importJSON(f); e.target.value = ''; });
1181
1200
  on(document, 'click', e => { if (R && !e.target.closest('.bd-menuwrap')) { const m = R.root.querySelector('#bdMenu'); if (m) m.hidden = true; } });
1182
1201
  on(window, 'resize', () => { applyView(); drawSel(); });
1202
+ const vv = window.visualViewport;
1203
+ if (vv) {
1204
+ const kb = () => R && R.root.style.setProperty('--kb', Math.max(0, Math.round(innerHeight - vv.height - vv.offsetTop)) + 'px');
1205
+ on(vv, 'resize', kb); on(vv, 'scroll', kb); kb();
1206
+ }
1183
1207
  if (document.fonts) document.fonts.load(`500 24px Caveat`).then(() => { if (R) { B.els.forEach(el => { if (el.type === 'text' && el.text) fitText(el); }); full(); } }).catch(() => {});
1184
1208
  chatSwipe(R.chat);
1185
1209
  full(); drawChat(); drawBrains(); setTool(tool); openChat(B.opts.chat && !matchMedia('(max-width: 820px)').matches, false, false);
package/web/index.html CHANGED
@@ -79,6 +79,15 @@ main .cmd code::-webkit-scrollbar { display: none; }
79
79
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none; transition: none; } }
80
80
  .copy.done .ic { display: none; }
81
81
  main strong { font-weight: 600; }
82
+ .feat { list-style: none; display: grid; gap: 14px; padding: 0; margin-top: 4px; }
83
+ .feat li { padding-left: 18px; position: relative; text-wrap: pretty; }
84
+ .feat li::before { content: ""; position: absolute; left: 0; top: .72em; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
85
+ .cmds { display: grid; gap: 8px; margin: 16px 0; }
86
+ .cmds > div { display: grid; grid-template-columns: minmax(0, 15.5rem) minmax(0, 1fr); gap: 4px 16px; align-items: baseline; padding: 10px 14px; border-radius: 12px; background: #e6e1d8; }
87
+ .cmds dt code { background: #f6f3ee; }
88
+ .cmds dd { font-size: .86em; color: var(--fg-muted); }
89
+ @media (max-width: 620px) { .cmds > div { grid-template-columns: 1fr; } }
90
+
82
91
 
83
92
  .footer .page { padding: 40px 0 56px; text-align: center; font-size: 13px; color: var(--fg-muted); }
84
93
  .footer a { color: var(--fg); text-decoration: underline; text-decoration-color: var(--red); text-decoration-thickness: 1px; text-underline-offset: 3px; }
@@ -117,16 +126,66 @@ main strong { font-weight: 600; }
117
126
 
118
127
  <section aria-labelledby="que">
119
128
  <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>
129
+ <ul class="feat">
130
+ <li><strong>Sessions en directe.</strong> Claude Code i Codex a qualsevol dels teus ordinadors: tria l’agent, la carpeta i el nivell de permisos, i la resposta et va arribant mentre treballa, amb les eines que fa servir.</li>
131
+ <li><strong>Aprovacions.</strong> Abans d’editar, executar o clicar res, te’n demana permís amb el motiu, i ho aproves o ho rebutges amb un toc. Tot queda registrat a Activitat.</li>
132
+ <li><strong>Avisos al mòbil.</strong> Una notificació quan un agent et demana permís o acaba la feina. Arriba buida: el detall el demana el mòbil directament a l’ordinador.</li>
133
+ <li><strong>Feines.</strong> Que es facin soles: ara, més tard o cada dia, i encadenades («quan Claude Code acabi, que Codex ho revisi»).</li>
134
+ <li><strong>Memòria compartida.</strong> El que recorda un agent, ho sap l’altre. Són fitxers markdown al teu ordinador.</li>
135
+ <li><strong>Ordinadors i servidors.</strong> El teu ordinador o un servidor (VPS), tots a la mateixa llista. Els afegeixes escrivint <code>usuari@servidor</code>.</li>
136
+ <li><strong>Pantalla remota.</strong> Mira i controla l’escriptori de l’ordinador des del mòbil, amb zoom amb dos dits i captures. Més avall.</li>
137
+ <li><strong>Navegador i escriptori per als agents.</strong> Poden mirar webs i apps; per actuar, sempre et demanen permís amb una captura.</li>
138
+ <li><strong>Telegram i veu.</strong> Parla amb el teu bot o amb notes de veu, aprova amb un botó, i escolta les respostes (ElevenLabs o OpenAI). Les claus es queden a l’ordinador.</li>
139
+ <li><strong>Git i GitHub.</strong> Clona un repositori, mira els canvis, fes commit i crea un PR des del mòbil, amb el <code>git</code> i el <code>gh</code> de l’ordinador.</li>
140
+ <li><strong>Models d’Ollama i xat local.</strong> Descarrega i fes servir models gratuïts a l’ordinador, o un model petit dins del navegador del mòbil.</li>
141
+ <li><strong>Pissarra.</strong> Un llenç per fer diagrames, i en Nio te’ls dibuixa si li ho expliques. Més avall.</li>
142
+ <li><strong>Com una app.</strong> Afegeix nio a la pantalla d’inici del mòbil; llisca als costats per canviar de pàgina i <code>Ctrl K</code> obre el buscador a l’ordinador.</li>
127
143
  </ul>
128
144
  </section>
129
145
 
146
+ <section aria-labelledby="cmds">
147
+ <h2 id="cmds">Comandes amb «/»</h2>
148
+ <p>Al missatge d’una sessió, escriu <code>/</code> i surten els suggeriments. Les de nio funcionen amb tots els agents; les de l’agent (comandes i skills) se li envien tal qual.</p>
149
+ <dl class="cmds">
150
+ <div><dt><code>/mode</code></dt><dd>Canvia els permisos: <code>l</code> llegir, <code>p</code> preguntar, <code>t</code> tot</dd></div>
151
+ <div><dt><code>/model</code></dt><dd>Canvia de model</dd></div>
152
+ <div><dt><code>/stop</code></dt><dd>Atura l’agent ara mateix</dd></div>
153
+ <div><dt><code>/nova</code></dt><dd>Nova sessió amb el mateix agent i carpeta</dd></div>
154
+ <div><dt><code>/carpeta</code></dt><dd>Canvia la carpeta de la sessió</dd></div>
155
+ <div><dt><code>/git</code></dt><dd>Veure canvis, fer commit i crear un PR</dd></div>
156
+ <div><dt><code>/nom</code></dt><dd>Posa nom a la sessió</dd></div>
157
+ <div><dt><code>/opcions</code></dt><dd>Opcions de la sessió</dd></div>
158
+ <div><dt><code>/ajuda</code></dt><dd>Llista de comandes</dd></div>
159
+ </dl>
160
+ </section>
161
+
162
+ <section aria-labelledby="screen">
163
+ <h2 id="screen">La pantalla remota</h2>
164
+ <p>Obre l’escriptori d’un dels teus ordinadors a pantalla completa, a la mateixa amplada que el mòbil, com un escriptori remot. Al mode <strong>Mirar</strong> només veus què fa l’agent; al mode <strong>Controlar</strong> pots clicar, fer doble clic, clic dret, arrossegar, desplaçar i escriure.</p>
165
+ <dl class="cmds">
166
+ <div><dt><code>− 100% +</code></dt><dd>Allunya, apropa o torna a enquadrar la imatge</dd></div>
167
+ <div><dt><code>1:1</code></dt><dd>Mida real, per llegir text petit</dd></div>
168
+ <div><dt><code>Desa captura</code></dt><dd>Baixa el fotograma actual com a imatge</dd></div>
169
+ <div><dt><code>Pantalla completa</code></dt><dd>A l’ordinador, amaga tota la resta</dd></div>
170
+ </dl>
171
+ <p><strong>Zoom amb dos dits.</strong> Pessiga per apropar i arrossega amb dos dits per moure’t; al mode Mirar, doble toc alterna entre enquadrar i apropar. Al mode Controlar, el botó «2 dits» tria si els dos dits desplacen l’ordinador o fan zoom. A l’ordinador, pessiga al trackpad o fes servir <code>Ctrl</code> i la roda. Cal activar «Apps de l’escriptori» a l’ordinador, i al Mac donar al terminal els permisos de Gravació de pantalla i Accessibilitat.</p>
172
+ </section>
173
+
174
+ <section aria-labelledby="board">
175
+ <h2 id="board">La pissarra</h2>
176
+ <p>Un llenç infinit per fer fluxos, mapes mentals i organigrames, al mòbil i a l’ordinador. Rectangles, el·lipses, rombes, fletxes que s’enganxen a les figures, línies, llapis, text i goma. Desfés i refés, quadrícula, i exportació a PNG, SVG o JSON.</p>
177
+ <p><strong>Zoom amb dos dits.</strong> Al mòbil, pessiga sobre el llenç per apropar o allunyar, i mou-lo amb els dos dits alhora. A l’ordinador, pessiga al trackpad o fes servir <code>Ctrl</code> i la roda del ratolí.</p>
178
+ <p><strong>Demana-ho a en Nio.</strong> Obre el xat de la pissarra i escriu què vols:</p>
179
+ <dl class="cmds">
180
+ <div><dt><code>Flux: A -> B -> C</code></dt><dd>Una fletxa per línia</dd></div>
181
+ <div><dt><code>Mapa mental de X: a, b, c</code></dt><dd>Tema central amb branques</dd></div>
182
+ <div><dt><code>Organigrama: Direcció &gt; Finances</code></dt><dd>Qui depèn de qui</dd></div>
183
+ <div><dt><code>Connecta Banc amb ERP</code></dt><dd>Uneix dues figures</dd></div>
184
+ <div><dt><code>Pinta Banc de verd</code></dt><dd>Canvia el color</dd></div>
185
+ <div><dt><code>Plantilla d’aprovació</code></dt><dd>Un diagrama de partida</dd></div>
186
+ </dl>
187
+ </section>
188
+
130
189
  <section aria-labelledby="teu">
131
190
  <h2 id="teu">Tot és teu</h2>
132
191
  <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>
@@ -139,7 +198,7 @@ main strong { font-weight: 600; }
139
198
  </main>
140
199
 
141
200
  <footer class="footer">
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>
201
+ <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="/novetats">Novetats</a> · <a href="/privadesa">Privadesa</a></div>
143
202
  </footer>
144
203
  <script src="/bot.js" defer></script>
145
204
  <script>
@@ -141,7 +141,7 @@ main strong { font-weight: 600; }
141
141
  </main>
142
142
 
143
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>
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="/novetats">Novetats</a> · <a href="/privadesa">Privadesa</a></div>
145
145
  </footer>
146
146
  <script src="/bot.js" defer></script>
147
147
  </body>
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-v23';
2
+ const SHELL = 'nio-shell-v33';
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) => {