"use client";
import * as React from "react";
import { SelectionBubble, type BubbleItem } from "@/components/ui/selection-bubble-menu";
function exec(command: string, value?: string) {
try {
document.execCommand(command, false, value);
} catch {
return;
}
}
function state(command: string) {
try {
return document.queryCommandState(command);
} catch {
return false;
}
}
const LinkIcon = (
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round">
<path d="M10 13a5 5 0 0 0 7 0l2-2a5 5 0 0 0-7-7l-1 1" />
<path d="M14 11a5 5 0 0 0-7 0l-2 2a5 5 0 0 0 7 7l1-1" />
</svg>
);
const ACTIONS: BubbleItem[] = [
{
id: "bold",
label: "Bold",
shortcut: "Cmd B",
icon: <span className="text-[14px] font-bold">B</span>,
isActive: () => state("bold"),
run: () => exec("bold"),
},
{
id: "italic",
label: "Italic",
shortcut: "Cmd I",
icon: <span className="font-serif text-[14px] italic">I</span>,
isActive: () => state("italic"),
run: () => exec("italic"),
},
{
id: "underline",
label: "Underline",
shortcut: "Cmd U",
icon: <span className="text-[13px] underline underline-offset-2">U</span>,
isActive: () => state("underline"),
run: () => exec("underline"),
},
{
id: "strike",
label: "Strikethrough",
icon: <span className="text-[13px] line-through">S</span>,
isActive: () => state("strikeThrough"),
run: () => exec("strikeThrough"),
},
{ divider: true },
{
id: "link",
label: "Add link",
icon: LinkIcon,
run: ({ text }) => {
const url = window.prompt("Link to", text.startsWith("http") ? text : "https://");
if (url) exec("createLink", url);
},
},
];
export default function SelectionBubbleDemo() {
return (
<div className="min-h-screen overflow-x-hidden bg-zinc-100 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100">
<div className="mx-auto max-w-2xl px-5 py-10 sm:py-14">
<div className="mb-6 flex items-center gap-2 text-[12px] text-zinc-400">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-zinc-400" />
Select any text to format it
</div>
<SelectionBubble actions={ACTIONS}>
<article
contentEditable
suppressContentEditableWarning
spellCheck={false}
className="rounded-2xl border border-zinc-200 bg-white p-6 leading-relaxed text-zinc-700 outline-none selection:bg-zinc-900/10 focus-visible:ring-2 focus-visible:ring-zinc-900/10 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300 dark:selection:bg-white/20 sm:p-8"
>
<h1 className="mb-3 text-[22px] font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">
The floating toolbar
</h1>
<p className="mb-4">
Highlight a few words in this paragraph and a small toolbar springs up right above
your selection. Make something <b>bold</b>, add a touch of <i>emphasis</i>, or drop
in a <u>link</u> - the menu follows the text, flips below when it runs out of room at
the top, and stays glued while you scroll.
</p>
<p className="mb-4">
It never steals your selection: pressing a button keeps the exact range you picked,
so you can stack formats without re-selecting. Try selecting across two lines, or a
single word near the very top of the card to watch it flip.
</p>
<p>
Everything here is one component - bring your own actions, icons, and shortcuts, and
it handles positioning, the caret, and keyboard selection on its own.
</p>
</article>
</SelectionBubble>
</div>
</div>
);
}