clay-server 2.16.0 → 2.17.0-beta.10

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.
@@ -241,6 +241,134 @@
241
241
  box-shadow: 0 0 0 1px var(--accent);
242
242
  }
243
243
 
244
+ /* Avatar upload button */
245
+ .profile-avatar-upload .profile-avatar-upload-icon {
246
+ display: flex;
247
+ align-items: center;
248
+ justify-content: center;
249
+ width: 100%;
250
+ height: 100%;
251
+ color: var(--text-muted);
252
+ }
253
+ .profile-avatar-upload .profile-avatar-upload-icon svg {
254
+ width: 24px;
255
+ height: 24px;
256
+ }
257
+ .profile-avatar-upload:hover .profile-avatar-upload-icon {
258
+ color: var(--text);
259
+ }
260
+ .profile-avatar-custom-preview {
261
+ width: 100%;
262
+ height: 100%;
263
+ object-fit: cover;
264
+ border-radius: 6px;
265
+ }
266
+
267
+ /* Avatar positioner overlay */
268
+ .avatar-positioner-overlay {
269
+ position: fixed;
270
+ inset: 0;
271
+ z-index: 10000;
272
+ background: rgba(0,0,0,0.6);
273
+ display: flex;
274
+ align-items: center;
275
+ justify-content: center;
276
+ }
277
+ .avatar-positioner-container {
278
+ background: var(--bg-secondary, #1e1e2e);
279
+ border-radius: 16px;
280
+ padding: 20px;
281
+ display: flex;
282
+ flex-direction: column;
283
+ align-items: center;
284
+ gap: 16px;
285
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
286
+ min-width: 260px;
287
+ position: relative;
288
+ }
289
+ .avatar-positioner-close {
290
+ position: absolute;
291
+ top: 8px;
292
+ right: 8px;
293
+ width: 28px;
294
+ height: 28px;
295
+ border: none;
296
+ background: transparent;
297
+ color: var(--text-muted);
298
+ font-size: 20px;
299
+ cursor: pointer;
300
+ border-radius: 50%;
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ line-height: 1;
305
+ }
306
+ .avatar-positioner-close:hover {
307
+ background: var(--bg-hover, rgba(255,255,255,0.1));
308
+ color: var(--text);
309
+ }
310
+ .avatar-positioner-title {
311
+ font-size: 14px;
312
+ font-weight: 600;
313
+ color: var(--text);
314
+ }
315
+ .avatar-positioner-viewport {
316
+ border-radius: 50%;
317
+ overflow: hidden;
318
+ position: relative;
319
+ cursor: grab;
320
+ border: 2px solid var(--border);
321
+ background: var(--bg);
322
+ touch-action: none;
323
+ }
324
+ .avatar-positioner-viewport:active {
325
+ cursor: grabbing;
326
+ }
327
+ .avatar-positioner-img {
328
+ position: absolute;
329
+ top: 0;
330
+ left: 0;
331
+ pointer-events: none;
332
+ will-change: transform;
333
+ }
334
+ .avatar-positioner-slider-wrap {
335
+ width: 100%;
336
+ padding: 0 8px;
337
+ }
338
+ .avatar-positioner-slider {
339
+ width: 100%;
340
+ accent-color: var(--accent);
341
+ }
342
+ .avatar-positioner-buttons {
343
+ display: flex;
344
+ gap: 8px;
345
+ width: 100%;
346
+ }
347
+ .avatar-positioner-btn {
348
+ flex: 1;
349
+ padding: 8px 0;
350
+ border-radius: 8px;
351
+ border: none;
352
+ cursor: pointer;
353
+ font-size: 13px;
354
+ font-weight: 600;
355
+ }
356
+ .avatar-positioner-btn-cancel {
357
+ background: var(--bg);
358
+ color: var(--text-muted);
359
+ border: 1px solid var(--border);
360
+ }
361
+ .avatar-positioner-btn-cancel:hover {
362
+ background: var(--bg-hover, var(--bg));
363
+ }
364
+ .avatar-positioner-btn-done {
365
+ background: var(--accent);
366
+ color: #fff;
367
+ }
368
+ .avatar-positioner-btn-done:hover {
369
+ filter: brightness(1.1);
370
+ }
371
+
244
372
  /* Color swatch grid */
245
373
  .profile-color-grid {
246
374
  display: flex;
@@ -15,10 +15,6 @@
15
15
 
16
16
  /* --- Update banner --- */
17
17
  .top-bar-update {
18
- position: absolute;
19
- left: 10px;
20
- top: 0;
21
- bottom: 0;
22
18
  display: flex;
23
19
  align-items: center;
24
20
  }
@@ -29,15 +29,18 @@
29
29
  <div id="layout">
30
30
  <!-- === Top Bar (full width, forms reverse-ㄱ with icon strip) === -->
31
31
  <div id="top-bar">
32
- <button id="pwa-install-pill" class="top-bar-install-btn hidden" title="Install app"><i data-lucide="download"></i></button>
33
- <button id="cmd-palette-btn" class="cmd-palette-searchbar" title="Command palette"><i data-lucide="search"></i><span class="cmd-palette-searchbar-text">Search sessions, projects, and commands</span><kbd class="cmd-palette-searchbar-kbd"></kbd></button>
34
- <div id="update-pill-wrap" class="top-bar-update hidden">
35
- <button id="update-pill" class="top-bar-update-btn"><i data-lucide="arrow-up-circle"></i> <span id="update-version"></span> is available. Update now</button>
32
+ <div class="top-bar-left-pills">
33
+ <button id="pwa-install-pill" class="top-bar-install-btn hidden" title="Open as app"><i data-lucide="download"></i> Open as app</button>
34
+ <button id="share-pill" class="top-bar-share-btn" title="Share"><i data-lucide="qr-code"></i> Share</button>
35
+ <div id="update-pill-wrap" class="top-bar-update hidden">
36
+ <button id="update-pill" class="top-bar-update-btn"><i data-lucide="arrow-up-circle"></i> <span id="update-version"></span> is available. Update now</button>
36
37
  <div id="update-popover" class="top-bar-popover">
37
38
  <div class="popover-row"><button id="update-now" class="popover-action popover-action-primary"><i data-lucide="download"></i> Update now</button></div>
38
39
  <div class="popover-row"><div class="popover-label">Or run manually:</div><div class="popover-cmd"><code id="update-manual-cmd">npx clay-server@latest</code><button class="popover-copy" title="Copy"><i data-lucide="copy"></i></button></div></div>
39
40
  </div>
40
41
  </div>
42
+ </div>
43
+ <button id="cmd-palette-btn" class="cmd-palette-searchbar" title="Command palette"><i data-lucide="search"></i><span class="cmd-palette-searchbar-text">Search sessions, projects, and commands</span><kbd class="cmd-palette-searchbar-kbd"></kbd></button>
41
44
  <div class="top-bar-actions">
42
45
  <!-- Pill badges -->
43
46
  <div id="skip-perms-pill" class="top-bar-pill pill-error hidden"><i data-lucide="shield-off"></i> <span>Skip perms</span></div>
@@ -721,6 +724,7 @@
721
724
  <div id="qr-overlay-inner">
722
725
  <div id="qr-canvas"></div>
723
726
  <div id="qr-url"></div>
727
+ <button id="qr-share-btn" class="qr-share-btn hidden"><i data-lucide="share-2"></i> Share</button>
724
728
  </div>
725
729
  </div>
726
730
 
@@ -1414,7 +1418,7 @@
1414
1418
  <div class="mate-intro-step"><span class="mate-intro-step-num">2</span> Have a short interview where your Mate gets to know you</div>
1415
1419
  <div class="mate-intro-step"><span class="mate-intro-step-num">3</span> Start talking</div>
1416
1420
  </div>
1417
- <p class="mate-intro-privacy">Mates run on Claude Code. No separate API keys or external services needed. Your conversations stay on your Clay server and are never stored elsewhere. <span class="mate-intro-experimental">Experimental</span></p>
1421
+ <p class="mate-intro-privacy">Mates run on Claude Code. No separate API keys or external services needed. Your conversations stay on your Clay server and are never stored elsewhere.</p>
1418
1422
  </div>
1419
1423
  </div>
1420
1424
  <!-- Step 1: Relationship -->
@@ -0,0 +1,36 @@
1
+ // Centralized avatar URL builder and style definitions
2
+ // All DiceBear avatar URLs should be constructed through this module.
3
+
4
+ export var AVATAR_STYLES = [
5
+ { id: 'thumbs', name: 'Thumbs' },
6
+ { id: 'bottts', name: 'Bots' },
7
+ { id: 'pixel-art', name: 'Pixel' },
8
+ { id: 'adventurer', name: 'Adventurer' },
9
+ { id: 'micah', name: 'Micah' },
10
+ { id: 'fun-emoji', name: 'Emoji' },
11
+ { id: 'icons', name: 'Icons' },
12
+ ];
13
+
14
+ // Build a DiceBear avatar URL from style, seed, and optional size.
15
+ export function avatarUrl(style, seed, size) {
16
+ var s = encodeURIComponent(seed || 'anonymous');
17
+ return 'https://api.dicebear.com/9.x/' + (style || 'thumbs') + '/svg?seed=' + s + '&size=' + (size || 64);
18
+ }
19
+
20
+ // Build avatar URL for a user object, preferring custom avatar if set.
21
+ export function userAvatarUrl(user, size) {
22
+ if (user && user.avatarCustom) return user.avatarCustom;
23
+ var style = (user && user.avatarStyle) || 'thumbs';
24
+ var seed = (user && (user.avatarSeed || user.username || user.id)) || 'anonymous';
25
+ return avatarUrl(style, seed, size);
26
+ }
27
+
28
+ // Build avatar URL for a mate object, preferring custom avatar if set.
29
+ export function mateAvatarUrl(mate, size) {
30
+ if (!mate) return avatarUrl('bottts', 'mate', size);
31
+ var p = mate.profile || mate;
32
+ if (p.avatarCustom || mate.avatarCustom) return p.avatarCustom || mate.avatarCustom;
33
+ var style = p.avatarStyle || mate.avatarStyle || 'bottts';
34
+ var seed = p.avatarSeed || mate.avatarSeed || mate.id || 'mate';
35
+ return avatarUrl(style, seed, size);
36
+ }
@@ -1,3 +1,4 @@
1
+ import { avatarUrl, userAvatarUrl, mateAvatarUrl } from './avatar.js';
1
2
  import { escapeHtml } from './utils.js';
2
3
  import { refreshIcons } from './icons.js';
3
4
  import { openSearch as openSessionSearch } from './session-search.js';
@@ -274,9 +275,7 @@ function renderHome(filter) {
274
275
  items.push({ type: "user", data: user });
275
276
  var userName = escapeHtml(user.displayName || user.username);
276
277
  var userSub = user.username ? "@" + escapeHtml(user.username) : "";
277
- var uAvatarStyle = user.avatarStyle || "thumbs";
278
- var uAvatarSeed = user.avatarSeed || user.username || user.id;
279
- var uAvatarUrl = "https://api.dicebear.com/9.x/" + uAvatarStyle + "/svg?seed=" + encodeURIComponent(uAvatarSeed) + "&size=28";
278
+ var uAvatarUrl = userAvatarUrl(user, 28);
280
279
  var uAvatarHtml = '<img src="' + uAvatarUrl + '" width="28" height="28" style="border-radius:50%;" alt="">';
281
280
  html += renderItem(flatIndex, uAvatarHtml, userName, userSub, null);
282
281
  flatIndex++;
@@ -298,10 +297,8 @@ function renderHome(filter) {
298
297
  var mp = mate.profile || {};
299
298
  items.push({ type: "mate", data: mate });
300
299
  var mateName = escapeHtml(mp.displayName || mate.name || "Mate");
301
- var avatarStyle = mp.avatarStyle || "bottts";
302
- var avatarSeed = mp.avatarSeed || mate.id;
303
- var avatarUrl = "https://api.dicebear.com/9.x/" + avatarStyle + "/svg?seed=" + encodeURIComponent(avatarSeed) + "&size=28";
304
- var avatarHtml = '<img src="' + avatarUrl + '" width="28" height="28" style="border-radius:50%;" alt="">';
300
+ var mateAvUrl = mateAvatarUrl(mate, 28);
301
+ var avatarHtml = '<img src="' + mateAvUrl + '" width="28" height="28" style="border-radius:50%;" alt="">';
305
302
  html += renderItem(flatIndex, avatarHtml, mateName, null, null);
306
303
  flatIndex++;
307
304
  }
@@ -1,3 +1,4 @@
1
+ import { avatarUrl, mateAvatarUrl } from './avatar.js';
1
2
  import { escapeHtml } from './utils.js';
2
3
  import { iconHtml, refreshIcons } from './icons.js';
3
4
  import { hideKnowledge } from './mate-knowledge.js';
@@ -134,19 +135,17 @@ export function showMateSidebar(mateId, mateData) {
134
135
  // Populate header
135
136
  var profile = mateData.profile || mateData || {};
136
137
  var displayName = profile.displayName || mateData.displayName || mateData.name || "Mate";
137
- var avatarStyle = profile.avatarStyle || "bottts";
138
- var avatarSeed = profile.avatarSeed || mateId;
139
138
 
140
139
  var mateColor = profile.avatarColor || mateData.avatarColor || "#7c3aed";
141
140
 
142
- var avatarUrl = "https://api.dicebear.com/9.x/" + encodeURIComponent(avatarStyle) + "/svg?seed=" + encodeURIComponent(avatarSeed) + "&size=32";
143
- if (avatarEl) avatarEl.src = avatarUrl;
141
+ var mateAvUrl = mateAvatarUrl(mateData, 32);
142
+ if (avatarEl) avatarEl.src = mateAvUrl;
144
143
  if (nameEl) nameEl.textContent = displayName;
145
144
 
146
145
  // Also populate collapsed header info
147
146
  var collapsedAvatar = document.getElementById("mate-collapsed-avatar");
148
147
  var collapsedName = document.getElementById("mate-collapsed-name");
149
- if (collapsedAvatar) collapsedAvatar.src = avatarUrl;
148
+ if (collapsedAvatar) collapsedAvatar.src = mateAvUrl;
150
149
  if (collapsedName) collapsedName.textContent = displayName;
151
150
 
152
151
  // Apply mate color to sidebar
@@ -215,12 +214,10 @@ export function updateMateSidebarProfile(mateData) {
215
214
  if (!columnEl || !mateData) return;
216
215
  var profile = mateData.profile || mateData || {};
217
216
  var displayName = profile.displayName || mateData.displayName || mateData.name || "Mate";
218
- var avatarStyle = profile.avatarStyle || "bottts";
219
- var avatarSeed = profile.avatarSeed || (mateData.id || "mate");
220
217
  var mateColor = profile.avatarColor || mateData.avatarColor || "#7c3aed";
221
218
 
222
219
  if (avatarEl) {
223
- avatarEl.src = "https://api.dicebear.com/9.x/" + encodeURIComponent(avatarStyle) + "/svg?seed=" + encodeURIComponent(avatarSeed) + "&size=32";
220
+ avatarEl.src = mateAvatarUrl(mateData, 32);
224
221
  }
225
222
  // Check if name changed for engrave effect
226
223
  var oldName = nameEl ? nameEl.textContent : "";