From a03df8e9ac5e9ba2b704d7d0d9ca91db7b4f420d Mon Sep 17 00:00:00 2001 From: DrakeNguyen Date: Fri, 27 Mar 2026 21:02:06 -0500 Subject: [PATCH 1/5] 0.1.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": { From e4aa43a6e48e63d4d7abd68dc15de81cb001ee8e Mon Sep 17 00:00:00 2001 From: DrakeNguyen Date: Fri, 27 Mar 2026 23:11:30 -0500 Subject: [PATCH 2/5] feat(diff-checker): enhance diff alignment and editor functionality - Updated DiffChecker component to improve text wrapping options, setting both original and modified wrap text to false by default. - Introduced new state variables for zoom epoch and alignment zones to enhance visual accuracy between editors. - Added logic to conditionally render alignment zones based on content presence in both editors, preventing phantom rows. - Enhanced view zone management to ensure accurate height calculations for wrapped lines, improving the overall user experience. This update aims to provide a more precise and user-friendly diff comparison experience. --- src/components/tools/DiffChecker.tsx | 133 ++++++++++++++++++++------- src/libs/monaco-utils.ts | 1 + 2 files changed, 103 insertions(+), 31 deletions(-) diff --git a/src/components/tools/DiffChecker.tsx b/src/components/tools/DiffChecker.tsx index 3ce8f81..a59245a 100644 --- a/src/components/tools/DiffChecker.tsx +++ b/src/components/tools/DiffChecker.tsx @@ -60,9 +60,10 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) { const [options, setOptions] = useState(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); } diff --git a/src/libs/monaco-utils.ts b/src/libs/monaco-utils.ts index 15e21ac..7323992 100644 --- a/src/libs/monaco-utils.ts +++ b/src/libs/monaco-utils.ts @@ -217,6 +217,7 @@ export function createMonacoOptions( suggestOnTriggerCharacters: false, // Disable suggestions on trigger characters by default acceptSuggestionOnCommitCharacter: false, // Disable auto-accept suggestions by default tabCompletion: 'off', // Disable tab completion by default + renderValidationDecorations: 'off', // No error/warning squiggles needed in developer tools scrollbar: { alwaysConsumeMouseWheel: false, // Allow page scrolling when editor reaches end }, From bb77721a320d08bf1e7bc8b1234a797a22b17532 Mon Sep 17 00:00:00 2001 From: DrakeNguyen Date: Sat, 28 Mar 2026 00:01:23 -0500 Subject: [PATCH 3/5] feat: enhance tool components with flexible layout adjustments - Updated multiple tool components to improve layout by adding flex properties and ensuring panels can grow and shrink with their containers. - Introduced `fillHeight` prop to `CodePanel` for better height management, allowing for a more responsive design. - Adjusted body sections of tools like BaseEncoder, CidrAnalyzer, and others to utilize flexbox for improved usability and visual consistency. These changes aim to enhance the user experience by providing a more adaptable interface across various tools. --- src/components/tools/BaseEncoder.tsx | 15 +++++++------- src/components/tools/CidrAnalyzer.tsx | 7 ++++--- src/components/tools/CronParser.tsx | 7 ++++--- src/components/tools/DataFormatConverter.tsx | 10 +++++----- src/components/tools/DiffChecker.tsx | 10 +++++----- src/components/tools/HashGenerator.tsx | 10 +++++----- src/components/tools/IpChecker.tsx | 7 ++++--- src/components/tools/IpToCidrConverter.tsx | 7 ++++--- src/components/tools/JsonFormatter.tsx | 10 +++++----- src/components/tools/JsonPathFinder.tsx | 10 +++++----- src/components/tools/JsonSchemaGenerator.tsx | 10 +++++----- src/components/tools/JwtDecoder.tsx | 9 +++++---- src/components/tools/JwtEncoder.tsx | 11 +++++----- src/components/tools/ListComparison.tsx | 10 +++++----- src/components/tools/ListConverter.tsx | 10 +++++----- src/components/tools/LoremIpsumGenerator.tsx | 7 ++++--- src/components/tools/NumberBaseConverter.tsx | 10 +++++----- src/components/tools/QrCodeDecoder.tsx | 10 +++++----- src/components/tools/QrCodeGenerator.tsx | 10 +++++----- src/components/tools/QrCodeScanner.tsx | 10 +++++----- src/components/tools/RegexTester.tsx | 18 +++++++++-------- src/components/tools/SchemaConverter.tsx | 10 +++++----- src/components/tools/SystemInfo.tsx | 7 ++++--- src/components/tools/TimestampConverter.tsx | 7 ++++--- src/components/tools/UrlDecoderTool.tsx | 10 +++++----- src/components/tools/UrlEncoderTool.tsx | 10 +++++----- src/components/tools/UuidGenerator.tsx | 7 ++++--- src/components/tools/XmlFormatter.tsx | 10 +++++----- src/components/tools/XmlPathFinder.tsx | 10 +++++----- src/components/tools/YamlPathFinder.tsx | 10 +++++----- src/components/ui/code-panel.tsx | 21 +++++++++++++------- 31 files changed, 164 insertions(+), 146 deletions(-) diff --git a/src/components/tools/BaseEncoder.tsx b/src/components/tools/BaseEncoder.tsx index 9ff7ef6..e90b1f8 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 */} - {/* Body Section */} -
-
+
+
{/* Controls */}
{/* Main Controls Row */} @@ -675,9 +675,9 @@ export function DiffChecker({ className, instanceId }: DiffCheckerProps) {
{/* Side-by-side Editor Panels */} -
+
{/* Original Panel */} - setOriginalText(value)} @@ -726,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..323c526 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 */}