feat: Complete markdown syntax refinement and variable protection
✅ New cleaner syntax implemented: - [card:type] instead of [card type='type'] - [button:style](url)Text[/button] instead of [button url='...' style='...'] - Standard markdown images:  ✅ Variable protection from markdown parsing: - Variables with underscores (e.g., {order_items_table}) now protected - HTML comment placeholders prevent italic/bold parsing - All variables render correctly in preview ✅ Button rendering fixes: - Buttons work in Visual mode inside cards - Buttons work in Preview mode - Button clicks prevented in visual editor - Proper styling for solid and outline buttons ✅ Backward compatibility: - Old syntax still supported - No breaking changes ✅ Bug fixes: - Fixed order_item_table → order_items_table naming - Fixed button regex to match across newlines - Added button/image parsing to parseMarkdownBasics - Prevented button clicks on .button and .button-outline classes 📚 Documentation: - NEW_MARKDOWN_SYNTAX.md - Complete user guide - MARKDOWN_SYNTAX_AND_VARIABLES.md - Technical analysis
This commit is contained in:
232
admin-spa/src/components/ui/markdown-toolbar.tsx
Normal file
232
admin-spa/src/components/ui/markdown-toolbar.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from './button';
|
||||
import { Bold, Italic, Heading1, Heading2, Link, List, ListOrdered, Quote, Code, Square, Plus, Image, MousePointer } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from './dialog';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from './select';
|
||||
|
||||
interface MarkdownToolbarProps {
|
||||
onInsert: (before: string, after?: string) => void;
|
||||
}
|
||||
|
||||
export function MarkdownToolbar({ onInsert }: MarkdownToolbarProps) {
|
||||
const [showCardDialog, setShowCardDialog] = useState(false);
|
||||
const [selectedCardType, setSelectedCardType] = useState('default');
|
||||
const [showButtonDialog, setShowButtonDialog] = useState(false);
|
||||
const [buttonStyle, setButtonStyle] = useState('solid');
|
||||
const [showImageDialog, setShowImageDialog] = useState(false);
|
||||
|
||||
const tools = [
|
||||
{ icon: Bold, label: 'Bold', before: '**', after: '**' },
|
||||
{ icon: Italic, label: 'Italic', before: '*', after: '*' },
|
||||
{ icon: Heading1, label: 'Heading 1', before: '# ', after: '' },
|
||||
{ icon: Heading2, label: 'Heading 2', before: '## ', after: '' },
|
||||
{ icon: Link, label: 'Link', before: '[', after: '](url)' },
|
||||
{ icon: List, label: 'Bullet List', before: '- ', after: '' },
|
||||
{ icon: ListOrdered, label: 'Numbered List', before: '1. ', after: '' },
|
||||
{ icon: Quote, label: 'Quote', before: '> ', after: '' },
|
||||
{ icon: Code, label: 'Code', before: '`', after: '`' },
|
||||
];
|
||||
|
||||
const cardTypes = [
|
||||
{ value: 'default', label: 'Default', description: 'Standard white card' },
|
||||
{ value: 'hero', label: 'Hero', description: 'Large header card with gradient' },
|
||||
{ value: 'success', label: 'Success', description: 'Green success message' },
|
||||
{ value: 'warning', label: 'Warning', description: 'Yellow warning message' },
|
||||
{ value: 'info', label: 'Info', description: 'Blue information card' },
|
||||
{ value: 'basic', label: 'Basic', description: 'Minimal styling' },
|
||||
];
|
||||
|
||||
const handleInsertCard = () => {
|
||||
const cardTemplate = selectedCardType === 'default'
|
||||
? '[card]\n\n## Your heading here\n\nYour content here...\n\n[/card]'
|
||||
: `[card:${selectedCardType}]\n\n## Your heading here\n\nYour content here...\n\n[/card]`;
|
||||
|
||||
onInsert(cardTemplate, '');
|
||||
setShowCardDialog(false);
|
||||
};
|
||||
|
||||
const handleInsertButton = () => {
|
||||
const buttonTemplate = `[button:${buttonStyle}](https://example.com)Click me[/button]`;
|
||||
onInsert(buttonTemplate, '');
|
||||
setShowButtonDialog(false);
|
||||
};
|
||||
|
||||
const handleInsertImage = () => {
|
||||
const imageTemplate = ``;
|
||||
onInsert(imageTemplate, '');
|
||||
setShowImageDialog(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1 p-2 border-b bg-muted/30">
|
||||
{/* Card Insert Button with Dialog */}
|
||||
<Dialog open={showCardDialog} onOpenChange={setShowCardDialog}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 gap-1"
|
||||
title="Insert Card"
|
||||
>
|
||||
<Square className="h-4 w-4" />
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Insert Card</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose a card type to insert into your template
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Card Type</label>
|
||||
<Select value={selectedCardType} onValueChange={setSelectedCardType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{cardTypes.map((type) => (
|
||||
<SelectItem key={type.value} value={type.value}>
|
||||
<div>
|
||||
<div className="font-medium">{type.label}</div>
|
||||
<div className="text-xs text-muted-foreground">{type.description}</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button onClick={handleInsertCard} className="w-full">
|
||||
Insert Card
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Button Insert Dialog */}
|
||||
<Dialog open={showButtonDialog} onOpenChange={setShowButtonDialog}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 gap-1"
|
||||
title="Insert Button"
|
||||
>
|
||||
<MousePointer className="h-4 w-4" />
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Insert Button</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose a button style to insert
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Button Style</label>
|
||||
<Select value={buttonStyle} onValueChange={setButtonStyle}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="solid">
|
||||
<div>
|
||||
<div className="font-medium">Solid</div>
|
||||
<div className="text-xs text-muted-foreground">Filled background</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="outline">
|
||||
<div>
|
||||
<div className="font-medium">Outline</div>
|
||||
<div className="text-xs text-muted-foreground">Border only</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button onClick={handleInsertButton} className="w-full">
|
||||
Insert Button
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Image Insert Dialog */}
|
||||
<Dialog open={showImageDialog} onOpenChange={setShowImageDialog}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 gap-1"
|
||||
title="Insert Image"
|
||||
>
|
||||
<Image className="h-4 w-4" />
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Insert Image</DialogTitle>
|
||||
<DialogDescription>
|
||||
Insert an image using standard Markdown syntax
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p>Syntax: <code className="px-1 py-0.5 bg-muted rounded"></code></p>
|
||||
<p className="mt-2">Example: <code className="px-1 py-0.5 bg-muted rounded"></code></p>
|
||||
</div>
|
||||
<Button onClick={handleInsertImage} className="w-full">
|
||||
Insert Image Template
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="w-px h-8 bg-border" />
|
||||
|
||||
{/* Other formatting tools */}
|
||||
{tools.map((tool) => (
|
||||
<Button
|
||||
key={tool.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onInsert(tool.before, tool.after)}
|
||||
className="h-8 w-8 p-0"
|
||||
title={tool.label}
|
||||
>
|
||||
<tool.icon className="h-4 w-4" />
|
||||
</Button>
|
||||
))}
|
||||
|
||||
<div className="flex-1" />
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span className="hidden sm:inline">Quick formatting:</span>
|
||||
<code className="px-1 py-0.5 bg-muted rounded">**bold**</code>
|
||||
<code className="px-1 py-0.5 bg-muted rounded">## heading</code>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user