@karbonjs/ui-svelte 0.2.3 → 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.
@@ -26,8 +26,7 @@
26
26
  let translateY = $state(0)
27
27
  let dragging = $state(false)
28
28
  let visible = $state(false)
29
- let startX = 0
30
- let startY = 0
29
+ let portal: HTMLDivElement | null = $state(null)
31
30
 
32
31
  const backdropClasses: Record<string, string> = {
33
32
  blur: 'bg-black/70 backdrop-blur-xl',
@@ -40,12 +39,20 @@
40
39
  const hasNext = $derived(index < images.length - 1)
41
40
  const caption = $derived(captions[index] ?? '')
42
41
 
43
- // Animate in
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
+
44
52
  $effect(() => {
45
53
  if (open) {
46
54
  requestAnimationFrame(() => { visible = true })
47
- } else {
48
- visible = false
55
+ return () => { visible = false }
49
56
  }
50
57
  })
51
58
 
@@ -93,6 +100,9 @@
93
100
  startY = e.clientY - translateY
94
101
  }
95
102
 
103
+ let startX = 0
104
+ let startY = 0
105
+
96
106
  function handleMouseMove(e: MouseEvent) {
97
107
  if (!dragging) return
98
108
  translateX = e.clientX - startX
@@ -114,39 +124,35 @@
114
124
 
115
125
  {#if open && images.length > 0}
116
126
  <!-- svelte-ignore a11y_no_static_element_interactions -->
127
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
117
128
  <div
118
- class="fixed inset-0 z-[99999] flex items-center justify-center transition-opacity duration-200 {backdropClasses[backdrop]} {className}"
119
- style="opacity: {visible ? 1 : 0}"
129
+ use:teleport
130
+ data-imgbox-root
131
+ class="imgbox-root {className}"
132
+ style="opacity: {visible ? 1 : 0};"
120
133
  >
121
- <!-- Close button -->
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 -->
122
142
  <button
123
143
  onclick={close}
124
144
  aria-label="Fermer"
125
- class="absolute top-4 right-4 z-10 rounded-full p-2.5 text-white/70 hover:text-white bg-black/30 hover:bg-black/50 transition-all cursor-pointer"
145
+ class="imgbox-close"
126
146
  >
127
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>
128
148
  </button>
129
149
 
130
- <!-- Zoom controls -->
131
- <div class="absolute top-4 left-1/2 -translate-x-1/2 z-10 flex items-center gap-1 bg-black/40 rounded-full px-3 py-1.5 transition-opacity duration-300" style="opacity: {visible ? 1 : 0}">
132
- <button onclick={zoomOut} aria-label="Dézoomer" class="text-white/60 hover:text-white transition-colors cursor-pointer p-1">
133
- <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>
134
- </button>
135
- <span class="text-white/80 text-xs font-medium min-w-[3rem] text-center">{Math.round(scale * 100)}%</span>
136
- <button onclick={zoomIn} aria-label="Zoomer" class="text-white/60 hover:text-white transition-colors cursor-pointer p-1">
137
- <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>
138
- </button>
139
- <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">
140
- <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>
141
- </button>
142
- </div>
143
-
144
150
  <!-- Prev -->
145
151
  {#if hasPrev}
146
152
  <button
147
153
  onclick={prev}
148
154
  aria-label="Image précédente"
149
- class="absolute left-4 z-10 rounded-full p-3 text-white/60 hover:text-white bg-black/20 hover:bg-black/50 transition-all cursor-pointer"
155
+ class="imgbox-nav imgbox-nav-prev"
150
156
  >
151
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>
152
158
  </button>
@@ -157,34 +163,149 @@
157
163
  <button
158
164
  onclick={next}
159
165
  aria-label="Image suivante"
160
- class="absolute right-4 z-10 rounded-full p-3 text-white/60 hover:text-white bg-black/20 hover:bg-black/50 transition-all cursor-pointer"
166
+ class="imgbox-nav imgbox-nav-next"
161
167
  >
162
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>
163
169
  </button>
164
170
  {/if}
165
171
 
166
- <!-- Image -->
167
- <!-- svelte-ignore a11y_click_events_have_key_events -->
172
+ <!-- Image container -->
173
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
168
174
  <div
169
- class="flex items-center justify-center w-full h-full p-12 {scale > 1 ? 'cursor-grab' : ''} {dragging ? 'cursor-grabbing' : ''}"
170
- onclick={(e) => { if (e.target === e.currentTarget && scale <= 1) close() }}
175
+ class="imgbox-stage {scale > 1 ? 'cursor-grab' : ''} {dragging ? '!cursor-grabbing' : ''}"
171
176
  onmousedown={handleMouseDown}
172
177
  onwheel={handleWheel}
173
178
  >
174
179
  <img
175
180
  src={images[index]}
176
181
  alt={caption || `Image ${index + 1}`}
177
- class="max-w-full max-h-full object-contain select-none transition-all duration-200"
178
- style="transform: scale({visible ? scale : 0.9}) translate({translateX / scale}px, {translateY / scale}px); opacity: {visible ? 1 : 0}"
182
+ class="imgbox-image"
183
+ style="transform: scale({visible ? scale : 0.9}) translate({translateX / scale}px, {translateY / scale}px); opacity: {visible ? 1 : 0};"
179
184
  draggable="false"
180
185
  />
181
186
  </div>
182
187
 
183
- <!-- Counter only (no caption on image) -->
188
+ <!-- Counter -->
184
189
  {#if images.length > 1}
185
- <div class="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 bg-black/40 rounded-full px-3 py-1">
186
- <span class="text-white/60 text-xs">{index + 1} / {images.length}</span>
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>
187
192
  </div>
188
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>
189
210
  </div>
190
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>