Preserve QPowerPoint text styling
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user