tokimeki-image-editor 0.1.1 → 0.1.3

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 (39) hide show
  1. package/dist/components/AdjustTool.svelte +317 -0
  2. package/dist/components/AdjustTool.svelte.d.ts +9 -0
  3. package/dist/components/BlurTool.svelte +613 -0
  4. package/dist/components/BlurTool.svelte.d.ts +15 -0
  5. package/dist/components/Canvas.svelte +214 -0
  6. package/dist/components/Canvas.svelte.d.ts +17 -0
  7. package/dist/components/CropTool.svelte +942 -0
  8. package/dist/components/CropTool.svelte.d.ts +14 -0
  9. package/dist/components/ExportTool.svelte +191 -0
  10. package/dist/components/ExportTool.svelte.d.ts +10 -0
  11. package/dist/components/FilterTool.svelte +492 -0
  12. package/dist/components/FilterTool.svelte.d.ts +12 -0
  13. package/dist/components/ImageEditor.svelte +735 -0
  14. package/dist/components/ImageEditor.svelte.d.ts +12 -0
  15. package/dist/components/RotateTool.svelte +157 -0
  16. package/dist/components/RotateTool.svelte.d.ts +9 -0
  17. package/dist/components/StampTool.svelte +678 -0
  18. package/dist/components/StampTool.svelte.d.ts +15 -0
  19. package/dist/components/Toolbar.svelte +136 -0
  20. package/dist/components/Toolbar.svelte.d.ts +10 -0
  21. package/dist/config/stamps.d.ts +2 -0
  22. package/dist/config/stamps.js +22 -0
  23. package/dist/i18n/index.d.ts +1 -0
  24. package/dist/i18n/index.js +9 -0
  25. package/dist/i18n/locales/en.json +68 -0
  26. package/dist/i18n/locales/ja.json +68 -0
  27. package/dist/index.d.ts +4 -0
  28. package/dist/index.js +5 -0
  29. package/dist/types.d.ts +97 -0
  30. package/dist/types.js +1 -0
  31. package/dist/utils/adjustments.d.ts +26 -0
  32. package/dist/utils/adjustments.js +525 -0
  33. package/dist/utils/canvas.d.ts +30 -0
  34. package/dist/utils/canvas.js +293 -0
  35. package/dist/utils/filters.d.ts +18 -0
  36. package/dist/utils/filters.js +114 -0
  37. package/dist/utils/history.d.ts +15 -0
  38. package/dist/utils/history.js +67 -0
  39. package/package.json +1 -1
