@karbonjs/ui-svelte 0.2.2 → 0.2.4

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.
@@ -15,7 +15,7 @@
15
15
  images,
16
16
  index = $bindable(0),
17
17
  open = $bindable(false),
18
- backdrop = 'dark',
18
+ backdrop = 'blur',
19
19
  captions = [],
20
20
  class: className = '',
21
21
  onclose
@@ -25,13 +25,13 @@
25
25
  let translateX = $state(0)
26
26
  let translateY = $state(0)
27
27
  let dragging = $state(false)
28
- let startX = 0
29
- let startY = 0
28
+ let visible = $state(false)
29
+ let portal: HTMLDivElement | null = $state(null)
30
30
 
31
31
  const backdropClasses: Record<string, string> = {
32
- blur: 'bg-black/80 backdrop-blur-md',
32
+ blur: 'bg-black/70 backdrop-blur-xl',
33
33
  dark: 'bg-black/90',
34
- transparent: 'bg-transparent',
34
+ transparent: 'bg-black/40 backdrop-blur-sm',
35
35
  none: ''
36
36
  }
37
37
 
@@ -39,6 +39,28 @@
39
39
  const hasNext = $derived(index < images.length - 1)
40
40
  const caption = $derived(captions[index] ?? '')
41
41
 
42
+ // Teleport to <body> to escape any parent transform/overflow
43
+ function teleport(node: HTMLElement) {
44
+ document.body.appendChild(node)
45
+ return {
46
+ destroy() {
47
+ node.remove()
48
+ }
49
+ }
50
+ }
51
+
52
+ $effect(() => {
53
+ if (open) {
54
+ requestAnimationFrame(() => { visible = true })
55
+ return () => { visible = false }
56
+ }
57
+ })
58
+
59
+ function close() {
60
+ visible = false
61
+ setTimeout(() => onclose(), 200)
62
+ }
63
+
42
64
  function prev() {
43
65
  if (hasPrev) { index--; resetTransform() }
44
66
  }
@@ -64,7 +86,7 @@
64
86
 
65
87
  function handleKeydown(e: KeyboardEvent) {
66
88
  if (!open) return
67
- if (e.key === 'Escape') onclose()
89
+ if (e.key === 'Escape') close()
68
90
  if (e.key === 'ArrowLeft') prev()
69
91
  if (e.key === 'ArrowRight') next()
70
92
  if (e.key === '+' || e.key === '=') zoomIn()
@@ -78,6 +100,9 @@
78
100
  startY = e.clientY - translateY
79
101
  }
80
102
 
