style: add eslint and prettier, fix style errors

This commit is contained in:
2026-02-13 09:59:19 +05:00
parent 5d6159fe29
commit 387c1751c6
101 changed files with 14596 additions and 12946 deletions
+108 -108
View File
@@ -1,108 +1,108 @@
<script lang="ts">
let active = $state(false);
</script>
<div class="crop-canvas-container">
<canvas class="crop-canvas"></canvas>
<div class="crop-box" class:active>
<div class="crop-handle nw"></div>
<div class="crop-handle ne"></div>
<div class="crop-handle sw"></div>
<div class="crop-handle se"></div>
<div class="crop-handle n"></div>
<div class="crop-handle s"></div>
<div class="crop-handle w"></div>
<div class="crop-handle e"></div>
</div>
</div>
<style>
.crop-canvas-container {
position: relative;
width: 100%;
aspect-ratio: 16 / 5;
background: var(--surface-subtle);
border-radius: var(--radius);
overflow: hidden;
cursor: crosshair;
}
.crop-canvas {
width: 100%;
height: 100%;
display: block;
}
.crop-box {
position: absolute;
border: 2px solid var(--action-primary);
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
cursor: move;
display: none;
}
.crop-box.active {
display: block;
}
.crop-handle {
position: absolute;
width: 10px;
height: 10px;
background: white;
border: 2px solid var(--action-primary);
border-radius: 50%;
}
.crop-handle.nw {
top: -5px;
left: -5px;
cursor: nw-resize;
}
.crop-handle.ne {
top: -5px;
right: -5px;
cursor: ne-resize;
}
.crop-handle.sw {
bottom: -5px;
left: -5px;
cursor: sw-resize;
}
.crop-handle.se {
bottom: -5px;
right: -5px;
cursor: se-resize;
}
.crop-handle.n {
top: -5px;
left: 50%;
transform: translateX(-50%);
cursor: n-resize;
}
.crop-handle.s {
bottom: -5px;
left: 50%;
transform: translateX(-50%);
cursor: s-resize;
}
.crop-handle.w {
left: -5px;
top: 50%;
transform: translateY(-50%);
cursor: w-resize;
}
.crop-handle.e {
right: -5px;
top: 50%;
transform: translateY(-50%);
cursor: e-resize;
}
</style>
<script lang="ts">
let active = $state(false);
</script>
<div class="crop-canvas-container">
<canvas class="crop-canvas"></canvas>
<div class="crop-box" class:active>
<div class="crop-handle nw"></div>
<div class="crop-handle ne"></div>
<div class="crop-handle sw"></div>
<div class="crop-handle se"></div>
<div class="crop-handle n"></div>
<div class="crop-handle s"></div>
<div class="crop-handle w"></div>
<div class="crop-handle e"></div>
</div>
</div>
<style>
.crop-canvas-container {
position: relative;
width: 100%;
aspect-ratio: 16 / 5;
background: var(--surface-subtle);
border-radius: var(--radius);
overflow: hidden;
cursor: crosshair;
}
.crop-canvas {
width: 100%;
height: 100%;
display: block;
}
.crop-box {
position: absolute;
border: 2px solid var(--action-primary);
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
cursor: move;
display: none;
}
.crop-box.active {
display: block;
}
.crop-handle {
position: absolute;
width: 10px;
height: 10px;
background: white;
border: 2px solid var(--action-primary);
border-radius: 50%;
}
.crop-handle.nw {
top: -5px;
left: -5px;
cursor: nw-resize;
}
.crop-handle.ne {
top: -5px;
right: -5px;
cursor: ne-resize;
}
.crop-handle.sw {
bottom: -5px;
left: -5px;
cursor: sw-resize;
}
.crop-handle.se {
bottom: -5px;
right: -5px;
cursor: se-resize;
}
.crop-handle.n {
top: -5px;
left: 50%;
transform: translateX(-50%);
cursor: n-resize;
}
.crop-handle.s {
bottom: -5px;
left: 50%;
transform: translateX(-50%);
cursor: s-resize;
}
.crop-handle.w {
left: -5px;
top: 50%;
transform: translateY(-50%);
cursor: w-resize;
}
.crop-handle.e {
right: -5px;
top: 50%;
transform: translateY(-50%);
cursor: e-resize;
}
</style>
+52 -52
View File
@@ -1,52 +1,52 @@
<script lang="ts">
import Card from "$components/layout/Card.svelte";
import SettingsGrid from "$components/layout/SettingsGrid.svelte";
import SettingsRow from "$components/layout/SettingsRow.svelte";
import Button from "$components/ui/Button.svelte";
import RangeSlider from "$components/ui/RangeSlider.svelte";
// import { Pencil, RotateCcw as Reset, Upload } from "@lucide/svelte";
// import Pencil from "@lucide/svelte/icons/pencil";
// import Reset from "@lucide/svelte/icons/rotate-ccw";
// import Upload from "@lucide/svelte/icons/upload";
import Pencil from "~icons/lucide/pencil";
import Reset from "~icons/lucide/rotate-ccw";
import Upload from "~icons/lucide/upload";
import CropInline from "./CropInline.svelte";
let brightness = $state(100);
let contrast = $state(100);
</script>
<Card title="Фоновое изображение">
<div class="crop-editor">
<CropInline />
<div class="crop-controls">
<Button label="Загрузить" ariaLabel="Upload" icon={Upload} type="secondary" />
<Button label="Редактировать" ariaLabel="Edit" icon={Reset} type="outline" />
<Button label="Сбросить" ariaLabel="Reset" icon={Pencil} type="outline" />
</div>
<SettingsGrid>
<SettingsRow label="Яркость">
<RangeSlider bind:value={brightness} />
</SettingsRow>
<SettingsRow label="Контраст">
<RangeSlider bind:value={contrast} min={50} max={150} />
</SettingsRow>
</SettingsGrid>
</div>
</Card>
<style>
.crop-editor {
display: flex;
flex-direction: column;
gap: 12px;
}
.crop-controls {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
</style>
<script lang="ts">
import Card from "$components/layout/Card.svelte";
import SettingsGrid from "$components/layout/SettingsGrid.svelte";
import SettingsRow from "$components/layout/SettingsRow.svelte";
import Button from "$components/ui/Button.svelte";
import RangeSlider from "$components/ui/RangeSlider.svelte";
// import { Pencil, RotateCcw as Reset, Upload } from "@lucide/svelte";
// import Pencil from "@lucide/svelte/icons/pencil";
// import Reset from "@lucide/svelte/icons/rotate-ccw";
// import Upload from "@lucide/svelte/icons/upload";
import Pencil from "~icons/lucide/pencil";
import Reset from "~icons/lucide/rotate-ccw";
import Upload from "~icons/lucide/upload";
import CropInline from "./CropInline.svelte";
let brightness = $state(100);
let contrast = $state(100);
</script>
<Card title="Фоновое изображение">
<div class="crop-editor">
<CropInline />
<div class="crop-controls">
<Button label="Загрузить" ariaLabel="Upload" icon={Upload} type="secondary" />
<Button label="Редактировать" ariaLabel="Edit" icon={Reset} type="outline" />
<Button label="Сбросить" ariaLabel="Reset" icon={Pencil} type="outline" />
</div>
<SettingsGrid>
<SettingsRow label="Яркость">
<RangeSlider bind:value={brightness} />
</SettingsRow>
<SettingsRow label="Контраст">
<RangeSlider bind:value={contrast} min={50} max={150} />
</SettingsRow>
</SettingsGrid>
</div>
</Card>
<style>
.crop-editor {
display: flex;
flex-direction: column;
gap: 12px;
}
.crop-controls {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
</style>
+86 -86
View File
@@ -1,86 +1,86 @@
<script lang="ts">
import { themeState } from "$states/theme.svelte";
import Moon from "~icons/lucide/moon";
import Sun from "~icons/lucide/sun";
function toggleTheme() {
themeState.toggle();
}
</script>
<header class="header">
<div class="header-content">
<h1>Twitch Panels</h1>
<button class="theme-toggle" aria-label="Toggle theme" onclick={toggleTheme}>
<Sun class="sun-icon" />
<Moon class="moon-icon" />
</button>
</div>
</header>
<style>
.header {
background: linear-gradient(135deg, var(--action-primary) 0%, var(--action-secondary) 100%);
color: var(--text-action);
padding: 16px 20px;
border-radius: var(--radius);
margin-bottom: 16px;
box-shadow: var(--shadow-md);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 20px;
font-weight: 600;
}
.theme-toggle {
width: 36px;
height: 36px;
border-radius: 6px;
border: none;
background: rgba(255, 255, 255, 0.2);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
position: relative;
}
.theme-toggle:hover {
background: rgba(255, 255, 255, 0.3);
}
:global(.theme-toggle svg) {
width: 18px;
height: 18px;
position: absolute;
transition: var(--transition);
}
:global(.sun-icon) {
opacity: 1;
transform: rotate(0deg);
}
:global(.moon-icon) {
opacity: 0;
transform: rotate(90deg);
}
:global([data-theme="dark"] .sun-icon) {
opacity: 0;
transform: rotate(90deg);
}
:global([data-theme="dark"] .moon-icon) {
opacity: 1;
transform: rotate(0deg);
}
</style>
<script lang="ts">
import { themeState } from "$states/theme.svelte";
import Moon from "~icons/lucide/moon";
import Sun from "~icons/lucide/sun";
function toggleTheme() {
themeState.toggle();
}
</script>
<header class="header">
<div class="header-content">
<h1>Twitch Panels</h1>
<button class="theme-toggle" aria-label="Toggle theme" onclick={toggleTheme}>
<Sun class="sun-icon" />
<Moon class="moon-icon" />
</button>
</div>
</header>
<style>
.header {
background: linear-gradient(135deg, var(--action-primary) 0%, var(--action-secondary) 100%);
color: var(--text-action);
padding: 16px 20px;
border-radius: var(--radius);
margin-bottom: 16px;
box-shadow: var(--shadow-md);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 20px;
font-weight: 600;
}
.theme-toggle {
width: 36px;
height: 36px;
border-radius: 6px;
border: none;
background: rgba(255, 255, 255, 0.2);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
position: relative;
}
.theme-toggle:hover {
background: rgba(255, 255, 255, 0.3);
}
:global(.theme-toggle svg) {
width: 18px;
height: 18px;
position: absolute;
transition: var(--transition);
}
:global(.sun-icon) {
opacity: 1;
transform: rotate(0deg);
}
:global(.moon-icon) {
opacity: 0;
transform: rotate(90deg);
}
:global([data-theme="dark"] .sun-icon) {
opacity: 0;
transform: rotate(90deg);
}
:global([data-theme="dark"] .moon-icon) {
opacity: 1;
transform: rotate(0deg);
}
</style>
+67 -68
View File
@@ -1,68 +1,67 @@
<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(--surface-elevated);
border: 1px solid var(--border-main);
border-radius: var(--radius);
padding: 16px;
margin-bottom: 16px;
transition: var(--transition);
overflow: hidden;
box-shadow: var(--shadow);
}
.card-title {
font-size: 15px;
font-weight: 600;
color: var(--text-main);
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;
}
.card-snippet {
display: flex;
align-items: center;
gap: 8px;
}
</style>
<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>
{#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(--surface-elevated);
border: 1px solid var(--border-main);
border-radius: var(--radius);
padding: 16px;
margin-bottom: 16px;
transition: var(--transition);
overflow: hidden;
box-shadow: var(--shadow);
}
.card-title {
font-size: 15px;
font-weight: 600;
color: var(--text-main);
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;
}
.card-snippet {
display: flex;
align-items: center;
gap: 8px;
}
</style>
+21 -21
View File
@@ -1,21 +1,21 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children: Snippet;
}
let { children }: Props = $props();
</script>
<div class="input-group">
{@render children()}
</div>
<style>
.input-group {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
</style>
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children: Snippet;
}
let { children }: Props = $props();
</script>
<div class="input-group">
{@render children()}
</div>
<style>
.input-group {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
</style>
+9 -9
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import ImageManager from "$components/image/ImageManager.svelte";
import PreviewManager from "$components/panel/PreviewManager.svelte";
</script>
<div class="panel-bar">
<ImageManager />
<PreviewManager />
</div>
<script lang="ts">
import ImageManager from "$components/image/ImageManager.svelte";
import PreviewManager from "$components/panel/PreviewManager.svelte";
</script>
<div class="panel-bar">
<ImageManager />
<PreviewManager />
</div>
+21 -21
View File
@@ -1,21 +1,21 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children: Snippet;
}
let { children }: Props = $props();
</script>
<div class="settings-grid">
{@render children()}
</div>
<style>
.settings-grid {
display: flex;
flex-direction: column;
gap: 12px;
}
</style>
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children: Snippet;
}
let { children }: Props = $props();
</script>
<div class="settings-grid">
{@render children()}
</div>
<style>
.settings-grid {
display: flex;
flex-direction: column;
gap: 12px;
}
</style>
+41 -41
View File
@@ -1,41 +1,41 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
label: string;
children: Snippet;
noLabel?: boolean;
}
let { label, children, noLabel = false }: Props = $props();
let tag = $derived(noLabel ? "div" : "label");
</script>
<svelte:element this={tag} class="setting-row">
<div class="setting-label">{label}</div>
<div class="setting-control">
{@render children()}
</div>
</svelte:element>
<style>
.setting-row {
display: grid;
grid-template-columns: 100px 1fr;
align-items: center;
gap: 12px;
}
.setting-label {
color: var(--text-muted);
font-size: 13px;
font-weight: 500;
}
.setting-control {
display: flex;
align-items: center;
gap: 10px;
}
</style>
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
label: string;
children: Snippet;
noLabel?: boolean;
}
let { label, children, noLabel = false }: Props = $props();
let tag = $derived(noLabel ? "div" : "label");
</script>
<svelte:element this={tag} class="setting-row">
<div class="setting-label">{label}</div>
<div class="setting-control">
{@render children()}
</div>
</svelte:element>
<style>
.setting-row {
display: grid;
grid-template-columns: 100px 1fr;
align-items: center;
gap: 12px;
}
.setting-label {
color: var(--text-muted);
font-size: 13px;
font-weight: 500;
}
.setting-control {
display: flex;
align-items: center;
gap: 10px;
}
</style>
+9 -9
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import TextConfig from "$components/text/TextConfig.svelte";
import TextManager from "$components/text/TextManager.svelte";
</script>
<div class="text-bar">
<TextManager />
<TextConfig />
</div>
<script lang="ts">
import TextConfig from "$components/text/TextConfig.svelte";
import TextManager from "$components/text/TextManager.svelte";
</script>
<div class="text-bar">
<TextManager />
<TextConfig />
</div>
+37 -37
View File
@@ -1,37 +1,37 @@
<script lang="ts">
import { PANEL_SETTINGS } from "$lib/constants";
import { imageConfigState } from "$states/imageConfig.svelte";
import { textConfigState } from "$states/textConfig.svelte";
import { Image, Layer, Stage, Text } from "svelte-konva";
interface Props {
text: string;
stage: Stage | undefined;
}
let { text, stage = $bindable() }: Props = $props();
let x = $derived(textConfigState.paddingX);
let y = $derived(10 + textConfigState.offsetY);
let width = $derived(PANEL_SETTINGS.PANEL_WIDTH - 2 * textConfigState.paddingX);
let height = PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT;
</script>
<Stage width={320} height={100} bind:this={stage}>
<Layer>
<Image image={imageConfigState.image}></Image>
<Text
{text}
{x}
{y}
{width}
{height}
fontSize={textConfigState.fontSize}
fill={textConfigState.color}
fontFamily={textConfigState.fontFamily}
align={textConfigState.align}
wrap="none"
ellipsis={true}
/>
</Layer>
</Stage>
<script lang="ts">
import { PANEL_SETTINGS } from "$lib/constants";
import { imageConfigState } from "$states/imageConfig.svelte";
import { textConfigState } from "$states/textConfig.svelte";
import { Image, Layer, Stage, Text } from "svelte-konva";
interface Props {
text: string;
stage: Stage | undefined;
}
let { text, stage = $bindable() }: Props = $props();
let x = $derived(textConfigState.paddingX);
let y = $derived(10 + textConfigState.offsetY);
let width = $derived(PANEL_SETTINGS.PANEL_WIDTH - 2 * textConfigState.paddingX);
let height = PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT;
</script>
<Stage width={320} height={100} bind:this={stage}>
<Layer>
<Image image={imageConfigState.image}></Image>
<Text
{text}
{x}
{y}
{width}
{height}
fontSize={textConfigState.fontSize}
fill={textConfigState.color}
fontFamily={textConfigState.fontFamily}
align={textConfigState.align}
wrap="none"
ellipsis={true}
/>
</Layer>
</Stage>
+22 -22
View File
@@ -1,22 +1,22 @@
<script lang="ts">
import { konvaAllStagesState } from "$states/konvaAllStages.svelte";
import { textsState } from "$states/texts.svelte";
import Preview from "./Preview.svelte";
</script>
<div class="outside-display">
{#each textsState.texts as { text, id }, idx (id)}
<Preview {text} bind:stage={konvaAllStagesState[idx]} />
{:else}
<p>No texts to preview</p>
{/each}
</div>
<style>
.outside-display {
position: fixed;
top: -1000px;
left: -1000px;
}
</style>
<script lang="ts">
import { konvaAllStagesState } from "$states/konvaAllStages.svelte";
import { textsState } from "$states/texts.svelte";
import Preview from "./Preview.svelte";
</script>
<div class="outside-display">
{#each textsState.texts as { text, id }, idx (id)}
<Preview {text} bind:stage={konvaAllStagesState[idx]} />
{:else}
<p>No texts to preview</p>
{/each}
</div>
<style>
.outside-display {
position: fixed;
top: -1000px;
left: -1000px;
}
</style>
+46 -40
View File
@@ -1,40 +1,46 @@
<script lang="ts">
import Button from "$components/ui/Button.svelte";
import { SlideDirection, type SlideDirectionType } from "$lib/constants";
import ChevronLeft from "~icons/lucide/chevron-left";
import ChevronRight from "~icons/lucide/chevron-right";
interface Props {
current: number;
direction: SlideDirectionType;
max: number;
}
let { current = $bindable(), direction = $bindable(SlideDirection.NEXT), max }: Props = $props();
let isFirst = $derived(current === 0);
let isLast = $derived(current === max - 1);
function prev() {
if (current > 0) current--;
direction = SlideDirection.PREV;
}
async function next() {
if (current < max - 1) current++;
direction = SlideDirection.NEXT;
}
</script>
<Button icon={ChevronLeft} ariaLabel="Previous slide" type="mini" onclick={prev} disabled={isFirst} />
<span class="panel-indicator">{current + 1} / {max}</span>
<Button icon={ChevronRight} ariaLabel="Next slide" type="mini" onclick={next} disabled={isLast} />
<style>
.panel-indicator {
font-size: 13px;
font-weight: 500;
min-width: 50px;
text-align: center;
}
</style>
<script lang="ts">
import Button from "$components/ui/Button.svelte";
import { SlideDirection, type SlideDirectionType } from "$lib/constants";
import ChevronLeft from "~icons/lucide/chevron-left";
import ChevronRight from "~icons/lucide/chevron-right";
interface Props {
current: number;
direction: SlideDirectionType;
max: number;
}
let { current = $bindable(), direction = $bindable(SlideDirection.NEXT), max }: Props = $props();
let isFirst = $derived(current === 0);
let isLast = $derived(current === max - 1);
function prev() {
if (current > 0) current--;
direction = SlideDirection.PREV;
}
async function next() {
if (current < max - 1) current++;
direction = SlideDirection.NEXT;
}
</script>
<Button
icon={ChevronLeft}
ariaLabel="Previous slide"
type="mini"
onclick={prev}
disabled={isFirst}
/>
<span class="panel-indicator">{current + 1} / {max}</span>
<Button icon={ChevronRight} ariaLabel="Next slide" type="mini" onclick={next} disabled={isLast} />
<style>
.panel-indicator {
font-size: 13px;
font-weight: 500;
min-width: 50px;
text-align: center;
}
</style>
+144 -136
View File
@@ -1,136 +1,144 @@
<script lang="ts">
import Card from "$components/layout/Card.svelte";
import Badge from "$components/ui/Badge.svelte";
import Button from "$components/ui/Button.svelte";
import { PANEL_SETTINGS, TRANSITION_DURATION, type SlideDirectionType } from "$lib/constants";
import { downloadService, type DownloadItem } from "$services/downloadService";
import { konvaAllStagesState } from "$states/konvaAllStages.svelte";
import { konvaStageState } from "$states/konvaStage.svelte";
import { textsState } from "$states/texts.svelte";
import { fly } from "svelte/transition";
import Download from "~icons/lucide/download";
import StickyNote from "~icons/lucide/sticky-note";
import Preview from "./Preview.svelte";
import PreviewAll from "./PreviewAll.svelte";
import PreviewControls from "./PreviewControls.svelte";
let current: number = $state(0);
let direction: SlideDirectionType = $state("next");
let xDirection = $derived(direction == "next" ? PANEL_SETTINGS.PANEL_WIDTH : -PANEL_SETTINGS.PANEL_WIDTH);
$effect(() => {
if (textsState.texts.length === 0) {
current = 0;
} else {
if (current > textsState.texts.length - 1) {
current = textsState.texts.length - 1;
}
}
});
function downloadAll() {
let downloadItems: Array<DownloadItem> = textsState.texts.map((text, idx) => ({
filename: text.text,
stage: konvaAllStagesState[idx]?.node,
}));
downloadService.downloadAll(downloadItems);
}
function downloadCurrent() {
let node = konvaStageState.stage?.node;
if (node) {
downloadService.downloadPanel(node, textsState.texts[current].text);
}
}
</script>
{#snippet panelTitle()}
Панели <Badge text={textsState.texts.length} />
{/snippet}
{#snippet panelControls()}
<PreviewControls bind:current bind:direction max={textsState.texts.length} />
{/snippet}
<Card title={panelTitle} titleSnippet={panelControls}>
<div class="panel-viewer">
<div class="panel-display">
{#if textsState.texts.length}
{#key current}
{@const text = textsState?.texts[current]?.text}
<div
class="konva-wrapper"
in:fly={{ x: xDirection, duration: TRANSITION_DURATION }}
out:fly={{ x: -xDirection, duration: TRANSITION_DURATION }}
>
<Preview {text} bind:stage={konvaStageState.stage} />
</div>
{/key}
{:else}
<div class="empty-state" aria-label="Empty texts info">
<StickyNote />
<p>Добавьте тексты для создания панелей</p>
</div>
{/if}
</div>
<div class="panel-actions">
<Button label="Скачать всё" ariaLabel="Download all" icon={Download} onclick={downloadAll} />
<Button label="Скачать" ariaLabel="Download current" type="secondary" icon={Download} onclick={downloadCurrent} />
</div>
</div>
</Card>
<PreviewAll />
<style>
.panel-viewer {
display: flex;
flex-direction: column;
gap: 12px;
}
.panel-display {
background: var(--surface-subtle);
border-radius: var(--radius);
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 150px;
position: relative;
}
.panel-actions {
display: flex;
gap: 8px;
}
.empty-state {
text-align: center;
padding: 32px 16px;
}
.empty-state :global(svg) {
width: 48px;
height: 48px;
margin: 0 auto 12px;
opacity: 0.5;
}
.empty-state p {
font-size: 14px;
}
.konva-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
</style>
<script lang="ts">
import Card from "$components/layout/Card.svelte";
import Badge from "$components/ui/Badge.svelte";
import Button from "$components/ui/Button.svelte";
import { PANEL_SETTINGS, TRANSITION_DURATION, type SlideDirectionType } from "$lib/constants";
import { downloadService, type DownloadItem } from "$services/downloadService";
import { konvaAllStagesState } from "$states/konvaAllStages.svelte";
import { konvaStageState } from "$states/konvaStage.svelte";
import { textsState } from "$states/texts.svelte";
import { fly } from "svelte/transition";
import Download from "~icons/lucide/download";
import StickyNote from "~icons/lucide/sticky-note";
import Preview from "./Preview.svelte";
import PreviewAll from "./PreviewAll.svelte";
import PreviewControls from "./PreviewControls.svelte";
let current: number = $state(0);
let direction: SlideDirectionType = $state("next");
let xDirection = $derived(
direction == "next" ? PANEL_SETTINGS.PANEL_WIDTH : -PANEL_SETTINGS.PANEL_WIDTH,
);
$effect(() => {
if (textsState.texts.length === 0) {
current = 0;
} else {
if (current > textsState.texts.length - 1) {
current = textsState.texts.length - 1;
}
}
});
function downloadAll() {
let downloadItems: Array<DownloadItem> = textsState.texts.map((text, idx) => ({
filename: text.text,
stage: konvaAllStagesState[idx]?.node,
}));
downloadService.downloadAll(downloadItems);
}
function downloadCurrent() {
let node = konvaStageState.stage?.node;
if (node) {
downloadService.downloadPanel(node, textsState.texts[current].text);
}
}
</script>
{#snippet panelTitle()}
Панели <Badge text={textsState.texts.length} />
{/snippet}
{#snippet panelControls()}
<PreviewControls bind:current bind:direction max={textsState.texts.length} />
{/snippet}
<Card title={panelTitle} titleSnippet={panelControls}>
<div class="panel-viewer">
<div class="panel-display">
{#if textsState.texts.length}
{#key current}
{@const text = textsState?.texts[current]?.text}
<div
class="konva-wrapper"
in:fly={{ x: xDirection, duration: TRANSITION_DURATION }}
out:fly={{ x: -xDirection, duration: TRANSITION_DURATION }}
>
<Preview {text} bind:stage={konvaStageState.stage} />
</div>
{/key}
{:else}
<div class="empty-state" aria-label="Empty texts info">
<StickyNote />
<p>Добавьте тексты для создания панелей</p>
</div>
{/if}
</div>
<div class="panel-actions">
<Button label="Скачать всё" ariaLabel="Download all" icon={Download} onclick={downloadAll} />
<Button
label="Скачать"
ariaLabel="Download current"
type="secondary"
icon={Download}
onclick={downloadCurrent}
/>
</div>
</div>
</Card>
<PreviewAll />
<style>
.panel-viewer {
display: flex;
flex-direction: column;
gap: 12px;
}
.panel-display {
background: var(--surface-subtle);
border-radius: var(--radius);
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 150px;
position: relative;
}
.panel-actions {
display: flex;
gap: 8px;
}
.empty-state {
text-align: center;
padding: 32px 16px;
}
.empty-state :global(svg) {
width: 48px;
height: 48px;
margin: 0 auto 12px;
opacity: 0.5;
}
.empty-state p {
font-size: 14px;
}
.konva-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
</style>
+33 -33
View File
@@ -1,33 +1,33 @@
<script lang="ts">
import Card from "$components/layout/Card.svelte";
import SettingsGrid from "$components/layout/SettingsGrid.svelte";
import SettingsRow from "$components/layout/SettingsRow.svelte";
import Alignment from "$components/ui/Alignment.svelte";
import ColorPicker from "$components/ui/ColorPicker.svelte";
import RangeSlider from "$components/ui/RangeSlider.svelte";
import SelectFont from "$components/ui/SelectFont.svelte";
import { textConfigState } from "$states/textConfig.svelte";
</script>
<Card title="Настройки текста">
<SettingsGrid>
<SettingsRow label="Размер">
<RangeSlider bind:value={textConfigState.fontSize} min={10} max={100} step={1} />
</SettingsRow>
<SettingsRow label="Шрифт">
<SelectFont bind:value={textConfigState.fontFamily} />
</SettingsRow>
<SettingsRow label="Цвет">
<ColorPicker bind:value={textConfigState.color} />
</SettingsRow>
<SettingsRow label="Выравнивание" noLabel={true}>
<Alignment bind:align={textConfigState.align} />
</SettingsRow>
<SettingsRow label="Отступы">
<RangeSlider bind:value={textConfigState.paddingX} min={0} max={100} step={1} />
</SettingsRow>
<SettingsRow label="Смещение">
<RangeSlider bind:value={textConfigState.offsetY} min={-100} max={100} step={1} />
</SettingsRow>
</SettingsGrid>
</Card>
<script lang="ts">
import Card from "$components/layout/Card.svelte";
import SettingsGrid from "$components/layout/SettingsGrid.svelte";
import SettingsRow from "$components/layout/SettingsRow.svelte";
import Alignment from "$components/ui/Alignment.svelte";
import ColorPicker from "$components/ui/ColorPicker.svelte";
import RangeSlider from "$components/ui/RangeSlider.svelte";
import SelectFont from "$components/ui/SelectFont.svelte";
import { textConfigState } from "$states/textConfig.svelte";
</script>
<Card title="Настройки текста">
<SettingsGrid>
<SettingsRow label="Размер">
<RangeSlider bind:value={textConfigState.fontSize} min={10} max={100} step={1} />
</SettingsRow>
<SettingsRow label="Шрифт">
<SelectFont bind:value={textConfigState.fontFamily} />
</SettingsRow>
<SettingsRow label="Цвет">
<ColorPicker bind:value={textConfigState.color} />
</SettingsRow>
<SettingsRow label="Выравнивание" noLabel={true}>
<Alignment bind:align={textConfigState.align} />
</SettingsRow>
<SettingsRow label="Отступы">
<RangeSlider bind:value={textConfigState.paddingX} min={0} max={100} step={1} />
</SettingsRow>
<SettingsRow label="Смещение">
<RangeSlider bind:value={textConfigState.offsetY} min={-100} max={100} step={1} />
</SettingsRow>
</SettingsGrid>
</Card>
+53 -53
View File
@@ -1,53 +1,53 @@
<script lang="ts">
import Button from "$components/ui/Button.svelte";
import { TRANSITION_DURATION } from "$lib/constants";
import { fly } from "svelte/transition";
import Cross from "~icons/lucide/x";
interface Props {
text: string;
id: number;
ondelete: (id: number) => void;
}
let { text = $bindable(), id, ondelete }: Props = $props();
</script>
<li class="text-item" transition:fly={{ x: 600, duration: TRANSITION_DURATION }}>
<input type="text" bind:value={text} />
<Button icon={Cross} ariaLabel="Delete" type="danger" onclick={() => ondelete(id)} />
</li>
<style>
.text-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background: var(--surface-subtle);
border: 1px solid var(--border-main);
border-radius: var(--radius);
transition: var(--transition);
}
.text-item:hover {
border-color: var(--border-strong);
}
.text-item input {
flex: 1;
padding: 6px 10px;
border: 1px solid var(--border-main);
border-radius: 4px;
font-size: 14px;
background: var(--surface-main);
color: var(--text-main);
transition: var(--transition);
font-family: inherit;
}
.text-item input:focus {
outline: none;
border-color: var(--border-strong);
}
</style>
<script lang="ts">
import Button from "$components/ui/Button.svelte";
import { TRANSITION_DURATION } from "$lib/constants";
import { fly } from "svelte/transition";
import Cross from "~icons/lucide/x";
interface Props {
text: string;
id: number;
ondelete: (id: number) => void;
}
let { text = $bindable(), id, ondelete }: Props = $props();
</script>
<li class="text-item" transition:fly={{ x: 600, duration: TRANSITION_DURATION }}>
<input type="text" bind:value={text} />
<Button icon={Cross} ariaLabel="Delete" type="danger" onclick={() => ondelete(id)} />
</li>
<style>
.text-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background: var(--surface-subtle);
border: 1px solid var(--border-main);
border-radius: var(--radius);
transition: var(--transition);
}
.text-item:hover {
border-color: var(--border-strong);
}
.text-item input {
flex: 1;
padding: 6px 10px;
border: 1px solid var(--border-main);
border-radius: 4px;
font-size: 14px;
background: var(--surface-main);
color: var(--text-main);
transition: var(--transition);
font-family: inherit;
}
.text-item input:focus {
outline: none;
border-color: var(--border-strong);
}
</style>
+47 -47
View File
@@ -1,47 +1,47 @@
<script lang="ts">
interface Props {
text: string;
ariaLabel: string;
onenter: () => void;
}
let { text = $bindable(), onenter, ariaLabel }: Props = $props();
function handleKeyboard(event: KeyboardEvent) {
if (event.key === "Enter") {
onenter();
}
}
</script>
<input
aria-label={ariaLabel}
bind:value={text}
class="text-input"
type="text"
placeholder="Введите текст..."
onkeydown={handleKeyboard}
/>
<style>
.text-input {
flex: 1;
padding: 10px 12px;
border: 1px solid var(--border-main);
border-radius: var(--radius);
font-size: 14px;
background: var(--surface-main);
color: var(--text-main);
transition: var(--transition);
font-family: inherit;
}
.text-input:focus {
outline: none;
border-color: var(--border-strong);
}
.text-input::placeholder {
color: var(--text-muted);
}
</style>
<script lang="ts">
interface Props {
text: string;
ariaLabel: string;
onenter: () => void;
}
let { text = $bindable(), onenter, ariaLabel }: Props = $props();
function handleKeyboard(event: KeyboardEvent) {
if (event.key === "Enter") {
onenter();
}
}
</script>
<input
aria-label={ariaLabel}
bind:value={text}
class="text-input"
type="text"
placeholder="Введите текст..."
onkeydown={handleKeyboard}
/>
<style>
.text-input {
flex: 1;
padding: 10px 12px;
border: 1px solid var(--border-main);
border-radius: var(--radius);
font-size: 14px;
background: var(--surface-main);
color: var(--text-main);
transition: var(--transition);
font-family: inherit;
}
.text-input:focus {
outline: none;
border-color: var(--border-strong);
}
.text-input::placeholder {
color: var(--text-muted);
}
</style>
+40 -40
View File
@@ -1,40 +1,40 @@
<script lang="ts">
import Card from "$components/layout/Card.svelte";
import InputGroup from "$components/layout/InputGroup.svelte";
import Button from "$components/ui/Button.svelte";
import { textsState } from "$states/texts.svelte";
import Plus from "~icons/lucide/plus";
import TextInlineEdit from "./TextInlineEdit.svelte";
import TextInput from "./TextInput.svelte";
let text: string = $state("");
function addText() {
textsState.addText(text);
text = "";
}
function deleteText(id: number) {
textsState.removeText(id);
}
</script>
<Card title="Тексты панелей">
<InputGroup>
<TextInput ariaLabel="Input new text" bind:text onenter={addText} />
<Button icon={Plus} ariaLabel="Add text" onclick={addText} />
</InputGroup>
<ul class="texts-list">
{#each textsState.texts as { text, id } (id)}
<TextInlineEdit {id} {text} ondelete={() => deleteText(id)} />
{/each}
</ul>
</Card>
<style>
.texts-list {
display: flex;
flex-direction: column;
gap: 8px;
}
</style>
<script lang="ts">
import Card from "$components/layout/Card.svelte";
import InputGroup from "$components/layout/InputGroup.svelte";
import Button from "$components/ui/Button.svelte";
import { textsState } from "$states/texts.svelte";
import Plus from "~icons/lucide/plus";
import TextInlineEdit from "./TextInlineEdit.svelte";
import TextInput from "./TextInput.svelte";
let text: string = $state("");
function addText() {
textsState.addText(text);
text = "";
}
function deleteText(id: number) {
textsState.removeText(id);
}
</script>
<Card title="Тексты панелей">
<InputGroup>
<TextInput ariaLabel="Input new text" bind:text onenter={addText} />
<Button icon={Plus} ariaLabel="Add text" onclick={addText} />
</InputGroup>
<ul class="texts-list">
{#each textsState.texts as { text, id } (id)}
<TextInlineEdit {id} {text} ondelete={() => deleteText(id)} />
{/each}
</ul>
</Card>
<style>
.texts-list {
display: flex;
flex-direction: column;
gap: 8px;
}
</style>
+99 -99
View File
@@ -1,99 +1,99 @@
<script lang="ts">
import { TextAlign, type TextAlignType } from "$lib/constants";
import TextAlignCenter from "~icons/lucide/text-align-center";
import TextAlignEnd from "~icons/lucide/text-align-end";
import TextAlignStart from "~icons/lucide/text-align-start";
interface Props {
align: TextAlignType;
}
let { align = $bindable(TextAlign.LEFT) }: Props = $props();
</script>
<div class="alignment-group">
<label class="alignment-item">
<input
type="radio"
name="alignment"
value={TextAlign.LEFT}
class="sr-only"
bind:group={align}
aria-label={`Set ${TextAlign.LEFT}`}
/>
<div class="alignment-button"><TextAlignStart /></div>
</label>
<label class="alignment-item">
<input
type="radio"
name="alignment"
value={TextAlign.CENTER}
class="sr-only"
bind:group={align}
aria-label={`Set ${TextAlign.CENTER}`}
/>
<div class="alignment-button"><TextAlignCenter /></div>
</label>
<label class="alignment-item">
<input
type="radio"
name="alignment"
value={TextAlign.RIGHT}
class="sr-only"
bind:group={align}
aria-label={`Set ${TextAlign.RIGHT}`}
/>
<div class="alignment-button"><TextAlignEnd /></div>
</label>
</div>
<style>
.alignment-group {
display: flex;
overflow: hidden;
width: 100%;
}
.alignment-button {
flex-grow: 1;
border: 1px solid var(--border-strong);
padding: 6px 12px;
cursor: pointer;
color: var(--text-muted);
transition: all 0.2s ease;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.alignment-button:hover {
color: var(--text-main);
background: var(--surface-subtle);
}
.alignment-item {
flex-grow: 1;
& input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
&:first-child .alignment-button {
border-top-left-radius: var(--radius);
border-bottom-left-radius: var(--radius);
}
&:last-child .alignment-button {
border-top-right-radius: var(--radius);
border-bottom-right-radius: var(--radius);
}
}
.alignment-item input:checked + .alignment-button {
background: var(--action-primary);
color: var(--text-action);
}
</style>
<script lang="ts">
import { TextAlign, type TextAlignType } from "$lib/constants";
import TextAlignCenter from "~icons/lucide/text-align-center";
import TextAlignEnd from "~icons/lucide/text-align-end";
import TextAlignStart from "~icons/lucide/text-align-start";
interface Props {
align: TextAlignType;
}
let { align = $bindable(TextAlign.LEFT) }: Props = $props();
</script>
<div class="alignment-group">
<label class="alignment-item">
<input
type="radio"
name="alignment"
value={TextAlign.LEFT}
class="sr-only"
bind:group={align}
aria-label={`Set ${TextAlign.LEFT}`}
/>
<div class="alignment-button"><TextAlignStart /></div>
</label>
<label class="alignment-item">
<input
type="radio"
name="alignment"
value={TextAlign.CENTER}
class="sr-only"
bind:group={align}
aria-label={`Set ${TextAlign.CENTER}`}
/>
<div class="alignment-button"><TextAlignCenter /></div>
</label>
<label class="alignment-item">
<input
type="radio"
name="alignment"
value={TextAlign.RIGHT}
class="sr-only"
bind:group={align}
aria-label={`Set ${TextAlign.RIGHT}`}
/>
<div class="alignment-button"><TextAlignEnd /></div>
</label>
</div>
<style>
.alignment-group {
display: flex;
overflow: hidden;
width: 100%;
}
.alignment-button {
flex-grow: 1;
border: 1px solid var(--border-strong);
padding: 6px 12px;
cursor: pointer;
color: var(--text-muted);
transition: all 0.2s ease;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.alignment-button:hover {
color: var(--text-main);
background: var(--surface-subtle);
}
.alignment-item {
flex-grow: 1;
& input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
&:first-child .alignment-button {
border-top-left-radius: var(--radius);
border-bottom-left-radius: var(--radius);
}
&:last-child .alignment-button {
border-top-right-radius: var(--radius);
border-bottom-right-radius: var(--radius);
}
}
.alignment-item input:checked + .alignment-button {
background: var(--action-primary);
color: var(--text-action);
}
</style>
+25 -25
View File
@@ -1,25 +1,25 @@
<script lang="ts">
interface Props {
text: string | number;
}
let { text }: Props = $props();
</script>
<span class="badge">{text}</span>
<style>
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
background: var(--action-primary);
color: var(--text-action);
border-radius: 10px;
font-size: 11px;
font-weight: 600;
}
</style>
<script lang="ts">
interface Props {
text: string | number;
}
let { text }: Props = $props();
</script>
<span class="badge">{text}</span>
<style>
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
background: var(--action-primary);
color: var(--text-action);
border-radius: 10px;
font-size: 11px;
font-weight: 600;
}
</style>
+115 -115
View File
@@ -1,115 +1,115 @@
<script lang="ts">
import type { Component } from "svelte";
import type { MouseEventHandler } from "svelte/elements";
interface Props {
icon: Component;
ariaLabel: string;
onclick?: MouseEventHandler<HTMLButtonElement>;
disabled?: boolean;
label?: string;
type?: "primary" | "secondary" | "danger" | "outline" | "mini";
extra?: "grow";
}
let {
icon: Icon,
ariaLabel,
onclick = () => {},
disabled = false,
label = "",
type = "primary",
extra,
}: Props = $props();
</script>
<button
class="btn"
class:btn-primary={type === "primary"}
class:btn-secondary={type === "secondary"}
class:btn-outline={type === "outline"}
class:btn-danger={type === "danger"}
class:btn-mini={type === "mini"}
class:grow={extra === "grow"}
{disabled}
{onclick}
aria-label={ariaLabel}
>
<Icon />
{label}
</button>
<style>
.btn {
--btn-main: var(--action-primary);
--btn-hover: var(--action-primary-hover);
padding: 10px 16px;
border: none;
border-radius: var(--radius);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
display: inline-flex;
align-items: center;
gap: 6px;
font-family: inherit;
white-space: nowrap;
background-color: var(--btn-main);
color: oklch(from var(--btn-main) 99% 0.05 h);
}
.btn :global(svg) {
width: 16px;
height: 16px;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn:hover:not(:disabled) {
background-color: var(--btn-hover);
box-shadow: var(--shadow-md);
}
.btn-secondary {
--btn-main: var(--action-secondary);
--btn-hover: var(--action-secondary-hover);
}
.btn-outline {
background: transparent;
color: var(--text-main);
border: 1px solid var(--border-strong);
}
.btn-outline:hover:not(:disabled) {
background: var(--surface-hover);
border-color: var(--border-strong);
color: var(--text-main);
}
.btn-danger {
--btn-main: var(--danger-base);
}
.btn-mini {
width: 32px;
height: 32px;
padding: 0;
border: 1px solid var(--border-main);
border-radius: var(--radius);
justify-content: center;
}
.btn-mini:hover:not(:disabled) {
border-color: var(--border-main);
}
.grow {
flex-grow: 1;
justify-content: center;
}
</style>
<script lang="ts">
import type { Component } from "svelte";
import type { MouseEventHandler } from "svelte/elements";
interface Props {
icon: Component;
ariaLabel: string;
onclick?: MouseEventHandler<HTMLButtonElement>;
disabled?: boolean;
label?: string;
type?: "primary" | "secondary" | "danger" | "outline" | "mini";
extra?: "grow";
}
let {
icon: Icon,
ariaLabel,
onclick = () => {},
disabled = false,
label = "",
type = "primary",
extra,
}: Props = $props();
</script>
<button
class="btn"
class:btn-primary={type === "primary"}
class:btn-secondary={type === "secondary"}
class:btn-outline={type === "outline"}
class:btn-danger={type === "danger"}
class:btn-mini={type === "mini"}
class:grow={extra === "grow"}
{disabled}
{onclick}
aria-label={ariaLabel}
>
<Icon />
{label}
</button>
<style>
.btn {
--btn-main: var(--action-primary);
--btn-hover: var(--action-primary-hover);
padding: 10px 16px;
border: none;
border-radius: var(--radius);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
display: inline-flex;
align-items: center;
gap: 6px;
font-family: inherit;
white-space: nowrap;
background-color: var(--btn-main);
color: var(--text-action);
}
.btn :global(svg) {
width: 16px;
height: 16px;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn:hover:not(:disabled) {
background-color: var(--btn-hover);
box-shadow: var(--shadow-md);
}
.btn-secondary {
--btn-main: var(--action-secondary);
--btn-hover: var(--action-secondary-hover);
}
.btn-outline {
background: transparent;
color: var(--text-main);
border: 1px solid var(--border-strong);
}
.btn-outline:hover:not(:disabled) {
background: var(--surface-hover);
border-color: var(--border-strong);
color: var(--text-main);
}
.btn-danger {
--btn-main: var(--danger-base);
}
.btn-mini {
width: 32px;
height: 32px;
padding: 0;
border: 1px solid var(--border-main);
border-radius: var(--radius);
justify-content: center;
}
.btn-mini:hover:not(:disabled) {
border-color: var(--border-main);
}
.grow {
flex-grow: 1;
justify-content: center;
}
</style>
+40 -40
View File
@@ -1,40 +1,40 @@
<script lang="ts">
import type { HexColor } from "$lib/types";
interface Props {
value: HexColor;
}
let { value = $bindable() }: Props = $props();
</script>
<input type="color" bind:value class="color-input" />
<span class="color-value">{value}</span>
<style>
.color-input {
width: 40px;
height: 32px;
border: 1px solid var(--border-main);
border-radius: var(--radius);
cursor: pointer;
transition: var(--transition);
background: var(--surface-main);
padding: 0px 8px;
flex: 1;
}
.color-input:hover {
border-color: var(--action-primary);
}
.color-value {
font-family: "Courier New", monospace;
font-size: 12px;
font-weight: 500;
color: var(--text-muted);
padding: 4px 8px;
background: var(--surface-main);
border-radius: 4px;
}
</style>
<script lang="ts">
import type { HexColor } from "$lib/types";
interface Props {
value: HexColor;
}
let { value = $bindable() }: Props = $props();
</script>
<input type="color" bind:value class="color-input" />
<span class="color-value">{value}</span>
<style>
.color-input {
width: 40px;
height: 32px;
border: 1px solid var(--border-main);
border-radius: var(--radius);
cursor: pointer;
transition: var(--transition);
background: var(--surface-main);
padding: 0px 8px;
flex: 1;
}
.color-input:hover {
border-color: var(--action-primary);
}
.color-value {
font-family: "Courier New", monospace;
font-size: 12px;
font-weight: 500;
color: var(--text-muted);
padding: 4px 8px;
background: var(--surface-main);
border-radius: 4px;
}
</style>
+68 -68
View File
@@ -1,68 +1,68 @@
<script lang="ts">
import type { ChangeEventHandler } from "svelte/elements";
interface Props {
value: number;
min?: number;
max?: number;
step?: number;
onchange?: ChangeEventHandler<HTMLInputElement>;
}
let { value = $bindable(), min = 0, max = 100, step = 1, onchange = () => {} }: Props = $props();
</script>
<input type="range" {min} {max} {step} bind:value class="slider" {onchange} />
<span class="value-display">{value}</span>
<style>
.slider {
flex: 1;
height: 4px;
border-radius: 2px;
background: var(--surface-control);
outline: none;
-webkit-appearance: none;
appearance: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--action-primary);
cursor: pointer;
transition: var(--transition);
}
.slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
}
.slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--action-primary);
cursor: pointer;
border: none;
transition: var(--transition);
}
.slider::-moz-range-thumb:hover {
transform: scale(1.1);
}
.value-display {
color: var(--text-main);
font-weight: 500;
min-width: 40px;
text-align: right;
font-size: 13px;
padding: 4px 8px;
background: var(--surface-main);
border-radius: 4px;
}
</style>
<script lang="ts">
import type { ChangeEventHandler } from "svelte/elements";
interface Props {
value: number;
min?: number;
max?: number;
step?: number;
onchange?: ChangeEventHandler<HTMLInputElement>;
}
let { value = $bindable(), min = 0, max = 100, step = 1, onchange = () => {} }: Props = $props();
</script>
<input type="range" {min} {max} {step} bind:value class="slider" {onchange} />
<span class="value-display">{value}</span>
<style>
.slider {
flex: 1;
height: 4px;
border-radius: 2px;
background: var(--surface-control);
outline: none;
-webkit-appearance: none;
appearance: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--action-primary);
cursor: pointer;
transition: var(--transition);
}
.slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
}
.slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--action-primary);
cursor: pointer;
border: none;
transition: var(--transition);
}
.slider::-moz-range-thumb:hover {
transform: scale(1.1);
}
.value-display {
color: var(--text-main);
font-weight: 500;
min-width: 40px;
text-align: right;
font-size: 13px;
padding: 4px 8px;
background: var(--surface-main);
border-radius: 4px;
}
</style>
+38 -38
View File
@@ -1,38 +1,38 @@
<script lang="ts">
interface Props {
value: string;
}
let { value = $bindable() }: Props = $props();
</script>
<select class="select-input" bind:value>
<option value="Arial">Arial</option>
<option value="Verdana">Verdana</option>
<option value="Georgia">Georgia</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Courier New">Courier New</option>
<option value="Impact">Impact</option>
<option value="Comic Sans MS">Comic Sans MS</option>
<option value="Trebuchet MS">Trebuchet MS</option>
</select>
<style>
.select-input {
padding: 8px 10px;
border: 1px solid var(--border-main);
border-radius: var(--radius);
background: var(--surface-main);
color: var(--text-main);
font-size: 13px;
cursor: pointer;
transition: var(--transition);
font-family: inherit;
width: 100%;
}
.select-input:focus {
outline: none;
border-color: var(--border-strong);
}
</style>
<script lang="ts">
interface Props {
value: string;
}
let { value = $bindable() }: Props = $props();
</script>
<select class="select-input" bind:value>
<option value="Arial">Arial</option>
<option value="Verdana">Verdana</option>
<option value="Georgia">Georgia</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Courier New">Courier New</option>
<option value="Impact">Impact</option>
<option value="Comic Sans MS">Comic Sans MS</option>
<option value="Trebuchet MS">Trebuchet MS</option>
</select>
<style>
.select-input {
padding: 8px 10px;
border: 1px solid var(--border-main);
border-radius: var(--radius);
background: var(--surface-main);
color: var(--text-main);
font-size: 13px;
cursor: pointer;
transition: var(--transition);
font-family: inherit;
width: 100%;
}
.select-input:focus {
outline: none;
border-color: var(--border-strong);
}
</style>