返回 oh-my-ppt
InsertToolRow.tsx
1 import { useState } from 'react'
2 import {
3 ChartColumn,
4 ChartLine,
5 ChartPie,
6 ChevronDown,
7 Copy,
8 Donut,
9 ImagePlus,
10 Layers,
11 Loader2,
12 Radar,
13 Shapes,
14 Sigma,
15 Smile,
16 Sparkles,
17 Trash2,
18 Type,
19 Video
20 } from 'lucide-react'
21 import { ART_TEXT_TEMPLATES } from '@renderer/lib/artTextTemplates'
22 import {
23 ICON_LIST,
24 ICON_VIEWBOX,
25 SHAPE_LIST,
26 serializeIconInner,
27 type InsertShapeType
28 } from '@renderer/components/session-detail/workspace/insert-shapes'
29 import {
30 CHART_TYPE_LIST,
31 type InsertChartType
32 } from '@renderer/components/session-detail/workspace/insert-charts'
33 import { useT, type I18nKey } from '@renderer/i18n'
34 import {
35 useEditSessionStore,
36 useSessionDetailRuntimeStore
37 } from '@renderer/store'
38 import {
39 AlertDialog,
40 AlertDialogAction,
41 AlertDialogCancel,
42 AlertDialogContent,
43 AlertDialogDescription,
44 AlertDialogTitle
45 } from '../../../../ui/AlertDialog'
46 import {
47 DropdownMenu,
48 DropdownMenuContent,
49 DropdownMenuItem,
50 DropdownMenuTrigger
51 } from '../../../../ui/DropdownMenu'
52 import { Popover, PopoverContent, PopoverTrigger } from '../../../../ui/Popover'
53 import { Tooltip, TooltipContent, TooltipTrigger } from '../../../../ui/Tooltip'
54 import type { InsertAssetType } from '../types'
55 import { ToolRowShell } from './ToolRowShell'
56 import type { ToolRowProps } from './types'
57
58 const toolButtonClass =
59 'group inline-flex h-7 min-w-[78px] shrink-0 items-center justify-center gap-1 rounded-full bg-[#fffaf1]/92 px-2.5 text-[10px] font-bold leading-none text-[#2f3b28] shadow-[inset_0_1px_0_rgba(255,255,255,0.7),0_4px_10px_rgba(47,59,40,0.09)] transition-colors hover:bg-white hover:text-[#1f2a1b] disabled:pointer-events-none disabled:opacity-40'
60 const iconWrapClass =
61 'inline-flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-full bg-[#d4e4c1]/78 text-[#3e4a32] group-hover:bg-[#8fbc8f]/42'
62 const iconClass = 'h-2.5 w-2.5'
63 const selectedToolButtonClass =
64 'inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full transition-colors disabled:pointer-events-none disabled:opacity-45'
65
66 // Keep these compact previews visually aligned with the full-size templates in artTextTemplates.ts.
67 const artTextPreviewStyles = `
68 .ppt-art-preview-card {
69 min-height: 90px;
70 border-radius: 8px;
71 border: 1px solid rgba(216, 204, 181, 0.78);
72 background: linear-gradient(145deg, rgba(31, 38, 29, 0.96), rgba(18, 24, 22, 0.94));
73 color: #fff;
74 padding: 9px;
75 text-align: left;
76 transition: border-color 0.16s ease, transform 0.16s ease, box-shadow 0.16s ease;
77 }
78 .ppt-art-preview-card:hover,
79 .ppt-art-preview-card:focus-visible {
80 border-color: rgba(143, 188, 143, 0.95);
81 box-shadow: 0 10px 24px rgba(47, 59, 40, 0.18);
82 transform: translateY(-1px);
83 outline: none;
84 }
85 .ppt-art-preview-label {
86 color: rgba(255, 250, 241, 0.76);
87 font-size: 10px;
88 font-weight: 700;
89 line-height: 1;
90 }
91 .ppt-art-preview-stage {
92 display: flex;
93 min-height: 52px;
94 align-items: center;
95 justify-content: center;
96 overflow: hidden;
97 padding-top: 7px;
98 text-align: center;
99 }
100 .ppt-art-preview-text {
101 display: inline-block;
102 max-width: 100%;
103 font-size: 18px;
104 font-weight: 850;
105 line-height: 1.12;
106 letter-spacing: 0;
107 }
108 .ppt-art-preview-gradient-shine .ppt-art-preview-text {
109 background: linear-gradient(135deg, #ffd89b, #c7e9fb, #f6d5f7, #a0e9ff);
110 background-size: 300% 300%;
111 background-clip: text;
112 -webkit-background-clip: text;
113 color: transparent;
114 animation: pptArtPreviewFlowGradient 6s ease infinite;
115 }
116 .ppt-art-preview-neon-glow .ppt-art-preview-text {
117 color: #0ff;
118 text-shadow: 0 0 5px #0ff, 0 0 12px #0ff, 0 0 22px #00aaff;
119 animation: pptArtPreviewNeonPulse 1.2s ease-in-out infinite alternate;
120 }
121 .ppt-art-preview-shadow-3d .ppt-art-preview-text {
122 color: #f3b33d;
123 text-shadow: 1px 1px 0 #7a4c1a, 2px 2px 0 #6b4215, 3px 3px 0 #5a3710, 4px 4px 0 #482d0c;
124 }
125 .ppt-art-preview-wave-text .ppt-art-preview-text {
126 display: flex;
127 gap: 2px;
128 }
129 .ppt-art-preview-wave-char {
130 display: inline-block;
131 animation: pptArtPreviewWaveFloat 1.4s infinite ease-in-out;
132 }
133 .ppt-art-preview-stroke-text .ppt-art-preview-text {
134 color: transparent;
135 -webkit-text-stroke: 1px #00e0ff;
136 text-stroke: 1px #00e0ff;
137 text-shadow: 0 0 10px rgba(0, 255, 255, 0.36);
138 }
139 .ppt-art-preview-typewriter .ppt-art-preview-text {
140 overflow: hidden;
141 white-space: nowrap;
142 border-right: 2px solid #f0f;
143 animation: pptArtPreviewTyping 3s steps(20, end) infinite, pptArtPreviewBlink 0.75s step-end infinite;
144 }
145 .ppt-art-preview-glitch .ppt-art-preview-text {
146 position: relative;
147 color: #fff;
148 text-shadow: 0.05em 0 0 rgba(255, 0, 0, 0.75), -0.05em -0.025em 0 rgba(0, 255, 0, 0.75);
149 animation: pptArtPreviewGlitchShake 0.3s infinite;
150 }
151 .ppt-art-preview-glitch .ppt-art-preview-text::before,
152 .ppt-art-preview-glitch .ppt-art-preview-text::after {
153 content: attr(data-text);
154 position: absolute;
155 inset: 0;
156 }
157 .ppt-art-preview-glitch .ppt-art-preview-text::before {
158 left: 1px;
159 text-shadow: -1px 0 red;
160 clip: rect(8px, 200px, 20px, 0);
161 }
162 .ppt-art-preview-glitch .ppt-art-preview-text::after {
163 left: -1px;
164 text-shadow: -1px 0 blue;
165 clip: rect(24px, 200px, 46px, 0);
166 }
167 .ppt-art-preview-fire-text .ppt-art-preview-text {
168 background-image: linear-gradient(0deg, #ff4d00, #ff9900, #ffcc00);
169 background-clip: text;
170 -webkit-background-clip: text;
171 color: transparent;
172 text-shadow: 0 0 10px #ff7b00, 0 0 16px #ff4400;
173 animation: pptArtPreviewFireFlicker 0.2s infinite alternate;
174 }
175 .ppt-art-preview-glass-text .ppt-art-preview-text {
176 border: 1px solid rgba(255, 255, 255, 0.25);
177 border-radius: 999px;
178 background: rgba(255, 255, 255, 0.12);
179 box-shadow: 0 4px 18px rgba(0, 0, 0, 0.2);
180 color: #fff3cf;
181 padding: 7px 12px;
182 }
183 .ppt-art-preview-rotate-3d .ppt-art-preview-stage {
184 perspective: 480px;
185 }
186 .ppt-art-preview-rotate-3d .ppt-art-preview-text {
187 color: #fe9c8f;
188 transform-style: preserve-3d;
189 animation: pptArtPreviewRotateText 5s infinite linear;
190 }
191 .ppt-art-preview-rainbow .ppt-art-preview-text {
192 animation: pptArtPreviewRainbowColor 2s linear infinite;
193 }
194 .ppt-art-preview-liquid .ppt-art-preview-text {
195 background: linear-gradient(45deg, #ff6bcb, #ffb86c, #2ed8ff, #ff6bcb);
196 background-size: 400% 400%;
197 background-clip: text;
198 -webkit-background-clip: text;
199 color: transparent;
200 animation: pptArtPreviewLiquidMove 5s ease infinite;
201 }
202 .ppt-art-preview-bounce-text .ppt-art-preview-text {
203 animation: pptArtPreviewTextBounce 1.2s ease infinite;
204 }
205 .ppt-art-preview-float-shadow .ppt-art-preview-text {
206 text-shadow: 0 8px 14px rgba(0, 0, 0, 0.35);
207 animation: pptArtPreviewFloatUp 2.6s infinite alternate;
208 }
209 .ppt-art-preview-metal-text .ppt-art-preview-text {
210 background: linear-gradient(135deg, #e8e8e8 0%, #a0a0b0 40%, #f5f5ff 50%, #b0b0c0 80%, #dfdfdf 100%);
211 background-clip: text;
212 -webkit-background-clip: text;
213 color: transparent;
214 filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.34)) drop-shadow(0 3px 7px rgba(0, 0, 0, 0.18));
215 }
216 @keyframes pptArtPreviewFlowGradient {
217 0% { background-position: 0% 50%; }
218 50% { background-position: 100% 50%; }
219 100% { background-position: 0% 50%; }
220 }
221 @keyframes pptArtPreviewNeonPulse {
222 0% { text-shadow: 0 0 2px #0ff, 0 0 6px #0aa; opacity: 0.9; }
223 100% { text-shadow: 0 0 8px #0ff, 0 0 22px #0cf; opacity: 1; }
224 }
225 @keyframes pptArtPreviewWaveFloat {
226 0%, 100% { transform: translateY(0); }
227 50% { transform: translateY(-6px); }
228 }
229 @keyframes pptArtPreviewTyping {
230 0% { width: 0; }
231 40% { width: 100%; }
232 80% { width: 100%; }
233 100% { width: 0; }
234 }
235 @keyframes pptArtPreviewBlink {
236 0%, 100% { border-color: #f0f; }
237 50% { border-color: transparent; }
238 }
239 @keyframes pptArtPreviewGlitchShake {
240 0% { transform: translate(0); }
241 40% { transform: translate(-1px, -1px); }
242 80% { transform: translate(1px, -1px); }
243 100% { transform: translate(0); }
244 }
245 @keyframes pptArtPreviewFireFlicker {
246 0% { opacity: 0.95; text-shadow: 0 0 8px #ff5500; }
247 100% { opacity: 1; text-shadow: 0 0 15px #ffaa33, 0 0 5px #ff3300; }
248 }
249 @keyframes pptArtPreviewRotateText {
250 0% { transform: rotateY(0deg); }
251 100% { transform: rotateY(360deg); }
252 }
253 @keyframes pptArtPreviewRainbowColor {
254 0% { color: #ff0000; }
255 17% { color: #ff9900; }
256 33% { color: #ffff00; }
257 50% { color: #00ff00; }
258 67% { color: #0099ff; }
259 83% { color: #8b00ff; }
260 100% { color: #ff0000; }
261 }
262 @keyframes pptArtPreviewLiquidMove {
263 0% { background-position: 0% 50%; }
264 50% { background-position: 100% 50%; }
265 100% { background-position: 0% 50%; }
266 }
267 @keyframes pptArtPreviewTextBounce {
268 0%, 100% { transform: scale(1); letter-spacing: 0; }
269 50% { transform: scale(1.05); letter-spacing: 2px; color: #fff2a0; }
270 }
271 @keyframes pptArtPreviewFloatUp {
272 0% { transform: translateY(0); text-shadow: 0 5px 12px rgba(0, 0, 0, 0.24); }
273 100% { transform: translateY(-5px); text-shadow: 0 18px 22px rgba(0, 0, 0, 0.45); }
274 }
275 `
276
277 export function InsertToolRow({ disabled }: ToolRowProps): React.JSX.Element {
278 const t = useT()
279 const [artTextOpen, setArtTextOpen] = useState(false)
280 const [shapeOpen, setShapeOpen] = useState(false)
281 const [iconOpen, setIconOpen] = useState(false)
282 const [chartOpen, setChartOpen] = useState(false)
283 const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
284 const actions = useSessionDetailRuntimeStore((state) => state.workspaceRibbonActions)
285 const selection = useEditSessionStore((state) => state.selection)
286 const isApplyingSyncElement = useEditSessionStore((state) => state.isApplyingSyncElement)
287
288 const renderSelectedElementTools = (): React.JSX.Element | null => {
289 if (!selection) return null
290 return (
291 <div className="absolute right-2 top-1/2 z-10 flex -translate-y-1/2 shrink-0 items-center gap-0.5 rounded-full border border-[#ead29a]/70 bg-[#fff4d8]/92 px-0.5 py-0.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.62),0_4px_12px_rgba(167,116,34,0.1)] backdrop-blur">
292 <Tooltip>
293 <TooltipTrigger asChild>
294 <button
295 type="button"
296 className={`${selectedToolButtonClass} bg-[#d8942f] text-white shadow-[0_4px_10px_rgba(168,105,23,0.2)] hover:bg-[#bd7621]`}
297 onClick={() => actions?.onApplySelectedToAllPages()}
298 disabled={disabled || isApplyingSyncElement}
299 aria-label={t('sessionDetail.applyElementToAllPages')}
300 >
301 {isApplyingSyncElement ? (
302 <Loader2 className="h-3 w-3 animate-spin" />
303 ) : (
304 <Layers className="h-3 w-3" />
305 )}
306 </button>
307 </TooltipTrigger>
308 <TooltipContent side="bottom">
309 {isApplyingSyncElement
310 ? t('sessionDetail.syncElementApplying')
311 : t('sessionDetail.applyElementToAllPages')}
312 </TooltipContent>
313 </Tooltip>
314 <Tooltip>
315 <TooltipTrigger asChild>
316 <button
317 type="button"
318 className={`${selectedToolButtonClass} text-[#6b765c] hover:bg-[#f9e7bd] hover:text-[#3e4a32]`}
319 onClick={() => actions?.onCopySelectedElement()}
320 disabled={disabled || isApplyingSyncElement}
321 aria-label={t('sessionDetail.copyElement')}
322 >
323 <Copy className="h-3 w-3" />
324 </button>
325 </TooltipTrigger>
326 <TooltipContent side="bottom">{t('sessionDetail.copyElement')}</TooltipContent>
327 </Tooltip>
328 <Tooltip>
329 <TooltipTrigger asChild>
330 <button
331 type="button"
332 className={`${selectedToolButtonClass} text-[#9a5a50] hover:bg-[#f8dfd9] hover:text-[#7d342c]`}
333 onClick={() => setDeleteConfirmOpen(true)}
334 disabled={disabled || isApplyingSyncElement}
335 aria-label={t('sessionDetail.deleteElement')}
336 >
337 <Trash2 className="h-3 w-3" />
338 </button>
339 </TooltipTrigger>
340 <TooltipContent side="bottom">{t('sessionDetail.deleteElement')}</TooltipContent>
341 </Tooltip>
342 <AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
343 <AlertDialogContent>
344 <AlertDialogTitle>{t('sessionDetail.deleteElement')}</AlertDialogTitle>
345 <AlertDialogDescription>
346 {t('sessionDetail.deleteElementConfirm')}
347 </AlertDialogDescription>
348 <div className="flex justify-end gap-2">
349 <AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
350 <AlertDialogAction
351 className="bg-[#c0392b] text-white hover:bg-[#a93226]"
352 onClick={() => actions?.onDeleteSelectedElement()}
353 >
354 {t('common.delete')}
355 </AlertDialogAction>
356 </div>
357 </AlertDialogContent>
358 </AlertDialog>
359 </div>
360 )
361 }
362
363 const renderMediaDropdown = (type: InsertAssetType): React.JSX.Element => {
364 const Icon = type === 'image' ? ImagePlus : Video
365 const label = type === 'image' ? t('editMode.addImage') : t('editMode.addVideo')
366 return (
367 <DropdownMenu>
368 <DropdownMenuTrigger asChild>
369 <button type="button" className={toolButtonClass} disabled={disabled}>
370 <span className={iconWrapClass}>
371 <Icon className={iconClass} />
372 </span>
373 {label}
374 <ChevronDown className="h-2.5 w-2.5 opacity-70" />
375 </button>
376 </DropdownMenuTrigger>
377 <DropdownMenuContent align="start" className="min-w-[10rem]">
378 <DropdownMenuItem onClick={() => actions?.onAddFromLibrary(type)}>
379 {t('editMode.fromLibrary')}
380 </DropdownMenuItem>
381 <DropdownMenuItem onClick={() => actions?.onAddFromLocal(type)}>
382 {t('editMode.fromLocal')}
383 </DropdownMenuItem>
384 </DropdownMenuContent>
385 </DropdownMenu>
386 )
387 }
388
389 const chartTypeIcons: Record<InsertChartType, typeof ChartColumn> = {
390 bar: ChartColumn,
391 line: ChartLine,
392 pie: ChartPie,
393 doughnut: Donut,
394 radar: Radar
395 }
396
397 const renderChartPopover = (): React.JSX.Element => (
398 <Popover open={chartOpen} onOpenChange={setChartOpen}>
399 <PopoverTrigger asChild>
400 <button type="button" className={toolButtonClass} disabled={disabled}>
401 <span className={iconWrapClass}>
402 <ChartColumn className={iconClass} />
403 </span>
404 {t('editMode.chart')}
405 <ChevronDown className="h-2.5 w-2.5 opacity-70" />
406 </button>
407 </PopoverTrigger>
408 <PopoverContent
409 align="start"
410 className="w-[280px] max-w-[calc(100vw-2rem)] border-[#d8ccb5]/85 bg-[#fff9ef] p-2"
411 >
412 <div className="grid grid-cols-3 gap-2">
413 {CHART_TYPE_LIST.map((item) => {
414 const Icon = chartTypeIcons[item.type]
415 return (
416 <button
417 type="button"
418 key={item.type}
419 className="flex min-h-[60px] flex-col items-center justify-center gap-1 rounded-lg border border-[#d8ccb5]/70 bg-white/70 px-2 py-2 text-[10px] font-bold text-[#3e4a32] transition-colors hover:border-[#8fbc8f] hover:bg-white"
420 onClick={() => {
421 actions?.onAddChart(item.type)
422 setChartOpen(false)
423 }}
424 >
425 <Icon className="h-5 w-5 text-[#5d6b4d]" />
426 <span>{t(item.labelKey as I18nKey)}</span>
427 </button>
428 )
429 })}
430 </div>
431 </PopoverContent>
432 </Popover>
433 )
434
435 const renderArtTextPreview = (
436 template: (typeof ART_TEXT_TEMPLATES)[number]
437 ): React.JSX.Element => {
438 if (template.id === 'wave-text') {
439 return (
440 <span className="ppt-art-preview-text">
441 {Array.from(template.defaultText).map((char, index) => (
442 <span
443 key={`${char}-${index}`}
444 className="ppt-art-preview-wave-char"
445 style={{ animationDelay: `${index * 0.08}s` }}
446 >
447 {char === ' ' ? '\u00a0' : char}
448 </span>
449 ))}
450 </span>
451 )
452 }
453
454 return (
455 <span
456 className="ppt-art-preview-text"
457 data-text={template.id === 'glitch' ? template.defaultText : undefined}
458 >
459 {template.defaultText}
460 </span>
461 )
462 }
463
464 const renderArtTextDropdown = (): React.JSX.Element => (
465 <Popover open={artTextOpen} onOpenChange={setArtTextOpen}>
466 <PopoverTrigger asChild>
467 <button type="button" className={toolButtonClass} disabled={disabled}>
468 <span className={iconWrapClass}>
469 <Sparkles className={iconClass} />
470 </span>
471 {t('editMode.artText')}
472 <ChevronDown className="h-2.5 w-2.5 opacity-70" />
473 </button>
474 </PopoverTrigger>
475 <PopoverContent
476 align="start"
477 className="w-[560px] max-w-[calc(100vw-2rem)] border-[#d8ccb5]/85 bg-[#fff9ef] p-2"
478 >
479 <style>{artTextPreviewStyles}</style>
480 <div className="grid max-h-[420px] grid-cols-3 gap-2 overflow-y-auto pr-1">
481 {ART_TEXT_TEMPLATES.map((template) => (
482 <button
483 type="button"
484 key={template.id}
485 className={`ppt-art-preview-card ppt-art-preview-${template.id}`}
486 onClick={() => {
487 actions?.onAddArtText(template.id)
488 setArtTextOpen(false)
489 }}
490 >
491 <div className="ppt-art-preview-label">{template.label}</div>
492 <div className="ppt-art-preview-stage">{renderArtTextPreview(template)}</div>
493 </button>
494 ))}
495 </div>
496 </PopoverContent>
497 </Popover>
498 )
499
500 const shapeLabelKey: Record<InsertShapeType, I18nKey> = {
501 rect: 'editMode.shapeRect',
502 'rounded-rect': 'editMode.shapeRoundedRect',
503 ellipse: 'editMode.shapeEllipse',
504 triangle: 'editMode.shapeTriangle',
505 diamond: 'editMode.shapeDiamond',
506 pentagon: 'editMode.shapePentagon',
507 hexagon: 'editMode.shapeHexagon',
508 parallelogram: 'editMode.shapeParallelogram',
509 trapezoid: 'editMode.shapeTrapezoid',
510 'star-5': 'editMode.shapeStar',
511 line: 'editMode.shapeLine',
512 'arrow-right': 'editMode.shapeArrowRight',
513 'chevron-right': 'editMode.shapeChevron'
514 }
515
516 const renderShapePreviewInner = (type: InsertShapeType): string => {
517 const def = SHAPE_LIST.find((s) => s.type === type)
518 if (!def) return ''
519 return def.renderInner(def.defaultWidth, def.defaultHeight, {
520 fill: def.defaultFill,
521 stroke: def.defaultStroke,
522 strokeWidth: def.strokeWidth
523 })
524 }
525
526 const renderShapePopover = (): React.JSX.Element => (
527 <Popover open={shapeOpen} onOpenChange={setShapeOpen}>
528 <PopoverTrigger asChild>
529 <button type="button" className={toolButtonClass} disabled={disabled}>
530 <span className={iconWrapClass}>
531 <Shapes className={iconClass} />
532 </span>
533 {t('editMode.addShape')}
534 <ChevronDown className="h-2.5 w-2.5 opacity-70" />
535 </button>
536 </PopoverTrigger>
537 <PopoverContent
538 align="start"
539 className="w-[280px] max-w-[calc(100vw-2rem)] border-[#d8ccb5]/85 bg-[#fff9ef] p-2"
540 >
541 <div className="grid grid-cols-3 gap-2">
542 {SHAPE_LIST.map((def) => (
543 <button
544 type="button"
545 key={def.type}
546 className="flex min-h-[68px] flex-col items-center justify-center gap-1 rounded-lg border border-[#d8ccb5]/70 bg-white/70 px-2 py-2 text-[10px] font-bold text-[#3e4a32] transition-colors hover:border-[#8fbc8f] hover:bg-white"
547 onClick={() => {
548 actions?.onAddShape(def.type)
549 setShapeOpen(false)
550 }}
551 >
552 <svg
553 viewBox={`0 0 ${def.defaultWidth} ${def.defaultHeight}`}
554 className="h-9 w-12"
555 preserveAspectRatio="xMidYMid meet"
556 dangerouslySetInnerHTML={{ __html: renderShapePreviewInner(def.type) }}
557 />
558 <span>{t(shapeLabelKey[def.type])}</span>
559 </button>
560 ))}
561 </div>
562 </PopoverContent>
563 </Popover>
564 )
565
566 const strokeIcons = ICON_LIST.filter((icon) => icon.variant !== 'badge')
567 const badgeIcons = ICON_LIST.filter((icon) => icon.variant === 'badge')
568
569 const renderIconGrid = (icons: typeof ICON_LIST): React.JSX.Element => (
570 <div className="grid grid-cols-6 gap-1.5">
571 {icons.map((icon) => {
572 const isBadge = icon.variant === 'badge'
573 return (
574 <button
575 type="button"
576 key={icon.id}
577 title={icon.label}
578 className="flex h-12 items-center justify-center rounded-lg border border-transparent text-[#3e4a32] transition-colors hover:border-[#8fbc8f] hover:bg-white"
579 onClick={() => {
580 actions?.onAddIcon(icon.id)
581 setIconOpen(false)
582 }}
583 >
584 <svg
585 viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
586 className={isBadge ? 'h-6 w-6' : 'h-5 w-5'}
587 fill="none"
588 stroke={isBadge ? 'none' : 'currentColor'}
589 strokeWidth={isBadge ? 0 : 2}
590 strokeLinecap="round"
591 strokeLinejoin="round"
592 dangerouslySetInnerHTML={{ __html: serializeIconInner(icon) }}
593 />
594 </button>
595 )
596 })}
597 </div>
598 )
599
600 const renderIconPopover = (): React.JSX.Element => (
601 <Popover open={iconOpen} onOpenChange={setIconOpen}>
602 <PopoverTrigger asChild>
603 <button type="button" className={toolButtonClass} disabled={disabled}>
604 <span className={iconWrapClass}>
605 <Smile className={iconClass} />
606 </span>
607 {t('editMode.addIcon')}
608 <ChevronDown className="h-2.5 w-2.5 opacity-70" />
609 </button>
610 </PopoverTrigger>
611 <PopoverContent
612 align="start"
613 className="w-[390px] max-w-[calc(100vw-2rem)] border-[#d8ccb5]/85 bg-[#fff9ef] p-2"
614 >
615 <div className="max-h-[360px] space-y-2 overflow-y-auto pr-1">
616 <div>
617 <div className="mb-1 px-1 text-[10px] font-bold uppercase tracking-wide text-[#7a875f]">
618 {t('editMode.iconSectionIcons')}
619 </div>
620 {renderIconGrid(strokeIcons)}
621 </div>
622 <div>
623 <div className="mb-1 px-1 text-[10px] font-bold uppercase tracking-wide text-[#7a875f]">
624 {t('editMode.iconSectionNumbers')}
625 </div>
626 {renderIconGrid(badgeIcons)}
627 </div>
628 </div>
629 </PopoverContent>
630 </Popover>
631 )
632
633 return (
634 <div className="relative">
635 {renderSelectedElementTools()}
636 <ToolRowShell>
637 <button
638 type="button"
639 className={toolButtonClass}
640 onClick={() => actions?.onAddText()}
641 disabled={disabled}
642 >
643 <span className={iconWrapClass}>
644 <Type className={iconClass} />
645 </span>
646 {t('editMode.addText')}
647 </button>
648 {renderArtTextDropdown()}
649 {renderShapePopover()}
650 {renderIconPopover()}
651 {renderMediaDropdown('image')}
652 {renderMediaDropdown('video')}
653 {renderChartPopover()}
654 <button
655 type="button"
656 className={toolButtonClass}
657 onClick={() => actions?.onAddFormula()}
658 disabled={disabled}
659 >
660 <span className={iconWrapClass}>
661 <Sigma className={iconClass} />
662 </span>
663 {t('editMode.formula')}
664 </button>
665 </ToolRowShell>
666 </div>
667 )
668 }
669
669 lines Plain Text