feat: WIP: update to new components structure
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import IconMoon from "$components/ui/Icons/IconMoon.svelte";
|
||||||
|
import IconSun from "$components/ui/Icons/IconSun.svelte";
|
||||||
|
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
let {}: Props = $props();
|
let {}: Props = $props();
|
||||||
@@ -16,20 +19,8 @@
|
|||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<h1>Twitch Panels</h1>
|
<h1>Twitch Panels</h1>
|
||||||
<button class="theme-toggle" aria-label="Toggle theme" onclick={toggleTheme}>
|
<button class="theme-toggle" aria-label="Toggle theme" onclick={toggleTheme}>
|
||||||
<svg class="sun-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<IconSun />
|
||||||
<circle cx="12" cy="12" r="5"></circle>
|
<IconMoon />
|
||||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
|
||||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
|
||||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
|
||||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
|
||||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
|
||||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
|
||||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
|
||||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
|
||||||
</svg>
|
|
||||||
<svg class="moon-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -73,29 +64,29 @@
|
|||||||
background: rgba(255, 255, 255, 0.3);
|
background: rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle svg {
|
:global(.theme-toggle svg) {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transition: var(--transition);
|
transition: var(--transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sun-icon {
|
:global(.sun-icon) {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.moon-icon {
|
:global(.moon-icon) {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global([data-theme="dark"]) .sun-icon {
|
:global([data-theme="dark"] .sun-icon) {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global([data-theme="dark"]) .moon-icon {
|
:global([data-theme="dark"] .moon-icon) {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string | Snippet;
|
||||||
|
children: Snippet;
|
||||||
|
titleSnippet?: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { title, children, titleSnippet = emptySnippet }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore block_empty -->
|
||||||
|
{#snippet emptySnippet()}{/snippet}
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-header-row">
|
||||||
|
<h2 class="card-title">
|
||||||
|
{#if typeof title === "string"}
|
||||||
|
{title}
|
||||||
|
{:else}
|
||||||
|
{@render title()}
|
||||||
|
{/if}
|
||||||
|
</h2>
|
||||||
|
<div class="card-snippet">
|
||||||
|
{@render titleSnippet()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import IconArrowLeft from "$components/ui/Icons/IconArrowLeft.svelte";
|
||||||
|
import IconArrowRight from "$components/ui/Icons/IconArrowRight.svelte";
|
||||||
|
import Card from "./Card.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="panel-bar">
|
||||||
|
<Card title="Фоновое изображение">фоновое изображение</Card>
|
||||||
|
|
||||||
|
{#snippet panelTitle()}
|
||||||
|
Панели <span class="badge" id="panelCount">0</span>
|
||||||
|
{/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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import TextManager from "$components/text/TextManager.svelte";
|
||||||
|
import Card from "./Card.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="text-bar">
|
||||||
|
<Card title="Тексты панелей"><TextManager /></Card>
|
||||||
|
|
||||||
|
<Card title="Настройки текста">Настройки текста</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
@@ -1,414 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from "$components/ui/Button.svelte";
|
import IconPlus from "$components/ui/Icons/IconPlus.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>
|
</script>
|
||||||
|
|
||||||
<div class="text-manager">
|
|
||||||
<div class="add-text-section">
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input
|
<input type="text" id="panelTextInput" placeholder="Введите текст..." class="text-input" />
|
||||||
type="text"
|
<button id="addTextBtn" class="btn btn-primary">
|
||||||
bind:value={newText}
|
<IconPlus />
|
||||||
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>
|
</button>
|
||||||
</div>
|
</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>
|
|
||||||
|
|||||||
@@ -0,0 +1,414 @@
|
|||||||
|
<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>
|
||||||
+23
-3
@@ -7,7 +7,8 @@
|
|||||||
import { panelService } from "../services/panelService";
|
import { panelService } from "../services/panelService";
|
||||||
import { textSettingsStore } from "../stores/panelStore";
|
import { textSettingsStore } from "../stores/panelStore";
|
||||||
|
|
||||||
import AppContainer from "../components/layout/AppContainer.svelte";
|
import PanelBar from "$components/layout/PanelBar.svelte";
|
||||||
|
import TextBar from "$components/layout/TextBar.svelte";
|
||||||
|
|
||||||
let uploadedImage = $state<string | undefined>(undefined);
|
let uploadedImage = $state<string | undefined>(undefined);
|
||||||
let panels = $state<Panel[]>([]);
|
let panels = $state<Panel[]>([]);
|
||||||
@@ -103,7 +104,12 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppContainer
|
<div class="main-grid">
|
||||||
|
<TextBar />
|
||||||
|
<PanelBar />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <AppContainer
|
||||||
errorMessage={exportService.getErrorMessage() || undefined}
|
errorMessage={exportService.getErrorMessage() || undefined}
|
||||||
{uploadedImage}
|
{uploadedImage}
|
||||||
{texts}
|
{texts}
|
||||||
@@ -118,4 +124,18 @@
|
|||||||
onTextDelete={handleDeleteText}
|
onTextDelete={handleDeleteText}
|
||||||
onDownload={handleDownload}
|
onDownload={handleDownload}
|
||||||
onDownloadAll={handleDownloadAll}
|
onDownloadAll={handleDownloadAll}
|
||||||
/>
|
/> -->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.main-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.main-grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export interface TextsState {
|
||||||
|
texts: string[];
|
||||||
|
addText(text: string): void;
|
||||||
|
removeText(text: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTexts: string[] = ["About me", "Links", "Projects"];
|
||||||
|
|
||||||
|
function createTextsState(): TextsState {
|
||||||
|
let texts: string[] = $state(defaultTexts);
|
||||||
|
|
||||||
|
return {
|
||||||
|
get texts() {
|
||||||
|
return texts;
|
||||||
|
},
|
||||||
|
addText(text: string) {
|
||||||
|
texts = [...texts, text];
|
||||||
|
},
|
||||||
|
removeText(text: string) {
|
||||||
|
texts = texts.filter((t) => t !== text);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const textsState = createTextsState();
|
||||||
@@ -15,6 +15,7 @@ const config = {
|
|||||||
alias: {
|
alias: {
|
||||||
$components: "src/components",
|
$components: "src/components",
|
||||||
$stores: "src/stores",
|
$stores: "src/stores",
|
||||||
|
$states: "src/states",
|
||||||
$services: "src/services",
|
$services: "src/services",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -12,6 +12,7 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
"moduleResolution": "bundler"
|
"moduleResolution": "bundler",
|
||||||
|
"allowArbitraryExtensions": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user