diff --git a/src/json-crdt-extensions/peritext/block/Inline.ts b/src/json-crdt-extensions/peritext/block/Inline.ts index 94b570bd86..d368951562 100644 --- a/src/json-crdt-extensions/peritext/block/Inline.ts +++ b/src/json-crdt-extensions/peritext/block/Inline.ts @@ -3,7 +3,6 @@ import {stringify} from '../../../json-text/stringify'; import {SliceBehavior, SliceTypeName} from '../slice/constants'; import {Range} from '../rga/Range'; import {ChunkSlice} from '../util/ChunkSlice'; -import {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint'; import {Cursor} from '../editor/Cursor'; import {hashId} from '../../../json-crdt/hash'; import {formatType} from '../slice/util'; @@ -15,34 +14,70 @@ import type {Peritext} from '../Peritext'; import type {Slice} from '../slice/types'; import type {PeritextMlAttributes, PeritextMlNode} from './types'; -/** The attribute started before this inline and ends after this inline. */ -export class InlineAttrPassing { +export abstract class AbstractInlineAttr { constructor(public slice: Slice) {} + + /** @returns Whether the attribute starts at the start of the inline. */ + isStart(): boolean { + return false; + } + + /** @returns Whether the attribute ends at the end of the inline. */ + isEnd(): boolean { + return false; + } + + /** @returns Whether the attribute is collapsed to a point. */ + isCollapsed(): boolean { + return false; + } } +/** The attribute started before this inline and ends after this inline. */ +export class InlineAttrPassing extends AbstractInlineAttr {} + /** The attribute starts at the beginning of this inline. */ -export class InlineAttrStart { - constructor(public slice: Slice) {} +export class InlineAttrStart extends AbstractInlineAttr { + isStart(): boolean { + return true; + } } /** The attribute ends at the end of this inline. */ -export class InlineAttrEnd { - constructor(public slice: Slice) {} +export class InlineAttrEnd extends AbstractInlineAttr { + isEnd(): boolean { + return true; + } } /** The attribute starts and ends in this inline, exactly contains it. */ -export class InlineAttrContained { - constructor(public slice: Slice) {} +export class InlineAttrContained extends AbstractInlineAttr { + isStart(): boolean { + return true; + } + isEnd(): boolean { + return true; + } } /** The attribute is collapsed at start of this inline. */ -export class InlineAttrStartPoint { - constructor(public slice: Slice) {} +export class InlineAttrStartPoint extends AbstractInlineAttr { + isStart(): boolean { + return true; + } + isCollapsed(): boolean { + return true; + } } /** The attribute is collapsed at end of this inline. */ -export class InlineAttrEndPoint { - constructor(public slice: Slice) {} +export class InlineAttrEndPoint extends AbstractInlineAttr { + isEnd(): boolean { + return true; + } + isCollapsed(): boolean { + return true; + } } export type InlineAttr = diff --git a/src/json-crdt-extensions/peritext/block/index.ts b/src/json-crdt-extensions/peritext/block/index.ts index 17f3552535..13792dfe57 100644 --- a/src/json-crdt-extensions/peritext/block/index.ts +++ b/src/json-crdt-extensions/peritext/block/index.ts @@ -1,3 +1,3 @@ export {Block, IBlock} from './Block'; export {LeafBlock} from './LeafBlock'; -export {Inline} from './Inline'; +export * from './Inline'; diff --git a/src/json-crdt-extensions/peritext/slice/constants.ts b/src/json-crdt-extensions/peritext/slice/constants.ts index 5c36f219f4..85461a8f87 100644 --- a/src/json-crdt-extensions/peritext/slice/constants.ts +++ b/src/json-crdt-extensions/peritext/slice/constants.ts @@ -40,6 +40,7 @@ export const enum SliceTypeCon { collapselist = 26, // Collapsible list - > List item collapse = 27, // Collapsible block note = 28, // Note block + mathblock = 29, // block // ------------------------------------------------ inline slices (-64 to -1) Cursor = -1, @@ -56,12 +57,12 @@ export const enum SliceTypeCon { ins = -12, // sup = -13, // sub = -14, // - math = -15, // + math = -15, // inline font = -16, // col = -17, // bg = -18, // kbd = -19, // - hidden = -20, // + spoiler = -20, // q = -21, // (inline quote) cite = -22, // (inline citation) footnote = -23, // or with href="#footnote-..." and title="Footnote ..." @@ -106,6 +107,7 @@ export enum SliceTypeName { collapselist = SliceTypeCon.collapselist, collapse = SliceTypeCon.collapse, note = SliceTypeCon.note, + mathblock = SliceTypeCon.mathblock, Cursor = SliceTypeCon.Cursor, RemoteCursor = SliceTypeCon.RemoteCursor, @@ -126,7 +128,7 @@ export enum SliceTypeName { col = SliceTypeCon.col, bg = SliceTypeCon.bg, kbd = SliceTypeCon.kbd, - hidden = SliceTypeCon.hidden, + spoiler = SliceTypeCon.spoiler, footnote = SliceTypeCon.footnote, ref = SliceTypeCon.ref, iaside = SliceTypeCon.iaside, diff --git a/src/json-crdt-peritext-ui/plugins/cursor/RenderCaret.tsx b/src/json-crdt-peritext-ui/plugins/cursor/RenderCaret.tsx index 398c48280f..4d53fb5bce 100644 --- a/src/json-crdt-peritext-ui/plugins/cursor/RenderCaret.tsx +++ b/src/json-crdt-peritext-ui/plugins/cursor/RenderCaret.tsx @@ -44,6 +44,10 @@ const innerClass = rule({ an: moveAnimation + ' .25s ease-out forwards', }); +const innerClass2 = rule({ + 'mix-blend-mode': 'hard-light', +}); + export interface RenderCaretProps extends CaretViewProps { children: React.ReactNode; } @@ -84,6 +88,9 @@ export const RenderCaret: React.FC = ({italic, children}) => { }} /> )} + + {/* Two carets overlay, so that they look good, both, on white and black backgrounds. */} + ); diff --git a/src/json-crdt-peritext-ui/plugins/cursor/constants.ts b/src/json-crdt-peritext-ui/plugins/cursor/constants.ts index c47c94e026..831a03ef03 100644 --- a/src/json-crdt-peritext-ui/plugins/cursor/constants.ts +++ b/src/json-crdt-peritext-ui/plugins/cursor/constants.ts @@ -1,6 +1,13 @@ export enum DefaultRendererColors { ActiveCursor = '#07f', InactiveCursor = 'rgba(127,127,127,.7)', - ActiveSelection = '#d7e9fd', + + /** + * Derived from #d7e9fd. 80% opacity used so that + * any inline formatting underneath the selection + * is still visible. + */ + ActiveSelection = 'rgba(196,223,253,.8)', + InactiveSelection = 'rgba(127,127,127,.2)', } diff --git a/src/json-crdt-peritext-ui/plugins/minimal/RenderInline/Spoiler.tsx b/src/json-crdt-peritext-ui/plugins/minimal/RenderInline/Spoiler.tsx new file mode 100644 index 0000000000..2ee96aebfc --- /dev/null +++ b/src/json-crdt-peritext-ui/plugins/minimal/RenderInline/Spoiler.tsx @@ -0,0 +1,23 @@ +// biome-ignore lint: React is used for JSX +import * as React from 'react'; +import {rule} from 'nano-theme'; + +const blockClass = rule({ + bg: '#222', + col: 'transparent', + bdrad: '2px', + '&:hover': { + bg: '#222', + col: 'rgba(255, 255, 255, 0.2)', + }, +}); + +export interface SpoilerProps { + children: React.ReactNode; +} + +export const Spoiler: React.FC = (props) => { + const {children} = props; + + return {children}; +}; diff --git a/src/json-crdt-peritext-ui/plugins/minimal/RenderInline.tsx b/src/json-crdt-peritext-ui/plugins/minimal/RenderInline/index.tsx similarity index 82% rename from src/json-crdt-peritext-ui/plugins/minimal/RenderInline.tsx rename to src/json-crdt-peritext-ui/plugins/minimal/RenderInline/index.tsx index 385ef6aea8..60e71de90e 100644 --- a/src/json-crdt-peritext-ui/plugins/minimal/RenderInline.tsx +++ b/src/json-crdt-peritext-ui/plugins/minimal/RenderInline/index.tsx @@ -1,10 +1,11 @@ // biome-ignore lint: React is used for JSX import * as React from 'react'; -import {usePeritext} from '../../react'; -import {useSyncStoreOpt} from '../../react/hooks'; -import {DefaultRendererColors} from './constants'; -import type {InlineViewProps} from '../../react/InlineView'; -import {CommonSliceType} from '../../../json-crdt-extensions'; +import {usePeritext} from '../../../react'; +import {useSyncStoreOpt} from '../../../react/hooks'; +import {DefaultRendererColors} from '../constants'; +import {CommonSliceType} from '../../../../json-crdt-extensions'; +import {Spoiler} from './Spoiler'; +import type {InlineViewProps} from '../../../react/InlineView'; interface RenderInlineSelectionProps extends RenderInlineProps { selection: [left: 'anchor' | 'focus' | '', right: 'anchor' | 'focus' | '']; @@ -44,8 +45,7 @@ export const RenderInline: React.FC = (props) => { if (attr[CommonSliceType.sub]) element = {element}; if (attr[CommonSliceType.math]) element = {element}; if (attr[CommonSliceType.kbd]) element = {element}; - if (attr[CommonSliceType.hidden]) - element = {element}; + if (attr[CommonSliceType.spoiler]) element = {element}; if (selection) { element = ( diff --git a/src/json-crdt-peritext-ui/plugins/minimal/TopToolbar/index.tsx b/src/json-crdt-peritext-ui/plugins/minimal/TopToolbar/index.tsx index 9261cabf98..07d00d3bbe 100644 --- a/src/json-crdt-peritext-ui/plugins/minimal/TopToolbar/index.tsx +++ b/src/json-crdt-peritext-ui/plugins/minimal/TopToolbar/index.tsx @@ -51,6 +51,7 @@ export const TopToolbar: React.FC = ({ctx}) => { {inlineGroupButton(CommonSliceType.b, 'Bold')} {inlineGroupButton(CommonSliceType.i, 'Italic')} {inlineGroupButton(CommonSliceType.u, 'Underline')} + {inlineGroupButton(CommonSliceType.overline, 'Overline')} {inlineGroupButton(CommonSliceType.s, 'Strikethrough')} {inlineGroupButton(CommonSliceType.code, 'Code')} {inlineGroupButton(CommonSliceType.mark, 'Mark')} @@ -60,7 +61,7 @@ export const TopToolbar: React.FC = ({ctx}) => { {inlineGroupButton(CommonSliceType.sub, 'Subscript')} {inlineGroupButton(CommonSliceType.math, 'Math')} {inlineGroupButton(CommonSliceType.kbd, 'Key')} - {inlineGroupButton(CommonSliceType.hidden, 'Spoiler')} + {inlineGroupButton(CommonSliceType.spoiler, 'Spoiler')} {inlineGroupButton(CommonSliceType.bookmark, 'Bookmark')} {button('Blue', () => { diff --git a/src/json-crdt-peritext-ui/plugins/minimal/text.ts b/src/json-crdt-peritext-ui/plugins/minimal/text.ts index 52d17be243..bc5a958d1f 100644 --- a/src/json-crdt-peritext-ui/plugins/minimal/text.ts +++ b/src/json-crdt-peritext-ui/plugins/minimal/text.ts @@ -13,6 +13,7 @@ export const text: PeritextPlugin['text'] = (props, inline) => { if (attrs[CommonSliceType.b]) style.fontWeight = 'bold'; if (attrs[CommonSliceType.i]) style.fontStyle = 'italic'; if (attrs[CommonSliceType.u]) textDecoration = 'underline'; + if (attrs[CommonSliceType.overline]) textDecoration = textDecoration ? textDecoration + ' overline' : 'overline'; if (attrs[CommonSliceType.s]) textDecoration = textDecoration ? textDecoration + ' line-through' : 'line-through'; if ((attr = attrs[CommonSliceType.col])) style.color = attr[0].slice.data() + ''; diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderBlock.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderBlock.tsx index 74d5956cf4..7a20a69a24 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderBlock.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderBlock.tsx @@ -1,7 +1,7 @@ // biome-ignore lint: React is used for JSX import * as React from 'react'; -import type {BlockViewProps} from '../../react/BlockView'; import {CommonSliceType} from '../../../json-crdt-extensions'; +import type {BlockViewProps} from '../../react/BlockView'; export interface RenderBlockProps extends BlockViewProps { children: React.ReactNode; diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline.tsx deleted file mode 100644 index 68fb81813b..0000000000 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// biome-ignore lint: React is used for JSX -import * as React from 'react'; -import {CommonSliceType} from '../../../json-crdt-extensions'; -import type {InlineViewProps} from '../../react/InlineView'; - -export interface RenderInlineProps extends InlineViewProps { - children: React.ReactNode; -} - -export const RenderInline: React.FC = (props) => { - const {inline, children} = props; - const attr = inline.attr(); - let element = children; - if (attr[CommonSliceType.code]) element = {element}; - if (attr[CommonSliceType.mark]) element = {element}; - if (attr[CommonSliceType.del]) element = {element}; - if (attr[CommonSliceType.ins]) element = {element}; - if (attr[CommonSliceType.sup]) element = {element}; - if (attr[CommonSliceType.sub]) element = {element}; - if (attr[CommonSliceType.math]) element = {element}; - if (attr[CommonSliceType.kbd]) element = {element}; - if (attr[CommonSliceType.hidden]) - element = {element}; - return element; -}; diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Code.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Code.tsx new file mode 100644 index 0000000000..cc523de898 --- /dev/null +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Code.tsx @@ -0,0 +1,41 @@ +// biome-ignore lint: React is used for JSX +import * as React from 'react'; +import {rule, drule, theme, useTheme} from 'nano-theme'; +import type {InlineAttr} from '../../../../json-crdt-extensions'; + +const blockClass = drule({ + ...theme.font.mono.mid, + fz: '.9em', + pdt: '.05em', + pdb: '.05em', +}); + +const startClass = rule({ + borderTopLeftRadius: '.3em', + borderBottomLeftRadius: '.3em', + pdl: '.24em', +}); + +const endClass = rule({ + borderTopRightRadius: '.3em', + borderBottomRightRadius: '.3em', + pdr: '.24em', +}); + +export interface CodeProps { + attr: InlineAttr; + children: React.ReactNode; +} + +export const Code: React.FC = (props) => { + const {children, attr} = props; + const theme = useTheme(); + const className = + blockClass({ + bg: theme.g(0.2, 0.1), + }) + + (attr.isStart() ? startClass : '') + + (attr.isEnd() ? endClass : ''); + + return {children}; +}; diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Del.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Del.tsx new file mode 100644 index 0000000000..0d715b67ae --- /dev/null +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Del.tsx @@ -0,0 +1,19 @@ +// biome-ignore lint: React is used for JSX +import * as React from 'react'; +import {rule} from 'nano-theme'; + +const delClass = rule({ + bg: '#ffebe9', + bxsh: '0 2px 0 0 #ffcecb', + col: 'red', +}); + +export interface DelProps { + children: React.ReactNode; +} + +export const Del: React.FC = (props) => { + const {children} = props; + + return {children}; +}; diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Ins.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Ins.tsx new file mode 100644 index 0000000000..a63ec6e151 --- /dev/null +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Ins.tsx @@ -0,0 +1,19 @@ +// biome-ignore lint: React is used for JSX +import * as React from 'react'; +import {rule} from 'nano-theme'; + +const blockClass = rule({ + bg: '#dafbe1', + bxsh: '0 2px 0 0 #aceebb', + td: 'none', +}); + +export interface InsProps { + children: React.ReactNode; +} + +export const Ins: React.FC = (props) => { + const {children} = props; + + return {children}; +}; diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Kbd.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Kbd.tsx new file mode 100644 index 0000000000..06a611ae78 --- /dev/null +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Kbd.tsx @@ -0,0 +1,44 @@ +// biome-ignore lint: React is used for JSX +import * as React from 'react'; +import {rule, theme} from 'nano-theme'; +import type {InlineAttr} from '../../../../json-crdt-extensions'; + +const blockClass = rule({ + ...theme.font.mono.mid, + mrt: '-.3em', + pdt: '.3em', + pdb: '.3em', + bg: theme.g(0.2), + bdt: `1px solid ${theme.g(0.3)}`, + bdb: `2px solid ${theme.g(0)}`, + lh: '1em', + fz: '.7em', + ws: 'nowrap', + bxsh: '0 0 .125em rgba(0,0,0,.5),0 .065em .19em rgba(0,0,0,.5),.065em 0 .125em rgba(0,0,0,.2)', + col: '#fff', +}); + +const startClass = rule({ + pdl: '.7em', + borderTopLeftRadius: '.3em', + borderBottomLeftRadius: '.3em', +}); + +const endClass = rule({ + pdr: 'calc(.7em - 2px)', + borderTopRightRadius: '.3em', + borderBottomRightRadius: '.3em', + bdr: `2px solid ${theme.g(0.1)}`, +}); + +export interface KbdProps { + attr: InlineAttr; + children: React.ReactNode; +} + +export const Kbd: React.FC = (props) => { + const {attr, children} = props; + const className = blockClass + (attr.isStart() ? startClass : '') + (attr.isEnd() ? endClass : ''); + + return {children}; +}; diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Spoiler.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Spoiler.tsx new file mode 100644 index 0000000000..735bef250a --- /dev/null +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Spoiler.tsx @@ -0,0 +1,23 @@ +// biome-ignore lint: React is used for JSX +import * as React from 'react'; +import {rule} from 'nano-theme'; + +const blockClass = rule({ + bg: '#222', + col: 'transparent', + bdrad: 'calc(min(2px, 0.15em))', + '&:hover': { + col: 'inherit', + bg: 'rgba(0,0,0,.16)', + }, +}); + +export interface SpoilerProps { + children: React.ReactNode; +} + +export const Spoiler: React.FC = (props) => { + const {children} = props; + + return {children}; +}; diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/index.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/index.tsx new file mode 100644 index 0000000000..d9581e23fd --- /dev/null +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/index.tsx @@ -0,0 +1,35 @@ +// biome-ignore lint: React is used for JSX +import * as React from 'react'; +import {CommonSliceType} from '../../../../json-crdt-extensions'; +import {Spoiler} from './Spoiler'; +import {Code} from './Code'; +import {Kbd} from './Kbd'; +import {Ins} from './Ins'; +import {Del} from './Del'; +import type {InlineViewProps} from '../../../react/InlineView'; + +export interface RenderInlineProps extends InlineViewProps { + children: React.ReactNode; +} + +export const RenderInline: React.FC = (props) => { + const {inline, children} = props; + const attrs = inline.attr(); + let element = children; + if (attrs[CommonSliceType.mark]) element = {element}; + if (attrs[CommonSliceType.sup]) element = {element}; + if (attrs[CommonSliceType.sub]) element = {element}; + if (attrs[CommonSliceType.math]) element = {element}; + if (attrs[CommonSliceType.ins]) element = {element}; + if (attrs[CommonSliceType.del]) element = {element}; + if (attrs[CommonSliceType.code]) { + const attr = attrs[CommonSliceType.code][0]; + if (attr) element = {element}; + } + if (attrs[CommonSliceType.kbd]) { + const attr = attrs[CommonSliceType.kbd][0]; + if (attr) element = {element}; + } + if (attrs[CommonSliceType.spoiler]) element = {element}; + return element; +}; diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/TopToolbar/index.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/TopToolbar/index.tsx index e087ed218d..d810e95324 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/TopToolbar/index.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/TopToolbar/index.tsx @@ -51,6 +51,7 @@ export const TopToolbar: React.FC = ({ctx}) => { {inlineGroupButton(CommonSliceType.b, 'Bold')} {inlineGroupButton(CommonSliceType.i, 'Italic')} {inlineGroupButton(CommonSliceType.u, 'Underline')} + {inlineGroupButton(CommonSliceType.overline, 'Overline')} {inlineGroupButton(CommonSliceType.s, 'Strikethrough')} {inlineGroupButton(CommonSliceType.code, 'Code')} {inlineGroupButton(CommonSliceType.mark, 'Mark')} @@ -60,7 +61,7 @@ export const TopToolbar: React.FC = ({ctx}) => { {inlineGroupButton(CommonSliceType.sub, 'Subscript')} {inlineGroupButton(CommonSliceType.math, 'Math')} {inlineGroupButton(CommonSliceType.kbd, 'Key')} - {inlineGroupButton(CommonSliceType.hidden, 'Spoiler')} + {inlineGroupButton(CommonSliceType.spoiler, 'Spoiler')} {inlineGroupButton(CommonSliceType.bookmark, 'Bookmark')} {button('Blue', () => { diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx index 70da3959e5..2e24b1a8e4 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx @@ -143,7 +143,7 @@ export class ToolbarState implements UiLifeCyclesRender { name: 'Classified', icon: () => , onSelect: () => { - et.format(CommonSliceType.hidden); + et.format(CommonSliceType.spoiler); }, }, ], @@ -159,37 +159,51 @@ export class ToolbarState implements UiLifeCyclesRender { { name: 'Code', icon: () => , - onSelect: () => {}, + onSelect: () => { + et.format(CommonSliceType.code); + }, }, { name: 'Math', icon: () => , - onSelect: () => {}, + onSelect: () => { + et.format(CommonSliceType.math); + }, }, { name: 'Superscript', icon: () => , - onSelect: () => {}, + onSelect: () => { + et.format(CommonSliceType.sup); + }, }, { name: 'Subscript', icon: () => , - onSelect: () => {}, + onSelect: () => { + et.format(CommonSliceType.sub); + }, }, { name: 'Keyboard key', icon: () => , - onSelect: () => {}, + onSelect: () => { + et.format(CommonSliceType.kbd); + }, }, { name: 'Insertion', icon: () => , - onSelect: () => {}, + onSelect: () => { + et.format(CommonSliceType.ins); + }, }, { name: 'Deletion', icon: () => , - onSelect: () => {}, + onSelect: () => { + et.format(CommonSliceType.del); + }, }, ], },