| 1 | import { NodeApi, PathApi } from "platejs"; |
| 2 | import { SlateElement, type SlateElementProps } from "platejs/static"; |
| 3 | |
| 4 | import { cn } from "@/lib/utils"; |
| 5 | import { |
| 6 | type TTimelineGroupElement, |
| 7 | type TTimelineItemElement, |
| 8 | } from "../../plugins/timeline-plugin"; |
| 9 | import { getAlignmentClasses } from "../../utils"; |
| 10 | import { PresentationIcon } from "../presentation-icon"; |
| 11 | import { |
| 12 | circleVariants, |
| 13 | connectorLineVariants, |
| 14 | containerVariants, |
| 15 | contentVariants, |
| 16 | } from "../timeline-item"; |
| 17 | |
| 18 | const HORIZONTAL_DOUBLE_CONTENT_CLASS = |
| 19 | "w-[min(22rem,calc(200%-2.5rem))] max-w-none px-3 text-center [&_h3]:mb-2 [&_h3]:text-base [&_p]:text-xs [&_p]:leading-snug"; |
| 20 | const VERTICAL_DOUBLE_CONTENT_CLASS = |
| 21 | "relative z-10 min-h-40 max-w-full py-5 [&_h3]:mb-2 [&_h3]:text-base [&_p]:text-xs [&_p]:leading-snug"; |
| 22 | |
| 23 | export function TimelineItemStatic(props: SlateElementProps) { |
| 24 | const element = props.element as TTimelineItemElement; |
| 25 | const parentPath = PathApi.parent( |
| 26 | props.editor.api.findPath(props.element) ?? [-1], |
| 27 | ); |
| 28 | const parentElement = NodeApi.get( |
| 29 | props.editor, |
| 30 | parentPath, |
| 31 | ) as TTimelineGroupElement; |
| 32 | const orientation = parentElement.orientation ?? "vertical"; |
| 33 | const sidedness = parentElement.sidedness ?? "single"; |
| 34 | const showLine = parentElement.showLine ?? true; |
| 35 | const numbered = parentElement.numbered ?? true; |
| 36 | const variant = parentElement.variant ?? "default"; |
| 37 | const index = |
| 38 | (props.editor.api.findPath(props.element)?.at(-1) as number) ?? 0; |
| 39 | const totalItems = parentElement.children.length; |
| 40 | const itemNumber = index + 1; |
| 41 | const isEven = |
| 42 | orientation === "horizontal" && sidedness === "double" |
| 43 | ? index % 2 === 0 |
| 44 | : itemNumber % 2 === 0; |
| 45 | const isFirstItem = index === 0; |
| 46 | const isLastItem = index === totalItems - 1; |
| 47 | |
| 48 | const alignment = parentElement.alignment ?? "left"; |
| 49 | const horizontalDoubleEdgeAlignmentClass = |
| 50 | sidedness === "double" && orientation === "horizontal" |
| 51 | ? isFirstItem |
| 52 | ? "justify-self-start" |
| 53 | : isLastItem |
| 54 | ? "justify-self-end" |
| 55 | : "justify-self-center" |
| 56 | : ""; |
| 57 | const horizontalDoubleTextAlignmentClass = |
| 58 | sidedness === "double" && orientation === "horizontal" |
| 59 | ? isFirstItem |
| 60 | ? "items-start text-left [&>*]:self-start [&>*]:text-left" |
| 61 | : isLastItem |
| 62 | ? "items-end text-right [&>*]:self-end [&>*]:text-right" |
| 63 | : "items-center text-center [&>*]:self-center [&>*]:text-center" |
| 64 | : ""; |
| 65 | const verticalDoubleOverlapClass = |
| 66 | sidedness === "double" && orientation === "vertical" |
| 67 | ? isFirstItem |
| 68 | ? "-mb-10 pb-10" |
| 69 | : isLastItem |
| 70 | ? "-mt-10 pt-10" |
| 71 | : "-my-10 py-10" |
| 72 | : ""; |
| 73 | const verticalDoubleTextAlignmentClass = |
| 74 | sidedness === "double" && orientation === "vertical" |
| 75 | ? isEven |
| 76 | ? "items-start text-left [&>*]:self-start [&>*]:text-left" |
| 77 | : "items-end text-right [&>*]:self-end [&>*]:text-right" |
| 78 | : ""; |
| 79 | const timelineColor = |
| 80 | (parentElement.color as string) || "var(--presentation-smart-layout)"; |
| 81 | const timelineTextColor = "var(--presentation-background)"; |
| 82 | const { icon } = element; |
| 83 | return ( |
| 84 | //* Container |
| 85 | <div |
| 86 | className={cn( |
| 87 | containerVariants({ |
| 88 | orientation, |
| 89 | sidedness, |
| 90 | isEven, |
| 91 | showLine, |
| 92 | alignment, |
| 93 | }), |
| 94 | sidedness === "single" && |
| 95 | alignment === "left" && |
| 96 | orientation === "horizontal" && |
| 97 | "flex-col", |
| 98 | sidedness === "single" && |
| 99 | alignment === "right" && |
| 100 | orientation === "horizontal" && |
| 101 | "flex-col-reverse", |
| 102 | sidedness === "single" && |
| 103 | alignment === "left" && |
| 104 | orientation === "vertical" && |
| 105 | "flex-row", |
| 106 | sidedness === "single" && |
| 107 | alignment === "right" && |
| 108 | orientation === "vertical" && |
| 109 | "flex-row-reverse", |
| 110 | )} |
| 111 | > |
| 112 | {/* Circle */} |
| 113 | <div |
| 114 | data-shape="ellipse" |
| 115 | data-shape-role="timeline-marker" |
| 116 | data-fill-color={timelineColor} |
| 117 | data-text-color={timelineTextColor} |
| 118 | data-shape-text={numbered ? String(itemNumber) : undefined} |
| 119 | className={cn( |
| 120 | circleVariants({ orientation, sidedness }), |
| 121 | sidedness === "single" && alignment === "right" && "rotate-180", |
| 122 | sidedness === "single" && |
| 123 | alignment === "right" && |
| 124 | orientation === "horizontal" && |
| 125 | "translate-y-3", |
| 126 | sidedness === "double" && |
| 127 | orientation === "horizontal" && |
| 128 | "row-start-2", |
| 129 | )} |
| 130 | style={ |
| 131 | { |
| 132 | backgroundColor: timelineColor, |
| 133 | color: timelineTextColor, |
| 134 | "--ring-color": timelineColor, |
| 135 | "--before-bg": timelineColor, |
| 136 | } as React.CSSProperties & { |
| 137 | "--ring-color": string; |
| 138 | "--before-bg": string; |
| 139 | } |
| 140 | } |
| 141 | > |
| 142 | {showLine ? ( |
| 143 | <div |
| 144 | aria-hidden="true" |
| 145 | data-shape="rect" |
| 146 | data-shape-role="timeline-connector" |
| 147 | data-fill-color={timelineColor} |
| 148 | data-orientation={orientation} |
| 149 | className={cn( |
| 150 | connectorLineVariants({ orientation, sidedness, isEven }), |
| 151 | )} |
| 152 | /> |
| 153 | ) : null} |
| 154 | <div |
| 155 | className={cn( |
| 156 | "relative z-10", |
| 157 | sidedness === "single" && alignment === "right" && "rotate-180", |
| 158 | )} |
| 159 | > |
| 160 | {icon ? ( |
| 161 | <PresentationIcon icon={icon} size={18} /> |
| 162 | ) : numbered ? ( |
| 163 | itemNumber |
| 164 | ) : ( |
| 165 | "" |
| 166 | )} |
| 167 | </div> |
| 168 | </div> |
| 169 | {/* Content */} |
| 170 | <SlateElement |
| 171 | className={cn( |
| 172 | "max-w-full", |
| 173 | contentVariants({ orientation, sidedness }), |
| 174 | getAlignmentClasses(alignment), |
| 175 | sidedness === "double" && |
| 176 | orientation === "horizontal" && |
| 177 | isEven && |
| 178 | cn( |
| 179 | "row-start-1 justify-end pb-8", |
| 180 | horizontalDoubleEdgeAlignmentClass, |
| 181 | horizontalDoubleTextAlignmentClass, |
| 182 | HORIZONTAL_DOUBLE_CONTENT_CLASS, |
| 183 | ), |
| 184 | sidedness === "double" && |
| 185 | orientation === "horizontal" && |
| 186 | !isEven && |
| 187 | cn( |
| 188 | "row-start-3 justify-start pt-8", |
| 189 | horizontalDoubleEdgeAlignmentClass, |
| 190 | horizontalDoubleTextAlignmentClass, |
| 191 | HORIZONTAL_DOUBLE_CONTENT_CLASS, |
| 192 | ), |
| 193 | sidedness === "double" && |
| 194 | orientation === "vertical" && |
| 195 | cn( |
| 196 | VERTICAL_DOUBLE_CONTENT_CLASS, |
| 197 | verticalDoubleOverlapClass, |
| 198 | verticalDoubleTextAlignmentClass, |
| 199 | ), |
| 200 | variant === "boxes" && |
| 201 | "flex-1 w-full rounded-xl border bg-card p-4 text-card-foreground shadow-sm", |
| 202 | )} |
| 203 | {...props} |
| 204 | > |
| 205 | {props.children} |
| 206 | </SlateElement> |
| 207 | </div> |
| 208 | ); |
| 209 | } |
| 210 |