@bendyline/squisq-editor-react 1.0.1 → 1.1.1

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 (65) hide show
  1. package/dist/DropZoneOverlay.d.ts +24 -0
  2. package/dist/DropZoneOverlay.d.ts.map +1 -0
  3. package/dist/DropZoneOverlay.js +53 -0
  4. package/dist/DropZoneOverlay.js.map +1 -0
  5. package/dist/EditorContext.d.ts +10 -2
  6. package/dist/EditorContext.d.ts.map +1 -1
  7. package/dist/EditorContext.js +49 -1
  8. package/dist/EditorContext.js.map +1 -1
  9. package/dist/EditorShell.d.ts +16 -1
  10. package/dist/EditorShell.d.ts.map +1 -1
  11. package/dist/EditorShell.js +55 -8
  12. package/dist/EditorShell.js.map +1 -1
  13. package/dist/ImageNodeView.d.ts +15 -0
  14. package/dist/ImageNodeView.d.ts.map +1 -0
  15. package/dist/ImageNodeView.js +52 -0
  16. package/dist/ImageNodeView.js.map +1 -0
  17. package/dist/MediaBin.d.ts +18 -0
  18. package/dist/MediaBin.d.ts.map +1 -0
  19. package/dist/MediaBin.js +141 -0
  20. package/dist/MediaBin.js.map +1 -0
  21. package/dist/PreviewControls.d.ts +41 -0
  22. package/dist/PreviewControls.d.ts.map +1 -0
  23. package/dist/PreviewControls.js +201 -0
  24. package/dist/PreviewControls.js.map +1 -0
  25. package/dist/PreviewPanel.d.ts +7 -7
  26. package/dist/PreviewPanel.d.ts.map +1 -1
  27. package/dist/PreviewPanel.js +183 -199
  28. package/dist/PreviewPanel.js.map +1 -1
  29. package/dist/Toolbar.d.ts +12 -1
  30. package/dist/Toolbar.d.ts.map +1 -1
  31. package/dist/Toolbar.js +4 -12
  32. package/dist/Toolbar.js.map +1 -1
  33. package/dist/WysiwygEditor.d.ts.map +1 -1
  34. package/dist/WysiwygEditor.js +3 -1
  35. package/dist/WysiwygEditor.js.map +1 -1
  36. package/dist/hooks/useFileDrop.d.ts +41 -0
  37. package/dist/hooks/useFileDrop.d.ts.map +1 -0
  38. package/dist/hooks/useFileDrop.js +167 -0
  39. package/dist/hooks/useFileDrop.js.map +1 -0
  40. package/dist/index.d.ts +9 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +6 -0
  43. package/dist/index.js.map +1 -1
  44. package/dist/tiptapBridge.d.ts.map +1 -1
  45. package/dist/tiptapBridge.js +4 -5
  46. package/dist/tiptapBridge.js.map +1 -1
  47. package/dist/utils/dropUtils.d.ts +36 -0
  48. package/dist/utils/dropUtils.d.ts.map +1 -0
  49. package/dist/utils/dropUtils.js +71 -0
  50. package/dist/utils/dropUtils.js.map +1 -0
  51. package/package.json +5 -3
  52. package/src/DropZoneOverlay.tsx +137 -0
  53. package/src/EditorContext.tsx +64 -1
  54. package/src/EditorShell.tsx +153 -20
  55. package/src/ImageNodeView.tsx +70 -0
  56. package/src/MediaBin.tsx +223 -0
  57. package/src/PreviewControls.tsx +340 -0
  58. package/src/PreviewPanel.tsx +216 -287
  59. package/src/Toolbar.tsx +40 -3
  60. package/src/WysiwygEditor.tsx +3 -1
  61. package/src/hooks/useFileDrop.ts +226 -0
  62. package/src/index.ts +29 -0
  63. package/src/styles/editor.css +349 -8
  64. package/src/tiptapBridge.ts +5 -6
  65. package/src/utils/dropUtils.ts +88 -0
