@lumir-company/editor 0.4.3 → 0.4.5

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/dist/index.mjs CHANGED
@@ -65,7 +65,10 @@ var createS3Uploader = (config) => {
65
65
  path,
66
66
  fileNameTransform,
67
67
  appendUUID,
68
- preserveExtension = true
68
+ preserveExtension = true,
69
+ onProgress,
70
+ uploadTimeoutMs = 12e4,
71
+ maxRetries = 2
69
72
  } = config;
70
73
  if (!apiEndpoint || apiEndpoint.trim() === "") {
71
74
  throw new Error(
@@ -104,6 +107,15 @@ var createS3Uploader = (config) => {
104
107
  }
105
108
  return `${env}/${path}/${filename}`;
106
109
  };
110
+ const debugLog = (loc, msg, data) => {
111
+ const p = fetch("http://127.0.0.1:7686/ingest/1f8ee1c5-0cf0-4ae7-91ed-5ea7ed17130a", {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "b73262" },
114
+ body: JSON.stringify({ sessionId: "b73262", location: loc, message: msg, data, timestamp: Date.now() })
115
+ });
116
+ if (p && typeof p.catch === "function") p.catch(() => {
117
+ });
118
+ };
107
119
  return async (file) => {
108
120
  try {
109
121
  if (!apiEndpoint || apiEndpoint.trim() === "") {
@@ -112,11 +124,26 @@ var createS3Uploader = (config) => {
112
124
  );
113
125
  }
114
126
  const fileName = generateHierarchicalFileName(file);
115
- const response = await fetch(
116
- `${apiEndpoint}?key=${encodeURIComponent(fileName)}`
117
- );
127
+ const contentType = file.type || "application/octet-stream";
128
+ const presignedUrlFull = `${apiEndpoint}?key=${encodeURIComponent(fileName)}&contentType=${encodeURIComponent(contentType)}`;
129
+ const tPresigned = Date.now();
130
+ debugLog("s3:step1:presignedReq", "fetching presigned URL", {
131
+ fileName,
132
+ contentType,
133
+ apiEndpoint
134
+ });
135
+ const response = await fetch(presignedUrlFull);
136
+ debugLog("s3:step2:presignedRes", "presigned response", {
137
+ ok: response.ok,
138
+ status: response.status,
139
+ elapsedMs: Date.now() - tPresigned
140
+ });
118
141
  if (!response.ok) {
119
142
  const errorText = await response.text() || "";
143
+ debugLog("s3:step2b:presignedErr", "presigned failed", {
144
+ status: response.status,
145
+ errorText: errorText.slice(0, 200)
146
+ });
120
147
  throw new Error(
121
148
  `Failed to get presigned URL: ${response.statusText}, ${errorText}`
122
149
  );
@@ -125,17 +152,64 @@ var createS3Uploader = (config) => {
125
152
  const { presignedUrl, publicUrl } = responseData;
126
153
  const validatedPresignedUrl = validateS3Url(presignedUrl, "presignedUrl");
127
154
  const validatedPublicUrl = validateS3Url(publicUrl, "publicUrl");
128
- const uploadResponse = await fetch(validatedPresignedUrl, {
129
- method: "PUT",
130
- headers: {
131
- "Content-Type": file.type || "application/octet-stream"
132
- },
133
- body: file
134
- });
135
- if (!uploadResponse.ok) {
136
- throw new Error(`Failed to upload file: ${uploadResponse.statusText}`);
155
+ const tPut = Date.now();
156
+ debugLog("s3:step3:putReq", "S3 PUT request", { publicUrlLen: validatedPublicUrl?.length });
157
+ let lastError;
158
+ const attempts = maxRetries + 1;
159
+ for (let attempt = 0; attempt < attempts; attempt++) {
160
+ try {
161
+ if (onProgress && typeof XMLHttpRequest !== "undefined") {
162
+ await new Promise((resolve, reject) => {
163
+ const xhr = new XMLHttpRequest();
164
+ xhr.timeout = uploadTimeoutMs;
165
+ xhr.upload.onprogress = (e) => {
166
+ if (e.lengthComputable) {
167
+ onProgress(Math.min(100, Math.round(e.loaded / e.total * 100)));
168
+ }
169
+ };
170
+ xhr.onload = () => {
171
+ if (xhr.status >= 200 && xhr.status < 300) resolve();
172
+ else reject(new Error(`Failed to upload file: ${xhr.statusText}`));
173
+ };
174
+ xhr.onerror = () => reject(new Error("Upload failed"));
175
+ xhr.ontimeout = () => reject(new Error("Upload timeout"));
176
+ xhr.open("PUT", validatedPresignedUrl);
177
+ xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
178
+ xhr.send(file);
179
+ });
180
+ } else {
181
+ const controller = new AbortController();
182
+ const timeoutId = setTimeout(() => controller.abort(), uploadTimeoutMs);
183
+ const uploadResponse = await fetch(validatedPresignedUrl, {
184
+ method: "PUT",
185
+ headers: {
186
+ "Content-Type": file.type || "application/octet-stream"
187
+ },
188
+ body: file,
189
+ signal: controller.signal
190
+ });
191
+ clearTimeout(timeoutId);
192
+ debugLog("s3:step4:putRes", "S3 PUT response", {
193
+ ok: uploadResponse.ok,
194
+ status: uploadResponse.status,
195
+ putElapsedMs: Date.now() - tPut
196
+ });
197
+ if (!uploadResponse.ok) {
198
+ throw new Error(`Failed to upload file: ${uploadResponse.statusText}`);
199
+ }
200
+ }
201
+ debugLog("s3:step5:return", "returning publicUrl", { urlPrefix: validatedPublicUrl.slice(0, 80) });
202
+ return validatedPublicUrl;
203
+ } catch (err) {
204
+ lastError = err instanceof Error ? err : new Error(String(err));
205
+ if (attempt < attempts - 1) {
206
+ debugLog("s3:putRetry", "PUT failed, retrying", { attempt: attempt + 1, attempts });
207
+ } else {
208
+ throw lastError;
209
+ }
210
+ }
137
211
  }
138
- return validatedPublicUrl;
212
+ throw lastError ?? new Error("Upload failed");
139
213
  } catch (error) {
140
214
  console.error("S3 upload failed:", error);
141
215
  throw error;
@@ -2417,15 +2491,14 @@ var LumirEditorError = class _LumirEditorError extends Error {
2417
2491
  }
2418
2492
  /**
2419
2493
  * 잘못된 파일 형식 에러 생성
2494
+ * @param allowVideoUpload true이면 "image and video" 메시지 사용
2420
2495
  */
2421
- static invalidFileType(fileName) {
2422
- return new _LumirEditorError(
2423
- `Invalid file type: ${fileName}. Only image files are allowed.`,
2424
- {
2425
- code: "INVALID_FILE_TYPE",
2426
- context: { fileName }
2427
- }
2428
- );
2496
+ static invalidFileType(fileName, allowVideoUpload) {
2497
+ const message = allowVideoUpload === true ? `Invalid file type: ${fileName}. Only image and video files are allowed.` : `Invalid file type: ${fileName}. Only image files are allowed.`;
2498
+ return new _LumirEditorError(message, {
2499
+ code: "INVALID_FILE_TYPE",
2500
+ context: { fileName }
2501
+ });
2429
2502
  }
2430
2503
  /**
2431
2504
  * S3 설정 에러 생성
@@ -2448,10 +2521,38 @@ var LumirEditorError = class _LumirEditorError extends Error {
2448
2521
 
2449
2522
  // src/constants/limits.ts
2450
2523
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
2524
+ var MAX_VIDEO_FILE_SIZE = 100 * 1024 * 1024;
2451
2525
  var BLOCKED_EXTENSIONS = [".svg", ".svgz"];
2526
+ var ALLOWED_VIDEO_MIME_TYPES = /* @__PURE__ */ new Set([
2527
+ "video/mp4",
2528
+ "video/webm",
2529
+ "video/ogg",
2530
+ "video/quicktime"
2531
+ // .mov
2532
+ ]);
2533
+ var ALLOWED_VIDEO_EXTENSIONS = [
2534
+ ".mp4",
2535
+ ".webm",
2536
+ ".ogg",
2537
+ ".mov"
2538
+ ];
2452
2539
 
2453
2540
  // src/components/LumirEditor.tsx
2454
- import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
2541
+ import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
2542
+ var DEBUG_LOG = (loc, msg, data) => {
2543
+ fetch("http://127.0.0.1:7686/ingest/1f8ee1c5-0cf0-4ae7-91ed-5ea7ed17130a", {
2544
+ method: "POST",
2545
+ headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "b73262" },
2546
+ body: JSON.stringify({
2547
+ sessionId: "b73262",
2548
+ location: loc,
2549
+ message: msg,
2550
+ data,
2551
+ timestamp: Date.now()
2552
+ })
2553
+ }).catch(() => {
2554
+ });
2555
+ };
2455
2556
  var ContentUtils = class {
2456
2557
  /**
2457
2558
  * JSON 문자열의 유효성을 검증합니다
@@ -2580,6 +2681,25 @@ var isImageFile = (file) => {
2580
2681
  }
2581
2682
  return file.type?.startsWith("image/") || !file.type && /\.(png|jpe?g|gif|webp|bmp)$/i.test(fileName);
2582
2683
  };
2684
+ var isVideoFile = (file) => {
2685
+ const sizeOk = file.size > 0 && file.size <= MAX_VIDEO_FILE_SIZE;
2686
+ const fileName = file.name?.toLowerCase() || "";
2687
+ const mimeMatch = ALLOWED_VIDEO_MIME_TYPES.has(file.type);
2688
+ const videoPrefix = typeof file.type === "string" && file.type.startsWith("video/");
2689
+ const extMatch = !file.type && ALLOWED_VIDEO_EXTENSIONS.some((ext) => fileName.endsWith(ext));
2690
+ const result = sizeOk && (mimeMatch || videoPrefix || extMatch);
2691
+ DEBUG_LOG("isVideoFile:check", "result", {
2692
+ fileName: file.name,
2693
+ fileType: file.type,
2694
+ fileSize: file.size,
2695
+ sizeOk,
2696
+ mimeMatch,
2697
+ videoPrefix,
2698
+ extMatch,
2699
+ result
2700
+ });
2701
+ return result;
2702
+ };
2583
2703
  var isHtmlFile = (file) => {
2584
2704
  return file.size > 0 && (file.type === "text/html" || file.name?.toLowerCase().endsWith(".html") || file.name?.toLowerCase().endsWith(".htm"));
2585
2705
  };
@@ -2593,15 +2713,17 @@ var escapeHtml = (str) => {
2593
2713
  };
2594
2714
  return str.replace(/[&<>"']/g, (char) => htmlEscapes[char]);
2595
2715
  };
2596
- var extractImageUrls = (blocks) => {
2716
+ var extractMediaUrls = (blocks) => {
2597
2717
  const urls = /* @__PURE__ */ new Set();
2598
2718
  const traverse = (blockList) => {
2599
2719
  for (const block of blockList) {
2600
2720
  if (block.type === "image" && block.props?.url) {
2601
2721
  const url = block.props.url;
2602
- if (typeof url === "string" && url.trim()) {
2603
- urls.add(url);
2604
- }
2722
+ if (typeof url === "string" && url.trim()) urls.add(url);
2723
+ }
2724
+ if (block.type === "video" && block.props?.url) {
2725
+ const url = block.props.url;
2726
+ if (typeof url === "string" && url.trim()) urls.add(url);
2605
2727
  }
2606
2728
  if (block.children && Array.isArray(block.children)) {
2607
2729
  traverse(block.children);
@@ -2611,12 +2733,10 @@ var extractImageUrls = (blocks) => {
2611
2733
  traverse(blocks);
2612
2734
  return urls;
2613
2735
  };
2614
- var findDeletedImageUrls = (previousUrls, currentUrls) => {
2736
+ var findDeletedMediaUrls = (previousUrls, currentUrls) => {
2615
2737
  const deleted = [];
2616
2738
  previousUrls.forEach((url) => {
2617
- if (!currentUrls.has(url)) {
2618
- deleted.push(url);
2619
- }
2739
+ if (!currentUrls.has(url)) deleted.push(url);
2620
2740
  });
2621
2741
  return deleted;
2622
2742
  };
@@ -2726,7 +2846,11 @@ function LumirEditor({
2726
2846
  onImageDelete
2727
2847
  }) {
2728
2848
  const [isUploading, setIsUploading] = useState7(false);
2849
+ const [uploadProgress, setUploadProgress] = useState7(null);
2729
2850
  const [errorMessage, setErrorMessage] = useState7(null);
2851
+ const floatingMenuFileInputRef = useRef8(null);
2852
+ const floatingMenuBlockRef = useRef8(null);
2853
+ const floatingMenuUploadStartTimeRef = useRef8(0);
2730
2854
  const handleError = useCallback13(
2731
2855
  (error) => {
2732
2856
  onError?.(error);
@@ -2757,6 +2881,13 @@ function LumirEditor({
2757
2881
  allowFileUpload
2758
2882
  );
2759
2883
  }, [disableExtensions, allowVideoUpload, allowAudioUpload, allowFileUpload]);
2884
+ useEffect7(() => {
2885
+ DEBUG_LOG("LumirEditor:init:disabledExtensions", "snapshot", {
2886
+ allowVideoUpload,
2887
+ hasVideoInDisabled: disabledExtensions.includes("video"),
2888
+ disabledList: disabledExtensions.slice(0, 15)
2889
+ });
2890
+ }, [allowVideoUpload, disabledExtensions]);
2760
2891
  const fileNameTransformRef = useRef8(s3Upload?.fileNameTransform);
2761
2892
  useEffect7(() => {
2762
2893
  fileNameTransformRef.current = s3Upload?.fileNameTransform;
@@ -2769,6 +2900,12 @@ function LumirEditor({
2769
2900
  path: s3Upload.path,
2770
2901
  appendUUID: s3Upload.appendUUID,
2771
2902
  preserveExtension: s3Upload.preserveExtension,
2903
+ uploadTimeoutMs: s3Upload.uploadTimeoutMs,
2904
+ maxRetries: s3Upload.maxRetries,
2905
+ onProgress: (percent) => {
2906
+ setUploadProgress(percent);
2907
+ s3Upload.onProgress?.(percent);
2908
+ },
2772
2909
  // 최신 콜백을 항상 사용하도록 ref를 통해 접근
2773
2910
  fileNameTransform: (originalName, file) => {
2774
2911
  return fileNameTransformRef.current ? fileNameTransformRef.current(originalName, file) : originalName;
@@ -2779,7 +2916,10 @@ function LumirEditor({
2779
2916
  s3Upload?.env,
2780
2917
  s3Upload?.path,
2781
2918
  s3Upload?.appendUUID,
2782
- s3Upload?.preserveExtension
2919
+ s3Upload?.preserveExtension,
2920
+ s3Upload?.uploadTimeoutMs,
2921
+ s3Upload?.maxRetries,
2922
+ s3Upload?.onProgress
2783
2923
  ]);
2784
2924
  const editor = useCreateBlockNote(
2785
2925
  {
@@ -2797,18 +2937,44 @@ function LumirEditor({
2797
2937
  tabBehavior,
2798
2938
  trailingBlock,
2799
2939
  uploadFile: async (file) => {
2800
- if (!isImageFile(file)) {
2801
- const error = LumirEditorError.invalidFileType(file.name);
2940
+ const allowedImage = isImageFile(file);
2941
+ const allowedVideo = allowVideoUpload && isVideoFile(file);
2942
+ DEBUG_LOG("uploadFile:step1:entry", "editor uploadFile callback invoked", {
2943
+ fileName: file.name,
2944
+ fileType: file.type,
2945
+ fileSize: file.size,
2946
+ allowVideoUpload,
2947
+ allowedImage,
2948
+ allowedVideo
2949
+ });
2950
+ if (!allowedImage && !allowedVideo) {
2951
+ const error = LumirEditorError.invalidFileType(
2952
+ file.name,
2953
+ allowVideoUpload
2954
+ );
2802
2955
  handleError(error);
2803
2956
  throw error;
2804
2957
  }
2805
2958
  try {
2806
- let imageUrl;
2959
+ setUploadProgress(0);
2960
+ let fileUrl;
2961
+ const branch = uploadFile ? "custom" : memoizedS3Upload?.apiEndpoint ? "s3" : "none";
2962
+ DEBUG_LOG("uploadFile:step2:branch", "upload path", {
2963
+ branch,
2964
+ hasCustomUploadFile: !!uploadFile,
2965
+ hasS3ApiEndpoint: !!memoizedS3Upload?.apiEndpoint
2966
+ });
2807
2967
  if (uploadFile) {
2808
- imageUrl = await uploadFile(file);
2968
+ const t0 = Date.now();
2969
+ DEBUG_LOG("uploadFile:step3a:custom", "calling custom uploadFile", { fileName: file.name });
2970
+ fileUrl = await uploadFile(file);
2971
+ DEBUG_LOG("uploadFile:step3a:done", "custom uploadFile returned", { urlLen: fileUrl?.length, elapsedMs: Date.now() - t0 });
2809
2972
  } else if (memoizedS3Upload?.apiEndpoint) {
2973
+ const t0 = Date.now();
2974
+ DEBUG_LOG("uploadFile:step3b:s3", "calling S3 uploader", { fileName: file.name });
2810
2975
  const s3Uploader = createS3Uploader(memoizedS3Upload);
2811
- imageUrl = await s3Uploader(file);
2976
+ fileUrl = await s3Uploader(file);
2977
+ DEBUG_LOG("uploadFile:step3b:done", "S3 uploader returned", { urlLen: fileUrl?.length, elapsedMs: Date.now() - t0 });
2812
2978
  } else {
2813
2979
  const error = LumirEditorError.s3ConfigError(
2814
2980
  "No upload method available. Please provide uploadFile or s3Upload configuration."
@@ -2816,8 +2982,16 @@ function LumirEditor({
2816
2982
  handleError(error);
2817
2983
  throw error;
2818
2984
  }
2819
- return imageUrl;
2985
+ DEBUG_LOG("uploadFile:step4:success", "returning URL", {
2986
+ fileName: file.name,
2987
+ urlPrefix: fileUrl.slice(0, 80)
2988
+ });
2989
+ return fileUrl;
2820
2990
  } catch (error) {
2991
+ DEBUG_LOG("uploadFile:step5:catch", "uploadFile threw", {
2992
+ fileName: file.name,
2993
+ errorMessage: error instanceof Error ? error.message : String(error)
2994
+ });
2821
2995
  if (error instanceof LumirEditorError) {
2822
2996
  throw error;
2823
2997
  }
@@ -2827,6 +3001,8 @@ function LumirEditor({
2827
3001
  );
2828
3002
  handleError(lumirError);
2829
3003
  throw lumirError;
3004
+ } finally {
3005
+ setUploadProgress(null);
2830
3006
  }
2831
3007
  },
2832
3008
  pasteHandler: (ctx) => {
@@ -2855,7 +3031,15 @@ function LumirEditor({
2855
3031
  }
2856
3032
  const fileList = event?.clipboardData?.files ?? null;
2857
3033
  const files = fileList ? Array.from(fileList) : [];
2858
- const acceptedFiles = files.filter(isImageFile);
3034
+ const acceptedFiles = files.filter(
3035
+ (f) => isImageFile(f) || allowVideoUpload && isVideoFile(f)
3036
+ );
3037
+ DEBUG_LOG("paste:step1:files", "paste clipboard files", {
3038
+ filesCount: files.length,
3039
+ acceptedCount: acceptedFiles.length,
3040
+ fileNames: files.map((f) => f.name),
3041
+ acceptedNames: acceptedFiles.map((f) => f.name)
3042
+ });
2859
3043
  if (files.length > 0 && acceptedFiles.length === 0) {
2860
3044
  event.preventDefault();
2861
3045
  return true;
@@ -2869,13 +3053,26 @@ function LumirEditor({
2869
3053
  try {
2870
3054
  for (const file of acceptedFiles) {
2871
3055
  try {
3056
+ DEBUG_LOG("paste:step2:upload", "calling uploadFile for paste", {
3057
+ fileName: file.name,
3058
+ fileType: file.type
3059
+ });
2872
3060
  const url = await editor2.uploadFile(file);
2873
- editor2.pasteHTML(
2874
- `<img src="${escapeHtml(url)}" alt="image" />`
2875
- );
3061
+ if (isImageFile(file)) {
3062
+ editor2.pasteHTML(
3063
+ `<img src="${escapeHtml(url)}" alt="image" />`
3064
+ );
3065
+ } else if (isVideoFile(file)) {
3066
+ const currentBlock = editor2.getTextCursorPosition().block;
3067
+ editor2.insertBlocks(
3068
+ [{ type: "video", props: { url } }],
3069
+ currentBlock,
3070
+ "after"
3071
+ );
3072
+ }
2876
3073
  } catch (err) {
2877
3074
  console.warn(
2878
- "Image upload failed, skipped:",
3075
+ "Upload failed, skipped:",
2879
3076
  file.name || "",
2880
3077
  err
2881
3078
  );
@@ -2898,6 +3095,7 @@ function LumirEditor({
2898
3095
  trailingBlock,
2899
3096
  uploadFile,
2900
3097
  memoizedS3Upload,
3098
+ allowVideoUpload,
2901
3099
  linkPreview?.apiEndpoint,
2902
3100
  placeholder
2903
3101
  ]
@@ -2918,25 +3116,25 @@ function LumirEditor({
2918
3116
  };
2919
3117
  return editor.onEditorContentChange(handleContentChange);
2920
3118
  }, [editor, onContentChange]);
2921
- const previousImageUrlsRef = useRef8(/* @__PURE__ */ new Set());
3119
+ const previousMediaUrlsRef = useRef8(/* @__PURE__ */ new Set());
2922
3120
  useEffect7(() => {
2923
3121
  if (!editor) return;
2924
3122
  const initialBlocks = editor.topLevelBlocks;
2925
- previousImageUrlsRef.current = extractImageUrls(initialBlocks);
3123
+ previousMediaUrlsRef.current = extractMediaUrls(initialBlocks);
2926
3124
  }, [editor]);
2927
3125
  useEffect7(() => {
2928
3126
  if (!editor || !onImageDelete) return;
2929
- const handleImageDeleteCheck = () => {
3127
+ const handleMediaDeleteCheck = () => {
2930
3128
  const currentBlocks = editor.topLevelBlocks;
2931
- const currentUrls = extractImageUrls(currentBlocks);
2932
- const previousUrls = previousImageUrlsRef.current;
2933
- const deletedUrls = findDeletedImageUrls(previousUrls, currentUrls);
3129
+ const currentUrls = extractMediaUrls(currentBlocks);
3130
+ const previousUrls = previousMediaUrlsRef.current;
3131
+ const deletedUrls = findDeletedMediaUrls(previousUrls, currentUrls);
2934
3132
  deletedUrls.forEach((url) => {
2935
3133
  onImageDelete(url);
2936
3134
  });
2937
- previousImageUrlsRef.current = currentUrls;
3135
+ previousMediaUrlsRef.current = currentUrls;
2938
3136
  };
2939
- return editor.onEditorContentChange(handleImageDeleteCheck);
3137
+ return editor.onEditorContentChange(handleMediaDeleteCheck);
2940
3138
  }, [editor, onImageDelete]);
2941
3139
  useEffect7(() => {
2942
3140
  const el = editor?.domElement;
@@ -2958,11 +3156,31 @@ function LumirEditor({
2958
3156
  const items = Array.from(e.dataTransfer.items ?? []);
2959
3157
  const files = items.filter((it) => it.kind === "file").map((it) => it.getAsFile()).filter((f) => !!f);
2960
3158
  const imageFiles = files.filter(isImageFile);
3159
+ const videoFiles = allowVideoUpload ? files.filter(isVideoFile) : [];
2961
3160
  const htmlFiles = files.filter(isHtmlFile);
2962
- if (imageFiles.length === 0 && htmlFiles.length === 0) return;
3161
+ DEBUG_LOG("drop:step1:files", "drop received", {
3162
+ filesCount: files.length,
3163
+ imageCount: imageFiles.length,
3164
+ videoCount: videoFiles.length,
3165
+ htmlCount: htmlFiles.length,
3166
+ allowVideoUpload,
3167
+ firstFile: files[0] ? {
3168
+ name: files[0].name,
3169
+ type: files[0].type,
3170
+ size: files[0].size,
3171
+ isImage: isImageFile(files[0]),
3172
+ isVideo: isVideoFile(files[0])
3173
+ } : null
3174
+ });
3175
+ if (imageFiles.length === 0 && htmlFiles.length === 0 && videoFiles.length === 0)
3176
+ return;
2963
3177
  (async () => {
2964
3178
  setIsUploading(true);
2965
3179
  try {
3180
+ DEBUG_LOG("drop:step2:async", "drop async started", {
3181
+ imageCount: imageFiles.length,
3182
+ videoCount: videoFiles.length
3183
+ });
2966
3184
  for (const file of imageFiles) {
2967
3185
  try {
2968
3186
  if (editor?.uploadFile) {
@@ -2981,6 +3199,34 @@ function LumirEditor({
2981
3199
  );
2982
3200
  }
2983
3201
  }
3202
+ DEBUG_LOG("drop:step3:videoLoop", "video loop start", {
3203
+ videoCount: videoFiles.length,
3204
+ names: videoFiles.map((f) => f.name)
3205
+ });
3206
+ for (const file of videoFiles) {
3207
+ try {
3208
+ if (editor?.uploadFile) {
3209
+ DEBUG_LOG("drop:step4:videoUpload", "calling uploadFile for video", {
3210
+ fileName: file.name
3211
+ });
3212
+ const url = await editor.uploadFile(file);
3213
+ if (url && typeof url === "string") {
3214
+ const currentBlock = editor.getTextCursorPosition().block;
3215
+ editor.insertBlocks(
3216
+ [{ type: "video", props: { url } }],
3217
+ currentBlock,
3218
+ "after"
3219
+ );
3220
+ }
3221
+ }
3222
+ } catch (err) {
3223
+ console.warn(
3224
+ "Video upload failed, skipped:",
3225
+ file.name || "",
3226
+ err
3227
+ );
3228
+ }
3229
+ }
2984
3230
  for (const file of htmlFiles) {
2985
3231
  try {
2986
3232
  const htmlContent = await file.text();
@@ -3020,7 +3266,7 @@ function LumirEditor({
3020
3266
  });
3021
3267
  el.removeEventListener("drop", handleDrop, { capture: true });
3022
3268
  };
3023
- }, [editor]);
3269
+ }, [editor, allowVideoUpload]);
3024
3270
  const computedSideMenu = useMemo(() => {
3025
3271
  return sideMenuAddButton ? sideMenu : false;
3026
3272
  }, [sideMenuAddButton, sideMenu]);
@@ -3033,42 +3279,110 @@ function LumirEditor({
3033
3279
  className: cn("lumirEditor", className),
3034
3280
  style: { position: "relative", display: "flex", flexDirection: "column" },
3035
3281
  children: [
3036
- floatingMenu && editor && /* @__PURE__ */ jsx16(
3037
- FloatingMenu,
3038
- {
3039
- editor,
3040
- position: floatingMenuPosition,
3041
- onImageUpload: async () => {
3042
- const input = document.createElement("input");
3043
- input.type = "file";
3044
- input.accept = "image/*";
3045
- input.onchange = async (e) => {
3046
- const file = e.target.files?.[0];
3047
- if (file && editor.uploadFile) {
3048
- try {
3049
- setIsUploading(true);
3050
- const url = await editor.uploadFile(file);
3051
- editor.insertBlocks(
3052
- [
3053
- {
3054
- type: "image",
3055
- props: { url }
3056
- }
3057
- ],
3058
- editor.getTextCursorPosition().block,
3059
- "after"
3060
- );
3061
- } catch (err) {
3062
- console.error("Image upload failed:", err);
3063
- } finally {
3064
- setIsUploading(false);
3282
+ floatingMenu && editor && /* @__PURE__ */ jsxs10(Fragment4, { children: [
3283
+ /* @__PURE__ */ jsx16(
3284
+ "input",
3285
+ {
3286
+ ref: floatingMenuFileInputRef,
3287
+ type: "file",
3288
+ accept: allowVideoUpload ? "image/*,video/mp4,video/webm,video/ogg,video/quicktime,.mov" : "image/*",
3289
+ style: {
3290
+ position: "absolute",
3291
+ left: "-9999px",
3292
+ opacity: 0,
3293
+ pointerEvents: "none"
3294
+ },
3295
+ onChange: async (e) => {
3296
+ const inputEl = e.target;
3297
+ const file = inputEl.files?.[0];
3298
+ DEBUG_LOG("FloatingMenu:step3:onchange", "file input onchange fired", {
3299
+ hasFile: !!file,
3300
+ fileName: file?.name,
3301
+ fileType: file?.type,
3302
+ fileSize: file?.size,
3303
+ hasUploadFile: !!editor?.uploadFile
3304
+ });
3305
+ const blockToInsertAfter = floatingMenuBlockRef.current;
3306
+ if (file && editor.uploadFile && blockToInsertAfter) {
3307
+ const allowedImage = isImageFile(file);
3308
+ const allowedVideo = allowVideoUpload && isVideoFile(file);
3309
+ DEBUG_LOG("FloatingMenu:step4:fileCheck", "allowed check", {
3310
+ fileName: file.name,
3311
+ allowedImage,
3312
+ allowedVideo
3313
+ });
3314
+ if (allowedImage || allowedVideo) {
3315
+ try {
3316
+ setIsUploading(true);
3317
+ floatingMenuUploadStartTimeRef.current = Date.now();
3318
+ DEBUG_LOG("FloatingMenu:step5:uploadStart", "calling editor.uploadFile", {
3319
+ fileName: file.name
3320
+ });
3321
+ const url = await editor.uploadFile(file);
3322
+ const blockType = allowedVideo ? "video" : "image";
3323
+ const elapsedMs = Date.now() - floatingMenuUploadStartTimeRef.current;
3324
+ DEBUG_LOG("FloatingMenu:step6:uploadDone", "upload returned, inserting block", {
3325
+ blockType,
3326
+ blockId: blockToInsertAfter.id,
3327
+ urlLen: url?.length,
3328
+ elapsedMs
3329
+ });
3330
+ editor.insertBlocks(
3331
+ [
3332
+ {
3333
+ type: blockType,
3334
+ props: { url }
3335
+ }
3336
+ ],
3337
+ blockToInsertAfter,
3338
+ "after"
3339
+ );
3340
+ } catch (err) {
3341
+ DEBUG_LOG("FloatingMenu:step7:catch", "upload or insert failed", {
3342
+ errMsg: err instanceof Error ? err.message : String(err)
3343
+ });
3344
+ console.error("Upload failed:", err);
3345
+ } finally {
3346
+ setIsUploading(false);
3347
+ }
3065
3348
  }
3066
3349
  }
3067
- };
3068
- input.click();
3350
+ inputEl.value = "";
3351
+ }
3069
3352
  }
3070
- }
3071
- ),
3353
+ ),
3354
+ /* @__PURE__ */ jsx16(
3355
+ FloatingMenu,
3356
+ {
3357
+ editor,
3358
+ position: floatingMenuPosition,
3359
+ onImageUpload: () => {
3360
+ DEBUG_LOG("FloatingMenu:step1:click", "upload button clicked", {
3361
+ allowVideoUpload
3362
+ });
3363
+ let blockToInsertAfter;
3364
+ try {
3365
+ blockToInsertAfter = editor.getTextCursorPosition().block;
3366
+ } catch (err) {
3367
+ DEBUG_LOG("FloatingMenu:step1b:error", "getTextCursorPosition failed", {
3368
+ err: err instanceof Error ? err.message : String(err)
3369
+ });
3370
+ return;
3371
+ }
3372
+ floatingMenuBlockRef.current = blockToInsertAfter;
3373
+ const input = floatingMenuFileInputRef.current;
3374
+ if (!input) return;
3375
+ input.accept = allowVideoUpload ? "image/*,video/mp4,video/webm,video/ogg,video/quicktime,.mov" : "image/*";
3376
+ input.value = "";
3377
+ DEBUG_LOG("FloatingMenu:step2:inputReady", "persistent input ref, about to click", {
3378
+ accept: input.accept
3379
+ });
3380
+ DEBUG_LOG("FloatingMenu:step2b:click", "input.click() about to be called", {});
3381
+ input.click();
3382
+ }
3383
+ }
3384
+ )
3385
+ ] }),
3072
3386
  /* @__PURE__ */ jsxs10(
3073
3387
  BlockNoteView,
3074
3388
  {
@@ -3095,8 +3409,10 @@ function LumirEditor({
3095
3409
  const filtered = items.filter((item) => {
3096
3410
  const key = (item?.key || "").toString().toLowerCase();
3097
3411
  const title = (item?.title || "").toString().toLowerCase();
3098
- if (["video", "audio", "file"].includes(key)) return false;
3099
- if (title.includes("video") || title.includes("audio") || title.includes("file"))
3412
+ if (key === "video" || title.includes("video"))
3413
+ return allowVideoUpload;
3414
+ if (["audio", "file"].includes(key)) return false;
3415
+ if (title.includes("audio") || title.includes("file"))
3100
3416
  return false;
3101
3417
  return true;
3102
3418
  });
@@ -3197,7 +3513,7 @@ function LumirEditor({
3197
3513
  )
3198
3514
  );
3199
3515
  },
3200
- [editor, linkPreview?.apiEndpoint]
3516
+ [editor, allowVideoUpload, linkPreview?.apiEndpoint]
3201
3517
  )
3202
3518
  }
3203
3519
  ),
@@ -3205,7 +3521,13 @@ function LumirEditor({
3205
3521
  ]
3206
3522
  }
3207
3523
  ),
3208
- isUploading && /* @__PURE__ */ jsx16("div", { className: "lumirEditor-upload-overlay", children: /* @__PURE__ */ jsx16("div", { className: "lumirEditor-spinner" }) }),
3524
+ isUploading && /* @__PURE__ */ jsxs10("div", { className: "lumirEditor-upload-overlay", children: [
3525
+ /* @__PURE__ */ jsx16("div", { className: "lumirEditor-spinner" }),
3526
+ uploadProgress !== null && /* @__PURE__ */ jsxs10("span", { className: "lumirEditor-upload-progress", children: [
3527
+ uploadProgress,
3528
+ "%"
3529
+ ] })
3530
+ ] }),
3209
3531
  errorMessage && /* @__PURE__ */ jsxs10("div", { className: "lumirEditor-error-toast", children: [
3210
3532
  /* @__PURE__ */ jsx16("span", { className: "lumirEditor-error-icon", children: "\u26A0\uFE0F" }),
3211
3533
  /* @__PURE__ */ jsx16("span", { className: "lumirEditor-error-message", children: errorMessage }),