import type { ComponentType, JSXElement } from '../../jsx'; import { Defs, getElementBounds, Group, Path, Rect } from '../../jsx'; import { BtnAdd, BtnRemove, BtnsGroup, ItemsGroup } from '../components'; import { FlexLayout } from '../layouts'; import { getColorPrimary, getPaletteColor } from '../utils'; import { registerStructure } from './registry'; import type { BaseStructureProps } from './types'; export interface SequenceColorSnakeStepsProps extends BaseStructureProps { gap?: number; itemsPerRow?: number; rowGap?: number; columnGap?: number; circleStrokeWidth?: number; } export const SequenceColorSnakeSteps: ComponentType< SequenceColorSnakeStepsProps > = (props) => { const { Title, Item, data, gap = 0, rowGap = 0, itemsPerRow = 3, circleStrokeWidth = 18, options, } = props; const { title, desc, items = [] } = data; const titleContent = Title ? : null; const colorPrimary = getColorPrimary(options); const btnBounds = getElementBounds(<BtnAdd indexes={[0]} />); const itemBounds = getElementBounds( <Item indexes={[0]} data={data} datum={items[0]} positionH="center" />, ); const btnElements: JSXElement[] = []; const itemElements: JSXElement[] = []; const decorElements: JSXElement[] = []; const arcRadius = (rowGap + itemBounds.height) / 2; const arcWidth = arcRadius; items.forEach((item, index) => { const rowIndex = Math.floor(index / itemsPerRow); const colIndex = index % itemsPerRow; const isReversedRow = rowIndex % 2 === 1; const actualColIndex = isReversedRow ? itemsPerRow - 1 - colIndex : colIndex; const itemX = actualColIndex * (itemBounds.width + gap) + arcWidth; const itemY = rowIndex * (itemBounds.height + rowGap); const indexes = [index]; itemElements.push( <Item indexes={indexes} datum={item} data={data} x={itemX} y={itemY} positionH="center" positionV={index % 2 === 1 ? 'flipped' : 'normal'} />, ); btnElements.push( <BtnRemove indexes={indexes} x={itemX + (itemBounds.width - btnBounds.width) / 2} y={itemY + itemBounds.height + 10} />, ); if (index === 0) { btnElements.push( <BtnAdd indexes={indexes} x={itemX + (itemBounds.width - btnBounds.width) / 2} y={itemY - btnBounds.height - 10} />, ); } if (index < items.length - 1) { const nextRowIndex = Math.floor((index + 1) / itemsPerRow); const isSameRow = rowIndex === nextRowIndex; if (isSameRow) { btnElements.push( <BtnAdd indexes={[index + 1]} x={itemX + (itemBounds.width - btnBounds.width) / 2} y={itemY - btnBounds.height - 10} />, ); } else { const currentItemY = itemY + itemBounds.height / 2; const nextItemY = itemY + itemBounds.height + rowGap + itemBounds.height / 2; let arcX, pathD, sweepFlag; if (isReversedRow) { arcX = itemX; sweepFlag = 0; pathD = `M ${arcX} ${currentItemY} A ${arcRadius} ${arcRadius} 0 0 ${sweepFlag} ${arcX} ${nextItemY}`; } else { arcX = itemX + itemBounds.width; sweepFlag = 1; pathD = `M ${arcX} ${currentItemY} A ${arcRadius} ${arcRadius} 0 0 ${sweepFlag} ${arcX} ${nextItemY}`; } const arcHeight = nextItemY - currentItemY; const currentColor = getPaletteColor(options, indexes); const nextColor = getPaletteColor(options, [index + 1]); const linearGradientId = `gradient-arc-${index}`; decorElements.push( <> <Defs> <linearGradient id={linearGradientId} x1="0%" y1="0%" x2="0%" y2="100%" > <stop offset="0%" stopColor={currentColor || colorPrimary} /> <stop offset="100%" stopColor={nextColor || colorPrimary} /> </linearGradient> </Defs> <Path d={pathD} stroke={`url(#${linearGradientId})`} strokeWidth={circleStrokeWidth} fill="none" width={arcWidth} height={arcHeight} data-element-type="shape" /> </>, ); const btnX = isReversedRow ? arcX - arcRadius - btnBounds.width / 2 : arcX + arcRadius - btnBounds.width / 2; const btnY = itemY + itemBounds.height + rowGap / 2 - btnBounds.height / 2; btnElements.push(<BtnAdd indexes={[index + 1]} x={btnX} y={btnY} />); } } }); if (items.length > 0) { const lastIndex = items.length - 1; const lastRowIndex = Math.floor(lastIndex / itemsPerRow); const lastColIndex = lastIndex % itemsPerRow; const isLastReversedRow = lastRowIndex % 2 === 1; const lastActualColIndex = isLastReversedRow ? itemsPerRow - 1 - lastColIndex : lastColIndex; const lastItemX = lastActualColIndex * (itemBounds.width + gap); const lastItemY = lastRowIndex * (itemBounds.height + rowGap); btnElements.push( <BtnAdd indexes={[items.length]} x={lastItemX + (itemBounds.width - btnBounds.width) / 2} y={lastItemY + itemBounds.height + btnBounds.height + 20} />, ); } // Add left rectangle bar for the first item when there's an arc on the left side if (items.length / itemsPerRow > 2) { const arcRadius = (rowGap + itemBounds.height) / 2; const firstItemColor = getPaletteColor(options, [0]); decorElements.push( <Rect x={0} y={itemBounds.height / 2 - circleStrokeWidth / 2} width={arcRadius} height={circleStrokeWidth} fill={firstItemColor || colorPrimary} data-element-type="shape" />, ); } return ( <FlexLayout id="infographic-container" flexDirection="column" justifyContent="center" alignItems="center" > {titleContent} <Group width={ itemsPerRow * itemBounds.width + (itemsPerRow - 1) * gap + arcRadius * 2 } height={ Math.ceil(items.length / itemsPerRow) * (itemBounds.height + rowGap) } > <Group>{decorElements}</Group> <ItemsGroup>{itemElements}</ItemsGroup> <BtnsGroup>{btnElements}</BtnsGroup> </Group> </FlexLayout> ); }; registerStructure('sequence-color-snake-steps', { component: SequenceColorSnakeSteps, composites: ['title', 'item'], });