@nuraly/lumenui 0.3.4 → 0.3.6

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 (80) hide show
  1. package/dist/nuralyui.bundle.js +2581 -1483
  2. package/dist/nuralyui.bundle.js.gz +0 -0
  3. package/dist/src/components/button/bundle.js +130 -39
  4. package/dist/src/components/button/bundle.js.gz +0 -0
  5. package/dist/src/components/button/button.component.js +7 -4
  6. package/dist/src/components/button/button.style.js +92 -2
  7. package/dist/src/components/canvas/base-canvas.component.d.ts +8 -0
  8. package/dist/src/components/canvas/base-canvas.component.js +75 -3
  9. package/dist/src/components/canvas/bundle.js +2546 -1200
  10. package/dist/src/components/canvas/bundle.js.gz +0 -0
  11. package/dist/src/components/canvas/controllers/collaboration.controller.d.ts +24 -11
  12. package/dist/src/components/canvas/controllers/collaboration.controller.js +176 -120
  13. package/dist/src/components/canvas/controllers/selection.controller.js +20 -0
  14. package/dist/src/components/canvas/interfaces/collaboration.interface.d.ts +8 -0
  15. package/dist/src/components/canvas/templates/index.d.ts +1 -0
  16. package/dist/src/components/canvas/templates/index.js +2 -0
  17. package/dist/src/components/canvas/templates/lock-overlay.template.d.ts +20 -0
  18. package/dist/src/components/canvas/templates/lock-overlay.template.js +33 -0
  19. package/dist/src/components/canvas/workflow-canvas.component.js +52 -24
  20. package/dist/src/components/canvas/workflow-canvas.style.js +62 -1
  21. package/dist/src/components/canvas/workflow-canvas.types.js +50 -4
  22. package/dist/src/components/chat-panel/bundle.js +10 -10
  23. package/dist/src/components/chat-panel/bundle.js.gz +0 -0
  24. package/dist/src/components/chat-panel/chat-panel.component.js +8 -8
  25. package/dist/src/components/chatbot/bundle.js +454 -289
  26. package/dist/src/components/chatbot/bundle.js.gz +0 -0
  27. package/dist/src/components/chatbot/chatbot.style.js +162 -21
  28. package/dist/src/components/chatbot/chatbot.types.d.ts +1 -0
  29. package/dist/src/components/chatbot/core/chatbot-core.controller.js +13 -7
  30. package/dist/src/components/chatbot/providers/workflow-socket-provider.js +1 -2
  31. package/dist/src/components/chatbot/templates/input-box.template.js +58 -30
  32. package/dist/src/components/chatbot/templates/message.template.js +41 -31
  33. package/dist/src/components/chatbot/templates/thread-sidebar.template.js +38 -39
  34. package/dist/src/components/colorpicker/bundle.js +15 -10
  35. package/dist/src/components/colorpicker/bundle.js.gz +0 -0
  36. package/dist/src/components/colorpicker/color-picker.component.js +15 -10
  37. package/dist/src/components/datepicker/bundle.js +10 -10
  38. package/dist/src/components/datepicker/bundle.js.gz +0 -0
  39. package/dist/src/components/datepicker/datepicker.component.js +14 -22
  40. package/dist/src/components/dropdown/bundle.js +13 -12
  41. package/dist/src/components/dropdown/bundle.js.gz +0 -0
  42. package/dist/src/components/dropdown/dropdown.component.js +10 -9
  43. package/dist/src/components/file-upload/bundle.js +15 -14
  44. package/dist/src/components/file-upload/bundle.js.gz +0 -0
  45. package/dist/src/components/file-upload/file-upload.component.js +15 -14
  46. package/dist/src/components/icon/bundle.js +7 -7
  47. package/dist/src/components/icon/bundle.js.gz +0 -0
  48. package/dist/src/components/icon/icon-paths.js +15 -0
  49. package/dist/src/components/iconpicker/bundle.js +214 -122
  50. package/dist/src/components/iconpicker/bundle.js.gz +0 -0
  51. package/dist/src/components/iconpicker/icon-picker.component.js +4 -4
  52. package/dist/src/components/menu/bundle.js +5 -2
  53. package/dist/src/components/menu/bundle.js.gz +0 -0
  54. package/dist/src/components/menu/menu.component.js +5 -2
  55. package/dist/src/components/modal/bundle.js +16 -13
  56. package/dist/src/components/modal/bundle.js.gz +0 -0
  57. package/dist/src/components/modal/modal.component.js +16 -13
  58. package/dist/src/components/panel/bundle.js +28 -28
  59. package/dist/src/components/panel/bundle.js.gz +0 -0
  60. package/dist/src/components/popconfirm/bundle.js +135 -41
  61. package/dist/src/components/popconfirm/bundle.js.gz +0 -0
  62. package/dist/src/components/popconfirm/popconfirm.component.d.ts +15 -119
  63. package/dist/src/components/popconfirm/popconfirm.component.js +158 -162
  64. package/dist/src/components/popconfirm/popconfirm.style.js +94 -0
  65. package/dist/src/components/presence/bundle.js +2 -1
  66. package/dist/src/components/presence/bundle.js.gz +0 -0
  67. package/dist/src/components/presence/presence.component.js +2 -1
  68. package/dist/src/components/table/bundle.js +3 -2
  69. package/dist/src/components/table/bundle.js.gz +0 -0
  70. package/dist/src/components/table/table.component.js +3 -2
  71. package/dist/src/components/tabs/bundle.js +3 -3
  72. package/dist/src/components/tabs/bundle.js.gz +0 -0
  73. package/dist/src/components/timepicker/bundle.js +3 -3
  74. package/dist/src/components/timepicker/bundle.js.gz +0 -0
  75. package/package.json +1 -1
  76. package/packages/common/dist/VERSIONS.md +1 -1
  77. package/packages/common/dist/shared/controllers/dropdown.controller.d.ts +4 -0
  78. package/packages/common/dist/shared/controllers/dropdown.controller.d.ts.map +1 -1
  79. package/packages/common/dist/shared/controllers/dropdown.controller.js +29 -3
  80. package/packages/common/dist/shared/controllers/dropdown.controller.js.map +1 -1
