@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.
- package/package.json +2 -2
- package/src/editor/RichTextEditor.svelte +956 -233
- package/src/overlay/ImgBox.svelte +155 -34
|
@@ -26,8 +26,7 @@
|
|
|
26
26
|
let translateY = $state(0)
|
|
27
27
|
let dragging = $state(false)
|
|
28
28
|
let visible = $state(false)
|
|
29
|
-
let
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
129
|
+
use:teleport
|
|
130
|
+
data-imgbox-root
|
|
131
|
+
class="imgbox-root {className}"
|
|
132
|
+
style="opacity: {visible ? 1 : 0};"
|
|
120
133
|
>
|
|
121
|
-
<!--
|
|
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="
|
|
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="
|
|
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="
|
|
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
|
|
172
|
+
<!-- Image container -->
|
|
173
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
168
174
|
<div
|
|
169
|
-
class="
|
|
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="
|
|
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
|
|
188
|
+
<!-- Counter -->
|
|
184
189
|
{#if images.length > 1}
|
|
185
|
-
<div class="
|
|
186
|
-
<span class="text-white/
|
|
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>
|