fix: remove old components
This commit is contained in:
@@ -1,24 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
interface Props {
|
|
||||||
errorMessage: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { errorMessage }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if errorMessage}
|
|
||||||
<div class="error-message">
|
|
||||||
{errorMessage}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.error-message {
|
|
||||||
background: #ffebee;
|
|
||||||
color: #c62828;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #ffcdd2;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Button from "$components/ui/Button.svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
backgroundImage?: string | undefined;
|
|
||||||
onUploadNewImage: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { backgroundImage, onUploadNewImage }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if backgroundImage}
|
|
||||||
<div class="background-preview">
|
|
||||||
<div class="background-header">
|
|
||||||
<h3>Фоновое изображение</h3>
|
|
||||||
<Button variant="secondary" size="sm" onclick={onUploadNewImage}>Загрузить</Button>
|
|
||||||
</div>
|
|
||||||
<img src={backgroundImage} alt="Фон" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.background-preview {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.background-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.background-preview h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.background-preview img {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 320px;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Button from "$components/ui/Button.svelte";
|
|
||||||
import { PANEL_SETTINGS } from "$lib/constants";
|
|
||||||
import type { ImageCropResult } from "$lib/types/panel";
|
|
||||||
import { setLoading } from "$stores/uiStore";
|
|
||||||
import CropperJS from "cropperjs";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
imageSrc: string;
|
|
||||||
onCropComplete: (croppedImage: string) => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { imageSrc, onCropComplete, onCancel }: Props = $props();
|
|
||||||
|
|
||||||
let imageElement: HTMLImageElement;
|
|
||||||
let cropper: CropperJS | undefined = undefined;
|
|
||||||
let isProcessing = $state(false);
|
|
||||||
let errorMessage = $state<string | undefined>(undefined);
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
initializeCropper();
|
|
||||||
return () => {
|
|
||||||
if (cropper) {
|
|
||||||
cropper.destroy();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function initializeCropper() {
|
|
||||||
if (!imageElement || !CropperJS) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cropper = new CropperJS(imageElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCrop() {
|
|
||||||
if (!cropper) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
isProcessing = true;
|
|
||||||
errorMessage = undefined;
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const cropperCanvasElement = cropper.getCropperCanvas();
|
|
||||||
|
|
||||||
if (!cropperCanvasElement) {
|
|
||||||
throw new Error("Не удалось получить обрезанное изображение");
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvas = await cropperCanvasElement.$toCanvas({
|
|
||||||
width: PANEL_SETTINGS.PANEL_WIDTH,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!canvas) {
|
|
||||||
throw new Error("Не удалось получить canvas");
|
|
||||||
}
|
|
||||||
|
|
||||||
const croppedImage = canvas.toDataURL("image/png");
|
|
||||||
|
|
||||||
const cropResult: ImageCropResult = {
|
|
||||||
success: true,
|
|
||||||
croppedImage,
|
|
||||||
};
|
|
||||||
|
|
||||||
onCropComplete(croppedImage);
|
|
||||||
} catch (error) {
|
|
||||||
errorMessage = error instanceof Error ? error.message : "Ошибка обрезки изображения";
|
|
||||||
} finally {
|
|
||||||
isProcessing = false;
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCancel() {
|
|
||||||
if (cropper) {
|
|
||||||
cropper.destroy();
|
|
||||||
}
|
|
||||||
onCancel();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="cropper-container">
|
|
||||||
<div class="cropper-wrapper">
|
|
||||||
<img bind:this={imageElement} src={imageSrc} alt="" class="cropper-image" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if errorMessage}
|
|
||||||
<div class="error-message">
|
|
||||||
<strong>Ошибка:</strong>
|
|
||||||
{errorMessage}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="cropper-actions">
|
|
||||||
<Button variant="secondary" onclick={handleCancel} disabled={isProcessing}>Отмена</Button>
|
|
||||||
<Button variant="primary" onclick={handleCrop} disabled={isProcessing} loading={isProcessing}>Применить</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cropper-info">
|
|
||||||
<p>💡 Изображение будет обрезано до ширины 320px</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.cropper-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-wrapper {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 600px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-image {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 1rem;
|
|
||||||
background: #f9f9f9;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cropper-info {
|
|
||||||
text-align: center;
|
|
||||||
color: #666;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
background: #ffebee;
|
|
||||||
color: #c62828;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid #ffcdd2;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cropper-view-box),
|
|
||||||
:global(.cropper-face) {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cropper-line) {
|
|
||||||
background-color: rgba(0, 123, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cropper-point) {
|
|
||||||
background-color: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cropper-container) {
|
|
||||||
direction: ltr;
|
|
||||||
font-size: 0;
|
|
||||||
line-height: 0;
|
|
||||||
position: relative;
|
|
||||||
touch-action: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cropper-container img) {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
image-orientation: 0deg;
|
|
||||||
max-height: none !important;
|
|
||||||
max-width: none !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cropper-wrap-box, .cropper-canvas, .cropper-drag-box, .cropper-crop-box, .cropper-modal) {
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cropper-wrap-box, .cropper-canvas) {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cropper-drag-box) {
|
|
||||||
background-color: #fff;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cropper-modal) {
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.cropper-crop-box) {
|
|
||||||
border-color: rgba(0, 123, 255, 0.5);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import ImageUpload from "./ImageUpload.svelte";
|
|
||||||
import ImageCropper from "./ImageCropper.svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
currentStep: string;
|
|
||||||
uploadedImage: string | undefined;
|
|
||||||
onImageUpload: (image: string) => void;
|
|
||||||
onCropComplete: (croppedImage: string) => void;
|
|
||||||
onCropCancel: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { currentStep, uploadedImage, onImageUpload, onCropComplete, onCropCancel }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if currentStep === "upload"}
|
|
||||||
<ImageUpload onImageSelect={onImageUpload} />
|
|
||||||
{:else if currentStep === "crop" && uploadedImage}
|
|
||||||
<ImageCropper imageSrc={uploadedImage} {onCropComplete} onCancel={onCropCancel} />
|
|
||||||
{/if}
|
|
||||||
@@ -1,401 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Button from "$components/ui/Button.svelte";
|
|
||||||
import type { ImageUploadResult } from "$lib/types/panel";
|
|
||||||
import { handleError } from "$lib/utils/errorHandler";
|
|
||||||
import { clearError, setCurrentStep, setLoading, uiStore } from "$stores/uiStore";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import { imageService } from "../../services/imageService";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onImageSelect: (image: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { onImageSelect }: Props = $props();
|
|
||||||
|
|
||||||
let dropZone = $state<HTMLElement | undefined>(undefined);
|
|
||||||
let fileInput = $state<HTMLInputElement | undefined>(undefined);
|
|
||||||
let urlInput = $state<HTMLInputElement | undefined>(undefined);
|
|
||||||
let urlForm = $state<HTMLFormElement | undefined>(undefined);
|
|
||||||
|
|
||||||
let isDragOver = $state(false);
|
|
||||||
let showUrlInput = $state(false);
|
|
||||||
let uploadedImage = $state<string | undefined>(undefined);
|
|
||||||
let errorMessage = $state<string | undefined>(undefined);
|
|
||||||
|
|
||||||
const uiError = $derived($uiStore.error);
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
setupPasteHandler();
|
|
||||||
setupDragAndDrop();
|
|
||||||
});
|
|
||||||
|
|
||||||
function setupPasteHandler() {
|
|
||||||
document.addEventListener("paste", handlePaste);
|
|
||||||
return () => document.removeEventListener("paste", handlePaste);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupDragAndDrop() {
|
|
||||||
if (!dropZone) return;
|
|
||||||
|
|
||||||
dropZone!.addEventListener("dragover", handleDragOver);
|
|
||||||
dropZone!.addEventListener("dragleave", handleDragLeave);
|
|
||||||
dropZone!.addEventListener("drop", handleDrop);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
dropZone!.removeEventListener("dragover", handleDragOver);
|
|
||||||
dropZone!.removeEventListener("dragleave", handleDragLeave);
|
|
||||||
dropZone!.removeEventListener("drop", handleDrop);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDragOver(e: DragEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
isDragOver = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDragLeave(e: DragEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
isDragOver = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDrop(e: DragEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
isDragOver = false;
|
|
||||||
|
|
||||||
const files = e.dataTransfer?.files;
|
|
||||||
if (!files || files.length === 0) return;
|
|
||||||
|
|
||||||
const file = files[0];
|
|
||||||
await handleFileUpload(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleFileUpload(file: File) {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
clearError();
|
|
||||||
const result: ImageUploadResult = await imageService.handleFileUpload(file);
|
|
||||||
|
|
||||||
if (result.success && result.image) {
|
|
||||||
uploadedImage = result.image;
|
|
||||||
onImageSelect(result.image);
|
|
||||||
setCurrentStep("crop");
|
|
||||||
} else {
|
|
||||||
errorMessage = result.error || "Ошибка загрузки файла";
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
errorMessage = handleError(error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handlePaste(e: ClipboardEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
clearError();
|
|
||||||
|
|
||||||
const result: ImageUploadResult = await imageService.handlePasteUpload(e);
|
|
||||||
|
|
||||||
if (result.success && result.image) {
|
|
||||||
uploadedImage = result.image;
|
|
||||||
|
|
||||||
onImageSelect(result.image);
|
|
||||||
setCurrentStep("crop");
|
|
||||||
} else {
|
|
||||||
errorMessage = result.error || "Ошибка вставки изображения";
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
errorMessage = handleError(error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleUrlSubmit(e: Event) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!urlInput?.value.trim()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
clearError();
|
|
||||||
|
|
||||||
const result: ImageUploadResult = await imageService.handleUrlUpload(urlInput.value.trim());
|
|
||||||
|
|
||||||
if (result.success && result.image) {
|
|
||||||
uploadedImage = result.image;
|
|
||||||
|
|
||||||
onImageSelect(result.image);
|
|
||||||
setCurrentStep("crop");
|
|
||||||
showUrlInput = false;
|
|
||||||
} else {
|
|
||||||
errorMessage = result.error || "Ошибка загрузки изображения";
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
errorMessage = handleError(error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function triggerFileInput() {
|
|
||||||
fileInput?.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetUpload() {
|
|
||||||
uploadedImage = undefined;
|
|
||||||
errorMessage = undefined;
|
|
||||||
showUrlInput = false;
|
|
||||||
setCurrentStep("upload");
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="image-upload-container">
|
|
||||||
{#if uploadedImage}
|
|
||||||
<div class="uploaded-image-preview">
|
|
||||||
<img src={uploadedImage} alt="Uploaded preview" />
|
|
||||||
<div class="preview-actions">
|
|
||||||
<Button variant="secondary" onclick={resetUpload}>Загрузить другое</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div
|
|
||||||
bind:this={dropZone}
|
|
||||||
class="drop-zone {isDragOver ? 'drag-over' : ''} {uiError ? 'error' : ''}"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
onclick={triggerFileInput}
|
|
||||||
onkeydown={(e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
triggerFileInput();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="drop-zone-content">
|
|
||||||
<svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
||||||
<polyline points="7 10 12 15 17 10"></polyline>
|
|
||||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<h3>Загрузите изображение</h3>
|
|
||||||
<p>Перетащите файл сюда, нажмите для выбора, или вставьте через Ctrl+V</p>
|
|
||||||
|
|
||||||
<div class="upload-methods">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onclick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
triggerFileInput();
|
|
||||||
}}>Выбрать файл</Button
|
|
||||||
>
|
|
||||||
|
|
||||||
<Button variant="secondary" onclick={() => (showUrlInput = !showUrlInput)}>
|
|
||||||
{showUrlInput ? "Скрыть" : "По URL"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if showUrlInput}
|
|
||||||
<form bind:this={urlForm} onsubmit={handleUrlSubmit} class="url-input-form">
|
|
||||||
<input
|
|
||||||
bind:this={urlInput}
|
|
||||||
type="url"
|
|
||||||
placeholder="https://example.com/image.jpg"
|
|
||||||
required
|
|
||||||
class="url-input"
|
|
||||||
/>
|
|
||||||
<Button type="submit" variant="primary" disabled={$uiStore.isLoading} loading={$uiStore.isLoading}
|
|
||||||
>Загрузить</Button
|
|
||||||
>
|
|
||||||
</form>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="upload-tips">
|
|
||||||
<p>• Поддерживаемые форматы: JPG, PNG, WebP, GIF</p>
|
|
||||||
<p>• Максимальный размер: 10MB</p>
|
|
||||||
<p>• Или вставьте изображение через Ctrl+V</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
bind:this={fileInput}
|
|
||||||
type="file"
|
|
||||||
accept="image/jpeg,image/jpg,image/png,image/webp,image/gif"
|
|
||||||
style="display: none;"
|
|
||||||
onchange={(e) => {
|
|
||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
|
||||||
if (file) handleFileUpload(file);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#if errorMessage}
|
|
||||||
<div class="error-message">
|
|
||||||
<strong>Ошибка:</strong>
|
|
||||||
{errorMessage}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $uiStore.isLoading}
|
|
||||||
<div class="loading-overlay">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
<p>Загрузка...</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.image-upload-container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-zone {
|
|
||||||
border: 2px dashed #ccc;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
background: #f9f9f9;
|
|
||||||
min-height: 200px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-zone:hover {
|
|
||||||
border-color: #007bff;
|
|
||||||
background: #f0f8ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-zone.drag-over {
|
|
||||||
border-color: #007bff;
|
|
||||||
background: #e3f2fd;
|
|
||||||
transform: scale(1.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-zone.error {
|
|
||||||
border-color: #dc3545;
|
|
||||||
background: #ffebee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-zone-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-icon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-zone h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-zone p {
|
|
||||||
margin: 0;
|
|
||||||
color: #666;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-methods {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.url-input-form {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.url-input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-tips {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #666;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-tips p {
|
|
||||||
margin: 0.25rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uploaded-image-preview {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uploaded-image-preview img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 300px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-actions {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
background: #ffebee;
|
|
||||||
color: #c62828;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-top: 1rem;
|
|
||||||
border: 1px solid #ffcdd2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 4px solid #f3f3f3;
|
|
||||||
border-top: 4px solid #007bff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import ErrorMessage from "$components/feedback/ErrorMessage.svelte";
|
|
||||||
import AppContent from "$components/layout/AppContent.svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
errorMessage: string | undefined;
|
|
||||||
uploadedImage: string | undefined;
|
|
||||||
texts: Array<{ id: string; text: string }>;
|
|
||||||
backgroundImage: string | undefined;
|
|
||||||
panels: any[];
|
|
||||||
onUploadNewImage: () => void;
|
|
||||||
onImageUpload: (image: string) => void;
|
|
||||||
onCropComplete: (croppedImage: string) => void;
|
|
||||||
onCropCancel: () => void;
|
|
||||||
onTextAdd: (text: string) => void;
|
|
||||||
onTextUpdate: (id: string, newText: string) => void;
|
|
||||||
onTextDelete: (id: string) => void;
|
|
||||||
onDownload: (panel: any, konvaStage: any) => Promise<void>;
|
|
||||||
onDownloadAll: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
errorMessage,
|
|
||||||
uploadedImage,
|
|
||||||
texts,
|
|
||||||
backgroundImage,
|
|
||||||
panels,
|
|
||||||
onUploadNewImage,
|
|
||||||
onImageUpload,
|
|
||||||
onCropComplete,
|
|
||||||
onCropCancel,
|
|
||||||
onTextAdd,
|
|
||||||
onTextUpdate,
|
|
||||||
onTextDelete,
|
|
||||||
onDownload,
|
|
||||||
onDownloadAll,
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="app-container">
|
|
||||||
<ErrorMessage {errorMessage} />
|
|
||||||
|
|
||||||
<AppContent
|
|
||||||
{uploadedImage}
|
|
||||||
{texts}
|
|
||||||
{backgroundImage}
|
|
||||||
{panels}
|
|
||||||
{onImageUpload}
|
|
||||||
{onCropComplete}
|
|
||||||
{onCropCancel}
|
|
||||||
{onTextAdd}
|
|
||||||
{onTextUpdate}
|
|
||||||
{onTextDelete}
|
|
||||||
{onUploadNewImage}
|
|
||||||
{onDownload}
|
|
||||||
{onDownloadAll}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.app-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import MainSection from "$components/layout/MainSection.svelte";
|
|
||||||
import Sidebar from "$components/layout/Sidebar.svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
uploadedImage: string | undefined;
|
|
||||||
texts: Array<{ id: string; text: string }>;
|
|
||||||
backgroundImage: string | undefined;
|
|
||||||
panels: any[];
|
|
||||||
onImageUpload: (image: string) => void;
|
|
||||||
onCropComplete: (croppedImage: string) => void;
|
|
||||||
onCropCancel: () => void;
|
|
||||||
onTextAdd: (text: string) => void;
|
|
||||||
onTextUpdate: (id: string, newText: string) => void;
|
|
||||||
onTextDelete: (id: string) => void;
|
|
||||||
onUploadNewImage: () => void;
|
|
||||||
onDownload: (panel: any, konvaStage: any) => Promise<void>;
|
|
||||||
onDownloadAll: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
uploadedImage,
|
|
||||||
texts,
|
|
||||||
backgroundImage,
|
|
||||||
panels,
|
|
||||||
onImageUpload,
|
|
||||||
onCropComplete,
|
|
||||||
onCropCancel,
|
|
||||||
onTextAdd,
|
|
||||||
onTextUpdate,
|
|
||||||
onTextDelete,
|
|
||||||
onUploadNewImage,
|
|
||||||
onDownload,
|
|
||||||
onDownloadAll,
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="app-content">
|
|
||||||
<MainSection
|
|
||||||
{uploadedImage}
|
|
||||||
{texts}
|
|
||||||
{onImageUpload}
|
|
||||||
{onCropComplete}
|
|
||||||
{onCropCancel}
|
|
||||||
{onTextAdd}
|
|
||||||
{onTextUpdate}
|
|
||||||
{onTextDelete}
|
|
||||||
/>
|
|
||||||
<Sidebar {backgroundImage} {panels} {onUploadNewImage} {onDownload} {onDownloadAll} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.app-content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.app-content {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import ImageManager from "$components/image/ImageManager.svelte";
|
|
||||||
import TextSection from "$components/text/TextSection.svelte";
|
|
||||||
import { uiStore } from "$stores/uiStore";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
uploadedImage: string | undefined;
|
|
||||||
texts: Array<{ id: string; text: string }>;
|
|
||||||
onImageUpload: (image: string) => void;
|
|
||||||
onCropComplete: (croppedImage: string) => void;
|
|
||||||
onCropCancel: () => void;
|
|
||||||
onTextAdd: (text: string) => void;
|
|
||||||
onTextUpdate: (id: string, newText: string) => void;
|
|
||||||
onTextDelete: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
uploadedImage,
|
|
||||||
texts,
|
|
||||||
onImageUpload,
|
|
||||||
onCropComplete,
|
|
||||||
onCropCancel,
|
|
||||||
onTextAdd,
|
|
||||||
onTextUpdate,
|
|
||||||
onTextDelete,
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="main-section">
|
|
||||||
<ImageManager currentStep={$uiStore.currentStep} {uploadedImage} {onImageUpload} {onCropComplete} {onCropCancel} />
|
|
||||||
{#if $uiStore.currentStep === "text"}
|
|
||||||
<TextSection {texts} {onTextAdd} {onTextUpdate} {onTextDelete} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.main-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import BackgroundPreview from "$components/image/BackgroundPreview.svelte";
|
|
||||||
import PanelsList from "$components/panel/PanelsList.svelte";
|
|
||||||
import { uiStore } from "$stores/uiStore";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
backgroundImage: string | undefined;
|
|
||||||
panels: any[];
|
|
||||||
onUploadNewImage: () => void;
|
|
||||||
onDownload: (panel: any, konvaStage: any) => Promise<void>;
|
|
||||||
onDownloadAll: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { backgroundImage, panels, onUploadNewImage, onDownload, onDownloadAll }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="sidebar">
|
|
||||||
{#if $uiStore.currentStep === "text"}
|
|
||||||
<BackgroundPreview {backgroundImage} {onUploadNewImage} />
|
|
||||||
<PanelsList {panels} {onDownload} {onDownloadAll} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.sidebar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.sidebar {
|
|
||||||
order: -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Button from "$components/ui/Button.svelte";
|
|
||||||
import { PANEL_SETTINGS, TYPOGRAPHY, UI_SETTINGS } from "$lib/constants";
|
|
||||||
import type { Panel, TextItem } from "$lib/types/panel";
|
|
||||||
import { setCurrentStep } from "$stores/uiStore";
|
|
||||||
import type { Stage as KonvaStage } from "konva/lib/Stage";
|
|
||||||
import { Image, Layer, Stage, Text } from "svelte-konva";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
panel: Panel;
|
|
||||||
onDownload?: (panel: Panel, konvaStage: KonvaStage) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { panel, onDownload }: Props = $props();
|
|
||||||
|
|
||||||
let backgroundImage: HTMLImageElement | undefined = $state(undefined);
|
|
||||||
let konvaStage: KonvaStage | null = $state(null);
|
|
||||||
let stageComponent: any = $state(null);
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (stageComponent) {
|
|
||||||
konvaStage = stageComponent.node;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleUploadNewImage() {
|
|
||||||
setCurrentStep(UI_SETTINGS.STEPS.UPLOAD);
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (panel?.backgroundImage && panel.backgroundImage !== backgroundImage?.src) {
|
|
||||||
loadImage(panel.backgroundImage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function loadImage(src: string) {
|
|
||||||
const img = document.createElement("img");
|
|
||||||
img.onload = () => {
|
|
||||||
backgroundImage = img;
|
|
||||||
};
|
|
||||||
img.onerror = (error) => {
|
|
||||||
console.error("[PanelPreview] Failed to load background image");
|
|
||||||
};
|
|
||||||
img.src = src;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDownload() {
|
|
||||||
let stageToUse = konvaStage;
|
|
||||||
|
|
||||||
if (!stageToUse && stageComponent) {
|
|
||||||
stageToUse = stageComponent.node || stageComponent.stage || stageComponent._stage || stageComponent;
|
|
||||||
|
|
||||||
if (stageToUse && typeof stageToUse.toBlob === "function") {
|
|
||||||
} else {
|
|
||||||
if (typeof stageComponent.toBlob === "function") {
|
|
||||||
stageToUse = stageComponent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onDownload && stageToUse) {
|
|
||||||
onDownload(panel, stageToUse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTextPosition(textItem: TextItem) {
|
|
||||||
const panelWidth = PANEL_SETTINGS.PANEL_WIDTH;
|
|
||||||
const paddingX = textItem.paddingX;
|
|
||||||
const verticalOffset = textItem.verticalOffset;
|
|
||||||
const centerY = panel.height / 2 + verticalOffset;
|
|
||||||
const textWidth = panelWidth - paddingX * 2;
|
|
||||||
|
|
||||||
let position;
|
|
||||||
switch (textItem.textAlign) {
|
|
||||||
case "left":
|
|
||||||
position = { x: paddingX, y: centerY };
|
|
||||||
break;
|
|
||||||
case "right":
|
|
||||||
position = { x: panelWidth - textWidth - paddingX, y: centerY };
|
|
||||||
break;
|
|
||||||
case "center":
|
|
||||||
default:
|
|
||||||
position = { x: (panelWidth - textWidth) / 2, y: centerY };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="panel-preview">
|
|
||||||
<div class="preview-header">
|
|
||||||
<div class="panel-title">{panel.text?.text || "Без названия"}</div>
|
|
||||||
<Button variant="primary" size="sm" onclick={handleDownload}>Скачать</Button>
|
|
||||||
</div>
|
|
||||||
<div class="preview-sections">
|
|
||||||
<div class="preview-section">
|
|
||||||
<div class="canvas-container">
|
|
||||||
<Stage width={PANEL_SETTINGS.PANEL_WIDTH} height={panel.height} bind:this={stageComponent}>
|
|
||||||
<Layer>
|
|
||||||
{#if backgroundImage}
|
|
||||||
<Image image={backgroundImage} width={PANEL_SETTINGS.PANEL_WIDTH} height={panel.height} />
|
|
||||||
{/if}
|
|
||||||
</Layer>
|
|
||||||
|
|
||||||
<Layer>
|
|
||||||
{#if panel.text}
|
|
||||||
{@const textPosition = getTextPosition(panel.text)}
|
|
||||||
|
|
||||||
<Text
|
|
||||||
text={panel.text.text}
|
|
||||||
fontSize={panel.text.fontSize || TYPOGRAPHY.FONT_SIZE_DEFAULT}
|
|
||||||
fill={panel.text.color || TYPOGRAPHY.TEXT_COLOR_DEFAULT}
|
|
||||||
fontFamily={panel.text.fontFamily || "Arial"}
|
|
||||||
x={textPosition.x}
|
|
||||||
y={textPosition.y}
|
|
||||||
align={panel.text.textAlign || TYPOGRAPHY.TEXT_ALIGN_CENTER}
|
|
||||||
width={PANEL_SETTINGS.PANEL_WIDTH - textPosition.x * 2}
|
|
||||||
offsetX={0}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</Layer>
|
|
||||||
</Stage>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.panel-preview {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-title {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-sections {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvas-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 1rem;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Panel } from "$lib/types/panel";
|
|
||||||
import type { Stage as KonvaStage } from "konva/lib/Stage";
|
|
||||||
import Button from "../ui/Button.svelte";
|
|
||||||
import PanelPreview from "./PanelPreview.svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
panels: Panel[];
|
|
||||||
onDownload: (panel: Panel, konvaStage: KonvaStage) => void;
|
|
||||||
onDownloadAll: () => void;
|
|
||||||
}
|
|
||||||
let { panels, onDownload, onDownloadAll }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if panels.length > 0}
|
|
||||||
<div class="panels-container">
|
|
||||||
<div class="panels-header">
|
|
||||||
<h2>Созданные панели ({panels.length})</h2>
|
|
||||||
<Button variant="primary" onclick={onDownloadAll}>Скачать все</Button>
|
|
||||||
</div>
|
|
||||||
<div class="panels-list">
|
|
||||||
{#each panels as panel (panel.id)}
|
|
||||||
<div class="panel-item">
|
|
||||||
<PanelPreview {panel} onDownload={(panel, konvaStage) => onDownload(panel, konvaStage)} />
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.panels-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panels-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panels-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panels-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-item {
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import IconPlus from "$components/ui/Icons/IconPlus.svelte";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" id="panelTextInput" placeholder="Введите текст..." class="text-input" />
|
|
||||||
<button id="addTextBtn" class="btn btn-primary">
|
|
||||||
<IconPlus />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Button from "$components/ui/Button.svelte";
|
|
||||||
import { TYPOGRAPHY } from "$lib/constants";
|
|
||||||
import type { TextAlign, TextItem } from "$lib/types/panel";
|
|
||||||
import { textSettingsStore, updateAllTextSettings } from "$stores/panelStore";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onTextAdd: (text: string, settings?: Partial<TextItem>) => void;
|
|
||||||
onTextUpdate: (id: string, text: string) => void;
|
|
||||||
onTextDelete: (id: string) => void;
|
|
||||||
texts: Array<{ id: string; text: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { onTextAdd, onTextUpdate, onTextDelete, texts }: Props = $props();
|
|
||||||
|
|
||||||
let newText = $state("");
|
|
||||||
let errorMessage = $state<string | undefined>(undefined);
|
|
||||||
|
|
||||||
let commonTextSettings = $derived($textSettingsStore);
|
|
||||||
|
|
||||||
const availableFonts = TYPOGRAPHY.FONT_FAMILIES;
|
|
||||||
|
|
||||||
function handleAddText() {
|
|
||||||
if (!newText.trim()) {
|
|
||||||
errorMessage = "Введите текст";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newText.length > TYPOGRAPHY.MAX_TEXT_LENGTH) {
|
|
||||||
errorMessage = `Текст не должен превышать ${TYPOGRAPHY.MAX_TEXT_LENGTH} символов`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
errorMessage = undefined;
|
|
||||||
onTextAdd(newText.trim(), commonTextSettings);
|
|
||||||
newText = "";
|
|
||||||
} catch (error) {
|
|
||||||
errorMessage = error instanceof Error ? error.message : "Ошибка добавления текста";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyPress(e: KeyboardEvent) {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
handleAddText();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUpdateText(id: string, newText: string) {
|
|
||||||
if (onTextUpdate && newText.trim()) {
|
|
||||||
onTextUpdate(id, newText.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteText(id: string) {
|
|
||||||
if (onTextDelete) {
|
|
||||||
onTextDelete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="text-manager">
|
|
||||||
<div class="add-text-section">
|
|
||||||
<div class="input-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
bind:value={newText}
|
|
||||||
placeholder="Введите текст для панели (например: links, about me, projects...)"
|
|
||||||
class="text-input"
|
|
||||||
maxlength={TYPOGRAPHY.MAX_TEXT_LENGTH}
|
|
||||||
onkeypress={handleKeyPress}
|
|
||||||
/>
|
|
||||||
<Button variant="primary" onclick={handleAddText}>Добавить</Button>
|
|
||||||
</div>
|
|
||||||
{#if errorMessage}
|
|
||||||
<div class="error-message">
|
|
||||||
{errorMessage}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if texts.length > 0}
|
|
||||||
<div class="texts-list">
|
|
||||||
<h3>Созданные тексты ({texts.length})</h3>
|
|
||||||
<div class="texts-container">
|
|
||||||
{#each texts as textItem (textItem.id)}
|
|
||||||
<div class="text-item">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={textItem.text}
|
|
||||||
oninput={(e) => handleUpdateText(textItem.id, e.currentTarget.value)}
|
|
||||||
class="text-edit-input"
|
|
||||||
maxlength={TYPOGRAPHY.MAX_TEXT_LENGTH}
|
|
||||||
/>
|
|
||||||
<Button variant="danger" size="sm" onclick={() => handleDeleteText(textItem.id)} aria-label="Удалить текст">
|
|
||||||
×
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="common-settings-section">
|
|
||||||
<h3>Общие настройки текста</h3>
|
|
||||||
<p class="settings-note">Настройки применятся ко всем создаваемым панелям</p>
|
|
||||||
|
|
||||||
<div class="settings-grid">
|
|
||||||
<div class="control-group">
|
|
||||||
<label>
|
|
||||||
Размер шрифта:
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={TYPOGRAPHY.FONT_SIZE_MIN}
|
|
||||||
max={TYPOGRAPHY.FONT_SIZE_MAX}
|
|
||||||
step="1"
|
|
||||||
value={commonTextSettings.fontSize}
|
|
||||||
oninput={(e: Event) => {
|
|
||||||
updateAllTextSettings({ fontSize: parseInt((e.target as HTMLInputElement).value) });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span class="value-display">{commonTextSettings.fontSize}px</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label>
|
|
||||||
Шрифт:
|
|
||||||
<select
|
|
||||||
value={commonTextSettings.fontFamily}
|
|
||||||
onchange={(e) => {
|
|
||||||
updateAllTextSettings({ fontFamily: (e.target as HTMLSelectElement).value });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#each availableFonts as font}
|
|
||||||
<option value={font}>{font}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label>
|
|
||||||
Цвет:
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={commonTextSettings.color}
|
|
||||||
oninput={(e) => {
|
|
||||||
updateAllTextSettings({ color: (e.target as HTMLInputElement).value });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label>
|
|
||||||
Выравнивание:
|
|
||||||
<div class="alignment-buttons">
|
|
||||||
<button
|
|
||||||
class="align-btn {commonTextSettings.textAlign === 'left' ? 'active' : ''}"
|
|
||||||
onclick={() => {
|
|
||||||
updateAllTextSettings({ textAlign: "left" as TextAlign });
|
|
||||||
}}
|
|
||||||
aria-label="Выровнять по левому краю"
|
|
||||||
>
|
|
||||||
←
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="align-btn {commonTextSettings.textAlign === 'center' ? 'active' : ''}"
|
|
||||||
onclick={() => {
|
|
||||||
updateAllTextSettings({ textAlign: "center" as TextAlign });
|
|
||||||
}}
|
|
||||||
aria-label="Выровнять по центру"
|
|
||||||
>
|
|
||||||
↔
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="align-btn {commonTextSettings.textAlign === 'right' ? 'active' : ''}"
|
|
||||||
onclick={() => {
|
|
||||||
updateAllTextSettings({ textAlign: "right" as TextAlign });
|
|
||||||
}}
|
|
||||||
aria-label="Выровнять по правому краю"
|
|
||||||
>
|
|
||||||
→
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label>
|
|
||||||
Боковые отступы:
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max={TYPOGRAPHY.PADDING_X_MAX}
|
|
||||||
step="1"
|
|
||||||
value={commonTextSettings.paddingX}
|
|
||||||
oninput={(e) => {
|
|
||||||
updateAllTextSettings({ paddingX: parseInt((e.target as HTMLInputElement).value) });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span class="value-display">{commonTextSettings.paddingX}px</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label>
|
|
||||||
Смещение от центра:
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={TYPOGRAPHY.VERTICAL_OFFSET_MIN}
|
|
||||||
max={TYPOGRAPHY.VERTICAL_OFFSET_MAX}
|
|
||||||
step="1"
|
|
||||||
value={commonTextSettings.verticalOffset}
|
|
||||||
oninput={(e) => {
|
|
||||||
updateAllTextSettings({ verticalOffset: parseInt((e.target as HTMLInputElement).value) });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span class="value-display"
|
|
||||||
>{(commonTextSettings.verticalOffset ?? 0) > 0 ? "+" : ""}{commonTextSettings.verticalOffset ?? 0}px</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.text-manager {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.common-settings-section {
|
|
||||||
background: #f8f9fa;
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.common-settings-section h3 {
|
|
||||||
margin: 0 0 0.75rem 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-note {
|
|
||||||
margin: 0 0 1rem 0;
|
|
||||||
color: #666;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alignment-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-btn {
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
background: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-btn:hover {
|
|
||||||
background: #f0f0f0;
|
|
||||||
border-color: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-btn.active {
|
|
||||||
background: #007bff;
|
|
||||||
color: white;
|
|
||||||
border-color: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-text-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.texts-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.texts-list h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.texts-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-item {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-edit-input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-edit-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 0.6rem 0.75rem;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
background: #ffebee;
|
|
||||||
color: #c62828;
|
|
||||||
padding: 0.6rem 0.75rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #ffcdd2;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: white;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-item:hover {
|
|
||||||
border-color: #007bff;
|
|
||||||
background: #f0f8ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-group label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-group input[type="range"] {
|
|
||||||
flex: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-group input[type="color"] {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value-display {
|
|
||||||
min-width: 40px;
|
|
||||||
text-align: right;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { TextItem } from "$lib/types/panel";
|
|
||||||
import TextManager from "./TextManager.svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
texts: Array<{ id: string; text: string }>;
|
|
||||||
onTextAdd: (text: string, settings?: Partial<TextItem>) => void;
|
|
||||||
onTextUpdate: (id: string, text: string) => void;
|
|
||||||
onTextDelete: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { texts, onTextAdd, onTextUpdate, onTextDelete }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="text-section">
|
|
||||||
<h2>Добавьте тексты для панелей</h2>
|
|
||||||
<TextManager {onTextAdd} {onTextUpdate} {onTextDelete} {texts} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.text-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-section h2 {
|
|
||||||
margin: 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
type ButtonVariant = "primary" | "secondary" | "danger";
|
|
||||||
type ButtonSize = "sm" | "md" | "lg";
|
|
||||||
type ButtonType = "button" | "submit" | "reset";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
variant?: ButtonVariant;
|
|
||||||
size?: ButtonSize;
|
|
||||||
disabled?: boolean;
|
|
||||||
type?: ButtonType;
|
|
||||||
fullWidth?: boolean;
|
|
||||||
loading?: boolean;
|
|
||||||
class?: string;
|
|
||||||
children?: any;
|
|
||||||
onclick?: (event: MouseEvent) => void;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
variant = "primary",
|
|
||||||
size = "md",
|
|
||||||
disabled = false,
|
|
||||||
type = "button",
|
|
||||||
fullWidth = false,
|
|
||||||
loading = false,
|
|
||||||
class: className = "",
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button
|
|
||||||
{type}
|
|
||||||
{disabled}
|
|
||||||
class:btn-primary={variant === "primary"}
|
|
||||||
class:btn-secondary={variant === "secondary"}
|
|
||||||
class:btn-danger={variant === "danger"}
|
|
||||||
class:btn-sm={size === "sm"}
|
|
||||||
class:btn-md={size === "md"}
|
|
||||||
class:btn-lg={size === "lg"}
|
|
||||||
class:btn-full={fullWidth}
|
|
||||||
class:btn-loading={loading}
|
|
||||||
class={className}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{#if loading}
|
|
||||||
<span class="btn-spinner"></span>
|
|
||||||
{/if}
|
|
||||||
{@render children?.()}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sizes */
|
|
||||||
.btn-sm {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-md {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-lg {
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-full {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Variants */
|
|
||||||
.btn-primary {
|
|
||||||
background: #007bff;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
|
||||||
background: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: #6c757d;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover:not(:disabled) {
|
|
||||||
background: #545b62;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: #dc3545;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover:not(:disabled) {
|
|
||||||
background: #c82333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading state */
|
|
||||||
.btn-loading {
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-spinner {
|
|
||||||
display: inline-block;
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
border: 2px solid currentColor;
|
|
||||||
border-right-color: transparent;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.6s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
type IconButtonVariant = "primary" | "secondary" | "danger";
|
|
||||||
type IconButtonSize = "sm" | "md" | "lg";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
variant?: IconButtonVariant;
|
|
||||||
size?: IconButtonSize;
|
|
||||||
disabled?: boolean;
|
|
||||||
ariaLabel?: string;
|
|
||||||
class?: string;
|
|
||||||
children?: any;
|
|
||||||
onclick?: (event: MouseEvent) => void;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
variant = "secondary",
|
|
||||||
size = "md",
|
|
||||||
disabled = false,
|
|
||||||
ariaLabel = "",
|
|
||||||
class: className = "",
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button
|
|
||||||
{disabled}
|
|
||||||
aria-label={ariaLabel}
|
|
||||||
class:icon-btn-primary={variant === "primary"}
|
|
||||||
class:icon-btn-secondary={variant === "secondary"}
|
|
||||||
class:icon-btn-danger={variant === "danger"}
|
|
||||||
class:icon-btn-sm={size === "sm"}
|
|
||||||
class:icon-btn-md={size === "md"}
|
|
||||||
class:icon-btn-lg={size === "lg"}
|
|
||||||
class={className}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.icon-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
background: none;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sizes */
|
|
||||||
.icon-btn-sm {
|
|
||||||
padding: 0.125rem 0.25rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn-md {
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn-lg {
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Variants */
|
|
||||||
.icon-btn-primary {
|
|
||||||
color: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn-primary:hover:not(:disabled) {
|
|
||||||
color: #0056b3;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn-secondary {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn-secondary:hover:not(:disabled) {
|
|
||||||
color: #007bff;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn-danger {
|
|
||||||
color: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn-danger:hover:not(:disabled) {
|
|
||||||
color: #c82333;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,107 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Panel, TextItem } from "$lib/types/panel";
|
|
||||||
import type { Stage as KonvaStage } from "konva/lib/Stage";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import { exportService } from "../services/exportService";
|
|
||||||
import { imageService } from "../services/imageService";
|
|
||||||
import { panelService } from "../services/panelService";
|
|
||||||
import { textSettingsStore } from "../stores/panelStore";
|
|
||||||
|
|
||||||
import PanelBar from "$components/layout/PanelBar.svelte";
|
import PanelBar from "$components/layout/PanelBar.svelte";
|
||||||
import TextBar from "$components/layout/TextBar.svelte";
|
import TextBar from "$components/layout/TextBar.svelte";
|
||||||
|
|
||||||
let uploadedImage = $state<string | undefined>(undefined);
|
|
||||||
let panels = $state<Panel[]>([]);
|
|
||||||
let texts = $state<Array<{ id: string; text: string }>>([]);
|
|
||||||
let backgroundImage = $state<string | undefined>(undefined);
|
|
||||||
|
|
||||||
let textSettings = $derived($textSettingsStore);
|
|
||||||
|
|
||||||
let previousSettings = $state<string>("");
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
const currentSettings = textSettings;
|
|
||||||
const settingsString = JSON.stringify(currentSettings);
|
|
||||||
|
|
||||||
if (settingsString !== previousSettings) {
|
|
||||||
const updatedPanels = panels.map((panel) => ({
|
|
||||||
...panel,
|
|
||||||
text: { ...panel.text, ...currentSettings },
|
|
||||||
}));
|
|
||||||
|
|
||||||
Promise.resolve().then(() => {
|
|
||||||
if (panels.length > 0) {
|
|
||||||
panels = updatedPanels;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
previousSettings = settingsString;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
const defaultTexts = [
|
|
||||||
{ id: crypto.randomUUID(), text: "About" },
|
|
||||||
{ id: crypto.randomUUID(), text: "Links" },
|
|
||||||
];
|
|
||||||
texts = defaultTexts;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const loadedBackground = await imageService.loadDefaultBackground();
|
|
||||||
backgroundImage = loadedBackground;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load default background");
|
|
||||||
exportService.setErrorMessage("Не удалось загрузить фоновое изображение по умолчанию");
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialPanels = panelService.updatePanelsFromTexts(texts, [], backgroundImage || "");
|
|
||||||
panels = initialPanels;
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleImageUpload(image: string) {
|
|
||||||
uploadedImage = image;
|
|
||||||
imageService.handleImageUpload(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUploadNewImage() {
|
|
||||||
imageService.handleUploadNewImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCropComplete(croppedImage: string) {
|
|
||||||
backgroundImage = croppedImage;
|
|
||||||
uploadedImage = undefined;
|
|
||||||
imageService.handleCropComplete(croppedImage);
|
|
||||||
panels = panelService.updatePanelsBackground(panels, croppedImage);
|
|
||||||
panels = panelService.updatePanelsFromTexts(texts, panels, croppedImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCropCancel() {
|
|
||||||
uploadedImage = undefined;
|
|
||||||
imageService.handleCropCancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAddText(text: string, settings?: Partial<TextItem>) {
|
|
||||||
texts = panelService.addText(texts, text);
|
|
||||||
panels = panelService.updatePanelsFromTexts(texts, panels, backgroundImage!, settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUpdateText(id: string, newText: string) {
|
|
||||||
texts = panelService.updateText(texts, id, newText);
|
|
||||||
panels = panelService.updatePanelsFromTexts(texts, panels, backgroundImage!);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteText(id: string) {
|
|
||||||
texts = panelService.deleteText(texts, id);
|
|
||||||
panels = panelService.updatePanelsFromTexts(texts, panels, backgroundImage!);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDownload(panel: Panel, konvaStage: KonvaStage) {
|
|
||||||
await exportService.handleDownload(panel, konvaStage);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDownloadAll() {
|
|
||||||
await exportService.handleDownloadAll(panels);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="main-grid">
|
<div class="main-grid">
|
||||||
@@ -109,23 +8,6 @@
|
|||||||
<PanelBar />
|
<PanelBar />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <AppContainer
|
|
||||||
errorMessage={exportService.getErrorMessage() || undefined}
|
|
||||||
{uploadedImage}
|
|
||||||
{texts}
|
|
||||||
{backgroundImage}
|
|
||||||
{panels}
|
|
||||||
onUploadNewImage={handleUploadNewImage}
|
|
||||||
onImageUpload={handleImageUpload}
|
|
||||||
onCropComplete={handleCropComplete}
|
|
||||||
onCropCancel={handleCropCancel}
|
|
||||||
onTextAdd={handleAddText}
|
|
||||||
onTextUpdate={handleUpdateText}
|
|
||||||
onTextDelete={handleDeleteText}
|
|
||||||
onDownload={handleDownload}
|
|
||||||
onDownloadAll={handleDownloadAll}
|
|
||||||
/> -->
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.main-grid {
|
.main-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user