text formatting finished

This commit is contained in:
2026-03-17 18:20:00 -05:00
parent 35d968e2d8
commit 5f7f3995f8
+49 -2
View File
@@ -10,12 +10,15 @@ import {
Heading1, Heading1,
Heading2, Heading2,
Heading3, Heading3,
Heading,
Eraser, Eraser,
ListTodo, ListTodo,
Grid3X3 Grid3X3,
AlignJustify
} from "lucide-solid"; } from "lucide-solid";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Editor } from "@tiptap/core"; import type { Editor } from "@tiptap/core";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
interface GenericAction { interface GenericAction {
label: string; label: string;
@@ -28,6 +31,8 @@ interface GenericAction {
interface FormatGroup { interface FormatGroup {
name: string; name: string;
actions: GenericAction[]; actions: GenericAction[];
collapseOnMobile?: boolean;
mobileIcon?: Component;
} }
export const TextBubbleMenu: Component<{ editor: Editor }> = (props) => { export const TextBubbleMenu: Component<{ editor: Editor }> = (props) => {
@@ -76,6 +81,8 @@ export const TextBubbleMenu: Component<{ editor: Editor }> = (props) => {
}, },
{ {
name: "Headings", name: "Headings",
collapseOnMobile: true,
mobileIcon: () => <Heading size={16} />,
actions: [ actions: [
{ label: "Heading 1", icon: () => <Heading1 size={16} />, isActive: () => activeFormats().h1, command: () => props.editor.chain().focus().toggleHeading({ level: 1 }).run() }, { label: "Heading 1", icon: () => <Heading1 size={16} />, isActive: () => activeFormats().h1, command: () => props.editor.chain().focus().toggleHeading({ level: 1 }).run() },
{ label: "Heading 2", icon: () => <Heading2 size={16} />, isActive: () => activeFormats().h2, command: () => props.editor.chain().focus().toggleHeading({ level: 2 }).run() }, { label: "Heading 2", icon: () => <Heading2 size={16} />, isActive: () => activeFormats().h2, command: () => props.editor.chain().focus().toggleHeading({ level: 2 }).run() },
@@ -84,6 +91,8 @@ export const TextBubbleMenu: Component<{ editor: Editor }> = (props) => {
}, },
{ {
name: "Alignment", name: "Alignment",
collapseOnMobile: true,
mobileIcon: () => <AlignJustify size={16} />,
actions: [ actions: [
{ label: "Align Left", icon: () => <AlignLeft size={16} />, isActive: () => activeFormats().alignLeft, command: () => props.editor.chain().focus().setTextAlign('left').run() }, { label: "Align Left", icon: () => <AlignLeft size={16} />, isActive: () => activeFormats().alignLeft, command: () => props.editor.chain().focus().setTextAlign('left').run() },
{ label: "Align Center", icon: () => <AlignCenter size={16} />, isActive: () => activeFormats().alignCenter, command: () => props.editor.chain().focus().setTextAlign('center').run() }, { label: "Align Center", icon: () => <AlignCenter size={16} />, isActive: () => activeFormats().alignCenter, command: () => props.editor.chain().focus().setTextAlign('center').run() },
@@ -120,7 +129,7 @@ export const TextBubbleMenu: Component<{ editor: Editor }> = (props) => {
<For each={groups}> <For each={groups}>
{(group, groupIdx) => ( {(group, groupIdx) => (
<> <>
<div class="flex items-center gap-0 px-0.5"> <div class={cn("flex items-center gap-0 px-0.5", group.collapseOnMobile ? "hidden sm:flex" : "")}>
<For each={group.actions}> <For each={group.actions}>
{(action) => ( {(action) => (
<button <button
@@ -137,6 +146,7 @@ export const TextBubbleMenu: Component<{ editor: Editor }> = (props) => {
: "text-muted-foreground hover:bg-muted hover:text-foreground", : "text-muted-foreground hover:bg-muted hover:text-foreground",
action.iconClass action.iconClass
)} )}
title={action.label}
> >
<action.icon /> <action.icon />
<span class="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-2 py-1 text-[10px] font-bold bg-foreground text-background rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-[200] shadow-lg"> <span class="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-2 py-1 text-[10px] font-bold bg-foreground text-background rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-[200] shadow-lg">
@@ -146,6 +156,43 @@ export const TextBubbleMenu: Component<{ editor: Editor }> = (props) => {
)} )}
</For> </For>
</div> </div>
{group.collapseOnMobile && group.mobileIcon && (
<div class="flex sm:hidden items-center gap-0 px-0.5">
<Popover>
<PopoverTrigger class={cn(
"p-1.5 rounded-md transition-colors group relative shrink-0 text-muted-foreground hover:bg-muted hover:text-foreground",
group.actions.some(a => a.isActive?.()) && "bg-primary/20 text-primary"
)}>
<group.mobileIcon />
</PopoverTrigger>
<PopoverContent class="w-auto p-1 flex items-center gap-1 shadow-xl" align="center">
<For each={group.actions}>
{(action) => (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
action.command();
}}
class={cn(
"p-1.5 rounded-md transition-colors relative shrink-0",
action.isActive?.()
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
action.iconClass
)}
title={action.label}
>
<action.icon />
</button>
)}
</For>
</PopoverContent>
</Popover>
</div>
)}
{groupIdx() < groups.length - 1 && ( {groupIdx() < groups.length - 1 && (
<div class="w-[1px] h-4 bg-border/50 mx-1" /> <div class="w-[1px] h-4 bg-border/50 mx-1" />
)} )}