diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d28f688..7edd8db 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "devalue": "^5.3.2", "license-checker": "^25.0.1", "monaco-editor": "^0.53.0", + "monaco-vim": "^0.4.2", "nanoid": "^5.1.5", "npm-check-updates": "^17.1.3", "papaparse": "^5.5.3", @@ -2461,6 +2462,15 @@ "@types/trusted-types": "^1.0.6" } }, + "node_modules/monaco-vim": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/monaco-vim/-/monaco-vim-0.4.2.tgz", + "integrity": "sha512-rdbQC3O2rmpwX2Orzig/6gZjZfH7q7TIeB+uEl49sa+QyNm3jCKJOw5mwxBdFzTqbrPD+URfg6A2lEkuL5kymw==", + "license": "MIT", + "peerDependencies": { + "monaco-editor": "*" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index c1e30b8..018a596 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,6 +35,7 @@ "devalue": "^5.3.2", "license-checker": "^25.0.1", "monaco-editor": "^0.53.0", + "monaco-vim": "^0.4.2", "nanoid": "^5.1.5", "npm-check-updates": "^17.1.3", "papaparse": "^5.5.3", diff --git a/frontend/src/lib/components/editor/Editor.svelte b/frontend/src/lib/components/editor/Editor.svelte index 3fcb16d..ad7dd4d 100644 --- a/frontend/src/lib/components/editor/Editor.svelte +++ b/frontend/src/lib/components/editor/Editor.svelte @@ -5,6 +5,7 @@ import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'; import { BiMap } from '$lib/utils/maps'; import { previewQR as generateQR } from '$lib/utils/qrPreview'; + import { vimModeEnabled } from '$lib/store/vimMode.js'; /** @type {'domain'|'page'|'email'} */ export let contentType; @@ -12,6 +13,8 @@ export let baseURL = 'example.test'; export let domainMap = new BiMap({}); export let selectedDomain = ''; + export let externalVimMode = null; // allow external control of vim mode + let localVimMode = externalVimMode !== null ? externalVimMode : $vimModeEnabled; let editor = null; let previewFrame = null; let previewRenderDelayID = null; @@ -24,6 +27,7 @@ let externalFrameRef = null; let fileInputRef; let shadowContainer = null; + let vimStatusBar = null; const apiTemplates = [ { label: 'Custom Field 1', text: '{{.CustomField1}}' }, @@ -123,6 +127,11 @@ } }); + // enable vim mode if preference is enabled + if (localVimMode) { + initializeVimMode(); + } + editor.getModel().onDidChangeContent((e) => { if (previewRenderDelayID) { clearTimeout(previewRenderDelayID); @@ -136,6 +145,10 @@ return () => { document.body.classList.remove('overflow-hidden'); + if (vimModeInstance && vimModeInstance.dispose) { + vimModeInstance.dispose(); + vimModeInstance = null; + } if (editor) { editor.dispose(); monaco.editor.getModels().forEach((model) => model.dispose()); @@ -143,6 +156,52 @@ }; }); + // track vim mode state to prevent duplicate initialization + let vimModeInstance = null; + + const initializeVimMode = () => { + if (localVimMode && editor && !vimModeInstance) { + import('monaco-vim') + .then((vimModule) => { + const statusNode = vimStatusBar; + vimModeInstance = vimModule.initVimMode(editor, statusNode); + }) + .catch((e) => { + console.error('vim mode not available', e); + }); + } + }; + + const destroyVimMode = () => { + if (vimModeInstance) { + // use official monaco-vim dispose method + vimModeInstance.dispose(); + + // clear vim status bar + if (vimStatusBar) { + vimStatusBar.textContent = ''; + } + + vimModeInstance = null; + } + }; + + // sync with external vim mode control + $: if (externalVimMode !== null) { + localVimMode = externalVimMode; + } else { + localVimMode = $vimModeEnabled; + } + + // Watch for vim mode changes after initial load + $: if (editor && typeof localVimMode === 'boolean') { + if (localVimMode && !vimModeInstance) { + initializeVimMode(); + } else if (!localVimMode && vimModeInstance) { + destroyVimMode(); + } + } + const selectPreviewDomain = () => { baseURL = selectedDomain ? selectedDomain : baseURL; updatePreview(); @@ -564,6 +623,30 @@ Preview + + { + vimModeEnabled.update((v) => !v); + }} + class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200" + class:font-bold={localVimMode} + class:bg-cta-blue={localVimMode} + class:dark:bg-indigo-600={localVimMode} + class:text-white={localVimMode} + > + + + + Vim + + - + + {#if localVimMode} + + {/if} { value = editor.getValue(); }); - return cleanup; + return () => { + cleanup(); + if (vimModeInstance && vimModeInstance.dispose) { + vimModeInstance.dispose(); + vimModeInstance = null; + } + }; }); // Watch for external value changes @@ -101,6 +118,49 @@ editor.setValue(value || ''); } + const initializeVimMode = () => { + if (localVimMode && editor && !vimModeInstance) { + import('monaco-vim') + .then((vimModule) => { + const statusNode = vimStatusBar; + vimModeInstance = vimModule.initVimMode(editor, statusNode); + }) + .catch(() => { + console.warn('vim mode not available - monaco-vim package not installed'); + }); + } + }; + + const destroyVimMode = () => { + if (vimModeInstance) { + // use official monaco-vim dispose method + vimModeInstance.dispose(); + + // clear vim status bar + if (vimStatusBar) { + vimStatusBar.textContent = ''; + } + + vimModeInstance = null; + } + }; + + // sync with external vim mode control + $: if (externalVimMode !== null) { + localVimMode = externalVimMode; + } else { + localVimMode = $vimModeEnabled; + } + + // Watch for vim mode changes + $: if (editor && typeof localVimMode === 'boolean') { + if (localVimMode && !vimModeInstance) { + initializeVimMode(); + } else if (!localVimMode && vimModeInstance) { + destroyVimMode(); + } + } + let showExample = false; const loadExample = () => { @@ -114,12 +174,46 @@ + {#if showVimToggle} + + + { + vimModeEnabled.update((v) => !v); + }} + class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200" + class:font-bold={localVimMode} + class:bg-cta-blue={localVimMode} + class:dark:bg-indigo-600={localVimMode} + class:text-white={localVimMode} + > + Vim + + + + {/if} + {#if localVimMode} + + {/if} {#if placeholder} diff --git a/frontend/src/lib/components/editor/VimToggle.svelte b/frontend/src/lib/components/editor/VimToggle.svelte new file mode 100644 index 0000000..aa1c6bf --- /dev/null +++ b/frontend/src/lib/components/editor/VimToggle.svelte @@ -0,0 +1,16 @@ + + + + Vim + diff --git a/frontend/src/lib/store/vimMode.js b/frontend/src/lib/store/vimMode.js new file mode 100644 index 0000000..74bbfb9 --- /dev/null +++ b/frontend/src/lib/store/vimMode.js @@ -0,0 +1,26 @@ +import { writable } from 'svelte/store'; +import { getCookie, setCookie } from '$lib/utils/cookies.js'; + +// get initial vim mode preference from cookie +const getInitialVimMode = () => { + if (typeof document === 'undefined') { + return false; + } + const vimMode = getCookie('vim_mode_enabled'); + return vimMode === 'true'; +}; + +// create writable store for vim mode state +export const vimModeEnabled = writable(getInitialVimMode()); + +// subscribe to changes and save to cookie +vimModeEnabled.subscribe((enabled) => { + if (typeof document !== 'undefined') { + setCookie('vim_mode_enabled', enabled.toString()); + } +}); + +// helper function to toggle vim mode +export const toggleVimMode = () => { + vimModeEnabled.update(enabled => !enabled); +}; diff --git a/frontend/src/lib/utils/cookies.js b/frontend/src/lib/utils/cookies.js new file mode 100644 index 0000000..fd6fd87 --- /dev/null +++ b/frontend/src/lib/utils/cookies.js @@ -0,0 +1,57 @@ +/** + * simple cookie utility for storing user preferences + */ + +/** + * get a cookie value by name + * @param {string} name - cookie name + * @returns {string|null} cookie value or null if not found + */ +export function getCookie(name) { + if (typeof document === 'undefined') { + return null; + } + + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + + if (parts.length === 2) { + return parts.pop().split(';').shift(); + } + + return null; +} + +/** + * set a cookie value + * @param {string} name - cookie name + * @param {string} value - cookie value + * @param {number} days - expiration in days (default: 365) + */ +export function setCookie(name, value, days = 365) { + if (typeof document === 'undefined') { + return; + } + + const expires = new Date(); + expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000)); + + document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`; +} + +/** + * get vim mode preference from cookie + * @returns {boolean} vim mode enabled state + */ +export function getVimModePreference() { + const vimMode = getCookie('vim_mode_enabled'); + return vimMode === 'true'; +} + +/** + * save vim mode preference to cookie + * @param {boolean} enabled - vim mode enabled state + */ +export function setVimModePreference(enabled) { + setCookie('vim_mode_enabled', enabled.toString()); +} diff --git a/frontend/src/routes/api-sender/+page.svelte b/frontend/src/routes/api-sender/+page.svelte index 3d198d9..e67a4ee 100644 --- a/frontend/src/routes/api-sender/+page.svelte +++ b/frontend/src/routes/api-sender/+page.svelte @@ -31,6 +31,7 @@ import TableDropDownEllipsis from '$lib/components/table/TableDropDownEllipsis.svelte'; import DeleteAlert from '$lib/components/modal/DeleteAlert.svelte'; import SimpleCodeEditor from '$lib/components/editor/SimpleCodeEditor.svelte'; + import VimToggle from '$lib/components/editor/VimToggle.svelte'; // services const appStateService = AppStateService.instance; @@ -426,16 +427,20 @@ X-Custom-Header: Hello Friend" >Request Headers - - Request Body - - optional + + + Request Body + + optional + +
Request Body
optional