103
+ let startX = 0
104
+ let startY = 0
105
+
81
106
  function handleMouseMove(e: MouseEvent) {
82
107
  if (!dragging) return
83
108
  translateX = e.clientX - startX
@@ -99,35 +124,37 @@
99
124
 
100
125
  {#if open && images.length > 0}
101
126
  <!-- svelte-ignore a11y_no_static_element_interactions -->
102
- <div class="fixed inset-0 z-[70] flex items-center justify-center {backdropClasses[backdrop]} {className}">
103
- <!-- Close button -->
127
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
128
+ <div
129
+ use:teleport
130
+ data-imgbox-root
131
+ class="imgbox-root {className}"
132
+ style="opacity: {visible ? 1 : 0};"
133
+ >
134
+ <!-- Backdrop — click here to close, captures all stray events -->
135
+ <div
136
+ class="imgbox-backdrop {backdropClasses[backdrop]}"
137
+ onclick={() => { if (scale <= 1) close() }}
138
+ role="presentation"
139
+ ></div>
140
+
141
+ <!-- Close button — top right -->
104
142
  <button
105
- onclick={onclose}
143
+ onclick={close}
106
144
  aria-label="Fermer"
107
- class="absolute top-4 right-4 z-10 rounded-full p-2 text-white/60 hover:text-white hover:bg-white/10 transition-colors cursor-pointer"
145
+ class="imgbox-close"
108
146
  >
109
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
147
+ <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
110
148
  </button>
111
149
 
112
- <!-- Zoom controls -->
113
- <div class="absolute top-4 left-1/2 -translate-x-1/2 z-10 flex items-center gap-2 bg-black/40 rounded-full px-3 py-1.5">
114
- <button onclick={zoomOut} aria-label="Dézoomer" class="text-white/60 hover:text-white transition-colors cursor-pointer p-1">
115
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="8" x2="14" y1="11" y2="11"/></svg>
116
- </button>
117
- <span class="text-white/80 text-xs font-medium min-w-[3rem] text-center">{Math.round(scale * 100)}%</span>
118
- <button onclick={zoomIn} aria-label="Zoomer" class="text-white/60 hover:text-white transition-colors cursor-pointer p-1">
119
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="11" x2="11" y1="8" y2="14"/><line x1="8" x2="14" y1="11" y2="11"/></svg>
120
- </button>
121
- </div>
122
-
123
150
  <!-- Prev -->
124
151
  {#if hasPrev}
125
152
  <button
126
153
  onclick={prev}
127
154
  aria-label="Image précédente"
128
- class="absolute left-4 z-10 rounded-full p-2 text-white/60 hover:text-white hover:bg-white/10 transition-colors cursor-pointer"
155
+ class="imgbox-nav imgbox-nav-prev"
129
156
  >
130
- <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
157
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
131
158
  </button>
132
159
  {/if}
133
160
 
@@ -136,39 +163,149 @@
136
163
  <button
137
164
  onclick={next}
138
165
  aria-label="Image suivante"
139
- class="absolute right-4 z-10 rounded-full p-2 text-white/60 hover:text-white hover:bg-white/10 transition-colors cursor-pointer"
166
+ class="imgbox-nav imgbox-nav-next"
140
167
  >
141
- <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
168
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
142
169
  </button>
143
170
  {/if}
144
171
 
145
- <!-- Image -->
146
- <!-- svelte-ignore a11y_click_events_have_key_events -->
172
+ <!-- Image container -->
173
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
147
174
  <div
148
- class="flex items-center justify-center w-full h-full p-16 {scale > 1 ? 'cursor-grab' : ''} {dragging ? 'cursor-grabbing' : ''}"
149
- onclick={(e) => { if (e.target === e.currentTarget && scale <= 1) onclose() }}
175
+ class="imgbox-stage {scale > 1 ? 'cursor-grab' : ''} {dragging ? '!cursor-grabbing' : ''}"
150
176
  onmousedown={handleMouseDown}
151
177
  onwheel={handleWheel}
152
178
  >
153
179
  <img
154
180
  src={images[index]}
155
181
  alt={caption || `Image ${index + 1}`}
156
- class="max-w-full max-h-full object-contain select-none transition-transform duration-150"
157
- style="transform: scale({scale}) translate({translateX / scale}px, {translateY / scale}px)"
182
+ class="imgbox-image"
183
+ style="transform: scale({visible ? scale : 0.9}) translate({translateX / scale}px, {translateY / scale}px); opacity: {visible ? 1 : 0};"
158
184
  draggable="false"
159
185
  />
160
186
  </div>
161
187
 
162
- <!-- Caption + counter -->
163
- {#if caption || images.length > 1}
164
- <div class="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 text-center">
165
- {#if caption}
166
- <p class="text-white/80 text-sm mb-1">{caption}</p>
167
- {/if}
168
- {#if images.length > 1}
169
- <span class="text-white/40 text-xs">{index + 1} / {images.length}</span>
170
- {/if}
188
+ <!-- Counter -->
189
+ {#if images.length > 1}
190
+ <div class="imgbox-counter">
191
+ <span class="text-white/40 text-xs bg-black/30 rounded-full px-2 py-0.5">{index + 1} / {images.length}</span>
171
192
  </div>
172
193
  {/if}
194
+
195
+ <!-- Zoom controls — bottom center, full-width wrapper -->
196
+ <div class="imgbox-controls" style="opacity: {visible ? 1 : 0};">
197
+ <div class="flex items-center gap-1 bg-black/40 rounded-full px-3 py-1.5">
198
+ <button onclick={zoomOut} aria-label="Dézoomer" class="text-white/60 hover:text-white transition-colors cursor-pointer p-1">
199
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="8" x2="14" y1="11" y2="11"/></svg>
200
+ </button>
201
+ <span class="text-white/80 text-xs font-medium min-w-[3rem] text-center">{Math.round(scale * 100)}%</span>
202
+ <button onclick={zoomIn} aria-label="Zoomer" class="text-white/60 hover:text-white transition-colors cursor-pointer p-1">
203
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="11" x2="11" y1="8" y2="14"/><line x1="8" x2="14" y1="11" y2="11"/></svg>
204
+ </button>
205
+ <button onclick={resetTransform} aria-label="Réinitialiser" class="text-white/60 hover:text-white transition-colors cursor-pointer p-1 ml-1 border-l border-white/20 pl-2">
206
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
207
+ </button>
208
+ </div>
209
+ </div>
173
210
  </div>
174
211
  {/if}
212
+
213
+ <style>
214
+ .imgbox-root {
215
+ position: fixed;
216
+ inset: 0;
217
+ z-index: 99999;
218
+ display: flex;
219
+ align-items: center;
220
+ justify-content: center;
221
+ pointer-events: auto;
222
+ transition: opacity 0.2s ease;
223
+ }
224
+
225
+ .imgbox-backdrop {
226
+ position: absolute;
227
+ inset: 0;
228
+ z-index: 0;
229
+ pointer-events: auto;
230
+ cursor: default;
231
+ }
232
+
233
+ .imgbox-close {
234
+ @apply rounded-full p-2.5 text-white/70 bg-black/30 transition-all cursor-pointer;
235
+ position: absolute;
236
+ top: 16px;
237
+ right: 16px;
238
+ z-index: 10;
239
+ pointer-events: auto;
240
+ border: none;
241
+ }
242
+
243
+ .imgbox-close:hover {
244
+ @apply text-white bg-black/50;
245
+ }
246
+
247
+ .imgbox-nav {
248
+ @apply rounded-full p-3 text-white/60 bg-black/20 transition-all cursor-pointer;
249
+ position: absolute;
250
+ top: 50%;
251
+ transform: translateY(-50%);
252
+ z-index: 10;
253
+ pointer-events: auto;
254
+ border: none;
255
+ }
256
+
257
+ .imgbox-nav:hover {
258
+ @apply text-white bg-black/50;
259
+ }
260
+
261
+ .imgbox-nav-prev { left: 16px; }
262
+ .imgbox-nav-next { right: 16px; }
263
+
264
+ .imgbox-stage {
265
+ position: relative;
266
+ z-index: 1;
267
+ display: flex;
268
+ align-items: center;
269
+ justify-content: center;
270
+ width: 100%;
271
+ height: 100%;
272
+ padding: 48px;
273
+ pointer-events: auto;
274
+ }
275
+
276
+ .imgbox-image {
277
+ max-width: 100%;
278
+ max-height: 100%;
279
+ object-fit: contain;
280
+ user-select: none;
281
+ transition: transform 0.15s ease;
282
+ pointer-events: none;
283
+ }
284
+
285
+ .imgbox-counter {
286
+ position: absolute;
287
+ bottom: 56px;
288
+ left: 0;
289
+ right: 0;
290
+ display: flex;
291
+ justify-content: center;
292
+ z-index: 10;
293
+ pointer-events: none;
294
+ }
295
+
296
+ .imgbox-controls {
297
+ position: absolute;
298
+ bottom: 16px;
299
+ left: 0;
300
+ right: 0;
301
+ display: flex;
302
+ justify-content: center;
303
+ z-index: 10;
304
+ pointer-events: none;
305
+ transition: opacity 0.3s ease;
306
+ }
307
+
308
+ .imgbox-controls > div {
309
+ pointer-events: auto;
310
+ }
311
+ </style>