From 3895ca985f845dd99c9f0af83af7d5aec3c9da20 Mon Sep 17 00:00:00 2001 From: Ishan Panta Date: Fri, 3 Apr 2026 08:37:16 +0545 Subject: [PATCH 01/21] [add] extend speed options with higher presets and custom speed input add 3x, 4x, 5x speed presets and a custom playback speed input field that accepts any integer value up to 16x. change PlaybackSpeed type from a fixed union to number with min/max constants and clamp utility. update project persistence to validate any speed in range instead of exact value matching. add i18n keys for en, es, zh-CN. closes #252 --- src/components/video-editor/SettingsPanel.tsx | 90 ++++++++++++++++++- .../video-editor/projectPersistence.ts | 15 ++-- src/components/video-editor/types.ts | 14 ++- src/i18n/locales/en/settings.json | 4 +- src/i18n/locales/es/settings.json | 4 +- src/i18n/locales/zh-CN/settings.json | 4 +- 6 files changed, 117 insertions(+), 14 deletions(-) diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index f5afe35..cdb00ce 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -53,7 +53,70 @@ import type { WebcamLayoutPreset, ZoomDepth, } from "./types"; -import { SPEED_OPTIONS } from "./types"; +import { MAX_PLAYBACK_SPEED, SPEED_OPTIONS } from "./types"; + +function CustomSpeedInput({ + value, + onChange, + onError, +}: { + value: number; + onChange: (val: number) => void; + onError: () => void; +}) { + const isPreset = SPEED_OPTIONS.some((o) => o.speed === value); + const [draft, setDraft] = useState(isPreset ? "" : String(Math.round(value))); + const [isFocused, setIsFocused] = useState(false); + + const prevValue = useRef(value); + if (!isFocused && prevValue.current !== value) { + prevValue.current = value; + setDraft(isPreset ? "" : String(Math.round(value))); + } + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + const digits = e.target.value.replace(/\D/g, ""); + if (digits === "") { + setDraft(""); + return; + } + const num = Number(digits); + if (num > MAX_PLAYBACK_SPEED) { + onError(); + return; + } + setDraft(digits); + if (num >= 1) onChange(num); + }, + [onChange, onError], + ); + + const handleBlur = useCallback(() => { + setIsFocused(false); + if (!draft || Number(draft) < 1) { + setDraft(isPreset ? "" : String(Math.round(value))); + } + }, [draft, isPreset, value]); + + return ( +
+ setIsFocused(true)} + onChange={handleChange} + onBlur={handleBlur} + onKeyDown={(e) => e.key === "Enter" && (e.target as HTMLInputElement).blur()} + className="w-12 bg-white/5 border border-white/10 rounded-md px-1 py-0.5 text-[11px] font-semibold text-[#d97706] text-center focus:outline-none focus:border-[#d97706]/40" + /> + × +
+ ); +} const WALLPAPER_COUNT = 18; const WALLPAPER_RELATIVE = Array.from( @@ -537,7 +600,7 @@ export function SettingsPanel({ )} -
+
{SPEED_OPTIONS.map((option) => { const isActive = selectedSpeedValue === option.speed; return ( @@ -562,6 +625,29 @@ export function SettingsPanel({ ); })}
+
+
+ + {t("speed.customPlaybackSpeed")} + + {selectedSpeedId ? ( + onSpeedChange?.(val)} + onError={() => toast.error(t("speed.maxSpeedError"))} + /> + ) : ( +
+
+ -- +
+ × +
+ )} +
+
{!selectedSpeedId && (

{t("speed.selectRegion")}

)} diff --git a/src/components/video-editor/projectPersistence.ts b/src/components/video-editor/projectPersistence.ts index 99f1bba..bfe6972 100644 --- a/src/components/video-editor/projectPersistence.ts +++ b/src/components/video-editor/projectPersistence.ts @@ -5,6 +5,7 @@ import { ASPECT_RATIOS, type AspectRatio } from "@/utils/aspectRatioUtils"; import { type AnnotationRegion, type CropRegion, + clampPlaybackSpeed, DEFAULT_ANNOTATION_POSITION, DEFAULT_ANNOTATION_SIZE, DEFAULT_ANNOTATION_STYLE, @@ -14,6 +15,8 @@ import { DEFAULT_WEBCAM_LAYOUT_PRESET, DEFAULT_WEBCAM_POSITION, DEFAULT_ZOOM_DEPTH, + MAX_PLAYBACK_SPEED, + MIN_PLAYBACK_SPEED, type SpeedRegion, type TrimRegion, type WebcamLayoutPreset, @@ -219,14 +222,10 @@ export function normalizeProjectEditor(editor: Partial): Pro const endMs = Math.max(startMs + 1, rawEnd); const speed = - region.speed === 0.25 || - region.speed === 0.5 || - region.speed === 0.75 || - region.speed === 1.25 || - region.speed === 1.5 || - region.speed === 1.75 || - region.speed === 2 - ? region.speed + isFiniteNumber(region.speed) && + region.speed >= MIN_PLAYBACK_SPEED && + region.speed <= MAX_PLAYBACK_SPEED + ? clampPlaybackSpeed(region.speed) : DEFAULT_PLAYBACK_SPEED; return { diff --git a/src/components/video-editor/types.ts b/src/components/video-editor/types.ts index ce49f8e..d52e60a 100644 --- a/src/components/video-editor/types.ts +++ b/src/components/video-editor/types.ts @@ -132,7 +132,16 @@ export const DEFAULT_CROP_REGION: CropRegion = { height: 1, }; -export type PlaybackSpeed = 0.25 | 0.5 | 0.75 | 1.25 | 1.5 | 1.75 | 2; +export type PlaybackSpeed = number; + +export const MIN_PLAYBACK_SPEED = 0.1; +// Anything above 16x causes the playhead to stall during preview +// due to the video decoder not being able to keep up. +export const MAX_PLAYBACK_SPEED = 16; + +export function clampPlaybackSpeed(speed: number): PlaybackSpeed { + return Math.round(Math.min(MAX_PLAYBACK_SPEED, Math.max(MIN_PLAYBACK_SPEED, speed)) * 100) / 100; +} export interface SpeedRegion { id: string; @@ -149,6 +158,9 @@ export const SPEED_OPTIONS: Array<{ speed: PlaybackSpeed; label: string }> = [ { speed: 1.5, label: "1.5×" }, { speed: 1.75, label: "1.75×" }, { speed: 2, label: "2×" }, + { speed: 3, label: "3×" }, + { speed: 4, label: "4×" }, + { speed: 5, label: "5×" }, ]; export const DEFAULT_PLAYBACK_SPEED: PlaybackSpeed = 1.5; diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 36b7462..ad5f308 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -7,7 +7,9 @@ "speed": { "playbackSpeed": "Playback Speed", "selectRegion": "Select a speed region to adjust", - "deleteRegion": "Delete Speed Region" + "deleteRegion": "Delete Speed Region", + "customPlaybackSpeed": "Custom Playback Speed", + "maxSpeedError": "Speed can't go higher than 16×" }, "trim": { "deleteRegion": "Delete Trim Region" diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json index 4674480..f4c2d42 100644 --- a/src/i18n/locales/es/settings.json +++ b/src/i18n/locales/es/settings.json @@ -7,7 +7,9 @@ "speed": { "playbackSpeed": "Velocidad de reproducción", "selectRegion": "Selecciona una región de velocidad para ajustar", - "deleteRegion": "Eliminar región de velocidad" + "deleteRegion": "Eliminar región de velocidad", + "customPlaybackSpeed": "Velocidad personalizada", + "maxSpeedError": "La velocidad no puede superar 16×" }, "trim": { "deleteRegion": "Eliminar región de recorte" diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index 41bf55b..d38291e 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -7,7 +7,9 @@ "speed": { "playbackSpeed": "播放速度", "selectRegion": "选择要调整的速度区域", - "deleteRegion": "删除速度区域" + "deleteRegion": "删除速度区域", + "customPlaybackSpeed": "自定义播放速度", + "maxSpeedError": "速度不能超过 16×" }, "trim": { "deleteRegion": "删除剪辑区域" From 14cd045e65a4ba5320b885568b6e94bf67f6d04f Mon Sep 17 00:00:00 2001 From: Ayush765-spec Date: Fri, 3 Apr 2026 18:57:05 +0530 Subject: [PATCH 02/21] [Feature]: Ability to start a new recording from the editor --- electron/electron-env.d.ts | 1 + electron/ipc/handlers.ts | 5 ++ electron/main.ts | 11 ++++ electron/preload.ts | 3 ++ package-lock.json | 4 +- src/components/video-editor/VideoEditor.tsx | 56 ++++++++++++++++++++- src/vite-env.d.ts | 1 + 7 files changed, 78 insertions(+), 3 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 573aee8..82c7e57 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -26,6 +26,7 @@ interface Window { electronAPI: { getSources: (opts: Electron.SourcesOptions) => Promise; switchToEditor: () => Promise; + switchToHud: () => Promise; openSourceSelector: () => Promise; selectSource: (source: ProcessedDesktopSource) => Promise; getSelectedSource: () => Promise; diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 78d8344..eb9e96b 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -217,7 +217,12 @@ export function registerIpcHandlers( getMainWindow: () => BrowserWindow | null, getSourceSelectorWindow: () => BrowserWindow | null, onRecordingStateChange?: (recording: boolean, sourceName: string) => void, + switchToHud?: () => void, ) { + ipcMain.handle("switch-to-hud", () => { + if (switchToHud) switchToHud(); + }); + ipcMain.handle("get-sources", async (_, opts) => { const sources = await desktopCapturer.getSources(opts); return sources.map((source) => ({ diff --git a/electron/main.ts b/electron/main.ts index 7e19d46..0f06f9e 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -371,6 +371,16 @@ app.whenReady().then(async () => { // Ensure recordings directory exists await ensureRecordingsDir(); + function switchToHudWrapper() { + if (mainWindow) { + isForceClosing = true; + mainWindow.close(); + isForceClosing = false; + mainWindow = null; + } + showMainWindow(); + } + registerIpcHandlers( createEditorWindowWrapper, createSourceSelectorWindowWrapper, @@ -384,6 +394,7 @@ app.whenReady().then(async () => { showMainWindow(); } }, + switchToHudWrapper, ); createWindow(); }); diff --git a/electron/preload.ts b/electron/preload.ts index 8f1836b..34731e5 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -18,6 +18,9 @@ contextBridge.exposeInMainWorld("electronAPI", { switchToEditor: () => { return ipcRenderer.invoke("switch-to-editor"); }, + switchToHud: () => { + return ipcRenderer.invoke("switch-to-hud"); + }, openSourceSelector: () => { return ipcRenderer.invoke("open-source-selector"); }, diff --git a/package-lock.json b/package-lock.json index 58e37a3..5d07f2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openscreen", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openscreen", - "version": "1.2.0", + "version": "1.3.0", "dependencies": { "@fix-webm-duration/fix": "^1.0.1", "@pixi/filter-drop-shadow": "^5.2.0", diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 304d10f..dae009a 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -1,8 +1,16 @@ import type { Span } from "dnd-timeline"; -import { FolderOpen, Languages, Save } from "lucide-react"; +import { FolderOpen, Languages, Save, Video } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { useI18n, useScopedT } from "@/contexts/I18nContext"; import { useShortcuts } from "@/contexts/ShortcutsContext"; import { INITIAL_EDITOR_STATE, useEditorHistory } from "@/hooks/useEditorHistory"; @@ -107,6 +115,7 @@ export default function VideoEditor() { const [exportProgress, setExportProgress] = useState(null); const [exportError, setExportError] = useState(null); const [showExportDialog, setShowExportDialog] = useState(false); + const [showNewRecordingDialog, setShowNewRecordingDialog] = useState(false); const [exportQuality, setExportQuality] = useState("good"); const [exportFormat, setExportFormat] = useState("mp4"); const [gifFrameRate, setGifFrameRate] = useState(15); @@ -464,6 +473,13 @@ export default function VideoEditor() { await saveProject(true); }, [saveProject]); + const handleNewRecordingConfirm = useCallback(async () => { + setShowNewRecordingDialog(false); + await window.electronAPI.clearCurrentVideoPath(); + await window.electronAPI.setCurrentRecordingSession(null); + await window.electronAPI.switchToHud(); + }, []); + const handleLoadProject = useCallback(async () => { const result = await window.electronAPI.loadProjectFile(); @@ -1393,6 +1409,36 @@ export default function VideoEditor() { return (
+ + + + New Recording + + Start a new recording? Your current recording will be discarded. + + + + + + + + +
+ @@ -1470,7 +1472,7 @@ export default function VideoEditor() { className="flex items-center gap-1 px-2 py-1 rounded-md text-white/50 hover:text-white/90 hover:bg-white/10 transition-all duration-150 text-[11px] font-medium" >
@@ -755,11 +829,16 @@ export function SettingsPanel({ )} - +
- {t("effects.title")} + + {t("effects.title")} +
@@ -783,7 +862,9 @@ export function SettingsPanel({ {t("effects.motionBlur")}
- {motionBlurAmount === 0 ? t("effects.off") : motionBlurAmount.toFixed(2)} + {motionBlurAmount === 0 + ? t("effects.off") + : motionBlurAmount.toFixed(2)} {t("effects.roundness")} - {borderRadius}px + + {borderRadius}px + onBorderRadiusChange?.(values[0])} + onValueChange={(values) => + onBorderRadiusChange?.(values[0]) + } onValueCommit={() => onBorderRadiusCommit?.()} min={0} max={16} @@ -840,11 +925,15 @@ export function SettingsPanel({ {t("effects.padding")} - {webcamLayoutPreset === "vertical-stack" ? "—" : `${padding}%`} + {webcamLayoutPreset === "vertical-stack" + ? "—" + : `${padding}%`} onPaddingChange?.(values[0])} onValueCommit={() => onPaddingCommit?.()} min={0} @@ -874,7 +963,9 @@ export function SettingsPanel({
- {t("background.title")} + + {t("background.title")} +
@@ -939,7 +1030,9 @@ export function SettingsPanel({ role="button" > ))} @@ -1265,7 +1392,9 @@ export function SettingsPanel({ {gifOutputDimensions.width} × {gifOutputDimensions.height}px
- {t("gifSettings.loop")} + + {t("gifSettings.loop")} + - {exportFormat === "gif" ? t("export.gifButton") : t("export.videoButton")} + {exportFormat === "gif" + ? t("export.gifButton") + : t("export.videoButton")}
@@ -1314,7 +1445,9 @@ export function SettingsPanel({ ))}
)} + {webcamLayoutPreset === "picture-in-picture" && ( +
+
+
+ {t("layout.webcamSize")} +
+
+ {webcamSizePreset}% +
+
+ onWebcamSizePresetChange?.(values[0])} + onValueCommit={() => onWebcamSizePresetCommit?.()} + min={10} + max={50} + step={1} + className="w-full" + /> +
+ )}
)} - +
- - {t("effects.title")} - + {t("effects.title")}
@@ -862,9 +815,7 @@ export function SettingsPanel({ {t("effects.motionBlur")} - {motionBlurAmount === 0 - ? t("effects.off") - : motionBlurAmount.toFixed(2)} + {motionBlurAmount === 0 ? t("effects.off") : motionBlurAmount.toFixed(2)} {t("effects.roundness")} - - {borderRadius}px - + {borderRadius}px - onBorderRadiusChange?.(values[0]) - } + onValueChange={(values) => onBorderRadiusChange?.(values[0])} onValueCommit={() => onBorderRadiusCommit?.()} min={0} max={16} @@ -925,15 +872,11 @@ export function SettingsPanel({ {t("effects.padding")} - {webcamLayoutPreset === "vertical-stack" - ? "—" - : `${padding}%`} + {webcamLayoutPreset === "vertical-stack" ? "—" : `${padding}%`} onPaddingChange?.(values[0])} onValueCommit={() => onPaddingCommit?.()} min={0} @@ -963,9 +906,7 @@ export function SettingsPanel({
- - {t("background.title")} - + {t("background.title")}
@@ -1030,9 +971,7 @@ export function SettingsPanel({ role="button" > ))} @@ -1392,9 +1299,7 @@ export function SettingsPanel({ {gifOutputDimensions.width} × {gifOutputDimensions.height}px
- - {t("gifSettings.loop")} - + {t("gifSettings.loop")} - {exportFormat === "gif" - ? t("export.gifButton") - : t("export.videoButton")} + {exportFormat === "gif" ? t("export.gifButton") : t("export.videoButton")}
@@ -1445,9 +1348,7 @@ export function SettingsPanel({ diff --git a/src/i18n/locales/en/editor.json b/src/i18n/locales/en/editor.json index 1b772a2..ea2ceaa 100644 --- a/src/i18n/locales/en/editor.json +++ b/src/i18n/locales/en/editor.json @@ -1,7 +1,7 @@ { "newRecording": { - "title": "New Recording", - "description": "Start a new recording? Your current recording will be discarded.", + "title": "Return to Recorder", + "description": "Your current session has been saved.", "cancel": "Cancel", "confirm": "Confirm" }, diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index e3ed9b4..d76ee15 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -20,6 +20,7 @@ interface Window { getSources: (opts: Electron.SourcesOptions) => Promise; switchToEditor: () => Promise; switchToHud: () => Promise; + startNewRecording: () => Promise<{ success: boolean; error?: string }>; openSourceSelector: () => Promise; selectSource: (source: ProcessedDesktopSource) => Promise; getSelectedSource: () => Promise; From 90ba71332395ca989c12f5dff4ac09db0b9ae35d Mon Sep 17 00:00:00 2001 From: AmitwalaH Date: Mon, 6 Apr 2026 15:08:49 +0530 Subject: [PATCH 11/21] fix(i18n): update tutorial dialog translation keys for all locales --- src/i18n/locales/en/dialogs.json | 13 ++++++++----- src/i18n/locales/es/dialogs.json | 12 +++++++----- src/i18n/locales/zh-CN/dialogs.json | 12 +++++++----- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/i18n/locales/en/dialogs.json b/src/i18n/locales/en/dialogs.json index 66a33c2..d1fb685 100644 --- a/src/i18n/locales/en/dialogs.json +++ b/src/i18n/locales/en/dialogs.json @@ -27,10 +27,11 @@ "triggerLabel": "How trimming works", "title": "How Trimming Works", "description": "Understanding how to cut out unwanted parts of your video.", - "explanation": "The Trim tool works by defining the segments you want to", - "explanationRemove": "remove", - "explanationCovered": "covered", - "explanationEnd": "by a red trim segment will be cut out when you export.", + "explanationBefore": "The Trim tool works by defining the segments you want to", + "remove": "remove", + "explanationMiddle": "— anything", + "covered": "covered", + "explanationAfter": "by a red trim segment will be cut out when you export.", "visualExample": "Visual Example", "removed": "REMOVED", "kept": "Kept", @@ -39,7 +40,9 @@ "part3": "Part 3", "finalVideo": "Final Video", "step1Title": "1. Add Trim", - "step1Description": "Press T or click the scissors icon to mark a section for removal.", + "step1DescriptionBefore": "Press", + "step1DescriptionAfter": "or click the scissors icon to mark a section for removal.", + "step2Title": "2. Adjust", "step2Description": "Drag the edges of the red region to cover exactly what you want to cut out." }, diff --git a/src/i18n/locales/es/dialogs.json b/src/i18n/locales/es/dialogs.json index acf2a04..6d7fe7d 100644 --- a/src/i18n/locales/es/dialogs.json +++ b/src/i18n/locales/es/dialogs.json @@ -27,10 +27,11 @@ "triggerLabel": "Cómo funciona el recorte", "title": "Cómo funciona el recorte", "description": "Aprende a eliminar las partes no deseadas de tu video.", - "explanation": "La herramienta de recorte funciona definiendo los segmentos que deseas", - "explanationRemove": "eliminar", - "explanationCovered": "cubierto", - "explanationEnd": "por un segmento rojo de recorte será eliminado al exportar.", + "explanationBefore": "La herramienta de recorte funciona definiendo los segmentos que deseas", + "remove": "eliminar", + "explanationMiddle": "— cualquier parte", + "covered": "cubierta", + "explanationAfter": "por un segmento rojo será eliminada al exportar.", "visualExample": "Ejemplo visual", "removed": "ELIMINADO", "kept": "Conservado", @@ -39,7 +40,8 @@ "part3": "Parte 3", "finalVideo": "Video final", "step1Title": "1. Agregar recorte", - "step1Description": "Presiona T o haz clic en el ícono de tijeras para marcar una sección a eliminar.", + "step1DescriptionBefore": "Presiona", + "step1DescriptionAfter": "o haz clic en el ícono de tijeras para marcar una sección a eliminar.", "step2Title": "2. Ajustar", "step2Description": "Arrastra los bordes de la región roja para cubrir exactamente lo que deseas eliminar." }, diff --git a/src/i18n/locales/zh-CN/dialogs.json b/src/i18n/locales/zh-CN/dialogs.json index 3f181bc..0385b36 100644 --- a/src/i18n/locales/zh-CN/dialogs.json +++ b/src/i18n/locales/zh-CN/dialogs.json @@ -27,10 +27,11 @@ "triggerLabel": "剪辑功能说明", "title": "剪辑功能说明", "description": "了解如何剪掉视频中不需要的部分。", - "explanation": "剪辑工具通过定义您要", - "explanationRemove": "移除", - "explanationCovered": "覆盖", - "explanationEnd": "的片段来工作。被红色剪辑区域覆盖的部分将在导出时被剪掉。", + "explanationBefore": "剪辑工具通过定义您要", + "remove": "移除", + "explanationMiddle": "——任何被", + "covered": "覆盖", + "explanationAfter": "的红色剪辑区域部分将在导出时被剪掉。", "visualExample": "示例演示", "removed": "已移除", "kept": "保留", @@ -39,7 +40,8 @@ "part3": "第 3 部分", "finalVideo": "最终视频", "step1Title": "1. 添加剪辑", - "step1Description": "按 T 或点击剪刀图标来标记要移除的片段。", + "step1DescriptionBefore": "按", + "step1DescriptionAfter": "键或点击剪刀图标来标记要移除的片段。", "step2Title": "2. 调整", "step2Description": "拖动红色区域的边缘,精确覆盖您要剪掉的部分。" }, From 4e2a53b2004f88f4847c501b8e016d752c96ab21 Mon Sep 17 00:00:00 2001 From: AmitwalaH Date: Mon, 6 Apr 2026 15:19:24 +0530 Subject: [PATCH 12/21] fix: spacing issues in tutorial translations --- src/i18n/locales/en/dialogs.json | 6 +++--- src/i18n/locales/es/dialogs.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/i18n/locales/en/dialogs.json b/src/i18n/locales/en/dialogs.json index d1fb685..a84b5fd 100644 --- a/src/i18n/locales/en/dialogs.json +++ b/src/i18n/locales/en/dialogs.json @@ -29,7 +29,7 @@ "description": "Understanding how to cut out unwanted parts of your video.", "explanationBefore": "The Trim tool works by defining the segments you want to", "remove": "remove", - "explanationMiddle": "— anything", + "explanationMiddle": " — anything", "covered": "covered", "explanationAfter": "by a red trim segment will be cut out when you export.", "visualExample": "Visual Example", @@ -40,8 +40,8 @@ "part3": "Part 3", "finalVideo": "Final Video", "step1Title": "1. Add Trim", - "step1DescriptionBefore": "Press", - "step1DescriptionAfter": "or click the scissors icon to mark a section for removal.", + "step1DescriptionBefore": "Press ", + "step1DescriptionAfter": " or click the scissors icon to mark a section for removal.", "step2Title": "2. Adjust", "step2Description": "Drag the edges of the red region to cover exactly what you want to cut out." diff --git a/src/i18n/locales/es/dialogs.json b/src/i18n/locales/es/dialogs.json index 6d7fe7d..f8a5e63 100644 --- a/src/i18n/locales/es/dialogs.json +++ b/src/i18n/locales/es/dialogs.json @@ -29,7 +29,7 @@ "description": "Aprende a eliminar las partes no deseadas de tu video.", "explanationBefore": "La herramienta de recorte funciona definiendo los segmentos que deseas", "remove": "eliminar", - "explanationMiddle": "— cualquier parte", + "explanationMiddle": " — cualquier parte", "covered": "cubierta", "explanationAfter": "por un segmento rojo será eliminada al exportar.", "visualExample": "Ejemplo visual", @@ -40,8 +40,8 @@ "part3": "Parte 3", "finalVideo": "Video final", "step1Title": "1. Agregar recorte", - "step1DescriptionBefore": "Presiona", - "step1DescriptionAfter": "o haz clic en el ícono de tijeras para marcar una sección a eliminar.", + "step1DescriptionBefore": "Presiona ", + "step1DescriptionAfter": " o haz clic en el ícono de tijeras para marcar una sección a eliminar.", "step2Title": "2. Ajustar", "step2Description": "Arrastra los bordes de la región roja para cubrir exactamente lo que deseas eliminar." }, From c36349d950a7fcc95050accff61af89b9f1aadf7 Mon Sep 17 00:00:00 2001 From: "Nadir A." Date: Tue, 7 Apr 2026 03:05:21 +0300 Subject: [PATCH 13/21] feat(i18n): add Turkish (tr) locale support Add complete Turkish translation across all 7 i18n namespaces: - common: actions, playback controls, locale metadata - launch: HUD tooltips, audio/webcam controls, source selector - editor: error messages, export, project, recording permissions - dialogs: export progress, trim tutorial, unsaved changes, file dialogs - settings: all panels (zoom, speed, trim, layout, effects, background, crop, export, annotations, custom fonts, language, audio) - shortcuts: keyboard shortcuts panel and all actions - timeline: toolbar buttons, hints, labels, errors, success messages Also adds "tr" to SUPPORTED_LOCALES config and i18n validation script. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/i18n-check.mjs | 2 +- src/i18n/config.ts | 2 +- src/i18n/locales/tr/common.json | 29 +++++ src/i18n/locales/tr/dialogs.json | 68 ++++++++++++ src/i18n/locales/tr/editor.json | 35 ++++++ src/i18n/locales/tr/launch.json | 48 ++++++++ src/i18n/locales/tr/settings.json | 170 +++++++++++++++++++++++++++++ src/i18n/locales/tr/shortcuts.json | 36 ++++++ src/i18n/locales/tr/timeline.json | 50 +++++++++ 9 files changed, 438 insertions(+), 2 deletions(-) create mode 100644 src/i18n/locales/tr/common.json create mode 100644 src/i18n/locales/tr/dialogs.json create mode 100644 src/i18n/locales/tr/editor.json create mode 100644 src/i18n/locales/tr/launch.json create mode 100644 src/i18n/locales/tr/settings.json create mode 100644 src/i18n/locales/tr/shortcuts.json create mode 100644 src/i18n/locales/tr/timeline.json diff --git a/scripts/i18n-check.mjs b/scripts/i18n-check.mjs index 3fd0331..c320946 100644 --- a/scripts/i18n-check.mjs +++ b/scripts/i18n-check.mjs @@ -11,7 +11,7 @@ import path from "node:path"; const LOCALES_DIR = path.resolve("src/i18n/locales"); const BASE_LOCALE = "en"; -const COMPARE_LOCALES = ["zh-CN", "es"]; +const COMPARE_LOCALES = ["zh-CN", "es", "tr"]; function getKeys(obj, prefix = "") { const keys = []; diff --git a/src/i18n/config.ts b/src/i18n/config.ts index a96c7ef..063cc4f 100644 --- a/src/i18n/config.ts +++ b/src/i18n/config.ts @@ -1,5 +1,5 @@ export const DEFAULT_LOCALE = "en" as const; -export const SUPPORTED_LOCALES = ["en", "zh-CN", "es"] as const; +export const SUPPORTED_LOCALES = ["en", "zh-CN", "es", "tr"] as const; export const I18N_NAMESPACES = [ "common", "dialogs", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json new file mode 100644 index 0000000..3ec132c --- /dev/null +++ b/src/i18n/locales/tr/common.json @@ -0,0 +1,29 @@ +{ + "actions": { + "cancel": "İptal", + "save": "Kaydet", + "delete": "Sil", + "close": "Kapat", + "share": "Paylaş", + "done": "Tamam", + "open": "Aç", + "upload": "Yükle", + "export": "Dışa Aktar", + "file": "Dosya", + "edit": "Düzenle", + "view": "Görünüm", + "window": "Pencere", + "quit": "Çıkış", + "stopRecording": "Kaydı Durdur" + }, + "playback": { + "play": "Oynat", + "pause": "Duraklat", + "fullscreen": "Tam Ekran", + "exitFullscreen": "Tam Ekrandan Çık" + }, + "locale": { + "name": "Türkçe", + "short": "TR" + } +} diff --git a/src/i18n/locales/tr/dialogs.json b/src/i18n/locales/tr/dialogs.json new file mode 100644 index 0000000..5661e45 --- /dev/null +++ b/src/i18n/locales/tr/dialogs.json @@ -0,0 +1,68 @@ +{ + "export": { + "complete": "Dışa Aktarım Tamamlandı", + "yourFormatReady": "{{format}} dosyanız hazır", + "showInFolder": "Klasörde Göster", + "finalizingVideo": "Video dışa aktarımı sonlandırılıyor...", + "compilingGifProgress": "GIF derleniyor... %{{progress}}", + "compilingGifWait": "GIF derleniyor... Bu biraz zaman alabilir", + "takeMoment": "Bu biraz zaman alabilir...", + "failed": "Dışa Aktarım Başarısız", + "tryAgain": "Lütfen tekrar deneyin", + "finalizingVideoTitle": "Video Sonlandırılıyor", + "compilingGif": "GIF Derleniyor", + "exportingFormat": "{{format}} Dışa Aktarılıyor", + "compiling": "Derleniyor", + "renderingFrames": "Kareler İşleniyor", + "processing": "İşleniyor...", + "finalizing": "Sonlandırılıyor...", + "compilingStatus": "Derleniyor...", + "status": "Durum", + "format": "Biçim", + "frames": "Kareler", + "cancelExport": "Dışa Aktarımı İptal Et", + "savedSuccessfully": "{{format}} başarıyla kaydedildi!" + }, + "tutorial": { + "triggerLabel": "Kırpma nasıl çalışır", + "title": "Kırpma Nasıl Çalışır", + "description": "Videonuzun istenmeyen bölümlerini nasıl keseceğinizi anlayın.", + "explanation": "Kırpma aracı, kaldırmak istediğiniz bölümleri tanımlayarak çalışır.", + "explanationRemove": "kaldırmak", + "explanationCovered": "kaplanan", + "explanationEnd": "kırmızı kırpma bölgesi ile işaretlenen kısımlar dışa aktarımda kesilecektir.", + "visualExample": "Görsel Örnek", + "removed": "KALDIRILDI", + "kept": "Korundu", + "part1": "Bölüm 1", + "part2": "Bölüm 2", + "part3": "Bölüm 3", + "finalVideo": "Son Video", + "step1Title": "1. Kırpma Ekle", + "step1Description": "Kaldırılacak bölümü işaretlemek için T tuşuna basın veya makas simgesine tıklayın.", + "step2Title": "2. Ayarla", + "step2Description": "Kesmek istediğiniz kısmı tam olarak kaplamak için kırmızı bölgenin kenarlarını sürükleyin." + }, + "unsavedChanges": { + "title": "Kaydedilmemiş Değişiklikler", + "message": "Kaydedilmemiş değişiklikleriniz var.", + "detail": "Kapatmadan önce projenizi kaydetmek ister misiniz?", + "saveAndClose": "Kaydet ve Kapat", + "discardAndClose": "Kaydetmeden Kapat", + "loadProject": "Proje Yükle…", + "saveProject": "Proje Kaydet…", + "saveProjectAs": "Farklı Kaydet…" + }, + "fileDialogs": { + "saveGif": "Dışa Aktarılan GIF'i Kaydet", + "saveVideo": "Dışa Aktarılan Videoyu Kaydet", + "selectVideo": "Video Dosyası Seç", + "saveProject": "OpenScreen Projesini Kaydet", + "openProject": "OpenScreen Projesini Aç", + "gifImage": "GIF Görüntüsü", + "mp4Video": "MP4 Video", + "videoFiles": "Video Dosyaları", + "openscreenProject": "OpenScreen Projesi", + "allFiles": "Tüm Dosyalar" + } +} diff --git a/src/i18n/locales/tr/editor.json b/src/i18n/locales/tr/editor.json new file mode 100644 index 0000000..dfa4cb1 --- /dev/null +++ b/src/i18n/locales/tr/editor.json @@ -0,0 +1,35 @@ +{ + "errors": { + "noVideoLoaded": "Video yüklenmedi", + "videoNotReady": "Video hazır değil", + "unableToDetermineSourcePath": "Kaynak video yolu belirlenemiyor", + "failedToSaveGif": "GIF kaydedilemedi", + "gifExportFailed": "GIF dışa aktarımı başarısız oldu", + "failedToSaveVideo": "Video kaydedilemedi", + "exportFailed": "Dışa aktarım başarısız oldu", + "exportFailedWithError": "Dışa aktarım başarısız: {{error}}", + "failedToSaveExport": "Dışa aktarım kaydedilemedi", + "failedToSaveExportedVideo": "Dışa aktarılan video kaydedilemedi", + "failedToRevealInFolder": "Klasörde gösterme hatası: {{error}}" + }, + "export": { + "canceled": "Dışa aktarım iptal edildi", + "exportedSuccessfully": "{{format}} başarıyla dışa aktarıldı" + }, + "project": { + "saveCanceled": "Proje kaydetme iptal edildi", + "failedToSave": "Proje kaydedilemedi", + "savedTo": "Proje şuraya kaydedildi: {{path}}", + "failedToLoad": "Proje yüklenemedi", + "invalidFormat": "Geçersiz proje dosyası biçimi", + "loadedFrom": "Proje şuradan yüklendi: {{path}}" + }, + "recording": { + "failedCameraAccess": "Kamera erişimi istenemedi.", + "cameraBlocked": "Kamera erişimi engellendi. Kamerayı kullanmak için sistem ayarlarından izin verin.", + "systemAudioUnavailable": "Sistem sesi kullanılamıyor. Sistem sesi olmadan kaydediliyor.", + "microphoneDenied": "Mikrofon erişimi reddedildi. Kayıt ses olmadan devam edecek.", + "cameraDenied": "Kamera erişimi reddedildi. Kayıt kamera olmadan devam edecek.", + "permissionDenied": "Kayıt izni reddedildi. Lütfen ekran kaydına izin verin." + } +} diff --git a/src/i18n/locales/tr/launch.json b/src/i18n/locales/tr/launch.json new file mode 100644 index 0000000..f48d99d --- /dev/null +++ b/src/i18n/locales/tr/launch.json @@ -0,0 +1,48 @@ +{ + "tooltips": { + "hideHUD": "Kontrol panelini gizle", + "closeApp": "Uygulamayı kapat", + "restartRecording": "Kaydı yeniden başlat", + "cancelRecording": "Kaydı iptal et", + "pauseRecording": "Kaydı duraklat", + "resumeRecording": "Kayda devam et", + "openVideoFile": "Video dosyası aç", + "openProject": "Proje aç" + }, + "audio": { + "enableSystemAudio": "Sistem sesini etkinleştir", + "disableSystemAudio": "Sistem sesini devre dışı bırak", + "enableMicrophone": "Mikrofonu etkinleştir", + "disableMicrophone": "Mikrofonu devre dışı bırak", + "defaultMicrophone": "Varsayılan Mikrofon", + "enableNoiseReduction": "Gürültü azaltmayı etkinleştir (yapay zeka destekli)", + "disableNoiseReduction": "Gürültü azaltmayı devre dışı bırak", + "noiseReduction": "Gürültü azaltma", + "clickToCycle": "Seviye değiştirmek için tıklayın", + "nrLevel": { + "light": "Hafif", + "moderate": "Orta", + "aggressive": "Güçlü" + }, + "noiseReductionPrompt": "Daha net ses için yapay zeka destekli gürültü azaltmayı etkinleştirmek ister misiniz?", + "enableNoiseReductionShort": "Etkinleştir" + }, + "webcam": { + "enableWebcam": "Kamerayı etkinleştir", + "disableWebcam": "Kamerayı devre dışı bırak", + "defaultCamera": "Varsayılan Kamera", + "searching": "Aranıyor...", + "noneFound": "Kamera bulunamadı", + "unavailable": "Kamera kullanılamıyor" + }, + "sourceSelector": { + "loading": "Kaynaklar yükleniyor...", + "screens": "Ekranlar ({{count}})", + "windows": "Pencereler ({{count}})", + "defaultSourceName": "Ekran" + }, + "recording": { + "selectSource": "Lütfen kayıt için bir kaynak seçin" + }, + "language": "Dil" +} diff --git a/src/i18n/locales/tr/settings.json b/src/i18n/locales/tr/settings.json new file mode 100644 index 0000000..1fa4668 --- /dev/null +++ b/src/i18n/locales/tr/settings.json @@ -0,0 +1,170 @@ +{ + "zoom": { + "level": "Yakınlaştırma Seviyesi", + "selectRegion": "Ayarlamak için bir yakınlaştırma bölgesi seçin", + "deleteZoom": "Yakınlaştırmayı Sil", + "focusMode": { + "title": "Odak Modu", + "manual": "Manuel", + "auto": "Otomatik", + "autoDescription": "Kamera kaydedilen imleç konumunu takip eder" + } + }, + "speed": { + "playbackSpeed": "Oynatma Hızı", + "selectRegion": "Ayarlamak için bir hız bölgesi seçin", + "deleteRegion": "Hız Bölgesini Sil" + }, + "trim": { + "deleteRegion": "Kırpma Bölgesini Sil" + }, + "layout": { + "title": "Düzen", + "preset": "Ön Ayar", + "selectPreset": "Ön ayar seçin", + "pictureInPicture": "Resim İçinde Resim", + "verticalStack": "Dikey Yığın", + "webcamShape": "Kamera Şekli" + }, + "effects": { + "title": "Video Efektleri", + "blurBg": "Arka Planı Bulanıklaştır", + "motionBlur": "Hareket Bulanıklığı", + "off": "kapalı", + "shadow": "Gölge", + "roundness": "Yuvarlaklık", + "padding": "Dolgu" + }, + "background": { + "title": "Arka Plan", + "image": "Görüntü", + "color": "Renk", + "gradient": "Gradyan", + "uploadCustom": "Özel Yükle", + "gradientLabel": "Gradyan {{index}}" + }, + "crop": { + "title": "Kırpma", + "cropVideo": "Videoyu Kırp", + "dragInstruction": "Kırpma alanını ayarlamak için her kenarı sürükleyin", + "ratio": "Oran", + "free": "Serbest", + "done": "Tamam", + "lockAspectRatio": "En boy oranını kilitle", + "unlockAspectRatio": "En boy oranının kilidini aç" + }, + "exportFormat": { + "mp4": "MP4", + "gif": "GIF", + "mp4Video": "MP4 Video", + "mp4Description": "Yüksek kaliteli video dosyası", + "gifAnimation": "GIF Animasyon", + "gifDescription": "Paylaşım için hareketli görüntü" + }, + "exportQuality": { + "title": "Dışa Aktarım Kalitesi", + "low": "Düşük", + "medium": "Orta", + "high": "Yüksek" + }, + "gifSettings": { + "frameRate": "GIF Kare Hızı", + "size": "GIF Boyutu", + "loop": "GIF Döngüsü" + }, + "project": { + "save": "Projeyi Kaydet", + "load": "Proje Yükle" + }, + "export": { + "videoButton": "Videoyu Dışa Aktar", + "gifButton": "GIF Olarak Dışa Aktar", + "chooseSaveLocation": "Kayıt Konumu Seç" + }, + "links": { + "reportBug": "Hata Bildir", + "starOnGithub": "GitHub'da Yıldızla" + }, + "imageUpload": { + "invalidFileType": "Geçersiz dosya türü", + "jpgOnly": "Lütfen bir JPG veya JPEG görüntü dosyası yükleyin.", + "uploadSuccess": "Özel görüntü başarıyla yüklendi!", + "failedToUpload": "Görüntü yüklenemedi", + "errorReading": "Dosya okunurken bir hata oluştu." + }, + "annotation": { + "title": "Açıklama Ayarları", + "active": "Aktif", + "typeText": "Metin", + "typeImage": "Görüntü", + "typeArrow": "Ok", + "textContent": "Metin İçeriği", + "textPlaceholder": "Metninizi girin...", + "fontStyle": "Yazı Tipi Stili", + "selectStyle": "Stil seçin", + "size": "Boyut", + "customFonts": "Özel Yazı Tipleri", + "textColor": "Metin Rengi", + "background": "Arka Plan", + "none": "Yok", + "color": "Renk", + "clearBackground": "Arka Planı Temizle", + "uploadImage": "Görüntü Yükle", + "supportedFormats": "Desteklenen biçimler: JPG, PNG, GIF, WebP", + "arrowDirection": "Ok Yönü", + "strokeWidth": "Çizgi Kalınlığı: {{width}}px", + "arrowColor": "Ok Rengi", + "deleteAnnotation": "Açıklamayı Sil", + "shortcutsAndTips": "Kısayollar ve İpuçları", + "tipMovePlayhead": "Oynatma imlecini çakışan açıklama bölümüne taşıyın ve bir öğe seçin.", + "tipTabCycle": "Çakışan öğeler arasında geçiş yapmak için Tab tuşunu kullanın.", + "tipShiftTabCycle": "Geriye doğru geçiş yapmak için Shift+Tab kullanın.", + "invalidImageType": "Geçersiz dosya türü", + "imageFormatsOnly": "Lütfen bir JPG, PNG, GIF veya WebP görüntü dosyası yükleyin.", + "imageUploadSuccess": "Görüntü başarıyla yüklendi!", + "failedImageUpload": "Görüntü yüklenemedi" + }, + "fontStyles": { + "classic": "Klasik", + "editor": "Editör", + "strong": "Kalın", + "typewriter": "Daktilo", + "deco": "Dekoratif", + "simple": "Sade", + "modern": "Modern", + "clean": "Temiz" + }, + "customFont": { + "dialogTitle": "Google Yazı Tipi Ekle", + "urlLabel": "Google Fonts İçe Aktarım URL'si", + "urlPlaceholder": "https://fonts.googleapis.com/css2?family=Roboto&display=swap", + "urlHelp": "Google Fonts'tan alabilirsiniz: Bir yazı tipi seçin → \"Get font\"a tıklayın → @import URL'sini kopyalayın", + "nameLabel": "Görünen Ad", + "namePlaceholder": "Özel Yazı Tipim", + "nameHelp": "Yazı tipinin seçicide nasıl görüneceğini belirler", + "addButton": "Yazı Tipi Ekle", + "addingButton": "Ekleniyor...", + "errorEmptyUrl": "Lütfen bir Google Fonts içe aktarım URL'si girin", + "errorInvalidUrl": "Lütfen geçerli bir Google Fonts URL'si girin", + "errorEmptyName": "Lütfen bir yazı tipi adı girin", + "errorExtractFailed": "URL'den yazı tipi ailesi çıkarılamadı", + "successMessage": "\"{{fontName}}\" yazı tipi başarıyla eklendi", + "failedToAdd": "Yazı tipi eklenemedi", + "errorTimeout": "Yazı tipinin yüklenmesi çok uzun sürdü. Lütfen URL'yi kontrol edip tekrar deneyin.", + "errorLoadFailed": "Yazı tipi yüklenemedi. Lütfen Google Fonts URL'sinin doğruluğunu kontrol edin." + }, + "language": { + "title": "Dil" + }, + "audio": { + "title": "Ses", + "noiseReduction": "Gürültü Azaltma", + "level": "Seviye", + "nrLevel": { + "light": "Hafif", + "moderate": "Orta", + "aggressive": "Güçlü" + }, + "nrDescription": "Yapay zeka destekli gürültü azaltma arka plan gürültüsünü temizler. Daha yüksek seviyeler daha agresiftir ancak ses kalitesini etkileyebilir." + } +} diff --git a/src/i18n/locales/tr/shortcuts.json b/src/i18n/locales/tr/shortcuts.json new file mode 100644 index 0000000..8eb7931 --- /dev/null +++ b/src/i18n/locales/tr/shortcuts.json @@ -0,0 +1,36 @@ +{ + "title": "Klavye Kısayolları", + "customize": "Özelleştir", + "configurable": "Yapılandırılabilir", + "fixed": "Sabit", + "pressKey": "Bir tuşa basın…", + "clickToChange": "Değiştirmek için tıklayın", + "pressEscToCancel": "İptal etmek için Esc tuşuna basın", + "helpText": "Bir kısayola tıklayın, ardından yeni tuş kombinasyonuna basın. İptal etmek için Esc tuşuna basın.", + "resetToDefaults": "Varsayılanlara sıfırla", + "alreadyUsedBy": "\"{{action}}\" tarafından zaten kullanılıyor", + "swap": "Değiştir", + "reservedShortcut": "Bu kısayol \"{{label}}\" için ayrılmıştır ve yeniden atanamaz.", + "savedToast": "Klavye kısayolları kaydedildi", + "resetToast": "Varsayılan kısayollara sıfırlandı — uygulamak için Kaydet'e tıklayın", + "actions": { + "addZoom": "Yakınlaştırma Ekle", + "addTrim": "Kırpma Ekle", + "addSpeed": "Hız Ekle", + "addAnnotation": "Açıklama Ekle", + "addKeyframe": "Anahtar Kare Ekle", + "deleteSelected": "Seçileni Sil", + "playPause": "Oynat / Duraklat" + }, + "fixedActions": { + "undo": "Geri Al", + "redo": "Yinele", + "cycleAnnotationsForward": "Açıklamalar Arasında İleri Geç", + "cycleAnnotationsBackward": "Açıklamalar Arasında Geri Geç", + "deleteSelectedAlt": "Seçileni Sil (alternatif)", + "panTimeline": "Zaman Çizelgesini Kaydır", + "zoomTimeline": "Zaman Çizelgesini Yakınlaştır", + "frameBack": "Önceki Kare", + "frameForward": "Sonraki Kare" + } +} diff --git a/src/i18n/locales/tr/timeline.json b/src/i18n/locales/tr/timeline.json new file mode 100644 index 0000000..b39a5d1 --- /dev/null +++ b/src/i18n/locales/tr/timeline.json @@ -0,0 +1,50 @@ +{ + "buttons": { + "addZoom": "Yakınlaştırma Ekle (Z)", + "suggestZooms": "İmleçten Yakınlaştırma Öner", + "addTrim": "Kırpma Ekle (T)", + "addAnnotation": "Açıklama Ekle (A)", + "addSpeed": "Hız Ekle (S)" + }, + "hints": { + "pressZoom": "Yakınlaştırma eklemek için Z tuşuna basın", + "pressTrim": "Kırpma eklemek için T tuşuna basın", + "pressAnnotation": "Açıklama eklemek için A tuşuna basın", + "pressSpeed": "Hız eklemek için S tuşuna basın" + }, + "labels": { + "pan": "Kaydır", + "zoom": "Yakınlaştır", + "zoomItem": "Yakınlaştırma {{index}}", + "trimItem": "Kırpma {{index}}", + "speedItem": "Hız {{index}}", + "annotationItem": "Açıklama", + "imageItem": "Görüntü", + "emptyText": "Boş metin" + }, + "emptyState": { + "noVideo": "Video Yüklenmedi", + "dragAndDrop": "Düzenlemeye başlamak için bir video sürükleyip bırakın" + }, + "errors": { + "cannotPlaceZoom": "Buraya yakınlaştırma yerleştirilemiyor", + "zoomExistsAtLocation": "Bu konumda zaten bir yakınlaştırma var veya yeterli alan yok.", + "zoomSuggestionUnavailable": "Yakınlaştırma öneri işleyicisi kullanılamıyor", + "noCursorTelemetry": "İmleç telemetrisi mevcut değil", + "noCursorTelemetryDescription": "İmleç tabanlı öneriler oluşturmak için önce bir ekran kaydı yapın.", + "noUsableTelemetry": "Kullanılabilir imleç telemetrisi yok", + "noUsableTelemetryDescription": "Kayıt yeterli imleç hareketi verisi içermiyor.", + "noDwellMoments": "Belirgin imleç bekleme anları bulunamadı", + "noDwellMomentsDescription": "Önemli işlemlerde daha yavaş imleç duraklamaları olan bir kayıt deneyin.", + "noAutoZoomSlots": "Otomatik yakınlaştırma alanı yok", + "noAutoZoomSlotsDescription": "Algılanan bekleme noktaları mevcut yakınlaştırma bölgeleriyle çakışıyor.", + "cannotPlaceTrim": "Buraya kırpma yerleştirilemiyor", + "trimExistsAtLocation": "Bu konumda zaten bir kırpma var veya yeterli alan yok.", + "cannotPlaceSpeed": "Buraya hız yerleştirilemiyor", + "speedExistsAtLocation": "Bu konumda zaten bir hız bölgesi var veya yeterli alan yok." + }, + "success": { + "addedZoomSuggestions": "{{count}} imleç tabanlı yakınlaştırma önerisi eklendi", + "addedZoomSuggestionsPlural": "{{count}} imleç tabanlı yakınlaştırma önerisi eklendi" + } +} From e739653b3fc97c4361561e71c563009a74c5f12d Mon Sep 17 00:00:00 2001 From: FabLrc Date: Tue, 7 Apr 2026 12:05:36 +0200 Subject: [PATCH 14/21] feat(i18n): add French translations for various application components --- src/i18n/config.ts | 2 +- src/i18n/locales/fr/common.json | 29 ++++++ src/i18n/locales/fr/dialogs.json | 68 ++++++++++++ src/i18n/locales/fr/editor.json | 35 +++++++ src/i18n/locales/fr/launch.json | 37 +++++++ src/i18n/locales/fr/settings.json | 159 +++++++++++++++++++++++++++++ src/i18n/locales/fr/shortcuts.json | 36 +++++++ src/i18n/locales/fr/timeline.json | 50 +++++++++ 8 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 src/i18n/locales/fr/common.json create mode 100644 src/i18n/locales/fr/dialogs.json create mode 100644 src/i18n/locales/fr/editor.json create mode 100644 src/i18n/locales/fr/launch.json create mode 100644 src/i18n/locales/fr/settings.json create mode 100644 src/i18n/locales/fr/shortcuts.json create mode 100644 src/i18n/locales/fr/timeline.json diff --git a/src/i18n/config.ts b/src/i18n/config.ts index a96c7ef..38680f9 100644 --- a/src/i18n/config.ts +++ b/src/i18n/config.ts @@ -1,5 +1,5 @@ export const DEFAULT_LOCALE = "en" as const; -export const SUPPORTED_LOCALES = ["en", "zh-CN", "es"] as const; +export const SUPPORTED_LOCALES = ["en", "zh-CN", "es", "fr"] as const; export const I18N_NAMESPACES = [ "common", "dialogs", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json new file mode 100644 index 0000000..7eb7f83 --- /dev/null +++ b/src/i18n/locales/fr/common.json @@ -0,0 +1,29 @@ +{ + "actions": { + "cancel": "Annuler", + "save": "Enregistrer", + "delete": "Supprimer", + "close": "Fermer", + "share": "Partager", + "done": "Terminer", + "open": "Ouvrir", + "upload": "Téléverser", + "export": "Exporter", + "file": "Fichier", + "edit": "Éditer", + "view": "Affichage", + "window": "Fenêtre", + "quit": "Quitter", + "stopRecording": "Arrêter l'enregistrement" + }, + "playback": { + "play": "Lecture", + "pause": "Pause", + "fullscreen": "Plein écran", + "exitFullscreen": "Quitter le plein écran" + }, + "locale": { + "name": "Français", + "short": "FR" + } +} diff --git a/src/i18n/locales/fr/dialogs.json b/src/i18n/locales/fr/dialogs.json new file mode 100644 index 0000000..b4056a5 --- /dev/null +++ b/src/i18n/locales/fr/dialogs.json @@ -0,0 +1,68 @@ +{ + "export": { + "complete": "Export terminé", + "yourFormatReady": "Votre {{format}} est prêt", + "showInFolder": "Afficher dans le dossier", + "finalizingVideo": "Finalisation de l'export vidéo...", + "compilingGifProgress": "Compilation du GIF... {{progress}}%", + "compilingGifWait": "Compilation du GIF... Cela peut prendre un moment", + "takeMoment": "Cela peut prendre un moment...", + "failed": "Export échoué", + "tryAgain": "Veuillez réessayer", + "finalizingVideoTitle": "Finalisation de la vidéo", + "compilingGif": "Compilation du GIF", + "exportingFormat": "Export de {{format}}", + "compiling": "Compilation en cours", + "renderingFrames": "Rendu des images", + "processing": "Traitement en cours...", + "finalizing": "Finalisation...", + "compilingStatus": "Compilation...", + "status": "Statut", + "format": "Format", + "frames": "Images", + "cancelExport": "Annuler l'export", + "savedSuccessfully": "{{format}} enregistré avec succès !" + }, + "tutorial": { + "triggerLabel": "Comment fonctionne la coupe", + "title": "Comment fonctionne la coupe", + "description": "Comprendre comment supprimer les parties indésirables de votre vidéo.", + "explanation": "L'outil Coupe fonctionne en définissant les segments que vous souhaitez", + "explanationRemove": "supprimer", + "explanationCovered": "couvert", + "explanationEnd": "par un segment de coupe rouge sera coupé lors de l'export.", + "visualExample": "Exemple visuel", + "removed": "SUPPRIMÉ", + "kept": "Conservé", + "part1": "Partie 1", + "part2": "Partie 2", + "part3": "Partie 3", + "finalVideo": "Vidéo finale", + "step1Title": "1. Ajouter une coupe", + "step1Description": "Appuyez sur T ou cliquez sur l'icône ciseaux pour marquer une section à supprimer.", + "step2Title": "2. Ajuster", + "step2Description": "Faites glisser les bords de la région rouge pour couvrir exactement ce que vous souhaitez couper." + }, + "unsavedChanges": { + "title": "Modifications non enregistrées", + "message": "Vous avez des modifications non enregistrées.", + "detail": "Voulez-vous enregistrer votre projet avant de fermer ?", + "saveAndClose": "Enregistrer et fermer", + "discardAndClose": "Ignorer et fermer", + "loadProject": "Charger un projet…", + "saveProject": "Enregistrer le projet…", + "saveProjectAs": "Enregistrer le projet sous…" + }, + "fileDialogs": { + "saveGif": "Enregistrer le GIF exporté", + "saveVideo": "Enregistrer la vidéo exportée", + "selectVideo": "Sélectionner un fichier vidéo", + "saveProject": "Enregistrer le projet OpenScreen", + "openProject": "Ouvrir un projet OpenScreen", + "gifImage": "Image GIF", + "mp4Video": "Vidéo MP4", + "videoFiles": "Fichiers vidéo", + "openscreenProject": "Projet OpenScreen", + "allFiles": "Tous les fichiers" + } +} diff --git a/src/i18n/locales/fr/editor.json b/src/i18n/locales/fr/editor.json new file mode 100644 index 0000000..779bcd7 --- /dev/null +++ b/src/i18n/locales/fr/editor.json @@ -0,0 +1,35 @@ +{ + "errors": { + "noVideoLoaded": "Aucune vidéo chargée", + "videoNotReady": "Vidéo non prête", + "unableToDetermineSourcePath": "Impossible de déterminer le chemin de la vidéo source", + "failedToSaveGif": "Échec de l'enregistrement du GIF", + "gifExportFailed": "L'export du GIF a échoué", + "failedToSaveVideo": "Échec de l'enregistrement de la vidéo", + "exportFailed": "L'export a échoué", + "exportFailedWithError": "L'export a échoué : {{error}}", + "failedToSaveExport": "Échec de l'enregistrement de l'export", + "failedToSaveExportedVideo": "Échec de l'enregistrement de la vidéo exportée", + "failedToRevealInFolder": "Erreur lors de l'affichage dans le dossier : {{error}}" + }, + "export": { + "canceled": "Export annulé", + "exportedSuccessfully": "{{format}} exporté avec succès" + }, + "project": { + "saveCanceled": "Enregistrement du projet annulé", + "failedToSave": "Échec de l'enregistrement du projet", + "savedTo": "Projet enregistré dans {{path}}", + "failedToLoad": "Échec du chargement du projet", + "invalidFormat": "Format de fichier projet invalide", + "loadedFrom": "Projet chargé depuis {{path}}" + }, + "recording": { + "failedCameraAccess": "Échec de la demande d'accès à la caméra.", + "cameraBlocked": "L'accès à la caméra est bloqué. Activez-le dans les paramètres système pour utiliser la webcam.", + "systemAudioUnavailable": "Audio système non disponible. Enregistrement sans audio système.", + "microphoneDenied": "Accès au microphone refusé. L'enregistrement continuera sans audio.", + "cameraDenied": "Accès à la caméra refusé. L'enregistrement continuera sans webcam.", + "permissionDenied": "Permission d'enregistrement refusée. Veuillez autoriser l'enregistrement d'écran." + } +} diff --git a/src/i18n/locales/fr/launch.json b/src/i18n/locales/fr/launch.json new file mode 100644 index 0000000..f4bfb27 --- /dev/null +++ b/src/i18n/locales/fr/launch.json @@ -0,0 +1,37 @@ +{ + "tooltips": { + "hideHUD": "Masquer le HUD", + "closeApp": "Fermer l'application", + "restartRecording": "Redémarrer l'enregistrement", + "cancelRecording": "Annuler l'enregistrement", + "pauseRecording": "Mettre en pause l'enregistrement", + "resumeRecording": "Reprendre l'enregistrement", + "openVideoFile": "Ouvrir un fichier vidéo", + "openProject": "Ouvrir un projet" + }, + "audio": { + "enableSystemAudio": "Activer l'audio système", + "disableSystemAudio": "Désactiver l'audio système", + "enableMicrophone": "Activer le microphone", + "disableMicrophone": "Désactiver le microphone", + "defaultMicrophone": "Microphone par défaut" + }, + "webcam": { + "enableWebcam": "Activer la webcam", + "disableWebcam": "Désactiver la webcam", + "defaultCamera": "Caméra par défaut", + "searching": "Recherche en cours...", + "noneFound": "Aucune caméra trouvée", + "unavailable": "Caméra non disponible" + }, + "sourceSelector": { + "loading": "Chargement des sources...", + "screens": "Écrans ({{count}})", + "windows": "Fenêtres ({{count}})", + "defaultSourceName": "Écran" + }, + "recording": { + "selectSource": "Veuillez sélectionner une source à enregistrer" + }, + "language": "Langue" +} diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json new file mode 100644 index 0000000..dd7610f --- /dev/null +++ b/src/i18n/locales/fr/settings.json @@ -0,0 +1,159 @@ +{ + "zoom": { + "level": "Niveau de zoom", + "selectRegion": "Sélectionnez une région de zoom à ajuster", + "deleteZoom": "Supprimer le zoom", + "focusMode": { + "title": "Mode focus", + "manual": "Manuel", + "auto": "Auto", + "autoDescription": "La caméra suit la position du curseur enregistré" + } + }, + "speed": { + "playbackSpeed": "Vitesse de lecture", + "selectRegion": "Sélectionnez une région de vitesse à ajuster", + "deleteRegion": "Supprimer la région de vitesse" + }, + "trim": { + "deleteRegion": "Supprimer la région de coupe" + }, + "layout": { + "title": "Mise en page", + "preset": "Préréglage", + "selectPreset": "Choisir un préréglage", + "pictureInPicture": "Incrustation d'image", + "verticalStack": "Empilement vertical", + "webcamShape": "Forme de la caméra" + }, + "effects": { + "title": "Effets vidéo", + "blurBg": "Flou arrière-plan", + "motionBlur": "Flou de mouvement", + "off": "désactivé", + "shadow": "Ombre", + "roundness": "Arrondi", + "padding": "Marge" + }, + "background": { + "title": "Arrière-plan", + "image": "Image", + "color": "Couleur", + "gradient": "Dégradé", + "uploadCustom": "Téléverser une image", + "gradientLabel": "Dégradé {{index}}" + }, + "crop": { + "title": "Recadrage", + "cropVideo": "Recadrer la vidéo", + "dragInstruction": "Faites glisser chaque côté pour ajuster la zone de recadrage", + "ratio": "Ratio", + "free": "Libre", + "done": "Terminer", + "lockAspectRatio": "Verrouiller le ratio", + "unlockAspectRatio": "Déverrouiller le ratio" + }, + "exportFormat": { + "mp4": "MP4", + "gif": "GIF", + "mp4Video": "Vidéo MP4", + "mp4Description": "Fichier vidéo haute qualité", + "gifAnimation": "Animation GIF", + "gifDescription": "Image animée pour le partage" + }, + "exportQuality": { + "title": "Qualité d'export", + "low": "Faible", + "medium": "Moyenne", + "high": "Haute" + }, + "gifSettings": { + "frameRate": "Fréquence d'images GIF", + "size": "Taille du GIF", + "loop": "GIF en boucle" + }, + "project": { + "save": "Enregistrer le projet", + "load": "Charger un projet" + }, + "export": { + "videoButton": "Exporter la vidéo", + "gifButton": "Exporter le GIF", + "chooseSaveLocation": "Choisir l'emplacement d'enregistrement" + }, + "links": { + "reportBug": "Signaler un bug", + "starOnGithub": "Étoile sur GitHub" + }, + "imageUpload": { + "invalidFileType": "Type de fichier invalide", + "jpgOnly": "Veuillez téléverser un fichier image JPG ou JPEG.", + "uploadSuccess": "Image personnalisée téléversée avec succès !", + "failedToUpload": "Échec du téléversement de l'image", + "errorReading": "Une erreur s'est produite lors de la lecture du fichier." + }, + "annotation": { + "title": "Paramètres d'annotation", + "active": "Actif", + "typeText": "Texte", + "typeImage": "Image", + "typeArrow": "Flèche", + "textContent": "Contenu du texte", + "textPlaceholder": "Saisissez votre texte...", + "fontStyle": "Style de police", + "selectStyle": "Choisir un style", + "size": "Taille", + "customFonts": "Polices personnalisées", + "textColor": "Couleur du texte", + "background": "Arrière-plan", + "none": "Aucun", + "color": "Couleur", + "clearBackground": "Supprimer l'arrière-plan", + "uploadImage": "Téléverser une image", + "supportedFormats": "Formats supportés : JPG, PNG, GIF, WebP", + "arrowDirection": "Direction de la flèche", + "strokeWidth": "Épaisseur du trait : {{width}}px", + "arrowColor": "Couleur de la flèche", + "deleteAnnotation": "Supprimer l'annotation", + "shortcutsAndTips": "Raccourcis & Astuces", + "tipMovePlayhead": "Déplacez la tête de lecture sur la section d'annotation et sélectionnez un élément.", + "tipTabCycle": "Utilisez Tab pour cycler entre les éléments superposés.", + "tipShiftTabCycle": "Utilisez Shift+Tab pour cycler en sens inverse.", + "invalidImageType": "Type de fichier invalide", + "imageFormatsOnly": "Veuillez téléverser un fichier image JPG, PNG, GIF ou WebP.", + "imageUploadSuccess": "Image téléversée avec succès !", + "failedImageUpload": "Échec du téléversement de l'image" + }, + "fontStyles": { + "classic": "Classique", + "editor": "Éditeur", + "strong": "Gras", + "typewriter": "Machine à écrire", + "deco": "Déco", + "simple": "Simple", + "modern": "Moderne", + "clean": "Épuré" + }, + "customFont": { + "dialogTitle": "Ajouter une police Google", + "urlLabel": "URL d'import Google Fonts", + "urlPlaceholder": "https://fonts.googleapis.com/css2?family=Roboto&display=swap", + "urlHelp": "Obtenez-la depuis Google Fonts : Sélectionnez une police → Cliquez sur « Obtenir la police » → Copiez l'URL @import", + "nameLabel": "Nom d'affichage", + "namePlaceholder": "Ma police personnalisée", + "nameHelp": "C'est ainsi que la police apparaîtra dans le sélecteur de polices", + "addButton": "Ajouter la police", + "addingButton": "Ajout en cours...", + "errorEmptyUrl": "Veuillez saisir une URL d'import Google Fonts", + "errorInvalidUrl": "Veuillez saisir une URL Google Fonts valide", + "errorEmptyName": "Veuillez saisir un nom de police", + "errorExtractFailed": "Impossible d'extraire la famille de polices depuis l'URL", + "successMessage": "Police « {{fontName}} » ajoutée avec succès", + "failedToAdd": "Échec de l'ajout de la police", + "errorTimeout": "La police a mis trop de temps à charger. Vérifiez l'URL et réessayez.", + "errorLoadFailed": "La police n'a pas pu être chargée. Vérifiez que l'URL Google Fonts est correcte." + }, + "language": { + "title": "Langue" + } +} diff --git a/src/i18n/locales/fr/shortcuts.json b/src/i18n/locales/fr/shortcuts.json new file mode 100644 index 0000000..ebd2181 --- /dev/null +++ b/src/i18n/locales/fr/shortcuts.json @@ -0,0 +1,36 @@ +{ + "title": "Raccourcis clavier", + "customize": "Personnaliser", + "configurable": "Configurable", + "fixed": "Fixe", + "pressKey": "Appuyez sur une touche…", + "clickToChange": "Cliquez pour modifier", + "pressEscToCancel": "Appuyez sur Échap pour annuler", + "helpText": "Cliquez sur un raccourci puis appuyez sur la nouvelle combinaison de touches. Appuyez sur Échap pour annuler.", + "resetToDefaults": "Réinitialiser les valeurs par défaut", + "alreadyUsedBy": "Déjà utilisé par {{action}}", + "swap": "Échanger", + "reservedShortcut": "Ce raccourci est réservé pour « {{label}} » et ne peut pas être réassigné.", + "savedToast": "Raccourcis clavier enregistrés", + "resetToast": "Réinitialisé aux raccourcis par défaut — cliquez sur Enregistrer pour appliquer", + "actions": { + "addZoom": "Ajouter un zoom", + "addTrim": "Ajouter une coupe", + "addSpeed": "Ajouter une vitesse", + "addAnnotation": "Ajouter une annotation", + "addKeyframe": "Ajouter une image-clé", + "deleteSelected": "Supprimer la sélection", + "playPause": "Lecture / Pause" + }, + "fixedActions": { + "undo": "Annuler", + "redo": "Rétablir", + "cycleAnnotationsForward": "Cycler les annotations en avant", + "cycleAnnotationsBackward": "Cycler les annotations en arrière", + "deleteSelectedAlt": "Supprimer la sélection (alt)", + "panTimeline": "Panoramique de la timeline", + "zoomTimeline": "Zoom de la timeline", + "frameBack": "Image précédente", + "frameForward": "Image suivante" + } +} diff --git a/src/i18n/locales/fr/timeline.json b/src/i18n/locales/fr/timeline.json new file mode 100644 index 0000000..abee16c --- /dev/null +++ b/src/i18n/locales/fr/timeline.json @@ -0,0 +1,50 @@ +{ + "buttons": { + "addZoom": "Ajouter un zoom (Z)", + "suggestZooms": "Suggérer des zooms depuis le curseur", + "addTrim": "Ajouter une coupe (T)", + "addAnnotation": "Ajouter une annotation (A)", + "addSpeed": "Ajouter une vitesse (S)" + }, + "hints": { + "pressZoom": "Appuyez sur Z pour ajouter un zoom", + "pressTrim": "Appuyez sur T pour ajouter une coupe", + "pressAnnotation": "Appuyez sur A pour ajouter une annotation", + "pressSpeed": "Appuyez sur S pour ajouter une vitesse" + }, + "labels": { + "pan": "Panoramique", + "zoom": "Zoom", + "zoomItem": "Zoom {{index}}", + "trimItem": "Coupe {{index}}", + "speedItem": "Vitesse {{index}}", + "annotationItem": "Annotation", + "imageItem": "Image", + "emptyText": "Texte vide" + }, + "emptyState": { + "noVideo": "Aucune vidéo chargée", + "dragAndDrop": "Glissez-déposez une vidéo pour commencer à éditer" + }, + "errors": { + "cannotPlaceZoom": "Impossible de placer le zoom ici", + "zoomExistsAtLocation": "Un zoom existe déjà à cet emplacement ou l'espace disponible est insuffisant.", + "zoomSuggestionUnavailable": "Gestionnaire de suggestions de zoom non disponible", + "noCursorTelemetry": "Aucune télémétrie de curseur disponible", + "noCursorTelemetryDescription": "Enregistrez d'abord un screencast pour générer des suggestions basées sur le curseur.", + "noUsableTelemetry": "Aucune télémétrie de curseur utilisable", + "noUsableTelemetryDescription": "L'enregistrement ne contient pas suffisamment de données de mouvement du curseur.", + "noDwellMoments": "Aucun moment de pause du curseur trouvé", + "noDwellMomentsDescription": "Essayez un enregistrement avec des pauses plus lentes du curseur sur les actions importantes.", + "noAutoZoomSlots": "Aucun emplacement de zoom automatique disponible", + "noAutoZoomSlotsDescription": "Les points de pause détectés chevauchent des régions de zoom existantes.", + "cannotPlaceTrim": "Impossible de placer la coupe ici", + "trimExistsAtLocation": "Une coupe existe déjà à cet emplacement ou l'espace disponible est insuffisant.", + "cannotPlaceSpeed": "Impossible de placer la vitesse ici", + "speedExistsAtLocation": "Une région de vitesse existe déjà à cet emplacement ou l'espace disponible est insuffisant." + }, + "success": { + "addedZoomSuggestions": "{{count}} suggestion de zoom basée sur le curseur ajoutée", + "addedZoomSuggestionsPlural": "{{count}} suggestions de zoom basées sur le curseur ajoutées" + } +} From 7a8fb807e6f2be2bf273f066d71aca071b51595e Mon Sep 17 00:00:00 2001 From: FabLrc Date: Tue, 7 Apr 2026 12:17:10 +0200 Subject: [PATCH 15/21] feat(i18n): add French translations for common and dialogs namespaces --- electron/i18n.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/electron/i18n.ts b/electron/i18n.ts index b385008..2dfb4d3 100644 --- a/electron/i18n.ts +++ b/electron/i18n.ts @@ -5,10 +5,12 @@ import commonEn from "../src/i18n/locales/en/common.json"; import dialogsEn from "../src/i18n/locales/en/dialogs.json"; import commonEs from "../src/i18n/locales/es/common.json"; import dialogsEs from "../src/i18n/locales/es/dialogs.json"; +import commonFr from "../src/i18n/locales/fr/common.json"; +import dialogsFr from "../src/i18n/locales/fr/dialogs.json"; import commonZh from "../src/i18n/locales/zh-CN/common.json"; import dialogsZh from "../src/i18n/locales/zh-CN/dialogs.json"; -type Locale = "en" | "zh-CN" | "es"; +type Locale = "en" | "zh-CN" | "es" | "fr"; type Namespace = "common" | "dialogs"; type MessageMap = Record; @@ -16,12 +18,13 @@ const messages: Record> = { en: { common: commonEn, dialogs: dialogsEn }, "zh-CN": { common: commonZh, dialogs: dialogsZh }, es: { common: commonEs, dialogs: dialogsEs }, + fr: { common: commonFr, dialogs: dialogsFr }, }; let currentLocale: Locale = "en"; export function setMainLocale(locale: string) { - if (locale === "en" || locale === "zh-CN" || locale === "es") { + if (locale === "en" || locale === "zh-CN" || locale === "es" || locale === "fr") { currentLocale = locale; } } From 1f56bb42c31ac02d96511545dcb199381df0b788 Mon Sep 17 00:00:00 2001 From: FabLrc Date: Tue, 7 Apr 2026 12:17:53 +0200 Subject: [PATCH 16/21] fix(i18n): update French translations for cycle annotations shortcuts --- src/i18n/locales/fr/shortcuts.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/locales/fr/shortcuts.json b/src/i18n/locales/fr/shortcuts.json index ebd2181..5c6e494 100644 --- a/src/i18n/locales/fr/shortcuts.json +++ b/src/i18n/locales/fr/shortcuts.json @@ -25,8 +25,8 @@ "fixedActions": { "undo": "Annuler", "redo": "Rétablir", - "cycleAnnotationsForward": "Cycler les annotations en avant", - "cycleAnnotationsBackward": "Cycler les annotations en arrière", + "cycleAnnotationsForward": "Parcourir les annotations en avant", + "cycleAnnotationsBackward": "Parcourir les annotations en arrière", "deleteSelectedAlt": "Supprimer la sélection (alt)", "panTimeline": "Panoramique de la timeline", "zoomTimeline": "Zoom de la timeline", From 6bff2a2a2c83783f0403381a87091d726512ebaa Mon Sep 17 00:00:00 2001 From: Marc Diaz Date: Tue, 7 Apr 2026 12:58:33 -0400 Subject: [PATCH 17/21] feat: use export testing --- .github/workflows/ci.yml | 13 + .gitignore | 5 +- package-lock.json | 1917 +++++++++++++++-- package.json | 3 + src/lib/exporter/gifExporter.browser.test.ts | 43 + .../exporter/videoExporter.browser.test.ts | 43 + vitest.browser.config.ts | 30 + 7 files changed, 1857 insertions(+), 197 deletions(-) create mode 100644 src/lib/exporter/gifExporter.browser.test.ts create mode 100644 src/lib/exporter/videoExporter.browser.test.ts create mode 100644 vitest.browser.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2b04db..63f2857 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,19 @@ jobs: - run: npm ci - run: npx tsc --noEmit + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npx playwright install --with-deps chromium + - run: npm run test:browser + build: name: Build runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 70cc387..9393ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,7 @@ release/** # Playwright test-results -playwright-report/ \ No newline at end of file +playwright-report/ + +# Vitest browser mode screenshots +__screenshots__/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fdbd6b9..762262a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,8 @@ "@types/react-dom": "^18.2.21", "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^4.2.1", + "@vitest/browser": "^4.0.16", + "@vitest/browser-playwright": "^4.0.16", "autoprefixer": "^10.4.21", "electron": "^39.2.7", "electron-builder": "^26.7.0", @@ -1898,14 +1900,13 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "netbsd" @@ -1932,14 +1933,13 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openbsd" @@ -1966,14 +1966,13 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openharmony" @@ -3205,6 +3204,56 @@ "node": ">=18" } }, + "node_modules/@playwright/test/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/@playwright/test/node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -4471,8 +4520,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", @@ -4665,7 +4713,6 @@ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, - "license": "MIT", "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" @@ -4691,8 +4738,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/dom-mediacapture-transform": { "version": "0.1.11", @@ -4946,12 +4992,1215 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/browser": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.16.tgz", + "integrity": "sha512-t4toy8X/YTnjYEPoY0pbDBg3EvDPg1elCDrfc+VupPHwoN/5/FNQ8Z+xBYIaEnOE2vVEyKwqYBzZ9h9rJtZVcg==", + "dev": true, + "dependencies": { + "@vitest/mocker": "4.0.16", + "@vitest/utils": "4.0.16", + "magic-string": "^0.30.21", + "pixelmatch": "7.1.0", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.0.3", + "ws": "^8.18.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.16" + } + }, + "node_modules/@vitest/browser-playwright": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.16.tgz", + "integrity": "sha512-I2Fy/ANdphi1yI46d15o0M1M4M0UJrUiVKkH5oKeRZZCdPg0fw/cfTKZzv9Ge9eobtJYp4BGblMzXdXH0vcl5g==", + "dev": true, + "dependencies": { + "@vitest/browser": "4.0.16", + "@vitest/mocker": "4.0.16", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "4.0.16" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/@vitest/mocker": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", + "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "dev": true, + "dependencies": { + "@vitest/spy": "4.0.16", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/browser-playwright/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@vitest/browser-playwright/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@vitest/browser-playwright/node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/browser/node_modules/@vitest/mocker": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", + "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "dev": true, + "dependencies": { + "@vitest/spy": "4.0.16", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/browser/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/@vitest/browser/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@vitest/browser/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@vitest/browser/node_modules/pixelmatch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz", + "integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==", + "dev": true, + "dependencies": { + "pngjs": "^7.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/@vitest/browser/node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/@vitest/browser/node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.0.16", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", "dev": true, - "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", @@ -4969,7 +6218,6 @@ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", "dev": true, - "license": "MIT", "dependencies": { "tinyrainbow": "^3.0.3" }, @@ -4982,7 +6230,6 @@ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", "dev": true, - "license": "MIT", "dependencies": { "@vitest/utils": "4.0.16", "pathe": "^2.0.3" @@ -4996,7 +6243,6 @@ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", "dev": true, - "license": "MIT", "dependencies": { "@vitest/pretty-format": "4.0.16", "magic-string": "^0.30.21", @@ -5011,7 +6257,6 @@ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", "dev": true, - "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" } @@ -5021,7 +6266,6 @@ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", "dev": true, - "license": "MIT", "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" @@ -5728,7 +6972,6 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" } @@ -6353,11 +7596,10 @@ } }, "node_modules/chai": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", - "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" } @@ -7681,8 +8923,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -7795,7 +9036,6 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } @@ -9522,6 +10762,268 @@ "node": ">=0.10.0" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -10647,6 +12149,15 @@ "node": ">=4" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -11187,8 +12698,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/pe-library": { "version": "0.4.1", @@ -11472,12 +12982,13 @@ } }, "node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", "dev": true, + "peer": true, "dependencies": { - "playwright-core": "1.58.2" + "playwright-core": "1.59.1" }, "bin": { "playwright": "cli.js" @@ -11490,10 +13001,11 @@ } }, "node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", "dev": true, + "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -11511,6 +13023,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -11548,9 +13061,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -11565,7 +13078,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12916,6 +14428,20 @@ "node": ">=10" } }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -13147,8 +14673,7 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/string_decoder": { "version": "1.3.0", @@ -13862,11 +15387,10 @@ } }, "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, - "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -13941,6 +15465,15 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -14339,7 +15872,6 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, - "license": "MIT", "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", @@ -14413,14 +15945,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "aix" @@ -14430,14 +15961,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -14447,14 +15977,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -14464,14 +15993,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -14481,14 +16009,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -14498,14 +16025,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -14515,14 +16041,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -14532,14 +16057,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -14549,14 +16073,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -14566,14 +16089,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -14583,14 +16105,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -14600,14 +16121,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -14617,14 +16137,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -14634,14 +16153,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -14651,14 +16169,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -14668,14 +16185,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -14685,14 +16201,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -14702,14 +16217,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "netbsd" @@ -14719,14 +16233,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openbsd" @@ -14736,14 +16249,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "sunos" @@ -14753,14 +16265,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -14770,14 +16281,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -14787,14 +16297,13 @@ } }, "node_modules/vitest/node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -14808,7 +16317,6 @@ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", "dev": true, - "license": "MIT", "dependencies": { "@vitest/spy": "4.0.16", "estree-walker": "^3.0.3", @@ -14831,12 +16339,11 @@ } }, "node_modules/vitest/node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -14844,32 +16351,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/vitest/node_modules/fdir": { @@ -14877,7 +16384,6 @@ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, - "license": "MIT", "engines": { "node": ">=12.0.0" }, @@ -14891,11 +16397,10 @@ } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -14904,24 +16409,23 @@ } }, "node_modules/vitest/node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, - "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -14930,14 +16434,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", - "less": "^4.0.0", + "less": "*", "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -15137,6 +16641,27 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xhr": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", diff --git a/package.json b/package.json index 8817372..586a846 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "test": "vitest --run", "test:watch": "vitest", "build-vite": "tsc && vite build", + "test:browser": "vitest --config vitest.browser.config.ts --run", "test:e2e": "playwright test", "prepare": "husky" }, @@ -82,6 +83,8 @@ "@types/react-dom": "^18.2.21", "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^4.2.1", + "@vitest/browser": "^4.0.16", + "@vitest/browser-playwright": "^4.0.16", "autoprefixer": "^10.4.21", "electron": "^39.2.7", "electron-builder": "^26.7.0", diff --git a/src/lib/exporter/gifExporter.browser.test.ts b/src/lib/exporter/gifExporter.browser.test.ts new file mode 100644 index 0000000..db9b144 --- /dev/null +++ b/src/lib/exporter/gifExporter.browser.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import sampleVideoUrl from "../../../tests/fixtures/sample.webm?url"; +import { GifExporter } from "./gifExporter"; +import type { ExportProgress } from "./types"; + +describe("GifExporter (real browser)", () => { + it("exports a valid GIF blob from a real video", async () => { + const progressEvents: ExportProgress[] = []; + + const exporter = new GifExporter({ + videoUrl: sampleVideoUrl, + width: 320, + height: 180, + frameRate: 15, + loop: true, + sizePreset: "medium", + wallpaper: "#1a1a2e", + zoomRegions: [], + showShadow: false, + shadowIntensity: 0, + showBlur: false, + cropRegion: { x: 0, y: 0, width: 1, height: 1 }, + onProgress: (p) => progressEvents.push(p), + }); + + const result = await exporter.export(); + + expect(result.success, result.error).toBe(true); + expect(result.blob).toBeInstanceOf(Blob); + + const buf = await result.blob!.arrayBuffer(); + const header = new TextDecoder().decode(new Uint8Array(buf, 0, 6)); + expect(header).toMatch(/^GIF8[79]a/); + + expect(result.blob!.size).toBeGreaterThan(1024); + + expect(progressEvents.length).toBeGreaterThan(0); + + const finalizing = progressEvents.filter((p) => p.phase === "finalizing"); + expect(finalizing.length).toBeGreaterThan(0); + expect(finalizing.at(-1)!.percentage).toBe(100); + }); +}); diff --git a/src/lib/exporter/videoExporter.browser.test.ts b/src/lib/exporter/videoExporter.browser.test.ts new file mode 100644 index 0000000..ec2b0f6 --- /dev/null +++ b/src/lib/exporter/videoExporter.browser.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import sampleVideoUrl from "../../../tests/fixtures/sample.webm?url"; +import type { ExportProgress } from "./types"; +import { VideoExporter } from "./videoExporter"; + +describe("VideoExporter (real browser)", () => { + it("exports a valid MP4 blob from a real video", async () => { + const progressEvents: ExportProgress[] = []; + + const exporter = new VideoExporter({ + videoUrl: sampleVideoUrl, + width: 320, + height: 180, + frameRate: 15, + bitrate: 1_000_000, + wallpaper: "#1a1a2e", + zoomRegions: [], + showShadow: false, + shadowIntensity: 0, + showBlur: false, + cropRegion: { x: 0, y: 0, width: 1, height: 1 }, + onProgress: (p) => progressEvents.push(p), + }); + + const result = await exporter.export(); + + expect(result.success, result.error).toBe(true); + expect(result.blob).toBeInstanceOf(Blob); + + const buf = await result.blob!.arrayBuffer(); + const bytes = new Uint8Array(buf); + const ftyp = new TextDecoder().decode(bytes.slice(4, 8)); + expect(ftyp).toBe("ftyp"); + + expect(result.blob!.size).toBeGreaterThan(1024); + + expect(progressEvents.length).toBeGreaterThan(0); + + const finalizing = progressEvents.filter((p) => p.phase === "finalizing"); + expect(finalizing.length).toBeGreaterThan(0); + expect(finalizing.at(-1)!.percentage).toBe(100); + }); +}); diff --git a/vitest.browser.config.ts b/vitest.browser.config.ts new file mode 100644 index 0000000..8f07b0c --- /dev/null +++ b/vitest.browser.config.ts @@ -0,0 +1,30 @@ +import path from "node:path"; +import { playwright } from "@vitest/browser-playwright"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.browser.test.{ts,tsx}"], + browser: { + enabled: true, + provider: playwright({ + launch: { + // Software WebGL so Pixi.js works in headless CI without a GPU. + args: ["--enable-unsafe-swiftshader", "--use-gl=swiftshader"], + }, + }), + headless: true, + instances: [{ browser: "chromium" }], + }, + // GIF export encodes frames and renders — give it plenty of time. + testTimeout: 120_000, + hookTimeout: 30_000, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, + // Let Vite treat .webm and .wasm files as static assets importable via `?url`. + assetsInclude: ["**/*.webm"], +}); From b65c68d1397f22896097305462254e88c54ad45e Mon Sep 17 00:00:00 2001 From: Marc Diaz Date: Tue, 7 Apr 2026 13:02:11 -0400 Subject: [PATCH 18/21] fix: use headless --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63f2857..e7f1a48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: node-version: 22 cache: npm - run: npm ci - - run: npx playwright install --with-deps chromium + - run: npx playwright install --with-deps chromium-headless-shell - run: npm run test:browser build: From 33609432e1e13c16f1d6a8c1b8e2fa11d65d45cb Mon Sep 17 00:00:00 2001 From: Marc Diaz Date: Tue, 7 Apr 2026 13:05:27 -0400 Subject: [PATCH 19/21] fix: use npm for install --- .github/workflows/ci.yml | 2 +- package.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7f1a48..4194797 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: node-version: 22 cache: npm - run: npm ci - - run: npx playwright install --with-deps chromium-headless-shell + - run: npm run test:browser:install - run: npm run test:browser build: diff --git a/package.json b/package.json index 586a846..9951b04 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "test:watch": "vitest", "build-vite": "tsc && vite build", "test:browser": "vitest --config vitest.browser.config.ts --run", + "test:browser:install": "playwright install --with-deps chromium-headless-shell", "test:e2e": "playwright test", "prepare": "husky" }, From b8fe1a1ec8392e6d6f5e4acf7c993b4c98f87e4a Mon Sep 17 00:00:00 2001 From: Marc Diaz Date: Tue, 7 Apr 2026 13:32:49 -0400 Subject: [PATCH 20/21] fix(playwright): use one version --- package-lock.json | 57 +++++------------------------------------------ package.json | 2 +- 2 files changed, 6 insertions(+), 53 deletions(-) diff --git a/package-lock.json b/package-lock.json index 762262a..4f43a91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.3.13", - "@playwright/test": "^1.58.2", + "@playwright/test": "^1.59.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", @@ -3190,12 +3190,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", - "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", "dev": true, "dependencies": { - "playwright": "1.58.2" + "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" @@ -3204,50 +3204,6 @@ "node": ">=18" } }, - "node_modules/@playwright/test/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/@playwright/test/node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", - "dev": true, - "dependencies": { - "playwright-core": "1.58.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/@playwright/test/node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "dev": true, - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -12986,7 +12942,6 @@ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", "dev": true, - "peer": true, "dependencies": { "playwright-core": "1.59.1" }, @@ -13005,7 +12960,6 @@ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", "dev": true, - "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -13023,7 +12977,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } diff --git a/package.json b/package.json index 9951b04..d41fd40 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.3.13", - "@playwright/test": "^1.58.2", + "@playwright/test": "^1.59.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", From 3482be9864a106890e16a99e504a5911ef76925e Mon Sep 17 00:00:00 2001 From: Marc Diaz Date: Tue, 7 Apr 2026 13:50:26 -0400 Subject: [PATCH 21/21] refactor: remove extraneous comments --- vitest.browser.config.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/vitest.browser.config.ts b/vitest.browser.config.ts index 8f07b0c..ba5cc42 100644 --- a/vitest.browser.config.ts +++ b/vitest.browser.config.ts @@ -16,7 +16,6 @@ export default defineConfig({ headless: true, instances: [{ browser: "chromium" }], }, - // GIF export encodes frames and renders — give it plenty of time. testTimeout: 120_000, hookTimeout: 30_000, }, @@ -25,6 +24,5 @@ export default defineConfig({ "@": path.resolve(__dirname, "src"), }, }, - // Let Vite treat .webm and .wasm files as static assets importable via `?url`. assetsInclude: ["**/*.webm"], });