@@ -21,6 +21,7 @@ import TaskList from '@tiptap/extension-task-list';
21
21
  import TaskItem from '@tiptap/extension-task-item';
22
22
  import Placeholder from '@tiptap/extension-placeholder';
23
23
  import { HeadingWithTemplate } from './TemplateAnnotation';
24
+ import { ImageWithMediaProvider } from './ImageNodeView';
24
25
  import { useEditorContext } from './EditorContext';
25
26
  import { markdownToTiptap, tiptapToMarkdown } from './tiptapBridge';
26
27
 
@@ -73,6 +74,7 @@ export function WysiwygEditor({
73
74
  TableHeader,
74
75
  TaskList,
75
76
  TaskItem.configure({ nested: true }),
77
+ ImageWithMediaProvider.configure({ inline: false }),
76
78
  Placeholder.configure({ placeholder }),
77
79
  ],
78
80
  content: markdownToTiptap(stripFrontmatter(markdownSource).body),
@@ -117,7 +119,7 @@ export function WysiwygEditor({
117
119
 
118
120
  return (
119
121
  <div
120
- className={className}
122
+ className={`squisq-wysiwyg-container${className ? ` ${className}` : ''}`}
121
123
  style={{ width: '100%', height: '100%', overflow: 'auto' }}
122
124
  data-testid="wysiwyg-container"
123
125
  >
@@ -0,0 +1,226 @@
1
+ /**
2
+ * useFileDrop
3
+ *
4
+ * React hook that manages HTML5 drag-and-drop state for file uploads.
5
+ * Tracks whether files are being dragged over the target element,
6
+ * classifies dragged file types (media vs text), and dispatches
7
+ * drop events to callers.
8
+ */
9
+
10
+ import { useCallback, useRef, useState } from 'react';
11
+
12
+ // ─── File classification ────────────────────────────────
13
+
14
+ const MEDIA_EXTENSIONS = new Set([
15
+ 'png',
16
+ 'jpg',
17
+ 'jpeg',
18
+ 'gif',
19
+ 'webp',
20
+ 'svg',
21
+ 'avif',
22
+ 'ico',
23
+ 'mp4',
24
+ 'webm',
25
+ 'mov',
26
+ 'avi',
27
+ 'mp3',
28
+ 'wav',
29
+ 'ogg',
30
+ 'aac',
31
+ 'm4a',
32
+ 'flac',
33
+ ]);
34
+
35
+ const TEXT_EXTENSIONS = new Set(['md', 'txt', 'docx']);
36
+
37
+ export type FileCategory = 'media' | 'text' | 'unknown';
38
+ export type DragContentType = 'media' | 'text' | 'mixed' | null;
39
+ export type DropTarget = 'media' | 'insert' | 'replace';
40
+
41
+ function extensionOf(name: string): string {
42
+ const dot = name.lastIndexOf('.');
43
+ return dot >= 0 ? name.slice(dot + 1).toLowerCase() : '';
44
+ }
45
+
46
+ export function classifyFile(file: { name: string; type: string }): FileCategory {
47
+ const ext = extensionOf(file.name);
48
+ if (MEDIA_EXTENSIONS.has(ext)) return 'media';
49
+ if (TEXT_EXTENSIONS.has(ext)) return 'text';
50
+
51
+ // Fallback to MIME type
52
+ if (
53
+ file.type.startsWith('image/') ||
54
+ file.type.startsWith('video/') ||
55
+ file.type.startsWith('audio/')
56
+ ) {
57
+ return 'media';
58
+ }
59
+ if (file.type === 'text/plain' || file.type === 'text/markdown') {
60
+ return 'text';
61
+ }
62
+ return 'unknown';
63
+ }
64
+
65
+ /**
66
+ * Classify dragged items from a DataTransfer during dragenter/dragover.
67
+ * Browsers restrict full file access during drag — only MIME types are
68
+ * available via DataTransferItem.type.
69
+ */
70
+ function classifyDataTransferItems(items: DataTransferItemList): DragContentType {
71
+ let hasMedia = false;
72
+ let hasText = false;
73
+
74
+ for (let i = 0; i < items.length; i++) {
75
+ const item = items[i];
76
+ if (item.kind !== 'file') continue;
77
+
78
+ const mime = item.type.toLowerCase();
79
+ if (mime.startsWith('image/') || mime.startsWith('video/') || mime.startsWith('audio/')) {
80
+ hasMedia = true;
81
+ } else if (
82
+ mime === 'text/plain' ||
83
+ mime === 'text/markdown' ||
84
+ mime === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
85
+ ) {
86
+ hasText = true;
87
+ } else {
88
+ // Unknown MIME — browsers often report '' for many file types during drag.
89
+ // We can't classify, so assume mixed to show all drop zones.
90
+ hasMedia = true;
91
+ hasText = true;
92
+ }
93
+ }
94
+
95
+ if (hasMedia && hasText) return 'mixed';
96
+ if (hasMedia) return 'media';
97
+ if (hasText) return 'text';
98
+ return null;
99
+ }
100
+
101
+ // ─── Hook ────────────────────────────────────────────────
102
+
103
+ export interface UseFileDropOptions {
104
+ /** Called when files are dropped on a specific target zone. */
105
+ onDrop: (files: File[], target: DropTarget) => void;
106
+ /** Whether drop is enabled (default: true) */
107
+ enabled?: boolean;
108
+ }
109
+
110
+ export interface UseFileDropResult {
111
+ /** Whether a drag-with-files is currently hovering over the container */
112
+ isDragging: boolean;
113
+ /** Classification of the dragged content */
114
+ dragContentType: DragContentType;
115
+ /** Attach these to the container element */
116
+ containerProps: {
117
+ onDragEnter: (e: React.DragEvent) => void;
118
+ onDragOver: (e: React.DragEvent) => void;
119
+ onDragLeave: (e: React.DragEvent) => void;
120
+ onDrop: (e: React.DragEvent) => void;
121
+ };
122
+ /** Create props for an individual drop zone target */
123
+ zoneProps: (target: DropTarget) => {
124
+ onDragOver: (e: React.DragEvent) => void;
125
+ onDrop: (e: React.DragEvent) => void;
126
+ };
127
+ }
128
+
129
+ export function useFileDrop({ onDrop, enabled = true }: UseFileDropOptions): UseFileDropResult {
130
+ const [isDragging, setIsDragging] = useState(false);
131
+ const [dragContentType, setDragContentType] = useState<DragContentType>(null);
132
+
133
+ // Counter-based tracking: dragenter/dragleave fire on child elements,
134
+ // so we track a count rather than a boolean.
135
+ const dragCounterRef = useRef(0);
136
+
137
+ const handleDragEnter = useCallback(
138
+ (e: React.DragEvent) => {
139
+ if (!enabled) return;
140
+ e.preventDefault();
141
+ dragCounterRef.current++;
142
+
143
+ if (dragCounterRef.current === 1) {
144
+ setIsDragging(true);
145
+ if (e.dataTransfer.items) {
146
+ setDragContentType(classifyDataTransferItems(e.dataTransfer.items));
147
+ } else {
148
+ // Fallback: can't classify, show all zones
149
+ setDragContentType('mixed');
150
+ }
151
+ }
152
+ },
153
+ [enabled],
154
+ );
155
+
156
+ const handleDragOver = useCallback(
157
+ (e: React.DragEvent) => {
158
+ if (!enabled) return;
159
+ e.preventDefault();
160
+ // Required to allow drop
161
+ e.dataTransfer.dropEffect = 'copy';
162
+ },
163
+ [enabled],
164
+ );
165
+
166
+ const handleDragLeave = useCallback(
167
+ (e: React.DragEvent) => {
168
+ if (!enabled) return;
169
+ e.preventDefault();
170
+ dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
171
+
172
+ if (dragCounterRef.current === 0) {
173
+ setIsDragging(false);
174
+ setDragContentType(null);
175
+ }
176
+ },
177
+ [enabled],
178
+ );
179
+
180
+ const handleDrop = useCallback(
181
+ (e: React.DragEvent) => {
182
+ if (!enabled) return;
183
+ e.preventDefault();
184
+ dragCounterRef.current = 0;
185
+ setIsDragging(false);
186
+ setDragContentType(null);
187
+ // Actual file handling is done by zone-specific onDrop
188
+ },
189
+ [enabled],
190
+ );
191
+
192
+ const zoneProps = useCallback(
193
+ (target: DropTarget) => ({
194
+ onDragOver: (e: React.DragEvent) => {
195
+ e.preventDefault();
196
+ e.stopPropagation();
197
+ e.dataTransfer.dropEffect = 'copy';
198
+ },
199
+ onDrop: (e: React.DragEvent) => {
200
+ e.preventDefault();
201
+ e.stopPropagation();
202
+ dragCounterRef.current = 0;
203
+ setIsDragging(false);
204
+ setDragContentType(null);
205
+
206
+ const files = Array.from(e.dataTransfer.files);
207
+ if (files.length > 0) {
208
+ onDrop(files, target);
209
+ }
210
+ },
211
+ }),
212
+ [onDrop],
213
+ );
214
+
215
+ return {
216
+ isDragging,
217
+ dragContentType,
218
+ containerProps: {
219
+ onDragEnter: handleDragEnter,
220
+ onDragOver: handleDragOver,
221
+ onDragLeave: handleDragLeave,
222
+ onDrop: handleDrop,
223
+ },
224
+ zoneProps,
225
+ };
226
+ }
package/src/index.ts CHANGED
@@ -40,6 +40,12 @@ export type { WysiwygEditorProps } from './WysiwygEditor.js';
40
40
 
41
41
  export { PreviewPanel } from './PreviewPanel.js';
42
42
  export type { PreviewPanelProps } from './PreviewPanel.js';
43
+ export {
44
+ PreviewSettingsProvider,
45
+ PreviewToolbarControls,
46
+ usePreviewSettings,
47
+ } from './PreviewControls.js';
48
+ export type { PreviewSettings } from './PreviewControls.js';
43
49
 
44
50
  // Chrome (for custom layouts)
45
51
  export { ViewSwitcher } from './ViewSwitcher.js';
@@ -48,9 +54,32 @@ export type { ViewSwitcherProps } from './ViewSwitcher.js';
48
54
  export { Toolbar } from './Toolbar.js';
49
55
  export type { ToolbarProps } from './Toolbar.js';
50
56
 
57
+ export { MediaBin } from './MediaBin.js';
58
+ export type { MediaBinProps } from './MediaBin.js';
59
+
51
60
  export { StatusBar } from './StatusBar.js';
52
61
  export type { StatusBarProps } from './StatusBar.js';
53
62
 
63
+ // Drag-and-drop
64
+ export { DropZoneOverlay } from './DropZoneOverlay.js';
65
+ export type { DropZoneOverlayProps } from './DropZoneOverlay.js';
66
+
67
+ export { useFileDrop, classifyFile } from './hooks/useFileDrop.js';
68
+ export type {
69
+ FileCategory,
70
+ DragContentType,
71
+ DropTarget,
72
+ UseFileDropOptions,
73
+ UseFileDropResult,
74
+ } from './hooks/useFileDrop.js';
75
+
76
+ export {
77
+ partitionFiles,
78
+ processMediaFiles,
79
+ processTextFile,
80
+ processTextFiles,
81
+ } from './utils/dropUtils.js';
82
+
54
83
  // Bridge utilities
55
84
  export { markdownToTiptap, tiptapToMarkdown } from './tiptapBridge.js';
56
85
 
@@ -58,7 +58,7 @@
58
58
  display: flex;
59
59
  align-items: center;
60
60
  flex-wrap: wrap;
61
- padding: 4px 12px;
61
+ padding: 0 12px 0 0;
62
62
  gap: 2px;
63
63
  }
64
64
 
@@ -67,7 +67,12 @@
67
67
  .squisq-toolbar-view-tabs {
68
68
  display: flex;
69
69
  gap: 0;
70
- margin-right: 2px;
70
+ margin-right: 12px;
71
+ padding: 0 16px 0 12px;
72
+ background: rgba(0, 0, 0, 0.07);
73
+ border-right: 1px solid rgba(0, 0, 0, 0.12);
74
+ align-self: stretch;
75
+ align-items: center;
71
76
  }
72
77
 
73
78
  .squisq-toolbar-actions {
@@ -80,7 +85,7 @@
80
85
  padding: 4px 14px;
81
86
  border: none;
82
87
  background: transparent;
83
- color: #6b7280;
88
+ color: #4b5563;
84
89
  cursor: pointer;
85
90
  font-size: 13px;
86
91
  font-weight: 500;
@@ -91,11 +96,12 @@
91
96
  }
92
97
 
93
98
  .squisq-toolbar-view-tab:hover {
94
- color: #1f2937;
99
+ color: #111827;
95
100
  }
96
101
 
97
102
  .squisq-toolbar-view-tab--active {
98
- color: #2563eb;
103
+ color: #1d4ed8;
104
+ font-weight: 600;
99
105
  border-bottom-color: #2563eb;
100
106
  }
101
107
 
@@ -237,12 +243,18 @@
237
243
 
238
244
  /* ─── WYSIWYG Editor ─────────────────────────────────── */
239
245
 
246
+ .squisq-wysiwyg-container {
247
+ background: #eeecea;
248
+ }
249
+
240
250
  .squisq-wysiwyg-editor {
241
251
  padding: 16px 24px;
242
252
  max-width: 800px;
243
253
  margin: 0 auto;
244
254
  outline: none;
245
255
  min-height: 100%;
256
+ background: #fff;
257
+ box-shadow: 0 0 8px rgba(0, 0, 0, 0.06);
246
258
  }
247
259
 
248
260
  .squisq-wysiwyg-editor h1 {
@@ -437,17 +449,22 @@
437
449
  /* no border-top — sits directly in header */
438
450
  }
439
451
 
452
+ .squisq-editor-shell[data-theme='dark'] .squisq-toolbar-view-tabs {
453
+ background: rgba(255, 255, 255, 0.08);
454
+ border-right-color: rgba(255, 255, 255, 0.15);
455
+ }
456
+
440
457
  /* View tabs (dark) */
441
458
  .squisq-editor-shell[data-theme='dark'] .squisq-toolbar-view-tab {
442
- color: #9ca3af;
459
+ color: #d1d5db;
443
460
  }
444
461
 
445
462
  .squisq-editor-shell[data-theme='dark'] .squisq-toolbar-view-tab:hover {
446
- color: #e5e7eb;
463
+ color: #f9fafb;
447
464
  }
448
465
 
449
466
  .squisq-editor-shell[data-theme='dark'] .squisq-toolbar-view-tab--active {
450
- color: #60a5fa;
467
+ color: #93c5fd;
451
468
  border-bottom-color: #60a5fa;
452
469
  }
453
470
 
@@ -516,8 +533,14 @@
516
533
  }
517
534
 
518
535
  /* WYSIWYG Editor */
536
+ .squisq-editor-shell[data-theme='dark'] .squisq-wysiwyg-container {
537
+ background: #0f1219;
538
+ }
539
+
519
540
  .squisq-editor-shell[data-theme='dark'] .squisq-wysiwyg-editor {
520
541
  color: #e5e7eb;
542
+ background: #111827;
543
+ box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
521
544
  }
522
545
 
523
546
  .squisq-editor-shell[data-theme='dark'] .squisq-wysiwyg-editor blockquote {
@@ -613,3 +636,321 @@
613
636
  padding-top: 2px;
614
637
  }
615
638
  }
639
+
640
+ /* ─── Media Bin ──────────────────────────────────────── */
641
+
642
+ .squisq-media-bin {
643
+ width: 220px;
644
+ border-left: 1px solid #e5e7eb;
645
+ background: #f9fafb;
646
+ display: flex;
647
+ flex-direction: column;
648
+ flex-shrink: 0;
649
+ overflow: hidden;
650
+ }
651
+
652
+ .squisq-media-bin-header {
653
+ display: flex;
654
+ align-items: center;
655
+ justify-content: space-between;
656
+ padding: 8px 10px;
657
+ border-bottom: 1px solid #e5e7eb;
658
+ flex-shrink: 0;
659
+ }
660
+
661
+ .squisq-media-bin-title {
662
+ font-size: 12px;
663
+ font-weight: 600;
664
+ color: #374151;
665
+ text-transform: uppercase;
666
+ letter-spacing: 0.05em;
667
+ }
668
+
669
+ .squisq-media-bin-upload {
670
+ font-size: 11px;
671
+ padding: 2px 8px;
672
+ cursor: pointer;
673
+ background: #e5e7eb;
674
+ color: #374151;
675
+ border: 1px solid #d1d5db;
676
+ border-radius: 4px;
677
+ }
678
+
679
+ .squisq-media-bin-upload:hover {
680
+ background: #d1d5db;
681
+ }
682
+
683
+ .squisq-media-bin-upload:disabled {
684
+ cursor: default;
685
+ opacity: 0.5;
686
+ }
687
+
688
+ .squisq-media-bin-list {
689
+ flex: 1;
690
+ overflow: auto;
691
+ padding: 4px 0;
692
+ }
693
+
694
+ .squisq-media-bin-empty {
695
+ padding: 20px 10px;
696
+ font-size: 12px;
697
+ color: #9ca3af;
698
+ text-align: center;
699
+ line-height: 1.5;
700
+ }
701
+
702
+ .squisq-media-bin-item {
703
+ display: flex;
704
+ align-items: center;
705
+ gap: 6px;
706
+ padding: 4px 10px;
707
+ cursor: default;
708
+ font-size: 11px;
709
+ color: #374151;
710
+ border-bottom: 1px solid #f3f4f6;
711
+ }
712
+
713
+ .squisq-media-bin-thumb {
714
+ width: 32px;
715
+ height: 32px;
716
+ object-fit: cover;
717
+ border-radius: 2px;
718
+ flex-shrink: 0;
719
+ background: #e5e7eb;
720
+ }
721
+
722
+ .squisq-media-bin-icon {
723
+ width: 32px;
724
+ height: 32px;
725
+ display: flex;
726
+ align-items: center;
727
+ justify-content: center;
728
+ font-size: 16px;
729
+ flex-shrink: 0;
730
+ background: #f3f4f6;
731
+ border-radius: 2px;
732
+ }
733
+
734
+ .squisq-media-bin-meta {
735
+ min-width: 0;
736
+ flex: 1;
737
+ }
738
+
739
+ .squisq-media-bin-name {
740
+ overflow: hidden;
741
+ text-overflow: ellipsis;
742
+ white-space: nowrap;
743
+ }
744
+
745
+ .squisq-media-bin-size {
746
+ font-size: 10px;
747
+ color: #9ca3af;
748
+ }
749
+
750
+ /* ─── Media Bin (dark) ───────────────────────────────── */
751
+
752
+ .squisq-media-bin--dark,
753
+ .squisq-editor-shell[data-theme='dark'] .squisq-media-bin {
754
+ background: #1e293b;
755
+ border-left-color: #374151;
756
+ }
757
+
758
+ .squisq-media-bin--dark .squisq-media-bin-header,
759
+ .squisq-editor-shell[data-theme='dark'] .squisq-media-bin .squisq-media-bin-header {
760
+ border-bottom-color: #374151;
761
+ }
762
+
763
+ .squisq-media-bin--dark .squisq-media-bin-title,
764
+ .squisq-editor-shell[data-theme='dark'] .squisq-media-bin .squisq-media-bin-title {
765
+ color: #d1d5db;
766
+ }
767
+
768
+ .squisq-media-bin--dark .squisq-media-bin-upload,
769
+ .squisq-editor-shell[data-theme='dark'] .squisq-media-bin .squisq-media-bin-upload {
770
+ background: #374151;
771
+ color: #d1d5db;
772
+ border-color: #4b5563;
773
+ }
774
+
775
+ .squisq-media-bin--dark .squisq-media-bin-upload:hover,
776
+ .squisq-editor-shell[data-theme='dark'] .squisq-media-bin .squisq-media-bin-upload:hover {
777
+ background: #4b5563;
778
+ }
779
+
780
+ .squisq-media-bin--dark .squisq-media-bin-item,
781
+ .squisq-editor-shell[data-theme='dark'] .squisq-media-bin .squisq-media-bin-item {
782
+ color: #d1d5db;
783
+ border-bottom-color: #2d3748;
784
+ }
785
+
786
+ .squisq-media-bin--dark .squisq-media-bin-icon,
787
+ .squisq-editor-shell[data-theme='dark'] .squisq-media-bin .squisq-media-bin-icon {
788
+ background: #374151;
789
+ }
790
+
791
+ .squisq-media-bin--dark .squisq-media-bin-thumb,
792
+ .squisq-editor-shell[data-theme='dark'] .squisq-media-bin .squisq-media-bin-thumb {
793
+ background: #374151;
794
+ }
795
+
796
+ .squisq-media-bin--dark .squisq-media-bin-size,
797
+ .squisq-editor-shell[data-theme='dark'] .squisq-media-bin .squisq-media-bin-size {
798
+ color: #6b7280;
799
+ }
800
+
801
+ .squisq-media-bin--dark .squisq-media-bin-empty,
802
+ .squisq-editor-shell[data-theme='dark'] .squisq-media-bin .squisq-media-bin-empty {
803
+ color: #6b7280;
804
+ }
805
+
806
+ /* ============================================
807
+ Drop Zone Overlay
808
+ ============================================ */
809
+
810
+ .squisq-drop-overlay {
811
+ position: absolute;
812
+ inset: 0;
813
+ z-index: 50;
814
+ display: flex;
815
+ align-items: center;
816
+ justify-content: center;
817
+ background: rgba(255, 255, 255, 0.85);
818
+ backdrop-filter: blur(4px);
819
+ }
820
+
821
+ .squisq-drop-overlay-inner {
822
+ display: flex;
823
+ gap: 24px;
824
+ padding: 32px;
825
+ max-width: 90%;
826
+ }
827
+
828
+ .squisq-drop-zone {
829
+ display: flex;
830
+ flex-direction: column;
831
+ align-items: center;
832
+ justify-content: center;
833
+ gap: 8px;
834
+ width: 200px;
835
+ min-height: 180px;
836
+ padding: 24px 16px;
837
+ border: 2px dashed #94a3b8;
838
+ border-radius: 12px;
839
+ background: rgba(255, 255, 255, 0.9);
840
+ cursor: default;
841
+ transition:
842
+ border-color 0.15s,
843
+ background-color 0.15s,
844
+ transform 0.15s;
845
+ text-align: center;
846
+ user-select: none;
847
+ }
848
+
849
+ .squisq-drop-zone--active {
850
+ transform: scale(1.04);
851
+ }
852
+
853
+ .squisq-drop-zone--disabled {
854
+ opacity: 0.45;
855
+ cursor: not-allowed;
856
+ }
857
+
858
+ /* Variant colors */
859
+ .squisq-drop-zone--media {
860
+ border-color: #60a5fa;
861
+ }
862
+ .squisq-drop-zone--media.squisq-drop-zone--active {
863
+ border-color: #3b82f6;
864
+ background: rgba(219, 234, 254, 0.9);
865
+ }
866
+
867
+ .squisq-drop-zone--insert {
868
+ border-color: #34d399;
869
+ }
870
+ .squisq-drop-zone--insert.squisq-drop-zone--active {
871
+ border-color: #10b981;
872
+ background: rgba(209, 250, 229, 0.9);
873
+ }
874
+
875
+ .squisq-drop-zone--replace {
876
+ border-color: #f97316;
877
+ }
878
+ .squisq-drop-zone--replace.squisq-drop-zone--active {
879
+ border-color: #ea580c;
880
+ background: rgba(255, 237, 213, 0.9);
881
+ }
882
+
883
+ /* Zone content */
884
+ .squisq-drop-zone-icon {
885
+ font-size: 32px;
886
+ line-height: 1;
887
+ }
888
+
889
+ .squisq-drop-zone-label {
890
+ font-size: 16px;
891
+ font-weight: 600;
892
+ color: #1e293b;
893
+ }
894
+
895
+ .squisq-drop-zone-desc {
896
+ font-size: 12px;
897
+ color: #64748b;
898
+ line-height: 1.4;
899
+ }
900
+
901
+ /* Dark mode */
902
+ .squisq-editor-shell[data-theme='dark'] .squisq-drop-overlay {
903
+ background: rgba(17, 24, 39, 0.88);
904
+ }
905
+
906
+ .squisq-editor-shell[data-theme='dark'] .squisq-drop-zone {
907
+ border-color: #4b5563;
908
+ background: rgba(31, 41, 55, 0.9);
909
+ }
910
+
911
+ .squisq-editor-shell[data-theme='dark'] .squisq-drop-zone--media {
912
+ border-color: #3b82f6;
913
+ }
914
+ .squisq-editor-shell[data-theme='dark'] .squisq-drop-zone--media.squisq-drop-zone--active {
915
+ border-color: #60a5fa;
916
+ background: rgba(30, 58, 138, 0.6);
917
+ }
918
+
919
+ .squisq-editor-shell[data-theme='dark'] .squisq-drop-zone--insert {
920
+ border-color: #10b981;
921
+ }
922
+ .squisq-editor-shell[data-theme='dark'] .squisq-drop-zone--insert.squisq-drop-zone--active {
923
+ border-color: #34d399;
924
+ background: rgba(6, 78, 59, 0.6);
925
+ }
926
+
927
+ .squisq-editor-shell[data-theme='dark'] .squisq-drop-zone--replace {
928
+ border-color: #ea580c;
929
+ }
930
+ .squisq-editor-shell[data-theme='dark'] .squisq-drop-zone--replace.squisq-drop-zone--active {
931
+ border-color: #f97316;
932
+ background: rgba(124, 45, 18, 0.6);
933
+ }
934
+
935
+ .squisq-editor-shell[data-theme='dark'] .squisq-drop-zone-label {
936
+ color: #e2e8f0;
937
+ }
938
+
939
+ .squisq-editor-shell[data-theme='dark'] .squisq-drop-zone-desc {
940
+ color: #94a3b8;
941
+ }
942
+
943
+ /* Responsive */
944
+ @media (max-width: 640px) {
945
+ .squisq-drop-overlay-inner {
946
+ flex-direction: column;
947
+ gap: 16px;
948
+ align-items: center;
949
+ }
950
+
951
+ .squisq-drop-zone {
952
+ width: 100%;
953
+ max-width: 240px;
954
+ min-height: 120px;
955
+ }
956
+ }