@@ -259,14 +259,13 @@ export default css `
259
259
  }
260
260
 
261
261
  .thread-item--active {
262
- background-color: #f4f0fd;
263
- color: #7c3aed;
264
- border-color: #7c3aed;
262
+ background-color: #ededed;
263
+ color: #161616;
264
+ border-color: transparent;
265
265
  }
266
266
 
267
267
  .thread-item--active:hover {
268
- background-color: #f4f0fd;
269
- opacity: 0.95;
268
+ background-color: #e5e5e5;
270
269
  }
271
270
 
272
271
  .thread-item__header {
@@ -288,14 +287,24 @@ export default css `
288
287
  }
289
288
 
290
289
  .thread-item__actions {
291
- display: none;
290
+ display: flex;
292
291
  align-items: center;
293
292
  gap: 2px;
294
293
  flex-shrink: 0;
295
294
  }
296
295
 
297
- .thread-item:hover .thread-item__actions {
298
- display: flex;
296
+ .thread-item__actions .thread-item__menu {
297
+ opacity: 0;
298
+ pointer-events: none;
299
+ transition: opacity 0.15s ease;
300
+ }
301
+
302
+ .thread-item:hover .thread-item__menu,
303
+ .thread-item--active .thread-item__menu,
304
+ .thread-item:focus-within .thread-item__menu,
305
+ .thread-item__actions nr-dropdown[open] .thread-item__menu {
306
+ opacity: 1;
307
+ pointer-events: auto;
299
308
  }
300
309
 
301
310
  .thread-item__action-btn {
@@ -323,27 +332,23 @@ export default css `
323
332
  background-color: rgba(59, 130, 246, 0.1);
324
333
  }
325
334
 
326
- .thread-item__actions nr-popconfirm {
327
- margin-top: 4px;
328
- }
329
-
330
335
  .thread-item__delete:hover {
331
336
  color: #ef4444;
332
337
  background-color: rgba(239, 68, 68, 0.1);
333
338
  }
334
339
 
335
340
  .thread-item--active .thread-item__action-btn {
336
- color: rgba(255, 255, 255, 0.55);
341
+ color: rgba(22, 22, 22, 0.6);
337
342
  }
338
343
 
339
344
  .thread-item--active .thread-item__action-btn:hover {
340
- color: #3b82f6;
341
- background-color: rgba(59, 130, 246, 0.15);
345
+ color: #161616;
346
+ background-color: rgba(0, 0, 0, 0.06);
342
347
  }
343
348
 
344
349
  .thread-item--active .thread-item__delete:hover {
345
350
  color: #ef4444;
346
- background-color: rgba(239, 68, 68, 0.15);
351
+ background-color: rgba(239, 68, 68, 0.1);
347
352
  }
348
353
 
349
354
  .thread-item__bookmark--active {
@@ -376,7 +381,7 @@ export default css `
376
381
  }
377
382
 
378
383
  .thread-item--active .thread-item__rename-input {
379
- background: rgba(255, 255, 255, 0.1);
384
+ background: #ffffff;
380
385
  }
381
386
 
382
387
  .thread-item__preview {
@@ -415,11 +420,13 @@ export default css `
415
420
  .messages {
416
421
  flex: 1;
417
422
  overflow-y: auto;
423
+ overflow-x: hidden;
418
424
  display: flex;
419
425
  flex-direction: column;
420
426
  gap: 0;
421
427
  background-color: #ffffff;
422
- padding: 8px 12px;
428
+ padding: 8px 1rem;
429
+ box-sizing: border-box;
423
430
  justify-content: flex-start; /* Always align messages to top */
424
431
  }
425
432
 
@@ -479,16 +486,22 @@ export default css `
479
486
  border: 0 solid transparent;
480
487
  }
481
488
 
482
- /* Message attachments (file tags) */
489
+ /* Message attachments (file thumbs) */
483
490
  .message__attachments {
484
491
  display: flex;
485
492
  flex-wrap: wrap;
486
- gap: 0.25rem;
487
- margin-top: 0.25rem;
493
+ gap: 0.375rem;
494
+ margin-top: 0.375rem;
488
495
  position: relative;
489
496
  z-index: 1;
490
497
  }
491
498
 
499
+ .file-thumb--message {
500
+ width: 48px;
501
+ height: 48px;
502
+ border-radius: 8px;
503
+ }
504
+
492
505
  .message-file-preview-dropdown {
493
506
  display: inline-block;
494
507
  position: relative;
@@ -986,6 +999,134 @@ export default css `
986
999
  cursor: help;
987
1000
  }
988
1001
 
1002
+ /* Claude-style upload thumbnail chip */
1003
+ .file-thumb {
1004
+ position: relative;
1005
+ display: inline-flex;
1006
+ align-items: center;
1007
+ justify-content: center;
1008
+ width: 56px;
1009
+ height: 56px;
1010
+ border-radius: 10px;
1011
+ overflow: hidden;
1012
+ background-color: #f3f4f6;
1013
+ border: 1px solid #e5e7eb;
1014
+ cursor: pointer;
1015
+ outline: none;
1016
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
1017
+ flex-shrink: 0;
1018
+ }
1019
+
1020
+ .file-thumb:hover,
1021
+ .file-thumb:focus-visible {
1022
+ border-color: #7c3aed;
1023
+ box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.15);
1024
+ }
1025
+
1026
+ .file-thumb__image {
1027
+ width: 100%;
1028
+ height: 100%;
1029
+ object-fit: cover;
1030
+ display: block;
1031
+ }
1032
+
1033
+ .file-thumb__ext {
1034
+ width: 100%;
1035
+ height: 100%;
1036
+ display: flex;
1037
+ align-items: center;
1038
+ justify-content: center;
1039
+ background: linear-gradient(135deg, #ede9fe, #ddd6fe);
1040
+ color: #5b21b6;
1041
+ font-weight: 600;
1042
+ font-size: 0.7rem;
1043
+ letter-spacing: 0.04em;
1044
+ text-transform: uppercase;
1045
+ }
1046
+
1047
+ .file-thumb__ext-label {
1048
+ padding: 0 4px;
1049
+ max-width: 100%;
1050
+ overflow: hidden;
1051
+ text-overflow: ellipsis;
1052
+ white-space: nowrap;
1053
+ }
1054
+
1055
+ .file-thumb__spinner {
1056
+ position: absolute;
1057
+ inset: 0;
1058
+ display: flex;
1059
+ align-items: center;
1060
+ justify-content: center;
1061
+ background-color: rgba(255, 255, 255, 0.55);
1062
+ backdrop-filter: blur(1px);
1063
+ }
1064
+
1065
+ .file-thumb__spinner-ring {
1066
+ width: 20px;
1067
+ height: 20px;
1068
+ border-radius: 50%;
1069
+ border: 2px solid rgba(124, 58, 237, 0.25);
1070
+ border-top-color: #7c3aed;
1071
+ animation: file-thumb-spin 0.8s linear infinite;
1072
+ }
1073
+
1074
+ .file-thumb--uploading {
1075
+ pointer-events: none;
1076
+ }
1077
+
1078
+ .file-thumb--uploading .file-thumb__remove {
1079
+ display: none;
1080
+ }
1081
+
1082
+ @keyframes file-thumb-spin {
1083
+ to { transform: rotate(360deg); }
1084
+ }
1085
+
1086
+ .file-thumb__remove {
1087
+ position: absolute;
1088
+ top: 2px;
1089
+ right: 2px;
1090
+ width: 16px;
1091
+ height: 16px;
1092
+ padding: 0;
1093
+ border: none;
1094
+ border-radius: 50%;
1095
+ background-color: rgba(17, 24, 39, 0.7);
1096
+ color: #ffffff;
1097
+ display: inline-flex;
1098
+ align-items: center;
1099
+ justify-content: center;
1100
+ cursor: pointer;
1101
+ opacity: 0;
1102
+ transition: opacity 0.15s ease, background-color 0.15s ease;
1103
+ }
1104
+
1105
+ .file-thumb:hover .file-thumb__remove,
1106
+ .file-thumb:focus-within .file-thumb__remove {
1107
+ opacity: 1;
1108
+ }
1109
+
1110
+ .file-thumb__remove:hover {
1111
+ background-color: rgba(17, 24, 39, 0.9);
1112
+ }
1113
+
1114
+ /* Dropdown preview extension tile (non-image files) */
1115
+ .file-preview-ext {
1116
+ width: 72px;
1117
+ height: 72px;
1118
+ display: flex;
1119
+ align-items: center;
1120
+ justify-content: center;
1121
+ border-radius: 10px;
1122
+ background: linear-gradient(135deg, #ede9fe, #ddd6fe);
1123
+ color: #5b21b6;
1124
+ font-weight: 600;
1125
+ font-size: 0.875rem;
1126
+ letter-spacing: 0.04em;
1127
+ text-transform: uppercase;
1128
+ }
1129
+
989
1130
  /* File preview dropdown content */
990
1131
  .file-preview-content {
991
1132
  display: flex;
@@ -84,6 +84,7 @@ export interface ChatbotFile {
84
84
  url?: string;
85
85
  previewUrl?: string;
86
86
  uploadProgress?: number;
87
+ isUploading?: boolean;
87
88
  error?: string;
88
89
  metadata?: Record<string, any>;
89
90
  }
@@ -348,14 +348,20 @@ export class ChatbotCoreController {
348
348
  continue;
349
349
  }
350
350
  const chatbotFile = yield this.fileHandler.createChatbotFile(file);
351
- const uploaded = yield this.providerService.uploadFileToProvider(file);
352
- if (uploaded) {
353
- Object.assign(chatbotFile, uploaded);
354
- }
355
- uploadedFiles.push(chatbotFile);
356
351
  this.fileHandler.addFile(chatbotFile);
357
- if (this.ui.showFilePreview) {
358
- this.ui.showFilePreview(chatbotFile);
352
+ try {
353
+ const uploaded = yield this.providerService.uploadFileToProvider(file);
354
+ const finalUpdates = Object.assign({ isUploading: false, uploadProgress: 100 }, (uploaded || {}));
355
+ this.fileHandler.updateFile(chatbotFile.id, finalUpdates);
356
+ Object.assign(chatbotFile, finalUpdates);
357
+ uploadedFiles.push(chatbotFile);
358
+ if (this.ui.showFilePreview) {
359
+ this.ui.showFilePreview(chatbotFile);
360
+ }
361
+ }
362
+ catch (uploadError) {
363
+ this.fileHandler.removeFile(chatbotFile.id);
364
+ throw uploadError;
359
365
  }
360
366
  }
361
367
  catch (error) {
@@ -88,7 +88,6 @@ export class WorkflowSocketProvider {
88
88
  this.socket = io(socketNs, {
89
89
  path: this.config.socketPath,
90
90
  query: { __params: JSON.stringify({ workflowId: this.config.workflowId }) },
91
- transports: ['websocket', 'polling'],
92
91
  autoConnect: true,
93
92
  reconnection: true,
94
93
  reconnectionAttempts: 5
@@ -96,7 +95,7 @@ export class WorkflowSocketProvider {
96
95
  return new Promise((resolve, reject) => {
97
96
  const timeout = setTimeout(() => {
98
97
  reject(new Error('Socket connection timeout'));
99
- }, 10000);
98
+ }, 30000);
100
99
  this.socket.on('connect', () => {
101
100
  clearTimeout(timeout);
102
101
  this.connected = true;
@@ -8,7 +8,9 @@ import { repeat } from 'lit/directives/repeat.js';
8
8
  import { styleMap } from 'lit/directives/style-map.js';
9
9
  import { msg } from '@lit/localize';
10
10
  /**
11
- * Renders context tags for uploaded files with hover preview
11
+ * Renders thumbnail chips for uploaded files (image thumb for images,
12
+ * extension badge for other file types). While a file is uploading, a
13
+ * spinner overlay is shown on top of the thumbnail.
12
14
  */
13
15
  function renderContextTags(files, onRemove, onFileClick) {
14
16
  const formatFileSize = (bytes) => {
@@ -19,57 +21,83 @@ function renderContextTags(files, onRemove, onFileClick) {
19
21
  const i = Math.floor(Math.log(bytes) / Math.log(k));
20
22
  return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
21
23
  };
22
- const getFileIcon = (mimeType) => {
23
- if (mimeType.startsWith('image/'))
24
- return 'image';
25
- if (mimeType.startsWith('video/'))
26
- return 'video';
27
- if (mimeType.startsWith('audio/'))
28
- return 'music';
29
- if (mimeType === 'application/pdf')
30
- return 'file-pdf';
31
- if (mimeType.startsWith('text/'))
32
- return 'file-text';
33
- return 'file';
24
+ const getExtension = (name, mimeType) => {
25
+ const dot = name.lastIndexOf('.');
26
+ if (dot >= 0 && dot < name.length - 1) {
27
+ return name.slice(dot + 1).toUpperCase().slice(0, 4);
28
+ }
29
+ if (mimeType) {
30
+ const slash = mimeType.indexOf('/');
31
+ if (slash >= 0)
32
+ return mimeType.slice(slash + 1).toUpperCase().slice(0, 4);
33
+ }
34
+ return 'FILE';
34
35
  };
35
36
  const isImage = (mimeType) => mimeType.startsWith('image/');
36
37
  return html `
37
38
  <div class="context-tags-row" part="context-tags">
38
39
  ${repeat(files, f => f.id, f => html `
39
- <nr-dropdown
40
- trigger="hover"
40
+ <nr-dropdown
41
+ trigger="hover"
41
42
  placement="top"
42
43
  size="small"
43
44
  class="file-preview-dropdown"
44
45
  >
45
- <nr-tag
46
+ <div
46
47
  slot="trigger"
47
- class="context-tag"
48
- size="small"
49
- closable
48
+ class="file-thumb ${f.isUploading ? 'file-thumb--uploading' : ''}"
49
+ role="button"
50
+ tabindex="0"
51
+ title="${f.name}"
50
52
  @click=${() => onFileClick === null || onFileClick === void 0 ? void 0 : onFileClick(f)}
51
- @nr-tag-close=${() => onRemove(f.id)}
52
- style="cursor: pointer;"
53
- >${f.name}</nr-tag>
54
-
53
+ >
54
+ ${isImage(f.mimeType) && (f.previewUrl || f.url) ? html `
55
+ <img
56
+ class="file-thumb__image"
57
+ src="${f.previewUrl || f.url}"
58
+ alt="${f.name}"
59
+ />
60
+ ` : html `
61
+ <div class="file-thumb__ext" data-ext="${getExtension(f.name, f.mimeType)}">
62
+ <span class="file-thumb__ext-label">${getExtension(f.name, f.mimeType)}</span>
63
+ </div>
64
+ `}
65
+ ${f.isUploading ? html `
66
+ <div class="file-thumb__spinner" aria-label="${msg('Uploading')}">
67
+ <span class="file-thumb__spinner-ring"></span>
68
+ </div>
69
+ ` : ''}
70
+ <button
71
+ type="button"
72
+ class="file-thumb__remove"
73
+ aria-label="${msg('Remove file')}"
74
+ title="${msg('Remove file')}"
75
+ @click=${(e) => { e.stopPropagation(); onRemove(f.id); }}
76
+ >
77
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round">
78
+ <line x1="6" y1="6" x2="18" y2="18"/>
79
+ <line x1="6" y1="18" x2="18" y2="6"/>
80
+ </svg>
81
+ </button>
82
+ </div>
83
+
55
84
  <div slot="content" class="file-preview-content">
56
85
  ${isImage(f.mimeType) && (f.url || f.previewUrl) ? html `
57
- <img
58
- src="${f.previewUrl || f.url}"
86
+ <img
87
+ src="${f.previewUrl || f.url}"
59
88
  alt="${f.name}"
60
89
  class="file-preview-image"
61
90
  />
62
91
  ` : html `
63
- <nr-icon
64
- .name=${getFileIcon(f.mimeType)}
65
- size="large"
66
- class="file-preview-icon"
67
- ></nr-icon>
92
+ <div class="file-preview-ext" data-ext="${getExtension(f.name, f.mimeType)}">
93
+ ${getExtension(f.name, f.mimeType)}
94
+ </div>
68
95
  `}
69
96
  <div class="file-preview-info">
70
97
  <div class="file-preview-name" title="${f.name}">${f.name}</div>
71
98
  <div class="file-preview-details">
72
99
  <span>${formatFileSize(f.size)}</span>
100
+ ${f.isUploading ? html `<span> · ${msg('Uploading…')}</span>` : ''}
73
101
  </div>
74
102
  </div>
75
103
  </div>
@@ -37,28 +37,27 @@ function formatFileSize(bytes) {
37
37
  const i = Math.floor(Math.log(bytes) / Math.log(k));
38
38
  return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
39
39
  }
40
- /**
41
- * Get icon name based on MIME type
42
- */
43
- function getFileIcon(mimeType) {
44
- if (mimeType.startsWith('image/'))
45
- return 'image';
46
- if (mimeType.startsWith('video/'))
47
- return 'video';
48
- if (mimeType.startsWith('audio/'))
49
- return 'music';
50
- if (mimeType === 'application/pdf')
51
- return 'file-pdf';
52
- if (mimeType.startsWith('text/'))
53
- return 'file-text';
54
- return 'file';
55
- }
56
40
  /**
57
41
  * Check if file is an image
58
42
  */
59
43
  function isImageFile(mimeType) {
60
44
  return mimeType.startsWith('image/');
61
45
  }
46
+ /**
47
+ * Derive a short extension label for non-image thumbnails
48
+ */
49
+ function getFileExtension(name, mimeType) {
50
+ const dot = name.lastIndexOf('.');
51
+ if (dot >= 0 && dot < name.length - 1) {
52
+ return name.slice(dot + 1).toUpperCase().slice(0, 4);
53
+ }
54
+ if (mimeType) {
55
+ const slash = mimeType.indexOf('/');
56
+ if (slash >= 0)
57
+ return mimeType.slice(slash + 1).toUpperCase().slice(0, 4);
58
+ }
59
+ return 'FILE';
60
+ }
62
61
  /**
63
62
  * Renders a single message
64
63
  */
@@ -87,33 +86,44 @@ export function renderMessage(message, handlers) {
87
86
  ${message.files && message.files.length > 0 ? html `
88
87
  <div class="message__attachments" part="message-attachments" role="list" aria-label="${msg('Attached files')}">
89
88
  ${message.files.map((f) => html `
90
- <nr-dropdown
91
- trigger="hover"
89
+ <nr-dropdown
90
+ trigger="hover"
92
91
  placement="top-end"
93
92
  size="small"
94
93
  class="message-file-preview-dropdown"
95
94
  >
96
- <nr-tag
95
+ <div
97
96
  slot="trigger"
98
- class="message__attachment-tag"
99
- size="small"
97
+ class="file-thumb file-thumb--message"
98
+ role="button"
99
+ tabindex="0"
100
+ title="${f.name}"
100
101
  @click=${() => { var _a; return (_a = handlers.onFileClick) === null || _a === void 0 ? void 0 : _a.call(handlers, f); }}
101
- style="cursor: pointer;"
102
- >${f.name}</nr-tag>
103
-
102
+ >
103
+ ${isImageFile(f.mimeType) && (f.url || f.previewUrl) ? html `
104
+ <img
105
+ class="file-thumb__image"
106
+ src="${f.previewUrl || f.url}"
107
+ alt="${f.name}"
108
+ />
109
+ ` : html `
110
+ <div class="file-thumb__ext" data-ext="${getFileExtension(f.name, f.mimeType)}">
111
+ <span class="file-thumb__ext-label">${getFileExtension(f.name, f.mimeType)}</span>
112
+ </div>
113
+ `}
114
+ </div>
115
+
104
116
  <div slot="content" class="message-file-preview-content">
105
117
  ${isImageFile(f.mimeType) && (f.url || f.previewUrl) ? html `
106
- <img
107
- src="${f.previewUrl || f.url}"
118
+ <img
119
+ src="${f.previewUrl || f.url}"
108
120
  alt="${f.name}"
109
121
  class="message-file-preview-image"
110
122
  />
111
123
  ` : html `
112
- <nr-icon
113
- .name=${getFileIcon(f.mimeType)}
114
- size="large"
115
- class="message-file-preview-icon"
116
- ></nr-icon>
124
+ <div class="file-preview-ext" data-ext="${getFileExtension(f.name, f.mimeType)}">
125
+ ${getFileExtension(f.name, f.mimeType)}
126
+ </div>
117
127
  `}
118
128
  <div class="message-file-preview-info">
119
129
  <div class="message-file-preview-name" title="${f.name}">${f.name}</div>
@@ -56,60 +56,59 @@ function renderThreadItem(thread, data, handlers) {
56
56
  <div class="thread-item__title">${thread.title || msg('New Chat')}</div>
57
57
  `}
58
58
  <div class="thread-item__actions">
59
- ${handlers.onRenameThread && data.editingThreadId !== thread.id ? html `
59
+ ${handlers.onBookmarkThread && thread.bookmarked ? html `
60
60
  <button
61
- class="thread-item__action-btn"
62
- title="${msg('Rename conversation')}"
63
- @click=${(e) => {
64
- e.stopPropagation();
65
- e.target.dispatchEvent(new CustomEvent('nr-thread-edit', {
66
- bubbles: true,
67
- composed: true,
68
- detail: { threadId: thread.id }
69
- }));
70
- }}
71
- part="thread-rename"
72
- >
73
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
74
- </button>
75
- ` : ''}
76
- ${handlers.onBookmarkThread ? html `
77
- <button
78
- class="thread-item__action-btn ${thread.bookmarked ? 'thread-item__bookmark--active' : ''}"
79
- title="${thread.bookmarked ? msg('Remove bookmark') : msg('Bookmark conversation')}"
61
+ class="thread-item__action-btn thread-item__bookmark--active"
62
+ title="${msg('Remove bookmark')}"
80
63
  @click=${(e) => {
81
64
  e.stopPropagation();
82
65
  handlers.onBookmarkThread(thread.id);
83
66
  }}
84
67
  part="thread-bookmark"
85
68
  >
86
- ${thread.bookmarked ? html `
87
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
88
- ` : html `
89
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
90
- `}
69
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
91
70
  </button>
92
71
  ` : ''}
93
- ${handlers.onDeleteThread ? html `
94
- <nr-popconfirm
95
- title="${msg('Delete this conversation?')}"
96
- description="${msg('This action cannot be undone.')}"
97
- ok-text="${msg('Delete')}"
98
- cancel-text="${msg('Cancel')}"
99
- ok-type="danger"
100
- placement="right"
72
+ ${data.editingThreadId !== thread.id && (handlers.onRenameThread || handlers.onBookmarkThread || handlers.onDeleteThread) ? html `
73
+ <nr-dropdown
74
+ trigger="click"
75
+ placement="bottom-end"
76
+ size="small"
77
+ auto-close
101
78
  @click=${(e) => e.stopPropagation()}
102
- @nr-confirm=${() => handlers.onDeleteThread(thread.id)}
79
+ @nr-dropdown-item-click=${(e) => {
80
+ var _a, _b;
81
+ const id = (_b = (_a = e.detail) === null || _a === void 0 ? void 0 : _a.item) === null || _b === void 0 ? void 0 : _b.id;
82
+ if (id === 'rename' && handlers.onRenameThread) {
83
+ e.target.dispatchEvent(new CustomEvent('nr-thread-edit', {
84
+ bubbles: true,
85
+ composed: true,
86
+ detail: { threadId: thread.id }
87
+ }));
88
+ }
89
+ else if (id === 'bookmark' && handlers.onBookmarkThread) {
90
+ handlers.onBookmarkThread(thread.id);
91
+ }
92
+ else if (id === 'delete' && handlers.onDeleteThread) {
93
+ handlers.onDeleteThread(thread.id);
94
+ }
95
+ }}
96
+ .items=${[
97
+ ...(handlers.onRenameThread ? [{ id: 'rename', label: msg('Rename') }] : []),
98
+ ...(handlers.onBookmarkThread ? [{ id: 'bookmark', label: thread.bookmarked ? msg('Remove bookmark') : msg('Bookmark') }] : []),
99
+ ...(handlers.onDeleteThread ? [{ id: 'delete', label: msg('Delete') }] : []),
100
+ ]}
103
101
  >
104
102
  <button
105
103
  slot="trigger"
106
- class="thread-item__action-btn thread-item__delete"
107
- title="${msg('Delete conversation')}"
108
- part="thread-delete"
104
+ class="thread-item__action-btn thread-item__menu"
105
+ title="${msg('More options')}"
106
+ part="thread-menu"
107
+ aria-label="${msg('More options')}"
109
108
  >
110
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
109
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="5" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="19" cy="12" r="1.8"/></svg>
111
110
  </button>
112
- </nr-popconfirm>
111
+ </nr-dropdown>
113
112
  ` : ''}
114
113
  </div>
115
114
  </div>