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 */}