@parley-im/parley 0.2.1 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parley-im/parley",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Send someone a link. They talk to a read-only Claude or Codex in your project, on your computer, and you get everything they said.",
5
5
  "keywords": [
6
6
  "claude",
package/web/app.css CHANGED
@@ -74,6 +74,8 @@ button, input, textarea, select { font: inherit; color: inherit; }
74
74
  .crumbs { display: flex; align-items: baseline; gap: 8px; min-width: 0; flex: 1; }
75
75
  .crumbs .product { color: var(--muted); font-weight: 500; }
76
76
  .crumbs .slash { color: var(--faint); }
77
+ .brand { display: inline-flex; align-items: center; gap: 10px; font-weight: 600; color: inherit; text-decoration: none !important; }
78
+ .brand:hover .mark { filter: brightness(1.1); }
77
79
  .crumbs .here { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
78
80
  .crumbs a { text-decoration: none; }
79
81
  .crumbs a:hover { text-decoration: underline; }
@@ -109,13 +111,16 @@ button, input, textarea, select { font: inherit; color: inherit; }
109
111
  .chip.low { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, var(--line)); }
110
112
  .info-btn { width: 34px; height: 34px; margin-right: -6px; }
111
113
 
112
- .sheet-back { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.35); z-index: 40; }
114
+ /* The menu: drops from the button at the top right, over the conversation. */
115
+ .sheet-back { position: absolute; inset: 0; z-index: 40; background: rgba(0, 0, 0, 0.12); }
113
116
  .sheet {
114
- position: fixed; z-index: 41; left: 50%; top: 50%; transform: translate(-50%, -50%);
115
- width: min(440px, calc(100% - 32px)); max-height: calc(100% - 48px); overflow-y: auto;
117
+ position: absolute; z-index: 41; right: 8px; top: calc(54px + env(safe-area-inset-top));
118
+ width: min(380px, calc(100% - 16px)); max-height: calc(100% - 70px - env(safe-area-inset-top)); overflow-y: auto;
116
119
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px; box-shadow: var(--shadow);
120
+ transform-origin: top right; animation: drop 0.14s ease-out;
117
121
  }
118
- .sheet header { display: flex; align-items: center; justify-content: space-between; padding: 12px 12px 4px 16px; }
122
+ .sheet header { display: flex; align-items: center; justify-content: space-between; padding: 8px 6px 2px 16px; }
123
+ .sheet header .icon-btn { width: 34px; height: 34px; }
119
124
  .sheet h2 { font-size: 16px; margin: 0; }
120
125
  .sheet-body { padding: 4px 16px 16px; font-size: 14px; }
121
126
  .sheet-body p { margin: 0 0 10px; color: var(--muted); }
@@ -125,9 +130,6 @@ button, input, textarea, select { font: inherit; color: inherit; }
125
130
  .sheet-body .fine { font-size: 12px; color: var(--faint); }
126
131
  .sheet-body details.how { margin-top: 14px; border-top: 1px solid var(--line); padding-top: 10px; }
127
132
  .sheet-body details.how summary { cursor: pointer; color: var(--text); font-weight: 500; margin-bottom: 8px; }
128
- @media (max-width: 560px) {
129
- .sheet { left: 0; right: 0; top: auto; bottom: 0; transform: none; width: auto; border-radius: 16px 16px 0 0; padding-bottom: env(safe-area-inset-bottom); }
130
- }
131
133
 
