@myrialabs/clopen 0.2.4 → 0.2.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 (43) hide show
  1. package/backend/chat/stream-manager.ts +136 -10
  2. package/backend/database/queries/session-queries.ts +9 -0
  3. package/backend/engine/adapters/claude/error-handler.ts +7 -2
  4. package/backend/engine/adapters/claude/stream.ts +21 -3
  5. package/backend/engine/adapters/opencode/message-converter.ts +37 -2
  6. package/backend/index.ts +25 -3
  7. package/backend/preview/browser/browser-preview-service.ts +16 -17
  8. package/backend/preview/browser/browser-video-capture.ts +199 -156
  9. package/backend/preview/browser/scripts/video-stream.ts +3 -5
  10. package/backend/snapshot/helpers.ts +15 -2
  11. package/backend/ws/snapshot/restore.ts +43 -2
  12. package/backend/ws/user/crud.ts +6 -3
  13. package/frontend/components/chat/input/ChatInput.svelte +6 -1
  14. package/frontend/components/chat/input/components/ChatInputActions.svelte +10 -0
  15. package/frontend/components/chat/message/MessageBubble.svelte +22 -1
  16. package/frontend/components/chat/tools/components/FileHeader.svelte +19 -5
  17. package/frontend/components/chat/tools/components/TerminalCommand.svelte +2 -2
  18. package/frontend/components/chat/widgets/FloatingTodoList.svelte +30 -26
  19. package/frontend/components/common/media/MediaPreview.svelte +187 -0
  20. package/frontend/components/files/FileViewer.svelte +23 -144
  21. package/frontend/components/git/DiffViewer.svelte +50 -130
  22. package/frontend/components/git/FileChangeItem.svelte +22 -0
  23. package/frontend/components/preview/browser/BrowserPreview.svelte +1 -0
  24. package/frontend/components/preview/browser/components/Canvas.svelte +110 -21
  25. package/frontend/components/preview/browser/components/Container.svelte +2 -1
  26. package/frontend/components/preview/browser/components/Toolbar.svelte +1 -8
  27. package/frontend/components/preview/browser/core/coordinator.svelte.ts +12 -4
  28. package/frontend/components/terminal/TerminalTabs.svelte +1 -2
  29. package/frontend/components/workspace/DesktopNavigator.svelte +27 -1
  30. package/frontend/components/workspace/WorkspaceLayout.svelte +3 -1
  31. package/frontend/components/workspace/panels/FilesPanel.svelte +77 -1
  32. package/frontend/components/workspace/panels/GitPanel.svelte +72 -28
  33. package/frontend/services/chat/chat.service.ts +6 -1
  34. package/frontend/services/preview/browser/browser-webcodecs.service.ts +13 -5
  35. package/frontend/stores/core/files.svelte.ts +15 -1
  36. package/frontend/stores/ui/todo-panel.svelte.ts +39 -0
  37. package/frontend/utils/file-type.ts +68 -0
  38. package/index.html +1 -0
  39. package/package.json +1 -1
  40. package/shared/constants/binary-extensions.ts +40 -0
  41. package/shared/types/messaging/tool.ts +1 -0
  42. package/shared/utils/file-type-detection.ts +9 -1
  43. package/static/manifest.json +16 -0
@@ -9,6 +9,7 @@
9
9
  -->
10
10
 
11
11
  <script lang="ts">
12
+ import { tick } from 'svelte';
12
13
  import type { SDKMessageFormatter } from '$shared/types/database/schema';
13
14
  import type { IconName } from '$shared/types/ui/icons';
14
15
  import Card from '$frontend/components/common/display/Card.svelte';
@@ -46,6 +47,23 @@
46
47
  onShowTokenUsage: () => void;
47
48
  onShowDebug: () => void;
48
49
  } = $props();
50
+
51
+ let scrollContainer: HTMLDivElement | undefined = $state();
52
+
53
+ // Auto-scroll reasoning/system content to bottom while receiving partial text
54
+ $effect(() => {
55
+ if (roleCategory !== 'reasoning' && roleCategory !== 'system') return;
56
+ if (!scrollContainer) return;
57
+ // Track message content changes (partialText for streaming, message for final)
58
+ const _track = message.type === 'stream_event' && 'partialText' in message
59
+ ? message.partialText
60
+ : message;
61
+ tick().then(() => {
62
+ if (scrollContainer) {
63
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
64
+ }
65
+ });
66
+ });
49
67
  </script>
