clay-server 3.8.0-beta.1 → 3.8.0-beta.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.
Files changed (40) hide show
  1. package/lib/builtin-mates.js +14 -14
  2. package/lib/mates.js +2 -6
  3. package/lib/project-mate-interaction.js +7 -7
  4. package/lib/project-session-pair.js +84 -4
  5. package/lib/project-sessions.js +1 -1
  6. package/lib/project-user-mention.js +4 -4
  7. package/lib/project-worker-proposal.js +1 -1
  8. package/lib/project.js +4 -3
  9. package/lib/public/css/avatar-imprints.css +46 -0
  10. package/lib/public/css/icon-strip.css +3 -3
  11. package/lib/public/css/pane.css +1 -7
  12. package/lib/public/css/profile.css +123 -95
  13. package/lib/public/css/session-actions.css +40 -9
  14. package/lib/public/css/user-settings.css +1 -1
  15. package/lib/public/index.html +2 -2
  16. package/lib/public/modules/app-cursors.js +2 -2
  17. package/lib/public/modules/app-dm.js +1 -8
  18. package/lib/public/modules/app-messages.js +2 -2
  19. package/lib/public/modules/app-projects.js +7 -6
  20. package/lib/public/modules/app-rate-limit.js +0 -1
  21. package/lib/public/modules/app-rendering.js +0 -3
  22. package/lib/public/modules/avatar-imprint.js +134 -0
  23. package/lib/public/modules/avatar.js +25 -19
  24. package/lib/public/modules/debate.js +10 -5
  25. package/lib/public/modules/mate-sidebar.js +2 -2
  26. package/lib/public/modules/mention.js +26 -16
  27. package/lib/public/modules/profile.js +73 -190
  28. package/lib/public/modules/session-actions.js +5 -3
  29. package/lib/public/modules/sidebar-mates.js +5 -4
  30. package/lib/public/modules/sidebar-sessions.js +1 -1
  31. package/lib/public/modules/split-pair-ui.js +2 -8
  32. package/lib/public/style.css +1 -0
  33. package/lib/server-auth.js +2 -2
  34. package/lib/server-dm.js +8 -8
  35. package/lib/server-mates.js +3 -3
  36. package/lib/server-settings.js +2 -2
  37. package/lib/server.js +5 -4
  38. package/lib/session-pair-mcp-server.js +16 -1
  39. package/lib/users.js +4 -4
  40. package/package.json +1 -1
@@ -18,13 +18,16 @@
18
18
  to { opacity: 1; transform: translateY(0); }
19
19
  }
20
20
 
