@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.
- package/package.json +2 -2
- package/src/editor/RichTextEditor.svelte +956 -233
- package/src/overlay/ImgBox.svelte +178 -41
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
images,
|
|
16
16
|
index = $bindable(0),
|
|
17
17
|
open = $bindable(false),
|
|
18
|
-
backdrop = '
|
|
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
|
|
29
|
-
let
|
|
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/
|
|
32
|
+
blur: 'bg-black/70 backdrop-blur-xl',
|
|
33
33
|
dark: 'bg-black/90',
|
|
34
|
-
transparent: 'bg-
|
|
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')
|
|
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
|
-
|
|
103
|
-
|
|
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={
|
|
143
|
+
onclick={close}
|
|
106
144
|
aria-label="Fermer"
|
|
107
|
-
class="
|
|
145
|
+
class="imgbox-close"
|
|
108
146
|
>
|
|
109
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="
|
|
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="
|
|
155
|
+
class="imgbox-nav imgbox-nav-prev"
|
|
129
156
|
>
|
|
130
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="
|
|
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="
|
|
166
|
+
class="imgbox-nav imgbox-nav-next"
|
|
140
167
|
>
|
|
141
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="
|
|
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
|
|
172
|
+
<!-- Image container -->
|
|
173
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
147
174
|
<div
|
|
148
|
-
class="
|
|
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="
|
|
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
|
-
<!--
|
|
163
|
-
{#if
|
|
164
|
-
<div class="
|
|
165
|
-
{
|
|
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>
|