Preserve QPowerPoint text styling

This commit is contained in:
Курнат Андрей
2026-06-01 07:08:32 +03:00
parent 36d6b2958d
commit 5a968dea72
6 changed files with 228 additions and 9 deletions
+86
View File
@@ -1,4 +1,5 @@
import { useRef, useState } from "react";
import type { CSSProperties } from "react";
import { Copy, Download, FileText, FileUp, Image as ImageIcon, Palette, Plus, Save, Trash2 } from "lucide-react";
import type { Slide, SlideDeck, SlideImage } from "../../types";
import { downloadBlobFile, downloadTextFile } from "../../utils/download";
@@ -28,6 +29,16 @@ const backgroundColorSwatches = [
{ color: "FCE4D6", label: "Коралловый фон" },
{ color: "1F2937", label: "Темный фон" }
];
const textColorSwatches = [
{ color: "", label: "Цвет темы" },
{ color: "202124", label: "Черный текст" },
{ color: "FFFFFF", label: "Белый текст" },
{ color: "0F5FAE", label: "Синий текст" },
{ color: "0F766E", label: "Зеленый текст" },
{ color: "C2410C", label: "Красный текст" }
];
const titleFontSizeOptions = [24, 28, 30, 34, 40, 44, 48];
const subtitleFontSizeOptions = [14, 16, 18, 20, 22, 24, 28];
const slideWidthInches = 13.333;
const slideHeightInches = 7.5;
@@ -84,11 +95,30 @@ function slideCanvasStyle(slide: Slide) {
return backgroundColor ? { background: `#${backgroundColor}` } : undefined;
}
function slideTextStyle(color?: string, fontSize?: number): CSSProperties | undefined {
const textColor = normalizedHexColor(color);
const normalizedFontSize = normalizeFontSize(fontSize);
const style: CSSProperties = {
...(textColor ? { color: `#${textColor}` } : {}),
...(normalizedFontSize ? { fontSize: `${normalizedFontSize}pt` } : {})
};
return Object.keys(style).length > 0 ? style : undefined;
}
function normalizedHexColor(value?: string) {
const raw = value?.trim().replace(/^#/, "").toUpperCase() ?? "";
return /^[0-9A-F]{6}$/.test(raw) ? raw : "";
}
function normalizeFontSize(value?: number) {
if (!Number.isFinite(value) || !value || value <= 0) {
return undefined;
}
return Math.round(value * 2) / 2;
}
function clonedSlideImages(images?: SlideImage[]) {
const suffix = Date.now();
return images?.map((image, index) => ({ ...image, id: `${image.id}-copy-${suffix}-${index}` }));
@@ -362,12 +392,14 @@ export function QPowerPoint({ deck, onChange }: QPowerPointProps) {
className="slide-title-input"
value={activeSlide.title}
aria-label="Заголовок слайда"
style={slideTextStyle(activeSlide.titleColor, activeSlide.titleFontSize)}
onChange={(event) => updateSlide({ ...activeSlide, title: event.target.value })}
/>
<textarea
className="slide-subtitle-input"
value={activeSlide.subtitle}
aria-label="Подзаголовок слайда"
style={slideTextStyle(activeSlide.subtitleColor, activeSlide.subtitleFontSize)}
onChange={(event) => updateSlide({ ...activeSlide, subtitle: event.target.value })}
/>
<div className="slide-accent-line" />
@@ -403,6 +435,60 @@ export function QPowerPoint({ deck, onChange }: QPowerPointProps) {
/>
))}
</div>
<div className="slide-text-controls">
<span>Заголовок</span>
<select
value={activeSlide.titleFontSize ?? ""}
onChange={(event) => updateSlide({ ...activeSlide, titleFontSize: event.target.value ? Number(event.target.value) : undefined })}
aria-label="Размер заголовка"
>
<option value="">Размер темы</option>
{titleFontSizeOptions.map((fontSize) => (
<option key={fontSize} value={fontSize}>
{fontSize}
</option>
))}
</select>
<div className="slide-background-swatches" aria-label="Цвет заголовка">
{textColorSwatches.map((swatch) => (
<button
key={`title-${swatch.label}`}
className={`fill-swatch ${swatch.color ? "" : "is-empty"} ${normalizedHexColor(activeSlide.titleColor) === swatch.color || (!activeSlide.titleColor && !swatch.color) ? "is-active" : ""}`.trim()}
type="button"
onClick={() => updateSlide({ ...activeSlide, titleColor: swatch.color || undefined })}
title={swatch.label}
aria-label={swatch.label}
style={swatch.color ? { backgroundColor: `#${swatch.color}` } : undefined}
/>
))}
</div>
<span>Подзаголовок</span>
<select
value={activeSlide.subtitleFontSize ?? ""}
onChange={(event) => updateSlide({ ...activeSlide, subtitleFontSize: event.target.value ? Number(event.target.value) : undefined })}
aria-label="Размер подзаголовка"
>
<option value="">Размер темы</option>
{subtitleFontSizeOptions.map((fontSize) => (
<option key={fontSize} value={fontSize}>
{fontSize}
</option>
))}
</select>
<div className="slide-background-swatches" aria-label="Цвет подзаголовка">
{textColorSwatches.map((swatch) => (
<button
key={`subtitle-${swatch.label}`}
className={`fill-swatch ${swatch.color ? "" : "is-empty"} ${normalizedHexColor(activeSlide.subtitleColor) === swatch.color || (!activeSlide.subtitleColor && !swatch.color) ? "is-active" : ""}`.trim()}
type="button"
onClick={() => updateSlide({ ...activeSlide, subtitleColor: swatch.color || undefined })}
title={swatch.label}
aria-label={swatch.label}
style={swatch.color ? { backgroundColor: `#${swatch.color}` } : undefined}
/>
))}
</div>
</div>
</section>
<section>
<h3>Изображения</h3>