@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.js CHANGED
@@ -91,7 +91,10 @@ var createS3Uploader = (config) => {
91
91
  path,
92
92
  fileNameTransform,
93
93
  appendUUID,
94
- preserveExtension = true
94
+ preserveExtension = true,
95
+ onProgress,
96
+ uploadTimeoutMs = 12e4,
97
+ maxRetries = 2
95
98
  } = config;
96
99
  if (!apiEndpoint || apiEndpoint.trim() === "") {
97
100
  throw new Error(
@@ -130,6 +133,15 @@ var createS3Uploader = (config) => {
130
133
  }
131
134
  return `${env}/${path}/${filename}`;
132
135
  };
136
+ const debugLog = (loc, msg, data) => {
137
+ const p = fetch("http://127.0.0.1:7686/ingest/1f8ee1c5-0cf0-4ae7-91ed-5ea7ed17130a", {
138
+ method: "POST",
139
+ headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "b73262" },
140
+ body: JSON.stringify({ sessionId: "b73262", location: loc, message: msg, data, timestamp: Date.now() })
141
+ });
142
+ if (p && typeof p.catch === "function") p.catch(() => {
143
+ });
144
+ };
133
145
  return async (file) => {
134
146
  try {
135
147
  if (!apiEndpoint || apiEndpoint.trim() === "") {
@@ -138,11 +150,26 @@ var createS3Uploader = (config) => {
138
150
  );
139
151
  }
140
152
  const fileName = generateHierarchicalFileName(file);
141
- const response = await fetch(
142
- `${apiEndpoint}?key=${encodeURIComponent(fileName)}`
143
- );
153
+ const contentType = file.type || "application/octet-stream";
154
+ const presignedUrlFull = `${apiEndpoint}?key=${encodeURIComponent(fileName)}&contentType=${encodeURIComponent(contentType)}`;
155
+ const tPresigned = Date.now();
156
+ debugLog("s3:step1:presignedReq", "fetching presigned URL", {
157
+ fileName,
158
+ contentType,
159
+ apiEndpoint
160
+ });
161
+ const response = await fetch(presignedUrlFull);
162
+ debugLog("s3:step2:presignedRes", "presigned response", {
163
+ ok: response.ok,
164
+ status: response.status,
165
+ elapsedMs: Date.now() - tPresigned
166
+ });
144
167
  if (!response.ok) {
145
168
  const errorText = await response.text() || "";
169
+ debugLog("s3:step2b:presignedErr", "presigned failed", {
170
+ status: response.status,
171
+ errorText: errorText.slice(0, 200)
172
+ });
146
173
  throw new Error(
147
174
  `Failed to get presigned URL: ${response.statusText}, ${errorText}`
148
175
  );
@@ -151,17 +178,64 @@ var createS3Uploader = (config) => {
151
178
  const { presignedUrl, publicUrl } = responseData;
152
179
  const validatedPresignedUrl = validateS3Url(presignedUrl, "presignedUrl");
153
180
  const validatedPublicUrl = validateS3Url(publicUrl, "publicUrl");
154
- const uploadResponse = await fetch(validatedPresignedUrl, {
155
- method: "PUT",
156
- headers: {
157
- "Content-Type": file.type || "application/octet-stream"
158
- },
159
- body: file
160
- });
161
- if (!uploadResponse.ok) {
162
- throw new Error(`Failed to upload file: ${uploadResponse.statusText}`);
181
+ const tPut = Date.now();
182
+ debugLog("s3:step3:putReq", "S3 PUT request", { publicUrlLen: validatedPublicUrl?.length });
183
+ let lastError;
184
+ const attempts = maxRetries + 1;
185
+ for (let attempt = 0; attempt < attempts; attempt++) {
186
+ try {
187
+ if (onProgress && typeof XMLHttpRequest !== "undefined") {
188
+ await new Promise((resolve, reject) => {
189
+ const xhr = new XMLHttpRequest();
190
+ xhr.timeout = uploadTimeoutMs;
191
+ xhr.upload.onprogress = (e) => {
192
+ if (e.lengthComputable) {
193
+ onProgress(Math.min(100, Math.round(e.loaded / e.total * 100)));
194
+ }
195
+ };
196
+ xhr.onload = () => {
197
+ if (xhr.status >= 200 && xhr.status < 300) resolve();
198
+ else reject(new Error(`Failed to upload file: ${xhr.statusText}`));
199
+ };
200
+ xhr.onerror = () => reject(new Error("Upload failed"));
201
+ xhr.ontimeout = () => reject(new Error("Upload timeout"));
202
+ xhr.open("PUT", validatedPresignedUrl);
203
+ xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
204
+ xhr.send(file);
205
+ });
206
+ } else {
207
+ const controller = new AbortController();
208
+ const timeoutId = setTimeout(() => controller.abort(), uploadTimeoutMs);
209
+ const uploadResponse = await fetch(validatedPresignedUrl, {
210
+ method: "PUT",
211
+ headers: {
212
+ "Content-Type": file.type || "application/octet-stream"
213
+ },
214
+ body: file,
215
+ signal: controller.signal
216
+ });
217
+ clearTimeout(timeoutId);
218
+ debugLog("s3:step4:putRes", "S3 PUT response", {
219
+ ok: uploadResponse.ok,
220
+ status: uploadResponse.status,
221
+ putElapsedMs: Date.now() - tPut
222
+ });
223
+ if (!uploadResponse.ok) {
224
+ throw new Error(`Failed to upload file: ${uploadResponse.statusText}`);
225
+ }
226
+ }
227
+ debugLog("s3:step5:return", "returning publicUrl", { urlPrefix: validatedPublicUrl.slice(0, 80) });
228
+ return validatedPublicUrl;
229
+ } catch (err) {
230
+ lastError = err instanceof Error ? err : new Error(String(err));
231
+ if (attempt < attempts - 1) {
232
+ debugLog("s3:putRetry", "PUT failed, retrying", { attempt: attempt + 1, attempts });
233
+ } else {
234
+ throw lastError;
235
+ }
236
+ }
163
237
  }
164
- return validatedPublicUrl;
238
+ throw lastError ?? new Error("Upload failed");
165
239
  } catch (error) {
166
240
  console.error("S3 upload failed:", error);
167
241
  throw error;
@@ -2438,15 +2512,14 @@ var LumirEditorError = class _LumirEditorError extends Error {
2438
2512
  }
2439
2513
  /**
2440
2514
  * 잘못된 파일 형식 에러 생성
2515
+ * @param allowVideoUpload true이면 "image and video" 메시지 사용
2441
2516
  */
2442
- static invalidFileType(fileName) {
2443
- return new _LumirEditorError(
2444
- `Invalid file type: ${fileName}. Only image files are allowed.`,
2445
- {
2446
- code: "INVALID_FILE_TYPE",
2447
- context: { fileName }
2448
- }
2449
- );
2517
+ static invalidFileType(fileName, allowVideoUpload) {
2518
+ const message = allowVideoUpload === true ? `Invalid file type: ${fileName}. Only image and video files are allowed.` : `Invalid file type: ${fileName}. Only image files are allowed.`;
2519
+ return new _LumirEditorError(message, {
2520
+ code: "INVALID_FILE_TYPE",
2521
+ context: { fileName }
2522
+ });
2450
2523
  }
2451
2524
  /**
2452
2525
  * S3 설정 에러 생성
@@ -2469,10 +2542,38 @@ var LumirEditorError = class _LumirEditorError extends Error {
2469
2542
 
2470
2543
  // src/constants/limits.ts
2471
2544
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
2545
+ var MAX_VIDEO_FILE_SIZE = 100 * 1024 * 1024;
2472
2546
  var BLOCKED_EXTENSIONS = [".svg", ".svgz"];
2547
+ var ALLOWED_VIDEO_MIME_TYPES = /* @__PURE__ */ new Set([
2548
+ "video/mp4",
2549
+ "video/webm",
2550
+ "video/ogg",
2551
+ "video/quicktime"
2552
+ // .mov
2553
+ ]);
2554
+ var ALLOWED_VIDEO_EXTENSIONS = [
2555
+ ".mp4",
2556
+ ".webm",
2557
+ ".ogg",
2558
+ ".mov"
2559
+ ];
2473
2560
 
2474
2561
  // src/components/LumirEditor.tsx
2475
2562
  var import_jsx_runtime16 = require("react/jsx-runtime");
2563
+ var DEBUG_LOG = (loc, msg, data) => {
2564
+ fetch("http://127.0.0.1:7686/ingest/1f8ee1c5-0cf0-4ae7-91ed-5ea7ed17130a", {
2565
+ method: "POST",
2566
+ headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "b73262" },
2567
+ body: JSON.stringify({
2568
+ sessionId: "b73262",
2569
+ location: loc,
2570
+ message: msg,
2571
+ data,
2572
+ timestamp: Date.now()
2573
+ })
2574
+ }).catch(() => {
2575
+ });
2576
+ };
2476
2577
  var ContentUtils = class {
2477
2578
  /**
2478
2579
  * JSON 문자열의 유효성을 검증합니다
@@ -2601,6 +2702,25 @@ var isImageFile = (file) => {
2601
2702
  }
2602
2703
  return file.type?.startsWith("image/") || !file.type && /\.(png|jpe?g|gif|webp|bmp)$/i.test(fileName);
2603
2704
  };
2705
+ var isVideoFile = (file) => {
2706
+ const sizeOk = file.size > 0 && file.size <= MAX_VIDEO_FILE_SIZE;
2707
+ const fileName = file.name?.toLowerCase() || "";
2708
+ const mimeMatch = ALLOWED_VIDEO_MIME_TYPES.has(file.type);
2709
+ const videoPrefix = typeof file.type === "string" && file.type.startsWith("video/");
2710
+ const extMatch = !file.type && ALLOWED_VIDEO_EXTENSIONS.some((ext) => fileName.endsWith(ext));
2711
+ const result = sizeOk && (mimeMatch || videoPrefix || extMatch);
2712
+ DEBUG_LOG("isVideoFile:check", "result", {
2713
+ fileName: file.name,
2714
+ fileType: file.type,
2715
+ fileSize: file.size,
2716
+ sizeOk,
2717
+ mimeMatch,
2718
+ videoPrefix,
2719
+ extMatch,
2720
+ result
2721
+ });
2722
+ return result;
2723
+ };
2604
2724
  var isHtmlFile = (file) => {
2605
2725
  return file.size > 0 && (file.type === "text/html" || file.name?.toLowerCase().endsWith(".html") || file.name?.toLowerCase().endsWith(".htm"));
2606
2726
  };
@@ -2614,15 +2734,17 @@ var escapeHtml = (str) => {
2614
2734
  };
2615
2735
  return str.replace(/[&<>"']/g, (char) => htmlEscapes[char]);
2616
2736
  };
2617
- var extractImageUrls = (blocks) => {
2737
+ var extractMediaUrls = (blocks) => {
2618
2738
  const urls = /* @__PURE__ */ new Set();
2619
2739
  const traverse = (blockList) => {
2620
2740
  for (const block of blockList) {
2621
2741
  if (block.type === "image" && block.props?.url) {
2622
2742
  const url = block.props.url;
2623
- if (typeof url === "string" && url.trim()) {
2624
- urls.add(url);
2625
- }
2743
+ if (typeof url === "string" && url.trim()) urls.add(url);
2744
+ }
2745
+ if (block.type === "video" && block.props?.url) {
2746
+ const url = block.props.url;
2747
+ if (typeof url === "string" && url.trim()) urls.add(url);
2626
2748
  }
2627
2749
  if (block.children && Array.isArray(block.children)) {
2628
2750
  traverse(block.children);
@@ -2632,12 +2754,10 @@ var extractImageUrls = (blocks) => {
2632
2754
  traverse(blocks);
2633
2755
  return urls;
2634
2756
  };
2635
- var findDeletedImageUrls = (previousUrls, currentUrls) => {
2757
+ var findDeletedMediaUrls = (previousUrls, currentUrls) => {
2636
2758
  const deleted = [];
2637
2759
  previousUrls.forEach((url) => {
2638
- if (!currentUrls.has(url)) {
2639
- deleted.push(url);
2640
- }
2760
+ if (!currentUrls.has(url)) deleted.push(url);
2641
2761
  });
2642
2762
  return deleted;
2643
2763
  };
@@ -2747,7 +2867,11 @@ function LumirEditor({
2747
2867
  onImageDelete
2748
2868
  }) {
2749
2869
  const [isUploading, setIsUploading] = (0, import_react16.useState)(false);
2870
+ const [uploadProgress, setUploadProgress] = (0, import_react16.useState)(null);
2750
2871
  const [errorMessage, setErrorMessage] = (0, import_react16.useState)(null);
2872
+ const floatingMenuFileInputRef = (0, import_react16.useRef)(null);
2873
+ const floatingMenuBlockRef = (0, import_react16.useRef)(null);
2874
+ const floatingMenuUploadStartTimeRef = (0, import_react16.useRef)(0);
2751
2875
  const handleError = (0, import_react16.useCallback)(
2752
2876
  (error) => {
2753
2877
  onError?.(error);
@@ -2778,6 +2902,13 @@ function LumirEditor({
2778
2902
  allowFileUpload
2779
2903
  );
2780
2904
  }, [disableExtensions, allowVideoUpload, allowAudioUpload, allowFileUpload]);
2905
+ (0, import_react16.useEffect)(() => {
2906
+ DEBUG_LOG("LumirEditor:init:disabledExtensions", "snapshot", {
2907
+ allowVideoUpload,
2908
+ hasVideoInDisabled: disabledExtensions.includes("video"),
2909
+ disabledList: disabledExtensions.slice(0, 15)
2910
+ });
2911
+ }, [allowVideoUpload, disabledExtensions]);
2781
2912
  const fileNameTransformRef = (0, import_react16.useRef)(s3Upload?.fileNameTransform);
2782
2913
  (0, import_react16.useEffect)(() => {
2783
2914
  fileNameTransformRef.current = s3Upload?.fileNameTransform;
@@ -2790,6 +2921,12 @@ function LumirEditor({
2790
2921
  path: s3Upload.path,
2791
2922
  appendUUID: s3Upload.appendUUID,
2792
2923
  preserveExtension: s3Upload.preserveExtension,
2924
+ uploadTimeoutMs: s3Upload.uploadTimeoutMs,
2925
+ maxRetries: s3Upload.maxRetries,
2926
+ onProgress: (percent) => {
2927
+ setUploadProgress(percent);
2928
+ s3Upload.onProgress?.(percent);
2929
+ },
2793
2930
  // 최신 콜백을 항상 사용하도록 ref를 통해 접근
2794
2931
  fileNameTransform: (originalName, file) => {
2795
2932
  return fileNameTransformRef.current ? fileNameTransformRef.current(originalName, file) : originalName;
@@ -2800,7 +2937,10 @@ function LumirEditor({
2800
2937
  s3Upload?.env,
2801
2938
  s3Upload?.path,
2802
2939
  s3Upload?.appendUUID,
2803
- s3Upload?.preserveExtension
2940
+ s3Upload?.preserveExtension,
2941
+ s3Upload?.uploadTimeoutMs,
2942
+ s3Upload?.maxRetries,
2943
+ s3Upload?.onProgress
2804
2944
  ]);
2805
2945
  const editor = (0, import_react17.useCreateBlockNote)(
2806
2946
  {
@@ -2818,18 +2958,44 @@ function LumirEditor({
2818
2958
  tabBehavior,
2819
2959
  trailingBlock,
2820
2960
  uploadFile: async (file) => {
2821
- if (!isImageFile(file)) {
2822
- const error = LumirEditorError.invalidFileType(file.name);
2961
+ const allowedImage = isImageFile(file);
2962
+ const allowedVideo = allowVideoUpload && isVideoFile(file);
2963
+ DEBUG_LOG("uploadFile:step1:entry", "editor uploadFile callback invoked", {
2964
+ fileName: file.name,
2965
+ fileType: file.type,
2966
+ fileSize: file.size,
2967
+ allowVideoUpload,
2968
+ allowedImage,
2969
+ allowedVideo
2970
+ });
2971
+ if (!allowedImage && !allowedVideo) {
2972
+ const error = LumirEditorError.invalidFileType(
2973
+ file.name,
2974
+ allowVideoUpload
2975
+ );
2823
2976
  handleError(error);
2824
2977
  throw error;
2825
2978
  }
2826
2979
  try {
2827
- let imageUrl;
2980
+ setUploadProgress(0);
2981
+ let fileUrl;
2982
+ const branch = uploadFile ? "custom" : memoizedS3Upload?.apiEndpoint ? "s3" : "none";
2983
+ DEBUG_LOG("uploadFile:step2:branch", "upload path", {
2984
+ branch,
2985
+ hasCustomUploadFile: !!uploadFile,
2986
+ hasS3ApiEndpoint: !!memoizedS3Upload?.apiEndpoint
2987
+ });
2828
2988
  if (uploadFile) {
2829
- imageUrl = await uploadFile(file);
2989
+ const t0 = Date.now();
2990
+ DEBUG_LOG("uploadFile:step3a:custom", "calling custom uploadFile", { fileName: file.name });
2991
+ fileUrl = await uploadFile(file);
2992
+ DEBUG_LOG("uploadFile:step3a:done", "custom uploadFile returned", { urlLen: fileUrl?.length, elapsedMs: Date.now() - t0 });
2830
2993
  } else if (memoizedS3Upload?.apiEndpoint) {
2994
+ const t0 = Date.now();
2995
+ DEBUG_LOG("uploadFile:step3b:s3", "calling S3 uploader", { fileName: file.name });
2831
2996
  const s3Uploader = createS3Uploader(memoizedS3Upload);
2832
- imageUrl = await s3Uploader(file);
2997
+ fileUrl = await s3Uploader(file);
2998
+ DEBUG_LOG("uploadFile:step3b:done", "S3 uploader returned", { urlLen: fileUrl?.length, elapsedMs: Date.now() - t0 });
2833
2999
  } else {
2834
3000
  const error = LumirEditorError.s3ConfigError(
2835
3001
  "No upload method available. Please provide uploadFile or s3Upload configuration."
@@ -2837,8 +3003,16 @@ function LumirEditor({
2837
3003
  handleError(error);
2838
3004
  throw error;
2839
3005
  }
2840
- return imageUrl;
3006
+ DEBUG_LOG("uploadFile:step4:success", "returning URL", {
3007
+ fileName: file.name,
3008
+ urlPrefix: fileUrl.slice(0, 80)
3009
+ });
3010
+ return fileUrl;
2841
3011
  } catch (error) {
3012
+ DEBUG_LOG("uploadFile:step5:catch", "uploadFile threw", {
3013
+ fileName: file.name,
3014
+ errorMessage: error instanceof Error ? error.message : String(error)
3015
+ });
2842
3016
  if (error instanceof LumirEditorError) {
2843
3017
  throw error;
2844
3018
  }
@@ -2848,6 +3022,8 @@ function LumirEditor({
2848
3022
  );
2849
3023
  handleError(lumirError);
2850
3024
  throw lumirError;
3025
+ } finally {
3026
+ setUploadProgress(null);
2851
3027
  }
2852
3028
  },
2853
3029
  pasteHandler: (ctx) => {
@@ -2876,7 +3052,15 @@ function LumirEditor({
2876
3052
  }
2877
3053
  const fileList = event?.clipboardData?.files ?? null;
2878
3054
  const files = fileList ? Array.from(fileList) : [];
2879
- const acceptedFiles = files.filter(isImageFile);
3055
+ const acceptedFiles = files.filter(
3056
+ (f) => isImageFile(f) || allowVideoUpload && isVideoFile(f)
3057
+ );
3058
+ DEBUG_LOG("paste:step1:files", "paste clipboard files", {
3059
+ filesCount: files.length,
3060
+ acceptedCount: acceptedFiles.length,
3061
+ fileNames: files.map((f) => f.name),
3062
+ acceptedNames: acceptedFiles.map((f) => f.name)
3063
+ });
2880
3064
  if (files.length > 0 && acceptedFiles.length === 0) {
2881
3065
  event.preventDefault();
2882
3066
  return true;
@@ -2890,13 +3074,26 @@ function LumirEditor({
2890
3074
  try {
2891
3075
  for (const file of acceptedFiles) {
2892
3076
  try {
3077
+ DEBUG_LOG("paste:step2:upload", "calling uploadFile for paste", {
3078
+ fileName: file.name,
3079
+ fileType: file.type
3080
+ });
2893
3081
  const url = await editor2.uploadFile(file);
2894
- editor2.pasteHTML(
2895
- `<img src="${escapeHtml(url)}" alt="image" />`
2896
- );
3082
+ if (isImageFile(file)) {
3083
+ editor2.pasteHTML(
3084
+ `<img src="${escapeHtml(url)}" alt="image" />`
3085
+ );
3086
+ } else if (isVideoFile(file)) {
3087
+ const currentBlock = editor2.getTextCursorPosition().block;
3088
+ editor2.insertBlocks(
3089
+ [{ type: "video", props: { url } }],
3090
+ currentBlock,
3091
+ "after"
3092
+ );
3093
+ }
2897
3094
  } catch (err) {
2898
3095
  console.warn(
2899
- "Image upload failed, skipped:",
3096
+ "Upload failed, skipped:",
2900
3097
  file.name || "",
2901
3098
  err
2902
3099
  );
@@ -2919,6 +3116,7 @@ function LumirEditor({
2919
3116
  trailingBlock,
2920
3117
  uploadFile,
2921
3118
  memoizedS3Upload,
3119
+ allowVideoUpload,
2922
3120
  linkPreview?.apiEndpoint,
2923
3121
  placeholder
2924
3122
  ]
@@ -2939,25 +3137,25 @@ function LumirEditor({
2939
3137
  };
2940
3138
  return editor.onEditorContentChange(handleContentChange);
2941
3139
  }, [editor, onContentChange]);
2942
- const previousImageUrlsRef = (0, import_react16.useRef)(/* @__PURE__ */ new Set());
3140
+ const previousMediaUrlsRef = (0, import_react16.useRef)(/* @__PURE__ */ new Set());
2943
3141
  (0, import_react16.useEffect)(() => {
2944
3142
  if (!editor) return;
2945
3143
  const initialBlocks = editor.topLevelBlocks;
2946
- previousImageUrlsRef.current = extractImageUrls(initialBlocks);
3144
+ previousMediaUrlsRef.current = extractMediaUrls(initialBlocks);
2947
3145
  }, [editor]);
2948
3146
  (0, import_react16.useEffect)(() => {
2949
3147
  if (!editor || !onImageDelete) return;
2950
- const handleImageDeleteCheck = () => {
3148
+ const handleMediaDeleteCheck = () => {
2951
3149
  const currentBlocks = editor.topLevelBlocks;
2952
- const currentUrls = extractImageUrls(currentBlocks);
2953
- const previousUrls = previousImageUrlsRef.current;
2954
- const deletedUrls = findDeletedImageUrls(previousUrls, currentUrls);
3150
+ const currentUrls = extractMediaUrls(currentBlocks);
3151
+ const previousUrls = previousMediaUrlsRef.current;
3152
+ const deletedUrls = findDeletedMediaUrls(previousUrls, currentUrls);
2955
3153
  deletedUrls.forEach((url) => {
2956
3154
  onImageDelete(url);
2957
3155
  });
2958
- previousImageUrlsRef.current = currentUrls;
3156
+ previousMediaUrlsRef.current = currentUrls;
2959
3157
  };
2960
- return editor.onEditorContentChange(handleImageDeleteCheck);
3158
+ return editor.onEditorContentChange(handleMediaDeleteCheck);
2961
3159
  }, [editor, onImageDelete]);
2962
3160
  (0, import_react16.useEffect)(() => {
2963
3161
  const el = editor?.domElement;
@@ -2979,11 +3177,31 @@ function LumirEditor({
2979
3177
  const items = Array.from(e.dataTransfer.items ?? []);
2980
3178
  const files = items.filter((it) => it.kind === "file").map((it) => it.getAsFile()).filter((f) => !!f);
2981
3179
  const imageFiles = files.filter(isImageFile);
3180
+ const videoFiles = allowVideoUpload ? files.filter(isVideoFile) : [];
2982
3181
  const htmlFiles = files.filter(isHtmlFile);
2983
- if (imageFiles.length === 0 && htmlFiles.length === 0) return;
3182
+ DEBUG_LOG("drop:step1:files", "drop received", {
3183
+ filesCount: files.length,
3184
+ imageCount: imageFiles.length,
3185
+ videoCount: videoFiles.length,
3186
+ htmlCount: htmlFiles.length,
3187
+ allowVideoUpload,
3188
+ firstFile: files[0] ? {
3189
+ name: files[0].name,
3190
+ type: files[0].type,
3191
+ size: files[0].size,
3192
+ isImage: isImageFile(files[0]),
3193
+ isVideo: isVideoFile(files[0])
3194
+ } : null
3195
+ });
3196
+ if (imageFiles.length === 0 && htmlFiles.length === 0 && videoFiles.length === 0)
3197
+ return;
2984
3198
  (async () => {
2985
3199
  setIsUploading(true);
2986
3200
  try {
3201
+ DEBUG_LOG("drop:step2:async", "drop async started", {
3202
+ imageCount: imageFiles.length,
3203
+ videoCount: videoFiles.length
3204
+ });
2987
3205
  for (const file of imageFiles) {
2988
3206
  try {
2989
3207
  if (editor?.uploadFile) {
@@ -3002,6 +3220,34 @@ function LumirEditor({
3002
3220
  );
3003
3221
  }
3004
3222
  }
3223
+ DEBUG_LOG("drop:step3:videoLoop", "video loop start", {
3224
+ videoCount: videoFiles.length,
3225
+ names: videoFiles.map((f) => f.name)
3226
+ });
3227
+ for (const file of videoFiles) {
3228
+ try {
3229
+ if (editor?.uploadFile) {
3230
+ DEBUG_LOG("drop:step4:videoUpload", "calling uploadFile for video", {
3231
+ fileName: file.name
3232
+ });
3233
+ const url = await editor.uploadFile(file);
3234
+ if (url && typeof url === "string") {
3235
+ const currentBlock = editor.getTextCursorPosition().block;
3236
+ editor.insertBlocks(
3237
+ [{ type: "video", props: { url } }],
3238
+ currentBlock,
3239
+ "after"
3240
+ );
3241
+ }
3242
+ }
3243
+ } catch (err) {
3244
+ console.warn(
3245
+ "Video upload failed, skipped:",
3246
+ file.name || "",
3247
+ err
3248
+ );
3249
+ }
3250
+ }
3005
3251
  for (const file of htmlFiles) {
3006
3252
  try {
3007
3253
  const htmlContent = await file.text();
@@ -3041,7 +3287,7 @@ function LumirEditor({
3041
3287
  });
3042
3288
  el.removeEventListener("drop", handleDrop, { capture: true });
3043
3289
  };
3044
- }, [editor]);
3290
+ }, [editor, allowVideoUpload]);
3045
3291
  const computedSideMenu = (0, import_react16.useMemo)(() => {
3046
3292
  return sideMenuAddButton ? sideMenu : false;
3047
3293
  }, [sideMenuAddButton, sideMenu]);
@@ -3054,42 +3300,110 @@ function LumirEditor({
3054
3300
  className: cn("lumirEditor", className),
3055
3301
  style: { position: "relative", display: "flex", flexDirection: "column" },
3056
3302
  children: [
3057
- floatingMenu && editor && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3058
- FloatingMenu,
3059
- {
3060
- editor,
3061
- position: floatingMenuPosition,
3062
- onImageUpload: async () => {
3063
- const input = document.createElement("input");
3064
- input.type = "file";
3065
- input.accept = "image/*";
3066
- input.onchange = async (e) => {
3067
- const file = e.target.files?.[0];
3068
- if (file && editor.uploadFile) {
3069
- try {
3070
- setIsUploading(true);
3071
- const url = await editor.uploadFile(file);
3072
- editor.insertBlocks(
3073
- [
3074
- {
3075
- type: "image",
3076
- props: { url }
3077
- }
3078
- ],
3079
- editor.getTextCursorPosition().block,
3080
- "after"
3081
- );
3082
- } catch (err) {
3083
- console.error("Image upload failed:", err);
3084
- } finally {
3085
- setIsUploading(false);
3303
+ floatingMenu && editor && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
3304
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3305
+ "input",
3306
+ {
3307
+ ref: floatingMenuFileInputRef,
3308
+ type: "file",
3309
+ accept: allowVideoUpload ? "image/*,video/mp4,video/webm,video/ogg,video/quicktime,.mov" : "image/*",
3310
+ style: {
3311
+ position: "absolute",
3312
+ left: "-9999px",
3313
+ opacity: 0,
3314
+ pointerEvents: "none"
3315
+ },
3316
+ onChange: async (e) => {
3317
+ const inputEl = e.target;
3318
+ const file = inputEl.files?.[0];
3319
+ DEBUG_LOG("FloatingMenu:step3:onchange", "file input onchange fired", {
3320
+ hasFile: !!file,
3321
+ fileName: file?.name,
3322
+ fileType: file?.type,
3323
+ fileSize: file?.size,
3324
+ hasUploadFile: !!editor?.uploadFile
3325
+ });
3326
+ const blockToInsertAfter = floatingMenuBlockRef.current;
3327
+ if (file && editor.uploadFile && blockToInsertAfter) {
3328
+ const allowedImage = isImageFile(file);
3329
+ const allowedVideo = allowVideoUpload && isVideoFile(file);
3330
+ DEBUG_LOG("FloatingMenu:step4:fileCheck", "allowed check", {
3331
+ fileName: file.name,
3332
+ allowedImage,
3333
+ allowedVideo
3334
+ });
3335
+ if (allowedImage || allowedVideo) {
3336
+ try {
3337
+ setIsUploading(true);
3338
+ floatingMenuUploadStartTimeRef.current = Date.now();
3339
+ DEBUG_LOG("FloatingMenu:step5:uploadStart", "calling editor.uploadFile", {
3340
+ fileName: file.name
3341
+ });
3342
+ const url = await editor.uploadFile(file);
3343
+ const blockType = allowedVideo ? "video" : "image";
3344
+ const elapsedMs = Date.now() - floatingMenuUploadStartTimeRef.current;
3345
+ DEBUG_LOG("FloatingMenu:step6:uploadDone", "upload returned, inserting block", {
3346
+ blockType,
3347
+ blockId: blockToInsertAfter.id,
3348
+ urlLen: url?.length,
3349
+ elapsedMs
3350
+ });
3351
+ editor.insertBlocks(
3352
+ [
3353
+ {
3354
+ type: blockType,
3355
+ props: { url }
3356
+ }
3357
+ ],
3358
+ blockToInsertAfter,
3359
+ "after"
3360
+ );
3361
+ } catch (err) {
3362
+ DEBUG_LOG("FloatingMenu:step7:catch", "upload or insert failed", {
3363
+ errMsg: err instanceof Error ? err.message : String(err)
3364
+ });
3365
+ console.error("Upload failed:", err);
3366
+ } finally {
3367
+ setIsUploading(false);
3368
+ }
3086
3369
  }
3087
3370
  }
3088
- };
3089
- input.click();
3371
+ inputEl.value = "";
3372
+ }
3090
3373
  }
3091
- }
3092
- ),
3374
+ ),
3375
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3376
+ FloatingMenu,
3377
+ {
3378
+ editor,
3379
+ position: floatingMenuPosition,
3380
+ onImageUpload: () => {
3381
+ DEBUG_LOG("FloatingMenu:step1:click", "upload button clicked", {
3382
+ allowVideoUpload
3383
+ });
3384
+ let blockToInsertAfter;
3385
+ try {
3386
+ blockToInsertAfter = editor.getTextCursorPosition().block;
3387
+ } catch (err) {
3388
+ DEBUG_LOG("FloatingMenu:step1b:error", "getTextCursorPosition failed", {
3389
+ err: err instanceof Error ? err.message : String(err)
3390
+ });
3391
+ return;
3392
+ }
3393
+ floatingMenuBlockRef.current = blockToInsertAfter;
3394
+ const input = floatingMenuFileInputRef.current;
3395
+ if (!input) return;
3396
+ input.accept = allowVideoUpload ? "image/*,video/mp4,video/webm,video/ogg,video/quicktime,.mov" : "image/*";
3397
+ input.value = "";
3398
+ DEBUG_LOG("FloatingMenu:step2:inputReady", "persistent input ref, about to click", {
3399
+ accept: input.accept
3400
+ });
3401
+ DEBUG_LOG("FloatingMenu:step2b:click", "input.click() about to be called", {});
3402
+ input.click();
3403
+ }
3404
+ }
3405
+ )
3406
+ ] }),
3093
3407
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
3094
3408
  import_mantine.BlockNoteView,
3095
3409
  {
@@ -3116,8 +3430,10 @@ function LumirEditor({
3116
3430
  const filtered = items.filter((item) => {
3117
3431
  const key = (item?.key || "").toString().toLowerCase();
3118
3432
  const title = (item?.title || "").toString().toLowerCase();
3119
- if (["video", "audio", "file"].includes(key)) return false;
3120
- if (title.includes("video") || title.includes("audio") || title.includes("file"))
3433
+ if (key === "video" || title.includes("video"))
3434
+ return allowVideoUpload;
3435
+ if (["audio", "file"].includes(key)) return false;
3436
+ if (title.includes("audio") || title.includes("file"))
3121
3437
  return false;
3122
3438
  return true;
3123
3439
  });
@@ -3218,7 +3534,7 @@ function LumirEditor({
3218
3534
  )
3219
3535
  );
3220
3536
  },
3221
- [editor, linkPreview?.apiEndpoint]
3537
+ [editor, allowVideoUpload, linkPreview?.apiEndpoint]
3222
3538
  )
3223
3539
  }
3224
3540
  ),
@@ -3226,7 +3542,13 @@ function LumirEditor({
3226
3542
  ]
3227
3543
  }
3228
3544
  ),
3229
- isUploading && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "lumirEditor-upload-overlay", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "lumirEditor-spinner" }) }),
3545
+ isUploading && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "lumirEditor-upload-overlay", children: [
3546
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "lumirEditor-spinner" }),
3547
+ uploadProgress !== null && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "lumirEditor-upload-progress", children: [
3548
+ uploadProgress,
3549
+ "%"
3550
+ ] })
3551
+ ] }),
3230
3552
  errorMessage && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "lumirEditor-error-toast", children: [
3231
3553
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "lumirEditor-error-icon", children: "\u26A0\uFE0F" }),
3232
3554
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "lumirEditor-error-message", children: errorMessage }),