import { useEffect, useState } from "react"; import { Plus, X } from "../../icons/SystemIcons"; import { isTextEditableSelection, type DomEditSelection } from "./domEditing"; import type { ImportedFontAsset } from "./fontAssets"; import { normalizeTextMetricValue } from "./propertyPanelHelpers"; import { ColorField } from "./propertyPanelColor"; import { FontFamilyField } from "./propertyPanelFont"; import { PromotableControl } from "./PromotableControl"; import { FlatRow, FlatSegmentedRow } from "./propertyPanelFlatPrimitives"; import { resolveValueTier, VALUE_TIER_LABEL_CLASS, VALUE_TIER_VALUE_CLASS, } from "./propertyPanelValueTier"; import { detectAvailableWeights, formatTextFieldPreview, getTextFieldColor, getTextStyleValue, TextAreaField, WEIGHT_LABELS, } from "./propertyPanelSections"; /* ------------------------------------------------------------------ */ /* Flat text section (design_handoff_studio_inspector, #10a) */ /* ------------------------------------------------------------------ */ const ALIGN_OPTIONS = [ { key: "left", label: "left", node: "L" }, { key: "center", label: "center", node: "C" }, { key: "right", label: "right", node: "R" }, { key: "justify", label: "justify", node: "J" }, ]; const CASE_OPTIONS = [ { key: "none", label: "none", node: "โ€“" }, { key: "uppercase", label: "uppercase", node: "AG" }, { key: "lowercase", label: "lowercase", node: "ag" }, { key: "capitalize", label: "capitalize", node: "Ag" }, ]; function FlatTextFieldEditor({ field, styles, fontAssets, onImportFonts, onSetText, onSetTextFieldStyle, autoFocus = false, }: { field: DomEditSelection["textFields"][number]; styles: Record; fontAssets: ImportedFontAsset[]; onImportFonts?: (files: FileList | File[]) => Promise; onSetText: (value: string, fieldKey?: string) => void; onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void; autoFocus?: boolean; }) { const weight = getTextStyleValue(field, styles, "font-weight", "400"); const weightOptions = detectAvailableWeights( field.computedStyles["font-family"] || styles["font-family"] || "", ); const align = getTextStyleValue(field, styles, "text-align", "start"); const textTransform = getTextStyleValue(field, styles, "text-transform", "none"); const fontStyle = getTextStyleValue(field, styles, "font-style", "normal"); return ( <> {({ value, onCommit }) => ( onSetText(next, field.key))} /> )} {({ value, onCommit }) => ( onSetTextFieldStyle(field.key, "font-family", next))} /> )} onSetTextFieldStyle(field.key, "font-size", next)} />
Weight
onSetTextFieldStyle( field.key, "letter-spacing", normalizeTextMetricValue("letter-spacing", next), ) } onReset={() => onSetTextFieldStyle(field.key, "letter-spacing", "")} /> onSetTextFieldStyle( field.key, "line-height", normalizeTextMetricValue("line-height", next), ) } onReset={() => onSetTextFieldStyle(field.key, "line-height", "")} /> ({ key: option.key, node: option.node, label: option.label, active: align === option.key || (option.key === "left" && align === "start") || (option.key === "right" && align === "end"), }))} onChange={(next) => { // Re-clicking the option that's already visually active for a // logical value (authored "start"/"end") must not rewrite it to // the physical "left"/"right" โ€” that destroys the logical // semantics and is wrong for RTL content. Only write when the // user actually picked a different alignment. if ((next === "left" && align === "start") || (next === "right" && align === "end")) { return; } onSetTextFieldStyle(field.key, "text-align", next); }} /> ({ key: option.key, node: option.node, label: option.label, active: textTransform === option.key, })), { key: "normal", node: "A", label: "upright", active: fontStyle === "normal" }, { key: "italic", node: "A", label: "italic", active: fontStyle === "italic" }, ]} spacerAfterIndex={2} onChange={(next) => { if (next === "normal" || next === "italic") { onSetTextFieldStyle(field.key, "font-style", next); } else { onSetTextFieldStyle(field.key, "text-transform", next); } }} /> {({ value, onCommit }) => ( onSetTextFieldStyle(field.key, "color", next))} /> )} ); } export function FlatTextSection({ element, styles, fontAssets, onImportFonts, onSetText, onSetTextFieldStyle, onAddTextField, onRemoveTextField, }: { element: DomEditSelection; styles: Record; fontAssets: ImportedFontAsset[]; onImportFonts?: (files: FileList | File[]) => Promise; onSetText: (value: string, fieldKey?: string) => void; onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void; onAddTextField: (afterFieldKey?: string) => string | Promise | null; onRemoveTextField: (fieldKey: string) => void; }) { const [activeFieldKey, setActiveFieldKey] = useState( element.textFields[0]?.key ?? null, ); useEffect(() => { const nextFields = element.textFields; setActiveFieldKey((current) => { if (current && nextFields.some((field) => field.key === current)) return current; return nextFields[0]?.key ?? null; }); }, [element.id, element.selector, element.textFields]); if (!isTextEditableSelection(element)) return null; const textFields = element.textFields; const activeField = textFields.find((field) => field.key === activeFieldKey) ?? textFields[0]; if (!activeField) return null; if (textFields.length > 1) { return (
void Promise.resolve(onAddTextField(activeField.key)).then((nextKey) => { if (nextKey) setActiveFieldKey(nextKey); }) } onRemove={onRemoveTextField} />
); } return (
); } /* ------------------------------------------------------------------ */ /* Multi-field layer list (design_handoff_studio_inspector, #10a โ€” */ /* no mock exists for this row; layout originated by this plan, */ /* following the "left-rule nested content" convention established */ /* by Text's own content block, Motion's effect cards, and Media's */ /* cutout block. Flag for design review.) */ /* ------------------------------------------------------------------ */ export function FlatTextLayerList({ fields, activeFieldKey, styles, onSelect, onAdd, onRemove, }: { fields: DomEditSelection["textFields"]; activeFieldKey: string; styles: Record; onSelect: (fieldKey: string) => void; onAdd: () => void; onRemove: (fieldKey: string) => void; }) { return (
Text layers
{fields.map((field, index) => { const active = field.key === activeFieldKey; return (
onSelect(field.key)} className={`flex min-h-[26px] cursor-pointer items-center gap-2 rounded px-1 ${ active ? "bg-panel-accent/10" : "hover:bg-panel-hover" }`} > {formatTextFieldPreview(field.value) || `Text ${index + 1}`} {field.tagName} {fields.length > 1 && ( )}
); })}
); }