50
68
 
51
69
  <div class="relative overflow-hidden">
@@ -73,7 +91,10 @@
73
91
  />
74
92
 
75
93
  <!-- Message Content -->
76
- <div class="p-3 md:p-4">
94
+ <div
95
+ bind:this={scrollContainer}
96
+ class="p-3 md:p-4 {roleCategory === 'reasoning' || roleCategory === 'system' ? 'max-h-80 overflow-y-auto' : ''}"
97
+ >
77
98
  <div class="max-w-none space-y-4">
78
99
  <!-- Content rendering using MessageFormatter component -->
79
100
  <MessageFormatter {message} />
@@ -3,6 +3,15 @@
3
3
  import type { IconName } from '$shared/types/ui/icons';
4
4
  import { getFileIcon } from '$frontend/utils/file-icon-mappings';
5
5
  import { formatPath } from '../../shared/utils';
6
+ import { requestRevealFile } from '$frontend/stores/core/files.svelte';
7
+ import { getVisiblePanels, workspaceState } from '$frontend/stores/ui/workspace.svelte';
8
+
9
+ function handleClick() {
10
+ const visiblePanels = getVisiblePanels(workspaceState.layout);
11
+ if (visiblePanels.includes('files')) {
12
+ requestRevealFile(filePath);
13
+ }
14
+ }
6
15
 
