nio-cat 0.1.1 → 0.3.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/bin/nio.js +113 -3
- package/package.json +1 -1
- package/src/github.js +144 -0
- package/src/pair.js +5 -1
- package/src/server.js +53 -6
- package/web/app/app.js +286 -253
- package/web/app/style.css +56 -3
- package/web/board.css +17 -14
- package/web/board.js +60 -35
- package/web/index.html +1 -6
package/web/app/style.css
CHANGED
|
@@ -58,8 +58,10 @@ button { cursor: pointer; }
|
|
|
58
58
|
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--border); flex: none; }
|
|
59
59
|
.dot.on { background: var(--moss); box-shadow: 0 0 0 3px color-mix(in srgb, var(--moss) 18%, transparent); }
|
|
60
60
|
.dot.wait { background: var(--accent); }
|
|
61
|
-
.kbd { display: flex; gap:
|
|
62
|
-
.kbd
|
|
61
|
+
.kbd { display: flex; gap: 10px; align-items: center; overflow: hidden; width: 100%; padding: 9px 13px; border: 0; border-radius: 10px; background: none; color: var(--fg-muted); font-size: 14px; font-weight: 500; text-align: left; cursor: pointer; transition: background-color .2s, color .2s; }
|
|
62
|
+
.kbd:hover { color: var(--fg); background: color-mix(in srgb, var(--bg-2) 60%, transparent); }
|
|
63
|
+
.kbd .ico { width: 18px; height: 18px; }
|
|
64
|
+
.kbd kbd { border: 0; background: none; padding: 0; font: 500 12px var(--font); letter-spacing: .02em; color: var(--fg-muted); opacity: .7; }
|
|
63
65
|
.kbd .l { flex: 1 0 auto; white-space: nowrap; }
|
|
64
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); }
|
|
65
67
|
.main { min-width: 0; display: flex; flex-direction: column; }
|
|
@@ -146,7 +148,7 @@ button.chip:hover { border-color: var(--fg); }
|
|
|
146
148
|
.connect { display: grid; gap: 24px; min-width: 0; }
|
|
147
149
|
.connect-head h1 { font-size: clamp(30px, 4vw, 40px); font-weight: 500; letter-spacing: -.05em; line-height: 1.08; }
|
|
148
150
|
.connect-head .lead { color: var(--fg-muted); font-size: 16px; max-width: 58ch; margin-top: 10px; }
|
|
149
|
-
.connect-grid { display: grid; grid-template-columns: minmax(0,
|
|
151
|
+
.connect-grid { display: grid; grid-template-columns: minmax(0, 720px); gap: 16px; align-items: start; }
|
|
150
152
|
.conn-main { padding: 22px; display: grid; gap: 16px; min-width: 0; }
|
|
151
153
|
.stepper { list-style: none; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
152
154
|
.stepper li { display: flex; gap: 10px; align-items: flex-start; }
|
|
@@ -298,6 +300,8 @@ dialog.sheet[open] { animation: pop .22s var(--ease); }
|
|
|
298
300
|
|
|
299
301
|
/* ---------- Pissarra ---------- */
|
|
300
302
|
body.board-mode .page { padding: 0; max-width: none; }
|
|
303
|
+
body.board-mode { overflow: hidden; overscroll-behavior: none; }
|
|
304
|
+
body.board-mode .view-enter { animation: none; }
|
|
301
305
|
|
|
302
306
|
@media (prefers-reduced-motion: no-preference) {
|
|
303
307
|
.view-enter { animation: view-in .25s var(--ease); }
|
|
@@ -364,3 +368,52 @@ body.board-mode .page { padding: 0; max-width: none; }
|
|
|
364
368
|
.tour-foot .btn { height: 36px; padding: 0 14px; font-size: 13px; }
|
|
365
369
|
.board-root { height: calc(100dvh - 56px - var(--safe-t)) !important; }
|
|
366
370
|
}
|
|
371
|
+
|
|
372
|
+
/* Ordinadors, QR i git */
|
|
373
|
+
.conn.switch { cursor: pointer; border-radius: 8px; transition: color .2s; }
|
|
374
|
+
.conn.switch:hover { color: var(--fg); }
|
|
375
|
+
.conn.switch .ico { width: 14px; height: 14px; flex: none; }
|
|
376
|
+
.switch-chip { margin-top: 10px; cursor: pointer; }
|
|
377
|
+
.switch-chip .ico { width: 14px; height: 14px; }
|
|
378
|
+
.panel.hint { margin-bottom: 20px; padding: 16px 18px; }
|
|
379
|
+
.qr-box { width: min(260px, 100%); margin: 0 auto; padding: 14px; border-radius: 16px; background: #fffefd; border: 1px solid var(--border); }
|
|
380
|
+
.qr-box svg { display: block; width: 100%; height: auto; }
|
|
381
|
+
#ghWrap input[type="search"] { margin-bottom: 4px; }
|
|
382
|
+
|
|
383
|
+
/* Connexió: dues opcions a l'ordinador; explorador de carpetes a Nova sessió */
|
|
384
|
+
.choice-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; align-items: start; }
|
|
385
|
+
.choice .body { min-width: 0; }
|
|
386
|
+
@media (max-width: 820px) { .choice-grid { grid-template-columns: 1fr; } }
|
|
387
|
+
.fs-bar { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
388
|
+
.fs-path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left; }
|
|
389
|
+
.fs-list { max-height: 240px; overflow: auto; }
|
|
390
|
+
|
|
391
|
+
/* Assistent «Nova sessió»: drawer fix al telèfon, capçalera i peu fixos, cos amb scroll */
|
|
392
|
+
body:has(dialog.sheet[open]) { overflow: hidden; }
|
|
393
|
+
dialog.sheet { overscroll-behavior: contain; }
|
|
394
|
+
dialog.sheet.wiz { height: min(680px, 88dvh); overflow: hidden; }
|
|
395
|
+
dialog.sheet.wiz[open] { display: flex; flex-direction: column; }
|
|
396
|
+
dialog.sheet.wiz .sheet-in { padding: 0; gap: 0; flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
397
|
+
.wiz-head { flex: none; display: grid; gap: 8px; padding: 20px 24px 12px; }
|
|
398
|
+
.wiz-body { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 6px 24px 18px; display: grid; gap: 14px; align-content: start; }
|
|
399
|
+
.wiz-foot { flex: none; display: flex; justify-content: space-between; gap: 10px; padding: 12px 24px calc(16px + var(--safe-b)); border-top: 1px solid var(--border); background: var(--bg); }
|
|
400
|
+
.wiz-foot .btn { min-width: 110px; justify-content: center; }
|
|
401
|
+
.wiz-q { font-size: 18px; font-weight: 500; letter-spacing: -.03em; }
|
|
402
|
+
.wiz-dots { display: flex; gap: 6px; }
|
|
403
|
+
.wiz-dot { flex: 1; height: 18px; border: 0; background: var(--border) content-box; background-clip: content-box; padding: 7px 0; border-radius: 4px; }
|
|
404
|
+
.wiz-dot.on { background-color: var(--fg); }
|
|
405
|
+
.wiz-dot.done { background-color: var(--moss); }
|
|
406
|
+
.wiz-dot:disabled { cursor: default; }
|
|
407
|
+
.wiz .card[aria-pressed="true"] { border-color: var(--fg); box-shadow: inset 0 0 0 1px var(--fg); }
|
|
408
|
+
.fs-pick { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 10px 12px; border-radius: 10px; background: var(--bg-2); }
|
|
409
|
+
.fs-pick b { overflow-wrap: anywhere; }
|
|
410
|
+
.fs-list { max-height: none; }
|
|
411
|
+
@media (max-width: 820px) {
|
|
412
|
+
dialog.sheet.wiz { height: min(92dvh, calc(var(--vvh, 100dvh) - 8px)); }
|
|
413
|
+
.wiz-head { padding: 10px 18px 8px; }
|
|
414
|
+
.wiz-body { padding: 4px 18px 14px; }
|
|
415
|
+
.wiz-foot { padding: 12px 18px calc(14px + var(--safe-b)); }
|
|
416
|
+
.wiz-foot .btn { flex: 1; }
|
|
417
|
+
}
|
|
418
|
+
#fsForm { display: flex; gap: 8px; }
|
|
419
|
+
#fsForm input { flex: 1; min-width: 0; }
|
package/web/board.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Nio · Pissarra */
|
|
2
|
-
.board-root { position: relative; height: 100vh; height: 100dvh; --bd-bg: var(--bg); --bd-blue: #2b5bd8; }
|
|
3
|
-
.bd { display:
|
|
4
|
-
.bd-stage {
|
|
2
|
+
.board-root { position: relative; height: 100vh; height: 100dvh; overflow: hidden; overscroll-behavior: none; --bd-bg: var(--bg); --bd-blue: #2b5bd8; --chat-w: 360px; --bd-ease: cubic-bezier(.22,1,.36,1); }
|
|
3
|
+
.bd { container: stage / inline-size; position: relative; display: block; height: 100%; min-height: 0; overflow: hidden; }
|
|
4
|
+
.bd-stage { position: absolute; inset: 0; overflow: hidden; overscroll-behavior: none; touch-action: none; background-color: var(--bg); background-image: radial-gradient(circle, color-mix(in srgb, var(--fg) 16%, transparent) 1px, transparent 1.3px); user-select: none; -webkit-user-select: none; }
|
|
5
5
|
.bd-stage.nogrid { background-image: none; }
|
|
6
6
|
.bd-svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; touch-action: none; cursor: crosshair; }
|
|
7
7
|
.bd-stage[data-tool="select"] .bd-svg { cursor: default; }
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
.bd .ico { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; flex: none; }
|
|
29
29
|
.bd-bar { position: absolute; z-index: 4; display: flex; align-items: center; gap: 2px; padding: 4px; border-radius: 14px; border: 1px solid color-mix(in srgb, var(--border) 80%, transparent); background: color-mix(in srgb, var(--white) 80%, transparent); -webkit-backdrop-filter: blur(16px) saturate(160%); backdrop-filter: blur(16px) saturate(160%); box-shadow: 0 10px 30px -18px color-mix(in srgb, var(--fg) 40%, transparent); }
|
|
30
30
|
.bd-bar .sep { width: 1px; align-self: stretch; margin: 6px 4px; background: var(--border); }
|
|
31
|
-
.bd-tools { top: 14px; left: 50%; transform: translateX(-50%); }
|
|
31
|
+
.bd-tools { top: 14px; left: 50%; transform: translateX(-50%); transition: transform .34s var(--bd-ease); }
|
|
32
|
+
.chat-open .bd-tools { transform: translateX(calc(-50% - var(--chat-w) / 2)); }
|
|
32
33
|
.bd .tb, .bd .ib { position: relative; display: grid; place-items: center; width: 38px; height: 38px; border: 0; border-radius: 10px; background: none; color: var(--fg); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background-color .15s, color .15s, transform .12s; }
|
|
33
34
|
.bd .ib { width: 34px; height: 34px; }
|
|
34
35
|
.bd .tb:hover, .bd .ib:hover { background: color-mix(in srgb, var(--bg-2) 85%, transparent); }
|
|
@@ -39,7 +40,8 @@
|
|
|
39
40
|
.bd .pct { min-width: 54px; height: 34px; border: 0; border-radius: 10px; background: none; font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; cursor: pointer; }
|
|
40
41
|
.bd .pct:hover { background: color-mix(in srgb, var(--bg-2) 85%, transparent); }
|
|
41
42
|
.bd-zoom { left: 14px; bottom: 14px; }
|
|
42
|
-
.bd-right { top: 14px; right: 14px; }
|
|
43
|
+
.bd-right { top: 14px; right: 14px; transition: transform .34s var(--bd-ease); }
|
|
44
|
+
.chat-open .bd-right { transform: translateX(calc(-1 * var(--chat-w))); }
|
|
43
45
|
.bd .chat-t { width: auto; display: flex; gap: 8px; padding: 0 12px 0 9px; font-size: 13px; font-weight: 600; }
|
|
44
46
|
.bd .chat-t .tbbot { --s: 20px; --bw: .15; }
|
|
45
47
|
.only-m { display: none !important; }
|
|
@@ -84,9 +86,8 @@
|
|
|
84
86
|
.bd-help dl div { display: flex; align-items: center; gap: 6px; padding: 6px 0; border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent); font-size: 13px; }
|
|
85
87
|
.bd-help dd { flex: 1; }
|
|
86
88
|
|
|
87
|
-
.bd-chat { position:
|
|
88
|
-
.chat-open .bd-chat {
|
|
89
|
-
.bd-chat > * { width: 360px; flex: none; }
|
|
89
|
+
.bd-chat { position: absolute; z-index: 6; top: 0; right: 0; bottom: 0; display: flex; flex-direction: column; width: var(--chat-w); min-height: 0; overflow: hidden; border-left: 1px solid var(--border); background: var(--panel); transform: translateX(100%); visibility: hidden; transition: transform .34s var(--bd-ease), visibility 0s linear .34s; }
|
|
90
|
+
.chat-open .bd-chat { transform: none; visibility: visible; transition: transform .34s var(--bd-ease), visibility 0s; }
|
|
90
91
|
.bd-chat header { display: flex; justify-content: space-between; align-items: center; padding: 14px 14px 12px 16px; border-bottom: 1px solid var(--border); }
|
|
91
92
|
.bd-chat .who { display: flex; align-items: center; gap: 10px; }
|
|
92
93
|
.bd-chat .hbot { --s: 30px; --bw: .14; }
|
|
@@ -114,11 +115,10 @@
|
|
|
114
115
|
|
|
115
116
|
@media (prefers-reduced-motion: reduce) { .bd *, .bd *::before { animation: none !important; transition: none !important; } }
|
|
116
117
|
|
|
117
|
-
@media (max-width: 1100px) { .
|
|
118
|
+
@media (max-width: 1100px) { .board-root { --chat-w: 320px; } }
|
|
118
119
|
@media (max-width: 820px) {
|
|
119
120
|
.board-root { height: calc(100dvh - 56px - env(safe-area-inset-top)); }
|
|
120
|
-
.bd
|
|
121
|
-
.bd-stage { position: absolute; inset: 0; }
|
|
121
|
+
.chat-open .bd-tools, .chat-open .bd-right { transform: none; }
|
|
122
122
|
.bd-tools { top: auto; bottom: calc(12px + env(safe-area-inset-bottom)); left: 10px; right: 10px; transform: none; overflow-x: auto; scrollbar-width: none; }
|
|
123
123
|
.bd-tools::-webkit-scrollbar { display: none; }
|
|
124
124
|
.bd .tb { width: 40px; height: 40px; flex: none; }
|
|
@@ -132,15 +132,18 @@
|
|
|
132
132
|
.bd-style { display: none; }
|
|
133
133
|
.style-open .bd-style:not([hidden]) { display: grid; left: 10px; right: 10px; top: auto; bottom: calc(70px + env(safe-area-inset-bottom)); width: auto; grid-template-columns: 1fr 1fr; }
|
|
134
134
|
.bd-help .cols { grid-template-columns: 1fr; }
|
|
135
|
-
.bd-chat { position: absolute; left: 0; right: 0; bottom: 0; z-index: 9; width: auto
|
|
136
|
-
.chat-open .bd-chat { transform: none; }
|
|
137
|
-
.bd-chat > * { width: auto !important; }
|
|
135
|
+
.bd-chat { position: absolute; left: 0; right: 0; bottom: 0; z-index: 9; width: auto; height: min(72%, 560px); border: 0; border-top: 1px solid var(--border); border-radius: 20px 20px 0 0; transform: translateY(105%); transition: transform .34s var(--bd-ease), visibility 0s linear .34s; box-shadow: 0 -20px 50px -30px color-mix(in srgb, var(--fg) 55%, transparent); }
|
|
136
|
+
.chat-open .bd-chat { transform: none; transition: transform .34s var(--bd-ease), visibility 0s; }
|
|
138
137
|
.bd-chat header::before { content: ""; position: absolute; top: 6px; left: 50%; width: 36px; height: 4px; margin-left: -18px; border-radius: 4px; background: var(--border); }
|
|
139
138
|
.bd-empty { top: 42%; }
|
|
140
139
|
}
|
|
141
140
|
|
|
142
141
|
/* El llenç s'adapta a la seva amplada real (tauleta, xat obert, mòbil) */
|
|
143
142
|
@container stage (max-width: 720px) {
|
|
143
|
+
.bd-chat { position: absolute; left: 0; right: 0; bottom: 0; z-index: 9; width: auto; height: min(72%, 560px); border: 0; border-top: 1px solid var(--border); border-radius: 20px 20px 0 0; transform: translateY(105%); transition: transform .34s var(--bd-ease), visibility 0s linear .34s; box-shadow: 0 -20px 50px -30px color-mix(in srgb, var(--fg) 55%, transparent); }
|
|
144
|
+
.chat-open .bd-chat { transform: none; transition: transform .34s var(--bd-ease), visibility 0s; }
|
|
145
|
+
.bd-chat header::before { content: ""; position: absolute; top: 6px; left: 50%; width: 36px; height: 4px; margin-left: -18px; border-radius: 4px; background: var(--border); }
|
|
146
|
+
.chat-open .bd-tools, .chat-open .bd-right { transform: none; }
|
|
144
147
|
.bd-tools { top: auto; bottom: calc(12px + env(safe-area-inset-bottom)); left: 10px; right: 10px; transform: none; overflow-x: auto; scrollbar-width: none; justify-content: flex-start; }
|
|
145
148
|
.bd-tools::-webkit-scrollbar { display: none; }
|
|
146
149
|
.bd .tb { flex: none; }
|
package/web/board.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
'use strict';
|
|
4
4
|
const KEY = 'nio.board.v1';
|
|
5
5
|
const NS = 'http://www.w3.org/2000/svg';
|
|
6
|
-
const GAP =
|
|
6
|
+
const GAP = 3;
|
|
7
7
|
const HAND = '"Caveat", "Segoe Print", "Comic Sans MS", cursive';
|
|
8
8
|
const CLEAN = '"Manrope", system-ui, sans-serif';
|
|
9
9
|
const STROKES = [['#272224', 'Negre'], ['#e2382b', 'Vermell'], ['#2f7d4f', 'Verd'], ['#2b5bd8', 'Blau'], ['#e07b1f', 'Taronja'], ['#8a5cf6', 'Lila']];
|
|
@@ -86,6 +86,7 @@ function wrap(text, fs, maxW) {
|
|
|
86
86
|
return out;
|
|
87
87
|
}
|
|
88
88
|
const fsOf = el => SIZES[el.fs] || el.fsPx || SIZES.m;
|
|
89
|
+
const textW = el => Math.max(30, el.type === 'rect' ? el.w - 28 : el.type === 'ellipse' ? el.w * .7 - 8 : el.w * .5 - 6);
|
|
89
90
|
function fitText(el) {
|
|
90
91
|
const fs = fsOf(el), lines = String(el.text || '').split('\n');
|
|
91
92
|
el.w = Math.max(20, ...lines.map(l => measure(l, fs))) + 4;
|
|
@@ -100,6 +101,10 @@ function rline(x1, y1, x2, y2, r, amt) {
|
|
|
100
101
|
const mx = (x1 + x2) / 2 + j() * 1.4, my = (y1 + y2) / 2 + j() * 1.4;
|
|
101
102
|
return `M${f1(x1 + j())} ${f1(y1 + j())}Q${f1(mx)} ${f1(my)} ${f1(x2 + j())} ${f1(y2 + j())}`;
|
|
102
103
|
}
|
|
104
|
+
function rlineX(x1, y1, x2, y2, r, amt) {
|
|
105
|
+
const len = Math.hypot(x2 - x1, y2 - y1), o = Math.min(amt, len * .03 + .3), j = () => (r() - .5) * 2 * o;
|
|
106
|
+
return `M${f1(x1)} ${f1(y1)}Q${f1((x1 + x2) / 2 + j() * 1.4)} ${f1((y1 + y2) / 2 + j() * 1.4)} ${f1(x2)} ${f1(y2)}`;
|
|
107
|
+
}
|
|
103
108
|
function smooth(pts, closed) {
|
|
104
109
|
if (pts.length < 2) return '';
|
|
105
110
|
const p = closed ? [pts[pts.length - 1], ...pts, pts[0], pts[1]] : [pts[0], ...pts, pts[pts.length - 1]];
|
|
@@ -110,33 +115,43 @@ function smooth(pts, closed) {
|
|
|
110
115
|
}
|
|
111
116
|
return d;
|
|
112
117
|
}
|
|
118
|
+
function polyPath(pts, r, bow) {
|
|
119
|
+
let d = `M${f1(pts[0][0])} ${f1(pts[0][1])}`;
|
|
120
|
+
for (let i = 0; i < pts.length; i++) {
|
|
121
|
+
const p = pts[i], q = pts[(i + 1) % pts.length], o = Math.min(bow, Math.hypot(q[0] - p[0], q[1] - p[1]) * .02 + .3);
|
|
122
|
+
d += `Q${f1((p[0] + q[0]) / 2 + (r() - .5) * 2 * o)} ${f1((p[1] + q[1]) / 2 + (r() - .5) * 2 * o)} ${f1(q[0])} ${f1(q[1])}`;
|
|
123
|
+
}
|
|
124
|
+
return d + 'Z';
|
|
125
|
+
}
|
|
113
126
|
function shapePaths(el) {
|
|
114
|
-
const { x, y, w, h } = el, rough = B.opts.rough, r = rng(el.seed), amt =
|
|
127
|
+
const { x, y, w, h } = el, rough = B.opts.rough, r = rng(el.seed), amt = .8 + el.sw * .3, bow = 1 + el.sw * .3;
|
|
128
|
+
const jit = (pts, k) => pts.map(p => [p[0] + (r() - .5) * 2 * amt * k, p[1] + (r() - .5) * 2 * amt * k]);
|
|
115
129
|
if (el.type === 'rect') {
|
|
116
|
-
const
|
|
117
|
-
if (!rough)
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
const c = Math.min(8, w / 2, h / 2);
|
|
131
|
+
if (!rough) {
|
|
132
|
+
const clean = `M${x + c} ${y}H${x + w - c}Q${x + w} ${y} ${x + w} ${y + c}V${y + h - c}Q${x + w} ${y + h} ${x + w - c} ${y + h}H${x + c}Q${x} ${y + h} ${x} ${y + h - c}V${y + c}Q${x} ${y} ${x + c} ${y}Z`;
|
|
133
|
+
return { fill: clean, stroke: [clean] };
|
|
134
|
+
}
|
|
135
|
+
const pts = [[x, y], [x + w, y], [x + w, y + h], [x, y + h]], p0 = polyPath(jit(pts, .5), r, bow);
|
|
136
|
+
return { fill: p0, stroke: [p0, polyPath(jit(pts, 1.4), r, bow)] };
|
|
120
137
|
}
|
|
121
138
|
if (el.type === 'diamond') {
|
|
122
|
-
const
|
|
123
|
-
const clean = `M${
|
|
139
|
+
const pts = [[x + w / 2, y], [x + w, y + h / 2], [x + w / 2, y + h], [x, y + h / 2]];
|
|
140
|
+
const clean = `M${pts.map(p => p.join(' ')).join('L')}Z`;
|
|
124
141
|
if (!rough) return { fill: clean, stroke: [clean] };
|
|
125
|
-
const
|
|
126
|
-
return { fill:
|
|
142
|
+
const p0 = polyPath(jit(pts, .5), r, bow);
|
|
143
|
+
return { fill: p0, stroke: [p0, polyPath(jit(pts, 1.4), r, bow)] };
|
|
127
144
|
}
|
|
128
145
|
const cx = x + w / 2, cy = y + h / 2, rx = Math.max(1, w / 2), ry = Math.max(1, h / 2);
|
|
129
146
|
const clean = `M${cx - rx} ${cy}a${rx} ${ry} 0 1 0 ${rx * 2} 0a${rx} ${ry} 0 1 0 ${-rx * 2} 0Z`;
|
|
130
147
|
if (!rough) return { fill: clean, stroke: [clean] };
|
|
131
|
-
const
|
|
132
|
-
const n =
|
|
133
|
-
for (let i = 0; i
|
|
134
|
-
|
|
135
|
-
pts.push([cx + Math.cos(a) * rx * q, cy + Math.sin(a) * ry * q]);
|
|
136
|
-
}
|
|
137
|
-
return smooth(pts, false);
|
|
148
|
+
const ring = k => {
|
|
149
|
+
const n = 28, pts = [];
|
|
150
|
+
for (let i = 0; i < n; i++) { const a = (i / n) * Math.PI * 2, q = 1 + (r() - .5) * .02 * k; pts.push([cx + Math.cos(a) * rx * q, cy + Math.sin(a) * ry * q]); }
|
|
151
|
+
return smooth(pts, true) + 'Z';
|
|
138
152
|
};
|
|
139
|
-
|
|
153
|
+
const p0 = ring(1);
|
|
154
|
+
return { fill: p0, stroke: [p0, ring(2.4)] };
|
|
140
155
|
}
|
|
141
156
|
|
|
142
157
|
/* ---------- Geometria ---------- */
|
|
@@ -194,16 +209,16 @@ function elSVG(el, exp) {
|
|
|
194
209
|
const p = shapePaths(el);
|
|
195
210
|
if (el.fill && el.fill !== 'none') s += `<path d="${p.fill}" fill="${el.fill}"/>`;
|
|
196
211
|
s += p.stroke.map((d, i) => `<path d="${d}" ${st}${i ? ' opacity=".7"' : ''}/>`).join('');
|
|
197
|
-
if (el.text) s += labelSVG(el.text, el.x + el.w / 2, el.y + el.h / 2, fsOf(el), el.stroke,
|
|
212
|
+
if (el.text) s += labelSVG(el.text, el.x + el.w / 2, el.y + el.h / 2, fsOf(el), el.stroke, textW(el));
|
|
198
213
|
if (!exp) s += `<path class="hit" d="${p.fill}"/>`;
|
|
199
214
|
} else if (isLinear(el)) {
|
|
200
215
|
const [a, b] = ends(el), r = rng(el.seed), amt = 1 + el.sw * .4;
|
|
201
|
-
const body = B.opts.rough ?
|
|
216
|
+
const body = B.opts.rough ? rlineX(a[0], a[1], b[0], b[1], r, amt) + rlineX(a[0], a[1], b[0], b[1], r, amt * .7) : `M${f1(a[0])} ${f1(a[1])}L${f1(b[0])} ${f1(b[1])}`;
|
|
202
217
|
let head = '';
|
|
203
218
|
if (el.type === 'arrow') {
|
|
204
219
|
const ang = Math.atan2(b[1] - a[1], b[0] - a[0]), L = Math.min(16 + el.sw * 2, Math.hypot(b[0] - a[0], b[1] - a[1]) * .5);
|
|
205
220
|
const h1 = [b[0] - L * Math.cos(ang - .45), b[1] - L * Math.sin(ang - .45)], h2 = [b[0] - L * Math.cos(ang + .45), b[1] - L * Math.sin(ang + .45)];
|
|
206
|
-
head =
|
|
221
|
+
head = `M${f1(h1[0])} ${f1(h1[1])}L${f1(b[0])} ${f1(b[1])}L${f1(h2[0])} ${f1(h2[1])}`;
|
|
207
222
|
}
|
|
208
223
|
s += `<path d="${body}" ${st}/>`;
|
|
209
224
|
if (head) s += `<path d="${head}" fill="none" stroke="${el.stroke}" stroke-width="${el.sw}" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
@@ -341,7 +356,7 @@ function placeEditor() {
|
|
|
341
356
|
if (el.type === 'text') { x = el.x; y = el.y; w = Math.max(el.w, 120); ta.style.textAlign = 'left'; }
|
|
342
357
|
else {
|
|
343
358
|
const b = isLinear(el) ? (() => { const [a, c] = ends(el); return { x: (a[0] + c[0]) / 2 - 110, y: (a[1] + c[1]) / 2, w: 220, h: 0 }; })() : bbox(el);
|
|
344
|
-
w = isLinear(el) ? 220 : Math.max(60, (el
|
|
359
|
+
w = isLinear(el) ? 220 : Math.max(60, textW(el));
|
|
345
360
|
x = b.x + (b.w - w) / 2; y = b.y + b.h / 2; ta.style.textAlign = 'center';
|
|
346
361
|
}
|
|
347
362
|
ta.style.width = w * z + 'px';
|
|
@@ -358,6 +373,7 @@ function endEdit() {
|
|
|
358
373
|
if (el.text !== v) { if (!el._new) record(); el.text = v; }
|
|
359
374
|
delete el._new;
|
|
360
375
|
if (el.type === 'text') { if (!v) { B.els = B.els.filter(e => e !== el); sel.delete(el.id); } else fitText(el); }
|
|
376
|
+
else growToFit(el);
|
|
361
377
|
full(); save();
|
|
362
378
|
}
|
|
363
379
|
|
|
@@ -569,7 +585,7 @@ function paste() {
|
|
|
569
585
|
}
|
|
570
586
|
function setStyle(k, v) {
|
|
571
587
|
B.style[k] = v;
|
|
572
|
-
if (sel.size) { record(); sel.forEach(id => { const el = byId(id); if (!el) return; if (k === 'fill' && !isShape(el)) return; el[k] = v; if (k === 'fs') { el.fsPx = null; if (el.type === 'text') fitText(el); } }); full(); }
|
|
588
|
+
if (sel.size) { record(); sel.forEach(id => { const el = byId(id); if (!el) return; if (k === 'fill' && !isShape(el)) return; el[k] = v; if (k === 'fs') { el.fsPx = null; if (el.type === 'text') fitText(el); else growToFit(el); } }); full(); }
|
|
573
589
|
save(); panel();
|
|
574
590
|
}
|
|
575
591
|
function order(dir) {
|
|
@@ -608,13 +624,19 @@ function importJSON(file) {
|
|
|
608
624
|
}
|
|
609
625
|
|
|
610
626
|
/* ---------- Diagrames ---------- */
|
|
611
|
-
function nodeSize(label, type) {
|
|
612
|
-
const fs = SIZES[B.style.fs] || 24, lines = wrap(label, fs, 240);
|
|
613
|
-
const tw = Math.max(...lines.map(l => measure(l, fs)), 40);
|
|
614
|
-
|
|
615
|
-
if (type === '
|
|
616
|
-
|
|
617
|
-
|
|
627
|
+
function nodeSize(label, type, fsKey) {
|
|
628
|
+
const fs = SIZES[fsKey || B.style.fs] || 24, lines = wrap(label, fs, type === 'diamond' ? 150 : type === 'ellipse' ? 200 : 240);
|
|
629
|
+
const tw = Math.max(...lines.map(l => measure(l, fs)), 40), th = lines.length * fs * 1.25;
|
|
630
|
+
if (type === 'rect') return [Math.max(150, Math.ceil(tw + 56)), Math.max(72, Math.ceil(th + 40))];
|
|
631
|
+
if (type === 'ellipse') return [Math.max(160, Math.ceil((tw + 8) / .7 + 8)), Math.max(80, Math.ceil(th * 1.5 + 24))];
|
|
632
|
+
return [Math.max(190, Math.ceil((tw + 6) / .5 + 8)), Math.max(120, Math.ceil(th * 2 + 32))];
|
|
633
|
+
}
|
|
634
|
+
// Fa créixer la figura (des del centre) si el text no hi cap.
|
|
635
|
+
function growToFit(el) {
|
|
636
|
+
if (!isShape(el) || !el.text) return;
|
|
637
|
+
const [w, h] = nodeSize(el.text, el.type, el.fs), [cx, cy] = center(el);
|
|
638
|
+
const nw = Math.max(el.w, w), nh = Math.max(el.h, h);
|
|
639
|
+
if (nw !== el.w || nh !== el.h) { el.w = nw; el.h = nh; el.x = cx - nw / 2; el.y = cy - nh / 2; }
|
|
618
640
|
}
|
|
619
641
|
function guessType(label) {
|
|
620
642
|
const n = norm(label);
|
|
@@ -631,7 +653,7 @@ function findEl(q) {
|
|
|
631
653
|
}
|
|
632
654
|
function addNode(label, type, x, y, extra = {}) {
|
|
633
655
|
type = type || guessType(label);
|
|
634
|
-
const [w, h] = nodeSize(label, type);
|
|
656
|
+
const [w, h] = nodeSize(label, type, B.style.fs);
|
|
635
657
|
const el = mk(type, Object.assign({ x: x - w / 2, y: y - h / 2, w, h, text: label.trim() }, extra));
|
|
636
658
|
if (type === 'diamond' && el.fill === 'none') el.fill = '#fbf1bf';
|
|
637
659
|
B.els.push(el); return el;
|
|
@@ -1000,9 +1022,12 @@ function onKeyUp(e) { if (e.key === ' ') { spaceDown = false; R && R.stage.class
|
|
|
1000
1022
|
function openChat(open, focus = true, persist = true) {
|
|
1001
1023
|
if (persist) { B.opts.chat = open; save(); }
|
|
1002
1024
|
if (!R) return;
|
|
1025
|
+
const wasOpen = R.root.classList.contains('chat-open');
|
|
1003
1026
|
R.root.classList.toggle('chat-open', open);
|
|
1004
|
-
R.
|
|
1005
|
-
|
|
1027
|
+
R.chat.inert = !open;
|
|
1028
|
+
const t = R.root.querySelector('[data-bd=chat]'); t.setAttribute('aria-pressed', open);
|
|
1029
|
+
if (open && focus) setTimeout(() => R.input.focus(), reduce ? 0 : 280);
|
|
1030
|
+
if (!open && wasOpen && R.chat.contains(document.activeElement)) t.focus({ preventScroll: true });
|
|
1006
1031
|
}
|
|
1007
1032
|
|
|
1008
1033
|
/* ---------- Muntatge ---------- */
|
|
@@ -1128,7 +1153,7 @@ function mount(root) {
|
|
|
1128
1153
|
R = { root, stage: root.querySelector('#bdStage'), svg: root.querySelector('#bdSvg'), world: root.querySelector('#bdWorld'), els: root.querySelector('#bdEls'), sel: root.querySelector('#bdSel'),
|
|
1129
1154
|
editor: root.querySelector('#bdEdit'), empty: root.querySelector('#bdEmpty'), tools: root.querySelector('#bdTools'), style: root.querySelector('#bdStyle'),
|
|
1130
1155
|
zoomPct: root.querySelector('#bdPct'), help: root.querySelector('#bdHelp'), msgs: root.querySelector('#bdMsgs'), input: root.querySelector('#bdInput'),
|
|
1131
|
-
chatBot: root.querySelector('.bd-chat .hbot') };
|
|
1156
|
+
chatBot: root.querySelector('.bd-chat .hbot'), chat: root.querySelector('#bdChat') };
|
|
1132
1157
|
nodes.clear();
|
|
1133
1158
|
const first = !B.els.length && B.view.x === 0 && B.view.y === 0;
|
|
1134
1159
|
if (first) { const r = R.svg.getBoundingClientRect(); B.view = { x: r.width / 2, y: r.height / 2, z: 1 }; }
|
|
@@ -1138,7 +1163,7 @@ function mount(root) {
|
|
|
1138
1163
|
on(root, 'click', onClick);
|
|
1139
1164
|
on(R.editor, 'input', placeEditor); on(R.editor, 'blur', () => setTimeout(endEdit, 0));
|
|
1140
1165
|
on(R.root.querySelector('#bdForm'), 'submit', e => { e.preventDefault(); send(); });
|
|
1141
|
-
on(R.input, 'keydown', e => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); send(); } if (e.key === 'Escape')
|
|
1166
|
+
on(R.input, 'keydown', e => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); send(); } if (e.key === 'Escape') { e.stopPropagation(); openChat(false); } });
|
|
1142
1167
|
on(R.input, 'input', () => { R.input.style.height = ''; R.input.style.height = Math.min(140, R.input.scrollHeight) + 'px'; });
|
|
1143
1168
|
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.`); });
|
|
1144
1169
|
on(R.root.querySelector('#bdBrain'), 'focus', drawBrains);
|
package/web/index.html
CHANGED
|
@@ -108,7 +108,7 @@ main strong { font-weight: 600; }
|
|
|
108
108
|
<section aria-labelledby="com">
|
|
109
109
|
<h2 id="com">Com funciona</h2>
|
|
110
110
|
<p>Obre el terminal a la carpeta dels teus projectes i executa:</p>
|
|
111
|
-
<div class="cmd"><code id="cmd">npx
|
|
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
112
|
<p>Escaneja el codi QR que apareix i ja estàs connectat. A casa, per la wifi; amb <code>cloudflared</code>, des de qualsevol lloc. Només cal tenir <a href="https://nodejs.org">Node.js</a>, i cap dada no passa per servidors nostres.</p>
|
|
113
113
|
</section>
|
|
114
114
|
|
|
@@ -116,11 +116,6 @@ main strong { font-weight: 600; }
|
|
|
116
116
|
<h2 id="limits">Tu decideixes</h2>
|
|
117
117
|
<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
118
|
</section>
|
|
119
|
-
|
|
120
|
-
<section aria-labelledby="prova">
|
|
121
|
-
<h2 id="prova">Prova-ho</h2>
|
|
122
|
-
<p>Sense instal·lar res, amb un ordinador simulat: <a href="/app">obre el panell</a>. També hi trobaràs els teus models d’Ollama i un model petit que funciona dins del mateix mòbil.</p>
|
|
123
|
-
</section>
|
|
124
119
|
</main>
|
|
125
120
|
|
|
126
121
|
<footer class="footer">
|