diff --git a/src/app.css b/src/app.css index 989db93..c910ea2 100644 --- a/src/app.css +++ b/src/app.css @@ -43,7 +43,7 @@ body { } *:focus-visible { - outline: 2px solid #3b82f6; + outline: 2px solid #16a34a; outline-offset: 2px; } diff --git a/src/lib/components/ComparatorPanel.svelte b/src/lib/components/ComparatorPanel.svelte index 848c56c..e5e6326 100644 --- a/src/lib/components/ComparatorPanel.svelte +++ b/src/lib/components/ComparatorPanel.svelte @@ -176,19 +176,9 @@ } } - function toggleWatchKey(key: string) { - const current = new SvelteSet(watchedKeys); - if (current.has(key)) { - current.delete(key); - } else { - current.add(key); - } - cmpState.watchedKeysInput = Array.from(current).join(', '); - handleWatchInput(); - } - function getValueByPath(obj: unknown, path: string): unknown { - const parts = path.split('.'); + const normalized = path.replace(/\[(\d+)\]/g, '.$1'); + const parts = normalized.split('.'); function walk(current: unknown, idx: number): unknown { if (idx >= parts.length) return current; if (current === null || current === undefined) return undefined; @@ -488,7 +478,7 @@ type="text" bind:value={cmpState.server1Base} list="server1-url-history" - class="w-full rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 font-mono text-sm text-gray-700 transition-all focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:ring-indigo-500/40" + class="w-full rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 font-mono text-sm text-gray-700 transition-all focus:border-green-500 focus:ring-2 focus:ring-green-500/20 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:ring-green-500/40" /> {#each server1UrlHistory as url (url)} @@ -507,7 +497,7 @@ type="text" bind:value={cmpState.server2Base} list="server2-url-history" - class="w-full rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 font-mono text-sm text-gray-700 transition-all focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:ring-indigo-500/40" + class="w-full rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 font-mono text-sm text-gray-700 transition-all focus:border-green-500 focus:ring-2 focus:ring-green-500/20 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:ring-green-500/40" /> {#each server2UrlHistory as url (url)} @@ -528,7 +518,7 @@ + @@ -783,7 +787,7 @@ class="absolute inset-0 z-50 flex items-center justify-center bg-white/80 backdrop-blur-sm dark:bg-gray-900/80" >
- +
{:else if !cmpState.loading} @@ -823,7 +828,7 @@

Ready to Compare

- Configure your endpoints above, then click Run Comparison to analyze the API response differences.

@@ -872,7 +877,7 @@ type="text" bind:value={cmpState.ignoreSearch} placeholder="Search keys..." - class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200" + class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm focus:border-green-500 focus:ring-2 focus:ring-green-500/20 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200" /> @@ -900,13 +905,13 @@ type="checkbox" checked={ignoredKeys.includes(key)} onchange={() => toggleIgnoreKey(key)} - class="h-4 w-4 rounded border-gray-300 bg-white text-indigo-600 focus:ring-indigo-500 focus:ring-offset-0 dark:border-gray-600 dark:bg-gray-700" + class="h-4 w-4 rounded border-gray-300 bg-white text-green-600 focus:ring-green-500 focus:ring-offset-0 dark:border-gray-600 dark:bg-gray-700" /> {key} {#if ignoredKeys.includes(key)} - Ignored {/if} @@ -986,7 +991,7 @@ type="text" bind:value={cmpState.watchSearch} placeholder="Search keys..." - class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200" + class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm focus:border-green-500 focus:ring-2 focus:ring-green-500/20 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200" /> @@ -1013,14 +1018,14 @@ toggleWatchKey(key)} - class="h-4 w-4 rounded border-gray-300 bg-white text-indigo-600 focus:ring-indigo-500 focus:ring-offset-0 dark:border-gray-600 dark:bg-gray-700" + onchange={() => cmpState.toggleWatchKey(key)} + class="h-4 w-4 rounded border-gray-300 bg-white text-green-600 focus:ring-green-500 focus:ring-offset-0 dark:border-gray-600 dark:bg-gray-700" /> {key} {#if watchedKeys.includes(key)} - Watching {/if} diff --git a/src/lib/components/DiffViewer.svelte b/src/lib/components/DiffViewer.svelte index 8874659..677ba86 100644 --- a/src/lib/components/DiffViewer.svelte +++ b/src/lib/components/DiffViewer.svelte @@ -9,9 +9,16 @@ response2: unknown; focusPath?: string; ignoredKeys?: string[]; + numericTolerancePercent?: number; } - let { response1, response2, focusPath = '', ignoredKeys = [] }: Props = $props(); + let { + response1, + response2, + focusPath = '', + ignoredKeys = [], + numericTolerancePercent = 0 + }: Props = $props(); const trimmedPath = $derived(focusPath.trim()); const focused1 = $derived(trimmedPath ? getByPath(response1, trimmedPath) : response1); @@ -338,7 +345,7 @@
{#if isSearching} {#if localSearchQuery}
diff --git a/src/lib/components/GtfsRtLogViewer.svelte b/src/lib/components/GtfsRtLogViewer.svelte index af6dbeb..832ba58 100644 --- a/src/lib/components/GtfsRtLogViewer.svelte +++ b/src/lib/components/GtfsRtLogViewer.svelte @@ -131,7 +131,7 @@

{#if totalSnapshotsLoading} Loading... @@ -161,7 +161,7 @@ onclick={() => (gtfsRtLogState.timeRange = range as 'live' | '1h' | '24h' | 'all')} class="flex-1 rounded-md px-3 py-1.5 text-xs font-medium transition-all sm:flex-none {gtfsRtLogState.timeRange === range - ? 'bg-indigo-600 text-white shadow' + ? 'bg-green-600 text-white shadow' : 'text-gray-600 hover:bg-gray-200 dark:text-gray-300 dark:hover:bg-gray-700'}" > {range === '1h' @@ -203,7 +203,7 @@ bind:value={gtfsRtLogState.limit} min="10" max="1000" - class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/30 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200" + class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-900 focus:border-green-500 focus:ring-2 focus:ring-green-500/30 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200" /> @@ -255,7 +255,7 @@
diff --git a/src/lib/components/JsonDiff.svelte b/src/lib/components/JsonDiff.svelte index 976cdcc..eb7935e 100644 --- a/src/lib/components/JsonDiff.svelte +++ b/src/lib/components/JsonDiff.svelte @@ -134,7 +134,7 @@ class:border-green-400={status === 'added' && side === 'left'} class:border-gray-200={status === 'same'} > - "{key}""{key}": {#if isPrimitive(value)} diff --git a/src/lib/components/JsonTree.svelte b/src/lib/components/JsonTree.svelte index ca1234d..5a8e1ad 100644 --- a/src/lib/components/JsonTree.svelte +++ b/src/lib/components/JsonTree.svelte @@ -9,6 +9,7 @@ isPrimitive, sortEntries } from '$lib/utils/jsonCompare'; + import { comparatorState } from '$lib/panelState.svelte'; interface Props { value: unknown; @@ -26,6 +27,7 @@ currentPath?: string; syncedExpandedPaths?: Set; onToggle?: (path: string, expanded: boolean) => void; + numericTolerancePercent?: number; } const CHUNK_SIZE = 50; @@ -47,7 +49,8 @@ matchingPaths = new Set(), currentPath = '', syncedExpandedPaths, - onToggle + onToggle, + numericTolerancePercent = 0 }: Props = $props(); function matchesSearch(text: string): boolean { @@ -145,12 +148,28 @@ : false ); + function normalizeKeyPath(path: string): string { + return path.replace(/\[\d+\]/g, '.*'); + } + + const watchedKeySet = $derived( + new Set( + comparatorState.watchedKeysInput + .split(',') + .map((k) => k.trim()) + .filter((k) => k.length > 0) + .map((k) => normalizeKeyPath(k)) + ) + ); + + const isWatched = $derived(!!currentPath && watchedKeySet.has(normalizeKeyPath(currentPath))); + const isPrimitiveValue = $derived(isPrimitive(value)); const status = $derived( skipComparison ? 'same' : isPrimitiveValue || expanded || level === 0 - ? getDiffStatus(value, otherValue, side, ignoredKeys) + ? getDiffStatus(value, otherValue, side, ignoredKeys, numericTolerancePercent) : 'same' ); const hasDiff = $derived(status !== 'same' && !isReference); @@ -158,7 +177,7 @@ const diffCount = $derived( skipComparison || expanded || isReference || isPrimitiveValue ? 0 - : countDifferences(value, otherValue, ignoredKeys) + : countDifferences(value, otherValue, ignoredKeys, 999, numericTolerancePercent) ); const arrayItems = $derived(isArray(value) ? value : []); @@ -279,7 +298,7 @@ valueMatchesSearch ? 'bg-yellow-50/30 ring-2 ring-yellow-400/50 dark:bg-yellow-900/20' : ''} {currentPath && matchingPaths.has(currentPath) - ? 'bg-indigo-100/50 ring-2 ring-indigo-500/50 dark:bg-indigo-900/30' + ? 'bg-green-100/50 ring-2 ring-green-500/50 dark:bg-green-900/30' : ''}" > {#if isArray(value) || isObject(value)} @@ -290,7 +309,43 @@ {:else} - + + {/if} + + {#if currentPath} + {/if} @@ -353,7 +408,7 @@ e.stopPropagation(); expandAllChildren(); }} - class="flex items-center gap-1 rounded-md border border-emerald-200 bg-emerald-50 px-2 py-0.5 text-[10px] font-medium text-emerald-700 transition-colors hover:bg-emerald-100 dark:border-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400 dark:hover:bg-emerald-900/50" + class="flex items-center gap-1 rounded-md border border-green-200 bg-green-50 px-2 py-0.5 text-[10px] font-medium text-green-700 transition-colors hover:bg-green-100 dark:border-green-800 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50" title="Expand all items" > {/each} {#if hasMoreItems} @@ -499,6 +555,7 @@ currentPath={currentPath ? `${currentPath}.${key}` : key} {syncedExpandedPaths} {onToggle} + {numericTolerancePercent} /> {/each} {#if hasMoreEntries} diff --git a/src/lib/components/JsonViewer.svelte b/src/lib/components/JsonViewer.svelte index f4653bf..3f9947c 100644 --- a/src/lib/components/JsonViewer.svelte +++ b/src/lib/components/JsonViewer.svelte @@ -12,6 +12,7 @@ matchingPaths?: Set; syncedExpandedPaths?: Set; onToggle?: (path: string, expanded: boolean) => void; + numericTolerancePercent?: number; } let { @@ -22,7 +23,8 @@ searchQuery = '', matchingPaths = new Set(), syncedExpandedPaths, - onToggle + onToggle, + numericTolerancePercent = 0 }: Props = $props(); let globalExpand = $state(null); @@ -145,6 +147,7 @@ {matchingPaths} {syncedExpandedPaths} {onToggle} + {numericTolerancePercent} /> {:else}
No JSON data
diff --git a/src/lib/components/KeyLogViewer.svelte b/src/lib/components/KeyLogViewer.svelte index 232aa6f..7dd4f87 100644 --- a/src/lib/components/KeyLogViewer.svelte +++ b/src/lib/components/KeyLogViewer.svelte @@ -1,11 +1,11 @@