@@ -0,0 +1,735 @@
1
+ <script lang="ts">import { _ } from 'svelte-i18n';
2
+ import { Redo2, RotateCcw, Undo2 } from 'lucide-svelte';
3
+ import { loadImage, calculateFitScale, exportCanvas, downloadImage, applyTransform } from '../utils/canvas';
4
+ import { createEmptyHistory, createSnapshot, addToHistory, undo, redo, canUndo, canRedo } from '../utils/history';
5
+ import { createDefaultAdjustments } from '../utils/adjustments';
6
+ import Toolbar from './Toolbar.svelte';
7
+ import Canvas from './Canvas.svelte';
8
+ import CropTool from './CropTool.svelte';
9
+ import AdjustTool from './AdjustTool.svelte';
10
+ import FilterTool from './FilterTool.svelte';
11
+ import BlurTool from './BlurTool.svelte';
12
+ import StampTool from './StampTool.svelte';
13
+ import ExportTool from './ExportTool.svelte';
14
+ let { initialImage, width = 800, height = 600, isStandalone = false, onComplete, onCancel, onExport } = $props();
15
+ let state = $state({
16
+ mode: null,
17
+ imageData: {
18
+ original: null,
19
+ current: null,
20
+ width: 0,
21
+ height: 0
22
+ },
23
+ cropArea: null,
24
+ transform: {
25
+ rotation: 0,
26
+ flipHorizontal: false,
27
+ flipVertical: false,
28
+ scale: 1
29
+ },
30
+ adjustments: createDefaultAdjustments(),
31
+ exportOptions: {
32
+ format: 'png',
33
+ quality: 0.9
34
+ },
35
+ viewport: {
36
+ zoom: 1,
37
+ offsetX: 0,
38
+ offsetY: 0,
39
+ scale: 1
40
+ },
41
+ history: createEmptyHistory(),
42
+ blurAreas: [],
43
+ stampAreas: []
44
+ });
45
+ let canvasElement = $state(null);
46
+ let fileInput = $state(null);
47
+ let adjustmentThrottleTimer = null;
48
+ let pendingAdjustments = null;
49
+ let lastInitialImage = $state(undefined);
50
+ let clientWidth = $state(undefined);
51
+ let clientHeight = $state(undefined);
52
+ // Load initial image when provided
53
+ $effect(() => {
54
+ if (initialImage && initialImage !== lastInitialImage) {
55
+ lastInitialImage = initialImage;
56
+ if (typeof initialImage === 'string') {
57
+ // URL or Data URL
58
+ const img = new Image();
59
+ img.onload = () => {
60
+ state.imageData.original = img;
61
+ state.imageData.current = img;
62
+ state.imageData.width = img.width;
63
+ state.imageData.height = img.height;
64
+ // Calculate fit scale
65
+ const fitScale = calculateFitScale(img.width, img.height, width, height);
66
+ // Reset state
67
+ state.cropArea = null;
68
+ state.transform = {
69
+ rotation: 0,
70
+ flipHorizontal: false,
71
+ flipVertical: false,
72
+ scale: 1
73
+ };
74
+ state.viewport = {
75
+ zoom: 1,
76
+ offsetX: 0,
77
+ offsetY: 0,
78
+ scale: fitScale
79
+ };
80
+ state.blurAreas = [];
81
+ state.stampAreas = [];
82
+ // Reset history and save initial state
83
+ state.history = createEmptyHistory();
84
+ saveToHistory();
85
+ };
86
+ img.onerror = (error) => {
87
+ console.error('Failed to load initial image:', error);
88
+ };
89
+ img.src = initialImage;
90
+ }
91
+ else {
92
+ // File object
93
+ handleFileUpload(initialImage);
94
+ }
95
+ }
96
+ });
97
+ async function handleFileUpload(file) {
98
+ try {
99
+ const img = await loadImage(file);
100
+ state.imageData.original = img;
101
+ state.imageData.current = img;
102
+ state.imageData.width = img.width;
103
+ state.imageData.height = img.height;
104
+ // Calculate fit scale
105
+ const fitScale = calculateFitScale(img.width, img.height, width, height);
106
+ // Reset state
107
+ state.cropArea = null;
108
+ state.transform = {
109
+ rotation: 0,
110
+ flipHorizontal: false,
111
+ flipVertical: false,
112
+ scale: 1
113
+ };
114
+ state.viewport = {
115
+ zoom: 1,
116
+ offsetX: 0,
117
+ offsetY: 0,
118
+ scale: fitScale
119
+ };
120
+ state.blurAreas = [];
121
+ // Reset history and save initial state
122
+ state.history = createEmptyHistory();
123
+ saveToHistory();
124
+ }
125
+ catch (error) {
126
+ console.error('Failed to load image:', error);
127
+ }
128
+ }
129
+ function handleDrop(event) {
130
+ event.preventDefault();
131
+ const files = event.dataTransfer?.files;
132
+ if (files && files.length > 0) {
133
+ handleFileUpload(files[0]);
134
+ }
135
+ }
136
+ function handleDragOver(event) {
137
+ event.preventDefault();
138
+ }
139
+ function handleFileInputChange(event) {
140
+ const target = event.target;
141
+ if (target.files && target.files.length > 0) {
142
+ handleFileUpload(target.files[0]);
143
+ }
144
+ }
145
+ function handleModeChange(mode) {
146
+ state.mode = mode;
147
+ }
148
+ function handleCropApply(cropArea) {
149
+ if (!canvasElement || !state.imageData.original)
150
+ return;
151
+ state.cropArea = cropArea;
152
+ state.mode = null;
153
+ // Calculate scale to fit cropped area to canvas (fill canvas width/height)
154
+ const fitScale = calculateFitScale(cropArea.width, cropArea.height, width, height);
155
+ // Reset viewport to fit the cropped area
156
+ state.viewport = {
157
+ zoom: 1,
158
+ offsetX: 0,
159
+ offsetY: 0,
160
+ scale: fitScale
161
+ };
162
+ // Save to history
163
+ saveToHistory();
164
+ }
165
+ function handleTransformChange(transform) {
166
+ state.transform = { ...state.transform, ...transform };
167
+ // Save to history
168
+ saveToHistory();
169
+ }
170
+ function handleAdjustmentsChange(adjustments) {
171
+ // Immediately update state for responsive UI
172
+ state.adjustments = { ...state.adjustments, ...adjustments };
173
+ // Store pending adjustments for history
174
+ pendingAdjustments = { ...pendingAdjustments, ...adjustments };
175
+ // Clear existing timer
176
+ if (adjustmentThrottleTimer !== null) {
177
+ clearTimeout(adjustmentThrottleTimer);
178
+ }
179
+ // Only save to history after user stops adjusting (300ms delay)
180
+ adjustmentThrottleTimer = window.setTimeout(() => {
181
+ if (pendingAdjustments !== null) {
182
+ saveToHistory();
183
+ pendingAdjustments = null;
184
+ }
185
+ adjustmentThrottleTimer = null;
186
+ }, 300);
187
+ }
188
+ function handleFilterApply(adjustments) {
189
+ // Replace all adjustments with filter preset
190
+ state.adjustments = adjustments;
191
+ // Save to history immediately for filter changes
192
+ saveToHistory();
193
+ }
194
+ function handleBlurAreasChange(blurAreas) {
195
+ state.blurAreas = blurAreas;
196
+ saveToHistory();
197
+ }
198
+ function handleStampAreasChange(stampAreas) {
199
+ state.stampAreas = stampAreas;
200
+ saveToHistory();
201
+ }
202
+ function handleExport() {
203
+ if (!state.imageData.original)
204
+ return;
205
+ const exportCanvas = applyTransform(state.imageData.original, state.transform, state.adjustments, state.cropArea, state.blurAreas, state.stampAreas);
206
+ const dataUrl = exportCanvas.toDataURL(state.exportOptions.format === 'jpeg' ? 'image/jpeg' : 'image/png', state.exportOptions.quality);
207
+ const filename = `edited-image-${Date.now()}.${state.exportOptions.format}`;
208
+ downloadImage(dataUrl, filename);
209
+ if (onExport) {
210
+ onExport(dataUrl);
211
+ }
212
+ }
213
+ function handleComplete() {
214
+ if (!state.imageData.original || !onComplete)
215
+ return;
216
+ const exportCanvas = applyTransform(state.imageData.original, state.transform, state.adjustments, state.cropArea, state.blurAreas, state.stampAreas);
217
+ const dataUrl = exportCanvas.toDataURL(state.exportOptions.format === 'jpeg' ? 'image/jpeg' : 'image/png', state.exportOptions.quality);
218
+ // Convert to blob
219
+ exportCanvas.toBlob((blob) => {
220
+ if (blob) {
221
+ onComplete(dataUrl, blob);
222
+ }
223
+ }, state.exportOptions.format === 'jpeg' ? 'image/jpeg' : 'image/png', state.exportOptions.quality);
224
+ }
225
+ function handleCancel() {
226
+ if (onCancel) {
227
+ onCancel();
228
+ }
229
+ }
230
+ function handleReset() {
231
+ if (!state.imageData.original)
232
+ return;
233
+ const fitScale = calculateFitScale(state.imageData.original.width, state.imageData.original.height, width, height);
234
+ state.cropArea = null;
235
+ state.transform = {
236
+ rotation: 0,
237
+ flipHorizontal: false,
238
+ flipVertical: false,
239
+ scale: 1
240
+ };
241
+ state.viewport = {
242
+ zoom: 1,
243
+ offsetX: 0,
244
+ offsetY: 0,
245
+ scale: fitScale
246
+ };
247
+ }
248
+ function handleZoom(delta, centerX, centerY) {
249
+ const oldZoom = state.viewport.zoom;
250
+ const newZoom = Math.max(0.1, Math.min(5, oldZoom + delta));
251
+ if (centerX !== undefined && centerY !== undefined && canvasElement) {
252
+ // Zoom towards cursor position
253
+ const rect = canvasElement.getBoundingClientRect();
254
+ const x = centerX - rect.left - width / 2;
255
+ const y = centerY - rect.top - height / 2;
256
+ const zoomRatio = newZoom / oldZoom;
257
+ state.viewport.offsetX = x - (x - state.viewport.offsetX) * zoomRatio;
258
+ state.viewport.offsetY = y - (y - state.viewport.offsetY) * zoomRatio;
259
+ }
260
+ state.viewport.zoom = newZoom;
261
+ }
262
+ function saveToHistory() {
263
+ const snapshot = createSnapshot(state.cropArea, state.transform, state.adjustments, state.viewport, state.blurAreas, state.stampAreas);
264
+ state.history = addToHistory(state.history, snapshot);
265
+ }
266
+ function applySnapshot(snapshot) {
267
+ if (!snapshot)
268
+ return;
269
+ state.cropArea = snapshot.cropArea ? { ...snapshot.cropArea } : null;
270
+ state.transform = { ...snapshot.transform };
271
+ state.adjustments = { ...snapshot.adjustments };
272
+ state.viewport = { ...snapshot.viewport };
273
+ state.blurAreas = snapshot.blurAreas ? snapshot.blurAreas.map((area) => ({ ...area })) : [];
274
+ state.stampAreas = snapshot.stampAreas ? snapshot.stampAreas.map((area) => ({ ...area })) : [];
275
+ }
276
+ function handleUndo() {
277
+ const result = undo(state.history);
278
+ if (result.snapshot) {
279
+ state.history = result.history;
280
+ applySnapshot(result.snapshot);
281
+ }
282
+ }
283
+ function handleRedo() {
284
+ const result = redo(state.history);
285
+ if (result.snapshot) {
286
+ state.history = result.history;
287
+ applySnapshot(result.snapshot);
288
+ }
289
+ }
290
+ function openFileDialog() {
291
+ fileInput?.click();
292
+ }
293
+ function handleViewportChange(viewportUpdate) {
294
+ state.viewport = { ...state.viewport, ...viewportUpdate };
295
+ }
296
+ function handleKeyDown(event) {
297
+ // Check if input/textarea is focused
298
+ const target = event.target;
299
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
300
+ return;
301
+ }
302
+ // Ctrl+Z or Cmd+Z for undo
303
+ if ((event.ctrlKey || event.metaKey) && event.key === 'z' && !event.shiftKey) {
304
+ event.preventDefault();
305
+ handleUndo();
306
+ }
307
+ // Ctrl+Shift+Z or Cmd+Shift+Z for redo
308
+ if ((event.ctrlKey || event.metaKey) && event.key === 'z' && event.shiftKey) {
309
+ event.preventDefault();
310
+ handleRedo();
311
+ }
312
+ // Also support Ctrl+Y for redo
313
+ if ((event.ctrlKey || event.metaKey) && event.key === 'y') {
314
+ event.preventDefault();
315
+ handleRedo();
316
+ }
317
+ }
318
+ </script>
319
+
320
+ <svelte:window onkeydown={handleKeyDown} />
321
+
322
+ <div class="image-editor" style="width: {width}px;">
323
+ {#if !isStandalone && state.imageData.original}
324
+ <div class="embedded-controls">
325
+ <button class="embedded-btn embedded-btn-cancel" onclick={handleCancel}>
326
+ {$_('editor.cancel')}
327
+ </button>
328
+ <button class="embedded-btn embedded-btn-apply" onclick={handleComplete}>
329
+ {$_('editor.apply')}
330
+ </button>
331
+
332
+ <div class="editor-history-controls">
333
+ <button
334
+ class="editor-history-btn"
335
+ disabled={!canUndo(state.history)}
336
+ onclick={handleUndo}
337
+ title={$_('toolbar.undo')}
338
+ >
339
+ <Undo2 size={20} />
340
+ </button>
341
+
342
+ <button
343
+ class="editor-history-btn"
344
+ disabled={!canRedo(state.history)}
345
+ onclick={handleRedo}
346
+ title={$_('toolbar.redo')}
347
+ >
348
+ <Redo2 size={20} />
349
+ </button>
350
+
351
+ <button
352
+ class="editor-history-btn"
353
+ disabled={!!state.imageData.original}
354
+ onclick={handleReset}
355
+ title={$_('editor.reset')}
356
+ >
357
+ <RotateCcw size={20} />
358
+ </button>
359
+ </div>
360
+ </div>
361
+ {/if}
362
+
363
+ <div class="editor-body">
364
+ {#if !state.imageData.original && isStandalone}
365
+ <div
366
+ class="upload-area"
367
+ role="button"
368
+ tabindex="0"
369
+ ondrop={handleDrop}
370
+ ondragover={handleDragOver}
371
+ onclick={openFileDialog}
372
+ onkeydown={(e) => e.key === 'Enter' && openFileDialog()}
373
+ >
374
+ <p>{$_('editor.dropImageHere')}</p>
375
+ <input
376
+ bind:this={fileInput}
377
+ type="file"
378
+ accept="image/png,image/jpeg,image/jpg"
379
+ onchange={handleFileInputChange}
380
+ style="display: none;"
381
+ />
382
+ </div>
383
+ {:else if !state.imageData.original && !isStandalone}
384
+ <div class="no-image-message">
385
+ <p>{$_('editor.noImage')}</p>
386
+ </div>
387
+ {:else}
388
+ <div
389
+ class="canvas-container"
390
+ onwheel={(e) => {
391
+ e.preventDefault();
392
+ const delta = -e.deltaY * 0.001;
393
+ handleZoom(delta, e.clientX, e.clientY);
394
+ }}
395
+ bind:clientWidth
396
+ bind:clientHeight
397
+ >
398
+ <Canvas
399
+ bind:canvas={canvasElement}
400
+ width={clientWidth || width}
401
+ height={clientHeight || height}
402
+ image={state.imageData.original}
403
+ viewport={state.viewport}
404
+ transform={state.transform}
405
+ adjustments={state.adjustments}
406
+ cropArea={state.cropArea}
407
+ blurAreas={state.blurAreas}
408
+ stampAreas={state.stampAreas}
409
+ onZoom={handleZoom}
410
+ />
411
+
412
+ {#if state.mode === 'crop'}
413
+ <CropTool
414
+ canvas={canvasElement}
415
+ image={state.imageData.original}
416
+ viewport={state.viewport}
417
+ transform={state.transform}
418
+ onApply={handleCropApply}
419
+ onCancel={() => state.mode = null}
420
+ onViewportChange={handleViewportChange}
421
+ onTransformChange={handleTransformChange}
422
+ />
423
+ {:else if state.mode === 'blur'}
424
+ <BlurTool
425
+ canvas={canvasElement}
426
+ image={state.imageData.original}
427
+ viewport={state.viewport}
428
+ transform={state.transform}
429
+ blurAreas={state.blurAreas}
430
+ cropArea={state.cropArea}
431
+ onUpdate={handleBlurAreasChange}
432
+ onClose={() => state.mode = null}
433
+ onViewportChange={handleViewportChange}
434
+ />
435
+ {:else if state.mode === 'stamp'}
436
+ <StampTool
437
+ canvas={canvasElement}
438
+ image={state.imageData.original}
439
+ viewport={state.viewport}
440
+ transform={state.transform}
441
+ stampAreas={state.stampAreas}
442
+ cropArea={state.cropArea}
443
+ onUpdate={handleStampAreasChange}
444
+ onClose={() => state.mode = null}
445
+ onViewportChange={handleViewportChange}
446
+ />
447
+ {/if}
448
+
449
+ {#if state.mode === 'adjust'}
450
+ <div class="tools-panel">
451
+ <AdjustTool
452
+ adjustments={state.adjustments}
453
+ onChange={handleAdjustmentsChange}
454
+ onClose={() => state.mode = null}
455
+ />
456
+ </div>
457
+ {:else if state.mode === 'filter'}
458
+ <div class="tools-panel">
459
+ <FilterTool
460
+ image={state.imageData.original}
461
+ adjustments={state.adjustments}
462
+ transform={state.transform}
463
+ cropArea={state.cropArea}
464
+ onChange={handleFilterApply}
465
+ onClose={() => state.mode = null}
466
+ />
467
+ </div>
468
+ {:else if state.mode === 'export' && isStandalone}
469
+ <div class="tools-panel">
470
+ <ExportTool
471
+ options={state.exportOptions}
472
+ onChange={(options) => state.exportOptions = { ...state.exportOptions, ...options }}
473
+ onExport={handleExport}
474
+ onClose={() => state.mode = null}
475
+ />
476
+ </div>
477
+ {/if}
478
+ </div>
479
+ {/if}
480
+ </div>
481
+
482
+ <div class="editor-header">
483
+ <Toolbar
484
+ mode={state.mode}
485
+ hasImage={!!state.imageData.original}
486
+ canUndo={canUndo(state.history)}
487
+ canRedo={canRedo(state.history)}
488
+ isStandalone={isStandalone}
489
+ onModeChange={handleModeChange}
490
+ onReset={handleReset}
491
+ onUndo={handleUndo}
492
+ onRedo={handleRedo}
493
+ />
494
+ </div>
495
+ </div>
496
+
497
+ <style>
498
+ :global(input[type='range']) {
499
+ appearance: none;
500
+ }
501
+
502
+ .image-editor {
503
+ display: flex;
504
+ flex-direction: column;
505
+ gap: 1rem;
506
+ padding: 1rem;
507
+ background: #1a1a1a;
508
+ border-radius: 8px;
509
+ color: #fff;
510
+ }
511
+
512
+ @media (max-width: 767px) {
513
+
514
+ .image-editor {
515
+ width: 100% !important;
516
+ height: 90vh
517
+ }
518
+ }
519
+
520
+ .editor-header {
521
+ display: flex;
522
+ align-items: center;
523
+ width: 100%;
524
+ }
525
+
526
+ .editor-body {
527
+ display: flex;
528
+ flex-direction: column;
529
+ gap: 1rem;
530
+ }
531
+
532
+ @media (max-width: 767px) {
533
+
534
+ .editor-body {
535
+ flex: 1;
536
+ justify-content: center
537
+ }
538
+ }
539
+
540
+ .upload-area {
541
+ display: flex;
542
+ align-items: center;
543
+ justify-content: center;
544
+ min-height: 400px;
545
+ border: 2px dashed #666;
546
+ border-radius: 8px;
547
+ background: #2a2a2a;
548
+ cursor: pointer;
549
+ transition: all 0.2s;
550
+ }
551
+
552
+ .upload-area:hover {
553
+ border-color: #888;
554
+ background: #333;
555
+ }
556
+
557
+ .upload-area p {
558
+ margin: 0;
559
+ font-size: 1.1rem;
560
+ color: #999;
561
+ }
562
+
563
+ @media (max-width: 767px) {
564
+
565
+ .upload-area p {
566
+ font-size: .75rem
567
+ }
568
+ }
569
+
570
+ .canvas-container {
571
+ position: relative;
572
+ display: flex;
573
+ justify-content: center;
574
+ align-items: center;
575
+ background: #2a2a2a;
576
+ border-radius: 8px;
577
+ overflow: hidden;
578
+ }
579
+
580
+ @media (max-width: 767px) {
581
+
582
+ .canvas-container {
583
+ flex: 1;
584
+ min-height: 0
585
+ }
586
+ }
587
+
588
+ .tools-panel {
589
+ padding: 1rem;
590
+ background: #2a2a2a;
591
+ border-radius: 8px;
592
+ position: absolute;
593
+ width: min-content;
594
+ right: 1rem;
595
+ top: 1rem;
596
+ bottom: 1rem;
597
+ overflow-y: auto;
598
+ scrollbar-width: thin;
599
+ }
600
+
601
+ @media (max-width: 767px) {
602
+
603
+ .tools-panel {
604
+ position: absolute;
605
+ left: 0;
606
+ right: 0;
607
+ top: auto;
608
+ bottom: 0;
609
+ width: auto;
610
+ max-height: 50vh;
611
+ border-radius: 16px 16px 0 0;
612
+ z-index: 1001
613
+ }
614
+ }
615
+
616
+ .no-image-message {
617
+ display: flex;
618
+ align-items: center;
619
+ justify-content: center;
620
+ min-height: 400px;
621
+ background: #2a2a2a;
622
+ border-radius: 8px;
623
+ }
624
+
625
+ .no-image-message p {
626
+ margin: 0;
627
+ font-size: 1.1rem;
628
+ color: #999;
629
+ }
630
+
631
+ .embedded-controls {
632
+ position: relative;
633
+ display: flex;
634
+ justify-content: space-between;
635
+ gap: 1rem;
636
+ z-index: 1000;
637
+ }
638
+
639
+ @media (max-width: 767px) {
640
+
641
+ }
642
+
643
+ .embedded-btn {
644
+ padding: 0 1rem;
645
+ border: none;
646
+ font-size: .9rem;
647
+ font-weight: 600;
648
+ cursor: pointer;
649
+ transition: all 0.2s;
650
+ border-radius: 4px;
651
+ height: 36px;
652
+ }
653
+
654
+ .embedded-btn:hover {
655
+ opacity: .8;
656
+ }
657
+
658
+ @media (max-width: 767px) {
659
+
660
+ .embedded-btn {
661
+ padding: 0;
662
+ font-size: .75rem;
663
+ min-width: 80px
664
+ }
665
+ }
666
+
667
+ .embedded-btn-cancel {
668
+ background: #666;
669
+ color: #fff;
670
+ }
671
+
672
+ .embedded-btn-apply {
673
+ background: var(--primary-color, #63b97b);
674
+ color: #fff;
675
+ }
676
+
677
+ .experimental {
678
+ padding: 0 1rem;
679
+ background: var(--primary-color, #63b97b);
680
+ opacity: .7;
681
+ color: #fff;
682
+ height: 40px;
683
+ border-radius: 20px;
684
+ letter-spacing: .05em;
685
+ font-size: 16px;
686
+ font-weight: bold;
687
+ display: flex;
688
+ align-items: center;
689
+ margin: 0 auto 0 0;
690
+ }
691
+
692
+ .editor-history-controls {
693
+ position: absolute;
694
+ right: 0;
695
+ left: 0;
696
+ top: 0;
697
+ margin: auto;
698
+ width: fit-content;
699
+ display: flex;
700
+ align-items: center;
701
+ justify-content: center;
702
+ overflow: hidden;
703
+ border-radius: 8px;
704
+ border: 1px solid #444;
705
+ box-sizing: border-box;
706
+ }
707
+
708
+ .editor-history-btn {
709
+ appearance: none;
710
+ box-shadow: none;
711
+ border: none;
712
+ background: #333;
713
+ width: 36px;
714
+ height: 36px;
715
+ display: grid;
716
+ place-content: center;
717
+ color: #fff;
718
+ cursor: pointer;
719
+ transition: all .3s ease-in-out;
720
+ border-right: 1px solid #444;
721
+ }
722
+
723
+ .editor-history-btn:last-child {
724
+ border-right: none;
725
+ }
726
+
727
+ .editor-history-btn:hover {
728
+ opacity: .7;
729
+ }
730
+
731
+ .editor-history-btn:disabled {
732
+ background: #222;
733
+ color: #333;
734
+ cursor: not-allowed;
735
+ }</style>