16 changed files with 1993 additions and 337 deletions
@ -0,0 +1,753 @@
@@ -0,0 +1,753 @@
|
||||
import type { AdvancedEventLabSlice } from '@/lib/advanced-event-lab-slice' |
||||
import { Button } from '@/components/ui/button' |
||||
import { |
||||
DropdownMenu, |
||||
DropdownMenuContent, |
||||
DropdownMenuGroup, |
||||
DropdownMenuItem, |
||||
DropdownMenuLabel, |
||||
DropdownMenuSeparator, |
||||
DropdownMenuTrigger |
||||
} from '@/components/ui/dropdown-menu' |
||||
import { Input } from '@/components/ui/input' |
||||
import { ScrollArea } from '@/components/ui/scroll-area' |
||||
import type { EditorView } from '@codemirror/view' |
||||
import { |
||||
Braces, |
||||
ChevronDown, |
||||
Code2, |
||||
Heading, |
||||
Image as ImageIcon, |
||||
Link2, |
||||
List, |
||||
ListOrdered, |
||||
Minus, |
||||
Pilcrow, |
||||
Quote, |
||||
Sigma, |
||||
Table2, |
||||
Type, |
||||
ListTodo |
||||
} from 'lucide-react' |
||||
import type { MutableRefObject } from 'react' |
||||
import { useMemo, useState } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
import { labInsertRaw, labInsertSnippet, labWrapOrSnippet } from './markup-insert' |
||||
|
||||
/** Languages for fenced / source blocks (labels are English; widely recognized by highlighters). */ |
||||
const CODE_LANGUAGES = [ |
||||
'text', |
||||
'markdown', |
||||
'asciidoc', |
||||
'javascript', |
||||
'typescript', |
||||
'tsx', |
||||
'jsx', |
||||
'json', |
||||
'yaml', |
||||
'toml', |
||||
'html', |
||||
'xml', |
||||
'css', |
||||
'scss', |
||||
'sass', |
||||
'sql', |
||||
'python', |
||||
'rust', |
||||
'go', |
||||
'c', |
||||
'cpp', |
||||
'csharp', |
||||
'java', |
||||
'kotlin', |
||||
'swift', |
||||
'ruby', |
||||
'php', |
||||
'bash', |
||||
'shell', |
||||
'powershell', |
||||
'dockerfile', |
||||
'graphql', |
||||
'http', |
||||
'diff', |
||||
'ini', |
||||
'makefile', |
||||
'lua', |
||||
'r', |
||||
'matlab', |
||||
'latex', |
||||
'haskell', |
||||
'elixir', |
||||
'scala', |
||||
'zig', |
||||
'nim', |
||||
'wasm', |
||||
'protobuf' |
||||
] as const |
||||
|
||||
export type AdvancedEventLabMarkupToolbarProps = { |
||||
markupMode: 'markdown' | 'asciidoc' |
||||
viewRef: MutableRefObject<EditorView | null> |
||||
sliceRef: MutableRefObject<AdvancedEventLabSlice | null> |
||||
} |
||||
|
||||
export function AdvancedEventLabMarkupToolbar({ |
||||
markupMode, |
||||
viewRef, |
||||
sliceRef |
||||
}: AdvancedEventLabMarkupToolbarProps) { |
||||
const { t } = useTranslation() |
||||
const [codeFilter, setCodeFilter] = useState('') |
||||
const [langFilter, setLangFilter] = useState('') |
||||
|
||||
const filteredLangs = useMemo(() => { |
||||
const q = codeFilter.trim().toLowerCase() |
||||
if (!q) return [...CODE_LANGUAGES] |
||||
return CODE_LANGUAGES.filter((id) => id.toLowerCase().includes(q)) |
||||
}, [codeFilter]) |
||||
|
||||
const run = (fn: (v: EditorView) => void) => { |
||||
const v = viewRef.current |
||||
if (!v) return |
||||
fn(v) |
||||
} |
||||
|
||||
const mdFence = (lang: string) => { |
||||
run((v) => |
||||
labInsertSnippet(v, sliceRef, `\`\`\`${lang}\n`, 'your code here', `\n\`\`\`\n`) |
||||
) |
||||
} |
||||
|
||||
const adocSource = (lang: string) => { |
||||
run((v) => |
||||
labInsertSnippet( |
||||
v, |
||||
sliceRef, |
||||
`[source,${lang}]\n----\n`, |
||||
'your code here', |
||||
`\n----\n` |
||||
) |
||||
) |
||||
} |
||||
|
||||
if (markupMode === 'markdown') { |
||||
return ( |
||||
<div className="flex flex-wrap items-center gap-1.5 min-w-0 overflow-x-auto border-b bg-muted/30 px-2 py-2"> |
||||
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0 mr-1"> |
||||
{t('Advanced lab tb markup tools')} |
||||
</span> |
||||
|
||||
<DropdownMenu> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> |
||||
<Heading className="h-3.5 w-3.5" /> |
||||
{t('Advanced lab tb headings')} |
||||
<ChevronDown className="h-3 w-3 opacity-60" /> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent align="start" className="z-[280] max-h-80 overflow-y-auto w-56"> |
||||
<DropdownMenuLabel>{t('Advanced lab tb headings hint')}</DropdownMenuLabel> |
||||
{( |
||||
[ |
||||
'Advanced lab tb h1', |
||||
'Advanced lab tb h2', |
||||
'Advanced lab tb h3', |
||||
'Advanced lab tb h4', |
||||
'Advanced lab tb h5', |
||||
'Advanced lab tb h6' |
||||
] as const |
||||
).map((labelKey, i) => { |
||||
const n = i + 1 |
||||
return ( |
||||
<DropdownMenuItem |
||||
key={labelKey} |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertRaw( |
||||
v, |
||||
sliceRef, |
||||
`${n === 1 ? '' : '\n'}${'#'.repeat(n)} ${t('Advanced lab tb heading placeholder')}\n` |
||||
) |
||||
) |
||||
} |
||||
> |
||||
{t(labelKey)} |
||||
</DropdownMenuItem> |
||||
) |
||||
})} |
||||
<DropdownMenuSeparator /> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n---\n'))}> |
||||
{t('Advanced lab tb horizontalRule')} |
||||
</DropdownMenuItem> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
|
||||
<DropdownMenu> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> |
||||
<Type className="h-3.5 w-3.5" /> |
||||
{t('Advanced lab tb inline')} |
||||
<ChevronDown className="h-3 w-3 opacity-60" /> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent align="start" className="z-[280] w-56"> |
||||
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '**', 'bold'))}> |
||||
{t('Advanced lab tb bold')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '*', 'italic'))}> |
||||
{t('Advanced lab tb italic')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '~~', 'strikethrough'))}> |
||||
{t('Advanced lab tb strike')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '`', 'code'))}> |
||||
{t('Advanced lab tb inlineCode')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuSeparator /> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertSnippet(v, sliceRef, '[', 'link text', '](https://example.com)') |
||||
) |
||||
} |
||||
> |
||||
<Link2 className="h-3.5 w-3.5 mr-2 inline" /> |
||||
{t('Advanced lab tb link')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertSnippet(v, sliceRef, '') |
||||
) |
||||
} |
||||
> |
||||
<ImageIcon className="h-3.5 w-3.5 mr-2 inline" /> |
||||
{t('Advanced lab tb image')} |
||||
</DropdownMenuItem> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
|
||||
<DropdownMenu> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> |
||||
<List className="h-3.5 w-3.5" /> |
||||
{t('Advanced lab tb lists')} |
||||
<ChevronDown className="h-3 w-3 opacity-60" /> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent align="start" className="z-[280] w-56"> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n- item one\n- item two\n'))}> |
||||
<List className="h-3.5 w-3.5 mr-2 inline" /> |
||||
{t('Advanced lab tb bulletList')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n1. first\n2. second\n'))}> |
||||
<ListOrdered className="h-3.5 w-3.5 mr-2 inline" /> |
||||
{t('Advanced lab tb orderedList')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertRaw( |
||||
v, |
||||
sliceRef, |
||||
'\n- [x] Checked box\n- [ ] Unchecked box\n' |
||||
) |
||||
) |
||||
} |
||||
> |
||||
<ListTodo className="h-3.5 w-3.5 mr-2 inline" /> |
||||
{t('Advanced lab tb taskItem')} |
||||
</DropdownMenuItem> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
|
||||
<DropdownMenu> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> |
||||
<Quote className="h-3.5 w-3.5" /> |
||||
{t('Advanced lab tb blocks')} |
||||
<ChevronDown className="h-3 w-3 opacity-60" /> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent align="start" className="z-[280] w-64"> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n> quoted line\n'))}> |
||||
<Quote className="h-3.5 w-3.5 mr-2 inline" /> |
||||
{t('Advanced lab tb blockquote')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertRaw( |
||||
v, |
||||
sliceRef, |
||||
'\n| Col A | Col B |\n| --- | --- |\n| cell | cell |\n' |
||||
) |
||||
) |
||||
} |
||||
> |
||||
<Table2 className="h-3.5 w-3.5 mr-2 inline" /> |
||||
{t('Advanced lab tb pipeTable')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuSeparator /> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n[^1]\n'))}> |
||||
{t('Advanced lab tb footnoteRef')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertRaw(v, sliceRef, '\n[^1]: Footnote text goes here.\n') |
||||
) |
||||
} |
||||
> |
||||
{t('Advanced lab tb footnoteDef')} |
||||
</DropdownMenuItem> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
|
||||
<DropdownMenu onOpenChange={(o) => !o && setCodeFilter('')}> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> |
||||
<Code2 className="h-3.5 w-3.5" /> |
||||
{t('Advanced lab tb codeBlock')} |
||||
<ChevronDown className="h-3 w-3 opacity-60" /> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent align="start" className="z-[280] w-[min(22rem,92vw)] p-2"> |
||||
<p className="text-xs text-muted-foreground mb-2 px-1">{t('Advanced lab tb codeBlockHint')}</p> |
||||
<Input |
||||
value={codeFilter} |
||||
onChange={(e) => setCodeFilter(e.target.value)} |
||||
placeholder={t('Advanced lab tb filterLanguages')} |
||||
className="h-8 text-xs mb-2" |
||||
/> |
||||
<ScrollArea className="h-[min(50vh,18rem)] pr-2"> |
||||
<div className="flex flex-col gap-0.5"> |
||||
{filteredLangs.map((lang) => ( |
||||
<Button |
||||
key={lang} |
||||
type="button" |
||||
variant="ghost" |
||||
size="sm" |
||||
className="h-8 justify-start font-mono text-xs" |
||||
onClick={() => { |
||||
mdFence(lang) |
||||
setCodeFilter('') |
||||
}} |
||||
> |
||||
{lang} |
||||
</Button> |
||||
))} |
||||
</div> |
||||
</ScrollArea> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
|
||||
<DropdownMenu> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> |
||||
<Sigma className="h-3.5 w-3.5" /> |
||||
{t('Advanced lab tb math')} |
||||
<ChevronDown className="h-3 w-3 opacity-60" /> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent align="start" className="z-[280] w-[min(24rem,92vw)] max-h-[min(70vh,28rem)] overflow-y-auto"> |
||||
<DropdownMenuLabel>{t('Advanced lab tb mathIntro')}</DropdownMenuLabel> |
||||
<DropdownMenuSeparator /> |
||||
<DropdownMenuGroup> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertSnippet(v, sliceRef, '$', 'x^2 + y^2 = r^2', '$') |
||||
) |
||||
} |
||||
> |
||||
{t('Advanced lab tb mathInline')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertSnippet(v, sliceRef, '\n$$\n', 'E = mc^2', '\n$$\n') |
||||
) |
||||
} |
||||
> |
||||
{t('Advanced lab tb mathDisplay')} |
||||
</DropdownMenuItem> |
||||
</DropdownMenuGroup> |
||||
<DropdownMenuSeparator /> |
||||
<DropdownMenuLabel>{t('Advanced lab tb mathCommon')}</DropdownMenuLabel> |
||||
<DropdownMenuItem |
||||
onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\frac{numerator}{denominator}$'))} |
||||
> |
||||
{t('Advanced lab tb katexFrac')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\sqrt{x}$'))}> |
||||
{t('Advanced lab tb katexSqrt')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\sum_{i=1}^{n} i$'))} |
||||
> |
||||
{t('Advanced lab tb katexSum')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\int_{a}^{b} f(x)\\,dx$'))} |
||||
> |
||||
{t('Advanced lab tb katexInt')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertRaw( |
||||
v, |
||||
sliceRef, |
||||
'$$\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}$$' |
||||
) |
||||
) |
||||
} |
||||
> |
||||
{t('Advanced lab tb katexMatrix')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertRaw( |
||||
v, |
||||
sliceRef, |
||||
'$$\\begin{cases} x & x > 0 \\\\ -x & x \\le 0 \\end{cases}$$' |
||||
) |
||||
) |
||||
} |
||||
> |
||||
{t('Advanced lab tb katexCases')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuSeparator /> |
||||
<DropdownMenuLabel>{t('Advanced lab tb mathGreek')}</DropdownMenuLabel> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\alpha$'))}> |
||||
<code className="text-xs mr-2">{'\\alpha'}</code> {t('Advanced lab tb greekAlpha')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\beta$'))}> |
||||
<code className="text-xs mr-2">{'\\beta'}</code> {t('Advanced lab tb greekBeta')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\gamma$'))}> |
||||
<code className="text-xs mr-2">{'\\gamma'}</code> {t('Advanced lab tb greekGamma')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\delta$'))}> |
||||
<code className="text-xs mr-2">{'\\delta'}</code> {t('Advanced lab tb greekDelta')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\pi$'))}> |
||||
<code className="text-xs mr-2">{'\\pi'}</code> {t('Advanced lab tb greekPi')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\theta$'))}> |
||||
<code className="text-xs mr-2">{'\\theta'}</code> {t('Advanced lab tb greekTheta')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\lambda$'))}> |
||||
<code className="text-xs mr-2">{'\\lambda'}</code> {t('Advanced lab tb greekLambda')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\sigma$'))}> |
||||
<code className="text-xs mr-2">{'\\sigma'}</code> {t('Advanced lab tb greekSigma')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\omega$'))}> |
||||
<code className="text-xs mr-2">{'\\omega'}</code> {t('Advanced lab tb greekOmega')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\infty$'))}> |
||||
<code className="text-xs mr-2">{'\\infty'}</code> {t('Advanced lab tb greekInfty')} |
||||
</DropdownMenuItem> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
|
||||
<Button |
||||
type="button" |
||||
variant="ghost" |
||||
size="sm" |
||||
className="h-8 text-xs shrink-0" |
||||
title={t('Advanced lab tb hrTitle')} |
||||
onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n---\n'))} |
||||
> |
||||
<Minus className="h-3.5 w-3.5" /> |
||||
</Button> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
/* AsciiDoc */ |
||||
return ( |
||||
<div className="flex flex-wrap items-center gap-1.5 min-w-0 overflow-x-auto border-b bg-muted/30 px-2 py-2"> |
||||
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0 mr-1"> |
||||
{t('Advanced lab tb markup tools')} |
||||
</span> |
||||
|
||||
<DropdownMenu> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> |
||||
<Heading className="h-3.5 w-3.5" /> |
||||
{t('Advanced lab tb adocTitles')} |
||||
<ChevronDown className="h-3 w-3 opacity-60" /> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent align="start" className="z-[280] w-60"> |
||||
<DropdownMenuLabel>{t('Advanced lab tb adocTitlesHint')}</DropdownMenuLabel> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, `\n= ${t('Advanced lab tb documentTitle')}\n`))}> |
||||
{t('Advanced lab tb adocLevel0')} |
||||
</DropdownMenuItem> |
||||
{( |
||||
[ |
||||
'Advanced lab tb adocSection1', |
||||
'Advanced lab tb adocSection2', |
||||
'Advanced lab tb adocSection3', |
||||
'Advanced lab tb adocSection4', |
||||
'Advanced lab tb adocSection5' |
||||
] as const |
||||
).map((labelKey, i) => { |
||||
const n = i + 1 |
||||
return ( |
||||
<DropdownMenuItem |
||||
key={labelKey} |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertRaw( |
||||
v, |
||||
sliceRef, |
||||
`\n${'='.repeat(n + 1)} ${t('Advanced lab tb sectionTitle')}\n` |
||||
) |
||||
) |
||||
} |
||||
> |
||||
{t(labelKey)} |
||||
</DropdownMenuItem> |
||||
) |
||||
})} |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
|
||||
<DropdownMenu> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> |
||||
<Type className="h-3.5 w-3.5" /> |
||||
{t('Advanced lab tb inline')} |
||||
<ChevronDown className="h-3 w-3 opacity-60" /> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent align="start" className="z-[280] w-56"> |
||||
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '*', 'bold'))}> |
||||
{t('Advanced lab tb adocBold')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '_', 'italic'))}> |
||||
{t('Advanced lab tb adocItalic')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '`', 'mono'))}> |
||||
{t('Advanced lab tb adocMono')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuSeparator /> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertSnippet(v, sliceRef, 'link:https://example.com[', 'link text', ']') |
||||
) |
||||
} |
||||
> |
||||
<Link2 className="h-3.5 w-3.5 mr-2 inline" /> |
||||
{t('Advanced lab tb adocLink')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertRaw( |
||||
v, |
||||
sliceRef, |
||||
'\nimage::https://example.com/image.png[Alt text, width=640]\n' |
||||
) |
||||
) |
||||
} |
||||
> |
||||
<ImageIcon className="h-3.5 w-3.5 mr-2 inline" /> |
||||
{t('Advanced lab tb adocImage')} |
||||
</DropdownMenuItem> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
|
||||
<DropdownMenu> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> |
||||
<List className="h-3.5 w-3.5" /> |
||||
{t('Advanced lab tb lists')} |
||||
<ChevronDown className="h-3 w-3 opacity-60" /> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent align="start" className="z-[280] w-56"> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n* item one\n* item two\n'))}> |
||||
{t('Advanced lab tb adocUnordered')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n. first step\n. second step\n'))}> |
||||
{t('Advanced lab tb adocOrdered')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\nterm:: definition line\n'))}> |
||||
{t('Advanced lab tb adocLabeled')} |
||||
</DropdownMenuItem> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
|
||||
<DropdownMenu> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> |
||||
<Pilcrow className="h-3.5 w-3.5" /> |
||||
{t('Advanced lab tb adocBlocks')} |
||||
<ChevronDown className="h-3 w-3 opacity-60" /> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent align="start" className="z-[280] w-64"> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertSnippet(v, sliceRef, '\n____\n', 'Quoted paragraph', '\n____\n') |
||||
) |
||||
} |
||||
> |
||||
{t('Advanced lab tb adocQuote')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertSnippet(v, sliceRef, '\n....\n', 'Literal monospace block', '\n....\n') |
||||
) |
||||
} |
||||
> |
||||
{t('Advanced lab tb adocLiteral')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuSeparator /> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertSnippet( |
||||
v, |
||||
sliceRef, |
||||
'\n[NOTE]\n====\n', |
||||
'Note body', |
||||
'\n====\n' |
||||
) |
||||
) |
||||
} |
||||
> |
||||
{t('Advanced lab tb adocNote')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertSnippet( |
||||
v, |
||||
sliceRef, |
||||
'\n[TIP]\n====\n', |
||||
'Tip body', |
||||
'\n====\n' |
||||
) |
||||
) |
||||
} |
||||
> |
||||
{t('Advanced lab tb adocTip')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => |
||||
run((v) => |
||||
labInsertSnippet( |
||||
v, |
||||
sliceRef, |
||||
'\n[WARNING]\n====\n', |
||||
'Warning body', |
||||
'\n====\n' |
||||
) |
||||
) |
||||
} |
||||
> |
||||
{t('Advanced lab tb adocWarning')} |
||||
</DropdownMenuItem> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
|
||||
<DropdownMenu onOpenChange={(o) => !o && setLangFilter('')}> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> |
||||
<Braces className="h-3.5 w-3.5" /> |
||||
{t('Advanced lab tb adocSource')} |
||||
<ChevronDown className="h-3 w-3 opacity-60" /> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent align="start" className="z-[280] w-[min(22rem,92vw)] p-2"> |
||||
<p className="text-xs text-muted-foreground mb-2 px-1">{t('Advanced lab tb adocSourceHint')}</p> |
||||
<Input |
||||
value={langFilter} |
||||
onChange={(e) => setLangFilter(e.target.value)} |
||||
placeholder={t('Advanced lab tb filterLanguages')} |
||||
className="h-8 text-xs mb-2" |
||||
/> |
||||
<ScrollArea className="h-[min(50vh,18rem)] pr-2"> |
||||
<div className="flex flex-col gap-0.5"> |
||||
{CODE_LANGUAGES.filter((id) => |
||||
langFilter.trim() ? id.toLowerCase().includes(langFilter.trim().toLowerCase()) : true |
||||
).map((lang) => ( |
||||
<Button |
||||
key={lang} |
||||
type="button" |
||||
variant="ghost" |
||||
size="sm" |
||||
className="h-8 justify-start font-mono text-xs" |
||||
onClick={() => { |
||||
adocSource(lang) |
||||
setLangFilter('') |
||||
}} |
||||
> |
||||
{lang} |
||||
</Button> |
||||
))} |
||||
</div> |
||||
</ScrollArea> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
|
||||
<DropdownMenu> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> |
||||
<Sigma className="h-3.5 w-3.5" /> |
||||
{t('Advanced lab tb adocStem')} |
||||
<ChevronDown className="h-3 w-3 opacity-60" /> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent align="start" className="z-[280] w-[min(24rem,92vw)] max-h-[min(70vh,28rem)] overflow-y-auto"> |
||||
<DropdownMenuLabel>{t('Advanced lab tb adocStemHint')}</DropdownMenuLabel> |
||||
<DropdownMenuItem |
||||
onClick={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', 'x^2 + y^2', ']'))} |
||||
> |
||||
{t('Advanced lab tb adocStemInline')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuSeparator /> |
||||
<DropdownMenuItem |
||||
onClick={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', '\\frac{a}{b}', ']'))} |
||||
> |
||||
{t('Advanced lab tb katexFrac')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', '\\sqrt{x}', ']'))}> |
||||
{t('Advanced lab tb katexSqrt')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', '\\sum_{i=1}^{n} i', ']'))} |
||||
> |
||||
{t('Advanced lab tb katexSum')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
onClick={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', '\\int_{a}^{b} f(x)\\,dx', ']'))} |
||||
> |
||||
{t('Advanced lab tb katexInt')} |
||||
</DropdownMenuItem> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
|
||||
<Button |
||||
type="button" |
||||
variant="ghost" |
||||
size="sm" |
||||
className="h-8 text-xs shrink-0" |
||||
title={t('Advanced lab tb adocHrTitle')} |
||||
onClick={() => run((v) => labInsertRaw(v, sliceRef, "\n'''\n"))} |
||||
> |
||||
<Minus className="h-3.5 w-3.5" /> |
||||
</Button> |
||||
</div> |
||||
) |
||||
} |
||||
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
import { EditorSelection } from '@codemirror/state' |
||||
import type { EditorView } from '@codemirror/view' |
||||
import type { AdvancedEventLabSlice } from '@/lib/advanced-event-lab-slice' |
||||
|
||||
export function labSyncSliceFromView( |
||||
view: EditorView, |
||||
sliceRef: { current: AdvancedEventLabSlice | null } |
||||
) { |
||||
const s = sliceRef.current |
||||
if (s) s.content = view.state.doc.toString() |
||||
} |
||||
|
||||
export function labInsertSnippet( |
||||
view: EditorView, |
||||
sliceRef: { current: AdvancedEventLabSlice | null }, |
||||
before: string, |
||||
placeholder: string, |
||||
after: string |
||||
) { |
||||
const sel = view.state.selection.main |
||||
const insert = before + placeholder + after |
||||
const innerFrom = sel.from + before.length |
||||
const innerTo = innerFrom + placeholder.length |
||||
view.dispatch({ |
||||
changes: { from: sel.from, to: sel.to, insert }, |
||||
selection: EditorSelection.range(innerFrom, innerTo) |
||||
}) |
||||
view.focus() |
||||
labSyncSliceFromView(view, sliceRef) |
||||
} |
||||
|
||||
export function labInsertRaw( |
||||
view: EditorView, |
||||
sliceRef: { current: AdvancedEventLabSlice | null }, |
||||
text: string |
||||
) { |
||||
const sel = view.state.selection.main |
||||
view.dispatch({ |
||||
changes: { from: sel.from, to: sel.to, insert: text }, |
||||
selection: EditorSelection.cursor(sel.from + text.length) |
||||
}) |
||||
view.focus() |
||||
labSyncSliceFromView(view, sliceRef) |
||||
} |
||||
|
||||
/** If there is a selection, wrap it; otherwise insert snippet with placeholder between delimiters. */ |
||||
export function labWrapOrSnippet( |
||||
view: EditorView, |
||||
sliceRef: { current: AdvancedEventLabSlice | null }, |
||||
wrap: string, |
||||
placeholder: string |
||||
) { |
||||
const sel = view.state.selection.main |
||||
const selected = view.state.sliceDoc(sel.from, sel.to) |
||||
if (selected.length > 0) { |
||||
const insert = `${wrap}${selected}${wrap}` |
||||
view.dispatch({ |
||||
changes: { from: sel.from, to: sel.to, insert }, |
||||
selection: EditorSelection.cursor(sel.from + insert.length) |
||||
}) |
||||
view.focus() |
||||
labSyncSliceFromView(view, sliceRef) |
||||
} else { |
||||
labInsertSnippet(view, sliceRef, wrap, placeholder, wrap) |
||||
} |
||||
} |
||||
@ -0,0 +1,122 @@
@@ -0,0 +1,122 @@
|
||||
import EmojiPickerDialog from '@/components/EmojiPickerDialog' |
||||
import GifPicker from '@/components/GifPicker' |
||||
import MemePicker from '@/components/MemePicker' |
||||
import { Button } from '@/components/ui/button' |
||||
import { Separator } from '@/components/ui/separator' |
||||
import { isTouchDevice } from '@/lib/utils' |
||||
import type { TEmoji } from '@/types' |
||||
import { Film, ImageUp, Laugh, Mic, Settings, Smile } from 'lucide-react' |
||||
import { useTranslation } from 'react-i18next' |
||||
import Uploader from './Uploader' |
||||
import { MentionAndEventToolbarButtons } from './PostTextarea/Mention/MentionAndEventToolbarButtons' |
||||
|
||||
export type PostEditorFormatToolbarUploadHandlers = { |
||||
onUploadSuccess: (result: { url: string; tags: string[][]; file?: File }) => void |
||||
onUploadStart?: (file: File, cancel: () => void) => void |
||||
onUploadEnd?: (file: File) => void |
||||
onProgress?: (file: File, progress: number) => void |
||||
onUploadCompressPhase?: (file: File, phase: 'compressing' | 'uploading') => void |
||||
onUploadCompressProgress?: (file: File, percent: number) => void |
||||
} |
||||
|
||||
export type PostEditorFormatToolbarProps = { |
||||
insertText: (text: string) => void |
||||
insertEmoji: (emoji: string | TEmoji) => void |
||||
upload: PostEditorFormatToolbarUploadHandlers |
||||
showAudioUpload: boolean |
||||
audioUploadTitle: string |
||||
audioButtonHighlighted: boolean |
||||
showMoreOptions: boolean |
||||
onToggleMoreOptions: () => void |
||||
} |
||||
|
||||
/** |
||||
* Icon row under the composer: media upload, emoji/GIF/meme, npub + nevent/naddr, more options. |
||||
* Must render inside {@link NeventPickerProvider} when using mention/event buttons. |
||||
*/ |
||||
export function PostEditorFormatToolbar({ |
||||
insertText, |
||||
insertEmoji, |
||||
upload, |
||||
showAudioUpload, |
||||
audioUploadTitle, |
||||
audioButtonHighlighted, |
||||
showMoreOptions, |
||||
onToggleMoreOptions |
||||
}: PostEditorFormatToolbarProps) { |
||||
const { t } = useTranslation() |
||||
|
||||
return ( |
||||
<div className="flex flex-wrap items-center gap-2 min-w-0 shrink-0"> |
||||
{showAudioUpload && ( |
||||
<Uploader |
||||
onUploadSuccess={upload.onUploadSuccess} |
||||
onUploadStart={upload.onUploadStart} |
||||
onUploadEnd={upload.onUploadEnd} |
||||
onProgress={upload.onProgress} |
||||
onUploadCompressPhase={upload.onUploadCompressPhase} |
||||
onUploadCompressProgress={upload.onUploadCompressProgress} |
||||
accept="audio/*,.mka,audio/x-matroska" |
||||
> |
||||
<Button |
||||
type="button" |
||||
variant="ghost" |
||||
size="icon" |
||||
title={audioUploadTitle} |
||||
className={audioButtonHighlighted ? 'bg-accent' : ''} |
||||
> |
||||
<Mic className="h-4 w-4" /> |
||||
</Button> |
||||
</Uploader> |
||||
)} |
||||
<Uploader |
||||
onUploadSuccess={upload.onUploadSuccess} |
||||
onUploadStart={upload.onUploadStart} |
||||
onUploadEnd={upload.onUploadEnd} |
||||
onProgress={upload.onProgress} |
||||
onUploadCompressPhase={upload.onUploadCompressPhase} |
||||
onUploadCompressProgress={upload.onUploadCompressProgress} |
||||
accept="image/*" |
||||
> |
||||
<Button type="button" variant="ghost" size="icon" title={t('Upload Image')}> |
||||
<ImageUp /> |
||||
</Button> |
||||
</Uploader> |
||||
<Separator orientation="vertical" className="h-6 shrink-0" /> |
||||
{!isTouchDevice() && ( |
||||
<EmojiPickerDialog |
||||
onEmojiClick={(emoji) => { |
||||
if (emoji == null) return |
||||
insertEmoji(emoji) |
||||
}} |
||||
> |
||||
<Button type="button" variant="ghost" size="icon" title={t('Insert emoji')}> |
||||
<Smile /> |
||||
</Button> |
||||
</EmojiPickerDialog> |
||||
)} |
||||
<GifPicker onSelect={(gifUrl) => insertText(gifUrl)}> |
||||
<Button type="button" variant="ghost" size="icon" title={t('Insert GIF')}> |
||||
<Film className="h-4 w-4" /> |
||||
</Button> |
||||
</GifPicker> |
||||
<MemePicker onSelect={(memeUrl) => insertText(memeUrl)}> |
||||
<Button type="button" variant="ghost" size="icon" title={t('Insert meme')}> |
||||
<Laugh className="h-4 w-4" /> |
||||
</Button> |
||||
</MemePicker> |
||||
<Separator orientation="vertical" className="h-6 shrink-0" /> |
||||
<MentionAndEventToolbarButtons insertAtCursor={insertText} variant="ghost" /> |
||||
<Button |
||||
type="button" |
||||
variant="ghost" |
||||
size="icon" |
||||
title={t('More options')} |
||||
className={showMoreOptions ? 'bg-accent' : ''} |
||||
onClick={onToggleMoreOptions} |
||||
> |
||||
<Settings /> |
||||
</Button> |
||||
</div> |
||||
) |
||||
} |
||||
Loading…
Reference in new issue