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 23:39:16 -04:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import * as React from "react";
|
|
|
|
|
import { useEditor, EditorContent, type Editor } from "@tiptap/react";
|
|
|
|
|
import { BubbleMenu } from "@tiptap/react/menus";
|
|
|
|
|
import StarterKit from "@tiptap/starter-kit";
|
|
|
|
|
import Placeholder from "@tiptap/extension-placeholder";
|
|
|
|
|
import Image from "@tiptap/extension-image";
|
|
|
|
|
import TaskList from "@tiptap/extension-task-list";
|
|
|
|
|
import TaskItem from "@tiptap/extension-task-item";
|
|
|
|
|
import Highlight from "@tiptap/extension-highlight";
|
|
|
|
|
import Typography from "@tiptap/extension-typography";
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
import Underline from "@tiptap/extension-underline";
|
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 23:39:16 -04:00
|
|
|
import TextAlign from "@tiptap/extension-text-align";
|
|
|
|
|
import HorizontalRule from "@tiptap/extension-horizontal-rule";
|
|
|
|
|
import CodeBlock from "@tiptap/extension-code-block";
|
|
|
|
|
import { Table, TableRow, TableCell, TableHeader } from "@tiptap/extension-table";
|
|
|
|
|
import {
|
|
|
|
|
Bold,
|
|
|
|
|
Italic,
|
|
|
|
|
Underline as UnderlineIcon,
|
|
|
|
|
Strikethrough,
|
|
|
|
|
Code,
|
|
|
|
|
Link2,
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import {
|
|
|
|
|
Tooltip,
|
|
|
|
|
TooltipContent,
|
|
|
|
|
TooltipProvider,
|
|
|
|
|
TooltipTrigger,
|
|
|
|
|
} from "@/components/ui/tooltip";
|
|
|
|
|
|
|
|
|
|
import { SlashCommand } from "./slash-menu";
|
|
|
|
|
import { EditorToolbar } from "./toolbar";
|
|
|
|
|
import { BlockDragHandle } from "./drag-handle";
|
|
|
|
|
import { Callout } from "./extensions/callout";
|
|
|
|
|
import { Toggle } from "./extensions/toggle";
|
|
|
|
|
import { Mention } from "./extensions/mention";
|
|
|
|
|
import { Embed } from "./extensions/embed";
|
|
|
|
|
import { Divider } from "./extensions/divider";
|
|
|
|
|
import { AiBlock } from "./extensions/ai-block";
|
|
|
|
|
|
|
|
|
|
function bubbleCh(editor: Editor) {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
return editor.chain().focus() as any;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const editorContentStyles = cn(
|
|
|
|
|
"editor-prose min-h-[280px] w-full max-w-none px-14 py-10 text-[15px] leading-7 text-foreground antialiased",
|
|
|
|
|
"focus:outline-none [&_.ProseMirror]:outline-none [&_.ProseMirror]:min-h-[240px]",
|
|
|
|
|
"[&_.ProseMirror_p.is-empty]:relative [&_.ProseMirror_p.is-empty::before]:pointer-events-none [&_.ProseMirror_p.is-empty::before]:float-left [&_.ProseMirror_p.is-empty::before]:h-0 [&_.ProseMirror_p.is-empty::before]:text-muted-foreground [&_.ProseMirror_p.is-empty::before]:content-[attr(data-placeholder)]",
|
|
|
|
|
"[&_.ProseMirror_p]:my-1 [&_.ProseMirror_p]:leading-7",
|
|
|
|
|
"[&_.ProseMirror_h1]:mb-3 [&_.ProseMirror_h1]:mt-8 [&_.ProseMirror_h1]:text-3xl [&_.ProseMirror_h1]:font-bold [&_.ProseMirror_h1]:tracking-tight [&_.ProseMirror_h1]:text-foreground",
|
|
|
|
|
"[&_.ProseMirror_h2]:mb-2 [&_.ProseMirror_h2]:mt-6 [&_.ProseMirror_h2]:text-2xl [&_.ProseMirror_h2]:font-semibold [&_.ProseMirror_h2]:tracking-tight",
|
|
|
|
|
"[&_.ProseMirror_h3]:mb-2 [&_.ProseMirror_h3]:mt-5 [&_.ProseMirror_h3]:text-xl [&_.ProseMirror_h3]:font-semibold",
|
|
|
|
|
"[&_.ProseMirror_ul]:my-2 [&_.ProseMirror_ul]:list-disc [&_.ProseMirror_ul]:pl-6",
|
|
|
|
|
"[&_.ProseMirror_ol]:my-2 [&_.ProseMirror_ol]:list-decimal [&_.ProseMirror_ol]:pl-6",
|
|
|
|
|
"[&_.ProseMirror_li]:my-0.5 [&_.ProseMirror_li_p]:my-0",
|
|
|
|
|
"[&_.ProseMirror_blockquote]:my-3 [&_.ProseMirror_blockquote]:border-l-4 [&_.ProseMirror_blockquote]:border-teal [&_.ProseMirror_blockquote]:bg-muted/40 [&_.ProseMirror_blockquote]:py-1 [&_.ProseMirror_blockquote]:pl-4 [&_.ProseMirror_blockquote]:pr-2 [&_.ProseMirror_blockquote]:italic [&_.ProseMirror_blockquote]:text-muted-foreground",
|
|
|
|
|
"[&_.ProseMirror_pre]:my-4 [&_.ProseMirror_pre]:overflow-x-auto [&_.ProseMirror_pre]:rounded-lg [&_.ProseMirror_pre]:border [&_.ProseMirror_pre]:border-border [&_.ProseMirror_pre]:bg-muted/80 [&_.ProseMirror_pre]:p-4 [&_.ProseMirror_pre]:font-mono [&_.ProseMirror_pre]:text-sm",
|
|
|
|
|
"[&_.ProseMirror_code]:rounded-md [&_.ProseMirror_code]:bg-muted [&_.ProseMirror_code]:px-1.5 [&_.ProseMirror_code]:py-0.5 [&_.ProseMirror_code]:font-mono [&_.ProseMirror_code]:text-[0.9em] [&_.ProseMirror_code]:text-foreground",
|
|
|
|
|
"[&_.ProseMirror_pre_code]:bg-transparent [&_.ProseMirror_pre_code]:p-0",
|
|
|
|
|
"[&_.ProseMirror_hr]:my-6 [&_.ProseMirror_hr]:border-border",
|
|
|
|
|
"[&_.ProseMirror_img]:my-4 [&_.ProseMirror_img]:max-h-[480px] [&_.ProseMirror_img]:w-auto [&_.ProseMirror_img]:max-w-full [&_.ProseMirror_img]:rounded-lg [&_.ProseMirror_img]:border [&_.ProseMirror_img]:border-border",
|
|
|
|
|
"[&_.ProseMirror_table]:my-4 [&_.ProseMirror_table]:w-full [&_.ProseMirror_table]:border-collapse [&_.ProseMirror_table]:overflow-hidden [&_.ProseMirror_table]:rounded-md [&_.ProseMirror_table]:border [&_.ProseMirror_table]:border-border",
|
|
|
|
|
"[&_.ProseMirror_td]:border [&_.ProseMirror_td]:border-border [&_.ProseMirror_td]:bg-background [&_.ProseMirror_td]:px-3 [&_.ProseMirror_td]:py-2",
|
|
|
|
|
"[&_.ProseMirror_th]:border [&_.ProseMirror_th]:border-border [&_.ProseMirror_th]:bg-muted [&_.ProseMirror_th]:px-3 [&_.ProseMirror_th]:py-2 [&_.ProseMirror_th]:text-left [&_.ProseMirror_th]:font-medium",
|
|
|
|
|
"[&_.ProseMirror_.ProseMirror-selectednode]:ring-2 [&_.ProseMirror_.ProseMirror-selectednode]:ring-primary/40",
|
|
|
|
|
"dark:[&_.ProseMirror_blockquote]:bg-muted/20",
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export type BlockEditorProps = {
|
|
|
|
|
content?: string;
|
|
|
|
|
onChange?: (html: string) => void;
|
|
|
|
|
editable?: boolean;
|
|
|
|
|
className?: string;
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function BlockEditor({
|
|
|
|
|
content,
|
|
|
|
|
onChange,
|
|
|
|
|
editable = true,
|
|
|
|
|
className,
|
|
|
|
|
placeholder,
|
|
|
|
|
}: BlockEditorProps) {
|
|
|
|
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
const extensions = React.useMemo(
|
|
|
|
|
() => [
|
|
|
|
|
StarterKit.configure({
|
|
|
|
|
heading: { levels: [1, 2, 3] },
|
|
|
|
|
codeBlock: false,
|
|
|
|
|
horizontalRule: false,
|
|
|
|
|
link: {
|
|
|
|
|
openOnClick: false,
|
|
|
|
|
HTMLAttributes: {
|
|
|
|
|
class:
|
|
|
|
|
"text-primary underline underline-offset-2 decoration-primary/50",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
Placeholder.configure({
|
|
|
|
|
placeholder:
|
|
|
|
|
placeholder ??
|
|
|
|
|
'Write something, or type "/" for commands…',
|
|
|
|
|
}),
|
|
|
|
|
CodeBlock.configure({
|
|
|
|
|
HTMLAttributes: {
|
|
|
|
|
class: "font-mono text-sm",
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
HorizontalRule,
|
|
|
|
|
Image.configure({
|
|
|
|
|
HTMLAttributes: {
|
|
|
|
|
class: "rounded-lg border border-border",
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
TaskList,
|
|
|
|
|
TaskItem.configure({
|
|
|
|
|
nested: true,
|
|
|
|
|
HTMLAttributes: {
|
|
|
|
|
class: "flex gap-2",
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
Highlight.configure({
|
|
|
|
|
multicolor: true,
|
|
|
|
|
}),
|
|
|
|
|
Typography,
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
Underline,
|
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 23:39:16 -04:00
|
|
|
TextAlign.configure({
|
|
|
|
|
types: ["heading", "paragraph"],
|
|
|
|
|
}),
|
|
|
|
|
Table.configure({
|
|
|
|
|
resizable: false,
|
|
|
|
|
}),
|
|
|
|
|
TableRow,
|
|
|
|
|
TableHeader,
|
|
|
|
|
TableCell,
|
|
|
|
|
SlashCommand,
|
|
|
|
|
Callout,
|
|
|
|
|
Toggle,
|
|
|
|
|
Mention,
|
|
|
|
|
Embed,
|
|
|
|
|
Divider,
|
|
|
|
|
AiBlock,
|
|
|
|
|
],
|
|
|
|
|
[placeholder],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const editor = useEditor(
|
|
|
|
|
{
|
|
|
|
|
immediatelyRender: false,
|
|
|
|
|
extensions,
|
|
|
|
|
content: content ?? "<p></p>",
|
|
|
|
|
editable,
|
|
|
|
|
editorProps: {
|
|
|
|
|
attributes: {
|
|
|
|
|
class: "focus:outline-none",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
onUpdate: ({ editor: ed }) => {
|
|
|
|
|
onChange?.(ed.getHTML());
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
[extensions, editable],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (!editor) return;
|
|
|
|
|
editor.setEditable(editable);
|
|
|
|
|
}, [editable, editor]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<TooltipProvider delayDuration={200}>
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"group/editor overflow-hidden rounded-xl border border-border bg-card text-card-foreground shadow-sm",
|
|
|
|
|
className,
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<EditorToolbar editor={editor} />
|
|
|
|
|
<div ref={containerRef} className="relative bg-background dark:bg-background">
|
|
|
|
|
<BlockDragHandle editor={editor} containerRef={containerRef} />
|
|
|
|
|
<EditorContent editor={editor} className={editorContentStyles} />
|
|
|
|
|
{editor ? (
|
|
|
|
|
<BubbleMenu
|
|
|
|
|
editor={editor}
|
|
|
|
|
options={{
|
|
|
|
|
placement: "top",
|
|
|
|
|
}}
|
|
|
|
|
className="flex items-center gap-0.5 rounded-lg border border-border bg-popover p-1 shadow-lg"
|
|
|
|
|
>
|
|
|
|
|
<BubbleBtn
|
|
|
|
|
label="Bold"
|
|
|
|
|
shortcut="⌘B"
|
|
|
|
|
active={editor.isActive("bold")}
|
|
|
|
|
onClick={() => bubbleCh(editor).toggleBold().run()}
|
|
|
|
|
>
|
|
|
|
|
<Bold className="size-4" />
|
|
|
|
|
</BubbleBtn>
|
|
|
|
|
<BubbleBtn
|
|
|
|
|
label="Italic"
|
|
|
|
|
shortcut="⌘I"
|
|
|
|
|
active={editor.isActive("italic")}
|
|
|
|
|
onClick={() => bubbleCh(editor).toggleItalic().run()}
|
|
|
|
|
>
|
|
|
|
|
<Italic className="size-4" />
|
|
|
|
|
</BubbleBtn>
|
|
|
|
|
<BubbleBtn
|
|
|
|
|
label="Underline"
|
|
|
|
|
shortcut="⌘U"
|
|
|
|
|
active={editor.isActive("underline")}
|
|
|
|
|
onClick={() => bubbleCh(editor).toggleUnderline().run()}
|
|
|
|
|
>
|
|
|
|
|
<UnderlineIcon className="size-4" />
|
|
|
|
|
</BubbleBtn>
|
|
|
|
|
<BubbleBtn
|
|
|
|
|
label="Strikethrough"
|
|
|
|
|
shortcut="⌘⇧S"
|
|
|
|
|
active={editor.isActive("strike")}
|
|
|
|
|
onClick={() => bubbleCh(editor).toggleStrike().run()}
|
|
|
|
|
>
|
|
|
|
|
<Strikethrough className="size-4" />
|
|
|
|
|
</BubbleBtn>
|
|
|
|
|
<BubbleBtn
|
|
|
|
|
label="Code"
|
|
|
|
|
shortcut="⌘E"
|
|
|
|
|
active={editor.isActive("code")}
|
|
|
|
|
onClick={() => bubbleCh(editor).toggleCode().run()}
|
|
|
|
|
>
|
|
|
|
|
<Code className="size-4" />
|
|
|
|
|
</BubbleBtn>
|
|
|
|
|
<BubbleBtn
|
|
|
|
|
label="Link"
|
|
|
|
|
shortcut="⌘K"
|
|
|
|
|
active={editor.isActive("link")}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const prev = editor.getAttributes("link").href as
|
|
|
|
|
| string
|
|
|
|
|
| undefined;
|
|
|
|
|
const href = window.prompt("URL", prev ?? "https://");
|
|
|
|
|
if (href === null) return;
|
|
|
|
|
if (href === "") {
|
|
|
|
|
bubbleCh(editor).unsetLink().run();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
bubbleCh(editor).setLink({ href }).run();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Link2 className="size-4" />
|
|
|
|
|
</BubbleBtn>
|
|
|
|
|
</BubbleMenu>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</TooltipProvider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function BubbleBtn({
|
|
|
|
|
label,
|
|
|
|
|
shortcut,
|
|
|
|
|
active,
|
|
|
|
|
onClick,
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
label: string;
|
|
|
|
|
shortcut: string;
|
|
|
|
|
active: boolean;
|
|
|
|
|
onClick: () => void;
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className={cn(
|
|
|
|
|
"size-8",
|
|
|
|
|
active && "bg-primary/15 text-primary hover:bg-primary/20",
|
|
|
|
|
)}
|
|
|
|
|
onClick={onClick}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent side="top">
|
|
|
|
|
{label}
|
|
|
|
|
<span className="ml-2 text-xs opacity-70">{shortcut}</span>
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
);
|
|
|
|
|
}
|