feat: complete TASK-WP4-03 text templates
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import type { CanvasState } from "@dada/shared-contracts";
|
||||
|
||||
import { effectiveFontSize, P0A_FONT_OPTIONS, type TextStylePatch, type TextTemplateDefinition } from "./text-assets.js";
|
||||
|
||||
type CanvasElement = CanvasState["elements"][number];
|
||||
|
||||
function scalar<T extends string | number | boolean>(element: CanvasElement, key: string, fallback: T): T {
|
||||
const value = element.style_parameters?.[key];
|
||||
return typeof value === typeof fallback ? value as T : fallback;
|
||||
}
|
||||
|
||||
function NumberAndRange(props: { label: string; max: number; min: number; onChange: (value: number) => void; step: number; value: number }) {
|
||||
return <label className="editor-range editor-text-number"><span>{props.label}<output>{props.value}</output></span><input aria-label={`${props.label}滑杆`} max={props.max} min={props.min} onChange={(event) => props.onChange(Number(event.target.value))} step={props.step} type="range" value={props.value} /><input aria-label={props.label} max={props.max} min={props.min} onChange={(event) => props.onChange(Number(event.target.value))} step={props.step} type="number" value={props.value} /></label>;
|
||||
}
|
||||
|
||||
export function TextInspector(props: {
|
||||
draft: CanvasElement;
|
||||
onCancel: () => void;
|
||||
onComplete: () => void;
|
||||
onContent: (content: string) => void;
|
||||
onFont: (fontId: string | null) => void;
|
||||
onFontSize: (value: number) => void;
|
||||
onStyle: (patch: TextStylePatch) => void;
|
||||
onTemplate: (templateId: string) => void;
|
||||
templates: readonly TextTemplateDefinition[];
|
||||
}) {
|
||||
const style = props.draft.style_parameters ?? {};
|
||||
const backgroundOpacity = Math.round(scalar(props.draft, "background_opacity", 1) * 100);
|
||||
const strokeEnabled = scalar(props.draft, "stroke_enabled", false);
|
||||
const backgroundEnabled = scalar(props.draft, "background_enabled", false);
|
||||
return <div className="editor-text-inspector">
|
||||
<label>文字模板切换<select aria-label="文字模板切换" onChange={(event) => props.onTemplate(event.target.value)} value={props.draft.template_or_asset_id}>
|
||||
{props.templates.map((template) => <option disabled={!template.available} key={template.templateId} value={template.templateId}>{template.templateId} · {template.displayName}{template.available ? "" : "(素材暂不可用)"}</option>)}
|
||||
</select></label>
|
||||
<label>文字内容<textarea aria-label="文字内容" onChange={(event) => props.onContent(event.target.value)} rows={4} value={props.draft.content ?? ""} /></label>
|
||||
<label>字体覆盖<select aria-label="字体覆盖" onChange={(event) => props.onFont(event.target.value || null)} value={props.draft.font_override ?? ""}><option value="">模板默认字体</option>{P0A_FONT_OPTIONS.map((font) => <option key={font.fontId} value={font.fontId}>{font.fontId} · {font.displayName}</option>)}</select></label>
|
||||
<NumberAndRange label="有效字号" max={256} min={1} onChange={props.onFontSize} step={1} value={Math.round(effectiveFontSize(props.draft))} />
|
||||
<fieldset className="editor-color-fields"><legend>文字颜色</legend>
|
||||
<label>文字填充色<input aria-label="文字填充色" onChange={(event) => props.onStyle({ fillColor: event.target.value })} type="color" value={scalar(props.draft, "fill_color", "#111111")} /></label>
|
||||
<div className="editor-color-swatches" role="group" aria-label="文字参考色"><button aria-label="红色填充" onClick={() => props.onStyle({ fillColor: "#FA5751" })} style={{ background: "#FA5751" }} type="button" /><button aria-label="黑色填充" onClick={() => props.onStyle({ fillColor: "#000000" })} style={{ background: "#000000" }} type="button" /></div>
|
||||
</fieldset>
|
||||
<label className="editor-check"><input aria-label="启用描边" checked={strokeEnabled} onChange={(event) => props.onStyle({ strokeEnabled: event.target.checked })} type="checkbox" />启用描边</label>
|
||||
<NumberAndRange label="描边宽度" max={12} min={0} onChange={(value) => props.onStyle({ strokeWidth: value })} step={1} value={scalar(props.draft, "stroke_width", 0)} />
|
||||
<label>描边颜色<input aria-label="描边颜色" onChange={(event) => props.onStyle({ strokeColor: event.target.value })} type="color" value={scalar(props.draft, "stroke_color", "#000000")} /></label>
|
||||
<label className="editor-check"><input aria-label="启用文字背景" checked={backgroundEnabled} onChange={(event) => props.onStyle({ backgroundEnabled: event.target.checked })} type="checkbox" />启用文字背景</label>
|
||||
<label>文字背景色<input aria-label="文字背景色" onChange={(event) => props.onStyle({ backgroundColor: event.target.value })} type="color" value={scalar(props.draft, "background_color", "#FFE62C")} /></label>
|
||||
<NumberAndRange label="背景透明度" max={100} min={0} onChange={(value) => props.onStyle({ backgroundOpacity: value / 100 })} step={1} value={backgroundOpacity} />
|
||||
<label>文字对齐<select aria-label="文字对齐" onChange={(event) => props.onStyle({ textAlign: event.target.value as "center" | "left" | "right" })} value={scalar(props.draft, "text_align", "center")}><option value="left">左对齐</option><option value="center">居中</option><option value="right">右对齐</option></select></label>
|
||||
<NumberAndRange label="行距" max={1.9} min={1} onChange={(value) => props.onStyle({ lineHeight: value })} step={0.1} value={scalar(props.draft, "line_height", 1.2)} />
|
||||
<NumberAndRange label="字距" max={20} min={1} onChange={(value) => props.onStyle({ letterSpacing: value })} step={1} value={scalar(props.draft, "letter_spacing", 1)} />
|
||||
<div className="editor-inspector-actions"><button onClick={props.onCancel} type="button">取消</button><button aria-label="完成文字编辑" className="editor-primary" onClick={props.onComplete} type="button">完成</button></div>
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user