some styling issues, but it works
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
"@tiptap/extension-table-row": "^3.14.0",
|
"@tiptap/extension-table-row": "^3.14.0",
|
||||||
"@tiptap/extension-task-item": "^3.14.0",
|
"@tiptap/extension-task-item": "^3.14.0",
|
||||||
"@tiptap/extension-task-list": "^3.14.0",
|
"@tiptap/extension-task-list": "^3.14.0",
|
||||||
|
"@tiptap/extension-text-align": "^3.14.0",
|
||||||
"@tiptap/extension-text-style": "^3.14.0",
|
"@tiptap/extension-text-style": "^3.14.0",
|
||||||
"@tiptap/pm": "^3.14.0",
|
"@tiptap/pm": "^3.14.0",
|
||||||
"@tiptap/starter-kit": "^3.14.0",
|
"@tiptap/starter-kit": "^3.14.0",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
"google-auth-library": "^10.5.0",
|
"google-auth-library": "^10.5.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"jose": "^6.1.3",
|
"jose": "^6.1.3",
|
||||||
|
"mermaid": "^11.12.2",
|
||||||
"motion": "^12.23.26",
|
"motion": "^12.23.26",
|
||||||
"solid-js": "^1.9.5",
|
"solid-js": "^1.9.5",
|
||||||
"solid-tiptap": "^0.8.0",
|
"solid-tiptap": "^0.8.0",
|
||||||
|
|||||||
254
src/app.css
254
src/app.css
@@ -969,3 +969,257 @@ details[open] div[data-type="details-content"] {
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mermaid diagram styles */
|
||||||
|
.mermaid-diagram,
|
||||||
|
pre[data-type="mermaid"] {
|
||||||
|
margin: 2rem 0;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: var(--color-surface0);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid var(--color-surface2);
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid-diagram code,
|
||||||
|
pre[data-type="mermaid"] code {
|
||||||
|
display: block;
|
||||||
|
white-space: pre;
|
||||||
|
font-family: "JetBrainsMono", monospace;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rendered mermaid SVG container */
|
||||||
|
.mermaid-rendered {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid-rendered svg {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mermaid theme adjustments */
|
||||||
|
.mermaid .node rect,
|
||||||
|
.mermaid .node circle,
|
||||||
|
.mermaid .node polygon,
|
||||||
|
.mermaid .node ellipse,
|
||||||
|
.mermaid .node path {
|
||||||
|
fill: var(--color-surface1) !important;
|
||||||
|
stroke: var(--color-blue) !important;
|
||||||
|
stroke-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .node .label,
|
||||||
|
.mermaid .nodeLabel {
|
||||||
|
color: var(--color-text) !important;
|
||||||
|
fill: var(--color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .edgePath .path,
|
||||||
|
.mermaid .flowchart-link {
|
||||||
|
stroke: var(--color-blue) !important;
|
||||||
|
stroke-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .edgeLabel,
|
||||||
|
.mermaid .edgeLabel rect {
|
||||||
|
background-color: var(--color-surface0) !important;
|
||||||
|
fill: var(--color-surface0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .edgeLabel span {
|
||||||
|
color: var(--color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .cluster rect {
|
||||||
|
fill: var(--color-surface0) !important;
|
||||||
|
stroke: var(--color-surface2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .cluster-label {
|
||||||
|
fill: var(--color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Class diagram styles */
|
||||||
|
.mermaid .classGroup rect,
|
||||||
|
.mermaid .classGroup line {
|
||||||
|
stroke: var(--color-blue) !important;
|
||||||
|
fill: var(--color-surface1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .classLabel {
|
||||||
|
fill: var(--color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* State diagram styles */
|
||||||
|
.mermaid .statediagram-state rect {
|
||||||
|
fill: var(--color-surface1) !important;
|
||||||
|
stroke: var(--color-blue) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .statediagram-state text {
|
||||||
|
fill: var(--color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sequence diagram styles */
|
||||||
|
.mermaid .actor {
|
||||||
|
fill: var(--color-surface1) !important;
|
||||||
|
stroke: var(--color-blue) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .actor text,
|
||||||
|
.mermaid .messageText {
|
||||||
|
fill: var(--color-text) !important;
|
||||||
|
stroke: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .activation0,
|
||||||
|
.mermaid .activation1,
|
||||||
|
.mermaid .activation2 {
|
||||||
|
fill: var(--color-surface2) !important;
|
||||||
|
stroke: var(--color-blue) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ER diagram styles */
|
||||||
|
.mermaid .er.entityBox {
|
||||||
|
fill: var(--color-surface1) !important;
|
||||||
|
stroke: var(--color-blue) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .er.entityLabel {
|
||||||
|
fill: var(--color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .er.relationshipLabel {
|
||||||
|
fill: var(--color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gantt chart styles */
|
||||||
|
.mermaid .grid .tick line {
|
||||||
|
stroke: var(--color-surface2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .grid .tick text {
|
||||||
|
fill: var(--color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .task {
|
||||||
|
fill: var(--color-blue) !important;
|
||||||
|
stroke: var(--color-blue) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .taskText {
|
||||||
|
fill: var(--color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .taskTextOutsideRight,
|
||||||
|
.mermaid .taskTextOutsideLeft {
|
||||||
|
fill: var(--color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pie chart styles */
|
||||||
|
.mermaid .pieCircle {
|
||||||
|
stroke: var(--color-surface2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .pieTitleText {
|
||||||
|
fill: var(--color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .slice {
|
||||||
|
stroke-width: 2px;
|
||||||
|
stroke: var(--color-surface0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .legend rect {
|
||||||
|
fill: var(--color-blue) !important;
|
||||||
|
stroke: var(--color-blue) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override all text elements in mermaid SVG for high contrast */
|
||||||
|
.mermaid-rendered svg text,
|
||||||
|
.mermaid svg text,
|
||||||
|
svg.mermaid text {
|
||||||
|
fill: #ffffff !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
stroke: #000000 !important;
|
||||||
|
stroke-width: 0.25px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure percentage labels in pie charts are visible */
|
||||||
|
.mermaid text.slice,
|
||||||
|
.mermaid .slice {
|
||||||
|
fill: #ffffff !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
stroke: #000000 !important;
|
||||||
|
stroke-width: 0.75px !important;
|
||||||
|
paint-order: stroke fill !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text alignment styles */
|
||||||
|
.ProseMirror [style*="text-align: left"],
|
||||||
|
.ProseMirror p[style*="text-align: left"],
|
||||||
|
.ProseMirror h1[style*="text-align: left"],
|
||||||
|
.ProseMirror h2[style*="text-align: left"],
|
||||||
|
.ProseMirror h3[style*="text-align: left"],
|
||||||
|
.ProseMirror h4[style*="text-align: left"],
|
||||||
|
.ProseMirror h5[style*="text-align: left"],
|
||||||
|
.ProseMirror h6[style*="text-align: left"] {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror [style*="text-align: center"],
|
||||||
|
.ProseMirror p[style*="text-align: center"],
|
||||||
|
.ProseMirror h1[style*="text-align: center"],
|
||||||
|
.ProseMirror h2[style*="text-align: center"],
|
||||||
|
.ProseMirror h3[style*="text-align: center"],
|
||||||
|
.ProseMirror h4[style*="text-align: center"],
|
||||||
|
.ProseMirror h5[style*="text-align: center"],
|
||||||
|
.ProseMirror h6[style*="text-align: center"] {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror [style*="text-align: right"],
|
||||||
|
.ProseMirror p[style*="text-align: right"],
|
||||||
|
.ProseMirror h1[style*="text-align: right"],
|
||||||
|
.ProseMirror h2[style*="text-align: right"],
|
||||||
|
.ProseMirror h3[style*="text-align: right"],
|
||||||
|
.ProseMirror h4[style*="text-align: right"],
|
||||||
|
.ProseMirror h5[style*="text-align: right"],
|
||||||
|
.ProseMirror h6[style*="text-align: right"] {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror [style*="text-align: justify"],
|
||||||
|
.ProseMirror p[style*="text-align: justify"],
|
||||||
|
.ProseMirror h1[style*="text-align: justify"],
|
||||||
|
.ProseMirror h2[style*="text-align: justify"],
|
||||||
|
.ProseMirror h3[style*="text-align: justify"],
|
||||||
|
.ProseMirror h4[style*="text-align: justify"],
|
||||||
|
.ProseMirror h5[style*="text-align: justify"],
|
||||||
|
.ProseMirror h6[style*="text-align: justify"] {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image alignment */
|
||||||
|
.ProseMirror img[style*="text-align: center"] {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror img[style*="text-align: right"] {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror img[style*="text-align: left"] {
|
||||||
|
display: block;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|||||||
51
src/components/blog/MermaidRenderer.tsx
Normal file
51
src/components/blog/MermaidRenderer.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { onMount } from "solid-js";
|
||||||
|
import mermaid from "mermaid";
|
||||||
|
|
||||||
|
// Initialize mermaid once
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
theme: "dark",
|
||||||
|
securityLevel: "loose",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
themeVariables: {
|
||||||
|
darkMode: true,
|
||||||
|
primaryColor: "#2c2f40",
|
||||||
|
primaryTextColor: "#b5c1f1",
|
||||||
|
primaryBorderColor: "#739df2",
|
||||||
|
lineColor: "#739df2",
|
||||||
|
secondaryColor: "#3e4255",
|
||||||
|
tertiaryColor: "#505469"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function MermaidRenderer() {
|
||||||
|
onMount(() => {
|
||||||
|
// Find all mermaid diagrams and render them
|
||||||
|
const mermaidPres = document.querySelectorAll('pre[data-type="mermaid"]');
|
||||||
|
|
||||||
|
mermaidPres.forEach(async (pre, index) => {
|
||||||
|
const code = pre.querySelector("code");
|
||||||
|
if (!code) return;
|
||||||
|
|
||||||
|
const content = code.textContent || "";
|
||||||
|
if (!content.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const id = `mermaid-${index}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const { svg } = await mermaid.render(id, content);
|
||||||
|
|
||||||
|
// Replace the pre/code with rendered SVG
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "mermaid-rendered";
|
||||||
|
wrapper.innerHTML = svg;
|
||||||
|
pre.replaceWith(wrapper);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to render mermaid diagram:", err);
|
||||||
|
// Keep the original code block if rendering fails
|
||||||
|
pre.classList.add("mermaid-error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createEffect } from "solid-js";
|
import { createEffect } from "solid-js";
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
import type { HLJSApi } from "highlight.js";
|
import type { HLJSApi } from "highlight.js";
|
||||||
|
import MermaidRenderer from "./MermaidRenderer";
|
||||||
|
|
||||||
export interface PostBodyClientProps {
|
export interface PostBodyClientProps {
|
||||||
body: string;
|
body: string;
|
||||||
@@ -121,6 +122,7 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
|||||||
class="text-text prose dark:prose-invert max-w-none"
|
class="text-text prose dark:prose-invert max-w-none"
|
||||||
innerHTML={props.body}
|
innerHTML={props.body}
|
||||||
/>
|
/>
|
||||||
|
<MermaidRenderer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import DetailsSummary from "@tiptap/extension-details-summary";
|
|||||||
import DetailsContent from "@tiptap/extension-details-content";
|
import DetailsContent from "@tiptap/extension-details-content";
|
||||||
import { Node } from "@tiptap/core";
|
import { Node } from "@tiptap/core";
|
||||||
import { createLowlight, common } from "lowlight";
|
import { createLowlight, common } from "lowlight";
|
||||||
|
import { Mermaid } from "./extensions/Mermaid";
|
||||||
|
import TextAlign from "@tiptap/extension-text-align";
|
||||||
import css from "highlight.js/lib/languages/css";
|
import css from "highlight.js/lib/languages/css";
|
||||||
import js from "highlight.js/lib/languages/javascript";
|
import js from "highlight.js/lib/languages/javascript";
|
||||||
import ts from "highlight.js/lib/languages/typescript";
|
import ts from "highlight.js/lib/languages/typescript";
|
||||||
@@ -109,6 +111,160 @@ const AVAILABLE_LANGUAGES = [
|
|||||||
{ value: "yaml", label: "YAML" }
|
{ value: "yaml", label: "YAML" }
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
// Mermaid diagram templates
|
||||||
|
const MERMAID_TEMPLATES = [
|
||||||
|
{
|
||||||
|
name: "Flowchart",
|
||||||
|
code: `graph TD
|
||||||
|
A[Start] --> B{Decision}
|
||||||
|
B -->|Yes| C[Option 1]
|
||||||
|
B -->|No| D[Option 2]
|
||||||
|
C --> E[End]
|
||||||
|
D --> E`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sequence Diagram",
|
||||||
|
code: `sequenceDiagram
|
||||||
|
participant A as Alice
|
||||||
|
participant B as Bob
|
||||||
|
A->>B: Hello Bob!
|
||||||
|
B->>A: Hello Alice!`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "State Diagram",
|
||||||
|
code: `stateDiagram-v2
|
||||||
|
[*] --> Idle
|
||||||
|
Idle --> Processing
|
||||||
|
Processing --> Done
|
||||||
|
Done --> [*]`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Class Diagram",
|
||||||
|
code: `classDiagram
|
||||||
|
class Animal {
|
||||||
|
+String name
|
||||||
|
+makeSound()
|
||||||
|
}
|
||||||
|
class Dog {
|
||||||
|
+bark()
|
||||||
|
}
|
||||||
|
Animal <|-- Dog`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Entity Relationship",
|
||||||
|
code: `erDiagram
|
||||||
|
CUSTOMER ||--o{ ORDER : places
|
||||||
|
ORDER ||--|{ LINE-ITEM : contains
|
||||||
|
CUSTOMER {
|
||||||
|
string name
|
||||||
|
string email
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Gantt Chart",
|
||||||
|
code: `gantt
|
||||||
|
title Project Timeline
|
||||||
|
dateFormat YYYY-MM-DD
|
||||||
|
section Phase 1
|
||||||
|
Task 1 :a1, 2024-01-01, 30d
|
||||||
|
Task 2 :after a1, 20d`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pie Chart",
|
||||||
|
code: `pie title Languages Used
|
||||||
|
"JavaScript" : 45
|
||||||
|
"TypeScript" : 30
|
||||||
|
"Python" : 15
|
||||||
|
"Go" : 10`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Keyboard shortcuts data
|
||||||
|
interface ShortcutCategory {
|
||||||
|
name: string;
|
||||||
|
shortcuts: Array<{
|
||||||
|
keys: string;
|
||||||
|
keysAlt?: string;
|
||||||
|
description: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEYBOARD_SHORTCUTS: ShortcutCategory[] = [
|
||||||
|
{
|
||||||
|
name: "Text Formatting",
|
||||||
|
shortcuts: [
|
||||||
|
{ keys: "⌘ B", keysAlt: "Ctrl B", description: "Bold" },
|
||||||
|
{ keys: "⌘ I", keysAlt: "Ctrl I", description: "Italic" },
|
||||||
|
{ keys: "⌘ ⇧ X", keysAlt: "Ctrl Shift X", description: "Strikethrough" },
|
||||||
|
{ keys: "⌘ E", keysAlt: "Ctrl E", description: "Inline Code" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Headings",
|
||||||
|
shortcuts: [
|
||||||
|
{ keys: "⌘ ⌥ 1", keysAlt: "Ctrl Alt 1", description: "Heading 1" },
|
||||||
|
{ keys: "⌘ ⌥ 2", keysAlt: "Ctrl Alt 2", description: "Heading 2" },
|
||||||
|
{ keys: "⌘ ⌥ 3", keysAlt: "Ctrl Alt 3", description: "Heading 3" },
|
||||||
|
{ keys: "⌘ ⌥ 0", keysAlt: "Ctrl Alt 0", description: "Paragraph" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Lists",
|
||||||
|
shortcuts: [
|
||||||
|
{ keys: "⌘ ⇧ 7", keysAlt: "Ctrl Shift 7", description: "Ordered List" },
|
||||||
|
{ keys: "⌘ ⇧ 8", keysAlt: "Ctrl Shift 8", description: "Bullet List" },
|
||||||
|
{ keys: "⌘ ⇧ 9", keysAlt: "Ctrl Shift 9", description: "Task List" },
|
||||||
|
{ keys: "Tab", keysAlt: "Tab", description: "Indent" },
|
||||||
|
{ keys: "⇧ Tab", keysAlt: "Shift Tab", description: "Outdent" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Text Alignment",
|
||||||
|
shortcuts: [
|
||||||
|
{ keys: "⌘ ⇧ L", keysAlt: "Ctrl Shift L", description: "Align Left" },
|
||||||
|
{ keys: "⌘ ⇧ E", keysAlt: "Ctrl Shift E", description: "Align Center" },
|
||||||
|
{ keys: "⌘ ⇧ R", keysAlt: "Ctrl Shift R", description: "Align Right" },
|
||||||
|
{ keys: "⌘ ⇧ J", keysAlt: "Ctrl Shift J", description: "Justify" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Insert",
|
||||||
|
shortcuts: [
|
||||||
|
{ keys: "⌘ K", keysAlt: "Ctrl K", description: "Insert/Edit Link" },
|
||||||
|
{ keys: "⌘ ⇧ C", keysAlt: "Ctrl Shift C", description: "Code Block" },
|
||||||
|
{ keys: "⌘ Enter", keysAlt: "Ctrl Enter", description: "Hard Break" },
|
||||||
|
{ keys: "⌘ ⇧ -", keysAlt: "Ctrl Shift -", description: "Horizontal Rule" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Editing",
|
||||||
|
shortcuts: [
|
||||||
|
{ keys: "⌘ Z", keysAlt: "Ctrl Z", description: "Undo" },
|
||||||
|
{ keys: "⌘ ⇧ Z", keysAlt: "Ctrl Shift Z", description: "Redo" },
|
||||||
|
{ keys: "⌘ Y", keysAlt: "Ctrl Y", description: "Redo (Alt)" },
|
||||||
|
{ keys: "⌘ A", keysAlt: "Ctrl A", description: "Select All" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Other",
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
keys: "⌘ ⇧ \\",
|
||||||
|
keysAlt: "Ctrl Shift \\",
|
||||||
|
description: "Clear Formatting"
|
||||||
|
},
|
||||||
|
{ keys: "ESC", keysAlt: "ESC", description: "Exit Fullscreen" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const isMac = () => {
|
||||||
|
return (
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
/Mac|iPhone|iPad|iPod/.test(window.navigator.platform)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// IFrame extension
|
// IFrame extension
|
||||||
interface IframeOptions {
|
interface IframeOptions {
|
||||||
allowFullscreen: boolean;
|
allowFullscreen: boolean;
|
||||||
@@ -212,6 +368,14 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
left: 0
|
left: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [showMermaidTemplates, setShowMermaidTemplates] = createSignal(false);
|
||||||
|
const [mermaidMenuPosition, setMermaidMenuPosition] = createSignal({
|
||||||
|
top: 0,
|
||||||
|
left: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const [showKeyboardHelp, setShowKeyboardHelp] = createSignal(false);
|
||||||
|
|
||||||
const [isFullscreen, setIsFullscreen] = createSignal(false);
|
const [isFullscreen, setIsFullscreen] = createSignal(false);
|
||||||
|
|
||||||
const editor = createTiptapEditor(() => ({
|
const editor = createTiptapEditor(() => ({
|
||||||
@@ -262,6 +426,12 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "tiptap-table-cell"
|
class: "tiptap-table-cell"
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
Mermaid,
|
||||||
|
TextAlign.configure({
|
||||||
|
types: ["heading", "paragraph"],
|
||||||
|
alignments: ["left", "center", "right", "justify"],
|
||||||
|
defaultAlignment: "left"
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
content: props.preSet || `<p><em><b>Hello!</b> World</em></p>`,
|
content: props.preSet || `<p><em><b>Hello!</b> World</em></p>`,
|
||||||
@@ -638,6 +808,44 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Close mermaid menu on outside click
|
||||||
|
createEffect(() => {
|
||||||
|
if (showMermaidTemplates()) {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (
|
||||||
|
!target.closest(".mermaid-menu") &&
|
||||||
|
!target.closest("[data-mermaid-trigger]")
|
||||||
|
) {
|
||||||
|
setShowMermaidTemplates(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener("click", handleClickOutside);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return () => document.removeEventListener("click", handleClickOutside);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const showMermaidSelector = (e: MouseEvent) => {
|
||||||
|
const buttonRect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||||
|
setMermaidMenuPosition({
|
||||||
|
top: buttonRect.bottom + 5,
|
||||||
|
left: buttonRect.left
|
||||||
|
});
|
||||||
|
setShowMermaidTemplates(!showMermaidTemplates());
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertMermaidDiagram = (template: (typeof MERMAID_TEMPLATES)[0]) => {
|
||||||
|
const instance = editor();
|
||||||
|
if (!instance) return;
|
||||||
|
|
||||||
|
instance.chain().focus().setMermaid(template.code).run();
|
||||||
|
setShowMermaidTemplates(false);
|
||||||
|
};
|
||||||
|
|
||||||
// Toggle fullscreen mode
|
// Toggle fullscreen mode
|
||||||
const toggleFullscreen = () => {
|
const toggleFullscreen = () => {
|
||||||
setIsFullscreen(!isFullscreen());
|
setIsFullscreen(!isFullscreen());
|
||||||
@@ -1005,6 +1213,34 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
{/* Mermaid Template Selector */}
|
||||||
|
<Show when={showMermaidTemplates()}>
|
||||||
|
<div
|
||||||
|
class="mermaid-menu bg-mantle text-text border-surface2 fixed z-50 max-h-96 w-56 overflow-y-auto rounded border shadow-lg"
|
||||||
|
style={{
|
||||||
|
top: `${mermaidMenuPosition().top}px`,
|
||||||
|
left: `${mermaidMenuPosition().left}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="border-surface2 border-b p-2">
|
||||||
|
<div class="text-subtext0 text-xs font-semibold">
|
||||||
|
Select Diagram Type
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<For each={MERMAID_TEMPLATES}>
|
||||||
|
{(template) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => insertMermaidDiagram(template)}
|
||||||
|
class="hover:bg-surface1 w-full px-3 py-2 text-left text-sm"
|
||||||
|
>
|
||||||
|
{template.name}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div class="border-surface2 mb-2 flex flex-wrap gap-1 border-b pb-2">
|
<div class="border-surface2 mb-2 flex flex-wrap gap-1 border-b pb-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1151,6 +1387,65 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
▼ Details
|
▼ Details
|
||||||
</button>
|
</button>
|
||||||
<div class="border-surface2 mx-1 border-l"></div>
|
<div class="border-surface2 mx-1 border-l"></div>
|
||||||
|
|
||||||
|
{/* Text Alignment */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
instance().chain().focus().setTextAlign("left").run()
|
||||||
|
}
|
||||||
|
class={`${
|
||||||
|
instance().isActive({ textAlign: "left" })
|
||||||
|
? "bg-surface2"
|
||||||
|
: "hover:bg-surface1"
|
||||||
|
} rounded px-2 py-1 text-xs`}
|
||||||
|
title="Align Left"
|
||||||
|
>
|
||||||
|
⬅
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
instance().chain().focus().setTextAlign("center").run()
|
||||||
|
}
|
||||||
|
class={`${
|
||||||
|
instance().isActive({ textAlign: "center" })
|
||||||
|
? "bg-surface2"
|
||||||
|
: "hover:bg-surface1"
|
||||||
|
} rounded px-2 py-1 text-xs`}
|
||||||
|
title="Align Center"
|
||||||
|
>
|
||||||
|
↔
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
instance().chain().focus().setTextAlign("right").run()
|
||||||
|
}
|
||||||
|
class={`${
|
||||||
|
instance().isActive({ textAlign: "right" })
|
||||||
|
? "bg-surface2"
|
||||||
|
: "hover:bg-surface1"
|
||||||
|
} rounded px-2 py-1 text-xs`}
|
||||||
|
title="Align Right"
|
||||||
|
>
|
||||||
|
➡
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
instance().chain().focus().setTextAlign("justify").run()
|
||||||
|
}
|
||||||
|
class={`${
|
||||||
|
instance().isActive({ textAlign: "justify" })
|
||||||
|
? "bg-surface2"
|
||||||
|
: "hover:bg-surface1"
|
||||||
|
} rounded px-2 py-1 text-xs`}
|
||||||
|
title="Justify"
|
||||||
|
>
|
||||||
|
⬌
|
||||||
|
</button>
|
||||||
|
<div class="border-surface2 mx-1 border-l"></div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={showLanguagePicker}
|
onClick={showLanguagePicker}
|
||||||
@@ -1205,6 +1500,15 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
>
|
>
|
||||||
⊞ Table
|
⊞ Table
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={showMermaidSelector}
|
||||||
|
data-mermaid-trigger
|
||||||
|
class="hover:bg-surface1 rounded px-2 py-1 text-xs"
|
||||||
|
title="Insert Diagram"
|
||||||
|
>
|
||||||
|
📊 Diagram
|
||||||
|
</button>
|
||||||
<div class="border-surface2 mx-1 border-l"></div>
|
<div class="border-surface2 mx-1 border-l"></div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1227,6 +1531,14 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
>
|
>
|
||||||
{isFullscreen() ? "⇲ Exit" : "⇱ Fullscreen"}
|
{isFullscreen() ? "⇲ Exit" : "⇱ Fullscreen"}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowKeyboardHelp(!showKeyboardHelp())}
|
||||||
|
class="hover:bg-surface1 rounded px-2 py-1 text-xs"
|
||||||
|
title="Keyboard Shortcuts"
|
||||||
|
>
|
||||||
|
⌨ Help
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Table controls - shown when cursor is in a table */}
|
{/* Table controls - shown when cursor is in a table */}
|
||||||
<Show when={instance().isActive("table")}>
|
<Show when={instance().isActive("table")}>
|
||||||
@@ -1351,6 +1663,66 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
"h-[calc(100dvh-8rem)]": isFullscreen()
|
"h-[calc(100dvh-8rem)]": isFullscreen()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Keyboard Help Modal */}
|
||||||
|
<Show when={showKeyboardHelp()}>
|
||||||
|
<div
|
||||||
|
class="bg-opacity-50 fixed inset-0 z-[100] flex items-center justify-center bg-black"
|
||||||
|
onClick={() => setShowKeyboardHelp(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-base border-surface2 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg border p-6 shadow-2xl"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div class="mb-6 flex items-center justify-between">
|
||||||
|
<h2 class="text-text text-2xl font-bold">Keyboard Shortcuts</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowKeyboardHelp(false)}
|
||||||
|
class="hover:bg-surface1 text-subtext0 rounded p-2 text-xl"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Shortcuts Grid */}
|
||||||
|
<div class="space-y-6">
|
||||||
|
<For each={KEYBOARD_SHORTCUTS}>
|
||||||
|
{(category) => (
|
||||||
|
<div>
|
||||||
|
<h3 class="text-blue mb-3 text-lg font-semibold">
|
||||||
|
{category.name}
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<For each={category.shortcuts}>
|
||||||
|
{(shortcut) => (
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-text">
|
||||||
|
{shortcut.description}
|
||||||
|
</span>
|
||||||
|
<kbd class="bg-surface0 border-surface2 text-subtext0 rounded border px-3 py-1 font-mono text-sm">
|
||||||
|
{isMac()
|
||||||
|
? shortcut.keys
|
||||||
|
: shortcut.keysAlt || shortcut.keys}
|
||||||
|
</kbd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div class="text-subtext0 border-surface2 mt-6 border-t pt-4 text-center text-sm">
|
||||||
|
Press <span class="text-text font-semibold">⌨ Help</span> button
|
||||||
|
to toggle this help
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
63
src/components/blog/extensions/Mermaid.ts
Normal file
63
src/components/blog/extensions/Mermaid.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Node, mergeAttributes } from "@tiptap/core";
|
||||||
|
|
||||||
|
export const Mermaid = Node.create({
|
||||||
|
name: "mermaid",
|
||||||
|
group: "block",
|
||||||
|
content: "text*",
|
||||||
|
marks: "",
|
||||||
|
code: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
language: {
|
||||||
|
default: "mermaid",
|
||||||
|
parseHTML: (element) => element.getAttribute("data-language"),
|
||||||
|
renderHTML: (attributes) => {
|
||||||
|
return {
|
||||||
|
"data-language": attributes.language
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'pre[data-type="mermaid"]'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
"pre",
|
||||||
|
mergeAttributes(HTMLAttributes, {
|
||||||
|
"data-type": "mermaid",
|
||||||
|
class: "mermaid-diagram"
|
||||||
|
}),
|
||||||
|
["code", 0]
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setMermaid:
|
||||||
|
(content: string) =>
|
||||||
|
({ commands }) => {
|
||||||
|
return commands.insertContent({
|
||||||
|
type: this.name,
|
||||||
|
content: [{ type: "text", text: content }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
declare module "@tiptap/core" {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
mermaid: {
|
||||||
|
setMermaid: (content: string) => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user