From 7767c69a7ab6edb6d5af49199da6c8e9b5432c08 Mon Sep 17 00:00:00 2001 From: Joss Mackison <2730833+jossmac@users.noreply.github.com> Date: Fri, 18 Aug 2023 13:30:22 +1000 Subject: [PATCH] Document editor main content (#546) --- .changeset/witty-bats-listen.md | 13 + .../pkg/src/breadcrumbs/BreadcrumbItem.tsx | 4 +- .../pkg/src/date-time/InputSegment.tsx | 4 +- design-system/pkg/src/field/Field.tsx | 2 + .../pkg/src/field/FieldPrimitive.tsx | 1 + design-system/pkg/src/field/types.tsx | 10 +- .../pkg/src/text-field/TextFieldPrimitive.tsx | 4 +- design-system/pkg/src/typography/Prose.tsx | 191 ++++++++++++++ design-system/pkg/src/typography/index.ts | 1 + .../src/typography/stories/Prose.stories.tsx | 118 +++++++++ .../pkg/src/typography/text/useTextStyles.ts | 4 +- dev-projects/next-app/keystatic.config.tsx | 6 + packages/keystatic/src/app/entry-form.tsx | 74 +++--- .../document/DocumentEditor/Toolbar.tsx | 99 +++++-- .../blockquote/blockquote-ui.tsx | 16 +- .../code-block/code-block-ui.tsx | 21 +- .../document/DocumentEditor/divider.tsx | 19 +- .../DocumentEditor/heading/heading.tsx | 30 +-- .../fields/document/DocumentEditor/index.tsx | 246 ++++++++++-------- .../fields/document/DocumentEditor/leaf.tsx | 19 +- .../DocumentEditor/render-element.tsx | 22 +- .../keystatic/src/form/fields/document/ui.tsx | 50 ++-- 22 files changed, 643 insertions(+), 311 deletions(-) create mode 100644 .changeset/witty-bats-listen.md create mode 100644 design-system/pkg/src/typography/Prose.tsx create mode 100644 design-system/pkg/src/typography/stories/Prose.stories.tsx diff --git a/.changeset/witty-bats-listen.md b/.changeset/witty-bats-listen.md new file mode 100644 index 000000000..748bbf3a5 --- /dev/null +++ b/.changeset/witty-bats-listen.md @@ -0,0 +1,13 @@ +--- +'@example/next-app': patch +'@keystatic/core': patch +'@keystar/ui': patch +--- + +Optimise the editor appearance when `entryLayout="content"` for a more focused experience. + +Component library: + +- Update the antialiasing behaviour everywhere +- New `Prose` component from "@keystar/ui/typography" package. +- Improve `Field` implementation and types diff --git a/design-system/pkg/src/breadcrumbs/BreadcrumbItem.tsx b/design-system/pkg/src/breadcrumbs/BreadcrumbItem.tsx index 84a39fa2c..2b0f7c8fb 100644 --- a/design-system/pkg/src/breadcrumbs/BreadcrumbItem.tsx +++ b/design-system/pkg/src/breadcrumbs/BreadcrumbItem.tsx @@ -50,8 +50,8 @@ export function BreadcrumbItem(props: BreadcrumbItemProps) { fontSize: tokenSchema.typography.text.regular.size, fontFamily: tokenSchema.typography.fontFamily.base, fontWeight: tokenSchema.typography.fontWeight.medium, - WebkitFontSmoothing: 'antialiased', - MozOsxFontSmoothing: 'grayscale', + MozOsxFontSmoothing: 'auto', + WebkitFontSmoothing: 'auto', '&[data-size=small]': { fontSize: tokenSchema.typography.text.small.size, diff --git a/design-system/pkg/src/date-time/InputSegment.tsx b/design-system/pkg/src/date-time/InputSegment.tsx index f29174fd2..5ff89a0ca 100644 --- a/design-system/pkg/src/date-time/InputSegment.tsx +++ b/design-system/pkg/src/date-time/InputSegment.tsx @@ -67,8 +67,8 @@ function EditableSegment({ segment, state }: InputSegmentProps) { fontWeight: tokenSchema.typography.fontWeight.regular, lineHeight: tokenSchema.typography.lineheight.small, whiteSpace: 'nowrap', - WebkitFontSmoothing: 'antialiased', - MozOsxFontSmoothing: 'grayscale', + MozOsxFontSmoothing: 'auto', + WebkitFontSmoothing: 'auto', '[dir=ltr] &': { textAlign: 'right' }, '[dir=rtl] &': { textAlign: 'left' }, diff --git a/design-system/pkg/src/field/Field.tsx b/design-system/pkg/src/field/Field.tsx index eac8bdc88..417c787ee 100644 --- a/design-system/pkg/src/field/Field.tsx +++ b/design-system/pkg/src/field/Field.tsx @@ -19,6 +19,7 @@ export const Field = (props: InternalFieldProps) => { isReadOnly, isRequired, label, + ...otherProps } = props; let { labelProps, fieldProps, descriptionProps, errorMessageProps } = useField(props); @@ -40,6 +41,7 @@ export const Field = (props: InternalFieldProps) => { descriptionProps={descriptionProps} errorMessage={errorMessage} errorMessageProps={errorMessageProps} + {...otherProps} > {children(renderProps)} diff --git a/design-system/pkg/src/field/FieldPrimitive.tsx b/design-system/pkg/src/field/FieldPrimitive.tsx index 8fd4fd165..1d10e1239 100644 --- a/design-system/pkg/src/field/FieldPrimitive.tsx +++ b/design-system/pkg/src/field/FieldPrimitive.tsx @@ -59,6 +59,7 @@ export const FieldPrimitive: ForwardRefExoticComponent< ref={forwardedRef} direction="column" gap="medium" + minWidth={0} UNSAFE_className={styleProps.className} UNSAFE_style={styleProps.style} > diff --git a/design-system/pkg/src/field/types.tsx b/design-system/pkg/src/field/types.tsx index 36b6d8b23..e379bb8c1 100644 --- a/design-system/pkg/src/field/types.tsx +++ b/design-system/pkg/src/field/types.tsx @@ -9,7 +9,15 @@ import { ReactElement, ReactNode } from 'react'; import { BaseStyleProps } from '@keystar/ui/style'; -export type FieldRenderProp = (props: LabelAria['fieldProps']) => ReactElement; +type FieldRenderInputProps = LabelAria['fieldProps'] & { + disabled?: boolean; + readOnly?: boolean; + 'aria-required'?: boolean; + 'aria-invalid'?: boolean; +}; +export type FieldRenderProp = ( + inputProps: FieldRenderInputProps +) => ReactElement; export type FieldProps = { /** diff --git a/design-system/pkg/src/text-field/TextFieldPrimitive.tsx b/design-system/pkg/src/text-field/TextFieldPrimitive.tsx index 687de4237..8f850dcfa 100644 --- a/design-system/pkg/src/text-field/TextFieldPrimitive.tsx +++ b/design-system/pkg/src/text-field/TextFieldPrimitive.tsx @@ -189,8 +189,8 @@ function useTextFieldStyles() { textOverflow: 'ellipsis', verticalAlign: 'top', width: '100%', - WebkitFontSmoothing: 'antialiased', - MozOsxFontSmoothing: 'grayscale', + MozOsxFontSmoothing: 'auto', + WebkitFontSmoothing: 'auto', '::placeholder': { color: tokenSchema.color.foreground.neutralTertiary, diff --git a/design-system/pkg/src/typography/Prose.tsx b/design-system/pkg/src/typography/Prose.tsx new file mode 100644 index 000000000..b86e1291b --- /dev/null +++ b/design-system/pkg/src/typography/Prose.tsx @@ -0,0 +1,191 @@ +import { ReactNode } from 'react'; + +import { + BaseStyleProps, + FontSizeText, + classNames, + css, + filterStyleProps, + tokenSchema, + useStyleProps, +} from '@keystar/ui/style'; +import { forwardRefWithAs } from '@keystar/ui/utils/ts'; +import { toDataAttributes } from '../utils'; + +export type ProseProps = { + /** The content to render. */ + children?: ReactNode; + /** + * The size of body text. + * @default 'medium' + */ + size?: FontSizeText; +} & BaseStyleProps; + +/** A typographic component that adds styles for rendering remote HTML content. */ +export const Prose = forwardRefWithAs((props, ref) => { + const { children, elementType: ElementType = 'div', ...otherProps } = props; + const styleProps = useProseStyles(otherProps); + + return ( + + {children} + + ); +}); + +function useProseStyles(props: ProseProps) { + const { size = 'medium', ...otherProps } = props; + const styleProps = useStyleProps(otherProps); + + return { + ...styleProps, + ...toDataAttributes({ size }), + className: classNames( + 'ksv:Prose', + css({ + color: tokenSchema.color.foreground.neutral, + fontFamily: tokenSchema.typography.fontFamily.base, + height: '100%', + maxWidth: '100%', + minHeight: 0, + minWidth: 0, + MozOsxFontSmoothing: 'auto', + WebkitFontSmoothing: 'auto', + + '&[data-size="small"]': { + fontSize: tokenSchema.typography.text.small.size, + lineHeight: 1.6, + }, + '&[data-size="regular"]': { + fontSize: tokenSchema.typography.text.regular.size, + lineHeight: 1.5, + }, + '&[data-size="medium"]': { + fontSize: tokenSchema.typography.text.medium.size, + lineHeight: 1.5, + }, + '&[data-size="large"]': { + fontSize: tokenSchema.typography.text.large.size, + lineHeight: 1.4, + }, + + // Elements + // --------------------------------------------------------------------- + + '& :is(blockquote, p, pre, ol, ul)': { + marginBlock: '1em', + + ':first-child': { marginTop: 0 }, + ':last-child': { marginBottom: 0 }, + }, + 'ol ol, ul ul, ol ul, ul ol': { + marginBlock: 0, + }, + a: { + color: tokenSchema.color.foreground.accent, + }, + blockquote: { + borderInlineStart: `${tokenSchema.size.border.large} solid ${tokenSchema.color.border.neutral}`, + marginInline: '1em', + paddingInlineStart: '1em', + }, + hr: { + marginBlock: '1.5em', + backgroundColor: tokenSchema.color.alias.borderIdle, + border: 0, + height: tokenSchema.size.border.medium, + }, + img: { + height: 'auto', + maxWidth: '100%', + }, + + // code block + pre: { + backgroundColor: tokenSchema.color.background.surface, + borderRadius: tokenSchema.size.radius.medium, + color: tokenSchema.color.foreground.neutralEmphasis, + fontFamily: tokenSchema.typography.fontFamily.code, + fontSize: '0.85em', + lineHeight: tokenSchema.typography.lineheight.medium, + minWidth: 0, + maxWidth: '100%', + overflow: 'auto', + padding: tokenSchema.size.space.medium, + }, + 'pre > code': { + fontFamily: 'inherit', + }, + // inline code + '& :not(pre) > code': { + backgroundColor: tokenSchema.color.background.accent, + borderRadius: tokenSchema.size.radius.small, + color: tokenSchema.color.foreground.neutralEmphasis, + display: 'inline-block', + fontSize: '0.85em', + fontFamily: tokenSchema.typography.fontFamily.code, + paddingInline: tokenSchema.size.space.small, + }, + + // Headings + // --------------------------------------------------------------------- + + 'h1, h2, h3, h4, h5, h6': { + color: tokenSchema.color.foreground.neutralEmphasis, + fontWeight: tokenSchema.typography.fontWeight.semibold, + lineHeight: 1.25, + marginTop: '1.5em', + marginBottom: '0.67em', + + ':first-child': { marginTop: 0 }, + ':last-child': { marginBottom: 0 }, + }, + h1: { + fontSize: '2em', + fontWeight: tokenSchema.typography.fontWeight.bold, + }, + h2: { + fontSize: '1.5em', + }, + h3: { + fontSize: '1.25em', + fontWeight: tokenSchema.typography.fontWeight.bold, + }, + h4: { + fontSize: '1.1em', + }, + h5: { + fontSize: '1em', + fontWeight: tokenSchema.typography.fontWeight.bold, + }, + h6: { + fontSize: '0.9em', + }, + ...getListStyles(), + }), + styleProps.className + ), + }; +} + +function getListStyles() { + let styles: any = {}; + + let listDepth = 10; + const orderedListStyles = ['lower-roman', 'decimal', 'lower-alpha']; + const unorderedListStyles = ['square', 'disc', 'circle']; + + while (listDepth--) { + let arr = Array.from({ length: listDepth }); + if (arr.length) { + styles[arr.map(() => `ol`).join(' ')] = { + listStyle: orderedListStyles[listDepth % 3], + }; + styles[arr.map(() => `ul`).join(' ')] = { + listStyle: unorderedListStyles[listDepth % 3], + }; + } + } + return styles; +} diff --git a/design-system/pkg/src/typography/index.ts b/design-system/pkg/src/typography/index.ts index 4cb114b7b..0e768df02 100644 --- a/design-system/pkg/src/typography/index.ts +++ b/design-system/pkg/src/typography/index.ts @@ -7,6 +7,7 @@ export { Text, textClassList, useTextStyles, useTextContext } from './text'; export { Emoji } from './Emoji'; export { Kbd } from './Kbd'; export { Numeral } from './Numeral'; +export { Prose } from './Prose'; // types export type { EmojiProps } from './Emoji'; diff --git a/design-system/pkg/src/typography/stories/Prose.stories.tsx b/design-system/pkg/src/typography/stories/Prose.stories.tsx new file mode 100644 index 000000000..510e61005 --- /dev/null +++ b/design-system/pkg/src/typography/stories/Prose.stories.tsx @@ -0,0 +1,118 @@ +import { Prose } from '../Prose'; + +export default { + title: 'Components/Typography/Prose', +}; + +export const Default = () => ( + +); + +Default.story = { + name: 'default', +}; + +const htmlElements = `

h1 Heading

+

h2 Heading

+

h3 Heading

+

h4 Heading

+
h5 Heading
+
h6 Heading
+ +

Horizontal Rules

+
+ +

Emphasis

+

This is bold text

+

This is italic text

+

Strikethrough

+ +

Blockquotes

+
+

Blockquotes can also be nested…

+
+

…by using additional greater-than signs right next to each other…

+
+

…or with spaces between arrows.

+
+
+
+ +

Lists

+

Unordered

+ +

Ordered

+
    +
  1. Lorem ipsum dolor sit amet
  2. +
  3. Consectetur adipiscing elit
  4. +
  5. +Integer molestie lorem at massa +
      +
    1. Lorem ipsum dolor sit amet
    2. +
    3. Consectetur adipiscing elit
    4. +
    5. +Integer molestie lorem at massa +
        +
      1. Lorem ipsum dolor sit amet
      2. +
      3. Consectetur adipiscing elit
      4. +
      5. Integer molestie lorem at massa
      6. +
      7. You can use sequential numbers…
      8. +
      +
    6. +
    7. You can use sequential numbers…
    8. +
    +
  6. +
  7. You can use sequential numbers…
  8. +
+

Start numbering with offset:

+
    +
  1. foo
  2. +
  3. bar
  4. +
+ +

Code

+

Code block

+
+var foo = function (bar) {
+  return bar++;
+};
+
+console.log(foo(5));
+
+

Inline code

+

We export a config object wrapped in the config function imported from @keystatic/core.

+ +

Links

+

A paragraph that contains link text within it.

+ +

Images

+

Octocat

+ +

TODO: "not-prose"

+
+

Heading 3

+

Some paragraph text

+
    +
  • list
  • +
  • of
  • +
  • things
  • +
+
`; diff --git a/design-system/pkg/src/typography/text/useTextStyles.ts b/design-system/pkg/src/typography/text/useTextStyles.ts index 52935e18f..df5c688f7 100644 --- a/design-system/pkg/src/typography/text/useTextStyles.ts +++ b/design-system/pkg/src/typography/text/useTextStyles.ts @@ -60,8 +60,8 @@ export function useTextStyles( // ---------------------------------------------------------------------------- export const textOptimizationStyles = { - WebkitFontSmoothing: 'antialiased', - MozOsxFontSmoothing: 'grayscale', + MozOsxFontSmoothing: 'auto', + WebkitFontSmoothing: 'auto', } as const; const alignmentMap = { diff --git a/dev-projects/next-app/keystatic.config.tsx b/dev-projects/next-app/keystatic.config.tsx index 833cd1978..d6e64ec00 100644 --- a/dev-projects/next-app/keystatic.config.tsx +++ b/dev-projects/next-app/keystatic.config.tsx @@ -231,6 +231,12 @@ export default config({ bio: fields.document({ label: 'Bio', images: true, + formatting: true, + dividers: true, + links: true, + }), + notes: fields.document({ + label: 'Notes', formatting: { inlineMarks: { bold: true, diff --git a/packages/keystatic/src/app/entry-form.tsx b/packages/keystatic/src/app/entry-form.tsx index 7374be589..3d0ce971c 100644 --- a/packages/keystatic/src/app/entry-form.tsx +++ b/packages/keystatic/src/app/entry-form.tsx @@ -1,11 +1,11 @@ -import { Box, Grid } from '@keystar/ui/layout'; +import { Grid } from '@keystar/ui/layout'; import { SplitView, SplitPanePrimary, SplitPaneSecondary, } from '@keystar/ui/split-view'; +import { createContext, useContext } from 'react'; -import { FormatInfo } from './path-utils'; import { ReadonlyPropPath } from '../form/fields/document/DocumentEditor/component-blocks/utils'; import { PathContextProvider, @@ -25,11 +25,17 @@ import { Collection, Singleton, } from '..'; +import { FormatInfo } from './path-utils'; import { ScrollView } from './shell/primitives'; import { PageContainer } from './shell/page'; import { useContentPanelQuery } from './shell/context'; const emptyArray: ReadonlyPropPath = []; +const RESPONSIVE_PADDING = { + mobile: 'medium', + tablet: 'xlarge', + desktop: 'xxlarge', +}; export function containerWidthForEntryLayout( config: Collection | Singleton @@ -37,6 +43,11 @@ export function containerWidthForEntryLayout( return config.entryLayout === 'content' ? 'none' : 'medium'; } +const EntryLayoutSplitPaneContext = createContext<'main' | 'side' | null>(null); +export function useEntryLayoutSplitPaneContext() { + return useContext(EntryLayoutSplitPaneContext); +} + export function FormForEntry({ formatInfo, forceValidation, @@ -72,49 +83,34 @@ export function FormForEntry({ flex > - - + + - - + + - - - {Object.entries(props.fields).map(([key, propVal]) => - key === contentField.key ? null : ( - - - - ) - )} - - + + + + {Object.entries(props.fields).map(([key, propVal]) => + key === contentField.key ? null : ( + + + + ) + )} + + + @@ -124,9 +120,7 @@ export function FormForEntry({ return ( - + + {!!documentFeatures.formatting.headings.levels.length && ( )} + {/* make sure elements fill space */} + + {useMemo(() => { return ( viewState && ( @@ -129,7 +134,7 @@ export function Toolbar({ ); }, [viewState])} {!!hasBlockItems && } - + ); } @@ -139,35 +144,67 @@ const ToolbarGroup = ({ children }: { children: ReactNode }) => { }; const ToolbarContainer = ({ children }: { children: ReactNode }) => { + let entryLayoutPane = useEntryLayoutSplitPaneContext(); + if (entryLayoutPane === 'main') { + return ( +
+ {children} +
+ ); + } + return
{children}
; +}; + +const ToolbarWrapper = ({ children }: { children: ReactNode }) => { + let entryLayoutPane = useEntryLayoutSplitPaneContext(); return ( - - {children} - - + <> +
+ {children} +
+ ); }; const ToolbarScrollArea = (props: { children: ReactNode }) => { + let entryLayoutPane = useEntryLayoutSplitPaneContext(); return ( { '&::-webkit-scrollbar': { display: 'none', }, + + '&[data-layout="main"]': { + paddingInline: 0, + }, })} {...props} /> @@ -260,13 +301,17 @@ const HeadingMenu = ({ }; function InsertBlockMenu() { + let entryLayoutPane = useEntryLayoutSplitPaneContext(); const editor = useSlateStatic(); const componentBlocks = useDocumentEditorConfig().componentBlocks; return ( - + diff --git a/packages/keystatic/src/form/fields/document/DocumentEditor/blockquote/blockquote-ui.tsx b/packages/keystatic/src/form/fields/document/DocumentEditor/blockquote/blockquote-ui.tsx index 324fc6493..6030da175 100644 --- a/packages/keystatic/src/form/fields/document/DocumentEditor/blockquote/blockquote-ui.tsx +++ b/packages/keystatic/src/form/fields/document/DocumentEditor/blockquote/blockquote-ui.tsx @@ -10,8 +10,6 @@ import { Kbd, Text } from '@keystar/ui/typography'; import { useToolbarState } from '../toolbar-state'; import { isElementActive } from '../utils'; -import { Box } from '@keystar/ui/layout'; -import { blockElementSpacing } from '../ui-utils'; export const insertBlockquote = (editor: Editor) => { const isActive = isElementActive(editor, 'blockquote'); @@ -31,19 +29,7 @@ export const BlockquoteElement = ({ attributes, children, }: RenderElementProps) => { - return ( - - {children} - - ); + return
{children}
; }; const BlockquoteButton = () => { diff --git a/packages/keystatic/src/form/fields/document/DocumentEditor/code-block/code-block-ui.tsx b/packages/keystatic/src/form/fields/document/DocumentEditor/code-block/code-block-ui.tsx index 19fcb687b..d672151bf 100644 --- a/packages/keystatic/src/form/fields/document/DocumentEditor/code-block/code-block-ui.tsx +++ b/packages/keystatic/src/form/fields/document/DocumentEditor/code-block/code-block-ui.tsx @@ -10,7 +10,6 @@ import { codeIcon } from '@keystar/ui/icon/icons/codeIcon'; import { trash2Icon } from '@keystar/ui/icon/icons/trash2Icon'; import { Icon } from '@keystar/ui/icon'; import { Flex } from '@keystar/ui/layout'; -import { css, tokenSchema } from '@keystar/ui/style'; import { Tooltip, TooltipTrigger } from '@keystar/ui/tooltip'; import { Kbd, Text } from '@keystar/ui/typography'; @@ -102,25 +101,7 @@ export function CodeElement({ <> -
+          
             {children}
           
diff --git a/packages/keystatic/src/form/fields/document/DocumentEditor/divider.tsx b/packages/keystatic/src/form/fields/document/DocumentEditor/divider.tsx index bad7daaed..38a7e6285 100644 --- a/packages/keystatic/src/form/fields/document/DocumentEditor/divider.tsx +++ b/packages/keystatic/src/form/fields/document/DocumentEditor/divider.tsx @@ -3,8 +3,7 @@ import { Editor } from 'slate'; import { RenderElementProps, useSelected } from 'slate-react'; import { ActionButton } from '@keystar/ui/button'; -import { Box } from '@keystar/ui/layout'; -import { css, tokenSchema } from '@keystar/ui/style'; +import { tokenSchema } from '@keystar/ui/style'; import { Tooltip, TooltipTrigger } from '@keystar/ui/tooltip'; import { Text, Kbd } from '@keystar/ui/typography'; @@ -55,23 +54,15 @@ export const dividerButton = ( export function DividerElement({ attributes, children }: RenderElementProps) { const selected = useSelected(); return ( - +

{children} - +
); } diff --git a/packages/keystatic/src/form/fields/document/DocumentEditor/heading/heading.tsx b/packages/keystatic/src/form/fields/document/DocumentEditor/heading/heading.tsx index 65108900e..a036bfa53 100644 --- a/packages/keystatic/src/form/fields/document/DocumentEditor/heading/heading.tsx +++ b/packages/keystatic/src/form/fields/document/DocumentEditor/heading/heading.tsx @@ -1,19 +1,18 @@ +import { useState } from 'react'; import { ReactEditor, RenderElementProps, useSlateStatic } from 'slate-react'; -import { tokenSchema } from '@keystar/ui/style'; -import { blockElementSpacing } from '../ui-utils'; -import { BlockPopover, BlockPopoverTrigger } from '../primitives'; +import { Transforms } from 'slate'; + import { ActionButton } from '@keystar/ui/button'; import { Icon } from '@keystar/ui/icon'; import { trash2Icon } from '@keystar/ui/icon/icons/trash2Icon'; import { Flex } from '@keystar/ui/layout'; import { TooltipTrigger, Tooltip } from '@keystar/ui/tooltip'; -import { Transforms } from 'slate'; import { CustomAttributesDialog, CustomAttributesEditButton, } from '../custom-attributes'; -import { useState } from 'react'; +import { BlockPopover, BlockPopoverTrigger } from '../primitives'; import { useDocumentEditorConfig } from '../toolbar-state'; export const HeadingElement = ({ @@ -26,16 +25,11 @@ export const HeadingElement = ({ const { documentFeatures } = useDocumentEditorConfig(); const [dialogOpen, setDialogOpen] = useState(false); - if (Object.keys(documentFeatures.formatting.headings.schema).length === 0) { + if ( + Object.keys(documentFeatures.formatting.headings.schema.fields).length === 0 + ) { return ( - + {children} ); @@ -43,13 +37,7 @@ export const HeadingElement = ({ return ( <> - +
{children}
diff --git a/packages/keystatic/src/form/fields/document/DocumentEditor/index.tsx b/packages/keystatic/src/form/fields/document/DocumentEditor/index.tsx index 4d5ce133b..1ceb3ba56 100644 --- a/packages/keystatic/src/form/fields/document/DocumentEditor/index.tsx +++ b/packages/keystatic/src/form/fields/document/DocumentEditor/index.tsx @@ -23,11 +23,17 @@ import { import { Editable, ReactEditor, Slate, useSlate, withReact } from 'slate-react'; import { EditableProps } from 'slate-react/dist/components/editable'; -import { Box } from '@keystar/ui/layout'; -import { classNames, css, tokenSchema } from '@keystar/ui/style'; +import { + breakpointQueries, + classNames, + css, + tokenSchema, +} from '@keystar/ui/style'; +import { Prose } from '@keystar/ui/typography'; -import { DocumentFeatures } from './document-features'; +import { useEntryLayoutSplitPaneContext } from '../../../../app/entry-form'; import { ComponentBlock } from '../../../api'; +import { DocumentFeatures } from './document-features'; import { wrapLink } from './link/link'; import { clearFormatting, Mark, nodeTypeMatcher } from './utils'; import { Toolbar } from './Toolbar'; @@ -54,6 +60,7 @@ import { withPasting } from './pasting'; import { withShortcuts } from './shortcuts'; import { withSoftBreaks } from './soft-breaks'; import { getSelectedTableArea } from './table/with-table'; + // the docs site needs access to Editor and importing slate would use the version from the content field // so we're exporting it from here (note that this is not at all visible in the published version) export { Editor } from 'slate'; @@ -304,17 +311,32 @@ export function DocumentEditor({ componentBlocks: Record; documentFeatures: DocumentFeatures; } & Omit) { + let entryLayoutPane = useEntryLayoutSplitPaneContext(); const editor = useMemo( () => createDocumentEditor(documentFeatures, componentBlocks), [documentFeatures, componentBlocks] ); return ( - + { // for debugging false && } - + ); } @@ -425,6 +443,7 @@ function getPrismTokenLength(token: Prism.Token | string): number { } export function DocumentEditorEditable(props: EditableProps) { + const entryLayoutPane = useEntryLayoutSplitPaneContext(); const editor = useSlate(); const { componentBlocks, documentFeatures } = useDocumentEditorConfig(); @@ -435,92 +454,95 @@ export function DocumentEditorEditable(props: EditableProps) { return ( - ) => { - let decorations: Range[] = []; - if (node.type === 'component-block') { - if ( - node.children.length === 1 && - Element.isElement(node.children[0]) && - node.children[0].type === 'component-inline-prop' && - node.children[0].propPath === undefined - ) { - return decorations; - } - node.children.forEach((child, index) => { + + ) => { + let decorations: Range[] = []; + if (node.type === 'component-block') { if ( - Node.string(child) === '' && - Element.isElement(child) && - (child.type === 'component-block-prop' || - child.type === 'component-inline-prop') && - child.propPath !== undefined + node.children.length === 1 && + Element.isElement(node.children[0]) && + node.children[0].type === 'component-inline-prop' && + node.children[0].propPath === undefined ) { - const start = Editor.start(editor, [...path, index]); - const placeholder = getPlaceholderTextForPropPath( - child.propPath, - componentBlocks[node.component].schema, - node.props - ); - if (placeholder) { - decorations.push({ - placeholder, - anchor: start, - focus: start, - }); - } + return decorations; } - }); - } - if ( - node.type === 'code' && - node.children.length === 1 && - node.children[0].type === undefined && - node.language && - node.language in Prism.languages - ) { - const textPath = [...path, 0]; - const tokens = Prism.tokenize( - node.children[0].text, - Prism.languages[node.language] - ); - function consumeTokens( - start: number, - tokens: (string | Prism.Token)[] - ) { - for (const token of tokens) { - const length = getPrismTokenLength(token); - const end = start + length; - - if (typeof token !== 'string') { - decorations.push({ - ['prism_' + token.type]: true, - anchor: { path: textPath, offset: start }, - focus: { path: textPath, offset: end }, - }); - consumeTokens( - start, - Array.isArray(token.content) - ? token.content - : [token.content] + node.children.forEach((child, index) => { + if ( + Node.string(child) === '' && + Element.isElement(child) && + (child.type === 'component-block-prop' || + child.type === 'component-inline-prop') && + child.propPath !== undefined + ) { + const start = Editor.start(editor, [...path, index]); + const placeholder = getPlaceholderTextForPropPath( + child.propPath, + componentBlocks[node.component].schema, + node.props ); + if (placeholder) { + decorations.push({ + placeholder, + anchor: start, + focus: start, + }); + } } + }); + } + if ( + node.type === 'code' && + node.children.length === 1 && + node.children[0].type === undefined && + node.language && + node.language in Prism.languages + ) { + const textPath = [...path, 0]; + const tokens = Prism.tokenize( + node.children[0].text, + Prism.languages[node.language] + ); + function consumeTokens( + start: number, + tokens: (string | Prism.Token)[] + ) { + for (const token of tokens) { + const length = getPrismTokenLength(token); + const end = start + length; + + if (typeof token !== 'string') { + decorations.push({ + ['prism_' + token.type]: true, + anchor: { path: textPath, offset: start }, + focus: { path: textPath, offset: end }, + }); + consumeTokens( + start, + Array.isArray(token.content) + ? token.content + : [token.content] + ); + } - start = end; + start = end; + } } + consumeTokens(0, tokens); } - consumeTokens(0, tokens); - } - return decorations; - }, - [editor, componentBlocks] - )} - onKeyDown={onKeyDown} - renderElement={renderElement} - renderLeaf={renderLeaf} - {...props} - className={classNames(editableStyles, props.className)} - /> + return decorations; + }, + [editor, componentBlocks] + )} + onKeyDown={onKeyDown} + renderElement={renderElement} + renderLeaf={renderLeaf} + {...props} + data-layout={entryLayoutPane} + className={classNames(editableStyles, props.className)} + /> + ); } @@ -546,37 +568,31 @@ function Debugger() { ); } -const orderedListStyles = ['lower-roman', 'decimal', 'lower-alpha']; -const unorderedListStyles = ['square', 'disc', 'circle']; - let styles: any = { - color: tokenSchema.color.foreground.neutral, flex: 1, - fontFamily: tokenSchema.typography.fontFamily.base, - fontSize: tokenSchema.typography.text.regular.size, height: 'auto', - lineHeight: 1.4, minHeight: tokenSchema.size.scale[2000], minWidth: 0, padding: tokenSchema.size.space.medium, - // antialiased editor text, to match the rest of the app - MozOsxFontSmoothing: 'grayscale', - WebkitFontSmoothing: 'antialiased', -}; -let listDepth = 10; + '&[data-layout="main"]': { + boxSizing: 'border-box', + height: '100%', + padding: 0, + paddingTop: tokenSchema.size.space.medium, + minHeight: 0, + minWidth: 0, + maxWidth: 800, + marginInline: 'auto', -while (listDepth--) { - let arr = Array.from({ length: listDepth }); - if (arr.length) { - styles[arr.map(() => `ol`).join(' ')] = { - listStyle: orderedListStyles[listDepth % 3], - }; - styles[arr.map(() => `ul`).join(' ')] = { - listStyle: unorderedListStyles[listDepth % 3], - }; - } -} + [breakpointQueries.above.mobile]: { + padding: tokenSchema.size.space.xlarge, + }, + [breakpointQueries.above.tablet]: { + padding: tokenSchema.size.space.xxlarge, + }, + }, +}; const editableStyles = css({ ...styles, diff --git a/packages/keystatic/src/form/fields/document/DocumentEditor/leaf.tsx b/packages/keystatic/src/form/fields/document/DocumentEditor/leaf.tsx index 0c19f174d..09eae31b2 100644 --- a/packages/keystatic/src/form/fields/document/DocumentEditor/leaf.tsx +++ b/packages/keystatic/src/form/fields/document/DocumentEditor/leaf.tsx @@ -1,7 +1,7 @@ -import { Box } from '@keystar/ui/layout'; import { css, tokenSchema } from '@keystar/ui/style'; import { ReactNode, useState } from 'react'; import { RenderLeafProps } from 'slate-react'; + import { InsertMenu } from './insert-menu'; function Placeholder({ @@ -76,22 +76,7 @@ const Leaf = ({ leaf, text, children, attributes }: RenderLeafProps) => { } if (code) { - children = ( - - {children} - - ); + children = {children}; } if (bold) { children = {children}; diff --git a/packages/keystatic/src/form/fields/document/DocumentEditor/render-element.tsx b/packages/keystatic/src/form/fields/document/DocumentEditor/render-element.tsx index 46b58962b..c54af8156 100644 --- a/packages/keystatic/src/form/fields/document/DocumentEditor/render-element.tsx +++ b/packages/keystatic/src/form/fields/document/DocumentEditor/render-element.tsx @@ -1,6 +1,5 @@ import { RenderElementProps } from 'slate-react'; -import { BlockquoteElement } from './blockquote/blockquote-ui'; import { ComponentBlocksElement, ComponentInlineProp, @@ -18,7 +17,6 @@ import { TableHeadElement, TableRowElement, } from './table/table-ui'; -import { blockElementSpacing } from './ui-utils'; // some of the renderers read properties of the element // and TS doesn't understand the type narrowing when doing a spread for some reason @@ -72,23 +70,15 @@ export const renderElement = (props: RenderElementProps) => { /> ); case 'ordered-list': - return ( -
    - {props.children} -
- ); + return
    {props.children}
; case 'unordered-list': - return ( -
    - {props.children} -
- ); + return
    {props.children}
; case 'list-item': return
  • {props.children}
  • ; case 'list-item-content': return {props.children}; case 'blockquote': - return ; + return
    {props.children}
    ; case 'divider': return ; case 'image': @@ -143,11 +133,7 @@ export const renderElement = (props: RenderElementProps) => { default: let { textAlign } = props.element; return ( -

    +

    {props.children}

    ); diff --git a/packages/keystatic/src/form/fields/document/ui.tsx b/packages/keystatic/src/form/fields/document/ui.tsx index 519199485..6d1b01d84 100644 --- a/packages/keystatic/src/form/fields/document/ui.tsx +++ b/packages/keystatic/src/form/fields/document/ui.tsx @@ -1,13 +1,16 @@ 'use client'; -import { FieldPrimitive } from '@keystar/ui/field'; -import { DocumentEditor } from './DocumentEditor'; + +import { Field, FieldProps } from '@keystar/ui/field'; +import { useState } from 'react'; + import { ComponentBlock, DocumentElement, FormFieldInputProps, } from '../../api'; +import { useEntryLayoutSplitPaneContext } from '../../../app/entry-form'; +import { DocumentEditor } from './DocumentEditor'; import { DocumentFeatures } from './DocumentEditor/document-features'; -import { useState } from 'react'; let i = 0; @@ -23,6 +26,7 @@ export function DocumentFieldInput( documentFeatures: DocumentFeatures; } ) { + let entryLayoutPane = useEntryLayoutSplitPaneContext(); const [state, setState] = useState<{ key: number; value: (typeof props)['value']; @@ -35,18 +39,34 @@ export function DocumentFieldInput( setState({ key: newKey(), value: props.value }); } + let fieldProps: FieldProps = { + label: props.label, + description: props.description, + }; + if (entryLayoutPane === 'main') { + fieldProps = { + 'aria-label': props.label, + }; + } + return ( - - { - setState(state => ({ key: state.key, value: val as any })); - props.onChange(val as any); - }} - value={state.value as any} - /> - + + {inputProps => ( + { + setState(state => ({ key: state.key, value: val as any })); + props.onChange(val as any); + }} + value={state.value as any} + /> + )} + ); }