7
16
  interface Props {
8
17
  filePath: string;
@@ -18,10 +27,15 @@
18
27
  </script>
19
28
 
20
29
  <div class={box ? "bg-white dark:bg-slate-800 rounded-md border border-slate-200/60 dark:border-slate-700/60 p-3" : ""}>
21
- <div class="flex items-center gap-3 mb-1">
22
- <Icon
23
- name={getFileIcon(displayFileName)}
24
- class="w-6 h-6 {iconColor || ''}"
30
+ <button
31
+ type="button"
32
+ class="flex items-center gap-3 mb-1 w-full text-left hover:opacity-80 transition-opacity cursor-pointer"
33
+ onclick={handleClick}
34
+ title="Reveal in Files panel"
35
+ >
36
+ <Icon
37
+ name={getFileIcon(displayFileName)}
38
+ class="w-6 h-6 {iconColor || ''}"
25
39
  />
26
40
  <div class="flex-1 min-w-0">
27
41
  <h3 class="font-medium text-slate-900 dark:text-slate-100 truncate">
@@ -31,7 +45,7 @@
31
45
  {formatPath(filePath)}
32
46
  </p>
33
47
  </div>
34
- </div>
48
+ </button>
35
49
 
36
50
  {#if badges.length > 0}
37
51
  <div class="flex gap-2 mt-3">
@@ -33,14 +33,14 @@
33
33
  <span class="text-xs font-medium text-slate-700 dark:text-slate-300">Command:</span>
34
34
  </div>
35
35
  {#if timeout}
36
- <div class="inline-block ml-auto text-xs bg-orange-100 dark:bg-orange-900 text-orange-700 dark:text-orange-300 px-2 py-0.5 rounded">
36
+ <div class="inline-block ml-auto text-3xs bg-orange-100 dark:bg-orange-900 text-orange-700 dark:text-orange-300 px-2 py-0.5 rounded">
37
37
  Timeout: {timeout}ms
38
38
  </div>
39
39
  {/if}
40
40
  </div>
41
41
 
42
42
  <!-- Terminal-style command display -->
43
- <div class="bg-slate-50 dark:bg-slate-950 border border-slate-200/60 dark:border-slate-800/60 rounded-md p-2.5 font-mono text-sm">
43
+ <div class="max-h-64 overflow-y-auto bg-slate-50 dark:bg-slate-950 border border-slate-200/60 dark:border-slate-800/60 rounded-md p-2.5 font-mono text-sm">
44
44
  <div class="flex items-start gap-2">
45
45
  <span class="text-green-600 dark:text-green-400 select-none">$</span>
46
46
  <div class="flex-1 text-slate-900 dark:text-slate-200 break-all">
@@ -8,17 +8,14 @@
8
8
  <script lang="ts">
9
9
  import { sessionState } from '$frontend/stores/core/sessions.svelte';
10
10
  import { appState } from '$frontend/stores/core/app.svelte';
11
+ import { todoPanelState, saveTodoPanelState } from '$frontend/stores/ui/todo-panel.svelte';
11
12
  import Icon from '$frontend/components/common/display/Icon.svelte';
12
13
  import { fly } from 'svelte/transition';
13
14
  import type { TodoWriteToolInput } from '$shared/types/messaging';
14
15
 
15
- let isExpanded = $state(true);
16
- let isMinimized = $state(false);
17
-
18
- // Drag & snap state
19
- let posY = $state(80);
16
+ // Drag-only local state (posX is always transient, posY syncs to store on drop)
17
+ let posY = $state(todoPanelState.posY);
20
18
  let posX = $state(0);
21
- let snapSide = $state<'left' | 'right'>('right');
22
19
  let isDragging = $state(false);
23
20
 
24
21
  // Minimized button ref for measuring width at snap time
@@ -28,18 +25,18 @@
28
25
  let _sx = 0, _sy = 0, _mx = 0, _my = 0, _hasDragged = false;
29
26
 
30
27
  function getPanelWidth() {
31
- return isExpanded ? 330 : 230;
28
+ return todoPanelState.isExpanded ? 330 : 230;
32
29
  }
33
30
 
34
31
  // Always use `left` property so CSS can transition in both directions
35
32
  const panelDisplayLeft = $derived(
36
- isDragging ? posX : snapSide === 'right' ? window.innerWidth - getPanelWidth() - 16 : 16
33
+ isDragging ? posX : todoPanelState.snapSide === 'right' ? window.innerWidth - getPanelWidth() - 16 : 16
37
34
  );
38
35
 
39
36
  const minimizedDisplayLeft = $derived(
40
37
  isDragging
41
38
  ? posX
42
- : snapSide === 'right'
39
+ : todoPanelState.snapSide === 'right'
43
40
  ? window.innerWidth - (minimizedBtn?.offsetWidth ?? 90) - 16
44
41
  : 16
45
42
  );
@@ -69,7 +66,9 @@
69
66
  function endDrag(e: PointerEvent) {
70
67
  if (!isDragging) return;
71
68
  isDragging = false;
72
- snapSide = posX + getPanelWidth() / 2 < window.innerWidth / 2 ? 'left' : 'right';
69
+ todoPanelState.snapSide = posX + getPanelWidth() / 2 < window.innerWidth / 2 ? 'left' : 'right';
70
+ todoPanelState.posY = posY;
71
+ saveTodoPanelState();
73
72
  }
74
73
 
75
74
  // --- Minimized button drag (click = restore, drag = move) ---
@@ -103,7 +102,9 @@
103
102
  return;
104
103
  }
105
104
  const el = e.currentTarget as HTMLElement;
106
- snapSide = posX + el.offsetWidth / 2 < window.innerWidth / 2 ? 'left' : 'right';
105
+ todoPanelState.snapSide = posX + el.offsetWidth / 2 < window.innerWidth / 2 ? 'left' : 'right';
106
+ todoPanelState.posY = posY;
107
+ saveTodoPanelState();
107
108
  }
108
109
 
109
110
  // Extract the latest TodoWrite data from messages
@@ -151,19 +152,22 @@
151
152
  const shouldShow = $derived(latestTodos !== null && latestTodos.length > 0);
152
153
 
153
154
  function toggleExpand() {
154
- if (!isMinimized) {
155
- isExpanded = !isExpanded;
155
+ if (!todoPanelState.isMinimized) {
156
+ todoPanelState.isExpanded = !todoPanelState.isExpanded;
157
+ saveTodoPanelState();
156
158
  }
157
159
  }
158
160
 
159
161
  function minimize() {
160
- isMinimized = true;
161
- isExpanded = false;
162
+ todoPanelState.isMinimized = true;
163
+ todoPanelState.isExpanded = false;
164
+ saveTodoPanelState();
162
165
  }
163
166
 
164
167
  function restore() {
165
- isMinimized = false;
166
- isExpanded = true;
168
+ todoPanelState.isMinimized = false;
169
+ todoPanelState.isExpanded = true;
170
+ saveTodoPanelState();
167
171
  }
168
172
 
169
173
  function getStatusIcon(status: string) {
@@ -194,7 +198,7 @@
194
198
  </script>
195
199
 
196
200
  {#if shouldShow && !appState.isRestoring}
197
- {#if isMinimized}
201
+ {#if todoPanelState.isMinimized}
198
202
  <!-- Minimized state - small floating button, draggable -->
199
203
  <button
200
204
  bind:this={minimizedBtn}
@@ -210,7 +214,7 @@
210
214
  cursor: {isDragging ? 'grabbing' : 'grab'};
211
215
  transition: {isDragging ? 'none' : 'left 0.25s ease, top 0.15s ease'};
212
216
  "
213
- transition:fly={{ x: snapSide === 'right' ? 100 : -100, duration: 200 }}
217
+ transition:fly={{ x: todoPanelState.snapSide === 'right' ? 100 : -100, duration: 200 }}
214
218
  >
215
219
  <Icon name="lucide:list-todo" class="w-5 h-5" />
216
220
  <span class="text-sm font-medium">{progress.completed}/{progress.total}</span>
@@ -222,11 +226,11 @@
222
226
  style="
223
227
  top: {posY}px;
224
228
  left: {panelDisplayLeft}px;
225
- width: {isExpanded ? '330px' : '230px'};
226
- max-height: {isExpanded ? '600px' : '56px'};
229
+ width: {todoPanelState.isExpanded ? '330px' : '230px'};
230
+ max-height: {todoPanelState.isExpanded ? '600px' : '56px'};
227
231
  transition: {isDragging ? 'none' : 'left 0.25s ease, top 0.15s ease, width 0.3s, max-height 0.3s'};
228
232
  "
229
- transition:fly={{ x: snapSide === 'right' ? 100 : -100, duration: 300 }}
233
+ transition:fly={{ x: todoPanelState.snapSide === 'right' ? 100 : -100, duration: 300 }}
230
234
  >
231
235
  <!-- Header (drag handle) -->
232
236
  <div
@@ -244,7 +248,7 @@
244
248
  <span class="text-sm font-semibold text-slate-900 dark:text-slate-100">
245
249
  Task Progress
246
250
  </span>
247
- {#if !isExpanded}
251
+ {#if !todoPanelState.isExpanded}
248
252
  <span class="text-xs text-slate-600 dark:text-slate-400">
249
253
  {progress.completed}/{progress.total} tasks ({progress.percentage}%)
250
254
  </span>
@@ -256,10 +260,10 @@
256
260
  <button
257
261
  onclick={toggleExpand}
258
262
  class="p-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
259
- title={isExpanded ? 'Collapse' : 'Expand'}
263
+ title={todoPanelState.isExpanded ? 'Collapse' : 'Expand'}
260
264
  >
261
265
  <Icon
262
- name={isExpanded ? 'lucide:chevron-up' : 'lucide:chevron-down'}
266
+ name={todoPanelState.isExpanded ? 'lucide:chevron-up' : 'lucide:chevron-down'}
263
267
  class="w-4 h-4 text-slate-600 dark:text-slate-400"
264
268
  />
265
269
  </button>
@@ -273,7 +277,7 @@
273
277
  </div>
274
278
  </div>
275
279
 
276
- {#if isExpanded}
280
+ {#if todoPanelState.isExpanded}
277
281
  <!-- Progress bar -->
278
282
  <div class="px-4 py-3 border-b border-slate-100 dark:border-slate-800">
279
283
  <div class="flex items-center justify-between mb-2">
@@ -0,0 +1,187 @@
1
+ <script lang="ts">
2
+ import { onDestroy, untrack } from 'svelte';
3
+ import Icon from '$frontend/components/common/display/Icon.svelte';
4
+ import LoadingSpinner from '$frontend/components/common/feedback/LoadingSpinner.svelte';
5
+ import { isImageFile, isSvgFile, isPdfFile, isAudioFile, isVideoFile } from '$frontend/utils/file-type';
6
+ import { debug } from '$shared/utils/logger';
7
+ import ws from '$frontend/utils/ws';
8
+
9
+ interface Props {
10
+ /** File name for type detection */
11
+ fileName: string;
12
+ /** Absolute path used to load binary content via ws */
13
+ filePath: string;
14
+ /** Optional text content for SVG inline rendering */
15
+ svgContent?: string;
16
+ }
17
+
18
+ const { fileName, filePath, svgContent }: Props = $props();
19
+
20
+ let blobUrl = $state<string | null>(null);
21
+ let pdfBlobUrl = $state<string | null>(null);
22
+ let mediaBlobUrl = $state<string | null>(null);
23
+ let isLoading = $state(false);
24
+
25
+ function cleanup() {
26
+ if (blobUrl) {
27
+ URL.revokeObjectURL(blobUrl);
28
+ blobUrl = null;
29
+ }
30
+ if (pdfBlobUrl) {
31
+ URL.revokeObjectURL(pdfBlobUrl);
32
+ pdfBlobUrl = null;
33
+ }
34
+ if (mediaBlobUrl) {
35
+ URL.revokeObjectURL(mediaBlobUrl);
36
+ mediaBlobUrl = null;
37
+ }
38
+ }
39
+
40
+ async function loadBinaryContent(path: string, name: string) {
41
+ isLoading = true;
42
+ try {
43
+ const response = await ws.http('files:read-content', { path });
44
+
45
+ if (response.content) {
46
+ const binaryString = atob(response.content);
47
+ const bytes = new Uint8Array(binaryString.length);
48
+ for (let i = 0; i < binaryString.length; i++) {
49
+ bytes[i] = binaryString.charCodeAt(i);
50
+ }
51
+ const blob = new Blob([bytes], { type: response.contentType || 'application/octet-stream' });
52
+
53
+ if (isPdfFile(name)) {
54
+ if (pdfBlobUrl) URL.revokeObjectURL(pdfBlobUrl);
55
+ pdfBlobUrl = URL.createObjectURL(blob);
56
+ } else if (isAudioFile(name) || isVideoFile(name)) {
57
+ if (mediaBlobUrl) URL.revokeObjectURL(mediaBlobUrl);
58
+ mediaBlobUrl = URL.createObjectURL(blob);
59
+ } else {
60
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
61
+ blobUrl = URL.createObjectURL(blob);
62
+ }
63
+ }
64
+ } catch (err) {
65
+ debug.error('file', 'Failed to load binary content:', err);
66
+ } finally {
67
+ isLoading = false;
68
+ }
69
+ }
70
+
71
+ $effect(() => {
72
+ const name = fileName;
73
+ const path = filePath;
74
+ untrack(() => {
75
+ cleanup();
76
+ if (name && path) {
77
+ loadBinaryContent(path, name);
78
+ }
79
+ });
80
+ });
81
+
82
+ onDestroy(cleanup);
83
+ </script>
84
+
85
+ {#if isImageFile(fileName)}
86
+ <div class="flex items-center justify-center h-full p-4 overflow-hidden checkerboard-bg">
87
+ {#if isLoading}
88
+ <LoadingSpinner size="lg" />
89
+ {:else if blobUrl}
90
+ <img
91
+ src={blobUrl}
92
+ alt={fileName}
93
+ class="max-w-full max-h-full object-contain"
94
+ />
95
+ {:else}
96
+ <div class="flex flex-col items-center gap-2 text-slate-500 text-xs">
97
+ <Icon name="lucide:image-off" class="w-8 h-8 opacity-40" />
98
+ <span>Failed to load preview</span>
99
+ </div>
100
+ {/if}
101
+ </div>
102
+ {:else if isSvgFile(fileName)}
103
+ <div class="flex items-center justify-center h-full p-4 overflow-auto checkerboard-bg">
104
+ {#if isLoading}
105
+ <LoadingSpinner size="lg" />
106
+ {:else if blobUrl}
107
+ <img
108
+ src={blobUrl}
109
+ alt={fileName}
110
+ class="max-w-full max-h-full object-contain"
111
+ />
112
+ {:else if svgContent}
113
+ <div class="max-w-full max-h-full flex items-center justify-center">
114
+ {@html svgContent}
115
+ </div>
116
+ {:else}
117
+ <div class="flex flex-col items-center gap-2 text-slate-500 text-xs">
118
+ <Icon name="lucide:image-off" class="w-8 h-8 opacity-40" />
119
+ <span>Failed to load preview</span>
120
+ </div>
121
+ {/if}
122
+ </div>
123
+ {:else if isPdfFile(fileName)}
124
+ <div class="h-full w-full">
125
+ {#if isLoading}
126
+ <div class="flex items-center justify-center h-full">
127
+ <LoadingSpinner size="lg" />
128
+ </div>
129
+ {:else if pdfBlobUrl}
130
+ <iframe
131
+ src={pdfBlobUrl}
132
+ title={fileName}
133
+ class="w-full h-full border-0"
134
+ ></iframe>
135
+ {:else}
136
+ <div class="flex flex-col items-center justify-center h-full gap-2 text-slate-500 text-xs">
137
+ <Icon name="lucide:file-x" class="w-8 h-8 opacity-40" />
138
+ <span>Failed to load PDF preview</span>
139
+ </div>
140
+ {/if}
141
+ </div>
142
+ {:else if isAudioFile(fileName)}
143
+ <div class="flex flex-col items-center justify-center h-full p-8 checkerboard-bg">
144
+ <Icon name="lucide:music" class="w-16 h-16 text-violet-400 mb-6" />
145
+ <h3 class="text-sm font-semibold text-slate-900 dark:text-slate-100 mb-4">
146
+ {fileName}
147
+ </h3>
148
+ {#if isLoading}
149
+ <LoadingSpinner size="lg" />
150
+ {:else if mediaBlobUrl}
151
+ <audio controls class="w-full max-w-md" src={mediaBlobUrl}>
152
+ Your browser does not support the audio element.
153
+ </audio>
154
+ {:else}
155
+ <div class="flex flex-col items-center gap-2 text-slate-500 text-xs">
156
+ <Icon name="lucide:music" class="w-8 h-8 opacity-40" />
157
+ <span>Failed to load audio</span>
158
+ </div>
159
+ {/if}
160
+ </div>
161
+ {:else if isVideoFile(fileName)}
162
+ <div class="flex items-center justify-center h-full p-4 overflow-hidden checkerboard-bg">
163
+ {#if isLoading}
164
+ <LoadingSpinner size="lg" />
165
+ {:else if mediaBlobUrl}
166
+ <!-- svelte-ignore a11y_media_has_caption -->
167
+ <video controls class="max-w-full max-h-full object-contain" src={mediaBlobUrl}>
168
+ Your browser does not support the video element.
169
+ </video>
170
+ {:else}
171
+ <div class="flex flex-col items-center gap-2 text-slate-500 text-xs">
172
+ <Icon name="lucide:video-off" class="w-8 h-8 opacity-40" />
173
+ <span>Failed to load video</span>
174
+ </div>
175
+ {/if}
176
+ </div>
177
+ {/if}
178
+
179
+ <style>
180
+ .checkerboard-bg {
181
+ background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2220%22%20height%3D%2220%22%3E%3Crect%20width%3D%2220%22%20height%3D%2220%22%20fill%3D%22%23f0f0f0%22%2F%3E%3Crect%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23e0e0e0%22%2F%3E%3Crect%20x%3D%2210%22%20y%3D%2210%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23e0e0e0%22%2F%3E%3C%2Fsvg%3E');
182
+ }
183
+
184
+ :global(.dark) .checkerboard-bg {
185
+ background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2220%22%20height%3D%2220%22%3E%3Crect%20width%3D%2220%22%20height%3D%2220%22%20fill%3D%22%23181818%22%2F%3E%3Crect%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23222222%22%2F%3E%3Crect%20x%3D%2210%22%20y%3D%2210%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23222222%22%2F%3E%3C%2Fsvg%3E');
186
+ }
187
+ </style>