返回 presentation-ai
bullet-item.tsx
1 "use client";
2
3 import { cva } from "class-variance-authority";
4 import { NodeApi, PathApi } from "platejs";
5 import { PlateElement, type PlateElementProps } from "platejs/react";
6
7 import { IconPicker } from "@/components/ui/icon-picker";
8 import { cn } from "@/lib/utils";
9 import {
10 type TBulletGroupElement,
11 type TBulletItemElement,
12 } from "../plugins/bullet-plugin";
13 import { getAlignmentClasses } from "../utils";
14 import { getPresentationAccentColor } from "./color-utils";
15 import { getSiblingIndexContext } from "./sibling-index";
16
17 export const bulletItemVariants = cva("", {
18 variants: {
19 bulletType: {
20 numbered: "flex items-start",
21 basic: "flex items-start",
22 arrow: "flex items-start",
23 },
24 },
25 });
26
27 export const bulletMarkerVariants = cva("shrink-0", {
28 variants: {
29 bulletType: {
30 numbered:
31 "flex size-12 items-center justify-center rounded-md bg-primary text-xl font-bold text-primary-foreground",
32 basic: "mt-1 flex size-6 items-center justify-center rounded-full",
33 arrow: "mt-1 flex size-6 items-center justify-center",
34 },
35 },
36 });
37
38 // Arrow SVG component for arrow bullet type
39 export const ArrowMarker = ({ color }: { color: string }) => (
40 <svg
41 width="24"
42 height="24"
43 viewBox="0 0 155.139 155.139"
44 xmlns="http://www.w3.org/2000/svg"
45 >
46 <polygon
47 fill={color}
48 points="155.139,77.566 79.18,1.596 79.18,45.978 0,45.978 0,109.155 79.18,109.155 79.18,153.542"
49 />
50 </svg>
51 );
52
53 // BulletItem component for numbered blocks with content
54 export const BulletItem = (props: PlateElementProps<TBulletItemElement>) => {
55 const { index, parentElement } = getSiblingIndexContext<TBulletGroupElement>(
56 props.editor,
57 props.element,
58 props.path,
59 );
60 const fallbackParentPath = PathApi.parent(props.path);
61 const fallbackParentElement = NodeApi.get(
62 props.editor,
63 fallbackParentPath,
64 ) as TBulletGroupElement | undefined;
65 const resolvedParentElement = parentElement ?? fallbackParentElement;
66 const bulletType = resolvedParentElement?.bulletType ?? "numbered";
67
68 // Get alignment - use item alignment if set, otherwise inherit from parent
69 const itemAlignment = props.element.alignment;
70 const parentAlignment = resolvedParentElement?.alignment;
71 const alignment = itemAlignment ?? parentAlignment ?? "left";
72 const { icon } = props.element;
73 const markerColor = getPresentationAccentColor(
74 props.element,
75 resolvedParentElement,
76 "var(--presentation-primary)",
77 );
78
79 const handleIconSelect = (iconName: string) => {
80 const itemPath = props.editor.api.findPath(props.element);
81 if (!itemPath) return;
82 props.editor.tf.setNodes({ icon: iconName }, { at: itemPath });
83 };
84
85 const markerPlaceholder =
86 bulletType === "numbered" ? (
87 <span className="text-xl font-bold">{index + 1}</span>
88 ) : bulletType === "arrow" ? (
89 <ArrowMarker color={markerColor} />
90 ) : (
91 <span
92 className="size-2 rounded-full"
93 style={{ backgroundColor: markerColor }}
94 />
95 );
96
97 // Force sibling refresh when index changes
98 return (
99 <PlateElement {...props} className={cn("group/bullet-item relative")}>
100 {/* The bullet item layout with numbered block and content */}
101 <div
102 className={cn(
103 "gap-3",
104 bulletItemVariants({ bulletType }),
105 alignment === "right" && "flex-row-reverse",
106 )}
107 >
108 {/* Bullet marker - numbered, basic dot, or arrow */}
109 <IconPicker
110 defaultIcon={icon}
111 placeholder={markerPlaceholder}
112 onIconSelect={(iconName) => handleIconSelect(iconName)}
113 onIconRemove={() => {
114 const itemPath = props.editor.api.findPath(props.element);
115 if (!itemPath) return;
116 props.editor.tf.setNodes({ icon: "" }, { at: itemPath });
117 }}
118 className={cn(
119 bulletMarkerVariants({ bulletType }),
120 "shadow-none hover:opacity-80",
121 bulletType === "numbered" && "text-primary-foreground",
122 bulletType !== "numbered" && "border-transparent bg-transparent",
123 )}
124 size={bulletType === "numbered" ? "lg" : "md"}
125 data-decor="true"
126 style={{
127 backgroundColor:
128 bulletType === "numbered" ? markerColor : "transparent",
129 borderColor: "transparent",
130 color:
131 bulletType === "numbered"
132 ? "var(--presentation-background)"
133 : markerColor,
134 }}
135 />
136
137 <div className={cn("flex-1", getAlignmentClasses(alignment))}>
138 {props.children}
139 </div>
140 </div>
141 </PlateElement>
142 );
143 };
144
144 lines Plain Text