From 84559a5f03babe08c478f0e78cab119a50c4bdef Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 18 Jan 2025 14:53:34 +0100 Subject: [PATCH 01/30] =?UTF-8?q?chore(json-crdt-peritext-ui):=20?= =?UTF-8?q?=F0=9F=A4=96=20update=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index f57d3a6f65..280e6edf80 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "json-crdt-traces": "https://github.com/streamich/json-crdt-traces#ec825401dc05cbb74b9e0b3c4d6527399f54d54d", "json-logic-js": "^2.0.2", "nano-theme": "^1.4.3", - "nice-ui": "^1.25.0", + "nice-ui": "^1.26.0", "quill-delta": "^5.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/yarn.lock b/yarn.lock index 2a642cc898..657dea619a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1795,7 +1795,7 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== -code-colors-react@^3.2.1: +code-colors-react@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/code-colors-react/-/code-colors-react-3.3.0.tgz#830d54e0a9925e81354aa4f6a354f9303de1a7df" integrity sha512-RmQf0bNzVG9x+VtkRf+mmLiCzEC3uJLsUfcpOOcHlrwB+BGG++WfSG+YNU+oLewXiPnhwuqiFQYiM0XX9PU8OQ== @@ -3993,14 +3993,14 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -nice-ui@^1.25.0: - version "1.25.0" - resolved "https://registry.yarnpkg.com/nice-ui/-/nice-ui-1.25.0.tgz#7b0bf1187ff04c76acb764dc88d3e6af4b393db9" - integrity sha512-gFC7zWYe8By9TMjIS34WHVSc/9dTW4FVljSqh7zvWyqnzUf33DzGoJGZs5f+5GpdkQEbAwIJBCrs8Z2gno3S2Q== +nice-ui@^1.26.0: + version "1.26.0" + resolved "https://registry.yarnpkg.com/nice-ui/-/nice-ui-1.26.0.tgz#671e9d4db74761c7c7a5ed14e359158ad22e21de" + integrity sha512-GDzNl5ivBi/diMs5hEQw2dy05WLL44l9wHII8htqksY7502rlBvzaH3xtgQCMF12eEybc3udTLN2V8x77iZGLQ== dependencies: "@material-ui/core" "^4.12.4" clipboard-copy "^4.0.1" - code-colors-react "^3.2.1" + code-colors-react "^3.3.0" codemirror "5" emoji-js "^3.8.0" iconista "^2.21.0" From 5f98238c7c728a05ee2ff8d29d43567063bfaa27 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 18 Jan 2025 16:23:12 +0100 Subject: [PATCH 02/30] =?UTF-8?q?feat(json-crdt-peritext-ui):=20?= =?UTF-8?q?=F0=9F=8E=B8=20track=20state=20of=20caret=20visibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/toolbar/CaretToolbar/index.tsx | 765 +---------------- .../plugins/toolbar/RenderCaret.tsx | 10 +- .../plugins/toolbar/menus/menus.tsx | 776 ++++++++++++++++++ .../plugins/toolbar/state.ts | 2 + 4 files changed, 785 insertions(+), 768 deletions(-) create mode 100644 src/json-crdt-peritext-ui/plugins/toolbar/menus/menus.tsx diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/CaretToolbar/index.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/CaretToolbar/index.tsx index 67109b4d74..2b2b091dbd 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/CaretToolbar/index.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/CaretToolbar/index.tsx @@ -10,770 +10,7 @@ import {ToolbarMenu} from 'nice-ui/lib/4-card/Toolbar/ToolbarMenu'; import {FontStyleButton} from 'nice-ui/lib/2-inline-block/FontStyleButton'; import {keyframes, rule} from 'nano-theme'; import type {MenuItem} from 'nice-ui/lib/4-card/StructuralMenu/types'; - -export const annotations = (): MenuItem => { - return { - name: 'Annotations', - expand: 2, - children: [ - { - name: 'Link', - // icon: () => , - icon: () => , - onClick: () => {}, - }, - { - name: 'Comment', - icon: () => , - onClick: () => {}, - }, - { - name: 'Bookmark', - icon: () => , - onClick: () => {}, - }, - { - name: 'Footnote', - icon: () => , - onClick: () => {}, - }, - { - name: 'Aside', - icon: () => , - onClick: () => {}, - }, - ], - }; -}; - -export const secondBrain = (): MenuItem => { - return { - sepBefore: true, - name: 'Second brain', - display: () => ( - <> - Second brain - {/*   - AI */} - - ), - right: () => {'AI'}, - icon: () => , - children: [ - { - name: 'Ask question', - }, - { - name: 'Action', - children: [ - { - name: 'Make shorter', - icon: () => , - onClick: () => {}, - }, - { - name: 'Make longer', - icon: () => , - onClick: () => {}, - }, - {name: 'Add humor'}, - {name: 'Make more professional'}, - {name: 'Make it: ...'}, - ], - }, - { - name: 'Translate', - children: [ - { - name: 'Afrikaans', - onClick: () => {}, - }, - { - name: 'Arabic', - onClick: () => {}, - }, - { - name: 'Bengali', - onClick: () => {}, - }, - { - name: 'Bulgarian', - onClick: () => {}, - }, - { - name: 'Catalan', - onClick: () => {}, - }, - { - name: 'Cantonese', - onClick: () => {}, - }, - { - name: 'Croatian', - onClick: () => {}, - }, - { - name: 'Czech', - onClick: () => {}, - }, - { - name: 'Danish', - onClick: () => {}, - }, - { - name: 'Dutch', - onClick: () => {}, - }, - { - name: 'Lithuanian', - onClick: () => {}, - }, - { - name: 'Malay', - onClick: () => {}, - }, - { - name: 'Malayalam', - onClick: () => {}, - }, - { - name: 'Panjabi', - onClick: () => {}, - }, - { - name: 'Tamil', - onClick: () => {}, - }, - { - name: 'English', - onClick: () => {}, - }, - { - name: 'Finnish', - onClick: () => {}, - }, - { - name: 'French', - onClick: () => {}, - }, - { - name: 'German', - onClick: () => {}, - }, - { - name: 'Greek', - onClick: () => {}, - }, - { - name: 'Hebrew', - onClick: () => {}, - }, - { - name: 'Hindi', - onClick: () => {}, - }, - { - name: 'Hungarian', - onClick: () => {}, - }, - { - name: 'Indonesian', - onClick: () => {}, - }, - { - name: 'Italian', - onClick: () => {}, - }, - { - name: 'Japanese', - onClick: () => {}, - }, - { - name: 'Javanese', - onClick: () => {}, - }, - { - name: 'Korean', - onClick: () => {}, - }, - { - name: 'Norwegian', - onClick: () => {}, - }, - { - name: 'Polish', - onClick: () => {}, - }, - { - name: 'Portuguese', - onClick: () => {}, - }, - { - name: 'Romanian', - onClick: () => {}, - }, - { - name: 'Russian', - onClick: () => {}, - }, - { - name: 'Serbian', - onClick: () => {}, - }, - { - name: 'Slovak', - onClick: () => {}, - }, - { - name: 'Slovene', - onClick: () => {}, - }, - { - name: 'Spanish', - onClick: () => {}, - }, - { - name: 'Swedish', - onClick: () => {}, - }, - { - name: 'Thai', - onClick: () => {}, - }, - { - name: 'Turkish', - onClick: () => {}, - }, - { - name: 'Ukrainian', - onClick: () => {}, - }, - { - name: 'Vietnamese', - onClick: () => {}, - }, - ], - }, - ], - }; -}; - -export const inlineText: MenuItem = { - name: 'Inline text', - maxToolbarItems: 4, - children: [ - { - name: 'Formatting', - expandChild: 0, - children: [ - { - name: 'Common', - expand: 8, - children: [ - { - name: 'Bold', - icon: () => , - // icon: () => , - right: () => ⌘ B, - keys: ['⌘', 'b'], - onClick: () => {}, - }, - { - name: 'Italic', - // icon: () => , - // icon: () => , - icon: () => , - right: () => ⌘ I, - keys: ['⌘', 'i'], - onClick: () => {}, - }, - { - name: 'Underline', - icon: () => , - right: () => ⌘ U, - keys: ['⌘', 'u'], - onClick: () => {}, - }, - { - name: 'Strikethrough', - // icon: () => , - icon: () => , - onClick: () => {}, - }, - { - name: 'Overline', - icon: () => , - onClick: () => {}, - }, - { - name: 'Highlight', - icon: () => , - onClick: () => {}, - }, - { - name: 'Classified', - icon: () => , - onClick: () => {}, - }, - ], - }, - { - name: 'Technical separator', - sep: true, - }, - { - name: 'Technical', - expand: 8, - children: [ - { - name: 'Code', - icon: () => , - onClick: () => {}, - }, - { - name: 'Math', - icon: () => , - onClick: () => {}, - }, - { - name: 'Superscript', - icon: () => , - onClick: () => {}, - }, - { - name: 'Subscript', - icon: () => , - onClick: () => {}, - }, - { - name: 'Keyboard key', - icon: () => , - onClick: () => {}, - }, - { - name: 'Insertion', - icon: () => , - onClick: () => {}, - }, - { - name: 'Deletion', - icon: () => , - onClick: () => {}, - }, - ], - }, - { - name: 'Artistic separator', - sep: true, - }, - { - name: 'Artistic', - expand: 8, - children: [ - { - name: 'Color', - icon: () => , - onClick: () => {}, - }, - { - name: 'Background', - icon: () => , - onClick: () => {}, - }, - { - name: 'Border', - icon: () => , - onClick: () => {}, - }, - ], - }, - ], - }, - secondBrain(), - { - name: 'Annotations separator', - sep: true, - }, - annotations(), - { - name: 'Style separator', - sep: true, - }, - { - name: 'Typesetting', - expand: 4, - openOnTitleHov: true, - icon: () => , - onClick: () => {}, - children: [ - { - name: 'Sans-serif', - iconBig: () => , - onClick: () => {}, - }, - { - name: 'Serif', - iconBig: () => , - onClick: () => {}, - }, - { - name: 'Slab', - icon: () => , - iconBig: () => , - onClick: () => {}, - }, - { - name: 'Monospace', - iconBig: () => , - onClick: () => {}, - }, - // { - // name: 'Custom typeface separator', - // sep: true, - // }, - { - name: 'Custom typeface', - expand: 10, - icon: () => , - children: [ - { - name: 'Typeface', - // icon: () => , - icon: () => , - onClick: () => {}, - }, - { - name: 'Text size', - icon: () => , - onClick: () => {}, - }, - { - name: 'Letter spacing', - icon: () => , - onClick: () => {}, - }, - { - name: 'Word spacing', - icon: () => , - onClick: () => {}, - }, - { - name: 'Caps separator', - sep: true, - }, - { - name: 'Large caps', - icon: () => , - onClick: () => {}, - }, - { - name: 'Small caps', - icon: () => , - onClick: () => {}, - }, - ], - }, - ], - }, - { - name: 'Modify separator', - sep: true, - }, - { - name: 'Modify', - expand: 3, - onClick: () => {}, - children: [ - { - name: 'Pick layer', - right: () => ( - - 9+ - - ), - more: true, - icon: () => , - onClick: () => {}, - }, - { - name: 'Erase formatting', - danger: true, - icon: () => , - onClick: () => {}, - }, - { - name: 'Delete all in range', - danger: true, - more: true, - icon: () => , - onClick: () => {}, - }, - ], - }, - { - name: 'Clipboard separator', - sep: true, - }, - { - name: 'Copy, cut, and paste', - // icon: () => , - icon: () => , - expand: 0, - children: [ - { - id: 'copy-menu', - name: 'Copy', - // icon: () => , - icon: () => , - expand: 5, - children: [ - { - name: 'Copy', - icon: () => , - onClick: () => {}, - }, - { - name: 'Copy text only', - icon: () => , - onClick: () => {}, - }, - { - name: 'Copy as Markdown', - icon: () => , - right: () => , - onClick: () => {}, - }, - { - name: 'Copy as HTML', - icon: () => , - right: () => , - onClick: () => {}, - }, - ], - }, - { - name: 'Cut separator', - sep: true, - }, - { - id: 'cut-menu', - name: 'Cut', - // icon: () => , - icon: () => , - expand: 5, - children: [ - { - name: 'Cut', - danger: true, - icon: () => , - onClick: () => {}, - }, - { - name: 'Cut text only', - danger: true, - icon: () => , - onClick: () => {}, - }, - { - name: 'Cut as Markdown', - danger: true, - icon: () => , - right: () => , - onClick: () => {}, - }, - { - name: 'Cut as HTML', - danger: true, - icon: () => , - right: () => , - onClick: () => {}, - }, - ], - }, - { - name: 'Paste separator', - sep: true, - }, - { - id: 'paste-menu', - name: 'Paste', - icon: () => , - expand: 5, - children: [ - { - name: 'Paste', - icon: () => , - onClick: () => {}, - }, - { - name: 'Paste text only', - icon: () => , - onClick: () => {}, - }, - { - name: 'Paste formatting', - icon: () => , - onClick: () => {}, - }, - ], - }, - ], - }, - { - name: 'Insert', - icon: () => , - children: [ - { - name: 'Smart chip', - icon: () => , - children: [ - { - name: 'Date', - icon: () => , - onClick: () => {}, - }, - { - name: 'AI chip', - icon: () => , - onClick: () => {}, - }, - { - name: 'Solana wallet', - icon: () => , - onClick: () => {}, - }, - { - name: 'Dropdown', - icon: () => , - children: [ - { - name: 'Create new', - icon: () => , - onClick: () => {}, - }, - { - name: 'Document dropdowns separator', - sep: true, - }, - { - name: 'Document dropdowns', - expand: 8, - onClick: () => {}, - children: [ - { - name: 'Configuration 1', - icon: () => , - onClick: () => {}, - }, - { - name: 'Configuration 2', - icon: () => , - onClick: () => {}, - }, - ], - }, - { - name: 'Presets dropdowns separator', - sep: true, - }, - { - name: 'Presets dropdowns', - expand: 8, - onClick: () => {}, - children: [ - { - name: 'Project status', - icon: () => , - onClick: () => {}, - }, - { - name: 'Review status', - icon: () => , - onClick: () => {}, - }, - ], - }, - ], - }, - ], - }, - { - name: 'Link', - // icon: () => , - icon: () => , - onClick: () => {}, - }, - { - name: 'Reference', - icon: () => , - onClick: () => {}, - }, - { - name: 'File', - icon: () => , - onClick: () => {}, - }, - { - name: 'Template', - text: 'building blocks', - icon: () => , - children: [ - { - name: 'Meeting notes', - onClick: () => {}, - }, - { - name: 'Email draft (created by AI)', - onClick: () => {}, - }, - { - name: 'Product roadmap', - onClick: () => {}, - }, - { - name: 'Review tracker', - onClick: () => {}, - }, - { - name: 'Project assets', - onClick: () => {}, - }, - { - name: 'Content launch tracker', - onClick: () => {}, - }, - ], - }, - { - name: 'On-screen keyboard', - icon: () => , - onClick: () => {}, - }, - { - name: 'Emoji', - icon: () => , - onClick: () => {}, - }, - { - name: 'Special characters', - icon: () => , - onClick: () => {}, - }, - { - name: 'Variable', - icon: () => , - onClick: () => {}, - }, - ], - }, - { - name: 'Developer tools', - danger: true, - icon: () => , - onClick: () => {}, - }, - ], -}; +import {inlineText} from '../menus/menus'; const introAnimation = keyframes({ from: { diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx index f89f2a97be..d23a8e1288 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx @@ -5,6 +5,7 @@ import {CaretToolbar} from './CaretToolbar'; import type {CaretViewProps} from '../../react/cursor/CaretView'; import {useToolbarPlugin} from './context'; import type {PeritextEventDetailMap} from '../../events/types'; +import {useSyncStore} from '../../react/hooks'; const height = 1.9; @@ -34,16 +35,17 @@ export interface RenderCaretProps extends CaretViewProps { export const RenderCaret: React.FC = ({children}) => { const {toolbar} = useToolbarPlugin()!; + const showCaretToolbar = useSyncStore(toolbar.showCaretToolbar); - const lastEventIsCaretPositionChange = - toolbar.lastEvent?.type === 'cursor' && - typeof (toolbar.lastEvent?.detail as PeritextEventDetailMap['cursor']).at === 'number'; + // const lastEventIsCaretPositionChange = + // toolbar.lastEvent?.type === 'cursor' && + // typeof (toolbar.lastEvent?.detail as PeritextEventDetailMap['cursor']).at === 'number'; return ( {children} - {lastEventIsCaretPositionChange && } + {showCaretToolbar && } ); diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/menus/menus.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/menus/menus.tsx new file mode 100644 index 0000000000..2dee9674c3 --- /dev/null +++ b/src/json-crdt-peritext-ui/plugins/toolbar/menus/menus.tsx @@ -0,0 +1,776 @@ +// biome-ignore lint: React is used for JSX +import * as React from 'react'; +import {Paper} from 'nice-ui/lib/4-card/Paper'; +import {Flex} from 'nice-ui/lib/3-list-item/Flex'; +import {BasicButton} from '../../../components/BasicButton'; +import {Sidetip} from 'nice-ui/lib/1-inline/Sidetip'; +import {Code} from 'nice-ui/lib/1-inline/Code'; +import {Iconista} from 'nice-ui/lib/icons/Iconista'; +import {ToolbarMenu} from 'nice-ui/lib/4-card/Toolbar/ToolbarMenu'; +import {FontStyleButton} from 'nice-ui/lib/2-inline-block/FontStyleButton'; +import {keyframes, rule} from 'nano-theme'; +import type {MenuItem} from 'nice-ui/lib/4-card/StructuralMenu/types'; + +export const annotations = (): MenuItem => { + return { + name: 'Annotations', + expand: 2, + children: [ + { + name: 'Link', + // icon: () => , + icon: () => , + onClick: () => {}, + }, + { + name: 'Comment', + icon: () => , + onClick: () => {}, + }, + { + name: 'Bookmark', + icon: () => , + onClick: () => {}, + }, + { + name: 'Footnote', + icon: () => , + onClick: () => {}, + }, + { + name: 'Aside', + icon: () => , + onClick: () => {}, + }, + ], + }; +}; + +export const secondBrain = (): MenuItem => { + return { + sepBefore: true, + name: 'Second brain', + display: () => ( + <> + Second brain + {/*   + AI */} + + ), + right: () => {'AI'}, + icon: () => , + children: [ + { + name: 'Ask question', + }, + { + name: 'Action', + children: [ + { + name: 'Make shorter', + icon: () => , + onClick: () => {}, + }, + { + name: 'Make longer', + icon: () => , + onClick: () => {}, + }, + {name: 'Add humor'}, + {name: 'Make more professional'}, + {name: 'Make it: ...'}, + ], + }, + { + name: 'Translate', + children: [ + { + name: 'Afrikaans', + onClick: () => {}, + }, + { + name: 'Arabic', + onClick: () => {}, + }, + { + name: 'Bengali', + onClick: () => {}, + }, + { + name: 'Bulgarian', + onClick: () => {}, + }, + { + name: 'Catalan', + onClick: () => {}, + }, + { + name: 'Cantonese', + onClick: () => {}, + }, + { + name: 'Croatian', + onClick: () => {}, + }, + { + name: 'Czech', + onClick: () => {}, + }, + { + name: 'Danish', + onClick: () => {}, + }, + { + name: 'Dutch', + onClick: () => {}, + }, + { + name: 'Lithuanian', + onClick: () => {}, + }, + { + name: 'Malay', + onClick: () => {}, + }, + { + name: 'Malayalam', + onClick: () => {}, + }, + { + name: 'Panjabi', + onClick: () => {}, + }, + { + name: 'Tamil', + onClick: () => {}, + }, + { + name: 'English', + onClick: () => {}, + }, + { + name: 'Finnish', + onClick: () => {}, + }, + { + name: 'French', + onClick: () => {}, + }, + { + name: 'German', + onClick: () => {}, + }, + { + name: 'Greek', + onClick: () => {}, + }, + { + name: 'Hebrew', + onClick: () => {}, + }, + { + name: 'Hindi', + onClick: () => {}, + }, + { + name: 'Hungarian', + onClick: () => {}, + }, + { + name: 'Indonesian', + onClick: () => {}, + }, + { + name: 'Italian', + onClick: () => {}, + }, + { + name: 'Japanese', + onClick: () => {}, + }, + { + name: 'Javanese', + onClick: () => {}, + }, + { + name: 'Korean', + onClick: () => {}, + }, + { + name: 'Norwegian', + onClick: () => {}, + }, + { + name: 'Polish', + onClick: () => {}, + }, + { + name: 'Portuguese', + onClick: () => {}, + }, + { + name: 'Romanian', + onClick: () => {}, + }, + { + name: 'Russian', + onClick: () => {}, + }, + { + name: 'Serbian', + onClick: () => {}, + }, + { + name: 'Slovak', + onClick: () => {}, + }, + { + name: 'Slovene', + onClick: () => {}, + }, + { + name: 'Spanish', + onClick: () => {}, + }, + { + name: 'Swedish', + onClick: () => {}, + }, + { + name: 'Thai', + onClick: () => {}, + }, + { + name: 'Turkish', + onClick: () => {}, + }, + { + name: 'Ukrainian', + onClick: () => {}, + }, + { + name: 'Vietnamese', + onClick: () => {}, + }, + ], + }, + ], + }; +}; + +export const inlineText: MenuItem = { + name: 'Inline text', + maxToolbarItems: 4, + children: [ + { + name: 'Formatting', + expandChild: 0, + children: [ + { + name: 'Common', + expand: 8, + children: [ + { + name: 'Bold', + icon: () => , + // icon: () => , + right: () => ⌘ B, + keys: ['⌘', 'b'], + onClick: () => {}, + }, + { + name: 'Italic', + // icon: () => , + // icon: () => , + icon: () => , + right: () => ⌘ I, + keys: ['⌘', 'i'], + onClick: () => {}, + }, + { + name: 'Underline', + icon: () => , + right: () => ⌘ U, + keys: ['⌘', 'u'], + onClick: () => {}, + }, + { + name: 'Strikethrough', + // icon: () => , + icon: () => , + onClick: () => {}, + }, + { + name: 'Overline', + icon: () => , + onClick: () => {}, + }, + { + name: 'Highlight', + icon: () => , + onClick: () => {}, + }, + { + name: 'Classified', + icon: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Technical separator', + sep: true, + }, + { + name: 'Technical', + expand: 8, + children: [ + { + name: 'Code', + icon: () => , + onClick: () => {}, + }, + { + name: 'Math', + icon: () => , + onClick: () => {}, + }, + { + name: 'Superscript', + icon: () => , + onClick: () => {}, + }, + { + name: 'Subscript', + icon: () => , + onClick: () => {}, + }, + { + name: 'Keyboard key', + icon: () => , + onClick: () => {}, + }, + { + name: 'Insertion', + icon: () => , + onClick: () => {}, + }, + { + name: 'Deletion', + icon: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Artistic separator', + sep: true, + }, + { + name: 'Artistic', + expand: 8, + children: [ + { + name: 'Color', + icon: () => , + onClick: () => {}, + }, + { + name: 'Background', + icon: () => , + onClick: () => {}, + }, + { + name: 'Border', + icon: () => , + onClick: () => {}, + }, + ], + }, + ], + }, + secondBrain(), + { + name: 'Annotations separator', + sep: true, + }, + annotations(), + { + name: 'Style separator', + sep: true, + }, + { + name: 'Typesetting', + expand: 4, + openOnTitleHov: true, + icon: () => , + onClick: () => {}, + children: [ + { + name: 'Sans-serif', + iconBig: () => , + onClick: () => {}, + }, + { + name: 'Serif', + iconBig: () => , + onClick: () => {}, + }, + { + name: 'Slab', + icon: () => , + iconBig: () => , + onClick: () => {}, + }, + { + name: 'Monospace', + iconBig: () => , + onClick: () => {}, + }, + // { + // name: 'Custom typeface separator', + // sep: true, + // }, + { + name: 'Custom typeface', + expand: 10, + icon: () => , + children: [ + { + name: 'Typeface', + // icon: () => , + icon: () => , + onClick: () => {}, + }, + { + name: 'Text size', + icon: () => , + onClick: () => {}, + }, + { + name: 'Letter spacing', + icon: () => , + onClick: () => {}, + }, + { + name: 'Word spacing', + icon: () => , + onClick: () => {}, + }, + { + name: 'Caps separator', + sep: true, + }, + { + name: 'Large caps', + icon: () => , + onClick: () => {}, + }, + { + name: 'Small caps', + icon: () => , + onClick: () => {}, + }, + ], + }, + ], + }, + { + name: 'Modify separator', + sep: true, + }, + { + name: 'Modify', + expand: 3, + onClick: () => {}, + children: [ + { + name: 'Pick layer', + right: () => ( + + 9+ + + ), + more: true, + icon: () => , + onClick: () => {}, + }, + { + name: 'Erase formatting', + danger: true, + icon: () => , + onClick: () => {}, + }, + { + name: 'Delete all in range', + danger: true, + more: true, + icon: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Clipboard separator', + sep: true, + }, + { + name: 'Copy, cut, and paste', + // icon: () => , + icon: () => , + expand: 0, + children: [ + { + id: 'copy-menu', + name: 'Copy', + // icon: () => , + icon: () => , + expand: 5, + children: [ + { + name: 'Copy', + icon: () => , + onClick: () => {}, + }, + { + name: 'Copy text only', + icon: () => , + onClick: () => {}, + }, + { + name: 'Copy as Markdown', + icon: () => , + right: () => , + onClick: () => {}, + }, + { + name: 'Copy as HTML', + icon: () => , + right: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Cut separator', + sep: true, + }, + { + id: 'cut-menu', + name: 'Cut', + // icon: () => , + icon: () => , + expand: 5, + children: [ + { + name: 'Cut', + danger: true, + icon: () => , + onClick: () => {}, + }, + { + name: 'Cut text only', + danger: true, + icon: () => , + onClick: () => {}, + }, + { + name: 'Cut as Markdown', + danger: true, + icon: () => , + right: () => , + onClick: () => {}, + }, + { + name: 'Cut as HTML', + danger: true, + icon: () => , + right: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Paste separator', + sep: true, + }, + { + id: 'paste-menu', + name: 'Paste', + icon: () => , + expand: 5, + children: [ + { + name: 'Paste', + icon: () => , + onClick: () => {}, + }, + { + name: 'Paste text only', + icon: () => , + onClick: () => {}, + }, + { + name: 'Paste formatting', + icon: () => , + onClick: () => {}, + }, + ], + }, + ], + }, + { + name: 'Insert', + icon: () => , + children: [ + { + name: 'Smart chip', + icon: () => , + children: [ + { + name: 'Date', + icon: () => , + onClick: () => {}, + }, + { + name: 'AI chip', + icon: () => , + onClick: () => {}, + }, + { + name: 'Solana wallet', + icon: () => , + onClick: () => {}, + }, + { + name: 'Dropdown', + icon: () => , + children: [ + { + name: 'Create new', + icon: () => , + onClick: () => {}, + }, + { + name: 'Document dropdowns separator', + sep: true, + }, + { + name: 'Document dropdowns', + expand: 8, + onClick: () => {}, + children: [ + { + name: 'Configuration 1', + icon: () => , + onClick: () => {}, + }, + { + name: 'Configuration 2', + icon: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Presets dropdowns separator', + sep: true, + }, + { + name: 'Presets dropdowns', + expand: 8, + onClick: () => {}, + children: [ + { + name: 'Project status', + icon: () => , + onClick: () => {}, + }, + { + name: 'Review status', + icon: () => , + onClick: () => {}, + }, + ], + }, + ], + }, + ], + }, + { + name: 'Link', + // icon: () => , + icon: () => , + onClick: () => {}, + }, + { + name: 'Reference', + icon: () => , + onClick: () => {}, + }, + { + name: 'File', + icon: () => , + onClick: () => {}, + }, + { + name: 'Template', + text: 'building blocks', + icon: () => , + children: [ + { + name: 'Meeting notes', + onClick: () => {}, + }, + { + name: 'Email draft (created by AI)', + onClick: () => {}, + }, + { + name: 'Product roadmap', + onClick: () => {}, + }, + { + name: 'Review tracker', + onClick: () => {}, + }, + { + name: 'Project assets', + onClick: () => {}, + }, + { + name: 'Content launch tracker', + onClick: () => {}, + }, + ], + }, + { + name: 'On-screen keyboard', + icon: () => , + onClick: () => {}, + }, + { + name: 'Emoji', + icon: () => , + onClick: () => {}, + }, + { + name: 'Special characters', + icon: () => , + onClick: () => {}, + }, + { + name: 'Variable', + icon: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Developer tools', + danger: true, + icon: () => , + onClick: () => {}, + }, + ], +}; diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/state.ts b/src/json-crdt-peritext-ui/plugins/toolbar/state.ts index f716163125..cc4b134fc6 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/state.ts +++ b/src/json-crdt-peritext-ui/plugins/toolbar/state.ts @@ -1,9 +1,11 @@ +import {ValueSyncStore} from '../../../util/events/sync-store'; import type {UiLifeCyclesRender} from '../../dom/types'; import type {PeritextEventDetailMap} from '../../events/types'; import type {PeritextSurfaceState} from '../../react'; export class ToolbarState implements UiLifeCyclesRender { public lastEvent: PeritextEventDetailMap['change']['ev'] | undefined = void 0; + public readonly showCaretToolbar = new ValueSyncStore(true); constructor(public readonly surface: PeritextSurfaceState) {} From 26ccfab5fc9ebc4ecbd044c3c04b0da1aed30a72 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 18 Jan 2025 19:27:16 +0100 Subject: [PATCH 03/30] =?UTF-8?q?feat(json-crdt-peritext-ui):=20?= =?UTF-8?q?=F0=9F=8E=B8=20improve=20caret=20toolbar=20event=20tracking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/toolbar/RenderCaret.tsx | 23 ++++++++++++------- .../plugins/toolbar/state.ts | 10 ++++++-- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx index d23a8e1288..826437e6ab 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx @@ -1,11 +1,12 @@ // biome-ignore lint: React is used for JSX import * as React from 'react'; import {rule} from 'nano-theme'; -import {CaretToolbar} from './CaretToolbar'; -import type {CaretViewProps} from '../../react/cursor/CaretView'; +import {CaretToolbar} from 'nice-ui/lib/4-card/Toolbar/ToolbarMenu/CaretToolbar'; import {useToolbarPlugin} from './context'; -import type {PeritextEventDetailMap} from '../../events/types'; import {useSyncStore} from '../../react/hooks'; +import type {CaretViewProps} from '../../react/cursor/CaretView'; +import type {PeritextEventDetailMap} from '../../events/types'; +import {inlineText} from './menus/menus'; const height = 1.9; @@ -35,17 +36,23 @@ export interface RenderCaretProps extends CaretViewProps { export const RenderCaret: React.FC = ({children}) => { const {toolbar} = useToolbarPlugin()!; - const showCaretToolbar = useSyncStore(toolbar.showCaretToolbar); + const showCaretToolbar = toolbar.showCaretToolbar; + const showCaretToolbarValue = useSyncStore(showCaretToolbar); - // const lastEventIsCaretPositionChange = - // toolbar.lastEvent?.type === 'cursor' && - // typeof (toolbar.lastEvent?.detail as PeritextEventDetailMap['cursor']).at === 'number'; + const handleClose = React.useCallback(() => { + setTimeout(() => { + if (showCaretToolbar.value) showCaretToolbar.next(false); + }, 5); + }, []); return ( {children} - {showCaretToolbar && } + {/* {showCaretToolbar && } */} + {showCaretToolbarValue && ( + + )} ); diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/state.ts b/src/json-crdt-peritext-ui/plugins/toolbar/state.ts index cc4b134fc6..3deb7027f3 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/state.ts +++ b/src/json-crdt-peritext-ui/plugins/toolbar/state.ts @@ -5,7 +5,7 @@ import type {PeritextSurfaceState} from '../../react'; export class ToolbarState implements UiLifeCyclesRender { public lastEvent: PeritextEventDetailMap['change']['ev'] | undefined = void 0; - public readonly showCaretToolbar = new ValueSyncStore(true); + public readonly showCaretToolbar = new ValueSyncStore(false); constructor(public readonly surface: PeritextSurfaceState) {} @@ -14,7 +14,13 @@ export class ToolbarState implements UiLifeCyclesRender { public start() { const et = this.surface.events.et; const changeUnsubscribe = et.subscribe('change', (ev) => { - this.lastEvent = ev.detail.ev; + const lastEvent = ev.detail.ev; + this.lastEvent = lastEvent; + const lastEventIsCaretPositionChange = + lastEvent?.type === 'cursor' && + typeof (lastEvent?.detail as PeritextEventDetailMap['cursor']).at === 'number'; + if (lastEventIsCaretPositionChange) + this.showCaretToolbar.next(true); }); return () => { changeUnsubscribe(); From 79b0b72fcaa73fe135072887f9bd0a88658b0778 Mon Sep 17 00:00:00 2001 From: streamich Date: Mon, 20 Jan 2025 14:11:50 +0100 Subject: [PATCH 04/30] =?UTF-8?q?refactor(json-crdt-peritext-ui):=20?= =?UTF-8?q?=F0=9F=92=A1=20improve=20menu=20construction=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/toolbar/RenderCaret.tsx | 4 +- .../plugins/toolbar/state.ts | 29 - .../plugins/toolbar/state/index.tsx | 564 ++++++++++++++++++ .../toolbar/{menus => state}/menus.tsx | 134 +---- .../plugins/toolbar/types.ts | 3 + 5 files changed, 569 insertions(+), 165 deletions(-) delete mode 100644 src/json-crdt-peritext-ui/plugins/toolbar/state.ts create mode 100644 src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx rename src/json-crdt-peritext-ui/plugins/toolbar/{menus => state}/menus.tsx (79%) create mode 100644 src/json-crdt-peritext-ui/plugins/toolbar/types.ts diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx index 826437e6ab..6a8d21578d 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx @@ -5,8 +5,6 @@ import {CaretToolbar} from 'nice-ui/lib/4-card/Toolbar/ToolbarMenu/CaretToolbar' import {useToolbarPlugin} from './context'; import {useSyncStore} from '../../react/hooks'; import type {CaretViewProps} from '../../react/cursor/CaretView'; -import type {PeritextEventDetailMap} from '../../events/types'; -import {inlineText} from './menus/menus'; const height = 1.9; @@ -51,7 +49,7 @@ export const RenderCaret: React.FC = ({children}) => { {/* {showCaretToolbar && } */} {showCaretToolbarValue && ( - + )} diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/state.ts b/src/json-crdt-peritext-ui/plugins/toolbar/state.ts deleted file mode 100644 index 3deb7027f3..0000000000 --- a/src/json-crdt-peritext-ui/plugins/toolbar/state.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {ValueSyncStore} from '../../../util/events/sync-store'; -import type {UiLifeCyclesRender} from '../../dom/types'; -import type {PeritextEventDetailMap} from '../../events/types'; -import type {PeritextSurfaceState} from '../../react'; - -export class ToolbarState implements UiLifeCyclesRender { - public lastEvent: PeritextEventDetailMap['change']['ev'] | undefined = void 0; - public readonly showCaretToolbar = new ValueSyncStore(false); - - constructor(public readonly surface: PeritextSurfaceState) {} - - /** -------------------------------------------------- {@link UiLifeCyclesRender} */ - - public start() { - const et = this.surface.events.et; - const changeUnsubscribe = et.subscribe('change', (ev) => { - const lastEvent = ev.detail.ev; - this.lastEvent = lastEvent; - const lastEventIsCaretPositionChange = - lastEvent?.type === 'cursor' && - typeof (lastEvent?.detail as PeritextEventDetailMap['cursor']).at === 'number'; - if (lastEventIsCaretPositionChange) - this.showCaretToolbar.next(true); - }); - return () => { - changeUnsubscribe(); - }; - } -} diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx new file mode 100644 index 0000000000..d8384efea5 --- /dev/null +++ b/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx @@ -0,0 +1,564 @@ +// biome-ignore lint: React is used for JSX +import * as React from 'react'; +import {Sidetip} from 'nice-ui/lib/1-inline/Sidetip'; +import {Iconista} from 'nice-ui/lib/icons/Iconista'; +import {ValueSyncStore} from '../../../../util/events/sync-store'; +import {annotations, secondBrain} from './menus'; +import {Code} from 'nice-ui/lib/1-inline/Code'; +import {FontStyleButton} from 'nice-ui/lib/2-inline-block/FontStyleButton'; +import type {UiLifeCyclesRender} from '../../../dom/types'; +import type {PeritextEventDetailMap} from '../../../events/types'; +import type {PeritextSurfaceState} from '../../../react'; +import type {MenuItem} from '../types'; + +export class ToolbarState implements UiLifeCyclesRender { + public lastEvent: PeritextEventDetailMap['change']['ev'] | undefined = void 0; + public readonly showCaretToolbar = new ValueSyncStore(false); + + constructor(public readonly surface: PeritextSurfaceState) {} + + /** -------------------------------------------------- {@link UiLifeCyclesRender} */ + + public start() { + const et = this.surface.events.et; + const changeUnsubscribe = et.subscribe('change', (ev) => { + const lastEvent = ev.detail.ev; + this.lastEvent = lastEvent; + const lastEventIsCaretPositionChange = + lastEvent?.type === 'cursor' && + typeof (lastEvent?.detail as PeritextEventDetailMap['cursor']).at === 'number'; + if (lastEventIsCaretPositionChange) + this.showCaretToolbar.next(true); + }); + return () => { + changeUnsubscribe(); + }; + } + + // -------------------------------------------------------------------- Menus + + public readonly getFormattingMenu = (): MenuItem => { + return ( + { + name: 'Formatting', + expandChild: 0, + children: [ + { + name: 'Common', + expand: 8, + children: [ + { + name: 'Bold', + icon: () => , + // icon: () => , + right: () => ⌘ B, + keys: ['⌘', 'b'], + onClick: () => {}, + }, + { + name: 'Italic', + // icon: () => , + // icon: () => , + icon: () => , + right: () => ⌘ I, + keys: ['⌘', 'i'], + onClick: () => {}, + }, + { + name: 'Underline', + icon: () => , + right: () => ⌘ U, + keys: ['⌘', 'u'], + onClick: () => {}, + }, + { + name: 'Strikethrough', + // icon: () => , + icon: () => , + onClick: () => {}, + }, + { + name: 'Overline', + icon: () => , + onClick: () => {}, + }, + { + name: 'Highlight', + icon: () => , + onClick: () => {}, + }, + { + name: 'Classified', + icon: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Technical separator', + sep: true, + }, + { + name: 'Technical', + expand: 8, + children: [ + { + name: 'Code', + icon: () => , + onClick: () => {}, + }, + { + name: 'Math', + icon: () => , + onClick: () => {}, + }, + { + name: 'Superscript', + icon: () => , + onClick: () => {}, + }, + { + name: 'Subscript', + icon: () => , + onClick: () => {}, + }, + { + name: 'Keyboard key', + icon: () => , + onClick: () => {}, + }, + { + name: 'Insertion', + icon: () => , + onClick: () => {}, + }, + { + name: 'Deletion', + icon: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Artistic separator', + sep: true, + }, + { + name: 'Artistic', + expand: 8, + children: [ + { + name: 'Color', + icon: () => , + onClick: () => {}, + }, + { + name: 'Background', + icon: () => , + onClick: () => {}, + }, + { + name: 'Border', + icon: () => , + onClick: () => {}, + }, + ], + }, + ], + } + ); + }; + + public readonly getCaretMenu = (): MenuItem => { + return { + name: 'Inline text', + maxToolbarItems: 4, + children: [ + this.getFormattingMenu(), + secondBrain(), + { + name: 'Annotations separator', + sep: true, + }, + annotations(), + { + name: 'Style separator', + sep: true, + }, + { + name: 'Typesetting', + expand: 4, + openOnTitleHov: true, + icon: () => , + onClick: () => {}, + children: [ + { + name: 'Sans-serif', + iconBig: () => , + onClick: () => {}, + }, + { + name: 'Serif', + iconBig: () => , + onClick: () => {}, + }, + { + name: 'Slab', + icon: () => , + iconBig: () => , + onClick: () => {}, + }, + { + name: 'Monospace', + iconBig: () => , + onClick: () => {}, + }, + // { + // name: 'Custom typeface separator', + // sep: true, + // }, + { + name: 'Custom typeface', + expand: 10, + icon: () => , + children: [ + { + name: 'Typeface', + // icon: () => , + icon: () => , + onClick: () => {}, + }, + { + name: 'Text size', + icon: () => , + onClick: () => {}, + }, + { + name: 'Letter spacing', + icon: () => , + onClick: () => {}, + }, + { + name: 'Word spacing', + icon: () => , + onClick: () => {}, + }, + { + name: 'Caps separator', + sep: true, + }, + { + name: 'Large caps', + icon: () => , + onClick: () => {}, + }, + { + name: 'Small caps', + icon: () => , + onClick: () => {}, + }, + ], + }, + ], + }, + { + name: 'Modify separator', + sep: true, + }, + { + name: 'Modify', + expand: 3, + onClick: () => {}, + children: [ + { + name: 'Pick layer', + right: () => ( + + 9+ + + ), + more: true, + icon: () => , + onClick: () => {}, + }, + { + name: 'Erase formatting', + danger: true, + icon: () => , + onClick: () => {}, + }, + { + name: 'Delete all in range', + danger: true, + more: true, + icon: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Clipboard separator', + sep: true, + }, + { + name: 'Copy, cut, and paste', + // icon: () => , + icon: () => , + expand: 0, + children: [ + { + id: 'copy-menu', + name: 'Copy', + // icon: () => , + icon: () => , + expand: 5, + children: [ + { + name: 'Copy', + icon: () => , + onClick: () => {}, + }, + { + name: 'Copy text only', + icon: () => , + onClick: () => {}, + }, + { + name: 'Copy as Markdown', + icon: () => , + right: () => , + onClick: () => {}, + }, + { + name: 'Copy as HTML', + icon: () => , + right: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Cut separator', + sep: true, + }, + { + id: 'cut-menu', + name: 'Cut', + // icon: () => , + icon: () => , + expand: 5, + children: [ + { + name: 'Cut', + danger: true, + icon: () => , + onClick: () => {}, + }, + { + name: 'Cut text only', + danger: true, + icon: () => , + onClick: () => {}, + }, + { + name: 'Cut as Markdown', + danger: true, + icon: () => , + right: () => , + onClick: () => {}, + }, + { + name: 'Cut as HTML', + danger: true, + icon: () => , + right: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Paste separator', + sep: true, + }, + { + id: 'paste-menu', + name: 'Paste', + icon: () => , + expand: 5, + children: [ + { + name: 'Paste', + icon: () => , + onClick: () => {}, + }, + { + name: 'Paste text only', + icon: () => , + onClick: () => {}, + }, + { + name: 'Paste formatting', + icon: () => , + onClick: () => {}, + }, + ], + }, + ], + }, + { + name: 'Insert', + icon: () => , + children: [ + { + name: 'Smart chip', + icon: () => , + children: [ + { + name: 'Date', + icon: () => , + onClick: () => {}, + }, + { + name: 'AI chip', + icon: () => , + onClick: () => {}, + }, + { + name: 'Solana wallet', + icon: () => , + onClick: () => {}, + }, + { + name: 'Dropdown', + icon: () => , + children: [ + { + name: 'Create new', + icon: () => , + onClick: () => {}, + }, + { + name: 'Document dropdowns separator', + sep: true, + }, + { + name: 'Document dropdowns', + expand: 8, + onClick: () => {}, + children: [ + { + name: 'Configuration 1', + icon: () => , + onClick: () => {}, + }, + { + name: 'Configuration 2', + icon: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Presets dropdowns separator', + sep: true, + }, + { + name: 'Presets dropdowns', + expand: 8, + onClick: () => {}, + children: [ + { + name: 'Project status', + icon: () => , + onClick: () => {}, + }, + { + name: 'Review status', + icon: () => , + onClick: () => {}, + }, + ], + }, + ], + }, + ], + }, + { + name: 'Link', + // icon: () => , + icon: () => , + onClick: () => {}, + }, + { + name: 'Reference', + icon: () => , + onClick: () => {}, + }, + { + name: 'File', + icon: () => , + onClick: () => {}, + }, + { + name: 'Template', + text: 'building blocks', + icon: () => , + children: [ + { + name: 'Meeting notes', + onClick: () => {}, + }, + { + name: 'Email draft (created by AI)', + onClick: () => {}, + }, + { + name: 'Product roadmap', + onClick: () => {}, + }, + { + name: 'Review tracker', + onClick: () => {}, + }, + { + name: 'Project assets', + onClick: () => {}, + }, + { + name: 'Content launch tracker', + onClick: () => {}, + }, + ], + }, + { + name: 'On-screen keyboard', + icon: () => , + onClick: () => {}, + }, + { + name: 'Emoji', + icon: () => , + onClick: () => {}, + }, + { + name: 'Special characters', + icon: () => , + onClick: () => {}, + }, + { + name: 'Variable', + icon: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Developer tools', + danger: true, + icon: () => , + onClick: () => {}, + }, + ], + }; + }; +} diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/menus/menus.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/state/menus.tsx similarity index 79% rename from src/json-crdt-peritext-ui/plugins/toolbar/menus/menus.tsx rename to src/json-crdt-peritext-ui/plugins/toolbar/state/menus.tsx index 2dee9674c3..157228aff6 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/menus/menus.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/state/menus.tsx @@ -1,15 +1,10 @@ // biome-ignore lint: React is used for JSX import * as React from 'react'; -import {Paper} from 'nice-ui/lib/4-card/Paper'; -import {Flex} from 'nice-ui/lib/3-list-item/Flex'; -import {BasicButton} from '../../../components/BasicButton'; import {Sidetip} from 'nice-ui/lib/1-inline/Sidetip'; import {Code} from 'nice-ui/lib/1-inline/Code'; import {Iconista} from 'nice-ui/lib/icons/Iconista'; -import {ToolbarMenu} from 'nice-ui/lib/4-card/Toolbar/ToolbarMenu'; import {FontStyleButton} from 'nice-ui/lib/2-inline-block/FontStyleButton'; -import {keyframes, rule} from 'nano-theme'; -import type {MenuItem} from 'nice-ui/lib/4-card/StructuralMenu/types'; +import type {MenuItem} from '../types'; export const annotations = (): MenuItem => { return { @@ -262,133 +257,6 @@ export const inlineText: MenuItem = { name: 'Inline text', maxToolbarItems: 4, children: [ - { - name: 'Formatting', - expandChild: 0, - children: [ - { - name: 'Common', - expand: 8, - children: [ - { - name: 'Bold', - icon: () => , - // icon: () => , - right: () => ⌘ B, - keys: ['⌘', 'b'], - onClick: () => {}, - }, - { - name: 'Italic', - // icon: () => , - // icon: () => , - icon: () => , - right: () => ⌘ I, - keys: ['⌘', 'i'], - onClick: () => {}, - }, - { - name: 'Underline', - icon: () => , - right: () => ⌘ U, - keys: ['⌘', 'u'], - onClick: () => {}, - }, - { - name: 'Strikethrough', - // icon: () => , - icon: () => , - onClick: () => {}, - }, - { - name: 'Overline', - icon: () => , - onClick: () => {}, - }, - { - name: 'Highlight', - icon: () => , - onClick: () => {}, - }, - { - name: 'Classified', - icon: () => , - onClick: () => {}, - }, - ], - }, - { - name: 'Technical separator', - sep: true, - }, - { - name: 'Technical', - expand: 8, - children: [ - { - name: 'Code', - icon: () => , - onClick: () => {}, - }, - { - name: 'Math', - icon: () => , - onClick: () => {}, - }, - { - name: 'Superscript', - icon: () => , - onClick: () => {}, - }, - { - name: 'Subscript', - icon: () => , - onClick: () => {}, - }, - { - name: 'Keyboard key', - icon: () => , - onClick: () => {}, - }, - { - name: 'Insertion', - icon: () => , - onClick: () => {}, - }, - { - name: 'Deletion', - icon: () => , - onClick: () => {}, - }, - ], - }, - { - name: 'Artistic separator', - sep: true, - }, - { - name: 'Artistic', - expand: 8, - children: [ - { - name: 'Color', - icon: () => , - onClick: () => {}, - }, - { - name: 'Background', - icon: () => , - onClick: () => {}, - }, - { - name: 'Border', - icon: () => , - onClick: () => {}, - }, - ], - }, - ], - }, secondBrain(), { name: 'Annotations separator', diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/types.ts b/src/json-crdt-peritext-ui/plugins/toolbar/types.ts new file mode 100644 index 0000000000..6eea13da85 --- /dev/null +++ b/src/json-crdt-peritext-ui/plugins/toolbar/types.ts @@ -0,0 +1,3 @@ +import type {MenuItem} from 'nice-ui/lib/4-card/StructuralMenu/types'; + +export {MenuItem}; From 478ed93a03eebe5e6a20cb283500b86b7d5a9017 Mon Sep 17 00:00:00 2001 From: streamich Date: Mon, 20 Jan 2025 15:44:38 +0100 Subject: [PATCH 05/30] =?UTF-8?q?feat(json-crdt-peritext-ui):=20?= =?UTF-8?q?=F0=9F=8E=B8=20setup=20selection=20above-focus=20rendering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/toolbar/RenderCaret.tsx | 4 - .../plugins/toolbar/RenderFocus.tsx | 49 +++ .../plugins/toolbar/ToolbarPlugin.ts | 2 + .../plugins/toolbar/state/index.tsx | 393 ++++++++++++++++++ 4 files changed, 444 insertions(+), 4 deletions(-) create mode 100644 src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx index 6a8d21578d..2d60b41fbc 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx @@ -12,7 +12,6 @@ const blockClass = rule({ pos: 'relative', w: '0px', h: '100%', - bg: 'black', va: 'bottom', }); @@ -23,9 +22,6 @@ const overClass = rule({ isolation: 'isolate', us: 'none', transform: 'translateX(calc(-50% + 0px))', - // w: '1px', - // h: '1px', - // bd: '1px solid red', }); export interface RenderCaretProps extends CaretViewProps { diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx new file mode 100644 index 0000000000..2efca5bbaa --- /dev/null +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx @@ -0,0 +1,49 @@ +// biome-ignore lint: React is used for JSX +import * as React from 'react'; +import {rule} from 'nano-theme'; +import {useToolbarPlugin} from './context'; +import {useSyncStore} from '../../react/hooks'; +import type {CaretViewProps} from '../../react/cursor/CaretView'; + +const height = 1.9; + +const blockClass = rule({ + pos: 'relative', + w: '0px', + h: '100%', + va: 'bottom', +}); + +const overClass = rule({ + pos: 'absolute', + b: `${height}em`, + l: 0, + isolation: 'isolate', + us: 'none', + transform: 'translateX(calc(-50% + 0px))', +}); + +export interface RenderFocusProps extends CaretViewProps { + children: React.ReactNode; +} + +export const RenderFocus: React.FC = ({children}) => { + const {toolbar} = useToolbarPlugin()!; + // const showCaretToolbar = toolbar.showCaretToolbar; + // const showCaretToolbarValue = useSyncStore(showCaretToolbar); + + // const handleClose = React.useCallback(() => { + // setTimeout(() => { + // if (showCaretToolbar.value) showCaretToolbar.next(false); + // }, 5); + // }, []); + + return ( + + {children} + + focus bar + + + ); +}; diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/ToolbarPlugin.ts b/src/json-crdt-peritext-ui/plugins/toolbar/ToolbarPlugin.ts index d36394952e..8d1043b77b 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/ToolbarPlugin.ts +++ b/src/json-crdt-peritext-ui/plugins/toolbar/ToolbarPlugin.ts @@ -4,6 +4,7 @@ import {RenderPeritext} from './RenderPeritext'; import {text} from '../minimal/text'; import {RenderBlock} from './RenderBlock'; import {RenderCaret} from './RenderCaret'; +import {RenderFocus} from './RenderFocus'; import type {PeritextPlugin} from '../../react/types'; const h = React.createElement; @@ -19,4 +20,5 @@ export class ToolbarPlugin implements PeritextPlugin { h(RenderPeritext, {...props, children, surface}); public readonly caret: PeritextPlugin['caret'] = (props, children) => h(RenderCaret, props, children); + public readonly focus: PeritextPlugin['focus'] = (props, children) => h(RenderFocus, props, children); } 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 d8384efea5..0b34484853 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx @@ -561,4 +561,397 @@ export class ToolbarState implements UiLifeCyclesRender { ], }; }; + + public readonly getSelectionMenu = (): MenuItem => { + return { + name: 'Inline text', + maxToolbarItems: 4, + children: [ + this.getFormattingMenu(), + secondBrain(), + { + name: 'Annotations separator', + sep: true, + }, + annotations(), + { + name: 'Style separator', + sep: true, + }, + { + name: 'Typesetting', + expand: 4, + openOnTitleHov: true, + icon: () => , + onClick: () => {}, + children: [ + { + name: 'Sans-serif', + iconBig: () => , + onClick: () => {}, + }, + { + name: 'Serif', + iconBig: () => , + onClick: () => {}, + }, + { + name: 'Slab', + icon: () => , + iconBig: () => , + onClick: () => {}, + }, + { + name: 'Monospace', + iconBig: () => , + onClick: () => {}, + }, + // { + // name: 'Custom typeface separator', + // sep: true, + // }, + { + name: 'Custom typeface', + expand: 10, + icon: () => , + children: [ + { + name: 'Typeface', + // icon: () => , + icon: () => , + onClick: () => {}, + }, + { + name: 'Text size', + icon: () => , + onClick: () => {}, + }, + { + name: 'Letter spacing', + icon: () => , + onClick: () => {}, + }, + { + name: 'Word spacing', + icon: () => , + onClick: () => {}, + }, + { + name: 'Caps separator', + sep: true, + }, + { + name: 'Large caps', + icon: () => , + onClick: () => {}, + }, + { + name: 'Small caps', + icon: () => , + onClick: () => {}, + }, + ], + }, + ], + }, + { + name: 'Modify separator', + sep: true, + }, + { + name: 'Modify', + expand: 3, + onClick: () => {}, + children: [ + { + name: 'Pick layer', + right: () => ( + + 9+ + + ), + more: true, + icon: () => , + onClick: () => {}, + }, + { + name: 'Erase formatting', + danger: true, + icon: () => , + onClick: () => {}, + }, + { + name: 'Delete all in range', + danger: true, + more: true, + icon: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Clipboard separator', + sep: true, + }, + { + name: 'Copy, cut, and paste', + // icon: () => , + icon: () => , + expand: 0, + children: [ + { + id: 'copy-menu', + name: 'Copy', + // icon: () => , + icon: () => , + expand: 5, + children: [ + { + name: 'Copy', + icon: () => , + onClick: () => {}, + }, + { + name: 'Copy text only', + icon: () => , + onClick: () => {}, + }, + { + name: 'Copy as Markdown', + icon: () => , + right: () => , + onClick: () => {}, + }, + { + name: 'Copy as HTML', + icon: () => , + right: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Cut separator', + sep: true, + }, + { + id: 'cut-menu', + name: 'Cut', + // icon: () => , + icon: () => , + expand: 5, + children: [ + { + name: 'Cut', + danger: true, + icon: () => , + onClick: () => {}, + }, + { + name: 'Cut text only', + danger: true, + icon: () => , + onClick: () => {}, + }, + { + name: 'Cut as Markdown', + danger: true, + icon: () => , + right: () => , + onClick: () => {}, + }, + { + name: 'Cut as HTML', + danger: true, + icon: () => , + right: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Paste separator', + sep: true, + }, + { + id: 'paste-menu', + name: 'Paste', + icon: () => , + expand: 5, + children: [ + { + name: 'Paste', + icon: () => , + onClick: () => {}, + }, + { + name: 'Paste text only', + icon: () => , + onClick: () => {}, + }, + { + name: 'Paste formatting', + icon: () => , + onClick: () => {}, + }, + ], + }, + ], + }, + { + name: 'Insert', + icon: () => , + children: [ + { + name: 'Smart chip', + icon: () => , + children: [ + { + name: 'Date', + icon: () => , + onClick: () => {}, + }, + { + name: 'AI chip', + icon: () => , + onClick: () => {}, + }, + { + name: 'Solana wallet', + icon: () => , + onClick: () => {}, + }, + { + name: 'Dropdown', + icon: () => , + children: [ + { + name: 'Create new', + icon: () => , + onClick: () => {}, + }, + { + name: 'Document dropdowns separator', + sep: true, + }, + { + name: 'Document dropdowns', + expand: 8, + onClick: () => {}, + children: [ + { + name: 'Configuration 1', + icon: () => , + onClick: () => {}, + }, + { + name: 'Configuration 2', + icon: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Presets dropdowns separator', + sep: true, + }, + { + name: 'Presets dropdowns', + expand: 8, + onClick: () => {}, + children: [ + { + name: 'Project status', + icon: () => , + onClick: () => {}, + }, + { + name: 'Review status', + icon: () => , + onClick: () => {}, + }, + ], + }, + ], + }, + ], + }, + { + name: 'Link', + // icon: () => , + icon: () => , + onClick: () => {}, + }, + { + name: 'Reference', + icon: () => , + onClick: () => {}, + }, + { + name: 'File', + icon: () => , + onClick: () => {}, + }, + { + name: 'Template', + text: 'building blocks', + icon: () => , + children: [ + { + name: 'Meeting notes', + onClick: () => {}, + }, + { + name: 'Email draft (created by AI)', + onClick: () => {}, + }, + { + name: 'Product roadmap', + onClick: () => {}, + }, + { + name: 'Review tracker', + onClick: () => {}, + }, + { + name: 'Project assets', + onClick: () => {}, + }, + { + name: 'Content launch tracker', + onClick: () => {}, + }, + ], + }, + { + name: 'On-screen keyboard', + icon: () => , + onClick: () => {}, + }, + { + name: 'Emoji', + icon: () => , + onClick: () => {}, + }, + { + name: 'Special characters', + icon: () => , + onClick: () => {}, + }, + { + name: 'Variable', + icon: () => , + onClick: () => {}, + }, + ], + }, + { + name: 'Developer tools', + danger: true, + icon: () => , + onClick: () => {}, + }, + ], + }; + }; } From cf05b3aba07fedcd5241efcc118894d59f27f2f3 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 21 Jan 2025 19:20:38 +0100 Subject: [PATCH 06/30] =?UTF-8?q?feat(json-crdt-peritext-ui):=20?= =?UTF-8?q?=F0=9F=8E=B8=20make=20mouse=20down=20state=20reactive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dom/CursorController.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/json-crdt-peritext-ui/dom/CursorController.ts b/src/json-crdt-peritext-ui/dom/CursorController.ts index 7f231ce2a6..057c04bbc1 100644 --- a/src/json-crdt-peritext-ui/dom/CursorController.ts +++ b/src/json-crdt-peritext-ui/dom/CursorController.ts @@ -129,7 +129,7 @@ export class CursorController implements UiLifeCycles, Printable { private x = 0; private y = 0; - private mouseDown: boolean = false; + public readonly mouseDown = new ValueSyncStore(false); private readonly onMouseDown = (ev: MouseEvent): void => { const {clientX, clientY} = ev; @@ -137,7 +137,7 @@ export class CursorController implements UiLifeCycles, Printable { this.y = clientY; switch (ev.detail) { case 1: { - this.mouseDown = false; + this.mouseDown.next(false); const at = this.posAtPoint(clientX, clientY); if (at === -1) return; this.selAnchor = at; @@ -150,24 +150,24 @@ export class CursorController implements UiLifeCycles, Printable { ev.preventDefault(); et.cursor({at, edge: 'new'}); } else { - this.mouseDown = true; + this.mouseDown.next(true); ev.preventDefault(); et.cursor({at}); } break; } case 2: - this.mouseDown = false; + this.mouseDown.next(false); ev.preventDefault(); this.opts.et.cursor({unit: 'word'}); break; case 3: - this.mouseDown = false; + this.mouseDown.next(false); ev.preventDefault(); this.opts.et.cursor({unit: 'block'}); break; case 4: - this.mouseDown = false; + this.mouseDown.next(false); ev.preventDefault(); this.opts.et.cursor({unit: 'all'}); break; @@ -175,7 +175,7 @@ export class CursorController implements UiLifeCycles, Printable { }; private readonly onMouseMove = (ev: MouseEvent): void => { - if (!this.mouseDown) return; + if (!this.mouseDown.value) return; const at = this.selAnchor; if (at < 0) return; const {clientX, clientY} = ev; @@ -190,7 +190,7 @@ export class CursorController implements UiLifeCycles, Printable { }; private readonly onMouseUp = (ev: MouseEvent): void => { - this.mouseDown = false; + this.mouseDown.next(false); }; private onKeyDown = (event: KeyboardEvent): void => { @@ -242,6 +242,6 @@ export class CursorController implements UiLifeCycles, Printable { /** ----------------------------------------------------- {@link Printable} */ public toString(tab?: string): string { - return `cursor { focus: ${this.focus.value}, x: ${this.x}, y: ${this.y}, mouseDown: ${this.mouseDown} }`; + return `cursor { focus: ${this.focus.value}, x: ${this.x}, y: ${this.y}, mouseDown: ${this.mouseDown.value} }`; } } From 89b49563c926e34f2cce23760d6361d914d2cf27 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 21 Jan 2025 20:10:19 +0100 Subject: [PATCH 07/30] =?UTF-8?q?feat(json-crdt-peritext-ui):=20?= =?UTF-8?q?=F0=9F=8E=B8=20start=20focus=20floating=20menu=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/toolbar/CaretToolbar/index.tsx | 81 ------------------- .../plugins/toolbar/RenderCaret.tsx | 1 - .../plugins/toolbar/RenderFocus.tsx | 17 ++-- .../plugins/toolbar/state/index.tsx | 25 +++++- 4 files changed, 32 insertions(+), 92 deletions(-) delete mode 100644 src/json-crdt-peritext-ui/plugins/toolbar/CaretToolbar/index.tsx diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/CaretToolbar/index.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/CaretToolbar/index.tsx deleted file mode 100644 index 2b2b091dbd..0000000000 --- a/src/json-crdt-peritext-ui/plugins/toolbar/CaretToolbar/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -// biome-ignore lint: React is used for JSX -import * as React from 'react'; -import {Paper} from 'nice-ui/lib/4-card/Paper'; -import {Flex} from 'nice-ui/lib/3-list-item/Flex'; -import {BasicButton} from '../../../components/BasicButton'; -import {Sidetip} from 'nice-ui/lib/1-inline/Sidetip'; -import {Code} from 'nice-ui/lib/1-inline/Code'; -import {Iconista} from 'nice-ui/lib/icons/Iconista'; -import {ToolbarMenu} from 'nice-ui/lib/4-card/Toolbar/ToolbarMenu'; -import {FontStyleButton} from 'nice-ui/lib/2-inline-block/FontStyleButton'; -import {keyframes, rule} from 'nano-theme'; -import type {MenuItem} from 'nice-ui/lib/4-card/StructuralMenu/types'; -import {inlineText} from '../menus/menus'; - -const introAnimation = keyframes({ - from: { - tr: 'scale(.9)', - }, - to: { - tr: 'scale(1)', - }, -}); - -const blockClass = rule({ - an: introAnimation + ' .1s forwards', -}); - -// biome-ignore lint: empty interface -export type CaretToolbarProps = {}; - -export const CaretToolbar: React.FC = () => { - return ( - //
- - //
- ); - // return ( - // - // - // {}}> - // - // - // - // - // {}}> - // - // - // - // - // {}}> - // - // - // - // - // {}}> - // - // - // - // - // - // - // ); -}; diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx index 2d60b41fbc..6257a01186 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx @@ -43,7 +43,6 @@ export const RenderCaret: React.FC = ({children}) => { {children} - {/* {showCaretToolbar && } */} {showCaretToolbarValue && ( )} diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx index 2efca5bbaa..de20200ce0 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx @@ -1,6 +1,7 @@ // biome-ignore lint: React is used for JSX import * as React from 'react'; import {rule} from 'nano-theme'; +import {CaretToolbar} from 'nice-ui/lib/4-card/Toolbar/ToolbarMenu/CaretToolbar'; import {useToolbarPlugin} from './context'; import {useSyncStore} from '../../react/hooks'; import type {CaretViewProps} from '../../react/cursor/CaretView'; @@ -29,20 +30,20 @@ export interface RenderFocusProps extends CaretViewProps { export const RenderFocus: React.FC = ({children}) => { const {toolbar} = useToolbarPlugin()!; - // const showCaretToolbar = toolbar.showCaretToolbar; - // const showCaretToolbarValue = useSyncStore(showCaretToolbar); + const showFocusToolbar = toolbar.showFocusToolbar; + const showFocusToolbarValue = useSyncStore(showFocusToolbar); - // const handleClose = React.useCallback(() => { - // setTimeout(() => { - // if (showCaretToolbar.value) showCaretToolbar.next(false); - // }, 5); - // }, []); + const handleClose = React.useCallback(() => { + if (showFocusToolbar.value) showFocusToolbar.next(false); + }, []); return ( {children} - focus bar + {showFocusToolbarValue && ( + + )} ); 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 0b34484853..c43af48f9c 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx @@ -13,25 +13,44 @@ import type {MenuItem} from '../types'; export class ToolbarState implements UiLifeCyclesRender { public lastEvent: PeritextEventDetailMap['change']['ev'] | undefined = void 0; + public lastEventTs: number = 0; public readonly showCaretToolbar = new ValueSyncStore(false); + public readonly showFocusToolbar = new ValueSyncStore(false); constructor(public readonly surface: PeritextSurfaceState) {} - /** -------------------------------------------------- {@link UiLifeCyclesRender} */ + /** ------------------------------------------- {@link UiLifeCyclesRender} */ public start() { - const et = this.surface.events.et; + const {surface, showFocusToolbar} = this; + const {dom, events} = surface; + const {et} = events; const changeUnsubscribe = et.subscribe('change', (ev) => { const lastEvent = ev.detail.ev; this.lastEvent = lastEvent; + this.lastEventTs = Date.now(); const lastEventIsCaretPositionChange = lastEvent?.type === 'cursor' && typeof (lastEvent?.detail as PeritextEventDetailMap['cursor']).at === 'number'; if (lastEventIsCaretPositionChange) this.showCaretToolbar.next(true); }); + + const mouseDown = dom?.cursor.mouseDown; + const unsubscribeMouseDown = mouseDown?.subscribe(() => { + if (mouseDown.value) { + if (showFocusToolbar.value) showFocusToolbar.next(false); + } else { + if (!showFocusToolbar.value) { + showFocusToolbar.next(true); + + } + } + }); + return () => { changeUnsubscribe(); + unsubscribeMouseDown?.(); }; } @@ -568,6 +587,7 @@ export class ToolbarState implements UiLifeCyclesRender { maxToolbarItems: 4, children: [ this.getFormattingMenu(), + /* secondBrain(), { name: 'Annotations separator', @@ -951,6 +971,7 @@ export class ToolbarState implements UiLifeCyclesRender { icon: () => , onClick: () => {}, }, + */ ], }; }; From 8b4afdecc56c7ae2746e7cd03fce21bb9fad9496 Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 22 Jan 2025 15:25:30 +0100 Subject: [PATCH 08/30] =?UTF-8?q?feat(json-crdt-peritext-ui):=20?= =?UTF-8?q?=F0=9F=8E=B8=20improve=20on=20floating=20menus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/toolbar/RenderCaret.tsx | 22 ++++++++--- .../plugins/toolbar/RenderFocus.tsx | 10 +++-- .../plugins/toolbar/state/index.tsx | 38 +++++++++++-------- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx index 6257a01186..976a6810b9 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx @@ -6,7 +6,7 @@ import {useToolbarPlugin} from './context'; import {useSyncStore} from '../../react/hooks'; import type {CaretViewProps} from '../../react/cursor/CaretView'; -const height = 1.9; +const height = 1.8; const blockClass = rule({ pos: 'relative', @@ -30,19 +30,31 @@ export interface RenderCaretProps extends CaretViewProps { export const RenderCaret: React.FC = ({children}) => { const {toolbar} = useToolbarPlugin()!; - const showCaretToolbar = toolbar.showCaretToolbar; - const showCaretToolbarValue = useSyncStore(showCaretToolbar); + const showInlineToolbar = toolbar.showInlineToolbar; + const showCaretToolbarValue = useSyncStore(showInlineToolbar); const handleClose = React.useCallback(() => { setTimeout(() => { - if (showCaretToolbar.value) showCaretToolbar.next(false); + if (showInlineToolbar.value) showInlineToolbar.next(false); }, 5); }, []); return ( {children} - + { + // e.stopPropagation(); + // }} + // onMouseUp={(e) => { + // e.stopPropagation(); + // }} + // onClick={(e) => { + // e.stopPropagation(); + // }} + > {showCaretToolbarValue && ( )} diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx index de20200ce0..c342d143b1 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx @@ -6,7 +6,7 @@ import {useToolbarPlugin} from './context'; import {useSyncStore} from '../../react/hooks'; import type {CaretViewProps} from '../../react/cursor/CaretView'; -const height = 1.9; +const height = 1.8; const blockClass = rule({ pos: 'relative', @@ -30,11 +30,13 @@ export interface RenderFocusProps extends CaretViewProps { export const RenderFocus: React.FC = ({children}) => { const {toolbar} = useToolbarPlugin()!; - const showFocusToolbar = toolbar.showFocusToolbar; - const showFocusToolbarValue = useSyncStore(showFocusToolbar); + const showInlineToolbar = toolbar.showInlineToolbar; + const showFocusToolbarValue = useSyncStore(showInlineToolbar); + + console.log('showFocusToolbarValue', showFocusToolbarValue); const handleClose = React.useCallback(() => { - if (showFocusToolbar.value) showFocusToolbar.next(false); + if (showInlineToolbar.value) showInlineToolbar.next(false); }, []); return ( 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 c43af48f9c..9fa2f2d193 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx @@ -14,15 +14,14 @@ import type {MenuItem} from '../types'; export class ToolbarState implements UiLifeCyclesRender { public lastEvent: PeritextEventDetailMap['change']['ev'] | undefined = void 0; public lastEventTs: number = 0; - public readonly showCaretToolbar = new ValueSyncStore(false); - public readonly showFocusToolbar = new ValueSyncStore(false); + public readonly showInlineToolbar = new ValueSyncStore(false); constructor(public readonly surface: PeritextSurfaceState) {} /** ------------------------------------------- {@link UiLifeCyclesRender} */ public start() { - const {surface, showFocusToolbar} = this; + const {surface, showInlineToolbar: showFocusToolbar} = this; const {dom, events} = surface; const {et} = events; const changeUnsubscribe = et.subscribe('change', (ev) => { @@ -33,24 +32,31 @@ export class ToolbarState implements UiLifeCyclesRender { lastEvent?.type === 'cursor' && typeof (lastEvent?.detail as PeritextEventDetailMap['cursor']).at === 'number'; if (lastEventIsCaretPositionChange) - this.showCaretToolbar.next(true); + this.showInlineToolbar.next(true); }); - const mouseDown = dom?.cursor.mouseDown; - const unsubscribeMouseDown = mouseDown?.subscribe(() => { - if (mouseDown.value) { - if (showFocusToolbar.value) showFocusToolbar.next(false); - } else { - if (!showFocusToolbar.value) { - showFocusToolbar.next(true); - - } - } - }); + // const mouseDown = dom?.cursor.mouseDown; + // const unsubscribeMouseDown = mouseDown?.subscribe(() => { + // if (mouseDown.value) { + // if (showFocusToolbar.value) showFocusToolbar.next(false); + // } + // }); + + const source = dom?.opts.source; + const mouseDownListener = (event: MouseEvent) => { + if (showFocusToolbar.value) showFocusToolbar.next(false); + }; + const mouseUpListener = (event: MouseEvent) => { + if (!showFocusToolbar.value) showFocusToolbar.next(true); + }; + source?.addEventListener('mousedown', mouseDownListener); + source?.addEventListener('mouseup', mouseUpListener); return () => { changeUnsubscribe(); - unsubscribeMouseDown?.(); + // unsubscribeMouseDown?.(); + source?.removeEventListener('mousedown', mouseDownListener); + source?.removeEventListener('mouseup', mouseUpListener); }; } From c221a77e16cfc1943ddf5e8d4cf72657f09dbe71 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Thu, 23 Jan 2025 20:30:11 +0100 Subject: [PATCH 09/30] =?UTF-8?q?feat(json-crdt-peritext-ui):=20?= =?UTF-8?q?=F0=9F=8E=B8=20do=20not=20emite=20change=20event=20if=20old=20v?= =?UTF-8?q?alue=20strictly=20same?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx | 2 -- src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx | 7 ++++--- src/util/events/sync-store.ts | 3 ++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx index c342d143b1..3bbded9c4d 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx @@ -33,8 +33,6 @@ export const RenderFocus: React.FC = ({children}) => { const showInlineToolbar = toolbar.showInlineToolbar; const showFocusToolbarValue = useSyncStore(showInlineToolbar); - console.log('showFocusToolbarValue', showFocusToolbarValue); - const handleClose = React.useCallback(() => { if (showInlineToolbar.value) showInlineToolbar.next(false); }, []); 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 9fa2f2d193..6c5b8f4a4c 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx @@ -35,12 +35,13 @@ export class ToolbarState implements UiLifeCyclesRender { this.showInlineToolbar.next(true); }); - // const mouseDown = dom?.cursor.mouseDown; - // const unsubscribeMouseDown = mouseDown?.subscribe(() => { + const mouseDown = dom?.cursor.mouseDown; + const unsubscribeMouseDown = mouseDown?.subscribe(() => { + console.log('mouse down', mouseDown.value); // if (mouseDown.value) { // if (showFocusToolbar.value) showFocusToolbar.next(false); // } - // }); + }); const source = dom?.opts.source; const mouseDownListener = (event: MouseEvent) => { diff --git a/src/util/events/sync-store.ts b/src/util/events/sync-store.ts index 3c6b98aa6f..3f75e632a4 100644 --- a/src/util/events/sync-store.ts +++ b/src/util/events/sync-store.ts @@ -23,7 +23,8 @@ export class ValueSyncStore implements SyncStore { public readonly getSnapshot: () => V = () => this.value; - public next(value: V): void { + public next(value: V, force = false): void { + if (!force && this.value === value) return; this.value = value; this.fanout.emit(); } From 8b1c7b1486b5590c15b955a819e3bd22f4eb5fcc Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 24 Jan 2025 19:46:18 +0100 Subject: [PATCH 10/30] =?UTF-8?q?feat(json-crdt-peritext-ui):=20?= =?UTF-8?q?=F0=9F=8E=B8=20do=20not=20show=20focus=20floating=20menu=20whil?= =?UTF-8?q?e=20mouse=20down?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/toolbar/RenderFocus.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx index 3bbded9c4d..3495eefd77 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import {rule} from 'nano-theme'; import {CaretToolbar} from 'nice-ui/lib/4-card/Toolbar/ToolbarMenu/CaretToolbar'; import {useToolbarPlugin} from './context'; -import {useSyncStore} from '../../react/hooks'; +import {useSyncStore, useSyncStoreOpt} from '../../react/hooks'; import type {CaretViewProps} from '../../react/cursor/CaretView'; const height = 1.8; @@ -32,6 +32,11 @@ export const RenderFocus: React.FC = ({children}) => { const {toolbar} = useToolbarPlugin()!; const showInlineToolbar = toolbar.showInlineToolbar; const showFocusToolbarValue = useSyncStore(showInlineToolbar); + const mouseDown = !!useSyncStoreOpt(toolbar.surface.dom?.cursor.mouseDown); + + + // const showInlineToolbar = toolbar.showInlineToolbar; + // const showCaretToolbarValue = useSyncStore(showInlineToolbar); const handleClose = React.useCallback(() => { if (showInlineToolbar.value) showInlineToolbar.next(false); @@ -41,7 +46,7 @@ export const RenderFocus: React.FC = ({children}) => { {children} - {showFocusToolbarValue && ( + {!mouseDown && showFocusToolbarValue && ( )} From 2fc72c54d38525852792dbbcaa4497c294a9d7a7 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 25 Jan 2025 18:17:10 +0100 Subject: [PATCH 11/30] =?UTF-8?q?feat(json-crdt-peritext-ui):=20?= =?UTF-8?q?=F0=9F=8E=B8=20delay=20showing=20floating=20menus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/toolbar/RenderCaret.tsx | 5 ++++- .../plugins/toolbar/RenderFocus.tsx | 11 ++++++---- .../react/util/AfterTimeout.tsx | 22 +++++++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 src/json-crdt-peritext-ui/react/util/AfterTimeout.tsx diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx index 976a6810b9..42e79af785 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx @@ -5,6 +5,7 @@ import {CaretToolbar} from 'nice-ui/lib/4-card/Toolbar/ToolbarMenu/CaretToolbar' import {useToolbarPlugin} from './context'; import {useSyncStore} from '../../react/hooks'; import type {CaretViewProps} from '../../react/cursor/CaretView'; +import {AfterTimeout} from '../../react/util/AfterTimeout'; const height = 1.8; @@ -56,7 +57,9 @@ export const RenderCaret: React.FC = ({children}) => { // }} > {showCaretToolbarValue && ( - + + + )} diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx index 3495eefd77..dc2409cd50 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx @@ -5,6 +5,7 @@ import {CaretToolbar} from 'nice-ui/lib/4-card/Toolbar/ToolbarMenu/CaretToolbar' import {useToolbarPlugin} from './context'; import {useSyncStore, useSyncStoreOpt} from '../../react/hooks'; import type {CaretViewProps} from '../../react/cursor/CaretView'; +import {AfterTimeout} from '../../react/util/AfterTimeout'; const height = 1.8; @@ -31,8 +32,8 @@ export interface RenderFocusProps extends CaretViewProps { export const RenderFocus: React.FC = ({children}) => { const {toolbar} = useToolbarPlugin()!; const showInlineToolbar = toolbar.showInlineToolbar; - const showFocusToolbarValue = useSyncStore(showInlineToolbar); - const mouseDown = !!useSyncStoreOpt(toolbar.surface.dom?.cursor.mouseDown); + const showInlineToolbarValue = useSyncStore(showInlineToolbar); + // const mouseDown = !!useSyncStoreOpt(toolbar.surface.dom?.cursor.mouseDown); // const showInlineToolbar = toolbar.showInlineToolbar; @@ -46,8 +47,10 @@ export const RenderFocus: React.FC = ({children}) => { {children} - {!mouseDown && showFocusToolbarValue && ( - + {showInlineToolbarValue && ( + + + )} diff --git a/src/json-crdt-peritext-ui/react/util/AfterTimeout.tsx b/src/json-crdt-peritext-ui/react/util/AfterTimeout.tsx new file mode 100644 index 0000000000..ff19012ee6 --- /dev/null +++ b/src/json-crdt-peritext-ui/react/util/AfterTimeout.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; + +export interface IAfterTimeoutProps { + ms?: number; + children?: React.ReactNode; +} + +export const AfterTimeout: React.FC = ({ms = 200, children}) => { + const [ready, setReady] = React.useState(false); + + React.useEffect(() => { + const timer = setTimeout(() => { + setReady(true); + }, ms); + + return () => { + clearTimeout(timer); + }; + }, [ms]); + + return ready ? children : null; +}; From 549253eb7012519a9fc16f0665be32902f9b5aa0 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 25 Jan 2025 18:18:47 +0100 Subject: [PATCH 12/30] =?UTF-8?q?feat(json-crdt-peritext-ui):=20?= =?UTF-8?q?=F0=9F=8E=B8=20improve=20toolbar=20state=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/toolbar/state/index.tsx | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) 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 6c5b8f4a4c..cce8e03a1c 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx @@ -21,46 +21,58 @@ export class ToolbarState implements UiLifeCyclesRender { /** ------------------------------------------- {@link UiLifeCyclesRender} */ public start() { - const {surface, showInlineToolbar: showFocusToolbar} = this; + const {surface, showInlineToolbar} = this; const {dom, events} = surface; const {et} = events; + const mouseDown = dom!.cursor.mouseDown; + const source = dom!.opts.source; + const changeUnsubscribe = et.subscribe('change', (ev) => { const lastEvent = ev.detail.ev; - this.lastEvent = lastEvent; - this.lastEventTs = Date.now(); + this.setLastEv(lastEvent); const lastEventIsCaretPositionChange = lastEvent?.type === 'cursor' && typeof (lastEvent?.detail as PeritextEventDetailMap['cursor']).at === 'number'; - if (lastEventIsCaretPositionChange) - this.showInlineToolbar.next(true); + this.showInlineToolbar.next(this.doShowInlineToolbar()); }); - const mouseDown = dom?.cursor.mouseDown; const unsubscribeMouseDown = mouseDown?.subscribe(() => { - console.log('mouse down', mouseDown.value); - // if (mouseDown.value) { - // if (showFocusToolbar.value) showFocusToolbar.next(false); - // } + if (mouseDown.value) showInlineToolbar.next(false); }); - - const source = dom?.opts.source; + const mouseDownListener = (event: MouseEvent) => { - if (showFocusToolbar.value) showFocusToolbar.next(false); + if (showInlineToolbar.value) showInlineToolbar.next(false); }; const mouseUpListener = (event: MouseEvent) => { - if (!showFocusToolbar.value) showFocusToolbar.next(true); + if (!showInlineToolbar.value) showInlineToolbar.next(true); }; + source?.addEventListener('mousedown', mouseDownListener); source?.addEventListener('mouseup', mouseUpListener); return () => { changeUnsubscribe(); - // unsubscribeMouseDown?.(); + unsubscribeMouseDown?.(); source?.removeEventListener('mousedown', mouseDownListener); source?.removeEventListener('mouseup', mouseUpListener); }; } + private setLastEv(lastEvent: PeritextEventDetailMap['change']['ev']) { + this.lastEvent = lastEvent; + this.lastEventTs = Date.now(); + } + + private doShowInlineToolbar(): boolean { + const {surface, lastEvent} = this; + if (surface.dom!.cursor.mouseDown.value) return false; + if (!lastEvent) return false; + const lastEventIsCursorEvent = lastEvent?.type === 'cursor'; + if (!lastEventIsCursorEvent) return false; + if (!surface.peritext.editor.cursorCount()) return false; + return true; + } + // -------------------------------------------------------------------- Menus public readonly getFormattingMenu = (): MenuItem => { From 621e4d2381d9a0c2d111e3c3dd120d53c5f14371 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 25 Jan 2025 19:17:01 +0100 Subject: [PATCH 13/30] =?UTF-8?q?feat(json-crdt-peritext-ui):=20?= =?UTF-8?q?=F0=9F=8E=B8=20hide=20floating=20menu=20when=20focus=20blurred?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/toolbar/RenderCaret.tsx | 7 ++++--- .../plugins/toolbar/RenderFocus.tsx | 3 ++- .../plugins/toolbar/state/index.tsx | 10 +++++----- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx index 42e79af785..af87d3228d 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderCaret.tsx @@ -3,9 +3,9 @@ import * as React from 'react'; import {rule} from 'nano-theme'; import {CaretToolbar} from 'nice-ui/lib/4-card/Toolbar/ToolbarMenu/CaretToolbar'; import {useToolbarPlugin} from './context'; -import {useSyncStore} from '../../react/hooks'; -import type {CaretViewProps} from '../../react/cursor/CaretView'; +import {useSyncStore, useSyncStoreOpt} from '../../react/hooks'; import {AfterTimeout} from '../../react/util/AfterTimeout'; +import type {CaretViewProps} from '../../react/cursor/CaretView'; const height = 1.8; @@ -33,6 +33,7 @@ export const RenderCaret: React.FC = ({children}) => { const {toolbar} = useToolbarPlugin()!; const showInlineToolbar = toolbar.showInlineToolbar; const showCaretToolbarValue = useSyncStore(showInlineToolbar); + const focus = useSyncStoreOpt(toolbar.surface.dom?.cursor.focus) || false; const handleClose = React.useCallback(() => { setTimeout(() => { @@ -56,7 +57,7 @@ export const RenderCaret: React.FC = ({children}) => { // e.stopPropagation(); // }} > - {showCaretToolbarValue && ( + {(showCaretToolbarValue && focus) && ( diff --git a/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx b/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx index dc2409cd50..43cfa17dc2 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/RenderFocus.tsx @@ -33,6 +33,7 @@ export const RenderFocus: React.FC = ({children}) => { const {toolbar} = useToolbarPlugin()!; const showInlineToolbar = toolbar.showInlineToolbar; const showInlineToolbarValue = useSyncStore(showInlineToolbar); + const focus = useSyncStoreOpt(toolbar.surface.dom?.cursor.focus) || false; // const mouseDown = !!useSyncStoreOpt(toolbar.surface.dom?.cursor.mouseDown); @@ -47,7 +48,7 @@ export const RenderFocus: React.FC = ({children}) => { {children} - {showInlineToolbarValue && ( + {(showInlineToolbarValue && focus) && ( 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 cce8e03a1c..7f55ad0542 100644 --- a/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx +++ b/src/json-crdt-peritext-ui/plugins/toolbar/state/index.tsx @@ -30,9 +30,9 @@ export class ToolbarState implements UiLifeCyclesRender { const changeUnsubscribe = et.subscribe('change', (ev) => { const lastEvent = ev.detail.ev; this.setLastEv(lastEvent); - const lastEventIsCaretPositionChange = - lastEvent?.type === 'cursor' && - typeof (lastEvent?.detail as PeritextEventDetailMap['cursor']).at === 'number'; + // const lastEventIsCaretPositionChange = + // lastEvent?.type === 'cursor' && + // typeof (lastEvent?.detail as PeritextEventDetailMap['cursor']).at === 'number'; this.showInlineToolbar.next(this.doShowInlineToolbar()); }); @@ -41,10 +41,10 @@ export class ToolbarState implements UiLifeCyclesRender { }); const mouseDownListener = (event: MouseEvent) => { - if (showInlineToolbar.value) showInlineToolbar.next(false); + showInlineToolbar.next(false); }; const mouseUpListener = (event: MouseEvent) => { - if (!showInlineToolbar.value) showInlineToolbar.next(true); + showInlineToolbar.next(true); }; source?.addEventListener('mousedown', mouseDownListener); From 736d8e8addad0fd8c5f34c0b4fc8ba2b85418bfa Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 25 Jan 2025 20:16:51 +0100 Subject: [PATCH 14/30] =?UTF-8?q?feat(json-crdt-peritext-ui):=20?= =?UTF-8?q?=F0=9F=8E=B8=20do=20not=20change=20cursor=20position=20when=20e?= =?UTF-8?q?ditor=20blurred?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-peritext-ui/dom/CursorController.ts | 3 ++- .../plugins/minimal/TopToolbar/index.tsx | 6 ++++-- .../plugins/toolbar/TopToolbar/index.tsx | 7 +++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/json-crdt-peritext-ui/dom/CursorController.ts b/src/json-crdt-peritext-ui/dom/CursorController.ts index 057c04bbc1..c6b48c0ea4 100644 --- a/src/json-crdt-peritext-ui/dom/CursorController.ts +++ b/src/json-crdt-peritext-ui/dom/CursorController.ts @@ -119,7 +119,7 @@ export class CursorController implements UiLifeCycles, Printable { public readonly focus = new ValueSyncStore(false); - private readonly onFocus = (): void => { + private readonly onFocus = (event: Event): void => { this.focus.next(true); }; @@ -132,6 +132,7 @@ export class CursorController implements UiLifeCycles, Printable { public readonly mouseDown = new ValueSyncStore(false); private readonly onMouseDown = (ev: MouseEvent): void => { + if (!this.focus.value && this.opts.txt.editor.hasCursor()) return; const {clientX, clientY} = ev; this.x = clientX; this.y = clientY; 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 5bc90ca28e..9261cabf98 100644 --- a/src/json-crdt-peritext-ui/plugins/minimal/TopToolbar/index.tsx +++ b/src/json-crdt-peritext-ui/plugins/minimal/TopToolbar/index.tsx @@ -12,11 +12,13 @@ export interface TopToolbarProps { } export const TopToolbar: React.FC = ({ctx}) => { - const pending = useSyncStore(ctx.peritext.editor.pending); + const peritext = ctx.peritext; + const editor = peritext.editor; + const pending = useSyncStore(editor.pending); if (!ctx.dom) return null; - const [complete] = ctx.peritext.overlay.stat(ctx.peritext.editor.cursor); + const [complete] = editor.hasCursor() ? peritext.overlay.stat(editor.cursor) : [new Set()]; const inlineGroupButton = (type: string | number, name: React.ReactNode) => (