返回 presentation-ai
lib.tsx
1 "use client";
2
3 import { BlockSelectionPlugin } from "@platejs/selection/react";
4 import { type TElement } from "@platejs/slate";
5 import {
6 AreaChart,
7 ArrowDownUp,
8 ArrowLeftRight,
9 ArrowRight,
10 BarChart3,
11 ChartScatter,
12 Circle,
13 CircleDashed,
14 Clock,
15 Diamond,
16 Flower,
17 Funnel,
18 GitCompare,
19 Grid2x2,
20 Grid3x3,
21 ImageIcon,
22 Layers,
23 Leaf,
24 List,
25 ListOrdered,
26 PieChart,
27 Pill,
28 Quote,
29 Radar,
30 RefreshCw,
31 Scale,
32 Square,
33 SquareAsterisk,
34 SquareDashedBottomCode,
35 SquareSplitVertical,
36 SquareStack,
37 Star,
38 TrendingUp,
39 Triangle,
40 Workflow,
41 } from "lucide-react";
42 import { KEYS, NodeApi, PathApi, type NodeEntry, type Path } from "platejs";
43 import { type PlateEditor } from "platejs/react";
44 import { type CSSProperties, type ReactNode } from "react";
45 import { FaStairs } from "react-icons/fa6";
46
47 import { type MyEditor } from "@/components/plate/editor-kit";
48 import {
49 getConnectedCircleItemPosition,
50 getConnectedCircleItemTransform,
51 } from "./custom-elements/connected-circles-layout";
52 import {
53 getSnakeGridColumn,
54 getSnakeGridRow,
55 } from "./custom-elements/snake-shared";
56 import {
57 updateSiblingsAfterDrop,
58 updateSiblingsForcefully,
59 } from "./dnd/utils/updateSiblingsForcefully";
60 import { PALETTE_DROP_MUTABLE_KEY } from "./utils/paletteDrop";
61
62 export const BULLET_ITEM = "bullet";
63 export const BULLET_GROUP = "bullets";
64 export const STAIR_ITEM = "stair-item";
65 export const STAIRCASE_GROUP = "staircase";
66 export const CYCLE_ITEM = "cycle-item";
67 export const CYCLE_GROUP = "cycle";
68 export const ICON_ELEMENT = "icon";
69 export const ICON_LIST_ITEM = "icon-item";
70 export const ICON_LIST = "icons";
71 export const ARROW_LIST = "arrows";
72 export const ARROW_LIST_ITEM = "arrow-item";
73 export const PYRAMID_GROUP = "pyramid";
74 export const PYRAMID_ITEM = "pyramid-item";
75 export const TIMELINE_GROUP = "timeline";
76 export const TIMELINE_ITEM = "timeline-item";
77
78 // New components
79 export const BOX_GROUP = "boxes";
80 export const BOX_ITEM = "box-item";
81 export const COLUMN_GROUP = "column_group";
82 export const COLUMN_ITEM = "column";
83
84 export const COMPARE_GROUP = "compare";
85 export const COMPARE_SIDE = "compare-side";
86
87 export const BEFORE_AFTER_GROUP = "before-after";
88 export const BEFORE_AFTER_SIDE = "before-after-side";
89
90 export const PROS_CONS_GROUP = "pros-cons";
91 export const PROS_ITEM = "pros-item";
92 export const CONS_ITEM = "cons-item";
93
94 export const SEQUENCE_ARROW_GROUP = "arrow-vertical";
95 export const SEQUENCE_ARROW_ITEM = "arrow-vertical-item";
96
97 export const STATS_GROUP = "stats";
98 export const STATS_ITEM = "stats-item";
99
100 export const FLEX_BOX = "flex_box";
101
102 export const SLOPE_GROUP = "slope";
103 export const SLOPE_ITEM = "slope-item";
104 export const CONNECTED_CIRCLES_GROUP = "connected-circles";
105 export const CONNECTED_CIRCLES_ITEM = "connected-circle-item";
106 export const CIRCULAR_GRID_GROUP = "circular-grid";
107 export const CIRCULAR_GRID_ITEM = "circular-grid-item";
108 export const CIRCULAR_GRID_MAX_ITEMS = 6;
109 export const SNAKE_GROUP = "snake";
110 export const SNAKE_ITEM = "snake-item";
111 export const STEPS_GROUP = "steps";
112 export const STEPS_ITEM = "steps-item";
113
114 // Quote element
115 export const QUOTE_ELEMENT = "quote" as const;
116
117 // Individual chart element keys
118 export const PIE_CHART_ELEMENT = "chart-pie" as const;
119 export const BAR_CHART_ELEMENT = "chart-bar" as const;
120 export const AREA_CHART_ELEMENT = "chart-area" as const;
121 export const RADAR_CHART_ELEMENT = "chart-radar" as const;
122 export const SCATTER_CHART_ELEMENT = "chart-scatter" as const;
123 export const LINE_CHART_ELEMENT = "chart-line" as const;
124 export const RADIAL_BAR_CHART_ELEMENT = "chart-radial-bar" as const;
125 export const COMPOSED_CHART_ELEMENT = "chart-composed" as const;
126 export const TREEMAP_CHART_ELEMENT = "chart-treemap" as const;
127 export const BUBBLE_CHART_ELEMENT = "chart-bubble" as const;
128 export const DONUT_CHART_ELEMENT = "chart-donut" as const;
129
130 // New chart element keys - Phase 1: Standard Series
131 export const HISTOGRAM_CHART_ELEMENT = "chart-histogram" as const;
132 export const HEATMAP_CHART_ELEMENT = "chart-heatmap" as const;
133
134 // New chart element keys - Phase 2: Range Charts
135 export const RANGE_BAR_CHART_ELEMENT = "chart-range-bar" as const;
136 export const RANGE_AREA_CHART_ELEMENT = "chart-range-area" as const;
137 export const WATERFALL_CHART_ELEMENT = "chart-waterfall" as const;
138
139 // New chart element keys - Phase 3: Financial Charts
140 export const BOX_PLOT_CHART_ELEMENT = "chart-box-plot" as const;
141 export const CANDLESTICK_CHART_ELEMENT = "chart-candlestick" as const;
142 export const OHLC_CHART_ELEMENT = "chart-ohlc" as const;
143
144 // New chart element keys - Phase 4: Polar Charts
145 export const NIGHTINGALE_CHART_ELEMENT = "chart-nightingale" as const;
146 export const RADIAL_COLUMN_CHART_ELEMENT = "chart-radial-column" as const;
147
148 // New chart element keys - Phase 5: Hierarchical Charts
149 export const SUNBURST_CHART_ELEMENT = "chart-sunburst" as const;
150 export const SANKEY_CHART_ELEMENT = "chart-sankey" as const;
151 export const CHORD_CHART_ELEMENT = "chart-chord" as const;
152
153 // New chart element keys - Phase 6: Funnel Charts
154 export const FUNNEL_CHART_ELEMENT = "chart-funnel" as const;
155 export const CONE_FUNNEL_CHART_ELEMENT = "chart-cone-funnel" as const;
156 export const PYRAMID_CHART_ELEMENT = "chart-pyramid" as const;
157
158 // New chart element keys - Phase 7: Gauge Charts
159 export const RADIAL_GAUGE_ELEMENT = "chart-radial-gauge" as const;
160 export const LINEAR_GAUGE_ELEMENT = "chart-linear-gauge" as const;
161
162 // Button element key
163 export const BUTTON_ELEMENT = "button" as const;
164 export const CONTRIBUTOR_ELEMENT = "contributor" as const;
165 export const LABEL_ELEMENT = "label" as const;
166 export const PRESENTATION_TITLE_ELEMENT = "presentation-title" as const;
167
168 export type PresentationTitleVariant = "display" | "humongous" | "title";
169 export type PresentationElementAlignment = "center" | "left" | "right";
170
171 export type TPresentationTitleElement = TElement & {
172 alignment?: PresentationElementAlignment;
173 backgroundColor?: string;
174 color?: string;
175 textColor?: string;
176 type: typeof PRESENTATION_TITLE_ELEMENT;
177 variant?: PresentationTitleVariant;
178 };
179
180 export type TLabelElement = TElement & {
181 alignment?: PresentationElementAlignment;
182 backgroundColor?: string;
183 color?: string;
184 textColor?: string;
185 type: typeof LABEL_ELEMENT;
186 };
187
188 export type TContributorElement = TElement & {
189 alignment?: PresentationElementAlignment;
190 backgroundColor?: string;
191 color?: string;
192 editedLabel?: string;
193 textColor?: string;
194 type: typeof CONTRIBUTOR_ELEMENT;
195 updatedAt?: string | null;
196 };
197
198 // AntV Infographic element key
199 export const ANTV_INFOGRAPHIC = "antv-infographic" as const;
200
201 // Chart-specific capabilities for customization options
202 export const CHART_CAPABILITIES = {
203 [BAR_CHART_ELEMENT]: {
204 variants: ["default", "stacked", "horizontal"] as const,
205 },
206 [AREA_CHART_ELEMENT]: {
207 variants: ["default", "stacked"] as const,
208 curveTypes: ["linear", "monotone", "step", "natural"] as const,
209 },
210 [LINE_CHART_ELEMENT]: {
211 curveTypes: ["linear", "monotone", "step", "natural"] as const,
212 },
213 [PIE_CHART_ELEMENT]: {
214 variants: ["default"] as const,
215 },
216 [DONUT_CHART_ELEMENT]: {
217 // Donut chart has a fixed inner radius ratio
218 },
219 [RADAR_CHART_ELEMENT]: {
220 variants: ["default", "outline"] as const,
221 },
222 [SCATTER_CHART_ELEMENT]: {
223 scatterShapes: ["circle", "star", "square", "triangle", "diamond"] as const,
224 },
225 [BUBBLE_CHART_ELEMENT]: {
226 // Bubble chart uses z-axis for sizing, no special variants needed
227 },
228 } as const;
229
230 // Rich default chart data for each chart type
231 export const DEFAULT_CHART_DATA = {
232 // Label-value charts (pie, bar, area, line, radar, radial bar, treemap)
233 labelValue: [
234 { label: "Q1 Revenue", value: 45200 },
235 { label: "Q2 Revenue", value: 52800 },
236 { label: "Q3 Revenue", value: 48100 },
237 { label: "Q4 Revenue", value: 61400 },
238 { label: "Q1 Next Year", value: 58900 },
239 ],
240 // Multi-series for composed and stacked charts
241 multiSeries: [
242 { label: "Jan", revenue: 4500, expenses: 3200, profit: 1300 },
243 { label: "Feb", revenue: 5200, expenses: 3400, profit: 1800 },
244 { label: "Mar", revenue: 4800, expenses: 3100, profit: 1700 },
245 { label: "Apr", revenue: 6100, expenses: 3800, profit: 2300 },
246 { label: "May", revenue: 5900, expenses: 3500, profit: 2400 },
247 { label: "Jun", revenue: 6800, expenses: 4000, profit: 2800 },
248 ],
249 // XY coordinate data for scatter plots
250 scatter: [
251 { x: 10, y: 30 },
252 { x: 25, y: 45 },
253 { x: 35, y: 20 },
254 { x: 45, y: 55 },
255 { x: 55, y: 40 },
256 { x: 65, y: 70 },
257 { x: 75, y: 50 },
258 { x: 85, y: 85 },
259 { x: 95, y: 60 },
260 ],
261 // XYZ coordinate data for bubble charts (with z for radius)
262 bubble: [
263 { x: 10, y: 30, z: 15 },
264 { x: 25, y: 45, z: 22 },
265 { x: 35, y: 20, z: 18 },
266 { x: 45, y: 55, z: 28 },
267 { x: 55, y: 40, z: 12 },
268 { x: 65, y: 70, z: 35 },
269 { x: 75, y: 50, z: 20 },
270 { x: 85, y: 85, z: 30 },
271 ],
272 // Pre-binned histogram values shaped like a normal distribution
273 histogram: [
274 { label: "15-25", value: 3 },
275 { label: "25-35", value: 7 },
276 { label: "35-45", value: 15 },
277 { label: "45-55", value: 25 },
278 { label: "55-65", value: 20 },
279 { label: "65-75", value: 12 },
280 { label: "75-85", value: 5 },
281 ],
282 // OHLC data for candlestick and OHLC charts (date, open, high, low, close)
283 ohlc: [
284 { date: "2024-01-02", open: 100, high: 105, low: 98, close: 103 },
285 { date: "2024-01-03", open: 103, high: 108, low: 101, close: 107 },
286 { date: "2024-01-04", open: 107, high: 112, low: 104, close: 106 },
287 { date: "2024-01-05", open: 106, high: 110, low: 102, close: 109 },
288 { date: "2024-01-08", open: 109, high: 115, low: 107, close: 114 },
289 ],
290 // Box plot data (category, min, q1, median, q3, max)
291 boxPlot: [
292 { category: "Q1 Sales", min: 10, q1: 25, median: 50, q3: 75, max: 95 },
293 { category: "Q2 Sales", min: 15, q1: 30, median: 55, q3: 80, max: 100 },
294 { category: "Q3 Sales", min: 20, q1: 35, median: 60, q3: 85, max: 105 },
295 { category: "Q4 Sales", min: 12, q1: 28, median: 52, q3: 78, max: 98 },
296 ],
297 range: [
298 { category: "Planning", low: 12, high: 28 },
299 { category: "Design", low: 18, high: 36 },
300 { category: "Build", low: 30, high: 62 },
301 { category: "Launch", low: 48, high: 72 },
302 ],
303 waterfall: [
304 { category: "Starting", amount: 42000 },
305 { category: "New Sales", amount: 18000 },
306 { category: "Churn", amount: -7000 },
307 { category: "Expansion", amount: 11000 },
308 ],
309 hierarchy: [
310 { name: "Digital Experience", parent: "", value: 186 },
311 { name: "Acquisition", parent: "Digital Experience", value: 78 },
312 { name: "Organic Search", parent: "Acquisition", value: 34 },
313 { name: "Paid Campaigns", parent: "Acquisition", value: 27 },
314 { name: "Referral", parent: "Acquisition", value: 17 },
315 { name: "Engagement", parent: "Digital Experience", value: 64 },
316 { name: "Product Tours", parent: "Engagement", value: 26 },
317 { name: "Templates", parent: "Engagement", value: 22 },
318 { name: "Workspace Sharing", parent: "Engagement", value: 16 },
319 { name: "Retention", parent: "Digital Experience", value: 44 },
320 { name: "Weekly Active Teams", parent: "Retention", value: 25 },
321 { name: "Automations", parent: "Retention", value: 19 },
322 ],
323 flow: [
324 { from: "Visitors", to: "Leads", size: 80 },
325 { from: "Leads", to: "Trials", size: 42 },
326 { from: "Trials", to: "Customers", size: 18 },
327 ],
328 funnel: [
329 { label: "Visitors", value: 12000 },
330 { label: "Signups", value: 4200 },
331 { label: "Trials", value: 1800 },
332 { label: "Customers", value: 640 },
333 ],
334 heatmap: [
335 { x: "Mon", y: "Morning", value: 12 },
336 { x: "Mon", y: "Afternoon", value: 18 },
337 { x: "Tue", y: "Morning", value: 15 },
338 { x: "Tue", y: "Afternoon", value: 24 },
339 ],
340 gauge: [{ value: 64 }],
341 } as const;
342
343 // Element categories for organized dropdown
344 export const ELEMENT_CATEGORIES = {
345 BULLETS: "Bullets",
346 BOXES: "Boxes",
347 STEPS: "Steps",
348 SEQUENCES: "Sequences",
349 LAYOUTS: "Circles",
350 MEDIA_LISTS: "Media Lists",
351 COMPARISONS: "Comparisons",
352 CHARTS: "Charts",
353 STATS: "Stats",
354 QUOTES: "Quotes",
355 } as const;
356
357 // Category icons mapping
358 export const CATEGORY_ICONS = {
359 [ELEMENT_CATEGORIES.BULLETS]: <ListOrdered className="h-4 w-4" />,
360 [ELEMENT_CATEGORIES.BOXES]: <Square className="h-4 w-4" />,
361 [ELEMENT_CATEGORIES.STEPS]: <FaStairs className="h-4 w-4" />,
362 [ELEMENT_CATEGORIES.SEQUENCES]: <ArrowRight className="h-4 w-4" />,
363 [ELEMENT_CATEGORIES.LAYOUTS]: <CircleDashed className="h-4 w-4" />,
364 [ELEMENT_CATEGORIES.MEDIA_LISTS]: <ImageIcon className="h-4 w-4" />,
365 [ELEMENT_CATEGORIES.COMPARISONS]: <GitCompare className="h-4 w-4" />,
366 [ELEMENT_CATEGORIES.CHARTS]: <PieChart className="h-4 w-4" />,
367 [ELEMENT_CATEGORIES.STATS]: <BarChart3 className="h-4 w-4" />,
368 [ELEMENT_CATEGORIES.QUOTES]: <Quote className="h-4 w-4" />,
369 } as const;
370
371 // Chart data category mapping - maps each chart type to its data category
372 export type ChartDataCategory =
373 | "categorical" // All charts using label + value(s): Bar, Line, Area, Composed, Pie, Donut, Radar, Radial Bar, Nightingale, Radial Column
374 | "xy" // Scatter - x/y coordinates
375 | "xyz" // Bubble - x/y/z coordinates
376 | "range" // Range Bar, Range Area - category + low/high values
377 | "ohlc" // Candlestick, OHLC - financial data
378 | "box-plot" // Box Plot - statistical data
379 | "hierarchical" // Treemap, Sunburst - nested data
380 | "flow" // Sankey, Chord - from/to/size
381 | "funnel" // Funnel, Cone Funnel, Pyramid - stage/value
382 | "heatmap" // Heatmap - x/y/value matrix
383 | "histogram" // Histogram - single values for binning
384 | "waterfall" // Waterfall chart - standalone
385 | "gauge"; // Radial/Linear Gauge - single value
386
387 // Map of chart element types to their data categories
388 const CHART_DATA_CATEGORY_MAP: Record<string, ChartDataCategory> = {
389 // Categorical charts (label + value(s) - can transform between each other)
390 // This includes both cartesian (x/y axes) and polar displays
391 [PIE_CHART_ELEMENT]: "categorical",
392 [DONUT_CHART_ELEMENT]: "categorical",
393 [RADAR_CHART_ELEMENT]: "categorical",
394 [RADIAL_BAR_CHART_ELEMENT]: "categorical",
395 [NIGHTINGALE_CHART_ELEMENT]: "categorical",
396 [RADIAL_COLUMN_CHART_ELEMENT]: "categorical",
397 [BAR_CHART_ELEMENT]: "categorical",
398 [LINE_CHART_ELEMENT]: "categorical",
399 [AREA_CHART_ELEMENT]: "categorical",
400 [COMPOSED_CHART_ELEMENT]: "categorical",
401
402 // XY coordinate charts
403 [SCATTER_CHART_ELEMENT]: "xy",
404
405 // XYZ coordinate charts (includes size/z dimension)
406 [BUBBLE_CHART_ELEMENT]: "xyz",
407
408 // Range charts (category + low/high values)
409 [RANGE_BAR_CHART_ELEMENT]: "range",
410 [RANGE_AREA_CHART_ELEMENT]: "range",
411
412 // OHLC charts (financial - open, high, low, close)
413 [CANDLESTICK_CHART_ELEMENT]: "ohlc",
414 [OHLC_CHART_ELEMENT]: "ohlc",
415
416 // Box plot charts (statistical)
417 [BOX_PLOT_CHART_ELEMENT]: "box-plot",
418
419 // Hierarchical charts (nested data with children)
420 [TREEMAP_CHART_ELEMENT]: "hierarchical",
421 [SUNBURST_CHART_ELEMENT]: "hierarchical",
422
423 // Flow charts (from, to, size)
424 [SANKEY_CHART_ELEMENT]: "flow",
425 [CHORD_CHART_ELEMENT]: "flow",
426
427 // Funnel charts (stage/label + value)
428 [FUNNEL_CHART_ELEMENT]: "funnel",
429 [CONE_FUNNEL_CHART_ELEMENT]: "funnel",
430 [PYRAMID_CHART_ELEMENT]: "funnel",
431
432 // Heatmap charts (x, y, value matrix)
433 [HEATMAP_CHART_ELEMENT]: "heatmap",
434
435 // Histogram charts (single values for binning)
436 [HISTOGRAM_CHART_ELEMENT]: "histogram",
437
438 // Gauge charts (single value)
439 [RADIAL_GAUGE_ELEMENT]: "gauge",
440 [LINEAR_GAUGE_ELEMENT]: "gauge",
441
442 // Waterfall chart (standalone - specific data format)
443 [WATERFALL_CHART_ELEMENT]: "waterfall",
444 };
445
446 /**
447 * Gets the data category for a chart type
448 * Used to determine which charts can be converted to each other
449 */
450 export function getChartDataCategory(
451 chartType: string,
452 ): ChartDataCategory | null {
453 return CHART_DATA_CATEGORY_MAP[chartType] ?? null;
454 }
455
456 export function getDefaultChartDataForType(chartType: string): unknown {
457 if (chartType === COMPOSED_CHART_ELEMENT) {
458 return DEFAULT_CHART_DATA.multiSeries;
459 }
460
461 const targetDataCategory = getChartDataCategory(chartType);
462
463 switch (targetDataCategory) {
464 case "xy":
465 return DEFAULT_CHART_DATA.scatter;
466 case "xyz":
467 return DEFAULT_CHART_DATA.bubble;
468 case "ohlc":
469 return DEFAULT_CHART_DATA.ohlc;
470 case "box-plot":
471 return DEFAULT_CHART_DATA.boxPlot;
472 case "histogram":
473 return DEFAULT_CHART_DATA.histogram;
474 case "range":
475 return DEFAULT_CHART_DATA.range;
476 case "waterfall":
477 return DEFAULT_CHART_DATA.waterfall;
478 case "hierarchical":
479 return DEFAULT_CHART_DATA.hierarchy;
480 case "flow":
481 return DEFAULT_CHART_DATA.flow;
482 case "funnel":
483 return DEFAULT_CHART_DATA.funnel;
484 case "heatmap":
485 return DEFAULT_CHART_DATA.heatmap;
486 case "gauge":
487 return DEFAULT_CHART_DATA.gauge;
488 default:
489 return DEFAULT_CHART_DATA.labelValue;
490 }
491 }
492
493 /**
494 * Helper function to check if two chart types are compatible (can convert between them)
495 * Charts are compatible if they use the same data structure/category
496 */
497 export function areChartTypesCompatible(
498 chartType1: string,
499 chartType2: string,
500 ): boolean {
501 const category1 = getChartDataCategory(chartType1);
502 const category2 = getChartDataCategory(chartType2);
503
504 // If either chart doesn't have a category, they're not compatible
505 if (!category1 || !category2) {
506 return false;
507 }
508
509 // Charts are compatible if they share the same data category
510 return category1 === category2;
511 }
512
513 // Element capabilities - defines which elements support which layout options
514 export const ELEMENT_CAPABILITIES: Record<
515 string,
516 {
517 orientation?: readonly string[];
518 sidedness?: readonly ["single", "double"];
519 numbered?: boolean;
520 showLine?: boolean;
521 showIcon?: boolean;
522 columnSize?: readonly ["sm", "md", "lg", "xl"];
523 alignment?: readonly ("left" | "center" | "right")[];
524 variant?: readonly string[];
525 }
526 > = {
527 [TIMELINE_GROUP]: {
528 orientation: ["vertical", "horizontal"] as const,
529 sidedness: ["single", "double"] as const,
530 numbered: true,
531 showLine: true,
532 alignment: ["left", "right"] as readonly ("left" | "right")[],
533 variant: ["default", "boxes"] as const,
534 },
535 [ARROW_LIST]: {
536 orientation: ["vertical", "horizontal"] as const,
537 alignment: ["left", "center", "right"] as const,
538 },
539 [SEQUENCE_ARROW_GROUP]: {
540 orientation: ["vertical", "horizontal"] as const,
541 },
542 [STEPS_GROUP]: {
543 variant: ["default", "arrow", "box"] as const,
544 columnSize: ["sm", "md", "lg", "xl"] as const,
545 },
546 [FLEX_BOX]: {
547 columnSize: ["sm", "md", "lg", "xl"] as const,
548 },
549 [BOX_GROUP]: {
550 orientation: ["vertical", "horizontal"] as const,
551 columnSize: ["sm", "md", "lg", "xl"] as const,
552 },
553 [BULLET_GROUP]: {
554 columnSize: ["sm", "md", "lg", "xl"] as const,
555 },
556 [STATS_GROUP]: {
557 columnSize: ["sm", "md", "lg", "xl"] as const,
558 },
559 [SLOPE_GROUP]: {
560 alignment: ["left", "center", "right"] as const,
561 },
562 [CONNECTED_CIRCLES_GROUP]: {
563 alignment: ["left", "center", "right"] as const,
564 },
565 [CIRCULAR_GRID_GROUP]: {
566 alignment: ["left", "center", "right"] as const,
567 },
568 [SNAKE_GROUP]: {
569 alignment: ["left", "center", "right"] as const,
570 },
571 [ICON_LIST]: {
572 orientation: ["side", "top"] as const,
573 columnSize: ["sm", "md", "lg", "xl"] as const,
574 variant: ["icon", "image"] as const,
575 },
576 [COMPARE_GROUP]: {
577 columnSize: ["sm", "md", "lg", "xl"] as const,
578 },
579 [BEFORE_AFTER_GROUP]: {
580 columnSize: ["sm", "md", "lg", "xl"] as const,
581 },
582 [STAIRCASE_GROUP]: {
583 alignment: ["left", "right"] as readonly ("left" | "right")[],
584 variant: ["default", "inside"] as const,
585 },
586 [PYRAMID_GROUP]: {
587 alignment: ["left", "right"] as readonly ("left" | "right")[],
588 variant: ["default", "inside"] as const,
589 },
590 [KEYS.callout]: {
591 alignment: ["left", "center", "right"] as const,
592 variant: [
593 "note",
594 "info",
595 "warning",
596 "caution",
597 "success",
598 "question",
599 ] as const,
600 },
601 [BUTTON_ELEMENT]: {
602 alignment: ["left", "center", "right"] as const,
603 },
604 [CONTRIBUTOR_ELEMENT]: {
605 alignment: ["left", "center", "right"] as const,
606 },
607 [LABEL_ELEMENT]: {
608 alignment: ["left", "center", "right"] as const,
609 },
610 [PRESENTATION_TITLE_ELEMENT]: {
611 alignment: ["left", "center", "right"] as const,
612 variant: ["title", "display", "humongous"] as const,
613 },
614 };
615
616 // Helper functions to check element capabilities
617 export function supportsOrientation(elementType: string): boolean {
618 return (
619 elementType in ELEMENT_CAPABILITIES &&
620 "orientation" in
621 (ELEMENT_CAPABILITIES[elementType as keyof typeof ELEMENT_CAPABILITIES] ??
622 [])
623 );
624 }
625
626 export function supportsSidedness(elementType: string): boolean {
627 return (
628 elementType in ELEMENT_CAPABILITIES &&
629 "sidedness" in
630 (ELEMENT_CAPABILITIES[elementType as keyof typeof ELEMENT_CAPABILITIES] ??
631 [])
632 );
633 }
634
635 export function supportsNumbered(elementType: string): boolean {
636 return (
637 elementType in ELEMENT_CAPABILITIES &&
638 "numbered" in
639 (ELEMENT_CAPABILITIES[elementType as keyof typeof ELEMENT_CAPABILITIES] ??
640 [])
641 );
642 }
643
644 export function supportsShowLine(elementType: string): boolean {
645 return (
646 elementType in ELEMENT_CAPABILITIES &&
647 "showLine" in
648 (ELEMENT_CAPABILITIES[elementType as keyof typeof ELEMENT_CAPABILITIES] ??
649 [])
650 );
651 }
652
653 export function supportsAlignment(elementType: string): boolean {
654 return (
655 elementType in ELEMENT_CAPABILITIES &&
656 "alignment" in
657 (ELEMENT_CAPABILITIES[elementType as keyof typeof ELEMENT_CAPABILITIES] ??
658 [])
659 );
660 }
661
662 export function supportsColumnSize(elementType: string): boolean {
663 return (
664 elementType in ELEMENT_CAPABILITIES &&
665 "columnSize" in
666 (ELEMENT_CAPABILITIES[elementType as keyof typeof ELEMENT_CAPABILITIES] ??
667 {})
668 );
669 }
670
671 export function supportsVariant(elementType: string): boolean {
672 return (
673 elementType in ELEMENT_CAPABILITIES &&
674 "variant" in
675 (ELEMENT_CAPABILITIES[elementType as keyof typeof ELEMENT_CAPABILITIES] ??
676 {})
677 );
678 }
679
680 export function getVariantOptions(elementType: string): readonly string[] {
681 const capabilities =
682 ELEMENT_CAPABILITIES[elementType as keyof typeof ELEMENT_CAPABILITIES];
683 return capabilities?.variant ?? [];
684 }
685
686 export function getAlignmentOptions(elementType: string) {
687 const capabilities =
688 ELEMENT_CAPABILITIES[elementType as keyof typeof ELEMENT_CAPABILITIES];
689 return capabilities?.alignment ?? ["left", "center", "right"];
690 }
691
692 export function getOrientationOptions(elementType: string): readonly string[] {
693 const capabilities =
694 ELEMENT_CAPABILITIES[elementType as keyof typeof ELEMENT_CAPABILITIES];
695 return capabilities?.orientation ?? [];
696 }
697
698 export function getSidednessOptions(elementType: string): readonly string[] {
699 const capabilities =
700 ELEMENT_CAPABILITIES[elementType as keyof typeof ELEMENT_CAPABILITIES];
701 return capabilities?.sidedness ?? [];
702 }
703
704 export function getColumnSizeOptions(elementType: string): readonly string[] {
705 if (supportsColumnSize(elementType)) {
706 return ["sm", "md", "lg", "xl"] as const;
707 }
708 return [];
709 }
710
711 export const getColumnSizeLabel = (columnSize: "sm" | "md" | "lg" | "xl") => {
712 switch (columnSize) {
713 case "sm":
714 return "Small";
715 case "md":
716 return "Medium";
717 case "lg":
718 return "Large";
719 case "xl":
720 return "Extra Large";
721 }
722 };
723
724 // Grouped blocks structure for hierarchical dropdown - all variants stored directly
725 type GroupedBlockBase = {
726 type: string;
727 name: string;
728 icon: ReactNode;
729 variant?: string;
730 key?: string;
731 supportsOrientation?: boolean;
732 };
733
734 export const GROUPED_BLOCKS = {
735 [ELEMENT_CATEGORIES.BULLETS]: [
736 // Bullets variants
737 {
738 type: BULLET_GROUP,
739 name: "Numbered",
740 variant: "numbered",
741 key: "bulletType",
742 icon: <ListOrdered className="h-4 w-4" />,
743 },
744 {
745 type: BULLET_GROUP,
746 name: "Basic",
747 variant: "basic",
748 key: "bulletType",
749 icon: <List className="h-4 w-4" />,
750 },
751 {
752 type: BULLET_GROUP,
753 name: "Arrow",
754 variant: "arrow",
755 key: "bulletType",
756 icon: <ArrowRight className="h-4 w-4" />,
757 },
758 ],
759 [ELEMENT_CATEGORIES.BOXES]: [
760 // Boxes variants
761 {
762 type: BOX_GROUP,
763 name: "Solid",
764 variant: "solid",
765 key: "boxType",
766 icon: <Square className="h-4 w-4" />,
767 },
768 {
769 type: BOX_GROUP,
770 name: "Outline",
771 variant: "outline",
772 key: "boxType",
773 icon: <SquareDashedBottomCode className="h-4 w-4" />,
774 },
775 {
776 type: BOX_GROUP,
777 name: "With Icons",
778 variant: "icon",
779 key: "boxType",
780 icon: <SquareAsterisk className="h-4 w-4" />,
781 },
782 {
783 type: BOX_GROUP,
784 name: "Side Line",
785 variant: "sideline",
786 key: "boxType",
787 icon: <SquareSplitVertical className="h-4 w-4" />,
788 },
789 {
790 type: BOX_GROUP,
791 name: "Side Line Text",
792 variant: "side-label",
793 key: "boxType",
794 icon: <SquareSplitVertical className="h-4 w-4" />,
795 },
796 {
797 type: BOX_GROUP,
798 name: "Top Line Text",
799 variant: "top-label",
800 key: "boxType",
801 icon: <SquareDashedBottomCode className="h-4 w-4" />,
802 },
803 {
804 type: BOX_GROUP,
805 name: "Top Circle",
806 variant: "top-circle",
807 key: "boxType",
808 icon: <Circle className="h-4 w-4" />,
809 },
810 {
811 type: BOX_GROUP,
812 name: "Joined",
813 variant: "joined",
814 key: "boxType",
815 icon: <SquareStack className="h-4 w-4" />,
816 },
817 {
818 type: BOX_GROUP,
819 name: "Joined Icons",
820 variant: "joined-icon",
821 key: "boxType",
822 icon: <SquareAsterisk className="h-4 w-4" />,
823 },
824 {
825 type: BOX_GROUP,
826 name: "Leaf",
827 variant: "leaf",
828 key: "boxType",
829 icon: <Leaf className="h-4 w-4" />,
830 },
831 {
832 type: BOX_GROUP,
833 name: "Labeled",
834 variant: "labeled",
835 key: "boxType",
836 icon: <Layers className="h-4 w-4" />,
837 },
838 {
839 type: BOX_GROUP,
840 name: "Alternating",
841 variant: "alternating",
842 key: "boxType",
843 icon: <ArrowDownUp className="h-4 w-4" />,
844 supportsOrientation: true,
845 },
846 ],
847 [ELEMENT_CATEGORIES.STEPS]: [
848 {
849 type: STEPS_GROUP,
850 name: "Steps",
851 variant: "default",
852 key: "variant",
853 icon: <ListOrdered className="h-4 w-4" />,
854 },
855 {
856 type: STEPS_GROUP,
857 name: "Arrow Steps",
858 variant: "arrow",
859 key: "variant",
860 icon: <ArrowRight className="h-4 w-4" />,
861 },
862 {
863 type: STEPS_GROUP,
864 name: "Box Steps",
865 variant: "box",
866 key: "variant",
867 icon: <Square className="h-4 w-4" />,
868 },
869 {
870 type: STAIRCASE_GROUP,
871 name: "Staircase",
872 icon: <FaStairs className="h-4 w-4" />,
873 },
874 // Pyramid/Funnel variants
875 {
876 type: PYRAMID_GROUP,
877 name: "Pyramid",
878 variant: "pyramid",
879 key: "isFunnel",
880 icon: <Triangle className="h-4 w-4" />,
881 },
882 {
883 type: PYRAMID_GROUP,
884 name: "Funnel",
885 variant: "funnel",
886 key: "isFunnel",
887 icon: <Funnel className="h-4 w-4" />,
888 },
889 // Arrow Sequence - consolidated orientation variants
890 {
891 type: SEQUENCE_ARROW_GROUP,
892 name: "Arrow Sequence",
893 icon: <ArrowDownUp className="h-4 w-4" />,
894 supportsOrientation: true,
895 },
896 {
897 type: SLOPE_GROUP,
898 name: "Slope",
899 icon: <TrendingUp className="h-4 w-4" />,
900 },
901 ],
902 [ELEMENT_CATEGORIES.SEQUENCES]: [
903 // Arrows variants
904 {
905 type: ARROW_LIST,
906 name: "Arrow",
907 variant: "arrow",
908 key: "svgType",
909 icon: <ArrowRight className="h-4 w-4" />,
910 },
911 {
912 type: ARROW_LIST,
913 name: "Pill",
914 variant: "pill",
915 key: "svgType",
916 icon: <Pill className="h-4 w-4" />,
917 },
918 {
919 type: ARROW_LIST,
920 name: "Parallelogram",
921 variant: "parallelogram",
922 key: "svgType",
923 icon: <Diamond className="h-4 w-4" />,
924 },
925 // Timeline - consolidated orientation variants
926 {
927 type: TIMELINE_GROUP,
928 name: "Timeline",
929 variant: "default",
930 key: "variant",
931 icon: <Clock className="h-4 w-4" />,
932 supportsOrientation: true,
933 },
934 {
935 type: TIMELINE_GROUP,
936 name: "Timeline Boxes",
937 variant: "boxes",
938 key: "variant",
939 icon: <Square className="h-4 w-4" />,
940 supportsOrientation: true,
941 },
942 {
943 type: SNAKE_GROUP,
944 name: "Snake",
945 icon: <Workflow className="h-4 w-4" />,
946 },
947 ],
948 [ELEMENT_CATEGORIES.LAYOUTS]: [
949 {
950 type: CYCLE_GROUP,
951 name: "Cycle",
952 variant: "cycle",
953 key: "variant",
954 icon: <RefreshCw className="h-4 w-4" />,
955 },
956 {
957 type: CYCLE_GROUP,
958 name: "Flower",
959 variant: "flower",
960 key: "variant",
961 icon: <Flower className="h-4 w-4" />,
962 },
963 {
964 type: CYCLE_GROUP,
965 name: "Ring",
966 variant: "ring",
967 key: "variant",
968 icon: <CircleDashed className="h-4 w-4" />,
969 },
970 {
971 type: CYCLE_GROUP,
972 name: "Circle",
973 variant: "circle",
974 key: "variant",
975 icon: <Circle className="h-4 w-4" />,
976 },
977 {
978 type: CONNECTED_CIRCLES_GROUP,
979 name: "Connected Circles",
980 icon: <Workflow className="h-4 w-4" />,
981 },
982 {
983 type: CIRCULAR_GRID_GROUP,
984 name: "Circular Grid",
985 icon: <Grid2x2 className="h-4 w-4" />,
986 },
987 ],
988 [ELEMENT_CATEGORIES.MEDIA_LISTS]: [
989 {
990 type: ICON_LIST,
991 name: "Icon List",
992 variant: "icon",
993 key: "variant",
994 icon: <Star className="h-4 w-4" />,
995 },
996 {
997 type: ICON_LIST,
998 name: "Image List",
999 variant: "image",
1000 key: "variant",
1001 icon: <ImageIcon className="h-4 w-4" />,
1002 },
1003 ],
1004 [ELEMENT_CATEGORIES.COMPARISONS]: [
1005 {
1006 type: COMPARE_GROUP,
1007 name: "Compare",
1008 icon: <GitCompare className="h-4 w-4" />,
1009 },
1010 {
1011 type: BEFORE_AFTER_GROUP,
1012 name: "Before/After",
1013 icon: <ArrowLeftRight className="h-4 w-4" />,
1014 },
1015 {
1016 type: PROS_CONS_GROUP,
1017 name: "Pros & Cons",
1018 icon: <Scale className="h-4 w-4" />,
1019 },
1020 {
1021 type: COLUMN_GROUP,
1022 name: "Columns",
1023 icon: <SquareStack className="h-4 w-4" />,
1024 },
1025 ],
1026 [ELEMENT_CATEGORIES.CHARTS]: [
1027 {
1028 type: PIE_CHART_ELEMENT,
1029 name: "Pie Chart",
1030 icon: <PieChart className="h-4 w-4" />,
1031 },
1032 {
1033 type: DONUT_CHART_ELEMENT,
1034 name: "Donut Chart",
1035 icon: <Circle className="h-4 w-4" />,
1036 },
1037 {
1038 type: BAR_CHART_ELEMENT,
1039 name: "Bar Chart",
1040 icon: <BarChart3 className="h-4 w-4" />,
1041 },
1042 {
1043 type: AREA_CHART_ELEMENT,
1044 name: "Area Chart",
1045 icon: <AreaChart className="h-4 w-4" />,
1046 },
1047 {
1048 type: RADAR_CHART_ELEMENT,
1049 name: "Radar Chart",
1050 icon: <Radar className="h-4 w-4" />,
1051 },
1052 {
1053 type: SCATTER_CHART_ELEMENT,
1054 name: "Scatter Chart",
1055 icon: <ChartScatter className="h-4 w-4" />,
1056 },
1057 {
1058 type: BUBBLE_CHART_ELEMENT,
1059 name: "Bubble Chart",
1060 icon: <Circle className="h-4 w-4" />,
1061 },
1062 {
1063 type: LINE_CHART_ELEMENT,
1064 name: "Line Chart",
1065 icon: <TrendingUp className="h-4 w-4" />,
1066 },
1067 {
1068 type: RADIAL_BAR_CHART_ELEMENT,
1069 name: "Radial Bar",
1070 icon: <Circle className="h-4 w-4" />,
1071 },
1072 {
1073 type: RADIAL_COLUMN_CHART_ELEMENT,
1074 name: "Radial Column",
1075 icon: <Circle className="h-4 w-4" />,
1076 },
1077 {
1078 type: COMPOSED_CHART_ELEMENT,
1079 name: "Composed",
1080 icon: <Layers className="h-4 w-4" />,
1081 },
1082 {
1083 type: TREEMAP_CHART_ELEMENT,
1084 name: "Treemap",
1085 icon: <Grid3x3 className="h-4 w-4" />,
1086 },
1087 // New chart types
1088 {
1089 type: HISTOGRAM_CHART_ELEMENT,
1090 name: "Histogram",
1091 icon: <BarChart3 className="h-4 w-4" />,
1092 },
1093 {
1094 type: HEATMAP_CHART_ELEMENT,
1095 name: "Heatmap",
1096 icon: <Grid3x3 className="h-4 w-4" />,
1097 },
1098 {
1099 type: RANGE_BAR_CHART_ELEMENT,
1100 name: "Range Bar",
1101 icon: <BarChart3 className="h-4 w-4" />,
1102 },
1103 {
1104 type: RANGE_AREA_CHART_ELEMENT,
1105 name: "Range Area",
1106 icon: <AreaChart className="h-4 w-4" />,
1107 },
1108 {
1109 type: WATERFALL_CHART_ELEMENT,
1110 name: "Waterfall",
1111 icon: <BarChart3 className="h-4 w-4" />,
1112 },
1113 {
1114 type: BOX_PLOT_CHART_ELEMENT,
1115 name: "Box Plot",
1116 icon: <BarChart3 className="h-4 w-4" />,
1117 },
1118 {
1119 type: CANDLESTICK_CHART_ELEMENT,
1120 name: "Candlestick",
1121 icon: <TrendingUp className="h-4 w-4" />,
1122 },
1123 {
1124 type: OHLC_CHART_ELEMENT,
1125 name: "OHLC",
1126 icon: <TrendingUp className="h-4 w-4" />,
1127 },
1128 {
1129 type: NIGHTINGALE_CHART_ELEMENT,
1130 name: "Nightingale",
1131 icon: <PieChart className="h-4 w-4" />,
1132 },
1133 {
1134 type: SUNBURST_CHART_ELEMENT,
1135 name: "Sunburst",
1136 icon: <Circle className="h-4 w-4" />,
1137 },
1138 {
1139 type: SANKEY_CHART_ELEMENT,
1140 name: "Sankey",
1141 icon: <Layers className="h-4 w-4" />,
1142 },
1143 {
1144 type: CHORD_CHART_ELEMENT,
1145 name: "Chord",
1146 icon: <Circle className="h-4 w-4" />,
1147 },
1148 {
1149 type: FUNNEL_CHART_ELEMENT,
1150 name: "Funnel",
1151 icon: <BarChart3 className="h-4 w-4" />,
1152 },
1153 {
1154 type: CONE_FUNNEL_CHART_ELEMENT,
1155 name: "Cone Funnel",
1156 icon: <BarChart3 className="h-4 w-4" />,
1157 },
1158 {
1159 type: PYRAMID_CHART_ELEMENT,
1160 name: "Pyramid Chart",
1161 icon: <BarChart3 className="h-4 w-4" />,
1162 },
1163 {
1164 type: RADIAL_GAUGE_ELEMENT,
1165 name: "Radial Gauge",
1166 icon: <Circle className="h-4 w-4" />,
1167 },
1168 {
1169 type: LINEAR_GAUGE_ELEMENT,
1170 name: "Linear Gauge",
1171 icon: <BarChart3 className="h-4 w-4" />,
1172 },
1173 ],
1174 [ELEMENT_CATEGORIES.STATS]: [
1175 {
1176 type: STATS_GROUP,
1177 name: "Plain",
1178 variant: "plain",
1179 key: "statsType",
1180 icon: <BarChart3 className="h-4 w-4" />,
1181 },
1182 {
1183 type: STATS_GROUP,
1184 name: "Circle",
1185 variant: "circle",
1186 key: "statsType",
1187 icon: <Star className="h-4 w-4" />,
1188 },
1189 {
1190 type: STATS_GROUP,
1191 name: "Circle Bold",
1192 variant: "circle-bold",
1193 key: "statsType",
1194 icon: <Square className="h-4 w-4" />,
1195 },
1196 {
1197 type: STATS_GROUP,
1198 name: "Star Rating",
1199 variant: "star",
1200 key: "statsType",
1201 icon: <Star className="h-4 w-4" />,
1202 },
1203 {
1204 type: STATS_GROUP,
1205 name: "Bar",
1206 variant: "bar",
1207 key: "statsType",
1208 icon: <BarChart3 className="h-4 w-4" />,
1209 },
1210 {
1211 type: STATS_GROUP,
1212 name: "Dot Grid",
1213 variant: "dot-grid",
1214 key: "statsType",
1215 icon: <SquareStack className="h-4 w-4" />,
1216 },
1217 {
1218 type: STATS_GROUP,
1219 name: "Dot Line",
1220 variant: "dot-line",
1221 key: "statsType",
1222 icon: <ArrowRight className="h-4 w-4" />,
1223 },
1224 ],
1225 [ELEMENT_CATEGORIES.QUOTES]: [
1226 {
1227 type: QUOTE_ELEMENT,
1228 name: "Large Quote",
1229 variant: "large",
1230 key: "variant",
1231 icon: <Quote className="h-4 w-4" />,
1232 },
1233 {
1234 type: QUOTE_ELEMENT,
1235 name: "Side Quote with Icon",
1236 variant: "sidequote-icon",
1237 key: "variant",
1238 icon: <Quote className="h-4 w-4" />,
1239 },
1240 {
1241 type: QUOTE_ELEMENT,
1242 name: "Simple Side Quote",
1243 variant: "sidequote",
1244 key: "variant",
1245 icon: <Quote className="h-4 w-4" />,
1246 },
1247 ],
1248 } as const satisfies Record<string, readonly GroupedBlockBase[]>;
1249
1250 type GroupedBlockList = readonly GroupedBlockBase[];
1251
1252 // Keep BLOCKS for backward compatibility but mark as deprecated
1253 export const BLOCKS = Object.values(GROUPED_BLOCKS).flat();
1254
1255 const HIDDEN_LAYOUT_TOOLBAR_TYPES = new Set<string>([QUOTE_ELEMENT]);
1256
1257 // Helper function to check if an element type is a chart
1258 export function isChartType(elementType: string): boolean {
1259 return GROUPED_BLOCKS[ELEMENT_CATEGORIES.CHARTS].some(
1260 (chart) => chart.type === elementType,
1261 );
1262 }
1263
1264 export function getElementDisplayName(
1265 elementType: string,
1266 element?: Record<string, unknown>,
1267 ): string {
1268 if (element) {
1269 for (const elements of Object.values(GROUPED_BLOCKS)) {
1270 const foundVariant = elements.find((el) => {
1271 if (el.type !== elementType) return false;
1272
1273 const block = el as GroupedBlockBase;
1274 if (block.variant && block.key) {
1275 if (block.key === "isFunnel") {
1276 const isFunnelValue = element.isFunnel;
1277 const currentFunnelVariant =
1278 isFunnelValue === "funnel" || isFunnelValue === true
1279 ? "funnel"
1280 : "pyramid";
1281 return currentFunnelVariant === block.variant;
1282 }
1283
1284 const elementValue = element[block.key];
1285 return (
1286 elementValue === block.variant ||
1287 (!elementValue &&
1288 block.type === ICON_LIST &&
1289 block.variant === "icon") ||
1290 (!elementValue && block.variant === "default")
1291 );
1292 }
1293
1294 return false;
1295 });
1296
1297 if (foundVariant) return foundVariant.name;
1298 }
1299 }
1300
1301 for (const elements of Object.values(GROUPED_BLOCKS)) {
1302 const found = elements.find((el) => el.type === elementType);
1303 if (found) return found.name;
1304 }
1305
1306 return "Unknown";
1307 }
1308
1309 /**
1310 * Gets available conversion options based on the current element type
1311 * Returns grouped structure for hierarchical dropdown
1312 */
1313 export function getAvailableConversionOptions(
1314 currentElementType: string,
1315 ): Record<string, GroupedBlockList> {
1316 const isCurrentElementChart = isChartType(currentElementType);
1317
1318 // If current element is a chart, only show chart category
1319 if (isCurrentElementChart) {
1320 return {
1321 [ELEMENT_CATEGORIES.CHARTS]: GROUPED_BLOCKS[
1322 ELEMENT_CATEGORIES.CHARTS
1323 ].filter((chart) =>
1324 areChartTypesCompatible(currentElementType, chart.type),
1325 ),
1326 };
1327 }
1328
1329 // For non-chart elements, show all categories except charts
1330 const availableGroups: Record<string, GroupedBlockList> = {};
1331
1332 for (const [category, elements] of Object.entries(GROUPED_BLOCKS)) {
1333 if (category !== ELEMENT_CATEGORIES.CHARTS) {
1334 const filteredElements = elements.filter((element) => {
1335 if (HIDDEN_LAYOUT_TOOLBAR_TYPES.has(element.type)) {
1336 return currentElementType === element.type;
1337 }
1338 return true;
1339 });
1340
1341 if (filteredElements.length > 0) {
1342 availableGroups[category] = filteredElements;
1343 }
1344 }
1345 }
1346
1347 return availableGroups;
1348 }
1349
1350 type ParentChildRelationship = {
1351 child: string | readonly string[];
1352 };
1353
1354 export const PARENT_CHILD_RELATIONSHIP = {
1355 [BULLET_GROUP]: {
1356 child: BULLET_ITEM,
1357 },
1358 [STAIRCASE_GROUP]: {
1359 child: STAIR_ITEM,
1360 },
1361 [CYCLE_GROUP]: {
1362 child: CYCLE_ITEM,
1363 },
1364 [ICON_LIST]: {
1365 child: ICON_LIST_ITEM,
1366 },
1367 [ARROW_LIST]: {
1368 child: ARROW_LIST_ITEM,
1369 },
1370 [PYRAMID_GROUP]: {
1371 child: PYRAMID_ITEM,
1372 },
1373 [TIMELINE_GROUP]: {
1374 child: TIMELINE_ITEM,
1375 },
1376 [BOX_GROUP]: {
1377 child: BOX_ITEM,
1378 },
1379 [COLUMN_GROUP]: {
1380 child: COLUMN_ITEM,
1381 },
1382 [COMPARE_GROUP]: {
1383 child: COMPARE_SIDE,
1384 },
1385 [BEFORE_AFTER_GROUP]: {
1386 child: BEFORE_AFTER_SIDE,
1387 },
1388 [PROS_CONS_GROUP]: {
1389 child: [PROS_ITEM, CONS_ITEM],
1390 },
1391 [STATS_GROUP]: {
1392 child: STATS_ITEM,
1393 },
1394 [SEQUENCE_ARROW_GROUP]: {
1395 child: SEQUENCE_ARROW_ITEM,
1396 },
1397 [SLOPE_GROUP]: {
1398 child: SLOPE_ITEM,
1399 },
1400 [CONNECTED_CIRCLES_GROUP]: {
1401 child: CONNECTED_CIRCLES_ITEM,
1402 },
1403 [CIRCULAR_GRID_GROUP]: {
1404 child: CIRCULAR_GRID_ITEM,
1405 },
1406 [SNAKE_GROUP]: {
1407 child: SNAKE_ITEM,
1408 },
1409 [STEPS_GROUP]: {
1410 child: STEPS_ITEM,
1411 },
1412 } as const satisfies Record<string, ParentChildRelationship>;
1413
1414 export const LAYOUT_CHILD_TYPES = new Set<string>(
1415 Object.values(PARENT_CHILD_RELATIONSHIP).flatMap(({ child }) =>
1416 Array.isArray(child) ? child : [child],
1417 ),
1418 );
1419
1420 export function isLayoutChildType(type: unknown): type is string {
1421 return typeof type === "string" && LAYOUT_CHILD_TYPES.has(type);
1422 }
1423
1424 function isLayoutToolbarBlockType(type: unknown): type is string {
1425 return (
1426 type === "img" ||
1427 type === KEYS.table ||
1428 type === KEYS.mediaEmbed ||
1429 type === KEYS.callout ||
1430 type === ANTV_INFOGRAPHIC ||
1431 type === BUTTON_ELEMENT ||
1432 type === CONTRIBUTOR_ELEMENT ||
1433 type === LABEL_ELEMENT ||
1434 isChartType(typeof type === "string" ? type : "") ||
1435 isLayoutChildType(type) ||
1436 isLayoutParentType(type)
1437 );
1438 }
1439
1440 function isElementNode(node: unknown): node is TElement {
1441 return (
1442 typeof node === "object" &&
1443 node !== null &&
1444 "type" in node &&
1445 "children" in node
1446 );
1447 }
1448
1449 export function isLayoutParentType(type: unknown): type is string {
1450 return typeof type === "string" && type in PARENT_CHILD_RELATIONSHIP;
1451 }
1452
1453 function getLayoutChildTypes(parentType: string): readonly string[] {
1454 const childType =
1455 PARENT_CHILD_RELATIONSHIP[
1456 parentType as keyof typeof PARENT_CHILD_RELATIONSHIP
1457 ]?.child;
1458
1459 if (!childType) return [];
1460
1461 if (typeof childType === "string") {
1462 return [childType];
1463 }
1464
1465 return childType;
1466 }
1467
1468 export function getLayoutParentTypes(childType: string): readonly string[] {
1469 const parentTypes: string[] = [];
1470
1471 for (const [parentType, relationship] of Object.entries(
1472 PARENT_CHILD_RELATIONSHIP,
1473 )) {
1474 const children =
1475 typeof relationship.child === "string"
1476 ? [relationship.child]
1477 : relationship.child;
1478
1479 if (children.some((child) => child === childType)) {
1480 parentTypes.push(parentType);
1481 }
1482 }
1483
1484 return parentTypes;
1485 }
1486
1487 export function canLayoutChildTypeBePlacedInParent(
1488 childType: string,
1489 parentType: string,
1490 ): boolean {
1491 return getLayoutChildTypes(parentType).includes(childType);
1492 }
1493
1494 function getRelationshipChildType(parentType: string, index: number) {
1495 const childType = PARENT_CHILD_RELATIONSHIP[parentType]?.child;
1496
1497 if (Array.isArray(childType)) {
1498 return childType[index % childType.length];
1499 }
1500
1501 return childType;
1502 }
1503
1504 function getProcessedLayoutVariant(variant?: Record<string, unknown>) {
1505 const processedVariant = variant ? { ...variant } : {};
1506
1507 if (processedVariant.isFunnel !== undefined) {
1508 processedVariant.isFunnel =
1509 processedVariant.isFunnel === "funnel" ||
1510 processedVariant.isFunnel === true;
1511 }
1512
1513 return processedVariant;
1514 }
1515
1516 function getNodeById(
1517 editor: MyEditor,
1518 elementId: string | undefined,
1519 ): NodeEntry<TElement> | undefined {
1520 if (!elementId) return undefined;
1521
1522 return editor.api.node({
1523 id: elementId,
1524 at: [],
1525 }) as NodeEntry<TElement> | undefined;
1526 }
1527
1528 export function getDirectLayoutToolbarTargetEntry(
1529 editor: MyEditor,
1530 elementId: string | undefined,
1531 ): NodeEntry<TElement> | undefined {
1532 const entry = getNodeById(editor, elementId);
1533 if (!entry) return undefined;
1534
1535 const [element] = entry;
1536 return isLayoutToolbarBlockType(element.type) ? entry : undefined;
1537 }
1538
1539 export function getLayoutChangeTargetEntry(
1540 editor: MyEditor,
1541 targetElementId?: string,
1542 ): NodeEntry<TElement> | undefined {
1543 const selectionIds = editor.getOption(BlockSelectionPlugin, "selectedIds");
1544 const selectedElementId = Array.from(selectionIds ?? [])[0];
1545 const elementId =
1546 targetElementId ??
1547 (typeof selectedElementId === "string" ? selectedElementId : undefined);
1548 const directEntry = getNodeById(editor, elementId);
1549
1550 if (!directEntry) return undefined;
1551
1552 const [element, path] = directEntry;
1553
1554 if (isLayoutParentType(element.type) || isChartType(element.type)) {
1555 return directEntry;
1556 }
1557
1558 if (path.length === 0) return directEntry;
1559
1560 const parentPath = PathApi.parent(path) as Path;
1561 const parentElement = NodeApi.get(editor, parentPath);
1562
1563 if (isElementNode(parentElement) && isLayoutParentType(parentElement.type)) {
1564 return [parentElement, parentPath];
1565 }
1566
1567 return directEntry;
1568 }
1569
1570 // Single helper per latest instruction: given only editor and element, derive class.
1571 // Note: CYCLE_ITEM grid positioning is now handled by CycleElement parent component
1572 export function getGridClassForElement(
1573 _editor: PlateEditor,
1574 element: TElement,
1575 ): string {
1576 // CYCLE_ITEM grid positioning is now handled by CycleElement wrapper
1577 if (element.type === CYCLE_ITEM) return "";
1578
1579 if (element.type === PROS_ITEM || element.type === CONS_ITEM) return "h-full";
1580
1581 if (element.type === SNAKE_ITEM) return "z-10 min-w-0 text-center";
1582
1583 if (element.type === KEYS.column) return "flex-1";
1584 return "";
1585 }
1586
1587 export function getGridStyleForElement(
1588 editor: PlateEditor,
1589 element: TElement,
1590 ): CSSProperties | undefined {
1591 if (
1592 element.type !== CIRCULAR_GRID_ITEM &&
1593 element.type !== CONNECTED_CIRCLES_ITEM &&
1594 element.type !== SNAKE_ITEM
1595 ) {
1596 return undefined;
1597 }
1598
1599 const path = editor.api.findPath(element);
1600 if (!path) return undefined;
1601
1602 const index = path.at(-1);
1603 if (typeof index !== "number") return undefined;
1604
1605 if (element.type === SNAKE_ITEM) {
1606 const isTopBandItem = index % 2 === 1;
1607
1608 return {
1609 alignSelf: isTopBandItem ? "end" : "stretch",
1610 gridColumn: getSnakeGridColumn(index),
1611 gridRow: getSnakeGridRow(index),
1612 justifySelf: "stretch",
1613 };
1614 }
1615
1616 if (element.type === CONNECTED_CIRCLES_ITEM) {
1617 const parentPath = PathApi.parent(path);
1618 const parentElement = NodeApi.get(editor, parentPath);
1619 const total = isElementNode(parentElement)
1620 ? parentElement.children.length
1621 : 1;
1622 const position = getConnectedCircleItemPosition(index, total);
1623 const transform = getConnectedCircleItemTransform(index, total);
1624
1625 return {
1626 gridColumn: position.gridColumn,
1627 gridRow: position.gridRow,
1628 justifySelf: "center",
1629 transform,
1630 };
1631 }
1632
1633 const visibleIndex = Math.max(
1634 0,
1635 Math.min(index, CIRCULAR_GRID_MAX_ITEMS - 1),
1636 );
1637 const parentPath = PathApi.parent(path);
1638 const parentElement = NodeApi.get(editor, parentPath);
1639 const total = isElementNode(parentElement)
1640 ? Math.min(parentElement.children.length || 1, CIRCULAR_GRID_MAX_ITEMS)
1641 : 1;
1642 const isPromotedOddItem = total % 2 === 1 && visibleIndex === total - 1;
1643 const row = isPromotedOddItem
1644 ? 1
1645 : total === 4
1646 ? visibleIndex < 2
1647 ? 1
1648 : 3
1649 : Math.floor(visibleIndex / 2) + (total % 2 === 1 ? 2 : 1);
1650 const isLeftColumn = visibleIndex % 2 === 0;
1651
1652 return {
1653 alignSelf: row === 1 ? "end" : row === 3 ? "start" : "center",
1654 display: index < CIRCULAR_GRID_MAX_ITEMS ? undefined : "none",
1655 gridColumn: isPromotedOddItem ? "1 / 3" : isLeftColumn ? "1" : "2",
1656 gridRow: `${row}`,
1657 justifySelf: isPromotedOddItem ? "center" : isLeftColumn ? "end" : "start",
1658 transform:
1659 row === 2 || isPromotedOddItem
1660 ? undefined
1661 : `translateX(${isLeftColumn ? 72 : -72}px)`,
1662 };
1663 }
1664
1665 /**
1666 * Handles the conversion of layout elements to different types
1667 * @param editor - The Plate editor instance
1668 * @param type - The target element type to convert to
1669 * @param variant - Optional variant properties to apply
1670 */
1671 export function handleLayoutChange(
1672 editor: MyEditor,
1673 type: string,
1674 variant?: Record<string, unknown>,
1675 targetElementId?: string,
1676 ): TElement | null {
1677 const targetEntry = getLayoutChangeTargetEntry(editor, targetElementId);
1678 const [element, elementPath] = targetEntry ?? [];
1679
1680 if (!element || !elementPath) return null;
1681
1682 const getUpdatedElement = () => {
1683 const updatedElementId =
1684 typeof element.id === "string" ? element.id : undefined;
1685 const updatedEntry =
1686 getNodeById(editor, updatedElementId) ??
1687 (editor.api.node({ at: elementPath }) as NodeEntry<TElement> | undefined);
1688 const [updatedElement] = updatedEntry ?? [];
1689
1690 return updatedElement ?? null;
1691 };
1692
1693 // Handle non-chart layout/content element conversions.
1694 if (!isChartType(element.type)) {
1695 editor.tf.withoutNormalizing(() => {
1696 const processedVariant = getProcessedLayoutVariant(variant);
1697
1698 // If we're changing variants within the same type, just update the variant
1699 if (type === element.type && Object.keys(processedVariant).length > 0) {
1700 editor.tf.setNodes(
1701 { ...processedVariant, [PALETTE_DROP_MUTABLE_KEY]: false },
1702 {
1703 at: elementPath,
1704 },
1705 );
1706 } else {
1707 // If we're changing the element type, update type and variant
1708 editor.tf.setNodes(
1709 { type, ...processedVariant, [PALETTE_DROP_MUTABLE_KEY]: false },
1710 { at: elementPath },
1711 );
1712 // Update child element types
1713 element.children.forEach((_, index) => {
1714 const childType = getRelationshipChildType(type, index);
1715 if (!childType) return;
1716
1717 editor.tf.setNodes(
1718 { type: childType },
1719 { at: [...elementPath, index] },
1720 );
1721 });
1722 }
1723 updateSiblingsForcefully(editor, element, elementPath);
1724 });
1725 return getUpdatedElement();
1726 }
1727
1728 // Handle chart elements (direct conversion)
1729 if (isChartType(element.type)) {
1730 const currentData = (element as { data?: unknown }).data;
1731
1732 // Add default data if converting to a chart and there's no existing data
1733 const chartData =
1734 currentData && Array.isArray(currentData) && currentData.length > 0
1735 ? undefined // Keep existing data
1736 : getDefaultChartDataForType(type);
1737
1738 editor.tf.setNodes(
1739 {
1740 type,
1741 ...(chartData ? { data: chartData } : {}),
1742 [PALETTE_DROP_MUTABLE_KEY]: false,
1743 },
1744 { at: elementPath },
1745 );
1746 }
1747
1748 return getUpdatedElement();
1749 }
1750
1751 /**
1752 * Handles updating node properties with forced sibling updates
1753 * @param editor - The Plate editor instance
1754 * @param key - The property key to update
1755 * @param value - The new value for the property
1756 */
1757 export function handleNodePropertyUpdate(
1758 editor: MyEditor,
1759 key: string,
1760 value:
1761 | string
1762 | boolean
1763 | number
1764 | Record<string, unknown>
1765 | unknown[]
1766 | undefined,
1767 targetElementId?: string,
1768 ): void {
1769 const selectionIds = editor.getOption(BlockSelectionPlugin, "selectedIds");
1770 const targetId = targetElementId ?? Array.from(selectionIds ?? [])[0];
1771
1772 if (!targetId) return;
1773
1774 const node = Array.from(
1775 editor.api.nodes({
1776 at: [],
1777 match: (n) => "id" in n && n.id === targetId,
1778 }),
1779 );
1780 const [rawElement] = node?.[0] ?? [];
1781
1782 if (!rawElement) return;
1783
1784 const element = rawElement as TElement;
1785
1786 const elementPath = editor.api.findPath(element);
1787 if (!elementPath) return;
1788
1789 editor.tf.withoutNormalizing(() => {
1790 if (value === undefined) {
1791 // Remove the property by setting it to undefined
1792 editor.tf.setNodes(
1793 { [key]: undefined, [PALETTE_DROP_MUTABLE_KEY]: false },
1794 { at: elementPath },
1795 );
1796 } else {
1797 if (element.type === ANTV_INFOGRAPHIC && key === "syntax") {
1798 editor.tf.setNodes(
1799 { [key]: value, data: undefined, [PALETTE_DROP_MUTABLE_KEY]: false },
1800 { at: elementPath },
1801 );
1802 } else {
1803 // Update the node property - convert boolean to string for numbered property
1804 editor.tf.setNodes(
1805 { [key]: value, [PALETTE_DROP_MUTABLE_KEY]: false },
1806 { at: elementPath },
1807 );
1808 }
1809 }
1810 if (isLayoutParentType(element.type)) {
1811 updateSiblingsForcefully(editor, element, elementPath);
1812 } else {
1813 updateSiblingsAfterDrop(editor, element, elementPath);
1814 }
1815 });
1816 }
1817
1817 lines Plain Text