@cana-ai/walkie-talkie 0.1.1 β 0.1.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.
- package/README.md +39 -2
- package/dist/server.js +11 -9
- package/package.json +1 -1
- package/public/app.js +30 -5
- package/public/cana-logo.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +52 -7
- package/public/styles.css +158 -41
package/README.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://cana.build">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/Colate-Ltd/cana-walkie-talkie/main/public/cana-logo.png" alt="Cana" width="72" />
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
<h1 align="center">Cana Walkie-Talkie</h1>
|
|
8
|
+
|
|
9
|
+
<p align="center"><b>A real-time message bus for humanβagent and agentβagent coordination.</b></p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@cana-ai/walkie-talkie"><img src="https://img.shields.io/npm/v/@cana-ai/walkie-talkie?color=6366f1&label=npm" alt="npm version" /></a>
|
|
13
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-6366f1" alt="License: MIT" /></a>
|
|
14
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%A522.5-43853d" alt="Node β₯ 22.5" /></a>
|
|
15
|
+
Β·
|
|
16
|
+
<a href="https://cana.build">cana.build</a> Β·
|
|
17
|
+
<a href="https://cana.build/docs">Docs</a>
|
|
18
|
+
</p>
|
|
4
19
|
|
|
5
20
|
Spin up a WebSocket bus where people and AI agents (Claude Code, your own
|
|
6
21
|
scripts, anything that speaks WebSocket) join shared **channels**, exchange
|
|
@@ -43,6 +58,28 @@ push, and deep agent-platform integration (see the table below).
|
|
|
43
58
|
|
|
44
59
|
Requires **Node β₯ 22.5** (for built-in `node:sqlite`).
|
|
45
60
|
|
|
61
|
+
### Run instantly with `npx` β no clone, no install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx @cana-ai/walkie-talkie
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
That boots the bus **and** the bundled dashboard on **http://localhost:8787** and
|
|
68
|
+
prints a generated **admin token** on first run. Configure it inline with env vars:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
PORT=9000 ADMIN_TOKEN=$(openssl rand -hex 32) npx @cana-ai/walkie-talkie
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Pin a version with `npx @cana-ai/walkie-talkie@latest`, or install it globally:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm i -g @cana-ai/walkie-talkie
|
|
78
|
+
cana-walkie-talkie # same binary, now on your PATH
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Or clone for development
|
|
82
|
+
|
|
46
83
|
```bash
|
|
47
84
|
git clone https://github.com/Colate-Ltd/cana-walkie-talkie.git
|
|
48
85
|
cd cana-walkie-talkie
|
package/dist/server.js
CHANGED
|
@@ -20,22 +20,24 @@ const server = http.createServer(app);
|
|
|
20
20
|
attachWebSocket(server);
|
|
21
21
|
// Brand banner β Cana by Colate (https://cana.build)
|
|
22
22
|
const c = {
|
|
23
|
-
p1: "\x1b[38;5;147m", // light indigo
|
|
24
|
-
p2: "\x1b[38;5;141m", // cana purple
|
|
25
|
-
p3: "\x1b[38;5;99m", // deep violet
|
|
26
23
|
link: "\x1b[38;5;81m", // cyan link
|
|
27
24
|
bold: "\x1b[1m",
|
|
28
25
|
dim: "\x1b[2m",
|
|
29
26
|
reset: "\x1b[0m",
|
|
30
27
|
};
|
|
28
|
+
// "CANA.BUILD" wordmark with a topβbottom indigoβviolet gradient.
|
|
29
|
+
const grad = ["\x1b[38;5;189m", "\x1b[38;5;147m", "\x1b[38;5;141m", "\x1b[38;5;135m", "\x1b[38;5;99m", "\x1b[38;5;99m"];
|
|
30
|
+
const art = [
|
|
31
|
+
" βββββββ ββββββ ββββ βββ ββββββ βββββββ βββ βββββββββ βββββββ ",
|
|
32
|
+
"βββββββββββββββββββββ βββββββββββ βββββββββββ βββββββββ ββββββββ",
|
|
33
|
+
"βββ ββββββββββββββ βββββββββββ βββββββββββ βββββββββ βββ βββ",
|
|
34
|
+
"βββ ββββββββββββββββββββββββββ βββββββββββ βββββββββ βββ βββ",
|
|
35
|
+
"βββββββββββ ββββββ βββββββββ ββββββββββββββββββββββββββββββββββββββββββ",
|
|
36
|
+
" ββββββββββ ββββββ ββββββββ βββββββββββββ βββββββ ββββββββββββββββββ ",
|
|
37
|
+
];
|
|
31
38
|
const banner = [
|
|
32
39
|
"",
|
|
33
|
-
|
|
34
|
-
`${c.p1} βββββββββββββββββββββ βββββββββββ${c.reset}`,
|
|
35
|
-
`${c.p2} βββ ββββββββββββββ βββββββββββ${c.reset}`,
|
|
36
|
-
`${c.p2} βββ ββββββββββββββββββββββββββ${c.reset}`,
|
|
37
|
-
`${c.p3} βββββββββββ ββββββ βββββββββ βββ${c.reset}`,
|
|
38
|
-
`${c.p3} ββββββββββ ββββββ ββββββββ βββ${c.reset}`,
|
|
40
|
+
...art.map((line, i) => ` ${grad[i]}${c.bold}${line}${c.reset}`),
|
|
39
41
|
"",
|
|
40
42
|
` ${c.bold}Walkie-Talkie${c.reset} ${c.dim}Β· a shared workspace for teams and AI agents${c.reset}`,
|
|
41
43
|
` ${c.link}${c.bold}https://cana.build${c.reset} ${c.dim}β by Colate${c.reset}`,
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -10,14 +10,42 @@ const api = (path, opts = {}) =>
|
|
|
10
10
|
headers: { "content-type": "application/json", authorization: `Bearer ${adminToken}`, ...(opts.headers || {}) },
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
+
// Snapshot the auth-aware empty states so we can restore them later.
|
|
14
|
+
const emptyHTML = $("messages").innerHTML;
|
|
15
|
+
|
|
16
|
+
// ββ Auth state βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
17
|
+
function setAuthState() {
|
|
18
|
+
document.body.dataset.auth = adminToken ? "in" : "out";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resetViewer() {
|
|
22
|
+
closeWs();
|
|
23
|
+
activeChannel = null;
|
|
24
|
+
$("viewerHead").classList.add("hidden");
|
|
25
|
+
$("composer").classList.add("hidden");
|
|
26
|
+
$("messages").innerHTML = emptyHTML;
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
// ββ Admin token ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
14
30
|
$("adminToken").value = adminToken;
|
|
15
31
|
$("saveToken").onclick = () => {
|
|
16
32
|
adminToken = $("adminToken").value.trim();
|
|
33
|
+
if (!adminToken) return;
|
|
17
34
|
localStorage.setItem("wt-admin-token", adminToken);
|
|
35
|
+
setAuthState();
|
|
36
|
+
resetViewer();
|
|
18
37
|
loadChannels();
|
|
19
38
|
};
|
|
20
39
|
|
|
40
|
+
$("logout").onclick = () => {
|
|
41
|
+
adminToken = "";
|
|
42
|
+
localStorage.removeItem("wt-admin-token");
|
|
43
|
+
$("adminToken").value = "";
|
|
44
|
+
setAuthState();
|
|
45
|
+
renderChannels([]);
|
|
46
|
+
resetViewer();
|
|
47
|
+
};
|
|
48
|
+
|
|
21
49
|
// ββ Channels βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
22
50
|
async function loadChannels() {
|
|
23
51
|
const res = await api("/channels");
|
|
@@ -50,11 +78,7 @@ $("newChannel").onclick = async () => {
|
|
|
50
78
|
$("killChannel").onclick = async () => {
|
|
51
79
|
if (!activeChannel || !confirm(`Close #${activeChannel.name}?`)) return;
|
|
52
80
|
await api(`/channels/${activeChannel.id}/kill`, { method: "POST" });
|
|
53
|
-
|
|
54
|
-
activeChannel = null;
|
|
55
|
-
$("viewerHead").classList.add("hidden");
|
|
56
|
-
$("composer").classList.add("hidden");
|
|
57
|
-
$("messages").innerHTML = `<div class="empty">Channel closed.</div>`;
|
|
81
|
+
resetViewer();
|
|
58
82
|
loadChannels();
|
|
59
83
|
};
|
|
60
84
|
|
|
@@ -140,5 +164,6 @@ $("tkCreate").onclick = async () => {
|
|
|
140
164
|
|
|
141
165
|
function esc(s) { return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); }
|
|
142
166
|
|
|
167
|
+
setAuthState();
|
|
143
168
|
if (adminToken) loadChannels();
|
|
144
169
|
setInterval(() => { if (adminToken) loadChannels(); }, 10000);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/public/index.html
CHANGED
|
@@ -4,15 +4,41 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>Cana Walkie-Talkie</title>
|
|
7
|
+
<link rel="icon" href="/favicon.ico" sizes="any" />
|
|
8
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
|
9
|
+
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
|
7
10
|
<link rel="stylesheet" href="/styles.css" />
|
|
8
11
|
</head>
|
|
9
|
-
<body>
|
|
12
|
+
<body data-auth="out">
|
|
10
13
|
<header class="topbar">
|
|
11
|
-
<div class="brand"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
<div class="brand">
|
|
15
|
+
<a class="logo" href="https://cana.build" target="_blank" rel="noopener" title="cana.build">
|
|
16
|
+
<img class="logo-mark" src="/cana-logo.png" alt="Cana" width="22" height="22" />
|
|
17
|
+
<span class="logo-text"><b>cana</b><span>.build</span></span>
|
|
18
|
+
</a>
|
|
19
|
+
<span class="brand-sep"></span>
|
|
20
|
+
<span class="product">Walkie-Talkie</span>
|
|
21
|
+
<span class="tag">open-source core</span>
|
|
15
22
|
</div>
|
|
23
|
+
|
|
24
|
+
<nav class="topnav">
|
|
25
|
+
<a class="navlink" href="https://cana.build/docs" target="_blank" rel="noopener">Docs β</a>
|
|
26
|
+
<a class="navlink" href="https://cana.build" target="_blank" rel="noopener">cana.build β</a>
|
|
27
|
+
<a class="navlink" href="https://github.com/Colate-Ltd/cana-walkie-talkie" target="_blank" rel="noopener">GitHub β</a>
|
|
28
|
+
|
|
29
|
+
<!-- Logged out: token entry -->
|
|
30
|
+
<div class="admin auth-out">
|
|
31
|
+
<input id="adminToken" type="password" placeholder="Admin tokenβ¦" autocomplete="off" />
|
|
32
|
+
<button id="saveToken">Connect</button>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- Logged in: status + logout -->
|
|
36
|
+
<div class="admin auth-in">
|
|
37
|
+
<span class="status-dot" title="Authenticated">β</span>
|
|
38
|
+
<span class="status-text">Connected</span>
|
|
39
|
+
<button id="logout" class="ghost">Log out</button>
|
|
40
|
+
</div>
|
|
41
|
+
</nav>
|
|
16
42
|
</header>
|
|
17
43
|
|
|
18
44
|
<main class="layout">
|
|
@@ -22,6 +48,11 @@
|
|
|
22
48
|
<button id="newChannel" class="ghost">+ New</button>
|
|
23
49
|
</div>
|
|
24
50
|
<ul id="channelList" class="channel-list"></ul>
|
|
51
|
+
<div class="sidebar-foot">
|
|
52
|
+
<a href="https://cana.build/docs" target="_blank" rel="noopener">Documentation</a>
|
|
53
|
+
<span>Β·</span>
|
|
54
|
+
<a href="https://cana.build" target="_blank" rel="noopener">Powered by cana.build</a>
|
|
55
|
+
</div>
|
|
25
56
|
</aside>
|
|
26
57
|
|
|
27
58
|
<section class="viewer">
|
|
@@ -37,12 +68,26 @@
|
|
|
37
68
|
</div>
|
|
38
69
|
|
|
39
70
|
<div id="messages" class="messages">
|
|
40
|
-
|
|
71
|
+
<!-- Logged out -->
|
|
72
|
+
<div class="empty auth-out">
|
|
73
|
+
<div class="empty-card">
|
|
74
|
+
<div class="empty-logo"><img src="/cana-logo.png" alt="Cana" width="40" height="40" /><span class="empty-word"><b>cana</b><span>.build</span></span></div>
|
|
75
|
+
<h3>Welcome to Walkie-Talkie</h3>
|
|
76
|
+
<p>A real-time message bus for humanβagent and agentβagent coordination.</p>
|
|
77
|
+
<p class="muted">Enter your admin token above to get started.</p>
|
|
78
|
+
<div class="empty-links">
|
|
79
|
+
<a href="https://cana.build/docs" target="_blank" rel="noopener">Read the docs β</a>
|
|
80
|
+
<a href="https://github.com/Colate-Ltd/cana-walkie-talkie" target="_blank" rel="noopener">View on GitHub β</a>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<!-- Logged in, no channel selected -->
|
|
85
|
+
<div class="empty auth-in">Pick a channel, or create one to get started.</div>
|
|
41
86
|
</div>
|
|
42
87
|
|
|
43
88
|
<form id="composer" class="composer hidden">
|
|
44
89
|
<input id="composeTo" class="to" placeholder="@handle (optional)" />
|
|
45
|
-
<input id="composeText" class="text" placeholder="Messageβ¦" autocomplete="off" />
|
|
90
|
+
<input id="composeText" class="text" placeholder="Message the channelβ¦" autocomplete="off" />
|
|
46
91
|
<label class="priv"><input id="composePrivate" type="checkbox" /> private</label>
|
|
47
92
|
<button type="submit">Send</button>
|
|
48
93
|
</form>
|
package/public/styles.css
CHANGED
|
@@ -1,52 +1,169 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
--bg: #
|
|
3
|
-
--
|
|
2
|
+
--bg: #f7f8fc;
|
|
3
|
+
--panel: #ffffff;
|
|
4
|
+
--panel2: #f3f4f9;
|
|
5
|
+
--line: #e6e8f0;
|
|
6
|
+
--line2: #eef0f6;
|
|
7
|
+
--text: #1a1d29;
|
|
8
|
+
--muted: #6b7280;
|
|
9
|
+
--accent: #6366f1;
|
|
10
|
+
--accent-weak: #eef0fe;
|
|
11
|
+
--accent-grad: linear-gradient(135deg, #818cf8 0%, #6366f1 55%, #7c3aed 100%);
|
|
12
|
+
--danger: #ef4444;
|
|
13
|
+
--shadow: 0 1px 2px rgba(16, 24, 40, .04), 0 1px 3px rgba(16, 24, 40, .06);
|
|
14
|
+
--radius: 10px;
|
|
4
15
|
}
|
|
5
16
|
* { box-sizing: border-box; }
|
|
6
|
-
body {
|
|
7
|
-
|
|
8
|
-
.
|
|
9
|
-
|
|
10
|
-
|
|
17
|
+
body {
|
|
18
|
+
margin: 0;
|
|
19
|
+
font: 14px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
20
|
+
background: var(--bg);
|
|
21
|
+
color: var(--text);
|
|
22
|
+
-webkit-font-smoothing: antialiased;
|
|
23
|
+
overflow-x: hidden;
|
|
24
|
+
}
|
|
25
|
+
a { color: var(--accent); text-decoration: none; }
|
|
26
|
+
a:hover { text-decoration: underline; }
|
|
27
|
+
|
|
28
|
+
/* ββ Topbar βββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
29
|
+
.topbar {
|
|
30
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
31
|
+
height: 56px; padding: 0 20px;
|
|
32
|
+
background: var(--panel); border-bottom: 1px solid var(--line);
|
|
33
|
+
}
|
|
34
|
+
.brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
|
35
|
+
.logo { display: inline-flex; align-items: center; gap: 8px; color: var(--text); }
|
|
36
|
+
.logo:hover { text-decoration: none; }
|
|
37
|
+
.logo-mark { width: 22px; height: 22px; object-fit: contain; display: block; }
|
|
38
|
+
.logo-text { font-size: 17px; letter-spacing: -.01em; }
|
|
39
|
+
.logo-text b { font-weight: 700; }
|
|
40
|
+
.logo-text span { color: var(--accent); font-weight: 600; }
|
|
41
|
+
.brand-sep { width: 1px; height: 22px; background: var(--line); }
|
|
42
|
+
.product { font-weight: 600; font-size: 15px; }
|
|
43
|
+
.tag {
|
|
44
|
+
font-size: 11px; color: var(--accent); background: var(--accent-weak);
|
|
45
|
+
border: 1px solid #dfe1fd; padding: 2px 8px; border-radius: 999px; font-weight: 600;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.topnav { display: flex; align-items: center; gap: 6px; }
|
|
49
|
+
.navlink { color: var(--muted); font-size: 13px; padding: 6px 10px; border-radius: 8px; font-weight: 500; }
|
|
50
|
+
.navlink:hover { color: var(--text); background: var(--panel2); text-decoration: none; }
|
|
51
|
+
|
|
52
|
+
.admin { display: flex; align-items: center; gap: 8px; margin-left: 6px; padding-left: 10px; border-left: 1px solid var(--line); }
|
|
53
|
+
.status-dot { color: #22c55e; font-size: 10px; }
|
|
54
|
+
.status-text { font-size: 13px; color: var(--muted); font-weight: 500; }
|
|
55
|
+
|
|
56
|
+
/* Auth visibility β toggled by body[data-auth] */
|
|
57
|
+
body[data-auth="out"] .auth-in { display: none !important; }
|
|
58
|
+
body[data-auth="in"] .auth-out { display: none !important; }
|
|
59
|
+
|
|
60
|
+
/* ββ Controls βββββββββββββββββββββββββββββββββββββββββββββ */
|
|
11
61
|
input, select, button { font: inherit; }
|
|
12
|
-
input, select {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
62
|
+
input, select {
|
|
63
|
+
background: var(--panel); border: 1px solid var(--line); color: var(--text);
|
|
64
|
+
padding: 8px 11px; border-radius: 9px; outline: none; transition: border-color .12s, box-shadow .12s;
|
|
65
|
+
}
|
|
66
|
+
input:focus, select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak); }
|
|
67
|
+
input::placeholder { color: #9aa1b1; }
|
|
68
|
+
button {
|
|
69
|
+
background: var(--accent); color: #fff; border: none; padding: 8px 14px;
|
|
70
|
+
border-radius: 9px; cursor: pointer; font-weight: 600; transition: filter .12s, background .12s;
|
|
71
|
+
}
|
|
72
|
+
button:hover { filter: brightness(1.06); }
|
|
73
|
+
button.ghost { background: var(--panel); border: 1px solid var(--line); color: var(--text); font-weight: 500; }
|
|
74
|
+
button.ghost:hover { background: var(--panel2); filter: none; }
|
|
75
|
+
button.danger { color: var(--danger); border-color: #f3c4c4; background: var(--panel); }
|
|
76
|
+
button.danger:hover { background: #fef2f2; }
|
|
77
|
+
|
|
78
|
+
/* ββ Layout βββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
79
|
+
.layout { display: grid; grid-template-columns: 280px 1fr; height: calc(100vh - 56px); }
|
|
80
|
+
.sidebar { display: flex; flex-direction: column; border-right: 1px solid var(--line); background: var(--panel); }
|
|
81
|
+
.sidebar-head { display: flex; align-items: center; justify-content: space-between; padding: 16px 16px 10px; }
|
|
82
|
+
.sidebar-head h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin: 0; font-weight: 700; }
|
|
83
|
+
.sidebar-head .ghost { padding: 5px 10px; font-size: 13px; }
|
|
84
|
+
.channel-list { list-style: none; margin: 0; padding: 0 8px; flex: 1; overflow-y: auto; }
|
|
85
|
+
.channel-list li { padding: 10px 12px; border-radius: 9px; cursor: pointer; border: 1px solid transparent; transition: background .1s; }
|
|
22
86
|
.channel-list li:hover { background: var(--panel2); }
|
|
23
|
-
.channel-list li.active { background: var(--
|
|
87
|
+
.channel-list li.active { background: var(--accent-weak); border-color: #dfe1fd; }
|
|
24
88
|
.channel-list .cname { font-weight: 600; }
|
|
25
|
-
.channel-list .cmeta { font-size: 12px; color: var(--muted); }
|
|
26
|
-
.
|
|
27
|
-
.
|
|
89
|
+
.channel-list .cmeta { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
|
90
|
+
.sidebar-foot { padding: 12px 16px; border-top: 1px solid var(--line2); font-size: 12px; color: var(--muted); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
|
91
|
+
.sidebar-foot a { color: var(--muted); }
|
|
92
|
+
.sidebar-foot a:hover { color: var(--accent); }
|
|
93
|
+
|
|
94
|
+
/* ββ Viewer βββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
95
|
+
.viewer { display: flex; flex-direction: column; min-width: 0; background: var(--bg); }
|
|
96
|
+
.viewer-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 20px; border-bottom: 1px solid var(--line); background: var(--panel); }
|
|
28
97
|
.viewer-head h2 { margin: 0; font-size: 16px; }
|
|
29
98
|
.viewer-actions { display: flex; gap: 8px; }
|
|
30
|
-
.messages { flex: 1; overflow-y: auto; padding:
|
|
31
|
-
.empty { color: var(--muted); margin: auto; }
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.
|
|
35
|
-
.
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
.
|
|
99
|
+
.messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 10px; }
|
|
100
|
+
.empty { color: var(--muted); margin: auto; text-align: center; }
|
|
101
|
+
|
|
102
|
+
/* Welcome card (logged out) */
|
|
103
|
+
.empty-card { background: var(--panel); border: 1px solid var(--line); border-radius: 16px; padding: 32px 36px; max-width: 440px; box-shadow: var(--shadow); }
|
|
104
|
+
.empty-logo { font-size: 22px; margin-bottom: 14px; display: flex; align-items: center; justify-content: center; gap: 8px; }
|
|
105
|
+
.empty-logo img { width: 40px; height: 40px; object-fit: contain; }
|
|
106
|
+
.empty-word b { font-weight: 700; color: var(--text); }
|
|
107
|
+
.empty-word > span { color: var(--accent); font-weight: 600; }
|
|
108
|
+
.empty-card h3 { margin: 0 0 8px; font-size: 18px; color: var(--text); }
|
|
109
|
+
.empty-card p { margin: 6px 0; }
|
|
110
|
+
.empty-links { margin-top: 18px; display: flex; gap: 16px; justify-content: center; }
|
|
111
|
+
|
|
112
|
+
/* Messages */
|
|
113
|
+
.msg { padding: 9px 13px; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; max-width: 72%; box-shadow: var(--shadow); }
|
|
114
|
+
.msg.me { align-self: flex-end; background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
115
|
+
.msg.me .from { color: #e0e1ff; }
|
|
116
|
+
.msg.system { align-self: center; background: transparent; border: none; box-shadow: none; color: var(--muted); font-size: 12px; }
|
|
117
|
+
.msg .from { font-size: 12px; color: var(--accent); font-weight: 600; margin-bottom: 2px; }
|
|
118
|
+
.msg .pill { font-size: 10px; color: var(--muted); border: 1px solid var(--line); border-radius: 999px; padding: 1px 6px; margin-left: 6px; }
|
|
119
|
+
.msg.me .pill { color: #e0e1ff; border-color: rgba(255,255,255,.4); }
|
|
120
|
+
|
|
121
|
+
/* Composer */
|
|
122
|
+
.composer { display: flex; gap: 8px; padding: 14px 20px; border-top: 1px solid var(--line); background: var(--panel); }
|
|
123
|
+
.composer .to { width: 170px; }
|
|
39
124
|
.composer .text { flex: 1; }
|
|
40
|
-
.composer .priv { display: flex; align-items: center; gap:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.modal
|
|
44
|
-
.modal-card
|
|
45
|
-
.modal-card
|
|
46
|
-
.modal-card
|
|
47
|
-
.modal-card
|
|
48
|
-
.modal-
|
|
49
|
-
.
|
|
50
|
-
.
|
|
125
|
+
.composer .priv { display: flex; align-items: center; gap: 5px; color: var(--muted); font-size: 12px; white-space: nowrap; }
|
|
126
|
+
|
|
127
|
+
/* ββ Modal ββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
128
|
+
.modal { position: fixed; inset: 0; background: rgba(16, 24, 40, .45); display: flex; align-items: center; justify-content: center; backdrop-filter: blur(2px); }
|
|
129
|
+
.modal-card { background: var(--panel); border: 1px solid var(--line); border-radius: 16px; padding: 24px; width: 440px; max-width: 92vw; box-shadow: 0 20px 50px rgba(16, 24, 40, .2); }
|
|
130
|
+
.modal-card h3 { margin: 0 0 14px; }
|
|
131
|
+
.modal-card label { display: block; margin: 12px 0; font-size: 13px; color: var(--muted); }
|
|
132
|
+
.modal-card input[type=text], .modal-card input:not([type]), .modal-card select { width: 100%; margin-top: 5px; color: var(--text); }
|
|
133
|
+
.modal-card fieldset { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; }
|
|
134
|
+
.modal-card legend { font-size: 12px; color: var(--muted); padding: 0 6px; }
|
|
135
|
+
.modal-card fieldset label { display: inline-flex; align-items: center; gap: 6px; margin: 0 16px 0 0; color: var(--text); }
|
|
136
|
+
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
|
|
137
|
+
.tk-result { background: var(--panel2); border: 1px solid var(--line); border-radius: 12px; padding: 14px; margin-top: 14px; }
|
|
138
|
+
.tk-result code { display: block; word-break: break-all; background: #0f1117; color: #e6e8ee; padding: 9px 11px; border-radius: 8px; margin: 6px 0; font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
139
|
+
|
|
51
140
|
.muted { color: var(--muted); font-size: 12px; }
|
|
52
141
|
.hidden { display: none !important; }
|
|
142
|
+
|
|
143
|
+
/* ββ Responsive βββββββββββββββββββββββββββββββββββββββββββ */
|
|
144
|
+
@media (max-width: 900px) {
|
|
145
|
+
.topbar { height: auto; flex-wrap: wrap; gap: 10px; padding: 10px 14px; }
|
|
146
|
+
.brand { flex: 1 1 auto; }
|
|
147
|
+
.topnav { flex: 1 1 100%; flex-wrap: wrap; justify-content: flex-start; gap: 4px; }
|
|
148
|
+
.admin { margin-left: auto; }
|
|
149
|
+
.admin.auth-out { flex: 1 1 100%; margin-left: 0; padding-left: 0; border-left: none; }
|
|
150
|
+
.admin.auth-out #adminToken { flex: 1 1 0; width: 0; min-width: 0; }
|
|
151
|
+
#saveToken { flex: 0 0 auto; }
|
|
152
|
+
.layout { grid-template-columns: 1fr; height: auto; min-height: calc(100vh - 56px); }
|
|
153
|
+
.sidebar { border-right: none; border-bottom: 1px solid var(--line); max-height: 38vh; }
|
|
154
|
+
.channel-list { max-height: 26vh; }
|
|
155
|
+
.viewer { min-height: 60vh; }
|
|
156
|
+
.msg { max-width: 88%; }
|
|
157
|
+
.empty { width: 100%; margin: auto 0; }
|
|
158
|
+
.empty-card { padding: 24px 20px; margin: 0; width: 100%; max-width: 100%; }
|
|
159
|
+
.empty-card p { overflow-wrap: anywhere; }
|
|
160
|
+
.empty-links { flex-wrap: wrap; }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@media (max-width: 560px) {
|
|
164
|
+
.product, .tag { display: none; }
|
|
165
|
+
.navlink { padding: 6px 8px; font-size: 12px; }
|
|
166
|
+
.composer { flex-wrap: wrap; }
|
|
167
|
+
.composer .to { width: 100%; }
|
|
168
|
+
.composer .text { flex: 1 1 100%; }
|
|
169
|
+
}
|