diff --git a/apps/desktop/src/components/ImageDiff.svelte b/apps/desktop/src/components/ImageDiff.svelte
new file mode 100644
index 0000000000..fbc2244fc5
--- /dev/null
+++ b/apps/desktop/src/components/ImageDiff.svelte
@@ -0,0 +1,197 @@
+
+
+{#if loadError}
+
+
+ {#snippet caption()}
+ Can't preview this file type
+ {/snippet}
+
+
+{:else}
+
+{/if}
+
+
diff --git a/apps/desktop/src/components/UnifiedDiffView.svelte b/apps/desktop/src/components/UnifiedDiffView.svelte
index 452c3729d1..94b596b858 100644
--- a/apps/desktop/src/components/UnifiedDiffView.svelte
+++ b/apps/desktop/src/components/UnifiedDiffView.svelte
@@ -1,5 +1,6 @@
+
+{#snippet imageDimensions(metadata: ImageMetadata | null)}
+ {#if metadata}
+
+ {metadata.width}×{metadata.height}
+ {#if metadata.size}
+ · {formatFileSize(metadata.size)}
+ {/if}
+
+ {/if}
+{/snippet}
+
+{#snippet sizeDifference()}
+ {#if beforeImageMetadata?.size && afterImageMetadata?.size}
+ ·
+ beforeImageMetadata.size}
+ >
+ {formatSizeDifference(beforeImageMetadata.size, afterImageMetadata.size)}
+ {#if afterImageMetadata.size < beforeImageMetadata.size}
+ ↘
+ {:else if afterImageMetadata.size > beforeImageMetadata.size}
+ ↗
+ {/if}
+
+ {/if}
+{/snippet}
+
+{#snippet comparisonFooter()}
+
+{/snippet}
+
+{#snippet imagePanel(props: {
+ url: string;
+ label: string;
+ isBefore?: boolean;
+ metadata: ImageMetadata | null;
+})}
+
+
+

+
+
+
+
+{/snippet}
+
+{#snippet swipe(props: { controlValue: number; onValueChange: (value: number) => void })}
+
+
+
+

+
+
+

+
+
+
+ {@render comparisonFooter()}
+
+{/snippet}
+
+{#snippet onionSkin(props: { controlValue: number; onValueChange: (value: number) => void })}
+
+
+
+

+
+
+

+
+
+
+ Before
+
+ After
+
+ {@render comparisonFooter()}
+
+{/snippet}
+
+
+ {#if beforeImageUrl && afterImageUrl && !isLoading}
+
+ (viewMode = id as ViewMode)}>
+ 2-up
+ Swipe
+ Onion Skin
+
+
+ {/if}
+
+
+ {#if isLoading}
+
+
+
+
+
+
+
+
+ {:else if beforeImageUrl || afterImageUrl}
+ {#if viewMode === '2-up'}
+ {#if beforeImageUrl}
+ {@render imagePanel({
+ url: beforeImageUrl,
+ label: afterImageUrl ? 'Before' : 'Removed',
+ isBefore: true,
+ metadata: beforeImageMetadata
+ })}
+ {/if}
+
+ {#if afterImageUrl}
+ {@render imagePanel({
+ url: afterImageUrl,
+ label: beforeImageUrl ? 'After' : 'Added',
+ metadata: afterImageMetadata
+ })}
+ {/if}
+ {:else if viewMode === 'swipe' && beforeImageUrl && afterImageUrl}
+ {@render swipe({
+ controlValue: swipePosition,
+ onValueChange: (value) => (swipePosition = value)
+ })}
+ {:else if viewMode === 'onion-skin' && beforeImageUrl && afterImageUrl}
+ {@render onionSkin({
+ controlValue: onionOpacity,
+ onValueChange: (value) => (onionOpacity = value)
+ })}
+ {/if}
+ {/if}
+
+
+
+
diff --git a/packages/ui/src/lib/components/RangeInput.svelte b/packages/ui/src/lib/components/RangeInput.svelte
index 2eb6ca4458..a0c27a768b 100644
--- a/packages/ui/src/lib/components/RangeInput.svelte
+++ b/packages/ui/src/lib/components/RangeInput.svelte
@@ -40,8 +40,8 @@
onchange
}: Props = $props();
- // Calculate the percentage for the fill effect
- let percentage = $derived(((value - min) / (max - min)) * 100);
+ // Calculate the fill ratio (0-1)
+ let fillRatio = $derived((value - min) / (max - min));
{/if}
-
-
-
-
{
- oninput?.(parseFloat(e.currentTarget.value));
- }}
- onchange={(e) => {
- onchange?.(parseFloat(e.currentTarget.value));
- }}
- />
-
+
{
+ oninput?.(parseFloat(e.currentTarget.value));
+ }}
+ onchange={(e) => {
+ onchange?.(parseFloat(e.currentTarget.value));
+ }}
+ />
{#if error}
{error}
@@ -99,11 +92,10 @@
diff --git a/packages/ui/src/lib/components/SkeletonBone.svelte b/packages/ui/src/lib/components/SkeletonBone.svelte
index 2314a50055..26d24228cd 100644
--- a/packages/ui/src/lib/components/SkeletonBone.svelte
+++ b/packages/ui/src/lib/components/SkeletonBone.svelte
@@ -12,7 +12,7 @@
height = '1rem',
radius = 'var(--radius-ml)',
color = 'var(--clr-scale-ntrl-70)',
- opacity = 0.35
+ opacity = 0.34
}: Props = $props();
@@ -35,7 +35,7 @@
opacity: var(--opacity-value);
}
100% {
- opacity: calc(var(--opacity-value) + var(--opacity-value) * 0.5);
+ opacity: calc(var(--opacity-value) + var(--opacity-value) * 0.7);
}
}
diff --git a/packages/ui/src/lib/index.ts b/packages/ui/src/lib/index.ts
index c93213726a..167feb8f59 100644
--- a/packages/ui/src/lib/index.ts
+++ b/packages/ui/src/lib/index.ts
@@ -18,6 +18,7 @@ export { default as EditorLogo } from '$components/EditorLogo.svelte';
export { default as EmptyStatePlaceholder } from '$components/EmptyStatePlaceholder.svelte';
export { default as HunkDiff, type LineClickParams } from '$components/hunkDiff/HunkDiff.svelte';
export { default as Icon, type IconName } from '$components/Icon.svelte';
+export { default as ImageDiff } from '$components/ImageDiff.svelte';
export { default as InfoButton } from '$components/InfoButton.svelte';
export { default as InfoMessage, type MessageStyle } from '$components/InfoMessage.svelte';
export {
diff --git a/packages/ui/src/stories/components/ImageDiff.stories.svelte b/packages/ui/src/stories/components/ImageDiff.stories.svelte
new file mode 100644
index 0000000000..bf61a3bd7f
--- /dev/null
+++ b/packages/ui/src/stories/components/ImageDiff.stories.svelte
@@ -0,0 +1,64 @@
+
+
+
+ {#snippet template(args)}
+
+ {/snippet}
+
+
+
+ {#snippet template(args)}
+
+ {/snippet}
+
+
+
+ {#snippet template(args)}
+
+ {/snippet}
+
+
+
+ {#snippet template(args)}
+
+ {/snippet}
+