| 1 | "use client"; |
| 2 | |
| 3 | import { PlateElement, type PlateElementProps } from "platejs/react"; |
| 4 | import { useId } from "react"; |
| 5 | |
| 6 | import { cn } from "@/lib/utils"; |
| 7 | import { usePresentationState } from "@/states/presentation-state"; |
| 8 | import { type TBoxGroupElement } from "../plugins/box-plugin"; |
| 9 | import { columnSizeVariant, getDefaultColumnSize } from "../utils"; |
| 10 | import { |
| 11 | ALTERNATING_BOX_COLUMN_GAP_PX, |
| 12 | ALTERNATING_BOX_MIN_ROW_HEIGHT_PX, |
| 13 | ALTERNATING_BOX_ROW_GAP_PX, |
| 14 | getAlternatingBoxPlacementCss, |
| 15 | useAlternatingBoxFitScale, |
| 16 | } from "./alternating-box-layout"; |
| 17 | import { getDiagramFitFrameStyle } from "./diagram-fit"; |
| 18 | |
| 19 | function getBoxGroupJustifyClass( |
| 20 | alignment: TBoxGroupElement["alignment"] = "center", |
| 21 | ) { |
| 22 | if (alignment === "left") return "justify-start"; |
| 23 | if (alignment === "right") return "justify-end"; |
| 24 | return "justify-center"; |
| 25 | } |
| 26 | |
| 27 | export default function BoxGroup(props: PlateElementProps<TBoxGroupElement>) { |
| 28 | const isPresenting = usePresentationState((state) => state.isPresenting); |
| 29 | const alternatingLayoutId = useId(); |
| 30 | const { |
| 31 | alignment = "center", |
| 32 | boxType = "solid", |
| 33 | orientation = "horizontal", |
| 34 | } = props.element; |
| 35 | const columnSize = |
| 36 | props.element.columnSize ?? |
| 37 | getDefaultColumnSize(props.element.children.length); |
| 38 | const isAlternating = boxType === "alternating"; |
| 39 | const alternatingItemCount = Math.max(props.element.children.length, 1); |
| 40 | const alternatingPlacementCss = getAlternatingBoxPlacementCss( |
| 41 | alternatingLayoutId, |
| 42 | alternatingItemCount, |
| 43 | orientation, |
| 44 | ); |
| 45 | const { containerRef, fitStyle, frameStyle, layoutRef } = |
| 46 | useAlternatingBoxFitScale<HTMLDivElement>( |
| 47 | alternatingItemCount, |
| 48 | orientation, |
| 49 | ); |
| 50 | |
| 51 | if (isAlternating) { |
| 52 | return ( |
| 53 | <PlateElement {...props} className="relative mb-4"> |
| 54 | <div |
| 55 | ref={containerRef} |
| 56 | className={cn( |
| 57 | "flex w-full overflow-visible", |
| 58 | getBoxGroupJustifyClass(alignment), |
| 59 | )} |
| 60 | > |
| 61 | <div |
| 62 | style={{ |
| 63 | ...frameStyle, |
| 64 | ...getDiagramFitFrameStyle(alignment), |
| 65 | }} |
| 66 | > |
| 67 | <style>{alternatingPlacementCss}</style> |
| 68 | <div ref={layoutRef} className="relative" style={fitStyle}> |
| 69 | <div |
| 70 | aria-hidden="true" |
| 71 | className="pointer-events-none absolute rounded-full" |
| 72 | data-decor="true" |
| 73 | style={{ |
| 74 | background: |
| 75 | "color-mix(in oklch, var(--presentation-smart-layout, var(--presentation-primary)) 72%, transparent)", |
| 76 | boxShadow: |
| 77 | "0 0 0 1px color-mix(in oklch, var(--presentation-background) 65%, transparent)", |
| 78 | ...(orientation === "vertical" |
| 79 | ? { |
| 80 | bottom: 0, |
| 81 | left: "50%", |
| 82 | top: 0, |
| 83 | transform: "translateX(-50%)", |
| 84 | width: 2, |
| 85 | zIndex: 20, |
| 86 | } |
| 87 | : { |
| 88 | height: 2, |
| 89 | left: 0, |
| 90 | right: 0, |
| 91 | top: "50%", |
| 92 | transform: "translateY(-50%)", |
| 93 | zIndex: 20, |
| 94 | }), |
| 95 | }} |
| 96 | /> |
| 97 | <div |
| 98 | className="relative z-10 grid items-stretch *:min-w-0" |
| 99 | data-alternating-box-layout={alternatingLayoutId} |
| 100 | style={{ |
| 101 | columnGap: ALTERNATING_BOX_COLUMN_GAP_PX, |
| 102 | gridTemplateColumns: |
| 103 | orientation === "vertical" |
| 104 | ? "repeat(2, minmax(0, 1fr))" |
| 105 | : `repeat(${alternatingItemCount}, minmax(0, 1fr))`, |
| 106 | gridTemplateRows: |
| 107 | orientation === "vertical" |
| 108 | ? `repeat(${alternatingItemCount}, minmax(${ALTERNATING_BOX_MIN_ROW_HEIGHT_PX}px, auto))` |
| 109 | : `repeat(2, minmax(${ALTERNATING_BOX_MIN_ROW_HEIGHT_PX}px, 1fr))`, |
| 110 | rowGap: ALTERNATING_BOX_ROW_GAP_PX, |
| 111 | }} |
| 112 | > |
| 113 | {props.children} |
| 114 | </div> |
| 115 | </div> |
| 116 | </div> |
| 117 | </div> |
| 118 | </PlateElement> |
| 119 | ); |
| 120 | } |
| 121 | |
| 122 | return ( |
| 123 | <PlateElement {...props} className="relative mb-4"> |
| 124 | <div |
| 125 | className={cn( |
| 126 | "max-w-full", |
| 127 | isPresenting && "items-stretch *:min-w-0", |
| 128 | columnSizeVariant({ columnSize }), |
| 129 | boxType === "joined" || boxType === "joined-icon" ? "" : "gap-4", |
| 130 | )} |
| 131 | > |
| 132 | {props.children} |
| 133 | </div> |
| 134 | </PlateElement> |
| 135 | ); |
| 136 | } |
| 137 |