Files
yaak/src-web/components/MarkdownEditor.tsx
2025-01-03 20:41:00 -08:00

149 lines
4.1 KiB
TypeScript

import useSize from '@react-hook/size';
import classNames from 'classnames';
import { useRef } from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { useKeyValue } from '../hooks/useKeyValue';
import type { EditorProps } from './core/Editor/Editor';
import { Editor } from './core/Editor/Editor';
import { IconButton } from './core/IconButton';
import { SplitLayout } from './core/SplitLayout';
import { VStack } from './core/Stacks';
import { Prose } from './Prose';
interface Props extends Pick<EditorProps, 'heightMode' | 'stateKey'> {
placeholder: string;
className?: string;
defaultValue: string;
onChange: (value: string) => void;
name: string;
}
export function MarkdownEditor({
className,
defaultValue,
onChange,
name,
placeholder,
heightMode,
stateKey,
}: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const [width] = useSize(containerRef.current);
const wideEnoughForSplit = width > 600;
const { set: setViewMode, value: rawViewMode } = useKeyValue<'edit' | 'preview' | 'both'>({
namespace: 'global',
key: ['md_view', name],
fallback: 'edit',
});
if (rawViewMode == null) return null;
let viewMode = rawViewMode;
if (rawViewMode === 'both' && !wideEnoughForSplit) {
viewMode = 'edit';
}
const editor = (
<Editor
hideGutter
wrapLines
className="max-w-2xl max-h-full"
language="markdown"
defaultValue={defaultValue}
onChange={onChange}
placeholder={placeholder}
heightMode={heightMode}
stateKey={stateKey}
/>
);
const preview =
defaultValue.length === 0 ? (
<p className="text-text-subtle">No description</p>
) : (
<Prose className="max-w-xl overflow-y-auto max-h-full">
<Markdown
remarkPlugins={[remarkGfm]}
components={{
a: ({ href, children, ...rest }) => {
if (href && !href.match(/https?:\/\//)) {
href = `http://${href}`;
}
return (
<a target="_blank" rel="noreferrer noopener" href={href} {...rest}>
{children}
</a>
);
},
}}
>
{defaultValue}
</Markdown>
</Prose>
);
const contents =
viewMode === 'both' ? (
<SplitLayout
name="markdown-editor"
layout="horizontal"
firstSlot={({ style }) => <div style={style}>{editor}</div>}
secondSlot={({ style }) => (
<div style={style} className="border-l border-border-subtle pl-6">
{preview}
</div>
)}
/>
) : viewMode === 'preview' ? (
preview
) : (
editor
);
return (
<div
ref={containerRef}
className={classNames(
'w-full h-full pt-1.5 group rounded-md grid grid-cols-[minmax(0,1fr)_auto] grid-rows-1 gap-x-1.5',
className,
)}
>
<div className="h-full w-full">{contents}</div>
<VStack
space={1}
className="bg-surface opacity-20 group-hover:opacity-100 transition-opacity transform-gpu"
>
<IconButton
size="xs"
icon="text"
title="Switch to edit mode"
className={classNames(viewMode === 'edit' && 'bg-surface-highlight !text-text')}
event={{ id: 'md_mode', mode: viewMode }}
onClick={() => setViewMode('edit')}
/>
{wideEnoughForSplit && (
<IconButton
size="xs"
icon="columns_2"
title="Switch to edit mode"
className={classNames(viewMode === 'both' && 'bg-surface-highlight !text-text')}
event={{ id: 'md_mode', mode: viewMode }}
onClick={() => setViewMode('both')}
/>
)}
<IconButton
size="xs"
icon="eye"
title="Switch to preview mode"
className={classNames(viewMode === 'preview' && 'bg-surface-highlight !text-text')}
event={{ id: 'md_mode', mode: viewMode }}
onClick={() => setViewMode('preview')}
/>
</VStack>
</div>
);
}