UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

1,108 lines (1,107 loc) 72.3 kB
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 { secondBrain } from './menus'; import { Code } from 'nice-ui/lib/1-inline/Code'; import { FontStyleButton } from 'nice-ui/lib/2-inline-block/FontStyleButton'; import { CommonSliceType } from '../../../../json-crdt-extensions'; import { BehaviorSubject } from 'rxjs'; import { compare } from '../../../../json-crdt-patch'; import { NewFormatting } from './formattings'; import * as behavior from '../formatting/tags'; export class ToolbarState { surface; opts; txt; lastEvent = void 0; lastEventTs = 0; showInlineToolbar = new ValueSyncStore([false, 0]); /** * New slice configuration. This is used for new slices which are not yet * applied to the text as they need to be configured first. */ newSlice = new ValueSyncStore(void 0); /** * The ID of the active (where the main cursor or focus is placed) leaf block. */ activeLeafBlockId$ = new BehaviorSubject(null); constructor(surface, opts) { this.surface = surface; this.opts = opts; this.txt = this.surface.dom.txt; } _setActiveLeafBlockId = () => { const { activeLeafBlockId$, txt } = this; const { overlay, editor } = txt; const value = activeLeafBlockId$.getValue(); const cardinality = editor.cursorCard(); if (cardinality !== 1 || (cardinality === 1 && !editor.mainCursor()?.isCollapsed())) { if (value) activeLeafBlockId$.next(null); return; } const focus = editor.mainCursor()?.focus(); const marker = focus ? overlay.getOrNextLowerMarker(focus) : void 0; const markerId = marker?.marker.start.id ?? txt.str.id; const doSet = !value || compare(value, markerId) !== 0; if (doSet) activeLeafBlockId$.next(markerId); }; setLastEv(lastEvent) { 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; // } startSliceConfig(tag, menu) { const editor = this.txt.editor; const behavior = editor.getRegistry().get(tag); const range = editor.mainCursor()?.range(); if (!range) return; const newSlice = this.newSlice; if (!behavior) { newSlice.next(void 0); return; } const formatting = new NewFormatting(behavior, range, this); newSlice.next(formatting); return formatting; } // public registerSlice(tag: TypeTag, data: SliceRegistryEntryData): ToolBarSliceRegistryEntry { // const registry = this.txt.editor.getRegistry(); // const entry = registry.get(tag); // } /** ------------------------------------------- {@link UiLifeCycles} */ start() { const { surface, showInlineToolbar, newSlice: newSliceConfig } = this; const { dom, events } = surface; const { et } = events; const mouseDown = dom.cursor.mouseDown; const el = dom.el; const registry = this.txt.editor.getRegistry(); Object.assign(registry.get(-17 /* SliceTypeCon.a */)?.data() || {}, behavior.a); // registry.add({}); Object.assign(registry.get(-29 /* SliceTypeCon.col */)?.data() || {}, behavior.col); const changeUnsubscribe = et.subscribe('change', (ev) => { const lastEvent = ev.detail.ev; this.setLastEv(lastEvent); this._setActiveLeafBlockId(); }); const unsubscribeMouseDown = mouseDown?.subscribe(() => { // if (mouseDown.value) showInlineToolbar.next(false); }); const mouseDownListener = (event) => { // showInlineToolbar.next(false); // if (showInlineToolbar.value[0]) // showInlineToolbar.next([false, Date.now()]); }; const mouseUpListener = (event) => { if (!showInlineToolbar.value[0]) showInlineToolbar.next([true, Date.now()]); }; const onKeyDown = (event) => { switch (event.key) { case 'Escape': { if (newSliceConfig.value) { event.stopPropagation(); event.preventDefault; newSliceConfig.next(void 0); return; } break; } } }; const onKeyDownDocument = (event) => { switch (event.key) { case 'k': { if (event.metaKey) { const editor = this.txt.editor; if (editor.hasCursor() && !editor.mainCursor()?.isCollapsed() && (!newSliceConfig.value || newSliceConfig.value.behavior.tag !== -17 /* SliceTypeCon.a */)) { event.stopPropagation(); event.preventDefault; this.startSliceConfig(-17 /* SliceTypeCon.a */, this.linkMenuItem()); return; } } break; } } }; const onCursor = ({ detail }) => { // Close config popup on non-focus cursor moves. if (newSliceConfig.value) { const isFocusMove = detail.move && detail.move.length === 1 && detail.move[0][0] === 'focus'; if (!isFocusMove) { this.newSlice.next(void 0); } } }; el.addEventListener('mousedown', mouseDownListener); el.addEventListener('mouseup', mouseUpListener); el.addEventListener('keydown', onKeyDown); document.addEventListener('keydown', onKeyDownDocument); et.addEventListener('cursor', onCursor); const unsubscribeKeyHistory = dom.keys.history.subscribe(() => { // Flip selection anchor and focus edges on quick [Meta, Meta] key sequence. const keys = dom.keys.history.value; const last = keys[keys.length - 1]; const beforeLast = keys[keys.length - 2]; if (last?.key === 'Meta' && beforeLast?.key === 'Meta') if (last.ts - beforeLast.ts < 500) et.cursor({ flip: true }); }); return () => { changeUnsubscribe(); unsubscribeMouseDown?.(); el.removeEventListener('mousedown', mouseDownListener); el.removeEventListener('mouseup', mouseUpListener); el.removeEventListener('keydown', onKeyDown); document.removeEventListener('keydown', onKeyDownDocument); et.removeEventListener('cursor', onCursor); unsubscribeKeyHistory(); }; } // -------------------------------------------------------------------- Menus getFormattingMenu = () => { const et = this.surface.events.et; return { name: 'Formatting', expandChild: 0, children: [ { name: 'Common', expand: 8, children: [ { name: 'Bold', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "font-bold" }), // icon: () => <Iconista width={16} height={16} set="lucide" icon="bold" />, right: () => React.createElement(Sidetip, { small: true }, "\u2318 B"), keys: ['⌘', 'b'], onSelect: () => { et.format('tog', CommonSliceType.b); }, }, { name: 'Italic', // icon: () => <Iconista width={15} height={15} set="radix" icon="font-italic" />, // icon: () => <Iconista width={16} height={16} set="lucide" icon="italic" />, icon: () => React.createElement(Iconista, { width: 14, height: 14, set: "lucide", icon: "italic" }), right: () => React.createElement(Sidetip, { small: true }, "\u2318 I"), keys: ['⌘', 'i'], onSelect: () => { et.format('tog', CommonSliceType.i); }, }, { name: 'Underline', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "underline" }), right: () => React.createElement(Sidetip, { small: true }, "\u2318 U"), keys: ['⌘', 'u'], onSelect: () => { et.format('tog', CommonSliceType.u); }, }, { name: 'Strikethrough', // icon: () => <Iconista width={15} height={15} set="radix" icon="strikethrough" />, icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "strikethrough" }), onSelect: () => { et.format('tog', CommonSliceType.s); }, }, { name: 'Overline', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "overline" }), onSelect: () => { et.format('tog', CommonSliceType.overline); }, }, { name: 'Highlight', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "highlight" }), onSelect: () => { et.format('tog', CommonSliceType.mark); }, }, { name: 'Classified', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "lock-password" }), onSelect: () => { et.format('tog', CommonSliceType.spoiler); }, }, ], }, { name: 'Technical separator', sep: true, }, { name: 'Technical', expand: 8, children: [ { name: 'Code', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "code" }), onSelect: () => { et.format('tog', CommonSliceType.code); }, }, { name: 'Math', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "math-integral-x" }), onSelect: () => { et.format('tog', CommonSliceType.math); }, }, { name: 'Superscript', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "superscript" }), onSelect: () => { et.format('tog', CommonSliceType.sup); }, }, { name: 'Subscript', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "subscript" }), onSelect: () => { et.format('tog', CommonSliceType.sub); }, }, { name: 'Keyboard key', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "lucide", icon: "keyboard" }), onSelect: () => { et.format('tog', CommonSliceType.kbd); }, }, { name: 'Insertion', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "pencil-plus" }), onSelect: () => { et.format('tog', CommonSliceType.ins); }, }, { name: 'Deletion', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "pencil-minus" }), onSelect: () => { et.format('tog', CommonSliceType.del); }, }, ], }, { name: 'Artistic separator', sep: true, }, { name: 'Artistic', expand: 8, children: [ this.colorMenuItem(), { name: 'Background', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "lucide", icon: "paint-bucket" }), onSelect: () => { }, }, { name: 'Border', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "border-left" }), onSelect: () => { }, }, ], }, ], }; }; colorMenuItem = () => { const colorItem = { ...behavior.col.menu, onSelect: () => { this.startSliceConfig(CommonSliceType.col, colorItem); }, }; return colorItem; }; linkMenuItem = () => { const linkAction = { ...behavior.a.menu, onSelect: () => { this.startSliceConfig(CommonSliceType.a, linkAction); }, }; return linkAction; }; annotationsMenu = () => { return { name: 'Annotations', expand: 2, sepBefore: true, children: [ this.linkMenuItem(), // { // name: 'Comment', // icon: () => <Iconista width={16} height={16} set="lineicons" icon="comment-1-text" />, // onSelect: () => {}, // }, // { // name: 'Bookmark', // icon: () => <Iconista width={16} height={16} set="lineicons" icon="flag-2" />, // onSelect: () => {}, // }, // { // name: 'Footnote', // icon: () => <Iconista width={16} height={16} set="lucide" icon="footprints" />, // onSelect: () => {}, // }, { name: 'Aside', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "box-align-right" }), onSelect: () => { }, }, ], }; }; modifyMenu = () => { const et = this.surface.events.et; return { name: 'Modify', expand: 3, sepBefore: true, children: [ { name: 'Pick layer', right: () => (React.createElement(Code, { size: -1, gray: true }, "9+")), more: true, icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "layers" }), onSelect: () => { }, }, { name: 'Erase formatting', danger: true, icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "eraser" }), onSelect: () => { et.format({ action: 'erase' }); }, }, { name: 'Delete all in range', danger: true, more: true, icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "trash" }), onSelect: () => { et.format({ action: 'del' }); }, }, ], }; }; copyAsMenu = (action, ctx = {}) => { const icon = action === 'copy' ? () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "clipboard-copy" }) : () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "scissors" }); const et = this.surface.events.et; const iconMarkdown = () => React.createElement(Iconista, { width: 16, height: 16, set: "simple", icon: "markdown", style: { opacity: 0.5 } }); const iconHtml = () => React.createElement(Iconista, { width: 14, height: 14, set: "simple", icon: "html5", style: { opacity: 0.5 } }); const iconJson = () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "json", style: { opacity: 0.5 } }); const markdownAction = { name: 'Markdown', text: action + ' markdown md', icon, right: iconMarkdown, onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(markdownAction, action), action, format: 'md' }); }, }; const mdastAction = { name: 'MDAST', text: action + ' markdown md mdast', icon, right: iconMarkdown, onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(mdastAction, action), action, format: 'mdast' }); }, }; const htmlAction = { name: 'HTML', text: action + ' html', icon, right: iconHtml, onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(htmlAction, action), action, format: 'html' }); }, }; const hastAction = { name: 'HAST', text: action + ' html hast', icon, right: iconHtml, onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(hastAction, action), action, format: 'hast' }); }, }; const jsonAction = { name: 'Range view', text: action + ' range view peritext', icon, right: iconJson, onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(jsonAction, action), action, format: 'json' }); }, }; const jsonmlAction = { name: 'Fragment ML', text: action + ' peritext fragment ml node', icon, right: iconJson, onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(jsonmlAction, action), action, format: 'jsonml' }); }, }; const fragmentAction = { name: 'Fragment text', text: action + 'peritext fragment debug', icon, right: () => React.createElement(Iconista, { width: 16, height: 16, set: "lucide", icon: "text", style: { opacity: 0.5 } }), onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(fragmentAction, action), action, format: 'fragment' }); }, }; return { name: action === 'copy' ? 'Copy as' : 'Cut as', more: true, icon, children: [ markdownAction, mdastAction, { name: 'MD sep', sep: true, }, htmlAction, hastAction, { name: 'HTML sep', sep: true, }, jsonAction, jsonmlAction, fragmentAction, ], }; }; pasteAsMenu = (ctx = {}) => { const icon = () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "clipboard" }); const iconMarkdown = () => React.createElement(Iconista, { width: 16, height: 16, set: "simple", icon: "markdown", style: { opacity: 0.5 } }); const iconHtml = () => React.createElement(Iconista, { width: 14, height: 14, set: "simple", icon: "html5", style: { opacity: 0.5 } }); const iconJson = () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "json", style: { opacity: 0.5 } }); const et = this.surface.events.et; const markdownAction = { name: 'Markdown', text: 'paste markdown md', icon, right: iconMarkdown, onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(markdownAction, 'paste'), action: 'paste', format: 'md' }); }, }; const mdastAction = { name: 'MDAST', text: 'paste markdown md mdast', icon, right: iconMarkdown, onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(mdastAction, 'paste'), action: 'paste', format: 'mdast' }); }, }; const htmlAction = { name: 'HTML', text: 'paste html', icon, right: iconHtml, onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(htmlAction, 'paste'), action: 'paste', format: 'html' }); }, }; const hastAction = { name: 'HAST', text: 'paste html hast', icon, right: iconHtml, onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(hastAction, 'paste'), action: 'paste', format: 'hast' }); }, }; const jsonAction = { name: 'Range view', text: 'paste range view peritext', icon, right: iconJson, onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(jsonAction, 'paste'), action: 'paste', format: 'json' }); }, }; const jsonmlAction = { name: 'Fragment ML', text: 'paste peritext fragment ml node', icon, right: iconJson, onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(jsonmlAction, 'paste'), action: 'paste', format: 'jsonml' }); }, }; return { name: 'Paste as', more: true, icon, children: [ markdownAction, mdastAction, { name: 'MD sep', sep: true, }, htmlAction, hastAction, { name: 'HTML sep', sep: true, }, jsonAction, jsonmlAction, ], }; }; copyMenu = (ctx = {}) => { const et = this.surface.events.et; const copyAction = { name: 'Copy', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "clipboard-copy" }), onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(copyAction, 'copy'), action: 'copy' }); }, }; const copyTextOnlyAction = { name: 'Copy text only', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "clipboard-copy" }), onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(copyTextOnlyAction, 'copy'), action: 'copy', format: 'text' }); }, }; const children = [copyAction, copyTextOnlyAction]; if (!ctx.hideStyleActions) { const copyStyleAction = { name: 'Copy style', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "clipboard-copy" }), onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(copyStyleAction, 'copy'), action: 'copy', format: 'style' }); }, }; children.push(copyStyleAction); } children.push(this.copyAsMenu('copy', ctx)); return { id: 'copy-menu', name: 'Copy', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "clipboard-copy" }), expand: 5, children, }; }; cutMenu = (ctx = {}) => { const et = this.surface.events.et; const cutAction = { name: 'Cut', danger: true, icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "scissors" }), onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(cutAction, 'cut'), action: 'cut' }); }, }; const cutTextAction = { name: 'Cut text only', danger: true, icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "scissors" }), onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(cutTextAction, 'cut'), action: 'cut', format: 'text' }); }, }; return { id: 'cut-menu', name: 'Cut', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "scissors" }), expand: 5, children: [cutAction, cutTextAction, this.copyAsMenu('cut', ctx)], }; }; pasteMenu = (ctx = {}) => { const et = this.surface.events.et; const pasteAction = { name: 'Paste', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "clipboard" }), onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(pasteAction, 'paste'), action: 'paste' }); }, }; const pasteTextAction = { name: 'Paste text', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "clipboard" }), onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(pasteTextAction, 'paste'), action: 'paste', format: 'text' }); }, }; const children = [pasteAction, pasteTextAction]; if (!ctx.hideStyleActions) { const pasteStyleAction = { name: 'Paste style', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "clipboard" }), onSelect: () => { et.buffer({ ...ctx.onBeforeAction?.(pasteStyleAction, 'paste'), action: 'paste', format: 'style' }); }, }; children.push(pasteStyleAction); } children.push(this.pasteAsMenu(ctx)); return { id: 'paste-menu', name: 'Paste', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "clipboard" }), expand: 5, children, }; }; clipboardMenu = (ctx = {}) => { const copyMenu = this.copyMenu(ctx); const cutMenu = this.cutMenu(ctx); const pasteMenu = this.pasteMenu(ctx); cutMenu.sepBefore = true; pasteMenu.sepBefore = true; return { name: 'Copy, cut, and paste', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "lucide", icon: "copy" }), expand: 0, sepBefore: true, children: [copyMenu, cutMenu, pasteMenu], }; }; getCaretMenu = () => { return { name: 'Inline text', maxToolbarItems: 4, children: [ this.getFormattingMenu(), secondBrain(), { name: 'Annotations separator', sep: true, }, this.annotationsMenu(), { name: 'Style separator', sep: true, }, { name: 'Typesetting', expand: 4, openOnTitleHov: true, icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "typography" }), onSelect: () => { }, children: [ { name: 'Sans-serif', iconBig: () => React.createElement(FontStyleButton, { kind: 'sans' }), onSelect: () => { }, }, { name: 'Serif', iconBig: () => React.createElement(FontStyleButton, { kind: 'serif' }), onSelect: () => { }, }, { name: 'Slab', icon: () => React.createElement(FontStyleButton, { kind: 'slab', size: 16 }), iconBig: () => React.createElement(FontStyleButton, { kind: 'slab' }), onSelect: () => { }, }, { name: 'Monospace', iconBig: () => React.createElement(FontStyleButton, { kind: 'mono' }), onSelect: () => { }, }, // { // name: 'Custom typeface separator', // sep: true, // }, { name: 'Custom typeface', expand: 10, icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "font-style" }), children: [ { name: 'Typeface', // icon: () => <Iconista width={15} height={15} set="radix" icon="font-style" />, icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "font-family" }), onSelect: () => { }, }, { name: 'Text size', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "font-size" }), onSelect: () => { }, }, { name: 'Letter spacing', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "letter-spacing" }), onSelect: () => { }, }, { name: 'Word spacing', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "letter-spacing" }), onSelect: () => { }, }, { name: 'Caps separator', sep: true, }, { name: 'Large caps', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "letter-case-uppercase" }), onSelect: () => { }, }, { name: 'Small caps', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "letter-case-lowercase" }), onSelect: () => { }, }, ], }, ], }, { name: 'Modify separator', sep: true, }, this.modifyMenu(), this.clipboardMenu(), { name: 'Insert', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "lucide", icon: "between-vertical-end" }), children: [ { name: 'Smart chip', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "button" }), children: [ { name: 'Date', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "calendar" }), onSelect: () => { }, }, { name: 'AI chip', icon: () => React.createElement(Iconista, { style: { color: 'purple' }, width: 16, height: 16, set: "tabler", icon: "brain" }), onSelect: () => { }, }, { name: 'Solana wallet', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "wallet" }), onSelect: () => { }, }, { name: 'Dropdown', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "dropdown-menu" }), children: [ { name: 'Create new', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "plus" }), onSelect: () => { }, }, { name: 'Document dropdowns separator', sep: true, }, { name: 'Document dropdowns', expand: 8, onSelect: () => { }, children: [ { name: 'Configuration 1', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "dropdown-menu" }), onSelect: () => { }, }, { name: 'Configuration 2', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "dropdown-menu" }), onSelect: () => { }, }, ], }, { name: 'Presets dropdowns separator', sep: true, }, { name: 'Presets dropdowns', expand: 8, onSelect: () => { }, children: [ { name: 'Project status', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "dropdown-menu" }), onSelect: () => { }, }, { name: 'Review status', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "dropdown-menu" }), onSelect: () => { }, }, ], }, ], }, ], }, { name: 'Link', // icon: () => <Iconista width={15} height={15} set="lucide" icon="link" />, icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "link-2" }), onSelect: () => { }, }, { name: 'Reference', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "sewing-pin" }), onSelect: () => { }, }, { name: 'File', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "file" }), onSelect: () => { }, }, { name: 'Template', text: 'building blocks', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "tabler", icon: "template" }), children: [ { name: 'Meeting notes', onSelect: () => { }, }, { name: 'Email draft (created by AI)', onSelect: () => { }, }, { name: 'Product roadmap', onSelect: () => { }, }, { name: 'Review tracker', onSelect: () => { }, }, { name: 'Project assets', onSelect: () => { }, }, { name: 'Content launch tracker', onSelect: () => { }, }, ], }, { name: 'On-screen keyboard', icon: () => React.createElement(Iconista, { width: 15, height: 15, set: "radix", icon: "keyboard" }), onSelect: () => { }, }, { name: 'Emoji', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "lucide", icon: "smile-plus" }), onSelect: () => { }, }, { name: 'Special characters', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "lucide", icon: "omega" }), onSelect: () => { }, }, { name: 'Variable', icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "lucide", icon: "variable" }), onSelect: () => { }, }, ], }, { name: 'Developer tools', danger: true, icon: () => React.createElement(Iconista, { width: 16, height: 16, set: "lucide", icon: "square-chevron-right" }), onSelect: () => { }, }, ], }; }; getSelectionMenu = () => { return { name: 'Selection menu', // maxToolbarItems: 8, more: true, children: [ this.getFormattingMenu(), this.annotationsMenu(), this.modifyMenu(), this.clipboardMenu(), /* secondBrain(), { name: 'Annotations separator', sep: true, }, { name: 'Style separator', sep: true, }, { name: 'Typesetting', expand: 4, openOnTitleHov: true, icon: () => <Iconista width={16} height={16} set="tabler" icon="typography" />, onSelect: () => {}, children: [ { name: 'Sans-serif', iconBig: () => <FontStyleButton kind={'sans'} />, onSelect: () => {}, }, { name: 'Serif', iconBig: () => <FontStyleButton kind={'serif'} />, onSelect: () => {}, }, { name: 'Slab', icon: () => <FontStyleButton kind={'slab'} size={16} />, iconBig: () => <FontStyleButton kind={'slab'} />, onSelect: () => {}, }, { name: 'Monospace', iconBig: () => <FontStyleButton kind={'mono'} />, onSelect: () => {}, }, // { // name: 'Custom typeface separator', // sep: true, // }, { name: 'Custom typeface', expand: 10, icon: () => <Iconista width={15} height={15} set="radix" icon="font-style" />, children: [ { name: 'Typeface', // icon: () => <Iconista width={15} height={15} set="radix" icon="font-style" />, icon: () => <Iconista width={15} height={15} set="radix" icon="font-family" />, onSelect: () => {}, }, { name: 'Text size', icon: () => <Iconista width={15} height={15} set="radix" icon="font-size" />, onSelect: () => {}, }, { name: 'Letter spacing', icon: () => <Iconista width={15} height={15} set="radix" icon="letter-spacing" />, onSelect: () => {}, }, { name: 'Word spacing', icon: () => <Iconista width={15} height={15} set="radix" icon="letter-spacing" />, onSelect: () => {}, }, { name: 'Caps separator', sep: true, }, { name: 'Large caps', icon: () => <Iconista width={15} height={15} set="radix" icon="letter-case-uppercase" />, onSelect: () => {}, }, { name: 'Small caps', icon: () => <Iconista width={15} height={15} set="radix" icon="letter-case-lowercase" />, onSelect: () => {}, }, ], }, ], }, { name: 'Modify separator', sep: true, }, { name: 'Modify', expand: 3, onSelect: () => {}, children: [ { name: 'Pick layer', right: () => ( <Code size={-1} gray> 9+ </Code> ), more: true, icon: () => <Iconista width={15} height={15} set="radix" icon="layers" />, onSelect: () => {}, }, { name: 'Erase formatting', danger: true, icon: () => <Iconista width={16} height={16} set="tabler" icon="eraser" />, onSelect: () => {}, }, { name: 'Delete all in range', danger: true, more: true, icon: () => <Iconista width={16} height={16} set="tabler" icon="trash" />, onSelect: () => {}, }, ], }, { name: 'Clipboard separator', sep: true, }, { name: 'Copy, cut, and paste', // icon: () => <Iconista width={15} height={15} set="radix" icon="copy" />, icon: () => <Iconista width={16} height={16} set="lucide" icon="copy" />, expand: 0, children: [ { id: 'copy-menu', name: 'Copy', // icon: () => <Iconista width={15} height={15} set="radix" icon="copy" />, icon: () => <Iconista width={15} height={15} set="radix" icon="clipboard-copy" />, expand: 5, children: [ { name: 'Copy', icon: () => <Iconista width={15} height={15} set="radix" icon="clipboard-copy" />, onSelect: () => {}, },