This commit is contained in:
Igor Lobanov 2022-10-31 13:51:52 +01:00
parent 4d69749f55
commit 190a3226d5
30 changed files with 330 additions and 313 deletions

View File

@ -1,7 +1,7 @@
import styles from './Banner.module.scss'
import { t } from '../../utils/intl'
import { showModal } from '../../stores/ui'
import {clsx} from "clsx";
import { clsx } from 'clsx'
export default () => {
return (

View File

@ -4,7 +4,7 @@ import { Icon } from '../Nav/Icon'
import Subscribe from './Subscribe'
import { t } from '../../utils/intl'
import { locale } from '../../stores/ui'
import {clsx} from "clsx";
import { clsx } from 'clsx'
export const Footer = () => {
const locale_title = createMemo(() => (locale() === 'ru' ? 'English' : 'Русский'))

View File

@ -10,10 +10,10 @@ export const Editor = () => {
const onReconfigure = (text: EditorState) => ctrl.setState({ text })
const onChange = (text: EditorState) => ctrl.setState({ text, lastModified: new Date() })
// const editorCss = (config) => css``
const style = () => (store.error ? `display: none;` : (store.markdown ? `white-space: pre-wrap;` : ''))
const style = () => (store.error ? `display: none;` : store.markdown ? `white-space: pre-wrap;` : '')
return (
<ProseMirror
className='editor col-md-6 shift-content'
className="editor col-md-6 shift-content"
style={style()}
editorView={store.editorView}
text={store.text}

View File

@ -7,13 +7,13 @@ export default () => {
return (
<Switch fallback={<Other />}>
<Match when={store.error.id === 'invalid_state'}>
<InvalidState title='Invalid State' />
<InvalidState title="Invalid State" />
</Match>
<Match when={store.error.id === 'invalid_config'}>
<InvalidState title='Invalid Config' />
<InvalidState title="Invalid Config" />
</Match>
<Match when={store.error.id === 'invalid_draft'}>
<InvalidState title='Invalid Draft' />
<InvalidState title="Invalid Draft" />
</Match>
</Switch>
)
@ -24,8 +24,8 @@ const InvalidState = (props: { title: string }) => {
const onClick = () => ctrl.clean()
return (
<div class='error'>
<div class='container'>
<div class="error">
<div class="container">
<h1>{props.title}</h1>
<p>
There is an error with the editor state. This is probably due to an old version in which the data
@ -35,7 +35,7 @@ const InvalidState = (props: { title: string }) => {
<pre>
<code>{JSON.stringify(store.error.props)}</code>
</pre>
<button class='primary' onClick={onClick}>
<button class="primary" onClick={onClick}>
Clean
</button>
</div>
@ -53,13 +53,13 @@ const Other = () => {
}
return (
<div class='error'>
<div class='container'>
<div class="error">
<div class="container">
<h1>An error occurred.</h1>
<pre>
<code>{getMessage()}</code>
</pre>
<button class='primary' onClick={onClick}>
<button class="primary" onClick={onClick}>
Close
</button>
</div>

View File

@ -1,17 +1,19 @@
import type { JSX } from 'solid-js/jsx-runtime';
import type { JSX } from 'solid-js/jsx-runtime'
import type { Config } from '../store/context'
import '../styles/Layout.scss'
export type Styled = {
children: JSX.Element;
config?: Config;
'data-testid'?: string;
onClick?: () => void;
onMouseEnter?: (e: MouseEvent) => void;
children: JSX.Element
config?: Config
'data-testid'?: string
onClick?: () => void
onMouseEnter?: (e: MouseEvent) => void
}
export const Layout = (props: Styled) => {
return (<div onMouseEnter={props.onMouseEnter} class='layout container' data-testid={props['data-testid']}>
{props.children}
</div>)
return (
<div onMouseEnter={props.onMouseEnter} class="layout container" data-testid={props['data-testid']}>
{props.children}
</div>
)
}

View File

@ -6,14 +6,14 @@ import { Schema } from 'prosemirror-model'
import type { NodeViewFn, ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
interface ProseMirrorProps {
style?: string;
className?: string;
text?: Store<ProseMirrorState>;
editorView?: Store<EditorView>;
extensions?: Store<ProseMirrorExtension[]>;
onInit: (s: EditorState, v: EditorView) => void;
onReconfigure: (s: EditorState) => void;
onChange: (s: EditorState) => void;
style?: string
className?: string
text?: Store<ProseMirrorState>
editorView?: Store<EditorView>
extensions?: Store<ProseMirrorExtension[]>
onInit: (s: EditorState, v: EditorView) => void
onReconfigure: (s: EditorState) => void
onChange: (s: EditorState) => void
}
export const ProseMirror = (props: ProseMirrorProps) => {
@ -28,45 +28,39 @@ export const ProseMirror = (props: ProseMirrorProps) => {
props.onChange(newState)
}
createEffect((payload: [EditorState, ProseMirrorExtension[]]) => {
const [prevText, prevExtensions] = payload
const text = unwrap(props.text)
const extensions: ProseMirrorExtension[] = unwrap(props.extensions)
if (!text || !extensions?.length) {
createEffect(
(payload: [EditorState, ProseMirrorExtension[]]) => {
const [prevText, prevExtensions] = payload
const text = unwrap(props.text)
const extensions: ProseMirrorExtension[] = unwrap(props.extensions)
if (!text || !extensions?.length) {
return [text, extensions]
}
if (!props.editorView) {
const { editorState, nodeViews } = createEditorState(text, extensions)
const view = new EditorView(editorRef, { state: editorState, nodeViews, dispatchTransaction })
view.focus()
props.onInit(editorState, view)
return [editorState, extensions]
}
if (extensions !== prevExtensions || (!(text instanceof EditorState) && text !== prevText)) {
const { editorState, nodeViews } = createEditorState(text, extensions, prevText)
if (!editorState) return
editorView().updateState(editorState)
editorView().setProps({ nodeViews, dispatchTransaction })
props.onReconfigure(editorState)
editorView().focus()
return [editorState, extensions]
}
return [text, extensions]
}
if (!props.editorView) {
const { editorState, nodeViews } = createEditorState(text, extensions)
const view = new EditorView(editorRef, { state: editorState, nodeViews, dispatchTransaction })
view.focus()
props.onInit(editorState, view)
return [editorState, extensions]
}
if (extensions !== prevExtensions || (!(text instanceof EditorState) && text !== prevText)) {
const { editorState, nodeViews } = createEditorState(text, extensions, prevText)
if (!editorState) return
editorView().updateState(editorState)
editorView().setProps({ nodeViews, dispatchTransaction })
props.onReconfigure(editorState)
editorView().focus()
return [editorState, extensions]
}
return [text, extensions]
},
[props.text, props.extensions]
},
[props.text, props.extensions]
)
return (
<div
style={props.style}
ref={editorRef}
class={props.className}
spell-check={false}
/>
)
return <div style={props.style} ref={editorRef} class={props.className} spell-check={false} />
}
const createEditorState = (
@ -74,8 +68,8 @@ const createEditorState = (
extensions: ProseMirrorExtension[],
prevText?: EditorState
): {
editorState: EditorState;
nodeViews: { [key: string]: NodeViewFn };
editorState: EditorState
nodeViews: { [key: string]: NodeViewFn }
} => {
const reconfigure = text instanceof EditorState && prevText?.schema
let schemaSpec = { nodes: {} }
@ -104,7 +98,7 @@ const createEditorState = (
editorState = text.reconfigure({ schema, plugins } as EditorStateConfig)
} else if (text instanceof EditorState) {
editorState = EditorState.fromJSON({ schema, plugins }, text.toJSON())
} else if (text){
} else if (text) {
console.debug(text)
editorState = EditorState.fromJSON({ schema, plugins }, text)
}

View File

@ -8,16 +8,16 @@ import { isEmpty } from '../prosemirror/helpers'
import type { Styled } from './Layout'
import '../styles/Sidebar.scss'
const Off = (props) => <div class='sidebar-off'>{props.children}</div>
const Off = (props) => <div class="sidebar-off">{props.children}</div>
const Label = (props: Styled) => <h3 class='sidebar-label'>{props.children}</h3>
const Label = (props: Styled) => <h3 class="sidebar-label">{props.children}</h3>
const Link = (
props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string }
) => (
<button
class={`sidebar-link${props.className ? ' ' + props.className : ''}`}
style={{ "margin-bottom": props.withMargin ? '10px' : '' }}
style={{ 'margin-bottom': props.withMargin ? '10px' : '' }}
onClick={props.onClick}
disabled={props.disabled}
title={props.title}
@ -34,22 +34,23 @@ export const Sidebar = () => {
document.body.classList.toggle('dark')
ctrl.updateConfig({ theme: document.body.className })
}
const collabText = () => (store.collab?.started ? 'Stop' : (store.collab?.error ? 'Restart 🚨' : 'Start'))
const collabText = () => (store.collab?.started ? 'Stop' : store.collab?.error ? 'Restart 🚨' : 'Start')
const editorView = () => unwrap(store.editorView)
const onToggleMarkdown = () => ctrl.toggleMarkdown()
const onOpenDraft = (draft: Draft) => ctrl.openDraft(unwrap(draft))
const collabUsers = () => store.collab?.y?.provider.awareness.meta.size ?? 0
const onUndo = () => undo(editorView().state, editorView().dispatch)
const onRedo = () => redo(editorView().state, editorView().dispatch)
const onCopyAllAsMd = () => remote.copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md'))
const onCopyAllAsMd = () =>
remote.copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md'))
const onDiscard = () => ctrl.discard()
const [isHidden, setIsHidden] = createSignal<boolean | false>()
const toggleSidebar = () => {
setIsHidden(!isHidden());
setIsHidden(!isHidden())
}
toggleSidebar();
toggleSidebar()
const onCollab = () => {
const state = unwrap(store)
@ -84,11 +85,13 @@ export const Sidebar = () => {
}
const text = () =>
p.draft.path ? p.draft.path.slice(Math.max(0, p.draft.path.length - length)) : getContent(p.draft.text?.doc)
p.draft.path
? p.draft.path.slice(Math.max(0, p.draft.path.length - length))
: getContent(p.draft.text?.doc)
return (
// eslint-disable-next-line solid/no-react-specific-props
<Link className='draft' onClick={() => onOpenDraft(p.draft)} data-testid='open'>
<Link className="draft" onClick={() => onOpenDraft(p.draft)} data-testid="open">
{text()} {p.draft.path && '📎'}
</Link>
)
@ -96,9 +99,7 @@ export const Sidebar = () => {
const Keys = (props) => (
<span>
<For each={props.keys}>{(k: Element) => (
<i>{k}</i>
)}</For>
<For each={props.keys}>{(k: Element) => <i>{k}</i>}</For>
</span>
)
@ -116,10 +117,12 @@ export const Sidebar = () => {
return (
<div class={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}>
<span class='sidebar-opener' onClick={toggleSidebar}>Советы и&nbsp;предложения</span>
<span class="sidebar-opener" onClick={toggleSidebar}>
Советы и&nbsp;предложения
</span>
<Off onClick={() => editorView().focus()}>
<div class='sidebar-closer' onClick={toggleSidebar}/>
<div class="sidebar-closer" onClick={toggleSidebar} />
<Show when={true}>
<div>
{store.path && (
@ -127,27 +130,25 @@ export const Sidebar = () => {
<i>({store.path.slice(Math.max(0, store.path.length - 24))})</i>
</Label>
)}
<Link>
Пригласить соавторов
</Link>
<Link>
Настройки публикации
</Link>
<Link>
История правок
</Link>
<Link>Пригласить соавторов</Link>
<Link>Настройки публикации</Link>
<Link>История правок</Link>
<div class='theme-switcher'>
<div class="theme-switcher">
Ночная тема
<input type='checkbox' name='theme' id='theme' onClick={toggleTheme} />
<label for='theme'>Ночная тема</label>
<input type="checkbox" name="theme" id="theme" onClick={toggleTheme} />
<label for="theme">Ночная тема</label>
</div>
<Link
onClick={onDiscard}
disabled={!store.path && store.drafts.length === 0 && isEmpty(store.text)}
data-testid='discard'
data-testid="discard"
>
{store.path ? 'Close' : (store.drafts.length > 0 && isEmpty(store.text) ? 'Delete ⚠️' : 'Clear')}{' '}
{store.path
? 'Close'
: store.drafts.length > 0 && isEmpty(store.text)
? 'Delete ⚠️'
: 'Clear'}{' '}
<Keys keys={[mod, 'w']} />
</Link>
<Link onClick={onUndo}>
@ -156,7 +157,7 @@ export const Sidebar = () => {
<Link onClick={onRedo}>
Redo <Keys keys={[mod, 'Shift', 'z']} />
</Link>
<Link onClick={onToggleMarkdown} data-testid='markdown'>
<Link onClick={onToggleMarkdown} data-testid="markdown">
Markdown mode {store.markdown && '✅'} <Keys keys={[mod, 'm']} />
</Link>
<Link onClick={onCopyAllAsMd}>Copy all as MD {lastAction() === 'copy-md' && '📋'}</Link>

View File

@ -1,5 +1,10 @@
import markdownit from 'markdown-it'
import { MarkdownSerializer, MarkdownParser, defaultMarkdownSerializer, MarkdownSerializerState } from 'prosemirror-markdown'
import {
MarkdownSerializer,
MarkdownParser,
defaultMarkdownSerializer,
MarkdownSerializerState
} from 'prosemirror-markdown'
import type { Node, Schema } from 'prosemirror-model'
import type { EditorState } from 'prosemirror-state'
@ -12,7 +17,6 @@ export const serialize = (state: EditorState) => {
return text
}
function findAlignment(cell: Node): string | null {
const alignment = cell.attrs.style as string
if (!alignment) {

View File

@ -36,13 +36,13 @@ export default (plain = false): ProseMirrorExtension => ({
schema: () =>
plain
? {
nodes: plainSchema.spec.nodes,
marks: plainSchema.spec.marks
}
nodes: plainSchema.spec.nodes,
marks: plainSchema.spec.marks
}
: {
nodes: (markdownSchema.spec.nodes as OrderedMap<NodeSpec>).update('blockquote', blockquoteSchema),
marks: markdownSchema.spec.marks
},
nodes: (markdownSchema.spec.nodes as OrderedMap<NodeSpec>).update('blockquote', blockquoteSchema),
marks: markdownSchema.spec.marks
},
plugins: (prev, schema) => [
...prev,
keymap({

View File

@ -10,26 +10,26 @@ const blank = '\u00A0'
const onArrow =
(dir: 'left' | 'right') =>
(state: EditorState, dispatch: (tr: Transaction) => void, editorView: EditorView) => {
if (!state.selection.empty) return false
const $pos = state.selection.$head
const isCode = $pos.marks().find((m: Mark) => m.type.name === 'code')
const tr = state.tr
(state: EditorState, dispatch: (tr: Transaction) => void, editorView: EditorView) => {
if (!state.selection.empty) return false
const $pos = state.selection.$head
const isCode = $pos.marks().find((m: Mark) => m.type.name === 'code')
const tr = state.tr
if (dir === 'left') {
const up = editorView.endOfTextblock('up')
if (!$pos.nodeBefore && up && isCode) {
tr.insertText(blank, $pos.pos - 1, $pos.pos)
dispatch(tr)
}
} else {
const down = editorView.endOfTextblock('down')
if (!$pos.nodeAfter && down && isCode) {
tr.insertText(blank, $pos.pos, $pos.pos + 1)
dispatch(tr)
}
if (dir === 'left') {
const up = editorView.endOfTextblock('up')
if (!$pos.nodeBefore && up && isCode) {
tr.insertText(blank, $pos.pos - 1, $pos.pos)
dispatch(tr)
}
} else {
const down = editorView.endOfTextblock('down')
if (!$pos.nodeAfter && down && isCode) {
tr.insertText(blank, $pos.pos, $pos.pos + 1)
dispatch(tr)
}
}
}
const codeKeymap = {
ArrowLeft: onArrow('left'),

View File

@ -2,7 +2,11 @@ import { ySyncPlugin, yCursorPlugin, yUndoPlugin } from 'y-prosemirror'
import type { ProseMirrorExtension } from '../helpers'
import type { YOptions } from '../../store/context'
interface YUser { background: string, foreground: string, name: string }
interface YUser {
background: string
foreground: string
name: string
}
export const cursorBuilder = (user: YUser): HTMLElement => {
const cursor = document.createElement('span')
@ -19,10 +23,10 @@ export default (y: YOptions): ProseMirrorExtension => ({
plugins: (prev) =>
y
? [
...prev,
ySyncPlugin(y.type),
yCursorPlugin(y.provider.awareness, { cursorBuilder }),
yUndoPlugin()
]
...prev,
ySyncPlugin(y.type),
yCursorPlugin(y.provider.awareness, { cursorBuilder }),
yUndoPlugin()
]
: prev
})

View File

@ -14,7 +14,7 @@ import {
} from 'prosemirror-menu'
import { wrapInList } from 'prosemirror-schema-list'
import type{ NodeSelection } from 'prosemirror-state'
import type { NodeSelection } from 'prosemirror-state'
import { TextField, openPrompt } from './prompt'
import type { ProseMirrorExtension } from '../helpers'
@ -22,7 +22,6 @@ import type { Schema } from 'prosemirror-model'
// Helpers to create specific types of items
const cut = (something) => something.filter(Boolean)
function canInsert(state, nodeType) {
@ -45,7 +44,11 @@ function insertImageItem(nodeType) {
return canInsert(state, nodeType)
},
run(state, _, view) {
const { from, to, node: { attrs } } = state.selection as NodeSelection
const {
from,
to,
node: { attrs }
} = state.selection as NodeSelection
openPrompt({
title: 'Insert image',
@ -78,7 +81,9 @@ function cmdItem(cmd, options) {
for (const prop in options) passedOptions[prop] = options[prop]
if ((!options.enable || options.enable === true) && !options.select) { passedOptions[options.enable ? 'enable' : 'select'] = (state) => cmd(state) }
if ((!options.enable || options.enable === true) && !options.select) {
passedOptions[options.enable ? 'enable' : 'select'] = (state) => cmd(state)
}
return new MenuItem(passedOptions)
}
@ -130,7 +135,7 @@ function linkItem(markType) {
href: new TextField({
label: 'Link target',
required: true
}),
})
},
callback(attrs) {
toggleMark(markType, attrs)(view.state, view.dispatch)
@ -214,7 +219,7 @@ export function buildMenuItems(schema: Schema<any, any>) {
icon: {
width: 13,
height: 16,
path: "M9.82857 7.76C10.9371 6.99429 11.7143 5.73714 11.7143 4.57143C11.7143 1.98857 9.71428 0 7.14286 0H0V16H8.04571C10.4343 16 12.2857 14.0571 12.2857 11.6686C12.2857 9.93143 11.3029 8.44571 9.82857 7.76ZM3.42799 2.85708H6.85656C7.80513 2.85708 8.57085 3.6228 8.57085 4.57137C8.57085 5.51994 7.80513 6.28565 6.85656 6.28565H3.42799V2.85708ZM3.42799 13.1429H7.42799C8.37656 13.1429 9.14228 12.3772 9.14228 11.4286C9.14228 10.4801 8.37656 9.71434 7.42799 9.71434H3.42799V13.1429Z"
path: 'M9.82857 7.76C10.9371 6.99429 11.7143 5.73714 11.7143 4.57143C11.7143 1.98857 9.71428 0 7.14286 0H0V16H8.04571C10.4343 16 12.2857 14.0571 12.2857 11.6686C12.2857 9.93143 11.3029 8.44571 9.82857 7.76ZM3.42799 2.85708H6.85656C7.80513 2.85708 8.57085 3.6228 8.57085 4.57137C8.57085 5.51994 7.80513 6.28565 6.85656 6.28565H3.42799V2.85708ZM3.42799 13.1429H7.42799C8.37656 13.1429 9.14228 12.3772 9.14228 11.4286C9.14228 10.4801 8.37656 9.71434 7.42799 9.71434H3.42799V13.1429Z'
}
})
}
@ -225,7 +230,7 @@ export function buildMenuItems(schema: Schema<any, any>) {
icon: {
width: 14,
height: 16,
path: "M4.39216 0V3.42857H6.81882L3.06353 12.5714H0V16H8.78431V12.5714H6.35765L10.1129 3.42857H13.1765V0H4.39216Z"
path: 'M4.39216 0V3.42857H6.81882L3.06353 12.5714H0V16H8.78431V12.5714H6.35765L10.1129 3.42857H13.1765V0H4.39216Z'
}
})
}
@ -320,17 +325,16 @@ export function buildMenuItems(schema: Schema<any, any>) {
})
}
r.typeMenu = new Dropdown(
cut([r.makeHead1, r.makeHead2, r.makeHead3, r.typeMenu, r.wrapBlockQuote]),
{ label: 'Тт',
class: 'editor-dropdown' // TODO: use this class
// FIXME: icon svg code shouldn't be here
// icon: {
// width: 12,
// height: 12,
// path: "M6.39999 3.19998V0H20.2666V3.19998H14.9333V15.9999H11.7333V3.19998H6.39999ZM3.19998 8.5334H0V5.33342H9.59994V8.5334H6.39996V16H3.19998V8.5334Z"
// }
}) as MenuItem
r.typeMenu = new Dropdown(cut([r.makeHead1, r.makeHead2, r.makeHead3, r.typeMenu, r.wrapBlockQuote]), {
label: 'Тт',
class: 'editor-dropdown' // TODO: use this class
// FIXME: icon svg code shouldn't be here
// icon: {
// width: 12,
// height: 12,
// path: "M6.39999 3.19998V0H20.2666V3.19998H14.9333V15.9999H11.7333V3.19998H6.39999ZM3.19998 8.5334H0V5.33342H9.59994V8.5334H6.39996V16H3.19998V8.5334Z"
// }
}) as MenuItem
r.blockMenu = []
r.listMenu = [cut([r.wrapBulletList, r.wrapOrderedList])]
r.inlineMenu = [cut([r.toggleStrong, r.toggleEm, r.toggleMark])]

View File

@ -1,60 +1,54 @@
import { renderGrouped } from "prosemirror-menu";
import { Plugin } from "prosemirror-state";
import type { ProseMirrorExtension } from "../helpers";
import { buildMenuItems } from "./menu";
import { renderGrouped } from 'prosemirror-menu'
import { Plugin } from 'prosemirror-state'
import type { ProseMirrorExtension } from '../helpers'
import { buildMenuItems } from './menu'
export class SelectionTooltip {
tooltip: any;
tooltip: any
constructor(view: any, schema: any) {
this.tooltip = document.createElement("div");
this.tooltip.className = "tooltip";
view.dom.parentNode.appendChild(this.tooltip);
const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu as any);
this.tooltip.appendChild(dom);
this.update(view, null);
this.tooltip = document.createElement('div')
this.tooltip.className = 'tooltip'
view.dom.parentNode.appendChild(this.tooltip)
const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu as any)
this.tooltip.appendChild(dom)
this.update(view, null)
}
update(view: any, lastState: any) {
const state = view.state;
if (
lastState &&
lastState.doc.eq(state.doc) &&
lastState.selection.eq(state.selection)
)
{return;}
if (state.selection.empty) {
this.tooltip.style.display = "none";
return;
const state = view.state
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
return
}
this.tooltip.style.display = "";
const { from, to } = state.selection;
if (state.selection.empty) {
this.tooltip.style.display = 'none'
return
}
this.tooltip.style.display = ''
const { from, to } = state.selection
const start = view.coordsAtPos(from),
end = view.coordsAtPos(to);
const box = this.tooltip.offsetParent.getBoundingClientRect();
const left = Math.max((start.left + end.left) / 2, start.left + 3);
this.tooltip.style.left = left - box.left + "px";
this.tooltip.style.bottom = box.bottom - (start.top + 15) + "px";
end = view.coordsAtPos(to)
const box = this.tooltip.offsetParent.getBoundingClientRect()
const left = Math.max((start.left + end.left) / 2, start.left + 3)
this.tooltip.style.left = left - box.left + 'px'
this.tooltip.style.bottom = box.bottom - (start.top + 15) + 'px'
}
destroy() {
this.tooltip.remove();
this.tooltip.remove()
}
}
export function toolTip(schema: any) {
return new Plugin({
view(editorView: any) {
return new SelectionTooltip(editorView, schema);
},
});
return new SelectionTooltip(editorView, schema)
}
})
}
export default (): ProseMirrorExtension => ({
plugins: (prev, schema) => [
...prev,
toolTip(schema)
]
plugins: (prev, schema) => [...prev, toolTip(schema)]
})

View File

@ -10,7 +10,7 @@ export const tableInputRule = (schema: Schema) =>
new RegExp('^\\|{2,}\\s$'),
(state: EditorState, match: string[], start: number, end: number) => {
const tr = state.tr
const columns = Array.from({length: match[0].trim().length - 1})
const columns = Array.from({ length: match[0].trim().length - 1 })
const headers = columns.map(() => schema.node(schema.nodes.table_header, {}))
const cells = columns.map(() => schema.node(schema.nodes.table_cell, {}))
const table = schema.node(schema.nodes.table, {}, [

View File

@ -1,6 +1,13 @@
import { DOMOutputSpec, DOMSerializer, Node as ProsemirrorNode, NodeSpec, NodeType, Schema } from 'prosemirror-model'
import {
DOMOutputSpec,
DOMSerializer,
Node as ProsemirrorNode,
NodeSpec,
NodeType,
Schema
} from 'prosemirror-model'
import type { EditorView } from 'prosemirror-view'
import { wrappingInputRule , inputRules } from 'prosemirror-inputrules'
import { wrappingInputRule, inputRules } from 'prosemirror-inputrules'
import { splitListItem } from 'prosemirror-schema-list'
import { keymap } from 'prosemirror-keymap'
import type { NodeViewFn, ProseMirrorExtension } from '../helpers'
@ -59,8 +66,8 @@ class TodoItemView {
this.dom = res.dom
this.contentDOM = res.contentDOM
this.view = view
this.getPos = getPos;
(this.dom as HTMLElement).querySelector('input').addEventListener('click', this.handleClick.bind(this))
this.getPos = getPos
;(this.dom as HTMLElement).querySelector('input').addEventListener('click', this.handleClick.bind(this))
}
handleClick(e: MouseEvent) {
@ -90,5 +97,5 @@ export default (): ProseMirrorExtension => ({
todo_item: (node: ProsemirrorNode, view: EditorView, getPos: () => number) => {
return new TodoItemView(node, view, getPos)
}
} as unknown as { [key:string]: NodeViewFn }
} as unknown as { [key: string]: NodeViewFn }
})

View File

@ -3,9 +3,9 @@ import type { Node, Schema, SchemaSpec } from 'prosemirror-model'
import type { Decoration, EditorView, NodeView } from 'prosemirror-view'
export interface ProseMirrorExtension {
schema?: (prev: SchemaSpec) => SchemaSpec;
plugins?: (prev: Plugin[], schema: Schema) => Plugin[];
nodeViews?: { [key: string]: NodeViewFn };
schema?: (prev: SchemaSpec) => SchemaSpec
plugins?: (prev: Plugin[], schema: Schema) => Plugin[]
nodeViews?: { [key: string]: NodeViewFn }
}
export type ProseMirrorState = EditorState | unknown

View File

@ -5,7 +5,11 @@ import { Doc, XmlFragment } from 'yjs'
// import type { Reaction } from '../../../graphql/types.gen'
// import { setReactions } from '../../../stores/editor'
export const roomConnect = (room: string, username = '', keyname = 'collab'): [XmlFragment, WebrtcProvider] => {
export const roomConnect = (
room: string,
username = '',
keyname = 'collab'
): [XmlFragment, WebrtcProvider] => {
const ydoc = new Doc()
// const yarr = ydoc.getArray(keyname + '-reactions')
// TODO: use reactions

View File

@ -16,7 +16,6 @@ const isText = (x) => x && x.doc && x.selection
const isState = (x) => typeof x.lastModified !== 'string' && Array.isArray(x.drafts || [])
const isDraft = (x): boolean => x && (x.text || x.path)
export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
const [store, setState] = createStore(initial)
@ -54,8 +53,6 @@ export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
return true
}
const toggleMarkdown = () => {
const state = unwrap(store)
const editorState = store.text as EditorState
@ -65,7 +62,9 @@ export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
if (markdown) {
const lines = serialize(editorState).split('\n')
const nodes = lines.map((text) => text ? { type: 'paragraph', content: [{ type: 'text', text }] } : { type: 'paragraph' })
const nodes = lines.map((text) =>
text ? { type: 'paragraph', content: [{ type: 'text', text }] } : { type: 'paragraph' }
)
doc = { type: 'doc', content: nodes }
} else {
@ -275,26 +274,27 @@ export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
})
}
const saveState = () => debounce(async (state: State) => {
const data: State = {
lastModified: state.lastModified,
drafts: state.drafts,
config: state.config,
path: state.path,
markdown: state.markdown,
collab: {
room: state.collab?.room
const saveState = () =>
debounce(async (state: State) => {
const data: State = {
lastModified: state.lastModified,
drafts: state.drafts,
config: state.config,
path: state.path,
markdown: state.markdown,
collab: {
room: state.collab?.room
}
}
}
if (isInitialized(state.text)) {
data.text = store.editorView.state.toJSON()
} else if (state.text) {
data.text = state.text
}
if (isInitialized(state.text)) {
data.text = store.editorView.state.toJSON()
} else if (state.text) {
data.text = state.text
}
db.set('state', JSON.stringify(data))
}, 200)
db.set('state', JSON.stringify(data))
}, 200)
const setFullscreen = (fullscreen: boolean) => {
setState({ fullscreen })
@ -380,7 +380,7 @@ export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
}
const updateTheme = () => {
const { theme } = getTheme(unwrap(store))
const { theme } = getTheme(unwrap(store))
setState('config', { theme })
}

View File

@ -19,65 +19,65 @@ export interface ExtensionsProps {
typewriterMode?: boolean
}
export interface Args {
cwd?: string;
draft?: string;
room?: string;
text?: any;
cwd?: string
draft?: string
room?: string
text?: any
}
export interface PrettierConfig {
printWidth: number;
tabWidth: number;
useTabs: boolean;
semi: boolean;
singleQuote: boolean;
printWidth: number
tabWidth: number
useTabs: boolean
semi: boolean
singleQuote: boolean
}
export interface Config {
theme: string;
theme: string
// codeTheme: string;
font: string;
fontSize: number;
contentWidth: number;
typewriterMode: boolean;
prettier: PrettierConfig;
font: string
fontSize: number
contentWidth: number
typewriterMode: boolean
prettier: PrettierConfig
}
export interface ErrorObject {
id: string;
props?: unknown;
id: string
props?: unknown
}
export interface YOptions {
type: XmlFragment;
provider: WebrtcProvider;
type: XmlFragment
provider: WebrtcProvider
}
export interface Collab {
started?: boolean;
error?: boolean;
room?: string;
y?: YOptions;
started?: boolean
error?: boolean
room?: string
y?: YOptions
}
export type LoadingType = 'loading' | 'initialized'
export interface State {
isMac?: boolean
text?: ProseMirrorState;
editorView?: EditorView;
extensions?: ProseMirrorExtension[];
markdown?: boolean;
lastModified?: Date;
drafts: Draft[];
config: Config;
error?: ErrorObject;
loading?: LoadingType;
fullscreen?: boolean;
collab?: Collab;
path?: string;
args?: Args;
keymap?: { [key: string]: Command; }
text?: ProseMirrorState
editorView?: EditorView
extensions?: ProseMirrorExtension[]
markdown?: boolean
lastModified?: Date
drafts: Draft[]
config: Config
error?: ErrorObject
loading?: LoadingType
fullscreen?: boolean
collab?: Collab
path?: string
args?: Args
keymap?: { [key: string]: Command }
}
export interface Draft {
@ -91,7 +91,7 @@ export interface Draft {
export interface EditorActions {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key:string]: any
[key: string]: any
}
export class ServiceError extends Error {

View File

@ -12,7 +12,7 @@
color: var(--background);
border-color: var(--foreground);
}
.drop-cursor {
height: 2px !important;
opacity: 0.5;

View File

@ -1,5 +1,5 @@
.sidebar-container {
color: rgba(255,255,255,0.5);
color: rgba(255, 255, 255, 0.5);
font-family: 'Muller';
@include font-size(1.6rem);
overflow: hidden;
@ -143,7 +143,7 @@
}
&.draft {
color: rgba(255,255,255,0.5);
color: rgba(255, 255, 255, 0.5);
line-height: 1.4;
margin: 0 0 1em 1.5em;
width: calc(100% - 2rem);
@ -176,20 +176,22 @@
}
.theme-switcher {
border-bottom: 1px solid rgba(255,255,255,0.3);
border-top: 1px solid rgba(255,255,255,0.3);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
border-top: 1px solid rgba(255, 255, 255, 0.3);
display: flex;
justify-content: space-between;
margin: 1rem;
padding: 1em 0;
input[type=checkbox] {
input[type='checkbox'] {
opacity: 0;
position: absolute;
+ label {
background: url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.20869 7.73227C5.22953 7.36499 4.38795 6.70402 3.79906 5.83976C3.2103 4.97565 2.90318 3.95064 2.91979 2.90512C2.93639 1.8597 3.27597 0.844915 3.8919 0C2.82862 0.254038 1.87585 0.844877 1.17594 1.68438C0.475894 2.52388 0.0660276 3.5671 0.00731938 4.6585C-0.0513888 5.74989 0.244296 6.83095 0.850296 7.74073C1.45631 8.65037 2.34006 9.33992 3.36994 9.70637C4.39987 10.073 5.52063 10.0969 6.56523 9.77466C7.60985 9.45247 8.52223 8.80134 9.16667 7.91837C8.1842 8.15404 7.15363 8.08912 6.20869 7.73205V7.73227Z' fill='white'/%3E%3C/svg%3E%0A") no-repeat 30px 9px,
url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.41196 0H5.58811V2.43024H6.41196V0ZM5.99988 8.96576C4.36601 8.96576 3.03419 7.63397 3.03419 6.00007C3.04792 4.3662 4.36598 3.04818 5.99988 3.03439C7.63375 3.03439 8.96557 4.3662 8.96557 6.00007C8.96557 7.63395 7.63375 8.96576 5.99988 8.96576ZM5.58811 9.56977H6.41196V12H5.58811V9.56977ZM12.0002 5.58811H9.56996V6.41196H12.0002V5.58811ZM0 5.58811H2.43024V6.41196H0V5.58811ZM8.81339 3.76727L10.5318 2.04891L9.94925 1.46641L8.23089 3.18477L8.81339 3.76727ZM3.7745 8.8129L2.05614 10.5313L1.47364 9.94877L3.192 8.2304L3.7745 8.8129ZM9.95043 10.5269L10.5329 9.94437L8.81456 8.22601L8.23207 8.80851L9.95043 10.5269ZM3.76864 3.18731L3.18614 3.76981L1.46778 2.05145L2.05028 1.46895L3.76864 3.18731Z' fill='%231F1F1F'/%3E%3C/svg%3E%0A") #000 no-repeat 8px 8px;
background: url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.20869 7.73227C5.22953 7.36499 4.38795 6.70402 3.79906 5.83976C3.2103 4.97565 2.90318 3.95064 2.91979 2.90512C2.93639 1.8597 3.27597 0.844915 3.8919 0C2.82862 0.254038 1.87585 0.844877 1.17594 1.68438C0.475894 2.52388 0.0660276 3.5671 0.00731938 4.6585C-0.0513888 5.74989 0.244296 6.83095 0.850296 7.74073C1.45631 8.65037 2.34006 9.33992 3.36994 9.70637C4.39987 10.073 5.52063 10.0969 6.56523 9.77466C7.60985 9.45247 8.52223 8.80134 9.16667 7.91837C8.1842 8.15404 7.15363 8.08912 6.20869 7.73205V7.73227Z' fill='white'/%3E%3C/svg%3E%0A")
no-repeat 30px 9px,
url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.41196 0H5.58811V2.43024H6.41196V0ZM5.99988 8.96576C4.36601 8.96576 3.03419 7.63397 3.03419 6.00007C3.04792 4.3662 4.36598 3.04818 5.99988 3.03439C7.63375 3.03439 8.96557 4.3662 8.96557 6.00007C8.96557 7.63395 7.63375 8.96576 5.99988 8.96576ZM5.58811 9.56977H6.41196V12H5.58811V9.56977ZM12.0002 5.58811H9.56996V6.41196H12.0002V5.58811ZM0 5.58811H2.43024V6.41196H0V5.58811ZM8.81339 3.76727L10.5318 2.04891L9.94925 1.46641L8.23089 3.18477L8.81339 3.76727ZM3.7745 8.8129L2.05614 10.5313L1.47364 9.94877L3.192 8.2304L3.7745 8.8129ZM9.95043 10.5269L10.5329 9.94437L8.81456 8.22601L8.23207 8.80851L9.95043 10.5269ZM3.76864 3.18731L3.18614 3.76981L1.46778 2.05145L2.05028 1.46895L3.76864 3.18731Z' fill='%231F1F1F'/%3E%3C/svg%3E%0A")
#000 no-repeat 8px 8px;
border-radius: 14px;
cursor: pointer;
display: block;

View File

@ -7,7 +7,7 @@ export default (props: { article: Shout }) => (
<div class="floor floor--one-article">
<div class="wide-container row">
<div class="col-12">
<ArticleCard article={props.article} settings={{isSingle: true}} />
<ArticleCard article={props.article} settings={{ isSingle: true }} />
</div>
</div>
</div>

View File

@ -148,11 +148,11 @@
margin-bottom: 8px;
/* Red/500 */
color: #D00820;
color: #d00820;
a {
color: #D00820;
border-color: #D00820;
color: #d00820;
border-color: #d00820;
&:hover {
color: white;

View File

@ -44,10 +44,10 @@ export const Header = (props: Props) => {
const toggleFixed = () => setFixed(!fixed())
// effects
createEffect(() => {
const isFixed = fixed() || (modal() && modal() !== 'share');
const isFixed = fixed() || (modal() && modal() !== 'share')
document.body.classList.toggle('fixed', isFixed);
document.body.classList.toggle(styles.fixed, isFixed && !modal());
document.body.classList.toggle('fixed', isFixed)
document.body.classList.toggle(styles.fixed, isFixed && !modal())
})
// derived

View File

@ -54,28 +54,29 @@ export const ManifestPage = () => {
<div class="col-lg-10 offset-md-1">
<p>
Дискурс&nbsp;&mdash; независимый художественно-аналитический журнал с&nbsp;горизонтальной редакцией,
основанный на&nbsp;принципах свободы слова, прямой демократии и&nbsp;совместного редактирования.
Дискурс создаётся открытым медиасообществом ученых, журналистов, музыкантов, писателей,
предпринимателей, философов, инженеров, художников и&nbsp;специалистов со&nbsp;всего мира,
объединившихся, чтобы вместе делать общий журнал и&nbsp;объяснять с&nbsp;разных точек
зрения мозаичную картину современности.
Дискурс&nbsp;&mdash; независимый художественно-аналитический журнал с&nbsp;горизонтальной
редакцией, основанный на&nbsp;принципах свободы слова, прямой демократии и&nbsp;совместного
редактирования. Дискурс создаётся открытым медиасообществом ученых, журналистов, музыкантов,
писателей, предпринимателей, философов, инженеров, художников и&nbsp;специалистов
со&nbsp;всего мира, объединившихся, чтобы вместе делать общий журнал и&nbsp;объяснять
с&nbsp;разных точек зрения мозаичную картину современности.
</p>
<p>
Мы&nbsp;пишем о&nbsp;культуре, науке и&nbsp;обществе, рассказываем о&nbsp;новых идеях и&nbsp;современном искусстве,
публикуем статьи, исследования, репортажи, интервью людей, чью прямую речь стоит услышать,
и&nbsp;работы художников из&nbsp;разных стран&nbsp;&mdash; от&nbsp;фильмов и&nbsp;музыки
до&nbsp;живописи и&nbsp;фотографии. Помогая друг другу делать публикации качественнее
и&nbsp;общим голосованием выбирая лучшие материалы для журнала, мы&nbsp;создаём новую
горизонтальную журналистику, чтобы честно рассказывать о&nbsp;важном и&nbsp;интересном.
Мы&nbsp;пишем о&nbsp;культуре, науке и&nbsp;обществе, рассказываем о&nbsp;новых идеях
и&nbsp;современном искусстве, публикуем статьи, исследования, репортажи, интервью людей, чью
прямую речь стоит услышать, и&nbsp;работы художников из&nbsp;разных стран&nbsp;&mdash;
от&nbsp;фильмов и&nbsp;музыки до&nbsp;живописи и&nbsp;фотографии. Помогая друг другу делать
публикации качественнее и&nbsp;общим голосованием выбирая лучшие материалы для журнала,
мы&nbsp;создаём новую горизонтальную журналистику, чтобы честно рассказывать о&nbsp;важном
и&nbsp;интересном.
</p>
<p>
Редакция Дискурса открыта для всех: у&nbsp;нас нет цензуры, запретных тем и&nbsp;идеологических рамок.
Каждый может <a href="/create">прислать материал</a> в&nbsp;журнал
и&nbsp;<a href="/about/guide">присоединиться к&nbsp;редакции</a>. Предоставляя трибуну
для независимой журналистики и&nbsp;художественных проектов, мы&nbsp;помогаем людям
рассказывать свои истории так, чтобы они были услышаны. Мы&nbsp;убеждены: чем больше
голосов будет звучать на&nbsp;Дискурсе, тем громче в&nbsp;полифонии мнений будет слышна истина.
Редакция Дискурса открыта для всех: у&nbsp;нас нет цензуры, запретных тем
и&nbsp;идеологических рамок. Каждый может <a href="/create">прислать материал</a>{' '}
в&nbsp;журнал и&nbsp;<a href="/about/guide">присоединиться к&nbsp;редакции</a>. Предоставляя
трибуну для независимой журналистики и&nbsp;художественных проектов, мы&nbsp;помогаем людям
рассказывать свои истории так, чтобы они были услышаны. Мы&nbsp;убеждены: чем больше голосов
будет звучать на&nbsp;Дискурсе, тем громче в&nbsp;полифонии мнений будет слышна истина.
</p>
</div>
@ -91,23 +92,26 @@ export const ManifestPage = () => {
</p>
<h3 id="contribute">Предлагать материалы</h3>
<p>
<a href="/create">Создавайте</a> свои статьи и&nbsp;художественные работы&nbsp;&mdash; лучшие из них будут
опубликованы в&nbsp;журнале. Дискурс&nbsp;&mdash; некоммерческое издание, авторы публикуются
в&nbsp;журнале на&nbsp;общественных началах, получая при этом <a href="/create?collab=true">поддержку</a> редакции,
право голоса, множество других возможностей и&nbsp;читателей по&nbsp;всему миру.
<a href="/create">Создавайте</a> свои статьи и&nbsp;художественные работы&nbsp;&mdash;
лучшие из них будут опубликованы в&nbsp;журнале. Дискурс&nbsp;&mdash; некоммерческое
издание, авторы публикуются в&nbsp;журнале на&nbsp;общественных началах, получая при этом{' '}
<a href="/create?collab=true">поддержку</a> редакции, право голоса, множество других
возможностей и&nbsp;читателей по&nbsp;всему миру.
</p>
<h3 id="donate">Поддерживать проект</h3>
<p>Дискурс существует на&nbsp;пожертвования читателей. Если вам нравится журнал, пожалуйста,</p>
<p>
<a href="/about/help">поддержите</a> нашу работу. Ваши пожертвования пойдут на&nbsp;выпуск новых
материалов, оплату серверов, труда программистов, дизайнеров и&nbsp;редакторов.
Дискурс существует на&nbsp;пожертвования читателей. Если вам нравится журнал, пожалуйста,
</p>
<p>
<a href="/about/help">поддержите</a> нашу работу. Ваши пожертвования пойдут на&nbsp;выпуск
новых материалов, оплату серверов, труда программистов, дизайнеров и&nbsp;редакторов.
</p>
<h3 id="cooperation">Сотрудничать с&nbsp;журналом</h3>
<p>
Мы всегда открыты для сотрудничества и&nbsp;рады единомышленникам. Если вы хотите помогать
журналу с&nbsp;редактурой, корректурой, иллюстрациями, переводами, версткой, подкастами,
мероприятиями, фандрайзингом или как-то ещё&nbsp;&mdash; скорее пишите нам
на&nbsp;<a href="mailto:welcome@discours.io">welcome@discours.io</a>.
мероприятиями, фандрайзингом или как-то ещё&nbsp;&mdash; скорее пишите нам на&nbsp;
<a href="mailto:welcome@discours.io">welcome@discours.io</a>.
</p>
<p>
Если вы представляете некоммерческую организацию и&nbsp;хотите сделать с&nbsp;нами
@ -116,25 +120,26 @@ export const ManifestPage = () => {
</p>
<p>
Если вы разработчик и&nbsp;хотите помогать с&nbsp;развитием сайта Дискурса,{' '}
<a href="mailto:services@discours.io">присоединяйтесь к&nbsp;IT-команде самиздата</a>. Открытый
код платформы для независимой журналистики, а&nbsp;также всех наших спецпроектов
и&nbsp;медиаинструментов находится <a href="https://github.com/Discours">в&nbsp;свободном доступе на&nbsp;GitHub</a>.
<a href="mailto:services@discours.io">присоединяйтесь к&nbsp;IT-команде самиздата</a>.
Открытый код платформы для независимой журналистики, а&nbsp;также всех наших спецпроектов
и&nbsp;медиаинструментов находится{' '}
<a href="https://github.com/Discours">в&nbsp;свободном доступе на&nbsp;GitHub</a>.
</p>
<h3 id="follow">Как еще можно помочь</h3>
<p>
Советуйте Дискурс друзьям и&nbsp;знакомым. Обсуждайте и&nbsp;распространяйте наши
публикации&nbsp;&mdash; все материалы открытой редакции можно читать и&nbsp;перепечатывать
бесплатно. Подпишитесь на&nbsp;самиздат{' '}
<a href="https://vk.com/discoursio">ВКонтакте</a>,
бесплатно. Подпишитесь на&nbsp;самиздат <a href="https://vk.com/discoursio">ВКонтакте</a>,
в&nbsp;<a href="https://facebook.com/discoursio">Фейсбуке</a>
и&nbsp;в&nbsp;<a href="https://t.me/discoursio">Телеграме</a>, а&nbsp;также
на&nbsp;<Opener name="subscribe">рассылку лучших материалов</Opener>,
чтобы не&nbsp;пропустить ничего интересного.
и&nbsp;в&nbsp;<a href="https://t.me/discoursio">Телеграме</a>, а&nbsp;также на&nbsp;
<Opener name="subscribe">рассылку лучших материалов</Opener>, чтобы не&nbsp;пропустить
ничего интересного.
</p>
<p>
<a href="https://forms.gle/9UnHBAz9Q3tjH5dAA">Рассказывайте о&nbsp;впечатлениях</a>
от&nbsp;материалов открытой редакции, <Opener name="feedback">делитесь идеями</Opener>,
интересными темами, о&nbsp;которых хотели бы узнать больше, и&nbsp;историями, которые нужно рассказать.
интересными темами, о&nbsp;которых хотели бы узнать больше, и&nbsp;историями, которые нужно
рассказать.
</p>
</div>
@ -145,9 +150,9 @@ export const ManifestPage = () => {
<div class="col-lg-10 offset-md-1">
Если вы хотите предложить материал, сотрудничать, рассказать о&nbsp;проблеме, которую нужно
осветить, сообщить об&nbsp;ошибке или баге, что-то обсудить, уточнить или посоветовать,
пожалуйста, <Opener name="feedback">напишите нам здесь</Opener> или
на&nbsp;почту <a href="mailto:welcome@discours.io">welcome@discours.io</a>. Мы обязательно
ответим и&nbsp;постараемся реализовать все хорошие задумки.
пожалуйста, <Opener name="feedback">напишите нам здесь</Opener> или на&nbsp;почту{' '}
<a href="mailto:welcome@discours.io">welcome@discours.io</a>. Мы обязательно ответим
и&nbsp;постараемся реализовать все хорошие задумки.
</div>
</div>
</div>

View File

@ -47,7 +47,6 @@ import { CreatePage } from './Pages/CreatePage'
// const ThanksPage = lazy(() => import('./Pages/about/ThanksPage'))
// const CreatePage = lazy(() => import('./Pages/about/CreatePage'))
type RootSearchParams = {
modal: string
lang: string

View File

@ -1,6 +1,5 @@
import { gql } from '@urql/core'
export default gql`
mutation CreateShoutMutations($shout: ShoutInput!) {
createShout(input: $shout) {

View File

@ -1,6 +1,5 @@
import { gql } from '@urql/core'
export default gql`
mutation DeleteShoutMutation($shout: String!) {
deleteShout(slug: $shout) {

View File

@ -1,6 +1,5 @@
import { gql } from '@urql/core'
export default gql`
mutation DeleteReactionMutation($id: Int!) {
deleteReaction(id: $id) {

View File

@ -13,7 +13,7 @@
--danger-color: #fc6847;
--lightgray-color: rgb(84 16 17 / 6%);
--font: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, oxygen, ubuntu, cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
'Helvetica Neue', sans-serif;
}
* {