返回 oh-my-ppt
HtmlEditorInspectorPanel.tsx
根目录 / src / renderer / src / components / html-editor / HtmlEditorInspectorPanel.tsx
1 import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Type, X } from 'lucide-react'
2 import { RichTextBox } from '../ui/RichTextBox'
3 import { ToggleGroup, ToggleGroupItem } from '../ui/ToggleGroup'
4 import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip'
5 import { InspectorSection } from '../session-detail/element-inspector/InspectorSection'
6 import { AppearanceInspector } from '../session-detail/element-inspector/AppearanceInspector'
7 import { ArtTextInspector } from '../session-detail/element-inspector/ArtTextInspector'
8 import { ChartInspector } from '../session-detail/element-inspector/ChartInspector'
9 import { FormulaInspector } from '../session-detail/element-inspector/FormulaInspector'
10 import { LayerInspector } from '../session-detail/element-inspector/LayerInspector'
11 import { LayoutInspector } from '../session-detail/element-inspector/LayoutInspector'
12 import { MediaInspector } from '../session-detail/element-inspector/MediaInspector'
13 import type { ElementEditDraft } from '../session-detail/element-inspector/types'
14 import {
15 getElementKindLabel,
16 hasCapability,
17 isArtTextSelection
18 } from '../session-detail/element-inspector/types'
19 import type { EditSelectionPayload } from '@arcsin1/presentation-editor-runtime'
20 import { useT } from '@renderer/i18n'
21
22 const TEXT_ALIGN_OPTIONS = [
23 { value: 'left', icon: AlignLeft },
24 { value: 'center', icon: AlignCenter },
25 { value: 'right', icon: AlignRight },
26 { value: 'justify', icon: AlignJustify }
27 ] as const
28
29 type DraftChange = (
30 draft: ElementEditDraft,
31 options?: { commit?: boolean; fields?: Array<keyof ElementEditDraft> }
32 ) => void
33
34 function getPreviewScale(fontSize: string): number | undefined {
35 const parsed = Number(String(fontSize || '').replace(/px$/i, ''))
36 if (!Number.isFinite(parsed) || parsed <= 36) return undefined
37 return 1 / 3
38 }
39
40 export function HtmlEditorTextInspector({
41 draft,
42 onDraftChange
43 }: {
44 draft: ElementEditDraft
45 onDraftChange: DraftChange
46 }): React.JSX.Element {
47 const t = useT()
48 const textAlign = draft.textAlign || 'left'
49 const previewScale = getPreviewScale(draft.fontSize)
50 const getAlignLabel = (value: (typeof TEXT_ALIGN_OPTIONS)[number]['value']): string => {
51 switch (value) {
52 case 'center':
53 return t('sessionDetail.alignCenter')
54 case 'right':
55 return t('sessionDetail.alignRight')
56 case 'justify':
57 return t('sessionDetail.alignJustify')
58 default:
59 return t('sessionDetail.alignLeft')
60 }
61 }
62
63 return (
64 <InspectorSection
65 title={t('sessionDetail.textContent')}
66 icon={<Type className="h-3.5 w-3.5 text-[#7a875f]" />}
67 >
68 <RichTextBox
69 value={draft.html}
70 fallbackText={draft.text}
71 defaultColor={draft.color}
72 defaultFontSize={draft.fontSize}
73 previewScale={previewScale}
74 onChange={(value) => onDraftChange({ ...draft, html: value.html, text: value.text })}
75 onCommit={(value) =>
76 onDraftChange(
77 { ...draft, html: value.html, text: value.text },
78 { commit: true, fields: ['html'] }
79 )
80 }
81 />
82 <div className="mt-3 space-y-1.5">
83 <div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-[#8a806b]">
84 {t('sessionDetail.textAlign')}
85 </div>
86 <ToggleGroup
87 type="single"
88 value={textAlign}
89 onValueChange={(value) => {
90 if (!value) return
91 onDraftChange({ ...draft, textAlign: value }, { commit: true, fields: ['textAlign'] })
92 }}
93 aria-label={t('sessionDetail.textAlign')}
94 className="inline-flex overflow-hidden rounded-md border border-[#d9cfbd]/72 bg-[#fffaf1]/90 p-0.5 shadow-[inset_0_1px_2px_rgba(77,63,46,0.06)]"
95 >
96 {TEXT_ALIGN_OPTIONS.map(({ value, icon: Icon }) => {
97 const label = getAlignLabel(value)
98 return (
99 <Tooltip key={value}>
100 <TooltipTrigger asChild>
101 <ToggleGroupItem
102 value={value}
103 aria-label={label}
104 title={label}
105 className="rounded-[6px]"
106 onPointerDown={(event) => event.preventDefault()}
107 >
108 <Icon className="h-3.5 w-3.5" />
109 </ToggleGroupItem>
110 </TooltipTrigger>
111 <TooltipContent>{label}</TooltipContent>
112 </Tooltip>
113 )
114 })}
115 </ToggleGroup>
116 </div>
117 </InspectorSection>
118 )
119 }
120
121 export function HtmlEditorInspectorPanel({
122 selection,
123 draft,
124 onDraftChange,
125 onClose
126 }: {
127 selection: EditSelectionPayload | null
128 draft: ElementEditDraft
129 onDraftChange: DraftChange
130 onClose: () => void
131 }): React.JSX.Element {
132 const t = useT()
133 const snapshot = selection?.snapshot
134 const isArtText = isArtTextSelection(selection)
135
136 return (
137 <div className="flex min-h-0 h-full w-full flex-1 flex-col overflow-hidden">
138 <div className="relative mx-2 mt-2 overflow-hidden rounded-lg border border-[#e1d6c4]/58 bg-[#fffaf1]/68 px-2.5 py-2 shadow-[0_2px_8px_rgba(77,61,43,0.05)]">
139 <div className="relative flex items-center justify-between">
140 <div>
141 <div className="text-[10px] font-semibold uppercase tracking-[0.1em] text-[#7a875f]/90">
142 {t('sessionDetail.elementInspector')}
143 </div>
144 {selection ? (
145 <div className="mt-0.5 text-[10px] text-[#a0977e]">
146 {isArtText ? t('editMode.artText') : getElementKindLabel(selection)}
147 </div>
148 ) : null}
149 </div>
150 <button
151 type="button"
152 onClick={onClose}
153 className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-[#667257] transition-colors hover:bg-[#d4e4c1]/70 hover:text-[#34402c]"
154 aria-label={t('sessionDetail.closeInspector')}
155 title={t('sessionDetail.closeInspector')}
156 >
157 <X className="h-3.5 w-3.5" />
158 </button>
159 </div>
160 </div>
161
162 <div className="flex-1 space-y-2.5 overflow-y-auto px-2 py-2">
163 {!selection || !snapshot ? (
164 <div className="rounded-lg border border-[#e8c8c6]/62 bg-[#fdf0ef]/76 px-3 py-3 text-center shadow-[0_4px_10px_rgba(74,59,42,0.06)]">
165 <p className="whitespace-pre-line text-[11px] leading-5 text-[#8e5a53]">
166 {t('sessionDetail.inspectorUnavailable')}
167 </p>
168 </div>
169 ) : (
170 <>
171 <LayoutInspector selection={selection} draft={draft} onDraftChange={onDraftChange} />
172 {hasCapability(selection, 'layer') ? (
173 <LayerInspector selection={selection} draft={draft} onDraftChange={onDraftChange} />
174 ) : null}
175 {isArtText ? (
176 <ArtTextInspector selection={selection} draft={draft} onDraftChange={onDraftChange} />
177 ) : null}
178 {!isArtText && hasCapability(selection, 'text') ? (
179 <HtmlEditorTextInspector draft={draft} onDraftChange={onDraftChange} />
180 ) : null}
181 {hasCapability(selection, 'formula') ? (
182 <FormulaInspector selection={selection} draft={draft} onDraftChange={onDraftChange} />
183 ) : null}
184 {hasCapability(selection, 'chart') ? (
185 <ChartInspector selection={selection} draft={draft} onDraftChange={onDraftChange} />
186 ) : null}
187 {hasCapability(selection, 'appearance') ? (
188 <AppearanceInspector
189 selection={selection}
190 draft={draft}
191 onDraftChange={onDraftChange}
192 />
193 ) : null}
194 {hasCapability(selection, 'media') ? (
195 <MediaInspector selection={selection} draft={draft} onDraftChange={onDraftChange} />
196 ) : null}
197 </>
198 )}
199 </div>
200 </div>
201 )
202 }
203
203 lines Plain Text