132
134
  .notice {
133
135
  margin: 10px 16px 0; padding: 9px 12px; border-radius: 8px; font-size: 14px;
@@ -138,7 +140,7 @@ button, input, textarea, select { font: inherit; color: inherit; }
138
140
 
139
141
  /* ---------- chat ---------- */
140
142
 
141
- .app { display: flex; flex-direction: column; height: 100dvh; overflow: hidden; }
143
+ .app { position: relative; display: flex; flex-direction: column; height: 100dvh; overflow: hidden; }
142
144
  /* The chat never scrolls the page itself, only its conversation. */
143
145
  body:has(> .app) { overflow: hidden; overscroll-behavior: none; }
144
146
  .layout { flex: 1; min-height: 0; display: flex; }
@@ -393,6 +395,29 @@ body.panel { overflow: auto; }
393
395
  .site code { font-family: var(--mono); font-size: 0.88em; background: var(--subtle); border: 1px solid var(--line); padding: 0.05em 0.35em; border-radius: 5px; }
394
396
  .site h3 { margin: 32px 0 8px; }
395
397
  .stack { display: grid; gap: 10px; max-width: 360px; margin-top: 16px; }
398
+ /* Your account, top right of parley.im's pages. */
399
+ .account-menu { position: relative; }
400
+ .account-menu summary { list-style: none; display: flex; align-items: center; gap: 4px; padding: 3px 6px 3px 3px; border-radius: 99px; cursor: pointer; }
401
+ .account-menu summary::-webkit-details-marker { display: none; }
402
+ .account-menu summary:hover, .account-menu[open] summary { background: var(--subtle); }
403
+ .account-menu summary svg { width: 14px; height: 14px; fill: none; stroke: var(--muted); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
404
+ .avatar { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 50%; background: var(--accent-soft); color: var(--accent); font-weight: 600; font-size: 13px; }
405
+ .menu-panel {
406
+ position: absolute; right: 0; top: calc(100% + 8px); z-index: 30; min-width: 220px; max-width: calc(100vw - 24px);
407
+ background: var(--panel); border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow); padding: 6px;
408
+ animation: drop 0.14s ease-out;
409
+ }
410
+ .menu-panel a, .menu-panel button {
411
+ display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 10px; border: 0; border-radius: 8px;
412
+ background: none; color: var(--text); text-decoration: none; font-size: 14px; text-align: left; cursor: pointer;
413
+ }
414
+ .menu-panel a:hover, .menu-panel button:hover { background: var(--subtle); }
415
+ .menu-panel form { margin: 0; border-top: 1px solid var(--line); margin-top: 4px; padding-top: 4px; }
416
+ .menu-who { display: grid; padding: 8px 10px 10px; border-bottom: 1px solid var(--line); margin-bottom: 4px; }
417
+ .menu-who strong { font-size: 14px; }
418
+ .menu-who span { font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
419
+ @keyframes drop { from { opacity: 0; transform: translateY(-4px) scale(0.98); } to { opacity: 1; transform: none; } }
420
+
396
421
  .live { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; color: var(--muted); white-space: nowrap; }
397
422
  .docs { padding-bottom: 40px; }
398
423
  .docs h2 { font-size: 17px; letter-spacing: -0.01em; margin: 36px 0 10px; }
package/web/chat.html CHANGED
@@ -24,7 +24,7 @@
24
24
  <span class="status" id="status" hidden><span class="dot" id="status-dot"></span><span id="status-text"></span></span>
25
25
  <span class="chip" id="chip" hidden></span>
26
26
  <button class="icon-btn info-btn" id="info-btn" type="button" title="This conversation" hidden>
27
- <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14"/><path d="M5 12h14"/><path d="M5 17h9"/></svg>
27
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg>
28
28
  <span class="sr">This conversation: details and actions</span>
29
29
  </button>
30
30
  </header>
@@ -66,12 +66,19 @@
66
66
  </div>
67
67
  <aside class="side" id="side" hidden></aside>
68
68
  </div>
69
+ <!-- Inside the app, so it stays under the header when a phone's keyboard
70
+ moves things around. -->
71
+ <div class="sheet-back" id="sheet-back" hidden></div>
72
+ <section class="sheet" id="sheet" hidden aria-label="This conversation">
73
+ <header>
74
+ <h2>This conversation</h2>
75
+ <button class="icon-btn" id="sheet-close" type="button" title="Close">
76
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg><span class="sr">Close</span>
77
+ </button>
78
+ </header>
79
+ <div class="sheet-body" id="sheet-body"></div>
80
+ </section>
69
81
  </div>
70
- <div class="sheet-back" id="sheet-back" hidden></div>
71
- <section class="sheet" id="sheet" hidden aria-label="This conversation">
72
- <header><h2>This conversation</h2><button class="btn ghost" id="sheet-close" type="button">Done</button></header>
73
- <div class="sheet-body" id="sheet-body"></div>
74
- </section>
75
82
  <div class="drop" id="drop" hidden><div>Drop screenshots to add them</div></div>
76
83
  <script>window.PARLEY = {{CONFIG}};</script>
77
84
  <script src="/static/markdown.js"></script>
package/web/chat.js CHANGED
@@ -360,11 +360,18 @@
360
360
  el("p", {}, `When the replies run out, the chat pauses until ${whom} adds more. Replies come from ${theirs} computer, so it needs to be on, and the link stops working for good once the chat ends.`),
361
361
  ),
362
362
  );
