Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@portabletext/keyboard-shortcuts": "workspace:*",
"@portabletext/markdown": "workspace:*",
"@portabletext/patches": "workspace:*",
"@portabletext/plugin-dnd": "workspace:*",
"@portabletext/plugin-emoji-picker": "workspace:*",
"@portabletext/plugin-list-index": "workspace:*",
"@portabletext/plugin-markdown-shortcuts": "workspace:*",
Expand Down
104 changes: 57 additions & 47 deletions apps/playground/src/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type RenderStyleFunction,
} from '@portabletext/editor'
import {portableTextToMarkdown} from '@portabletext/markdown'
import {DndProvider} from '@portabletext/plugin-dnd'
import {ListIndexProvider} from '@portabletext/plugin-list-index'
import {MarkdownShortcutsPlugin} from '@portabletext/plugin-markdown-shortcuts'
import {OneLinePlugin} from '@portabletext/plugin-one-line'
Expand Down Expand Up @@ -161,54 +162,63 @@ export function Editor(props: {
</FullscreenAwareToolbarWrapper>
) : null}
<ListIndexProvider>
<FullscreenAwareContainer>
{featureFlags.codeBlockPlugin ? <CodeBlockPlugin /> : null}
{featureFlags.calloutPlugin ? <CalloutPlugin /> : null}
{featureFlags.factBoxPlugin ? <FactBoxPlugin /> : null}
{featureFlags.tablePlugin ? <TablePlugin /> : null}
<InlineObjectsPlugin />
{featureFlags.emojiPickerPlugin ? <EmojiPickerPlugin /> : null}
{featureFlags.mentionPickerPlugin ? (
<MentionPickerPlugin />
) : null}
{featureFlags.slashCommandPlugin ? (
<SlashCommandPickerPlugin />
) : null}
{featureFlags.codeEditorPlugin ? <CodeEditorPlugin /> : null}
{featureFlags.linkPlugin ? <PasteLinkPlugin /> : null}
{featureFlags.imageDeserializerPlugin ? (
<ImageDeserializerPlugin />
) : null}
{featureFlags.htmlDeserializerPlugin ? (
<HtmlDeserializerPlugin />
) : null}
{featureFlags.textFileDeserializerPlugin ? (
<TextFileDeserializerPlugin />
) : null}
{featureFlags.markdownDeserializerPlugin ? (
<MarkdownDeserializerPlugin />
) : null}
{featureFlags.markdownPlugin ? (
<MarkdownShortcutsPlugin {...markdownShortcutsPluginProps} />
) : null}
{featureFlags.oneLinePlugin ? <OneLinePlugin /> : null}
{featureFlags.typographyPlugin ? (
<TypographyPlugin
guard={createDecoratorGuard({
decorators: ({context}) =>
context.schema.decorators.flatMap((decorator) =>
decorator.name === 'code' ? [] : [decorator.name],
),
})}
<DndProvider>
<FullscreenAwareContainer>
{featureFlags.codeBlockPlugin ? <CodeBlockPlugin /> : null}
{featureFlags.calloutPlugin ? <CalloutPlugin /> : null}
{featureFlags.factBoxPlugin ? <FactBoxPlugin /> : null}
{featureFlags.tablePlugin ? <TablePlugin /> : null}
<InlineObjectsPlugin />
{featureFlags.emojiPickerPlugin ? (
<EmojiPickerPlugin />
) : null}
{featureFlags.mentionPickerPlugin ? (
<MentionPickerPlugin />
) : null}
{featureFlags.slashCommandPlugin ? (
<SlashCommandPickerPlugin />
) : null}
{featureFlags.codeEditorPlugin ? <CodeEditorPlugin /> : null}
{featureFlags.linkPlugin ? <PasteLinkPlugin /> : null}
{featureFlags.imageDeserializerPlugin ? (
<ImageDeserializerPlugin />
) : null}
{featureFlags.htmlDeserializerPlugin ? (
<HtmlDeserializerPlugin />
) : null}
{featureFlags.textFileDeserializerPlugin ? (
<TextFileDeserializerPlugin />
) : null}
{featureFlags.markdownDeserializerPlugin ? (
<MarkdownDeserializerPlugin />
) : null}
{featureFlags.markdownPlugin ? (
<MarkdownShortcutsPlugin
{...markdownShortcutsPluginProps}
/>
) : null}
{featureFlags.oneLinePlugin ? <OneLinePlugin /> : null}
{featureFlags.typographyPlugin ? (
<TypographyPlugin
guard={createDecoratorGuard({
decorators: ({context}) =>
context.schema.decorators.flatMap((decorator) =>
decorator.name === 'code' ? [] : [decorator.name],
),
})}
/>
) : null}
<FullscreenAwareEditable
rangeDecorations={props.rangeDecorations}
featureFlags={featureFlags}
loading={loading}
/>
) : null}
<FullscreenAwareEditable
rangeDecorations={props.rangeDecorations}
featureFlags={featureFlags}
loading={loading}
/>
<EditorFooter editorRef={props.editorRef} readOnly={readOnly} />
</FullscreenAwareContainer>
<EditorFooter
editorRef={props.editorRef}
readOnly={readOnly}
/>
</FullscreenAwareContainer>
</DndProvider>
</ListIndexProvider>
</EditorProvider>
</FullscreenProvider>
Expand Down
39 changes: 39 additions & 0 deletions apps/playground/src/plugins/block-drop-indicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type {Path} from '@portabletext/editor'
import {useDropPosition} from '@portabletext/plugin-dnd'
import type {JSX} from 'react'

