feat: start implement image upload and cropper

This commit is contained in:
2026-02-06 09:10:33 +05:00
parent 5244de7cc0
commit 070b49cbad
8 changed files with 203 additions and 31 deletions
+2
View File
@@ -8,6 +8,7 @@ node_modules
/.svelte-kit
/build
/coverage
/.VSCodeCounter
# OS
.DS_Store
@@ -22,3 +23,4 @@ Thumbs.db
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+108
View File
@@ -0,0 +1,108 @@
<script lang="ts">
let active = $state(false);
</script>
<div class="crop-canvas-container">
<canvas class="crop-canvas"></canvas>
<div class="crop-box" class:active>
<div class="crop-handle nw"></div>
<div class="crop-handle ne"></div>
<div class="crop-handle sw"></div>
<div class="crop-handle se"></div>
<div class="crop-handle n"></div>
<div class="crop-handle s"></div>
<div class="crop-handle w"></div>
<div class="crop-handle e"></div>
</div>
</div>
<style>
.crop-canvas-container {
position: relative;
width: 100%;
aspect-ratio: 16 / 5;
background: var(--bg-secondary);
border-radius: var(--radius);
overflow: hidden;
cursor: crosshair;
}
.crop-canvas {
width: 100%;
height: 100%;
display: block;
}
.crop-box {
position: absolute;
border: 2px solid var(--accent-primary);
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
cursor: move;
display: none;
}
.crop-box.active {
display: block;
}
.crop-handle {
position: absolute;
width: 10px;
height: 10px;
background: white;
border: 2px solid var(--accent-primary);
border-radius: 50%;
}
.crop-handle.nw {
top: -5px;
left: -5px;
cursor: nw-resize;
}
.crop-handle.ne {
top: -5px;
right: -5px;
cursor: ne-resize;
}
.crop-handle.sw {
bottom: -5px;
left: -5px;
cursor: sw-resize;
}
.crop-handle.se {
bottom: -5px;
right: -5px;
cursor: se-resize;
}
.crop-handle.n {
top: -5px;
left: 50%;
transform: translateX(-50%);
cursor: n-resize;
}
.crop-handle.s {
bottom: -5px;
left: 50%;
transform: translateX(-50%);
cursor: s-resize;
}
.crop-handle.w {
left: -5px;
top: 50%;
transform: translateY(-50%);
cursor: w-resize;
}
.crop-handle.e {
right: -5px;
top: 50%;
transform: translateY(-50%);
cursor: e-resize;
}
</style>
+48
View File
@@ -0,0 +1,48 @@
<script lang="ts">
import Card from "$components/layout/Card.svelte";
import SettingsGrid from "$components/layout/SettingsGrid.svelte";
import SettingsRow from "$components/layout/SettingsRow.svelte";
import Button from "$components/ui/Button.svelte";
import IconEdit from "$components/ui/Icons/IconEdit.svelte";
import IconReset from "$components/ui/Icons/IconReset.svelte";
import IconUpload from "$components/ui/Icons/IconUpload.svelte";
import RangeSlider from "$components/ui/RangeSlider.svelte";
import CropInline from "./CropInline.svelte";
let brightness = $state(100);
let contrast = $state(100);
</script>
<Card title="Фоновое изображение">
<div class="crop-editor">
<CropInline />
<div class="crop-controls">
<Button label="Загрузить" icon={IconUpload} type="secondary" />
<Button label="Редактировать" icon={IconEdit} type="outline" />
<Button label="Сбросить" icon={IconReset} type="outline" />
</div>
<SettingsGrid>
<SettingsRow label="Яркость">
<RangeSlider bind:value={brightness} />
</SettingsRow>
<SettingsRow label="Контраст">
<RangeSlider bind:value={contrast} min={50} max={150} />
</SettingsRow>
</SettingsGrid>
</div>
</Card>
<style>
.crop-editor {
display: flex;
flex-direction: column;
gap: 12px;
}
.crop-controls {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
</style>
-4
View File
@@ -3,10 +3,6 @@
import IconSun from "$components/ui/Icons/IconSun.svelte";
import { themeState } from "$states/theme.svelte";
interface Props {}
let {}: Props = $props();
function toggleTheme() {
themeState.toggle();
}
+4 -25
View File
@@ -1,32 +1,11 @@
<script lang="ts">
import Badge from "$components/ui/Badge.svelte";
import Button from "$components/ui/Button.svelte";
import IconArrowLeft from "$components/ui/Icons/IconArrowLeft.svelte";
import IconArrowRight from "$components/ui/Icons/IconArrowRight.svelte";
import IconUpload from "$components/ui/Icons/IconUpload.svelte";
import Card from "./Card.svelte";
import ImageManager from "$components/image/ImageManager.svelte";
import PanelPreview from "$components/panel/PanelPreview.svelte";
</script>
<div class="panel-bar">
<Card title="Фоновое изображение">
<Button label="Загрузить изображение" icon={IconUpload} onclick={() => {}} type="secondary" />
</Card>
{#snippet panelTitle()}
Панели <Badge text="0" />
{/snippet}
{#snippet panelControls()}
<button id="prevPanel" class="nav-btn" disabled>
<IconArrowLeft />
</button>
<span id="panelIndicator" class="panel-indicator">0 / 0</span>
<button id="nextPanel" class="nav-btn" disabled>
<IconArrowRight />
</button>
{/snippet}
<Card title={panelTitle} titleSnippet={panelControls}>Панели</Card>
<ImageManager />
<PanelPreview />
</div>
<style>
+25
View File
@@ -0,0 +1,25 @@
<script lang="ts">
import Card from "$components/layout/Card.svelte";
import Badge from "$components/ui/Badge.svelte";
import IconArrowLeft from "$components/ui/Icons/IconArrowLeft.svelte";
import IconArrowRight from "$components/ui/Icons/IconArrowRight.svelte";
</script>
{#snippet panelTitle()}
Панели <Badge text="0" />
{/snippet}
{#snippet panelControls()}
<button id="prevPanel" class="nav-btn" disabled>
<IconArrowLeft />
</button>
<span id="panelIndicator" class="panel-indicator">0 / 0</span>
<button id="nextPanel" class="nav-btn" disabled>
<IconArrowRight />
</button>
{/snippet}
<Card title={panelTitle} titleSnippet={panelControls}>Панели</Card>
<style>
</style>
+2 -2
View File
@@ -4,13 +4,13 @@
interface Props {
icon: Component;
onclick: MouseEventHandler<HTMLButtonElement>;
onclick?: MouseEventHandler<HTMLButtonElement>;
disabled?: boolean;
label?: string;
type?: "primary" | "secondary" | "danger" | "outline";
}
let { icon: Icon, onclick, disabled = false, label = "", type = "primary" }: Props = $props();
let { icon: Icon, onclick = () => {}, disabled = false, label = "", type = "primary" }: Props = $props();
</script>
<button
+14
View File
@@ -0,0 +1,14 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path
d="M21.2799 6.40005L11.7399 15.94C10.7899 16.89 7.96987 17.33 7.33987 16.7C6.70987 16.07 7.13987 13.25 8.08987 12.3L17.6399 2.75002C17.8754 2.49308 18.1605 2.28654 18.4781 2.14284C18.7956 1.99914 19.139 1.92124 19.4875 1.9139C19.8359 1.90657 20.1823 1.96991 20.5056 2.10012C20.8289 2.23033 21.1225 2.42473 21.3686 2.67153C21.6147 2.91833 21.8083 3.21243 21.9376 3.53609C22.0669 3.85976 22.1294 4.20626 22.1211 4.55471C22.1128 4.90316 22.0339 5.24635 21.8894 5.5635C21.7448 5.88065 21.5375 6.16524 21.2799 6.40005V6.40005Z"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M11 4H6C4.93913 4 3.92178 4.42142 3.17163 5.17157C2.42149 5.92172 2 6.93913 2 8V18C2 19.0609 2.42149 20.0783 3.17163 20.8284C3.92178 21.5786 4.93913 22 6 22H17C19.21 22 20 20.2 20 18V13"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

After

Width:  |  Height:  |  Size: 1001 B