363
+ // Just under the header, whatever its height on this screen.
364
+ const header = document.querySelector(".topbar").getBoundingClientRect();
365
+ els.sheet.style.top = `${header.bottom - els.app.getBoundingClientRect().top + 6}px`;
363
366
  els.sheet.hidden = els.sheetBack.hidden = false;
367
+ els.infoBtn.setAttribute("aria-expanded", "true");
364
368
  els.sheetClose.focus();
365
369
  }
366
- const closeSheet = () => (els.sheet.hidden = els.sheetBack.hidden = true);
367
- els.infoBtn.addEventListener("click", openSheet);
370
+ const closeSheet = () => {
371
+ els.sheet.hidden = els.sheetBack.hidden = true;
372
+ els.infoBtn.setAttribute("aria-expanded", "false");
373
+ };
374
+ els.infoBtn.addEventListener("click", () => (els.sheet.hidden ? openSheet() : closeSheet()));
368
375
  els.sheetClose.addEventListener("click", closeSheet);
369
376
  els.sheetBack.addEventListener("click", closeSheet);
370
377
  addEventListener("keydown", (e) => e.key === "Escape" && closeSheet());
@@ -736,6 +743,10 @@
736
743
  clearTimeout(retry);
737
744
  retry = null;
738
745
  ws = new WebSocket(`${location.origin.replace(/^http/, "ws")}${base}/ws`);