/**
* The drop-indicator line for a new-pipeline block. The engine draws this
* chrome for legacy top-level blocks but deliberately not for `defineX`
* renders, so containers and their nested blocks draw it themselves from the
* position `@portabletext/plugin-dnd` tracks. Mirrors the engine's
* `pt-drop-indicator`: a 1px `currentColor` line at the block's top edge for a
* `'start'` drop, the bottom edge for `'end'`.
*
* `position: absolute` keeps it out of flow, so the block element it renders
* into must be `relative`. A `<span>` (not a `<div>`) so it stays valid markup
* inside a `<p>` text block.
*/
export function BlockDropIndicator({path}: {path: Path}): JSX.Element | null {
const position = useDropPosition(path)

if (position === undefined) {
return null
}

return (
<span
contentEditable={false}
style={{
position: 'absolute',
left: 0,
right: 0,
[position === 'start' ? 'top' : 'bottom']: 0,
height: 0,
borderTop: '1px solid currentColor',
pointerEvents: 'none',
zIndex: 5,
}}
/>
)
}
54 changes: 54 additions & 0 deletions apps/playground/src/plugins/container-text-block.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type {TextBlockRenderProps} from '@portabletext/editor'
import {useListIndex} from '@portabletext/plugin-list-index'
import {createElement, type JSX} from 'react'
import {BlockDropIndicator} from './block-drop-indicator'

type StyleConfig = {tag: keyof JSX.IntrinsicElements; className: string}

