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 }
);
const logValueChanges = (value: ReportElements) => {
console.log('value', value);
};
const usedValue: ReportElements = hasBeenSuccessFullAtLeastOnce ? data?.elements || [] : value;
return (
@ -68,7 +72,7 @@ export const ReportPlayground: React.FC = () => {
<ThemePicker />
</div>
<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>
);

View File

@ -69,7 +69,7 @@ function DropdownMenuItem({
data-inset={inset}
data-variant={variant}
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
)}
{...props}

View File

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

View File

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

View File

@ -340,7 +340,7 @@ function InlineComboboxGroupLabel({
return (
<ComboboxGroupLabel
{...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 { MediaEmbedPlugin, useMediaState } from '@platejs/media/react';
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 { Caption, CaptionTextarea } from './CaptionNode';
import { MediaToolbar } from './MediaToolbar';
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(
ResizableProvider,
@ -34,10 +40,15 @@ export const MediaEmbedElement = withHOC(
});
const width = useResizableValue('width');
const provider = embed?.provider;
const hasElement = !!embed?.url;
if (!hasElement) {
return <MediaEmbedPlaceholder {...props} />;
}
return (
<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}>
<Resizable
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/*'],
content: 'Add a video',
icon: <FilmPlay />
},
[KEYS.mediaEmbed]: {
accept: ['*'],
content: 'Add a media embed',
icon: <FilmPlay />
}
};

View File

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

View File

@ -11,7 +11,8 @@ import { insertEquation, insertInlineEquation } from '@platejs/math';
import {
insertAudioPlaceholder,
insertFilePlaceholder,
insertMedia,
insertPlaceholder,
insertImagePlaceholder,
insertVideoPlaceholder
} from '@platejs/media';
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.equation]: (editor) => insertEquation(editor, { select: true }),
[KEYS.file]: (editor) => insertFilePlaceholder(editor, { select: true }),
[KEYS.img]: (editor) =>
insertMedia(editor, {
select: true,
type: KEYS.img
}),
[KEYS.mediaEmbed]: (editor) =>
insertMedia(editor, {
select: true,
type: KEYS.mediaEmbed
}),
[KEYS.img]: (editor) => {
insertImagePlaceholder(editor, {
select: true
});
},
[KEYS.mediaEmbed]: (editor) => {
editor.tf.insertNodes(
editor.api.create.block({
type: KEYS.mediaEmbed
}),
{ select: true }
);
},
[KEYS.table]: (editor) => editor.getTransforms(TablePlugin).insert.table({}, { select: true }),
[KEYS.toc]: (editor) => insertToc(editor, { select: true }),
[KEYS.video]: (editor) => insertVideoPlaceholder(editor, { select: true })

View File

@ -12,25 +12,31 @@ import {
import { KEYS } from 'platejs';
import { AudioElement } from '../elements/AudioNode';
import { MediaEmbedElement } from '../elements/MediaEmbedNode';
import { MediaEmbedElement, MediaEmbedPlaceholder } from '../elements/MediaEmbedNode';
import { FileElement } from '../elements/MediaFileNode';
import { ImageElement } from '../elements/MediaImageNode';
import { PlaceholderElement } from '../elements/MediaPlaceholderElement';
import { MediaPreviewDialog } from '../elements/MediaPreviewDialog';
import { MediaUploadToast } from '../elements/MediaUploadToast';
import { VideoElement } from '../elements/MediaVideoNode';
import { MediaPluginOptions } from '@platejs/media';
export const MediaKit = [
ImagePlugin.configure({
options: { disableUploadInsert: true },
render: { afterEditable: MediaPreviewDialog, node: ImageElement }
}),
MediaEmbedPlugin.withComponent(MediaEmbedElement),
// VideoPlugin.withComponent(VideoElement),
// AudioPlugin.withComponent(AudioElement),
// FilePlugin.withComponent(FileElement),
MediaEmbedPlugin.configure({
node: {
component: MediaEmbedElement,
isSelectable: true,
isElement: true
},
options: {}
}),
PlaceholderPlugin.configure({
options: { disableEmptyPlaceholder: true },
options: { disableEmptyPlaceholder: false },
render: { afterEditable: MediaUploadToast, node: PlaceholderElement }
}),
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
)}>
{label && (
<DropdownMenuLabel className="text-muted-foreground text-xs font-semibold select-none">
<DropdownMenuLabel className="text-muted-foreground font-base text-xs select-none">
{label}
</DropdownMenuLabel>
)}

View File

@ -1,10 +1,10 @@
import React from 'react';
export const FONT_BASE_THEME = {
'--font-heading': 'ui-sans-serif, -apple-system, BlinkMacSystemFont',
'--font-sans': 'ui-sans-serif, -apple-system, BlinkMacSystemFont',
'--font-mono':
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
// '--font-heading': 'ui-sans-serif, -apple-system, BlinkMacSystemFont',
// '--font-sans': 'ui-sans-serif, -apple-system, BlinkMacSystemFont',
// '--font-mono':
// 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
'--spacing': '0.25rem',
'--breakpoint-xl': '80rem',
'--breakpoint-2xl': '96rem',
@ -89,7 +89,8 @@ export const THEME_RESET_COLORS = {
'popover-foreground': '240 10% 3.9%',
primary: '240 5.9% 10%',
'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-foreground': '240 5.9% 10%'
},

View File

@ -78,21 +78,25 @@ export const HeaderElementSchema = z
})
.merge(AttributesSchema);
const ListStylesAttributesSchema = z.object({
listStyleType: z
.enum(['disc', 'circle', 'square', 'decimal', 'decimal-leading-zero', 'todo'])
.optional(),
listRestart: z.boolean().optional(),
listRestartPolite: z.boolean().optional(),
listStart: 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'),
listStyleType: z
.enum(['disc', 'circle', 'square', 'decimal', 'decimal-leading-zero'])
.optional(),
listRestart: z.boolean().optional(),
listRestartPolite: z.boolean().optional(),
listStart: z.number().int().min(0).optional(),
indent: z.number().int().min(0).optional(),
children: z.array(z.union([TextSchema, AnchorSchema, MentionSchema])),
})
.merge(AttributesSchema);
.merge(AttributesSchema)
.merge(ListStylesAttributesSchema);
// Blockquote element
export const BlockquoteElementSchema = z