ubiquitous-invention/apps/web/components/editor/extensions/toggle.tsx
Randall Stillwell a508ece6e7 feat: Full project management application scaffold
Complete architecture for a ClickUp/Notion/Miro-class project management app:

- Turborepo monorepo with Next.js 15, TypeScript, PostgreSQL (Drizzle ORM)
- Object-centered database schema (everything is an Object: tasks, projects, docs, whiteboards)
- NextAuth v5 authentication with credentials + OAuth providers
- tRPC v11 API layer with full CRUD for objects, properties, relations, templates, search
- Three-panel UI: collapsible sidebar, center content area, push-in right panel
- Purple/teal theme with light/dark mode via Shadcn/ui + Tailwind CSS
- Multiple views: List, Kanban board (dnd-kit), Table (spreadsheet), Embedded iframe
- TipTap rich text editor with slash commands, custom blocks (callout, toggle, mention, embed, divider), AI block
- Real-time collaboration via Yjs + Hocuspocus with presence/cursors
- tldraw whiteboard with custom shape cards (task, document, project)
- MCP server exposing all app data/tools for AI agents
- AI chat panel, editor AI slash commands, Cmd+K command palette
- Template system with built-in templates (Bug Report, Meeting Notes, Sprint)
- Full-text search with result highlighting
- Docker Compose for full-stack deployment (web + collab + postgres + redis)

Made-with: Cursor
2026-03-26 22:39:16 -05:00

134 lines
3.6 KiB
TypeScript

"use client";
import { Node, mergeAttributes } from "@tiptap/react";
import {
NodeViewContent,
NodeViewWrapper,
ReactNodeViewRenderer,
type NodeViewProps,
} from "@tiptap/react";
import { ChevronDown, ChevronRight } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
function ToggleView(props: NodeViewProps) {
const { node, updateAttributes, selected } = props;
const open = Boolean(node.attrs.open);
const title = typeof node.attrs.title === "string" ? node.attrs.title : "";
const onTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
updateAttributes({ title: e.target.value });
};
return (
<NodeViewWrapper
as="details"
className={cn(
"toggle-block my-2 rounded-lg border border-border bg-card/60 dark:bg-card/40",
selected && "ring-2 ring-primary/40 ring-offset-2 ring-offset-background",
)}
data-type="toggle"
open={open}
onToggle={(e: React.SyntheticEvent<HTMLDetailsElement>) => {
updateAttributes({ open: e.currentTarget.open });
}}
>
<summary
className="flex cursor-pointer list-none items-center gap-2 px-2 py-1.5 text-[15px] font-medium text-foreground marker:hidden [&::-webkit-details-marker]:hidden"
>
<span
className="flex size-6 shrink-0 items-center justify-center text-muted-foreground"
contentEditable={false}
>
{open ? (
<ChevronDown className="size-4" aria-hidden />
) : (
<ChevronRight className="size-4" aria-hidden />
)}
</span>
<input
className="min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
placeholder="Toggle"
type="text"
value={title}
onChange={onTitleChange}
onClick={(e) => e.stopPropagation()}
/>
</summary>
<div
className="toggle-body border-t border-border px-2 py-2 pl-9 [&_.ProseMirror_p]:my-1"
data-toggle-body="true"
>
<NodeViewContent />
</div>
</NodeViewWrapper>
);
}
export const Toggle = Node.create({
name: "toggle",
group: "block",
content: "block+",
defining: true,
addAttributes() {
return {
open: {
default: true,
parseHTML: (el) =>
el instanceof HTMLElement && el.hasAttribute("open"),
renderHTML: (attrs) => (attrs.open ? { open: "" } : {}),
},
title: {
default: "",
parseHTML: (el) => {
if (!(el instanceof HTMLElement)) return "";
const s = el.querySelector(":scope > summary");
return s?.textContent?.trim() ?? "";
},
renderHTML: (attrs) => ({}),
},
};
},
parseHTML() {
return [
{
tag: 'details[data-type="toggle"]',
contentElement: 'div[data-toggle-body="true"]',
},
];
},
renderHTML({ node, HTMLAttributes }) {
return [
"details",
mergeAttributes(HTMLAttributes, {
"data-type": "toggle",
...(node.attrs.open ? { open: "" } : {}),
}),
[
"summary",
{ class: "toggle-summary" },
node.attrs.title ? String(node.attrs.title) : "\u00a0",
],
["div", { "data-toggle-body": "true", class: "toggle-body" }, 0],
];
},
addNodeView() {
return ReactNodeViewRenderer(ToggleView);
},
addKeyboardShortcuts() {
return {
"Mod-Shift-Alt-t": () =>
this.editor.commands.insertContent({
type: this.name,
attrs: { open: true, title: "" },
content: [{ type: "paragraph" }],
}),
};
},
});