21
- /* Banner */
21
+ /* Identity header */
22
22
  .profile-banner {
23
23
  height: 52px;
24
24
  position: relative;
25
25
  transition: background 0.2s ease;
26
26
  border-radius: 14px 14px 0 0;
27
27
  z-index: 1;
28
+ background: var(--bg);
29
+ border-top: 3px solid var(--profile-accent, #5857fc);
30
+ border-bottom: 1px solid var(--border);
28
31
  }
29
32
 
30
33
  .profile-close-btn {
@@ -34,8 +37,8 @@
34
37
  width: 24px;
35
38
  height: 24px;
36
39
  border: none;
37
- background: rgba(0, 0, 0, 0.3);
38
- color: #fff;
40
+ background: var(--bg-alt);
41
+ color: var(--text-muted);
39
42
  border-radius: 50%;
40
43
  font-size: 16px;
41
44
  line-height: 1;
@@ -47,7 +50,8 @@
47
50
  }
48
51
 
49
52
  .profile-close-btn:hover {
50
- background: rgba(0, 0, 0, 0.5);
53
+ background: var(--bg-hover, var(--bg-alt));
54
+ color: var(--text);
51
55
  }
52
56
 
53
57
  /* Avatar row (overlaps banner) */
@@ -65,7 +69,7 @@
65
69
  .profile-popover-avatar {
66
70
  width: 60px;
67
71
  height: 60px;
68
- border-radius: 50%;
72
+ border-radius: 17px;
69
73
  display: flex;
70
74
  align-items: center;
71
75
  justify-content: center;
@@ -79,30 +83,7 @@
79
83
  width: 100%;
80
84
  height: 100%;
81
85
  object-fit: cover;
82
- border-radius: 50%;
83
- }
84
-
85
- /* Shuffle button next to Avatar label */
86
- .profile-shuffle-btn {
87
- background: none;
88
- border: none;
89
- padding: 0;
90
- margin-left: 4px;
91
- cursor: pointer;
92
- color: var(--text-muted);
93
- vertical-align: middle;
94
- transition: color 0.15s;
95
- display: inline-flex;
96
- align-items: center;
97
- }
98
-
99
- .profile-shuffle-btn .lucide {
100
- width: 12px;
101
- height: 12px;
102
- }
103
-
104
- .profile-shuffle-btn:hover {
105
- color: var(--text);
86
+ border-radius: 14px;
106
87
  }
107
88
 
108
89
  .profile-name-display {
@@ -202,66 +183,140 @@
202
183
  border-color: var(--accent);
203
184
  }
204
185
 
205
- /* Avatar style grid */
206
- .profile-avatar-grid {
207
- display: grid;
208
- grid-template-columns: repeat(4, 1fr);
209
- gap: 6px;
186
+ /* Clay Imprint identity control */
187
+ .profile-imprint-panel {
188
+ border: 1px solid var(--border);
189
+ border-radius: 12px;
190
+ background: var(--bg);
191
+ padding: 9px;
210
192
  }
211
193
 
212
- .profile-avatar-option {
213
- width: 100%;
214
- aspect-ratio: 1;
215
- border-radius: 10px;
216
- border: 2px solid var(--border);
217
- background: var(--bg);
218
- cursor: pointer;
219
- padding: 3px;
220
- overflow: hidden;
194
+ .profile-imprint-preview {
221
195
  display: flex;
222
196
  align-items: center;
223
- justify-content: center;
224
- transition: border-color 0.15s, transform 0.1s;
197
+ gap: 10px;
198
+ min-height: 54px;
225
199
  }
226
200
 
227
- .profile-avatar-option img {
228
- width: 100%;
229
- height: 100%;
230
- object-fit: contain;
231
- border-radius: 6px;
201
+ .profile-imprint-preview img {
202
+ width: 50px;
203
+ height: 50px;
204
+ border-radius: 13px;
205
+ flex: 0 0 auto;
232
206
  }
233
207
 
234
- .profile-avatar-option:hover {
235
- border-color: var(--text-muted);
236
- transform: scale(1.05);
208
+ .profile-imprint-preview span {
209
+ display: flex;
210
+ flex-direction: column;
211
+ gap: 2px;
212
+ min-width: 0;
237
213
  }
238
214
 
239
- .profile-avatar-option.profile-avatar-option-active {
240
- border-color: var(--accent);
241
- box-shadow: 0 0 0 1px var(--accent);
215
+ .profile-imprint-preview strong {
216
+ color: var(--text);
217
+ font-size: 12px;
218
+ font-weight: 650;
219
+ }
220
+
221
+ .profile-imprint-preview small {
222
+ color: var(--text-dimmer);
223
+ font-size: 10px;
224
+ }
225
+
226
+ .profile-imprint-actions {
227
+ display: grid;
228
+ grid-template-columns: 1fr 1fr;
229
+ gap: 5px;
230
+ margin-top: 8px;
242
231
  }
243
232
 
244
- /* Avatar upload button */
245
- .profile-avatar-upload .profile-avatar-upload-icon {
233
+ .profile-imprint-actions button {
234
+ min-width: 0;
235
+ height: 28px;
236
+ padding: 0 8px;
237
+ border: 1px solid var(--border);
238
+ border-radius: 7px;
239
+ background: var(--bg-alt);
240
+ color: var(--text-muted);
241
+ cursor: pointer;
242
+ font-size: 10px;
243
+ font-weight: 600;
244
+ white-space: nowrap;
246
245
  display: flex;
247
246
  align-items: center;
248
247
  justify-content: center;
249
- width: 100%;
250
- height: 100%;
251
- color: var(--text-muted);
248
+ gap: 4px;
249
+ transition: border-color 0.15s, color 0.15s, background 0.15s;
252
250
  }
253
- .profile-avatar-upload .profile-avatar-upload-icon svg {
254
- width: 24px;
255
- height: 24px;
251
+
252
+ .profile-imprint-actions button:hover {
253
+ color: var(--text);
254
+ border-color: var(--text-muted);
256
255
  }
257
- .profile-avatar-upload:hover .profile-avatar-upload-icon {
256
+
257
+ .profile-imprint-actions button.is-active {
258
258
  color: var(--text);
259
+ border-color: var(--accent);
260
+ box-shadow: inset 0 0 0 1px var(--accent);
261
+ }
262
+
263
+ .profile-imprint-actions button .lucide {
264
+ width: 11px;
265
+ height: 11px;
266
+ }
267
+
268
+ .profile-avatar-upload {
269
+ grid-column: 1 / -1;
270
+ }
271
+
272
+ .profile-mate-mark-actions .profile-avatar-upload {
273
+ grid-column: auto;
274
+ }
275
+
276
+ .profile-avatar-upload.is-active::before {
277
+ content: "";
278
+ width: 5px;
279
+ height: 5px;
280
+ border-radius: 50%;
281
+ background: var(--accent);
259
282
  }
283
+
260
284
  .profile-avatar-custom-preview {
261
285
  width: 100%;
262
286
  height: 100%;
263
287
  object-fit: cover;
264
- border-radius: 6px;
288
+ border-radius: 12px;
289
+ }
290
+
291
+ .profile-use-imprint.is-active::before {
292
+ content: "";
293
+ width: 5px;
294
+ height: 5px;
295
+ border-radius: 50%;
296
+ background: var(--accent);
297
+ }
298
+
299
+ .profile-imprint-actions button:focus-visible {
300
+ outline: 2px solid var(--accent);
301
+ outline-offset: 2px;
302
+ }
303
+
304
+ .profile-imprint-actions button:active {
305
+ transform: translateY(1px);
306
+ }
307
+
308
+ .profile-imprint-actions button:disabled {
309
+ opacity: 0.45;
310
+ cursor: default;
311
+ }
312
+
313
+ .profile-imprint-actions button:disabled:hover {
314
+ border-color: var(--border);
315
+ color: var(--text-muted);
316
+ }
317
+
318
+ .profile-imprint-actions button.is-active:hover {
319
+ color: var(--text);
265
320
  }
266
321
 
267
322
  /* Avatar positioner overlay */
@@ -297,7 +352,7 @@
297
352
  color: var(--text-muted);
298
353
  font-size: 20px;
299
354
  cursor: pointer;
300
- border-radius: 50%;
355
+ border-radius: 28px;
301
356
  display: flex;
302
357
  align-items: center;
303
358
  justify-content: center;
@@ -313,7 +368,7 @@
313
368
  color: var(--text);
314
369
  }
315
370
  .avatar-positioner-viewport {
316
- border-radius: 50%;
371
+ border-radius: 26%;
317
372
  overflow: hidden;
318
373
  position: relative;
319
374
  cursor: grab;
@@ -368,30 +423,3 @@
368
423
  .avatar-positioner-btn-done:hover {
369
424
  filter: brightness(1.1);
370
425
  }
371
-
372
- /* Color swatch grid */
373
- .profile-color-grid {
374
- display: flex;
375
- flex-wrap: wrap;
376
- gap: 6px;
377
- }
378
-
379
- .profile-color-swatch {
380
- width: 22px;
381
- height: 22px;
382
- border-radius: 50%;
383
- border: 2px solid transparent;
384
- cursor: pointer;
385
- transition: transform 0.15s, border-color 0.15s;
386
- padding: 0;
387
- }
388
-
389
- .profile-color-swatch:hover {
390
- transform: scale(1.15);
391
- }
392
-
393
- .profile-color-swatch.profile-color-active {
394
- border-color: #fff;
395
- box-shadow: 0 0 0 2px var(--accent);
396
- }
397
-
@@ -2,23 +2,29 @@
2
2
  display: inline-flex;
3
3
  align-items: center;
4
4
  justify-content: center;
5
- width: 28px;
5
+ gap: 6px;
6
+ width: auto;
6
7
  height: 28px;
7
- padding: 0;
8
- border: 1px solid transparent;
8
+ margin: 0;
9
+ padding: 0 9px;
10
+ border: 1px solid var(--border-subtle);
9
11
  border-radius: 8px;
10
- background: transparent;
11
- color: var(--text-dimmer);
12
+ background: color-mix(in srgb, var(--brand-indigo) 5%, var(--bg));
13
+ color: var(--text-secondary);
12
14
  cursor: pointer;
13
15
  flex-shrink: 0;
16
+ font: inherit;
17
+ font-size: 11px;
18
+ font-weight: 650;
19
+ white-space: nowrap;
14
20
  transition: color 0.15s, background 0.15s, border-color 0.15s;
15
21
  }
16
22
 
17
23
  #header-session-actions-btn:hover,
18
24
  #header-session-actions-btn[aria-expanded="true"] {
19
25
  color: var(--text);
20
- background: rgba(var(--overlay-rgb), 0.05);
21
- border-color: var(--border);
26
+ background: color-mix(in srgb, var(--brand-indigo) 9%, var(--bg));
27
+ border-color: color-mix(in srgb, var(--brand-indigo) 32%, var(--border));
22
28
  }
23
29
 
24
30
  #header-session-actions-btn:focus { outline: none; }
@@ -28,12 +34,26 @@
28
34
  }
29
35
 
30
36
  #header-session-actions-btn.hidden { display: none; }
31
- #header-session-actions-btn .lucide { width: 16px; height: 16px; }
37
+ #header-session-actions-btn .lucide:first-child {
38
+ width: 14px;
39
+ height: 14px;
40
+ color: color-mix(in srgb, var(--brand-indigo) 78%, var(--text));
41
+ }
42
+
43
+ #header-session-actions-btn .session-actions-trigger-chevron {
44
+ width: 13px;
45
+ height: 13px;
46
+ transition: transform 0.16s ease;
47
+ }
48
+
49
+ #header-session-actions-btn[aria-expanded="true"] .session-actions-trigger-chevron {
50
+ transform: rotate(180deg);
51
+ }
32
52
 
33
53
  .session-actions-menu {
34
54
  position: fixed;
35
55
  z-index: 10020;
36
- width: 286px;
56
+ width: min(300px, calc(100vw - 16px));
37
57
  padding: 6px;
38
58
  border: 1px solid var(--border);
39
59
  border-radius: 12px;
@@ -63,6 +83,17 @@
63
83
  }
64
84
 
65
85
  .session-actions-row:hover { background: var(--bg-alt); }
86
+ .session-actions-row.is-primary {
87
+ background: color-mix(in srgb, var(--brand-indigo) 6%, transparent);
88
+ }
89
+ .session-actions-row.is-primary:hover {
90
+ background: color-mix(in srgb, var(--brand-indigo) 10%, transparent);
91
+ }
92
+ .session-actions-row.is-primary .session-actions-row-icon {
93
+ border-color: transparent;
94
+ background: var(--brand-indigo);
95
+ color: var(--brand-paper);
96
+ }
66
97
  .session-actions-row:disabled { cursor: default; opacity: 0.42; }
67
98
  .session-actions-row:disabled:hover { background: transparent; }
68
99
 
@@ -342,7 +342,7 @@
342
342
  flex-shrink: 0;
343
343
  width: 56px;
344
344
  height: 56px;
345
- border-radius: 50%;
345
+ border-radius: 26%;
346
346
  overflow: hidden;
347
347
  border: 1px solid rgba(var(--overlay-rgb), 0.1);
348
348
  background: var(--bg);
@@ -388,8 +388,8 @@
388
388
  <div id="ralph-sticky" class="hidden"></div>
389
389
  <div id="debate-sticky" class="hidden"></div>
390
390
  <div class="status">
391
- <button id="header-session-actions-btn" type="button" aria-label="Session actions" title="Session actions" aria-haspopup="menu" aria-expanded="false">
392
- <i data-lucide="ellipsis"></i>
391
+ <button id="header-session-actions-btn" type="button" aria-label="Session actions" aria-haspopup="menu" aria-expanded="false">
392
+ <i data-lucide="bot"></i><span>Add AI worker</span><i class="session-actions-trigger-chevron" data-lucide="chevron-down"></i>
393
393
  </button>
394
394
  <button type="button" id="header-tui-font-btn" class="header-tui-font-btn hidden" title="Terminal font" aria-label="Terminal font">
395
395
  <i data-lucide="type"></i>
@@ -72,8 +72,8 @@ function createCursorElement(userId, displayName, color, avatarStyle, avatarSeed
72
72
  // Avatar
73
73
  var avatarImg = document.createElement("img");
74
74
  avatarImg.className = "remote-cursor-avatar";
75
- avatarImg.src = avatarCustom ? avatarCustom : avatarUrl(avatarStyle || "thumbs", avatarSeed || userId, 16);
76
- avatarImg.style.cssText = "width:14px;height:14px;border-radius:50%;background:#fff;flex-shrink:0;";
75
+ avatarImg.src = avatarCustom ? avatarCustom : avatarUrl(avatarStyle || "imprint", avatarSeed || userId, 16);
76
+ avatarImg.style.cssText = "width:14px;height:14px;border-radius:4px;background:#fff;flex-shrink:0;";
77
77
  tag.appendChild(avatarImg);
78
78
 
79
79
  // Name label
@@ -215,7 +215,7 @@ export function enterDmMode(key, targetUser, messages) {
215
215
  var termBtn = document.getElementById("terminal-toggle-btn");
216
216
  if (termBtn) termBtn.style.display = "none";
217
217
  // Apply mate color to chat title bar and panels
218
- var mateColor = (targetUser.profile && targetUser.profile.avatarColor) || targetUser.avatarColor || "#7c3aed";
218
+ var mateColor = (targetUser.profile && targetUser.profile.avatarColor) || targetUser.avatarColor || "#5857fc";
219
219
  document.body.style.setProperty("--mate-color", mateColor);
220
220
  document.body.style.setProperty("--mate-color-tint", mateColor + "0a");
221
221
  document.body.classList.add("mate-dm-active");
@@ -223,19 +223,12 @@ export function enterDmMode(key, targetUser, messages) {
223
223
  var mp = targetUser.profile || {};
224
224
  var mateAvUrlDm = mateAvatarUrl(targetUser, 36);
225
225
  var myUser = store.get('cachedAllUsers').find(function (u) { return u.id === store.get('myUserId'); });
226
- if (!myUser) {
227
- try { var cached = JSON.parse(localStorage.getItem("clay_my_user") || "null"); if (cached) myUser = cached; } catch(e) {}
228
- }
229
226
  var myAvatarUrl = userAvatarUrl(myUser || { id: store.get('myUserId') }, 36);
230
227
  var myDisplayName = (myUser && myUser.displayName) || "";
231
228
  document.body.dataset.mateAvatarUrl = mateAvUrlDm;
232
229
  document.body.dataset.mateName = mp.displayName || targetUser.displayName || targetUser.name || "";
233
230
  document.body.dataset.myAvatarUrl = myAvatarUrl;
234
231
  document.body.dataset.myDisplayName = myDisplayName;
235
- // Cache my info for restore after hard refresh
236
- if (myUser) {
237
- try { localStorage.setItem("clay_my_user", JSON.stringify({ displayName: myUser.displayName, avatarStyle: myUser.avatarStyle, avatarSeed: myUser.avatarSeed, avatarCustom: myUser.avatarCustom, username: myUser.username })); } catch(e) {}
238
- }
239
232
  var titleBarContent = document.querySelector(".title-bar-content");
240
233
  if (titleBarContent) {
241
234
  titleBarContent.style.background = mateColor;
@@ -95,7 +95,7 @@ export function processMessage(msg) {
95
95
  if (headerTitleEl) headerTitleEl.textContent = _mdn;
96
96
  var _tbpn = document.getElementById("title-bar-project-name");
97
97
  if (_tbpn) _tbpn.textContent = _mdn;
98
- var _mc2 = (store.get('dmTargetUser').profile && store.get('dmTargetUser').profile.avatarColor) || store.get('dmTargetUser').avatarColor || "#7c3aed";
98
+ var _mc2 = (store.get('dmTargetUser').profile && store.get('dmTargetUser').profile.avatarColor) || store.get('dmTargetUser').avatarColor || "#5857fc";
99
99
  var _tbc2 = document.querySelector(".title-bar-content");
100
100
  if (_tbc2) { _tbc2.style.background = _mc2; _tbc2.classList.add("mate-dm-active"); }
101
101
  document.body.classList.add("mate-dm-active");
@@ -299,7 +299,7 @@ export function processMessage(msg) {
299
299
  var tbProjectName = document.getElementById("title-bar-project-name");
300
300
  if (tbProjectName) tbProjectName.textContent = _mateDN;
301
301
  // Re-apply mate title bar styling (may be lost during project switch)
302
- var _mc = (store.get('dmTargetUser').profile && store.get('dmTargetUser').profile.avatarColor) || store.get('dmTargetUser').avatarColor || "#7c3aed";
302
+ var _mc = (store.get('dmTargetUser').profile && store.get('dmTargetUser').profile.avatarColor) || store.get('dmTargetUser').avatarColor || "#5857fc";
303
303
  var _tbc = document.querySelector(".title-bar-content");
304
304
  if (_tbc) { _tbc.style.background = _mc; _tbc.classList.add("mate-dm-active"); }
305
305
  document.body.classList.add("mate-dm-active");
@@ -257,12 +257,13 @@ export function updateProjectList(msg) {
257
257
  if (msg.dmConversations) store.set({ cachedDmConversations: msg.dmConversations });
258
258
  // renderUserStrip is handled by the store subscriber
259
259
  var st2 = store.snap();
260
- if (document.body.classList.contains("mate-dm-active") || document.body.classList.contains("wide-view")) {
261
- var refreshedMyUser = st2.cachedAllUsers.find(function (u) { return u.id === st2.myUserId; });
262
- if (refreshedMyUser) {
263
- document.body.dataset.myDisplayName = refreshedMyUser.displayName || refreshedMyUser.username || "";
264
- document.body.dataset.myAvatarUrl = userAvatarUrl(refreshedMyUser, 36);
265
- try { localStorage.setItem("clay_my_user", JSON.stringify({ displayName: refreshedMyUser.displayName, username: refreshedMyUser.username, avatarStyle: refreshedMyUser.avatarStyle, avatarSeed: refreshedMyUser.avatarSeed, avatarCustom: refreshedMyUser.avatarCustom })); } catch(e) {}
260
+ var refreshedMyUser = st2.cachedAllUsers.find(function (u) { return u.id === st2.myUserId; });
261
+ if (refreshedMyUser) {
262
+ document.body.dataset.myDisplayName = refreshedMyUser.displayName || refreshedMyUser.username || "";
263
+ document.body.dataset.myAvatarUrl = userAvatarUrl(refreshedMyUser, 36);
264
+ var myMessageAvatars = document.querySelectorAll('.dm-bubble-avatar-me');
265
+ for (var avatarIndex = 0; avatarIndex < myMessageAvatars.length; avatarIndex++) {
266
+ myMessageAvatars[avatarIndex].src = document.body.dataset.myAvatarUrl;
266
267
  }
267
268
  }
268
269
  // Render my avatar (always present, hidden behind user-island)
@@ -311,7 +311,6 @@ export function addScheduledMessageBubble(text, resetsAt) {
311
311
  if (isChannel) {
312
312
  // Channel mode: avatar + header with scheduled badge + message
313
313
  var _me = store.get('cachedAllUsers').find(function (u) { return u.id === store.get('myUserId'); });
314
- if (!_me) { try { _me = JSON.parse(localStorage.getItem("clay_my_user") || "null"); } catch(e) {} }
315
314
  var _myName = document.body.dataset.myDisplayName || (_me && (_me.displayName || _me.username)) || "Me";
316
315
 
317
316
  var avi = document.createElement("img");
@@ -552,9 +552,6 @@ export function addUserMessage(text, images, pastes, fromUserId, fromUserName, d
552
552
  _displayName = fromUserName || (_targetUser && (_targetUser.displayName || _targetUser.username)) || "User";
553
553
  } else {
554
554
  _targetUser = cachedAllUsers.find(function (u) { return u.id === myUserId; });
555
- if (!_targetUser) {
556
- try { _targetUser = JSON.parse(localStorage.getItem("clay_my_user") || "null"); } catch(e) {}
557
- }
558
555
  _displayName = document.body.dataset.myDisplayName || "";
559
556
  if (!_displayName) {
560
557
  _displayName = (_targetUser && (_targetUser.displayName || _targetUser.username)) || "Me";
@@ -0,0 +1,134 @@
1
+ // Clay Imprints: deterministic SVG identities derived from a stable seed.
2
+
3
+ var PAPER = "#f1efe8";
4
+ var INK = "#1b1b1a";
5
+ var LIGHT_BASE = "#e7e5de";
6
+ var LIGHT_FIELD = "#333330";
7
+ var REGISTRATION_INDIGO = "#5857fc";
8
+ var BRAND_GREEN = "#07e5a3";
9
+
10
+ function hashString(value) {
11
+ var text = String(value || "anonymous");
12
+ var hash = 2166136261;
13
+ for (var index = 0; index < text.length; index++) {
14
+ hash ^= text.charCodeAt(index);
15
+ hash = Math.imul(hash, 16777619);
16
+ }
17
+ return hash >>> 0;
18
+ }
19
+
20
+ function makeRandom(seed) {
21
+ var state = seed >>> 0;
22
+ return function nextRandom() {
23
+ state += 0x6d2b79f5;
24
+ var value = state;
25
+ value = Math.imul(value ^ (value >>> 15), value | 1);
26
+ value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
27
+ return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
28
+ };
29
+ }
30
+
31
+ function between(random, min, max) {
32
+ return min + random() * (max - min);
33
+ }
34
+
35
+ function number(value) {
36
+ return Math.round(value * 100) / 100;
37
+ }
38
+
39
+ function xmlText(value) {
40
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
41
+ }
42
+
43
+ function legacyIdentity(style, seed) {
44
+ var sourceStyle = String(style || "imprint").toLowerCase();
45
+ var sourceSeed = String(seed || "anonymous");
46
+ if (sourceStyle === "imprint") return sourceSeed;
47
+ return "legacy:" + sourceStyle + ":" + sourceSeed;
48
+ }
49
+
50
+ function curvePath(y, bow, endOffset) {
51
+ return "M -28 " + number(y) +
52
+ " C 17 " + number(y + bow) +
53
+ ", 71 " + number(y - bow) +
54
+ ", 124 " + number(y + endOffset);
55
+ }
56
+
57
+ function cutPath(random) {
58
+ var start = between(random, 37, 52);
59
+ var first = between(random, 14, 84);
60
+ var second = between(random, 12, 86);
61
+ var end = between(random, 40, 64);
62
+ return "M -22 " + number(start) + " C 21 " + number(first) + ", 73 " + number(second) + ", 118 " + number(end);
63
+ }
64
+
65
+ export function imprintSvg(options) {
66
+ var settings = options || {};
67
+ var size = Math.max(12, Math.min(256, Number(settings.size) || 64));
68
+ var identity = legacyIdentity(settings.style, settings.seed);
69
+ var hash = hashString(identity);
70
+ var random = makeRandom(hash);
71
+ var darkBase = random() > 0.52;
72
+ var base = darkBase ? INK : LIGHT_BASE;
73
+ var field = darkBase ? PAPER : LIGHT_FIELD;
74
+ var accent = REGISTRATION_INDIGO;
75
+ var rotation = Math.floor(random() * 4) * 90;
76
+ var bandWidth = between(random, 37, 56);
77
+ var secondaryWidth = between(random, 14, 24);
78
+ var cutWidth = between(random, 15, 25);
79
+ var firstY = between(random, 24, 42);
80
+ var secondY = between(random, 58, 78);
81
+ var firstBow = between(random, -34, 34);
82
+ var secondBow = between(random, -32, 32);
83
+ var firstPath = curvePath(firstY, firstBow, 6);
84
+ var secondPath = curvePath(secondY, -secondBow, -5);
85
+ var contours = "";
86
+
87
+ if (size >= 40) {
88
+ for (var contour = -1; contour <= 1; contour++) {
89
+ var offset = contour * 5.2;
90
+ contours += '<path d="' + curvePath(firstY + offset, firstBow, 0) + '" fill="none" stroke="' +
91
+ (darkBase ? PAPER : INK) + '" stroke-opacity="0.2" stroke-width="0.8"/>';
92
+ }
93
+ }
94
+
95
+ var clipId = "imprint-" + hash.toString(16);
96
+ return '<svg xmlns="http://www.w3.org/2000/svg" width="' + size + '" height="' + size +
97
+ '" viewBox="0 0 96 96"><defs><clipPath id="' + clipId +
98
+ '"><rect width="96" height="96" rx="24"/></clipPath></defs><g clip-path="url(#' + clipId +
99
+ ')"><rect width="96" height="96" fill="' + base + '"/><g transform="rotate(' + rotation +
100
+ ' 48 48)"><path d="' + firstPath + '" fill="none" stroke="' + field + '" stroke-width="' +
101
+ number(bandWidth) + '" stroke-linecap="square"/><path d="' + secondPath + '" fill="none" stroke="' +
102
+ accent + '" stroke-width="' + number(secondaryWidth) + '" stroke-linecap="square"/><path d="' +
103
+ cutPath(random) + '" fill="none" stroke="' + base + '" stroke-width="' + number(cutWidth) +
104
+ '" stroke-linecap="square"/>' + contours + '</g></g><rect x="0.5" y="0.5" width="95" height="95" rx="23.5" fill="none" stroke="' +
105
+ (darkBase ? "#000000" : INK) + '" stroke-opacity="0.16"/></svg>';
106
+ }
107
+
108
+ export function imprintDataUrl(options) {
109
+ return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(imprintSvg(options));
110
+ }
111
+
112
+ export function mateMarkSvg(options) {
113
+ var settings = options || {};
114
+ var size = Math.max(12, Math.min(256, Number(settings.size) || 64));
115
+ var identity = String(settings.seed || "M").trim();
116
+ var initial = Array.from(identity || "M")[0].toUpperCase();
117
+ var palette = [
118
+ { background: REGISTRATION_INDIGO, foreground: PAPER },
119
+ { background: BRAND_GREEN, foreground: INK },
120
+ { background: PAPER, foreground: REGISTRATION_INDIGO },
121
+ ][initial.codePointAt(0) % 3];
122
+ var clipId = "mate-mark";
123
+
124
+ return '<svg xmlns="http://www.w3.org/2000/svg" width="' + size + '" height="' + size +
125
+ '" viewBox="0 0 96 96"><defs><clipPath id="' + clipId +
126
+ '"><rect width="96" height="96" rx="24"/></clipPath></defs><g clip-path="url(#' + clipId +
127
+ ')"><rect width="96" height="96" fill="' + palette.background + '"/><text x="48" y="50" fill="' + palette.foreground +
128
+ '" font-family="Arial, Helvetica, sans-serif" font-size="43" font-weight="600" text-anchor="middle" dominant-baseline="middle">' +
129
+ xmlText(initial) + '</text></g><rect x="0.5" y="0.5" width="95" height="95" rx="23.5" fill="none" stroke="#000000" stroke-opacity="0.18"/></svg>';
130
+ }
131
+
132
+ export function mateMarkDataUrl(options) {
133
+ return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(mateMarkSvg(options));
134
+ }