update nodes

This commit is contained in:
Nate Kelley 2025-08-02 11:54:42 -06:00
parent f32a41b048
commit 76c87fb47c
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
13 changed files with 133 additions and 66 deletions

View File

@ -42,6 +42,10 @@ export const ReportPlayground: React.FC = () => {
{ wait: 150 } { wait: 150 }
); );
const logValueChanges = (value: ReportElements) => {
console.log('value', value);
};
const usedValue: ReportElements = hasBeenSuccessFullAtLeastOnce ? data?.elements || [] : value; const usedValue: ReportElements = hasBeenSuccessFullAtLeastOnce ? data?.elements || [] : value;
return ( return (
@ -68,7 +72,7 @@ export const ReportPlayground: React.FC = () => {
<ThemePicker /> <ThemePicker />
</div> </div>
<div className="bg-background h-full max-h-[calc(100vh-56px)] overflow-y-auto rounded border shadow"> <div className="bg-background h-full max-h-[calc(100vh-56px)] overflow-y-auto rounded border shadow">
<DynamicReportEditor value={usedValue} readOnly={false} /> <DynamicReportEditor value={usedValue} readOnly={false} onValueChange={logValueChanges} />
</div> </div>
</div> </div>
); );

View File

@ -69,7 +69,7 @@ function DropdownMenuItem({
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}

View File

@ -2,14 +2,12 @@ import dynamic from 'next/dynamic';
import { ReportEditorSkeleton } from './ReportEditorSkeleton'; import { ReportEditorSkeleton } from './ReportEditorSkeleton';
import { ReportEditor } from './ReportEditor'; import { ReportEditor } from './ReportEditor';
// export const DynamicReportEditor = dynamic( export const DynamicReportEditor = dynamic(
// () => import('@/components/ui/report/ReportEditor').then((mod) => mod.ReportEditor), () => import('@/components/ui/report/ReportEditor').then((mod) => mod.ReportEditor),
// { {
// ssr: false, ssr: false,
// loading: () => <ReportEditorSkeleton /> loading: () => <ReportEditorSkeleton />
// } }
// ); );
const DynamicReportEditor = ReportEditor;
export default DynamicReportEditor; export default DynamicReportEditor;

View File

@ -46,7 +46,7 @@ export function CodeBlockElement(props: PlateElementProps<TCodeBlockElement>) {
{isLangSupported(element.lang) && ( {isLangSupported(element.lang) && (
<Button <Button
variant="ghost" variant="ghost"
className="size-6 text-xs" size={'small'}
onClick={() => formatCodeBlock(editor, { element })} onClick={() => formatCodeBlock(editor, { element })}
title="Format code" title="Format code"
prefix={<BracketsCurly />}></Button> prefix={<BracketsCurly />}></Button>
@ -61,7 +61,7 @@ export function CodeBlockElement(props: PlateElementProps<TCodeBlockElement>) {
); );
} }
function CodeBlockCombobox() { const CodeBlockCombobox = React.memo(() => {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const readOnly = useReadOnly(); const readOnly = useReadOnly();
const editor = useEditorRef(); const editor = useEditorRef();
@ -83,8 +83,8 @@ function CodeBlockCombobox() {
return ( return (
<PopoverBase open={open} onOpenChange={setOpen}> <PopoverBase open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="ghost" aria-expanded={open} role="combobox"> <Button variant="ghost" size={'small'} aria-expanded={open} role="combobox">
{languages.find((language) => language.value === value)?.label ?? 'Plain Text'} {languages.find((language) => language.value === value)?.label ?? 'Plain text'}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-[200px] p-0" onCloseAutoFocus={() => setSearchValue('')}> <PopoverContent className="w-[200px] p-0" onCloseAutoFocus={() => setSearchValue('')}>
@ -120,7 +120,9 @@ function CodeBlockCombobox() {
</PopoverContent> </PopoverContent>
</PopoverBase> </PopoverBase>
); );
} });
CodeBlockCombobox.displayName = 'CodeBlockCombobox';
function CopyButton({ function CopyButton({
value, value,
@ -137,6 +139,7 @@ function CopyButton({
return ( return (
<Button <Button
variant="ghost" variant="ghost"
size={'small'}
prefix={hasCopied ? <Check /> : <Copy2 />} prefix={hasCopied ? <Check /> : <Copy2 />}
onClick={() => { onClick={() => {
void navigator.clipboard.writeText(typeof value === 'function' ? value() : value); void navigator.clipboard.writeText(typeof value === 'function' ? value() : value);
@ -159,7 +162,7 @@ export function CodeSyntaxLeaf(props: PlateLeafProps<TCodeSyntaxLeaf>) {
const languages: { label: string; value: string }[] = [ const languages: { label: string; value: string }[] = [
// { label: 'Auto', value: 'auto' }, // { label: 'Auto', value: 'auto' },
{ label: 'Plain Text', value: 'plaintext' }, { label: 'Plain text', value: 'plaintext' },
// { label: 'ABAP', value: 'abap' }, // { label: 'ABAP', value: 'abap' },
// { label: 'Agda', value: 'agda' }, // { label: 'Agda', value: 'agda' },
// { label: 'Arduino', value: 'arduino' }, // { label: 'Arduino', value: 'arduino' },

View File

@ -340,7 +340,7 @@ function InlineComboboxGroupLabel({
return ( return (
<ComboboxGroupLabel <ComboboxGroupLabel
{...props} {...props}
className={cn('text-muted-foreground mt-1.5 mb-2 px-3 text-xs font-medium', className)} className={cn('text-muted-foreground font-base mt-1.5 mb-2 px-3 text-xs', className)}
/> />
); );
} }

View File

@ -9,13 +9,19 @@ import type { PlateElementProps } from 'platejs/react';
import { parseTwitterUrl, parseVideoUrl } from '@platejs/media'; import { parseTwitterUrl, parseVideoUrl } from '@platejs/media';
import { MediaEmbedPlugin, useMediaState } from '@platejs/media/react'; import { MediaEmbedPlugin, useMediaState } from '@platejs/media/react';
import { ResizableProvider, useResizableValue } from '@platejs/resizable'; import { ResizableProvider, useResizableValue } from '@platejs/resizable';
import { PlateElement, withHOC } from 'platejs/react'; import { PlateElement, useFocused, useReadOnly, useSelected, withHOC } from 'platejs/react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Caption, CaptionTextarea } from './CaptionNode'; import { Caption, CaptionTextarea } from './CaptionNode';
import { MediaToolbar } from './MediaToolbar'; import { MediaToolbar } from './MediaToolbar';
import { mediaResizeHandleVariants, Resizable, ResizeHandle } from './ResizeHandle'; import { mediaResizeHandleVariants, Resizable, ResizeHandle } from './ResizeHandle';
import { Code3 } from '../../icons';
import { PopoverAnchor, PopoverBase, PopoverContent } from '../../popover';
import { useEffect, useState } from 'react';
import { Title } from '../../typography';
import { Input } from '../../inputs';
import { Button } from '../../buttons';
export const MediaEmbedElement = withHOC( export const MediaEmbedElement = withHOC(
ResizableProvider, ResizableProvider,
@ -34,10 +40,15 @@ export const MediaEmbedElement = withHOC(
}); });
const width = useResizableValue('width'); const width = useResizableValue('width');
const provider = embed?.provider; const provider = embed?.provider;
const hasElement = !!embed?.url;
if (!hasElement) {
return <MediaEmbedPlaceholder {...props} />;
}
return ( return (
<MediaToolbar plugin={MediaEmbedPlugin}> <MediaToolbar plugin={MediaEmbedPlugin}>
<PlateElement className="py-2.5" {...props}> <PlateElement className="media-embed py-2.5" {...props}>
<figure className="group relative m-0 w-full cursor-default" contentEditable={false}> <figure className="group relative m-0 w-full cursor-default" contentEditable={false}>
<Resizable <Resizable
align={align} align={align}
@ -113,3 +124,45 @@ export const MediaEmbedElement = withHOC(
); );
} }
); );
export const MediaEmbedPlaceholder = (props: PlateElementProps<TMediaEmbedElement>) => {
const readOnly = useReadOnly();
const selected = useSelected();
const focused = useFocused();
const isFocused = focused && selected && !readOnly;
return (
<PlateElement className="media-embed py-2.5" {...props}>
<PopoverBase open={isFocused}>
<PopoverAnchor>
<div
className={cn(
'bg-muted hover:bg-primary/10 flex cursor-pointer items-center rounded-sm p-3 pr-9 select-none'
)}
contentEditable={false}>
<div className="text-muted-foreground/80 relative mr-3 flex [&_svg]:size-6">
<Code3 />
</div>
<div className="text-muted-foreground text-sm whitespace-nowrap">Add a media embed</div>
</div>
{props.children}
</PopoverAnchor>
<PopoverContent
className="w-[250px] p-0"
onOpenAutoFocus={(e) => {
console.log('onOpenAutoFocus', e);
e.preventDefault();
}}>
<Title as="h4">Add a media embed</Title>
<div className="bg-gray-light h-0.5 w-full" />
<Input placeholder="Enter a URL" />
<Button block>Add media</Button>
</PopoverContent>
</PopoverBase>
</PlateElement>
);
};

View File

@ -41,6 +41,11 @@ const CONTENT: Record<
accept: ['video/*'], accept: ['video/*'],
content: 'Add a video', content: 'Add a video',
icon: <FilmPlay /> icon: <FilmPlay />
},
[KEYS.mediaEmbed]: {
accept: ['*'],
content: 'Add a media embed',
icon: <FilmPlay />
} }
}; };

View File

@ -18,20 +18,6 @@ import {
} from '@platejs/table/react'; } from '@platejs/table/react';
import { PopoverAnchor } from '@radix-ui/react-popover'; import { PopoverAnchor } from '@radix-ui/react-popover';
import { cva } from 'class-variance-authority'; import { cva } from 'class-variance-authority';
// import {
// ArrowDown,
// ArrowLeft,
// ArrowRight,
// ArrowUp,
// CombineIcon,
// EraserIcon,
// Grid2X2Icon,
// GripVertical,
// PaintBucketIcon,
// SquareSplitHorizontalIcon,
// Trash2Icon,
// XIcon
// } from 'lucide-react';
import { import {
ArrowDown, ArrowDown,
ArrowLeft, ArrowLeft,

View File

@ -11,7 +11,8 @@ import { insertEquation, insertInlineEquation } from '@platejs/math';
import { import {
insertAudioPlaceholder, insertAudioPlaceholder,
insertFilePlaceholder, insertFilePlaceholder,
insertMedia, insertPlaceholder,
insertImagePlaceholder,
insertVideoPlaceholder insertVideoPlaceholder
} from '@platejs/media'; } from '@platejs/media';
import { SuggestionPlugin } from '@platejs/suggestion/react'; import { SuggestionPlugin } from '@platejs/suggestion/react';
@ -41,16 +42,19 @@ const insertBlockMap: Record<string, (editor: PlateEditor, type: string) => void
[KEYS.codeBlock]: (editor) => insertCodeBlock(editor, { select: true }), [KEYS.codeBlock]: (editor) => insertCodeBlock(editor, { select: true }),
[KEYS.equation]: (editor) => insertEquation(editor, { select: true }), [KEYS.equation]: (editor) => insertEquation(editor, { select: true }),
[KEYS.file]: (editor) => insertFilePlaceholder(editor, { select: true }), [KEYS.file]: (editor) => insertFilePlaceholder(editor, { select: true }),
[KEYS.img]: (editor) => [KEYS.img]: (editor) => {
insertMedia(editor, { insertImagePlaceholder(editor, {
select: true, select: true
type: KEYS.img });
}), },
[KEYS.mediaEmbed]: (editor) => [KEYS.mediaEmbed]: (editor) => {
insertMedia(editor, { editor.tf.insertNodes(
select: true, editor.api.create.block({
type: KEYS.mediaEmbed type: KEYS.mediaEmbed
}), }),
{ select: true }
);
},
[KEYS.table]: (editor) => editor.getTransforms(TablePlugin).insert.table({}, { select: true }), [KEYS.table]: (editor) => editor.getTransforms(TablePlugin).insert.table({}, { select: true }),
[KEYS.toc]: (editor) => insertToc(editor, { select: true }), [KEYS.toc]: (editor) => insertToc(editor, { select: true }),
[KEYS.video]: (editor) => insertVideoPlaceholder(editor, { select: true }) [KEYS.video]: (editor) => insertVideoPlaceholder(editor, { select: true })

View File

@ -12,25 +12,31 @@ import {
import { KEYS } from 'platejs'; import { KEYS } from 'platejs';
import { AudioElement } from '../elements/AudioNode'; import { AudioElement } from '../elements/AudioNode';
import { MediaEmbedElement } from '../elements/MediaEmbedNode'; import { MediaEmbedElement, MediaEmbedPlaceholder } from '../elements/MediaEmbedNode';
import { FileElement } from '../elements/MediaFileNode'; import { FileElement } from '../elements/MediaFileNode';
import { ImageElement } from '../elements/MediaImageNode'; import { ImageElement } from '../elements/MediaImageNode';
import { PlaceholderElement } from '../elements/MediaPlaceholderElement'; import { PlaceholderElement } from '../elements/MediaPlaceholderElement';
import { MediaPreviewDialog } from '../elements/MediaPreviewDialog'; import { MediaPreviewDialog } from '../elements/MediaPreviewDialog';
import { MediaUploadToast } from '../elements/MediaUploadToast'; import { MediaUploadToast } from '../elements/MediaUploadToast';
import { VideoElement } from '../elements/MediaVideoNode'; import { VideoElement } from '../elements/MediaVideoNode';
import { MediaPluginOptions } from '@platejs/media';
export const MediaKit = [ export const MediaKit = [
ImagePlugin.configure({ ImagePlugin.configure({
options: { disableUploadInsert: true }, options: { disableUploadInsert: true },
render: { afterEditable: MediaPreviewDialog, node: ImageElement } render: { afterEditable: MediaPreviewDialog, node: ImageElement }
}), }),
MediaEmbedPlugin.withComponent(MediaEmbedElement), MediaEmbedPlugin.configure({
// VideoPlugin.withComponent(VideoElement), node: {
// AudioPlugin.withComponent(AudioElement), component: MediaEmbedElement,
// FilePlugin.withComponent(FileElement), isSelectable: true,
isElement: true
},
options: {}
}),
PlaceholderPlugin.configure({ PlaceholderPlugin.configure({
options: { disableEmptyPlaceholder: true }, options: { disableEmptyPlaceholder: false },
render: { afterEditable: MediaUploadToast, node: PlaceholderElement } render: { afterEditable: MediaUploadToast, node: PlaceholderElement }
}), }),
CaptionPlugin.configure({ CaptionPlugin.configure({
@ -40,4 +46,7 @@ export const MediaKit = [
} }
} }
}) })
// VideoPlugin.withComponent(VideoElement),
// AudioPlugin.withComponent(AudioElement),
// FilePlugin.withComponent(FileElement),
]; ];

View File

@ -262,7 +262,7 @@ export const ToolbarMenuGroup = ({
className className
)}> )}>
{label && ( {label && (
<DropdownMenuLabel className="text-muted-foreground text-xs font-semibold select-none"> <DropdownMenuLabel className="text-muted-foreground font-base text-xs select-none">
{label} {label}
</DropdownMenuLabel> </DropdownMenuLabel>
)} )}

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
export const FONT_BASE_THEME = { export const FONT_BASE_THEME = {
'--font-heading': 'ui-sans-serif, -apple-system, BlinkMacSystemFont', // '--font-heading': 'ui-sans-serif, -apple-system, BlinkMacSystemFont',
'--font-sans': 'ui-sans-serif, -apple-system, BlinkMacSystemFont', // '--font-sans': 'ui-sans-serif, -apple-system, BlinkMacSystemFont',
'--font-mono': // '--font-mono':
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', // 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
'--spacing': '0.25rem', '--spacing': '0.25rem',
'--breakpoint-xl': '80rem', '--breakpoint-xl': '80rem',
'--breakpoint-2xl': '96rem', '--breakpoint-2xl': '96rem',
@ -89,7 +89,8 @@ export const THEME_RESET_COLORS = {
'popover-foreground': '240 10% 3.9%', 'popover-foreground': '240 10% 3.9%',
primary: '240 5.9% 10%', primary: '240 5.9% 10%',
'primary-foreground': '0 0% 98%', 'primary-foreground': '0 0% 98%',
ring: '240 10% 3.9%', // Set ring to a super light gray (almost white)
ring: '0 0% 96%',
secondary: '240 4.8% 95.9%', secondary: '240 4.8% 95.9%',
'secondary-foreground': '240 5.9% 10%' 'secondary-foreground': '240 5.9% 10%'
}, },

View File

@ -78,21 +78,25 @@ export const HeaderElementSchema = z
}) })
.merge(AttributesSchema); .merge(AttributesSchema);
// Paragraph element with optional list styling and indentation const ListStylesAttributesSchema = z.object({
export const ParagraphElementSchema = z
.object({
type: z.literal('p'),
listStyleType: z listStyleType: z
.enum(['disc', 'circle', 'square', 'decimal', 'decimal-leading-zero']) .enum(['disc', 'circle', 'square', 'decimal', 'decimal-leading-zero', 'todo'])
.optional(), .optional(),
listRestart: z.boolean().optional(), listRestart: z.boolean().optional(),
listRestartPolite: z.boolean().optional(), listRestartPolite: z.boolean().optional(),
listStart: z.number().int().min(0).optional(), listStart: z.number().int().min(0).optional(),
indent: z.number().int().min(0).optional(), indent: z.number().int().min(0).optional(),
checked: z.boolean().optional(), //used with todo list style
});
// Paragraph element with optional list styling and indentation
export const ParagraphElementSchema = z
.object({
type: z.literal('p'),
children: z.array(z.union([TextSchema, AnchorSchema, MentionSchema])), children: z.array(z.union([TextSchema, AnchorSchema, MentionSchema])),
}) })
.merge(AttributesSchema); .merge(AttributesSchema)
.merge(ListStylesAttributesSchema);
// Blockquote element // Blockquote element
export const BlockquoteElementSchema = z export const BlockquoteElementSchema = z