746
+ // Keep-alives, answered by parley.im at the edge (no cost), keep phone
747
+ // networks and proxies from dropping a quiet connection.
748
+ const ping = setInterval(() => ws.readyState === WebSocket.OPEN && ws.send('{"t":"ping"}'), 30_000);
749
+ ws.addEventListener("close", () => clearInterval(ping));
739
750
  ws.onopen = () => {
740
751
  failures = 0;
741
752
  h.offline(false, "connection");
package/web/dashboard.js CHANGED
@@ -1,7 +1,12 @@
1
1
  // Keeps the parley.im dashboard current: who's on each session's page right
2
- // now, when they were last active, and whether a reply is coming in. It
3
- // never shows what was said; parley.im doesn't have that.
2
+ // now, when they were last active, and whether a reply is coming in. Each
3
+ // linked computer pushes news over its own WebSocket, which costs nothing
4
+ // while it's quiet, so nothing here polls. It never shows what was said;
5
+ // parley.im doesn't have that.
4
6
  (() => {
7
+ const REPLYING_MS = 6000;
8
+ const live = new Map(); // session id -> { here, active, replying, online }
9
+
5
10
  const ago = (ms) => {
6
11
  const s = Math.max(0, Math.round((Date.now() - ms) / 1000));
7
12
  if (s < 60) return "just now";
@@ -17,25 +22,57 @@
17
22
  // Matches live() in relay/src/pages.ts.
18
23
  const liveNodes = (s) => {
19
24
  if (s.online === false) return dot("off", "computer offline");
20
- if (s.replying) return dot("busy", "replying");
25
+ if (s.replying && Date.now() - s.replying < REPLYING_MS) return dot("busy", "replying");
21
26
  if (s.here) return dot("on", s.here > 1 ? `${s.here} people here` : "here now");
22
27
  if (s.active) return dot("", `active ${ago(s.active)}`);
23
28
  return dot("", "not opened yet");
24
29
  };
25
30
 
26
- async function refresh() {
27
- if (document.hidden) return;
28
- try {
29
- const res = await fetch("/dashboard.json", { cache: "no-store" });
30
- if (!res.ok) return;
31
- const { sessions } = await res.json();
32
- const rows = [...document.querySelectorAll("[data-session]")];
33
- // A session opened or ended elsewhere: show the page fresh.
34
- if (rows.map((r) => r.dataset.session).sort().join() !== sessions.map((s) => s.id).sort().join()) return location.reload();
35
- for (const s of sessions) rows.find((r) => r.dataset.session === s.id)?.querySelector(".live")?.replaceChildren(...liveNodes(s));
36
- } catch {}
31
+ function paint() {
32
+ for (const row of document.querySelectorAll("[data-session]")) {
33
+ const s = live.get(row.dataset.session);
34
+ if (s) row.querySelector(".live")?.replaceChildren(...liveNodes(s));
35
+ }
36
+ }
37
+
38
+ function apply(computer, m) {
39
+ if (m.t === "changed") return location.reload();
40
+ if (m.t !== "live") return;
41
+ // Times come from parley.im's clock; shift them onto this one.
42
+ const shift = Date.now() - m.now;
43
+ for (const row of document.querySelectorAll(`[data-device="${computer}"]`)) {
44
+ const s = m.sessions[row.dataset.session] ?? {};
45
+ live.set(row.dataset.session, {
46
+ online: m.on,
47
+ here: s.here ?? 0,
48
+ active: s.active ? s.active + shift : null,
49
+ replying: s.replying ? s.replying + shift : null,
50
+ });
51
+ }
52
+ const row = document.querySelector(`[data-computer="${computer}"]`);
53
+ if (row) {
54
+ row.querySelector(".dot").className = `dot${m.on ? " on" : ""}`;
55
+ row.querySelector(".computer-status").textContent = m.on ? `online now${m.version ? ` · parley ${m.version}` : ""}` : "offline";
56
+ }
57
+ paint();
58
+ }
59
+
60
+ function watch(computer, delay = 2000) {
61
+ const ws = new WebSocket(`${location.origin.replace(/^http/, "ws")}/dashboard/live?d=${encodeURIComponent(computer)}`);
62
+ const ping = setInterval(() => ws.readyState === WebSocket.OPEN && ws.send('{"t":"ping"}'), 30_000);
63
+ ws.onopen = () => (delay = 2000);
64
+ ws.onmessage = (event) => {
65
+ try {
66
+ apply(computer, JSON.parse(event.data));
67
+ } catch {}
68
+ };
69
+ ws.onclose = () => {
70
+ clearInterval(ping);
71
+ setTimeout(() => watch(computer, Math.min(delay * 2, 60_000)), delay);
72
+ };
37
73
  }
38
74
 
39
- setInterval(refresh, 4000);
40
- document.addEventListener("visibilitychange", refresh);
75
+ for (const row of document.querySelectorAll("[data-computer]")) watch(row.dataset.computer);
76
+ // "active 3m ago" and the end of "replying" move on with the clock alone.
77
+ setInterval(paint, 5000);
41
78
  })();
package/web/site.js ADDED
@@ -0,0 +1,11 @@
1
+ // parley.im's account menu closes when you click outside it or press Escape.
2
+ (() => {
3
+ const menu = document.querySelector(".account-menu");
4
+ if (!menu) return;
5
+ document.addEventListener("click", (e) => {
6
+ if (menu.open && !menu.contains(e.target)) menu.open = false;
7
+ });
8
+ document.addEventListener("keydown", (e) => {
9
+ if (e.key === "Escape") menu.open = false;
10
+ });
11
+ })();