Basic text formatting updates
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { type Component, createSignal, For, createEffect } from "solid-js";
|
||||
import { Type, Underline, Code, Quote, ImageIcon, Heading1, Heading2, Heading3, List, ListTodo, Film, FileText, AppWindow, Minus, Grid3X3, Highlighter, AlignLeft, AlignCenter, AlignRight, Trash2, Bold, Italic } from "lucide-solid";
|
||||
import { Type, Code, Quote, ImageIcon, Heading1, Heading2, Heading3, List, ListTodo, Film, FileText, AppWindow, Minus, Grid3X3, Trash2 } from "lucide-solid";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface CommandItem {
|
||||
@@ -20,33 +20,6 @@ export const getSuggestionItems = ({ query }: { query: string }): CommandItem[]
|
||||
editor.chain().focus().deleteRange(range).setNode("paragraph").run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Bold",
|
||||
description: "Strong text. (**text**)",
|
||||
aliases: ["b", "strong"],
|
||||
icon: Bold,
|
||||
command: ({ editor, range }: { editor: any, range: any }) => {
|
||||
editor.chain().focus().deleteRange(range).toggleBold().run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Italic",
|
||||
description: "Emphasized text. (*text*)",
|
||||
aliases: ["i", "emphasis"],
|
||||
icon: Italic,
|
||||
command: ({ editor, range }: { editor: any, range: any }) => {
|
||||
editor.chain().focus().deleteRange(range).toggleItalic().run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Underline",
|
||||
description: "Underlined text.",
|
||||
aliases: ["u"],
|
||||
icon: Underline,
|
||||
command: ({ editor, range }: { editor: any, range: any }) => {
|
||||
editor.chain().focus().deleteRange(range).toggleUnderline().run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Heading 1",
|
||||
description: "Big section heading.",
|
||||
@@ -158,87 +131,7 @@ export const getSuggestionItems = ({ query }: { query: string }): CommandItem[]
|
||||
editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Highlight",
|
||||
description: "Highlight selected text.",
|
||||
aliases: ["mark"],
|
||||
icon: Highlighter,
|
||||
command: ({ editor, range }: { editor: any, range: any }) => {
|
||||
editor.chain().focus().deleteRange(range).toggleHighlight().run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Align Left",
|
||||
description: "Align text to the left.",
|
||||
icon: AlignLeft,
|
||||
command: ({ editor, range }: { editor: any, range: any }) => {
|
||||
editor.chain().focus().deleteRange(range).setTextAlign("left").run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Align Center",
|
||||
description: "Align text to the center.",
|
||||
icon: AlignCenter,
|
||||
command: ({ editor, range }: { editor: any, range: any }) => {
|
||||
editor.chain().focus().deleteRange(range).setTextAlign("center").run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Align Right",
|
||||
description: "Align text to the right.",
|
||||
icon: AlignRight,
|
||||
command: ({ editor, range }: { editor: any, range: any }) => {
|
||||
editor.chain().focus().deleteRange(range).setTextAlign("right").run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Add Row Above",
|
||||
description: "Insert a new row above the cursor.",
|
||||
icon: Grid3X3,
|
||||
command: ({ editor, range }: { editor: any, range: any }) => {
|
||||
editor.chain().focus().deleteRange(range).addRowBefore().run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Add Row Below",
|
||||
description: "Insert a new row below the cursor.",
|
||||
icon: Grid3X3,
|
||||
command: ({ editor, range }: { editor: any, range: any }) => {
|
||||
editor.chain().focus().deleteRange(range).addRowAfter().run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Delete Row",
|
||||
description: "Delete the current row.",
|
||||
icon: Trash2,
|
||||
command: ({ editor, range }: { editor: any, range: any }) => {
|
||||
editor.chain().focus().deleteRange(range).deleteRow().run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Add Column Left",
|
||||
description: "Insert a new column to the left.",
|
||||
icon: Grid3X3,
|
||||
command: ({ editor, range }: { editor: any, range: any }) => {
|
||||
editor.chain().focus().deleteRange(range).addColumnBefore().run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Add Column Right",
|
||||
description: "Insert a new column to the right.",
|
||||
icon: Grid3X3,
|
||||
command: ({ editor, range }: { editor: any, range: any }) => {
|
||||
editor.chain().focus().deleteRange(range).addColumnAfter().run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Delete Column",
|
||||
description: "Delete the current column.",
|
||||
icon: Trash2,
|
||||
command: ({ editor, range }: { editor: any, range: any }) => {
|
||||
editor.chain().focus().deleteRange(range).deleteColumn().run();
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: "Delete Table",
|
||||
description: "Delete the entire table.",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Component, createEffect, createSignal, For, createMemo, onCleanup } from "solid-js";
|
||||
import { type Component, createEffect, createSignal, For, createMemo, onCleanup, Show } from "solid-js";
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, shareTask, revokeShare, splitTask, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag, now } from "@/store";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
@@ -18,6 +18,8 @@ import { useTheme } from "./ThemeProvider";
|
||||
import { getThemeAdjustedColor } from "@/lib/colors";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { getRecurrenceProgress } from "@/lib/recurrence";
|
||||
import { TextBubbleMenu } from "@/components/TextBubbleMenu";
|
||||
import { TableBubbleMenu } from "@/components/TableBubbleMenu";
|
||||
|
||||
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
|
||||
|
||||
@@ -493,10 +495,19 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
</div>
|
||||
|
||||
<div class="h-px w-full bg-border mt-5" />
|
||||
|
||||
<Show when={editorInstance()}>
|
||||
{(editor: any) => (
|
||||
<div class="flex flex-col pt-1 pb-1 relative z-30">
|
||||
<TextBubbleMenu editor={editor()} />
|
||||
<TableBubbleMenu editor={editor()} />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div class="flex-1 overflow-y-auto px-6 pb-20 flex flex-col">
|
||||
<div class="flex-1 overflow-y-auto px-6 pb-20 pt-4 flex flex-col">
|
||||
<Suspense fallback={<div class="h-32 animate-pulse bg-muted/20 rounded-lg" />}>
|
||||
<TaskEditor
|
||||
content={props.task.content}
|
||||
|
||||
@@ -35,7 +35,6 @@ import { Video } from "@/lib/extensions/video";
|
||||
import { VideoUpload } from "@/lib/extensions/video-upload";
|
||||
import { FileAttachment } from "@/lib/extensions/file-attachment";
|
||||
import { FileUpload } from "@/lib/extensions/file-upload";
|
||||
import { TableBubbleMenu } from "./TableBubbleMenu";
|
||||
|
||||
interface TaskEditorProps {
|
||||
content?: string;
|
||||
@@ -323,9 +322,6 @@ export const TaskEditor: Component<TaskEditorProps> = (props) => {
|
||||
// Handle full screen images as before
|
||||
return (
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<Show when={editor()}>
|
||||
{(e) => <TableBubbleMenu editor={e()} />}
|
||||
</Show>
|
||||
<div class="w-full flex-1 flex flex-col">
|
||||
<div
|
||||
class="w-full h-8 cursor-pointer group flex items-center justify-center shrink-0 transition-all"
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { type Component, createSignal, createEffect, For } from "solid-js";
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
Underline,
|
||||
Highlighter,
|
||||
AlignLeft,
|
||||
AlignCenter,
|
||||
AlignRight,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Eraser,
|
||||
ListTodo,
|
||||
Grid3X3
|
||||
} from "lucide-solid";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Editor } from "@tiptap/core";
|
||||
|
||||
interface GenericAction {
|
||||
label: string;
|
||||
icon: Component;
|
||||
isActive?: () => boolean;
|
||||
command: () => void;
|
||||
iconClass?: string;
|
||||
}
|
||||
|
||||
interface FormatGroup {
|
||||
name: string;
|
||||
actions: GenericAction[];
|
||||
}
|
||||
|
||||
export const TextBubbleMenu: Component<{ editor: Editor }> = (props) => {
|
||||
// Track state of active styles so we can highlight the buttons
|
||||
const [activeFormats, setActiveFormats] = createSignal<Record<string, boolean>>({});
|
||||
|
||||
createEffect(() => {
|
||||
const updateState = () => {
|
||||
const editor = props.editor;
|
||||
if (!editor) return;
|
||||
|
||||
setActiveFormats({
|
||||
bold: editor.isActive('bold'),
|
||||
italic: editor.isActive('italic'),
|
||||
underline: editor.isActive('underline'),
|
||||
highlight: editor.isActive('highlight'),
|
||||
alignLeft: editor.isActive({ textAlign: 'left' }),
|
||||
alignCenter: editor.isActive({ textAlign: 'center' }),
|
||||
alignRight: editor.isActive({ textAlign: 'right' }),
|
||||
h1: editor.isActive('heading', { level: 1 }),
|
||||
h2: editor.isActive('heading', { level: 2 }),
|
||||
h3: editor.isActive('heading', { level: 3 }),
|
||||
checklist: editor.isActive('taskList'),
|
||||
table: editor.isActive('table'),
|
||||
});
|
||||
};
|
||||
|
||||
props.editor.on("selectionUpdate", updateState);
|
||||
props.editor.on("transaction", updateState);
|
||||
|
||||
return () => {
|
||||
props.editor.off("selectionUpdate", updateState);
|
||||
props.editor.off("transaction", updateState);
|
||||
};
|
||||
});
|
||||
|
||||
const groups: FormatGroup[] = [
|
||||
{
|
||||
name: "Style",
|
||||
actions: [
|
||||
{ label: "Bold", icon: () => <Bold size={16} />, isActive: () => activeFormats().bold, command: () => props.editor.chain().focus().toggleBold().run() },
|
||||
{ label: "Italic", icon: () => <Italic size={16} />, isActive: () => activeFormats().italic, command: () => props.editor.chain().focus().toggleItalic().run() },
|
||||
{ label: "Underline", icon: () => <Underline size={16} />, isActive: () => activeFormats().underline, command: () => props.editor.chain().focus().toggleUnderline().run() },
|
||||
{ label: "Highlight", icon: () => <Highlighter size={16} />, isActive: () => activeFormats().highlight, command: () => props.editor.chain().focus().toggleHighlight().run() },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Headings",
|
||||
actions: [
|
||||
{ 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 3", icon: () => <Heading3 size={16} />, isActive: () => activeFormats().h3, command: () => props.editor.chain().focus().toggleHeading({ level: 3 }).run() },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Alignment",
|
||||
actions: [
|
||||
{ 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 Right", icon: () => <AlignRight size={16} />, isActive: () => activeFormats().alignRight, command: () => props.editor.chain().focus().setTextAlign('right').run() },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Blocks",
|
||||
actions: [
|
||||
{ label: "Checklist", icon: () => <ListTodo size={16} />, isActive: () => activeFormats().checklist, command: () => props.editor.chain().focus().toggleTaskList().run() },
|
||||
{ label: "Table", icon: () => <Grid3X3 size={16} />, isActive: () => activeFormats().table, command: () => props.editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Clear",
|
||||
actions: [
|
||||
{ label: "Clear Formatting", icon: () => <Eraser size={16} />, command: () => props.editor.chain().focus().clearNodes().unsetAllMarks().run() },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"relative z-[140] w-full bg-background/80 backdrop-blur-md border-b border-border transition-all duration-300 ease-in-out overflow-hidden flex flex-col h-9 opacity-100"
|
||||
)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center justify-center h-full px-2 max-w-4xl mx-auto w-full">
|
||||
<div class="flex items-center gap-1">
|
||||
<For each={groups}>
|
||||
{(group, groupIdx) => (
|
||||
<>
|
||||
<div class="flex items-center gap-0 px-0.5">
|
||||
<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 group relative shrink-0",
|
||||
action.isActive?.()
|
||||
? "bg-primary/20 text-primary"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
action.iconClass
|
||||
)}
|
||||
>
|
||||
<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">
|
||||
{action.label}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
{groupIdx() < groups.length - 1 && (
|
||||
<div class="w-[1px] h-4 bg-border/50 mx-1" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,6 +12,8 @@ import { TaskEditor } from "@/components/TaskEditor";
|
||||
import { TaskCard } from "@/components/TaskCard";
|
||||
import { NotesSidebar } from "@/components/NotesSidebar";
|
||||
import { toast } from "solid-sonner";
|
||||
import { TextBubbleMenu } from "@/components/TextBubbleMenu";
|
||||
import { TableBubbleMenu } from "@/components/TableBubbleMenu";
|
||||
|
||||
export const NotepadView: Component<{
|
||||
selectedNoteId: string | null;
|
||||
@@ -560,6 +562,14 @@ export const NotepadView: Component<{
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={editorInstance()}>
|
||||
{(editor) => (
|
||||
<div class="mt-4 -mx-4 px-4 md:-mx-6 md:px-6 relative border-t border-border/40 pt-1">
|
||||
<TextBubbleMenu editor={editor()} />
|
||||
<TableBubbleMenu editor={editor()} />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Inside Tabs Section Removed: Sub-tabs are now displayed on the outside as hanging bubbles */}
|
||||
|
||||
Reference in New Issue
Block a user