diff --git a/package.json b/package.json index 83feebd36..cde91d1c5 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,9 @@ "@ai-sdk/svelte": "^1.1.24", "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@f21fc7f", "@appwrite.io/pink-icons": "0.25.0", - "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc", + "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@957a779", "@appwrite.io/pink-legacy": "^1.0.3", - "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc", + "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@957a779", "@faker-js/faker": "^9.9.0", "@neodrag/svelte": "^2.3.3", "@popperjs/core": "^2.11.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7a9af9ae..2d3be5ae0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,14 +18,14 @@ importers: specifier: 0.25.0 version: 0.25.0 '@appwrite.io/pink-icons-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@957a779 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@957a779(svelte@5.25.3) '@appwrite.io/pink-legacy': specifier: ^1.0.3 version: 1.0.3 '@appwrite.io/pink-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@957a779 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@957a779(svelte@5.25.3) '@faker-js/faker': specifier: ^9.9.0 version: 9.9.0 @@ -284,8 +284,8 @@ packages: peerDependencies: svelte: ^4.0.0 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc} + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@957a779': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@957a779} version: 2.0.0-RC.1 peerDependencies: svelte: ^4.0.0 @@ -299,8 +299,8 @@ packages: '@appwrite.io/pink-legacy@1.0.3': resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==} - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc} + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@957a779': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@957a779} version: 2.0.0-RC.2 peerDependencies: svelte: ^4.0.0 @@ -3837,7 +3837,7 @@ snapshots: dependencies: svelte: 5.25.3 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc(svelte@5.25.3)': + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@957a779(svelte@5.25.3)': dependencies: svelte: 5.25.3 @@ -3850,7 +3850,7 @@ snapshots: '@appwrite.io/pink-icons': 1.0.0 the-new-css-reset: 1.11.3 - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc(svelte@5.25.3)': + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@957a779(svelte@5.25.3)': dependencies: '@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3) '@floating-ui/dom': 1.6.13 diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/+page.svelte b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/+page.svelte index 56216a755..311736556 100644 --- a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/editor/+page.svelte @@ -3,7 +3,7 @@ import { Container } from '$lib/layout'; import { sdk } from '$lib/stores/sdk'; import { ImageFormat, Query, type Models } from '@appwrite.io/console'; - import { Layout, Typography } from '@appwrite.io/pink-svelte'; + import { Layout, Typography, Canvas } from '@appwrite.io/pink-svelte'; import { onMount } from 'svelte'; import { CopyInput } from '$lib/components'; import ImageGrid from './components/imageGrid.svelte'; @@ -14,6 +14,7 @@ generateTransformationParams, type TransformationState } from '$lib/helpers/imageTransformations'; + import type { CanvasObjectUnion } from '@appwrite.io/pink-svelte/dist/canvas/index.js'; // Import type explicitly if needed or infer // UI State let activeTab = $state<'design' | 'code'>('design'); @@ -52,15 +53,95 @@ let appliedPresets = $state>({}); // fileId -> presetId // Canvas state - let canvasContainer = $state(); - let imageElement = $state(); - let isResizing = $state(false); - let resizeHandle = $state(null); - let startX = $state(0); - let startY = $state(0); - let startWidth = $state(0); - let startHeight = $state(0); let focalPointOverlay = $state(null); + let canvasCore = $state(null); + let currentZoom = $state(1); + + let canvasObjects = $state([]); + + // Force selection of the main image and ensure handles are visible + $effect(() => { + if (canvasCore && selectedFile && canvasObjects.length > 0) { + // Use a small timeout to ensure the canvas has initialized properly + setTimeout(() => { + canvasCore.selectObject('main-image'); + }, 0); + } + }); + + $effect(() => { + if (!canvasCore) return; + const unsubscribe = canvasCore.zoom.subscribe((z: number) => { + currentZoom = z; + zoom = Math.round(z * 100); + }); + return unsubscribe; + }); + // Sync init + $effect(() => { + if (selectedFile) { + const existing = canvasObjects.find((o) => o.id === 'main-image'); + if (!existing) { + // Initialize + canvasObjects = [ + { + id: 'main-image', + type: 'image', + x: 0, + y: 0, + width: transformationState.width || 700, + height: transformationState.height || 438, + rotation: transformationState.rotation || 0, + src: previewUrl, // Use current preview URL + alt: selectedFile.name, + maintainAspectRatio: transformationState.aspectRatioLocked, + selected: true, + // Ensure optional props are set + cropX: 0, + cropY: 0 + } + ]; + // Center the view on init + setTimeout(() => { + canvasCore?.panTo(0, 0); + canvasCore?.selectObject('main-image'); + }, 100); + } else { + // Only update if dimensions drastically mismatch initial load (prevent loops) + // or if rotation changes from external source (like input) + // But wait, if we bind, we should let Canvas update objects. + // We should only push to objects if the user manually types in the inputs. + // The issue is distinguishing input change vs drag change. + // We can check document.activeElement? + } + } + }); + + // Update transformationState when canvasObjects change (drag/resize) + $effect(() => { + const obj = canvasObjects.find((o) => o.id === 'main-image'); + if (obj) { + // Only update if not currently editing inputs? + // Actually, the binding should handle it if we are consistent. + transformationState.width = Math.round(obj.width); + transformationState.height = Math.round(obj.height); + if (obj.rotation !== undefined) transformationState.rotation = obj.rotation; + } + }); + + // When transformationState changes (e.g. user input), update canvas object + // We need to avoid loops. + $effect(() => { + const obj = canvasObjects.find((o) => o.id === 'main-image'); + if (obj) { + if (obj.width !== transformationState.width) obj.width = transformationState.width; + if (obj.height !== transformationState.height) obj.height = transformationState.height; + if (obj.rotation !== transformationState.rotation) + obj.rotation = transformationState.rotation; + if (obj.maintainAspectRatio !== transformationState.aspectRatioLocked) + obj.maintainAspectRatio = transformationState.aspectRatioLocked; + } + }); onMount(async () => { try { @@ -104,9 +185,17 @@ .toString() + '&mode=admin'; } - function getPreviewUrl(): string { + function getPreviewUrl(forCanvas = false): string { if (!selectedFile) return ''; const params = generateTransformationParams(transformationState); + + // For canvas preview, we want to omit width/height so the browser handles scaling + // This prevents "zooming"/flashing artifacts during resize drag + if (forCanvas) { + delete params.width; + delete params.height; + } + const previewParams: any = { bucketId: selectedFile.bucketId, fileId: selectedFile.$id, @@ -120,7 +209,8 @@ ); } - let previewUrl = $derived(getPreviewUrl()); + let previewUrl = $derived(getPreviewUrl(true)); // For canvas + let downloadUrl = $derived(getPreviewUrl(false)); // For final output/code // Watch for file selection changes and apply presets let lastSelectedFileId = $state(null); @@ -141,8 +231,15 @@ }); function handleFocalPointClick(event: MouseEvent) { - if (!canvasContainer || !selectedFile || isResizing) return; - const rect = canvasContainer.getBoundingClientRect(); + if (!selectedFile) return; + + const target = event.target as HTMLElement; + if (target.closest('.resize-handle')) return; + + const objectEl = target.closest('.canvas-object'); + if (!objectEl) return; + + const rect = objectEl.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; const width = rect.width; @@ -167,55 +264,6 @@ setTimeout(() => (focalPointOverlay = null), 1000); } - function handleResizeStart(event: MouseEvent, handle: string) { - event.stopPropagation(); - isResizing = true; - resizeHandle = handle; - startX = event.clientX; - startY = event.clientY; - startWidth = transformationState.width || 0; - startHeight = transformationState.height || 0; - } - - function handleMouseMove(event: MouseEvent) { - if (!isResizing || !resizeHandle) return; - const dx = event.clientX - startX; - const dy = event.clientY - startY; - const scale = zoom / 100; - - let newWidth = startWidth; - let newHeight = startHeight; - - if (resizeHandle.includes('e')) { - newWidth = Math.max(1, startWidth + dx / scale); - } - if (resizeHandle.includes('w')) { - newWidth = Math.max(1, startWidth - dx / scale); - } - if (resizeHandle.includes('s')) { - newHeight = Math.max(1, startHeight + dy / scale); - } - if (resizeHandle.includes('n')) { - newHeight = Math.max(1, startHeight - dy / scale); - } - - if (transformationState.aspectRatioLocked && transformationState.originalAspectRatio) { - if (resizeHandle.includes('e') || resizeHandle.includes('w')) { - newHeight = Math.round(newWidth / transformationState.originalAspectRatio); - } else { - newWidth = Math.round(newHeight * transformationState.originalAspectRatio); - } - } - - transformationState.width = newWidth; - transformationState.height = newHeight; - } - - function handleMouseUp() { - isResizing = false; - resizeHandle = null; - } - function handlePresetSelected(presetId: string | null) { selectedPresetId = presetId; if (presetId && selectedFile) { @@ -302,13 +350,13 @@
- +
-
+
@@ -317,72 +365,61 @@
-
{ - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleFocalPointClick(e as any); + + + -
- {selectedFile.name} - -
-
-
-
-
-
- - {#if focalPointOverlay || transformationState.gravity} - {@const pos = getFocalPointPosition()} -
-
- {/if} - -
- - - - -
+ showGrid={true} + width="100%" + height="100%" + bind:objects={canvasObjects}> + +
+ {#each canvasObjects as obj (obj.id)} + {#if obj.type === 'image'} + + {:else if obj.type === 'shape'} + + {/if} + {/each}
+ + + {#if focalPointOverlay || transformationState.gravity} + {#each canvasObjects as obj (obj.id)} + {#if obj.type === 'image' && obj.id === 'main-image'} + {@const pos = getFocalPointPosition()} +
+
+
+
+ {/if} + {/each} + {/if} + + +
{transformationState.width || 0} × {transformationState.height || 0} @@ -478,17 +515,6 @@ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } - .preview-container { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - position: relative; - padding: 2rem; - overflow: auto; - } - .preview-wrapper { position: relative; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); @@ -498,37 +524,10 @@ .preview-image { display: block; max-width: 100%; - max-height: 70vh; border-radius: var(--border-radius-small); user-select: none; } - .grid-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: none; - } - - .grid-line { - position: absolute; - background: rgba(255, 255, 255, 0.3); - } - - .grid-line-v { - top: 0; - bottom: 0; - width: 1px; - } - - .grid-line-h { - left: 0; - right: 0; - height: 1px; - } - .focal-overlay { position: absolute; background: rgba(59, 130, 246, 0.3); @@ -537,44 +536,39 @@ transition: all 0.2s; } - .resize-handles { + .overlay-controls { position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + bottom: 1rem; + left: 1rem; + right: 1rem; pointer-events: none; - } - - .handle { - position: absolute; - width: 12px; - height: 12px; - background: var(--color-primary-100); - border: 2px solid white; - border-radius: 50%; - pointer-events: all; + display: flex; + justify-content: space-between; + align-items: flex-end; z-index: 10; } - .handle-nw { - top: -6px; - left: -6px; + /* Fallback styles for resize handles to ensure visibility */ + /* Fallback styles for resize handles to ensure visibility */ + :global(.resize-handle) { + width: 10px !important; + height: 10px !important; + background-color: var(--color-primary-100, #00d9ff) !important; + border: 1px solid blue !important; + z-index: 20 !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) !important; + /* Inverse scale to keep constant size */ + transform: scale(calc(1 / var(--zoom, 1))) !important; + transform-origin: center !important; } - .handle-ne { - top: -6px; - right: -6px; + :global(.resize-handle:hover) { + /* Scale up slightly from the base inverse scale */ + transform: scale(calc(1.2 / var(--zoom, 1))) !important; } - .handle-sw { - bottom: -6px; - left: -6px; - } - - .handle-se { - bottom: -6px; - right: -6px; + .overlay-controls > * { + pointer-events: auto; } /* svelte-ignore css_unused_selector */ @@ -611,11 +605,6 @@ color: var(--color-neutral-70); } - .handle { - border: none; - padding: 0; - } - .controls-section { display: flex; flex-direction: column;