diff --git a/package.json b/package.json index 66e29a7..f258f69 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "devpockit-frontend", - "version": "0.1.8", + "version": "0.1.9", "description": "DevPockit Frontend - Developer Tools Web App", "license": "MIT", "repository": { diff --git a/src/components/tools/BaseEncoder.tsx b/src/components/tools/BaseEncoder.tsx index 9ff7ef6..04ec7c6 100644 --- a/src/components/tools/BaseEncoder.tsx +++ b/src/components/tools/BaseEncoder.tsx @@ -4,11 +4,10 @@ import { useToolState } from '@/components/providers/ToolStateProvider'; import { Button } from '@/components/ui/button'; import { CodePanel } from '@/components/ui/code-panel'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; -import { LabeledInput } from '@/components/ui/labeled-input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { DEFAULT_BASE_OPTIONS, BASE_ENCODING_TYPES, BASE_EXAMPLES, BASE64_VARIANTS, LINE_WRAP_OPTIONS, HEX_CASE_OPTIONS } from '@/config/base-encoder-config'; +import { BASE64_VARIANTS, BASE_ENCODING_TYPES, BASE_EXAMPLES, DEFAULT_BASE_OPTIONS, HEX_CASE_OPTIONS, LINE_WRAP_OPTIONS } from '@/config/base-encoder-config'; import { useCodeEditorTheme } from '@/hooks/useCodeEditorTheme'; -import { encodeBase, decodeBase, type BaseEncoderOptions, type BaseEncoderResult } from '@/libs/base-encoder'; +import { decodeBase, encodeBase, type BaseEncoderOptions, type BaseEncoderResult } from '@/libs/base-encoder'; import { cn } from '@/libs/utils'; import { ArrowPathIcon, ChevronDownIcon } from '@heroicons/react/24/outline'; import { useEffect, useMemo, useRef, useState } from 'react'; @@ -173,8 +172,8 @@ export function BaseEncoder({ className, instanceId }: BaseEncoderProps) { {/* Body Section */} -
-
+
+
{/* Controls */}
@@ -281,9 +280,9 @@ export function BaseEncoder({ className, instanceId }: BaseEncoderProps) {
{/* Side-by-side Editor Panels */} -
+
{/* Input Panel */} - {/* Output Panel */} - {/* Body Section */} -
-
+
+
{/* Controls */}
{/* Input Row */} @@ -301,11 +301,12 @@ export function CidrAnalyzer({ className, instanceId }: CidrAnalyzerProps) {
{/* Output Panel */} - {/* Body Section */} -
-
+
+
{/* Controls */}
{/* Expression Display */} @@ -405,11 +405,12 @@ export function CronParser({ className, instanceId }: CronParserProps) {
{/* Preview Panel */} - {/* Body Section */} -
-
+
+
{/* Controls */}
{/* Main Controls Row */} @@ -290,9 +290,9 @@ export function DataFormatConverter({ className, instanceId }: DataFormatConvert
{/* Side-by-side Editor Panels */} -
+
{/* Input Panel */} - {/* Output Panel */} - (DEFAULT_DIFF_OPTIONS); const [stats, setStats] = useState({ additions: 0, deletions: 0, changes: 0 }); const [isHydrated, setIsHydrated] = useState(false); - const [originalWrapText, setOriginalWrapText] = useState(true); - const [modifiedWrapText, setModifiedWrapText] = useState(true); + const [originalWrapText, setOriginalWrapText] = useState(false); + const [modifiedWrapText, setModifiedWrapText] = useState(false); const [editorsReady, setEditorsReady] = useState(false); + const [zoomEpoch, setZoomEpoch] = useState(0); // Editor refs for decorations const originalEditorRef = useRef(null); @@ -118,6 +119,9 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) { interface ViewZoneData { afterLineNumber: number; heightInLines: number; + // Lines in the OTHER editor that this gap is aligning with (for pixel-accurate sizing) + otherEditorStart: number; + otherEditorCount: number; } // Calculate diff and apply decorations with character-level highlighting @@ -154,7 +158,7 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) { }); }); // Pad original side so unchanged lines below stay aligned - originalViewZones.push({ afterLineNumber: originalLine - 1, heightInLines: lineCount }); + originalViewZones.push({ afterLineNumber: originalLine - 1, heightInLines: lineCount, otherEditorStart: modifiedLine, otherEditorCount: lineCount }); modifiedLine += lineCount; } else if (change.removed) { // Check if next is added (paired change for inline diff) @@ -231,11 +235,15 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) { modifiedViewZones.push({ afterLineNumber: modifiedLine + addedLines.length - 1, heightInLines: removedLines.length - addedLines.length, + otherEditorStart: originalLine + addedLines.length, + otherEditorCount: removedLines.length - addedLines.length, }); } else if (addedLines.length > removedLines.length) { originalViewZones.push({ afterLineNumber: originalLine + removedLines.length - 1, heightInLines: addedLines.length - removedLines.length, + otherEditorStart: modifiedLine + removedLines.length, + otherEditorCount: addedLines.length - removedLines.length, }); } @@ -249,7 +257,7 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) { type: 'removed', }); }); - modifiedViewZones.push({ afterLineNumber: modifiedLine - 1, heightInLines: lineCount }); + modifiedViewZones.push({ afterLineNumber: modifiedLine - 1, heightInLines: lineCount, otherEditorStart: originalLine, otherEditorCount: lineCount }); } originalLine += lineCount; } else { @@ -270,6 +278,10 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) { const monaco = (window as any).monaco; if (!monaco) return; + // Only insert alignment zones when both panels have content — avoids blocking + // the cursor in the empty editor with phantom rows at afterLineNumber: 0. + const shouldAlign = originalText.trim().length > 0 && modifiedText.trim().length > 0; + if (originalEditorRef.current) { const editor = originalEditorRef.current; const decorations = [ @@ -280,6 +292,10 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) { isWholeLine: true, className: 'diff-line-removed', glyphMarginClassName: 'diff-glyph-removed', + overviewRuler: { + color: 'rgba(239, 68, 68, 0.8)', + position: monaco.editor.OverviewRulerLane.Full, + }, }, })), // Inline character decorations @@ -295,21 +311,6 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) { decorations ); - // Apply alignment view zones - editor.changeViewZones((accessor: any) => { - originalViewZoneIdsRef.current.forEach((id) => accessor.removeZone(id)); - originalViewZoneIdsRef.current = []; - originalViewZones.forEach((zone) => { - const domNode = document.createElement('div'); - domNode.className = 'diff-placeholder-zone'; - const id = accessor.addZone({ - afterLineNumber: zone.afterLineNumber, - heightInLines: zone.heightInLines, - domNode, - }); - originalViewZoneIdsRef.current.push(id); - }); - }); } if (modifiedEditorRef.current) { @@ -322,6 +323,10 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) { isWholeLine: true, className: 'diff-line-added', glyphMarginClassName: 'diff-glyph-added', + overviewRuler: { + color: 'rgba(34, 197, 94, 0.8)', + position: monaco.editor.OverviewRulerLane.Full, + }, }, })), // Inline character decorations @@ -336,31 +341,77 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) { modifiedDecorationsRef.current, decorations ); + } + + if (!shouldAlign) return; - // Apply alignment view zones - editor.changeViewZones((accessor: any) => { + // Phase 1 — remove stale zones from both editors so getTopForLineNumber + // returns accurate positions unaffected by previously applied zones. + if (originalEditorRef.current) { + originalEditorRef.current.changeViewZones((accessor: any) => { + originalViewZoneIdsRef.current.forEach((id) => accessor.removeZone(id)); + originalViewZoneIdsRef.current = []; + }); + } + if (modifiedEditorRef.current) { + modifiedEditorRef.current.changeViewZones((accessor: any) => { modifiedViewZoneIdsRef.current.forEach((id) => accessor.removeZone(id)); modifiedViewZoneIdsRef.current = []; + }); + } + + // Phase 2 — add new zones. When either panel has word wrap on, derive heightInPx + // from getTopForLineNumber on the other editor so wrapped visual rows are accounted + // for. Fall back to heightInLines when wrap is off so Monaco auto-scales with zoom. + const usePixelZones = originalWrapText || modifiedWrapText; + + if (originalEditorRef.current) { + originalEditorRef.current.changeViewZones((accessor: any) => { + originalViewZones.forEach((zone) => { + const domNode = document.createElement('div'); + domNode.className = 'diff-placeholder-zone'; + let zoneDef: Record = { afterLineNumber: zone.afterLineNumber, domNode }; + if (usePixelZones && modifiedEditorRef.current) { + const topStart = modifiedEditorRef.current.getTopForLineNumber(zone.otherEditorStart); + const topEnd = modifiedEditorRef.current.getTopForLineNumber(zone.otherEditorStart + zone.otherEditorCount); + const heightInPx = topEnd - topStart; + zoneDef = heightInPx > 0 ? { ...zoneDef, heightInPx } : { ...zoneDef, heightInLines: zone.heightInLines }; + } else { + zoneDef.heightInLines = zone.heightInLines; + } + const id = accessor.addZone(zoneDef); + originalViewZoneIdsRef.current.push(id); + }); + }); + } + + if (modifiedEditorRef.current) { + modifiedEditorRef.current.changeViewZones((accessor: any) => { modifiedViewZones.forEach((zone) => { const domNode = document.createElement('div'); domNode.className = 'diff-placeholder-zone'; - const id = accessor.addZone({ - afterLineNumber: zone.afterLineNumber, - heightInLines: zone.heightInLines, - domNode, - }); + let zoneDef: Record = { afterLineNumber: zone.afterLineNumber, domNode }; + if (usePixelZones && originalEditorRef.current) { + const topStart = originalEditorRef.current.getTopForLineNumber(zone.otherEditorStart); + const topEnd = originalEditorRef.current.getTopForLineNumber(zone.otherEditorStart + zone.otherEditorCount); + const heightInPx = topEnd - topStart; + zoneDef = heightInPx > 0 ? { ...zoneDef, heightInPx } : { ...zoneDef, heightInLines: zone.heightInLines }; + } else { + zoneDef.heightInLines = zone.heightInLines; + } + const id = accessor.addZone(zoneDef); modifiedViewZoneIdsRef.current.push(id); }); }); } - }, [originalText, modifiedText, options.ignoreWhitespace]); + }, [originalText, modifiedText, options.ignoreWhitespace, originalWrapText, modifiedWrapText]); - // Apply decorations when diff calculation changes OR when editors become ready + // Apply decorations when diff calculation changes, editors become ready, or zoom changes useEffect(() => { if (editorsReady) { calculateDiffAndDecorate(); } - }, [calculateDiffAndDecorate, editorsReady]); + }, [calculateDiffAndDecorate, editorsReady, zoomEpoch]); // Scroll sync between editors useEffect(() => { @@ -482,7 +533,18 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) { const handleOriginalEditorMount = (editor: any) => { originalEditorRef.current = editor; - // Check if both editors are ready - the effect will handle decorations + // Increment zoomEpoch when font size or line height changes so pixel-based + // alignment zones are recomputed at the new scale. + editor.onDidChangeConfiguration((e: any) => { + const monaco = (window as any).monaco; + if ( + monaco && + (e.hasChanged(monaco.editor.EditorOption.fontSize) || + e.hasChanged(monaco.editor.EditorOption.lineHeight)) + ) { + setZoomEpoch((n) => n + 1); + } + }); if (modifiedEditorRef.current) { setEditorsReady(true); } @@ -490,7 +552,16 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) { const handleModifiedEditorMount = (editor: any) => { modifiedEditorRef.current = editor; - // Check if both editors are ready - the effect will handle decorations + editor.onDidChangeConfiguration((e: any) => { + const monaco = (window as any).monaco; + if ( + monaco && + (e.hasChanged(monaco.editor.EditorOption.fontSize) || + e.hasChanged(monaco.editor.EditorOption.lineHeight)) + ) { + setZoomEpoch((n) => n + 1); + } + }); if (originalEditorRef.current) { setEditorsReady(true); } @@ -553,8 +624,8 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) {
{/* Body Section */} -
-
+
+
{/* Controls */}
{/* Main Controls Row */} @@ -604,9 +675,9 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) {
{/* Side-by-side Editor Panels */} -
+
{/* Original Panel */} - setOriginalText(value)} @@ -655,7 +726,7 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) { /> {/* Modified Panel */} - setModifiedText(value)} diff --git a/src/components/tools/HashGenerator.tsx b/src/components/tools/HashGenerator.tsx index 9a7522f..b8b3073 100644 --- a/src/components/tools/HashGenerator.tsx +++ b/src/components/tools/HashGenerator.tsx @@ -153,8 +153,8 @@ export function HashGenerator({ className, instanceId }: HashGeneratorProps) {
{/* Body Section */} -
-
+
+
{/* Controls */}
{/* Main Controls Row */} @@ -239,9 +239,9 @@ export function HashGenerator({ className, instanceId }: HashGeneratorProps) {
{/* Side-by-side Editor Panels */} -
+
{/* Input Panel */} - {/* Output Panel */} - {/* Body Section */} -
-
+
+
{/* Controls */}
{/* IP Input Section */} @@ -304,11 +304,12 @@ export function IpChecker({ className, instanceId }: IpCheckerProps) {
{/* Output Panel */} - {/* Body Section */} -
-
+
+
{/* Controls */}
{/* Input Row */} @@ -207,11 +207,12 @@ export function IpToCidrConverter({ className, instanceId }: IpToCidrConverterPr
{/* Output Panel */} - {/* Body Section */} -
-
+
+
{/* Controls */}
{/* Main Controls Row */} @@ -202,9 +202,9 @@ export function JsonFormatter({ className, instanceId }: JsonFormatterProps) {
{/* Side-by-side Editor Panels */} -
+
{/* Input Panel */} - {/* Output Panel */} - {/* Body Section */} -
-
+
+
{/* JSONPath Input */}