/**
* The text-block render shared by every container (callout, fact-box, cell).
* Containers render through the `defineTextBlock` pipeline, which emits only
* `data-pt-*` attributes and no drop-indicator chrome, so each container would
* otherwise repeat the same three concerns: re-emit the `data-list-*`
* attributes the playground's counter CSS keys off (with the index from
* `useListIndex`, the one value `node` cannot supply), pick the element for the
* block style, and draw the {@link BlockDropIndicator}. They differ only in
* which styles they allow, so that is the single parameter.
*
* The block element is `relative` so the absolutely-positioned indicator
* aligns to it.
*/
export function ContainerTextBlock(props: {
attributes: TextBlockRenderProps['attributes']
children: TextBlockRenderProps['children']
node: TextBlockRenderProps['node']
path: TextBlockRenderProps['path']
styles: {normal: StyleConfig} & Record<string, StyleConfig>
}): JSX.Element {
const listIndex = useListIndex(props.path)

if (props.node.listItem !== undefined) {
return (
<div
{...props.attributes}
data-list-item={props.node.listItem}
data-level={props.node.level}
data-list-index={listIndex}
className="relative my-1"
>
{props.children}
<BlockDropIndicator path={props.path} />
</div>
)
}

const style =
props.styles[props.node.style ?? 'normal'] ?? props.styles.normal

return createElement(
style.tag,
{...props.attributes, className: `relative ${style.className}`.trim()},
props.children,
<BlockDropIndicator key="drop-indicator" path={props.path} />,
)
}
31 changes: 0 additions & 31 deletions apps/playground/src/plugins/list-item-block.tsx

This file was deleted.

75 changes: 25 additions & 50 deletions apps/playground/src/plugins/plugin.callout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,21 @@ import {
TriangleAlertIcon,
} from 'lucide-react'
import type {JSX} from 'react'
import {BlockDropIndicator} from './block-drop-indicator'
import {ContainerTextBlock} from './container-text-block'
import {DragHandle} from './drag-handle'
import {ListItemBlock} from './list-item-block'

const calloutTextStyles = {
normal: {tag: 'p', className: 'my-1'},
h1: {tag: 'h1', className: 'my-2 font-bold text-2xl'},
h2: {tag: 'h2', className: 'my-2 font-bold text-xl'},
h3: {tag: 'h3', className: 'my-2 font-bold text-lg'},
blockquote: {
tag: 'blockquote',
className:
'my-1 border-l-2 border-amber-600 pl-2 italic dark:border-amber-300',
},
} as const

const toneClassName: Record<string, string> = {
note: 'border-sky-400 bg-sky-50 text-sky-900 dark:bg-sky-950/40 dark:text-sky-100',
Expand Down Expand Up @@ -76,7 +89,7 @@ const calloutImageLeaf = defineBlockObject({
export const calloutContainer = defineContainer({
type: 'callout',
arrayField: 'content',
render: ({attributes, children, node, readOnly, selected}) => {
render: ({attributes, children, node, path, readOnly, selected}) => {
const tone = typeof node.tone === 'string' ? node.tone : 'note'
const toneStyle = toneClassName[tone] ?? defaultToneClassName
return (
Expand All @@ -93,60 +106,22 @@ export const calloutContainer = defineContainer({
</span>
<div className="min-w-0 flex-1 cursor-text">{children}</div>
<DragHandle readOnly={readOnly} />
<BlockDropIndicator path={path} />
</aside>
)
},
of: [
defineTextBlock({
type: 'block',
render: ({attributes, children, node, path}) => {
if (node.listItem !== undefined) {
return (
<ListItemBlock
attributes={attributes}
node={node}
path={path}
children={children}
/>
)
}

switch (node.style) {
case 'h1':
return (
<h1 {...attributes} className="my-2 font-bold text-2xl">
{children}
</h1>
)
case 'h2':
return (
<h2 {...attributes} className="my-2 font-bold text-xl">
{children}
</h2>
)
case 'h3':
return (
<h3 {...attributes} className="my-2 font-bold text-lg">
{children}
</h3>
)
case 'blockquote':
return (
<blockquote
{...attributes}
className="my-1 border-l-2 border-amber-600 pl-2 italic dark:border-amber-300"
>
{children}
</blockquote>
)
default:
return (
<p {...attributes} className="my-1">
{children}
</p>
)
}
},
render: ({attributes, children, node, path}) => (
<ContainerTextBlock
attributes={attributes}
node={node}
path={path}
styles={calloutTextStyles}
children={children}
/>
),
}),
calloutImageLeaf,
],
Expand Down
Loading
Loading