Merge pull request #48 from Discours/editor2

Editor 2
This commit is contained in:
Igor Lobanov 2022-11-02 19:03:02 +01:00 committed by GitHub
commit d903f233cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 539 additions and 488 deletions

View File

@ -1,3 +1,61 @@
.editor {
flex: 1;
padding-top: 1em;
a {
color: rgb(0 100 200);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:visited {
color: rgb(0 80 160);
}
label {
display: block;
}
input,
button,
select,
textarea {
font-family: inherit;
font-size: inherit;
-webkit-padding: 0.4em 0;
padding: 0.4em;
margin: 0 0 0.5em;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
button {
color: #333;
background-color: #f4f4f4;
outline: none;
}
button:disabled {
color: #999;
}
button:not(:disabled):active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}
}
.error {
display: none;
}
@ -5,3 +63,11 @@
.markdown {
white-space: pre-wrap;
}
.tooltip {
background: #fff;
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
color: #000;
display: flex;
position: absolute;
}

View File

@ -2,7 +2,6 @@ import type { EditorView } from 'prosemirror-view'
import type { EditorState } from 'prosemirror-state'
import { useState } from '../store/context'
import { ProseMirror } from './ProseMirror'
import '../styles/Editor.scss'
import styles from './Editor.module.scss'
import { clsx } from 'clsx'
@ -14,7 +13,7 @@ export const Editor = () => {
return (
<ProseMirror
cssClass={clsx('editor', 'col-md-6', 'shift-content', {
cssClass={clsx(styles.editor, 'col-md-6', 'shift-content', {
[styles.error]: store.error,
[styles.markdown]: store.markdown
})}

View File

@ -0,0 +1,52 @@
.error {
width: 100%;
overflow: auto;
padding: 50px;
display: flex;
justify-content: center;
::-webkit-scrollbar {
display: none;
}
button {
height: 50px;
padding: 0 20px;
font-size: 18px;
cursor: pointer;
display: inline-flex;
justify-content: center;
align-items: center;
outline: none;
text-decoration: none;
background: none;
font-family: inherit;
color: var(--foreground);
border: 1px solid var(--foreground);
&.primary {
color: var(--primary-foreground);
border: 0;
background: var(--primary-background);
}
&:hover {
opacity: 0.8;
}
}
.container {
max-width: 800px;
width: 100%;
height: fit-content;
}
pre {
background: var(--foreground);
border: 1px solid var(--foreground);
white-space: pre-wrap;
word-wrap: break-word;
border-radius: 2px;
padding: 10px;
}
}

View File

@ -1,6 +1,6 @@
import { Switch, Match } from 'solid-js'
import { useState } from '../store/context'
import '../styles/Button.scss'
import styles from './Error.module.scss'
export default () => {
const [store] = useState()
@ -24,8 +24,8 @@ const InvalidState = (props: { title: string }) => {
const onClick = () => ctrl.clean()
return (
<div class="error">
<div class="container">
<div class={styles.error}>
<div class={styles.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={styles.primary} onClick={onClick}>
Clean
</button>
</div>
@ -53,13 +53,13 @@ const Other = () => {
}
return (
<div class="error">
<div class="container">
<div class={styles.error}>
<div class={styles.container}>
<h1>An error occurred.</h1>
<pre>
<code>{getMessage()}</code>
</pre>
<button class="primary" onClick={onClick}>
<button class={styles.primary} onClick={onClick}>
Close
</button>
</div>

View File

@ -6,7 +6,7 @@
border-color: var(--background);
min-height: 100vh;
&.dark {
.dark & {
background: var(--foreground);
color: var(--background);
border-color: var(--foreground);

View File

@ -1,6 +1,7 @@
import type { JSX } from 'solid-js/jsx-runtime'
import type { Config } from '../store/context'
import '../styles/Layout.scss'
import { clsx } from 'clsx'
import styles from './Layout.module.scss'
export type Styled = {
children: JSX.Element
@ -12,7 +13,11 @@ export type Styled = {
export const Layout = (props: Styled) => {
return (
<div onMouseEnter={props.onMouseEnter} class="layout container" data-testid={props['data-testid']}>
<div
onMouseEnter={props.onMouseEnter}
class={clsx(styles.layout, 'container')}
data-testid={props['data-testid']}
>
{props.children}
</div>
)

View File

@ -4,6 +4,7 @@ import { EditorState, EditorStateConfig, Transaction } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
import { Schema } from 'prosemirror-model'
import type { NodeViewFn, ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
import '../styles/ProseMirror.scss'
interface ProseMirrorProps {
cssClass?: string

View File

@ -1,3 +1,221 @@
.withMargin {
margin-bottom: 10px;
.sidebarContainer {
color: rgb(255 255 255 / 50%);
@include font-size(1.6rem);
overflow: hidden;
position: relative;
top: 0;
p {
color: var(--foreground);
}
h4 {
@include font-size(120%);
margin-left: 1rem;
}
button {
height: auto;
min-height: 50px;
padding: 0 1rem;
width: 100%;
}
}
.sidebarOff {
background: #1f1f1f;
height: 100%;
min-height: 100vh;
padding: 40px 20px 20px;
top: 0;
transform: translateX(0);
transition: transform 0.3s;
overflow-y: auto;
scrollbar-width: none;
width: 350px;
.sidebarContainerHidden & {
transform: translateX(100%);
}
::-webkit-scrollbar {
display: none;
}
}
.sidebarOpener {
color: #000;
cursor: pointer;
opacity: 1;
position: absolute;
top: 1em;
transition: opacity 0.3s;
&:hover {
opacity: 0.5;
}
&::after {
background-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cmask id='mask0_1090_23825' style='mask-type:alpha' maskUnits='userSpaceOnUse' x='0' y='14' width='4' height='4'%3E%3Crect y='14.8237' width='3.17647' height='3.17647' fill='%23fff'/%3E%3C/mask%3E%3Cg mask='url(%23mask0_1090_23825)'%3E%3Cpath d='M16.0941 1.05908H0.847027C0.379194 1.05908 0 1.43828 0 1.90611V18.0003L3.38824 14.612H16.0942C16.562 14.612 16.9412 14.2328 16.9412 13.765V1.90614C16.9412 1.43831 16.562 1.05912 16.0942 1.05912L16.0941 1.05908ZM15.2471 12.9179H1.69412V2.7532H15.2471V12.9179Z' fill='black'/%3E%3C/g%3E%3Crect x='1' y='1' width='16' height='12.8235' stroke='black' stroke-width='2'/%3E%3Crect x='4.23535' y='3.17627' width='9.52941' height='2.11765' fill='black'/%3E%3Crect x='4.23535' y='9.5293' width='7.41176' height='2.11765' fill='black'/%3E%3Crect x='4.23535' y='6.35303' width='5.29412' height='2.11765' fill='black'/%3E%3C/svg%3E");
content: '';
height: 18px;
left: 100%;
margin-left: 0.3em;
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 18px;
}
}
.sidebarCloser {
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13.1517 0.423857L0.42375 13.1518L2.84812 15.5761L15.576 2.84822L13.1517 0.423857Z M15.576 13.1518L2.84812 0.423855L0.423751 2.84822L13.1517 15.5761L15.576 13.1518Z' fill='white'/%3E%3C/svg%3E%0A");
cursor: pointer;
height: 16px;
opacity: 1;
position: absolute;
transition: opacity 0.3s;
top: 20px;
width: 16px;
&:hover {
opacity: 0.5;
}
}
.sidebarLabel {
color: var(--foreground);
> i {
text-transform: none;
}
}
.sidebarContainer button,
.sidebarContainer a,
.sidebarItem {
margin: 0;
outline: none;
display: flex;
align-items: center;
line-height: 24px;
text-align: left;
}
.sidebarContainer a,
.sidebarItem {
font-size: 18px;
padding: 2px 0;
width: 100%;
}
.sidebarLink {
background: none;
border: 0;
color: inherit;
cursor: pointer;
font-size: inherit;
justify-content: flex-start;
&:hover {
color: #fff !important;
}
&:active {
> span i {
position: relative;
box-shadow: none;
top: 1px;
}
}
&[disabled] {
color: var(--foreground);
cursor: not-allowed;
}
&.draft {
color: rgb(255 255 255 / 50%);
line-height: 1.4;
margin: 0 0 1em 1.5em;
width: calc(100% - 2rem);
&:hover {
background: none;
}
}
> span {
justify-self: flex-end;
margin-left: auto;
> i {
border: 1px solid;
border-bottom-width: 2px;
border-radius: 0.2rem;
display: inline-block;
color: inherit;
font-size: 13px;
line-height: 1.4;
margin: 0 0.5em 0 0;
padding: 1px 4px;
&:last-child {
text-transform: uppercase;
}
}
}
}
.themeSwitcher {
border-bottom: 1px solid rgb(255 255 255 / 30%);
border-top: 1px solid rgb(255 255 255 / 30%);
display: flex;
justify-content: space-between;
margin: 1rem;
padding: 1em 0;
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;
border-radius: 14px;
cursor: pointer;
display: block;
height: 28px;
line-height: 10em;
overflow: hidden;
position: relative;
transition: background-color 0.3s;
width: 46px;
&::before {
background-color: #fff;
border-radius: 100%;
content: '';
height: 16px;
left: 6px;
position: absolute;
top: 6px;
transition: left 0.3s, color 0.3s;
width: 16px;
}
}
&:checked + label {
background-color: #fff;
&::before {
background-color: #1f1f1f;
left: 24px;
}
}
}
}

View File

@ -1,24 +1,25 @@
import { For, Show, createEffect, createSignal, onCleanup } from 'solid-js'
import { For, Show, createEffect, createSignal, onCleanup, onMount } from 'solid-js'
import { unwrap } from 'solid-js/store'
import { undo, redo } from 'prosemirror-history'
import { Draft, useState } from '../store/context'
import { mod } from '../env'
import * as remote from '../remote'
import { isEmpty } from '../prosemirror/helpers'
import type { Styled } from './Layout'
import '../styles/Sidebar.scss'
import { clsx } from 'clsx'
import styles from './Sidebar.module.scss'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
import { hideModal } from '../../../stores/ui'
const Off = (props) => <div class="sidebar-off">{props.children}</div>
const Off = (props) => <div class={styles.sidebarOff}>{props.children}</div>
const Label = (props: Styled) => <h3 class="sidebar-label">{props.children}</h3>
const Label = (props: Styled) => <h3 class={styles.sidebarLabel}>{props.children}</h3>
const Link = (
props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string }
) => (
<button
class={clsx('sidebar-link', props.className, {
class={clsx(styles.sidebarLink, props.className, {
[styles.withMargin]: props.withMargin
})}
onClick={props.onClick}
@ -33,10 +34,12 @@ const Link = (
export const Sidebar = () => {
const [store, ctrl] = useState()
const [lastAction, setLastAction] = createSignal<string | undefined>()
const toggleTheme = () => {
document.body.classList.toggle('dark')
ctrl.updateConfig({ theme: document.body.className })
}
const collabText = () => {
if (store.collab?.started) {
return 'Stop'
@ -70,14 +73,12 @@ export const Sidebar = () => {
const onCopyAllAsMd = () =>
remote.copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md'))
const onDiscard = () => ctrl.discard()
const [isHidden, setIsHidden] = createSignal<boolean | false>()
const [isHidden, setIsHidden] = createSignal(true)
const toggleSidebar = () => {
setIsHidden(!isHidden())
setIsHidden((oldIsHidden) => !oldIsHidden)
}
toggleSidebar()
const onCollab = () => {
const state = unwrap(store)
store.collab?.started ? ctrl.stopCollab(state) : ctrl.startCollab(state)
@ -117,7 +118,7 @@ export const Sidebar = () => {
return (
// eslint-disable-next-line solid/no-react-specific-props
<Link className="draft" onClick={() => onOpenDraft(p.draft)} data-testid="open">
<Link className={styles.draft} onClick={() => onOpenDraft(p.draft)} data-testid="open">
{text()} {p.draft.path && '📎'}
</Link>
)
@ -131,7 +132,7 @@ export const Sidebar = () => {
createEffect(() => {
setLastAction()
}, store.lastModified)
})
createEffect(() => {
if (!lastAction()) return
@ -141,63 +142,84 @@ export const Sidebar = () => {
onCleanup(() => clearTimeout(id))
})
const [mod, setMod] = createSignal<'Ctrl' | 'Cmd'>('Ctrl')
onMount(() => {
setMod(navigator.platform.includes('Mac') ? 'Cmd' : 'Ctrl')
})
const containerRef: { current: HTMLElement } = {
current: null
}
useEscKeyDownHandler(() => setIsHidden(true))
useOutsideClickHandler({
containerRef,
predicate: () => !isHidden(),
handler: () => setIsHidden(true)
})
return (
<div class={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}>
<span class="sidebar-opener" onClick={toggleSidebar}>
<div
class={clsx(styles.sidebarContainer, {
[styles.sidebarContainerHidden]: isHidden()
})}
ref={(el) => (containerRef.current = el)}
>
<span class={styles.sidebarOpener} onClick={toggleSidebar}>
Советы и&nbsp;предложения
</span>
<Off onClick={() => editorView().focus()}>
<div class="sidebar-closer" onClick={toggleSidebar} />
<Show when={true}>
<div>
{store.path && (
<Label>
<i>({store.path.slice(Math.max(0, store.path.length - 24))})</i>
</Label>
)}
<Link>Пригласить соавторов</Link>
<Link>Настройки публикации</Link>
<Link>История правок</Link>
<div class={styles.sidebarCloser} onClick={toggleSidebar} />
<div class="theme-switcher">
Ночная тема
<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"
>
{discardText()} <Keys keys={[mod, 'w']} />
</Link>
<Link onClick={onUndo}>
Undo <Keys keys={[mod, 'z']} />
</Link>
<Link onClick={onRedo}>
Redo <Keys keys={[mod, 'Shift', 'z']} />
</Link>
<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>
<Show when={store.drafts.length > 0}>
<h4>Drafts:</h4>
<p>
<For each={store.drafts}>{(draft) => <DraftLink draft={draft} />}</For>
</p>
</Show>
<Link onClick={onCollab} title={store.collab?.error ? 'Connection error' : ''}>
Collab {collabText()}
</Link>
<Show when={collabUsers() > 0}>
<span>
{collabUsers()} {collabUsers() === 1 ? 'user' : 'users'} connected
</span>
</Show>
<div>
{store.path && (
<Label>
<i>({store.path.slice(Math.max(0, store.path.length - 24))})</i>
</Label>
)}
<Link>Пригласить соавторов</Link>
<Link>Настройки публикации</Link>
<Link>История правок</Link>
<div class={styles.themeSwitcher}>
Ночная тема
<input type="checkbox" name="theme" id="theme" onClick={toggleTheme} />
<label for="theme">Ночная тема</label>
</div>
</Show>
<Link
onClick={onDiscard}
disabled={!store.path && store.drafts.length === 0 && isEmpty(store.text)}
data-testid="discard"
>
{discardText()} <Keys keys={[mod(), 'w']} />
</Link>
<Link onClick={onUndo}>
Undo <Keys keys={[mod(), 'z']} />
</Link>
<Link onClick={onRedo}>
Redo <Keys keys={[mod(), 'Shift', 'z']} />
</Link>
<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>
<Show when={store.drafts.length > 0}>
<h4>Drafts:</h4>
<p>
<For each={store.drafts}>{(draft) => <DraftLink draft={draft} />}</For>
</p>
</Show>
<Link onClick={onCollab} title={store.collab?.error ? 'Connection error' : ''}>
Collab {collabText()}
</Link>
<Show when={collabUsers() > 0}>
<span>
{collabUsers()} {collabUsers() === 1 ? 'user' : 'users'} connected
</span>
</Show>
</div>
</Off>
</div>
)

View File

@ -1,5 +1,6 @@
const dbPromise = async () => {
const { openDB } = await import('idb')
import { openDB } from 'idb'
const dbPromise = () => {
return openDB('discours.io', 2, {
upgrade(db) {
db.createObjectStore('keyval')

View File

@ -1,3 +1,2 @@
export const isDark = () => (window as any).matchMedia('(prefers-color-scheme: dark)').matches
export const mod = 'Ctrl'
export const alt = 'Alt'
export const isDark = () =>
typeof window !== undefined && window.matchMedia('(prefers-color-scheme: dark)').matches

View File

@ -9,6 +9,8 @@ import { keymap } from 'prosemirror-keymap'
import type { ProseMirrorExtension } from '../helpers'
import type OrderedMap from 'orderedmap'
import layoutStyles from '../../components/Layout.module.scss'
const plainSchema = new Schema({
nodes: {
doc: {
@ -53,6 +55,6 @@ export default (plain = false): ProseMirrorExtension => ({
keymap(buildKeymap(schema)),
keymap(baseKeymap),
history(),
dropCursor({ class: 'drop-cursor' })
dropCursor({ class: layoutStyles.dropCursor })
]
})

View File

@ -2,13 +2,14 @@ import { renderGrouped } from 'prosemirror-menu'
import { Plugin } from 'prosemirror-state'
import type { ProseMirrorExtension } from '../helpers'
import { buildMenuItems } from './menu'
import editorStyles from '../../components/Editor.module.scss'
export class SelectionTooltip {
tooltip: any
constructor(view: any, schema: any) {
this.tooltip = document.createElement('div')
this.tooltip.className = 'tooltip'
this.tooltip.className = editorStyles.tooltip
view.dom.parentNode.appendChild(this.tooltip)
const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu as any)
this.tooltip.appendChild(dom)

View File

@ -7,7 +7,6 @@ import { undo as yUndo, redo as yRedo } from 'y-prosemirror'
import debounce from 'lodash/debounce'
import { createSchema, createExtensions, createEmptyText } from '../prosemirror/setup'
import { State, Draft, Config, ServiceError, newState, ExtensionsProps, EditorActions } from './context'
import { mod } from '../env'
import { serialize, createMarkdownParser } from '../markdown'
import db from '../db'
import { isEmpty, isInitialized } from '../prosemirror/helpers'
@ -102,13 +101,13 @@ export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
return true
}
const keymap = {
[`${mod}-w`]: discard,
[`${mod}-z`]: onUndo,
[`Shift-${mod}-z`]: onRedo,
[`${mod}-y`]: onRedo,
[`${mod}-m`]: toggleMarkdown
} as ExtensionsProps['keymap']
const keymap: ExtensionsProps['keymap'] = {
[`Mod-w`]: discard,
[`Mod-z`]: onUndo,
[`Shift-Mod-z`]: onRedo,
[`Mod-y`]: onRedo,
[`Mod-m`]: toggleMarkdown
}
const createTextFromDraft = async (draft: Draft) => {
const state = unwrap(store)

View File

@ -63,7 +63,6 @@ export interface Collab {
export type LoadingType = 'loading' | 'initialized'
export interface State {
isMac?: boolean
text?: ProseMirrorState
editorView?: EditorView
extensions?: ProseMirrorExtension[]

View File

@ -1,25 +0,0 @@
button {
height: 50px;
padding: 0 20px;
font-size: 18px;
cursor: pointer;
display: inline-flex;
justify-content: center;
align-items: center;
outline: none;
text-decoration: none;
background: none;
font-family: inherit;
color: var(--foreground);
border: 1px solid var(--foreground);
&:hover {
opacity: 0.8;
}
}
button.primary {
color: var(--primary-foreground);
border: 0;
background: var(--primary-background);
}

View File

@ -1,26 +0,0 @@
.error {
width: 100%;
overflow: y-auto;
padding: 50px;
display: flex;
justify-content: center;
::-webkit-scrollbar {
display: none;
}
}
.error .container {
max-width: 800px;
width: 100%;
height: fit-content;
}
.error pre {
background: var(--foreground);
border: 1px solid var(--foreground);
white-space: pre-wrap;
word-wrap: break-word;
border-radius: 2px;
padding: 10px;
}

View File

@ -1,3 +0,0 @@
.index {
width: 350px;
}

View File

@ -1,61 +1,3 @@
.editor {
flex: 1;
padding-top: 1em;
a {
color: rgb(0 100 200);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:visited {
color: rgb(0 80 160);
}
}
label {
display: block;
}
input,
button,
select,
textarea {
font-family: inherit;
font-size: inherit;
-webkit-padding: 0.4em 0;
padding: 0.4em;
margin: 0 0 0.5em;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
button {
color: #333;
background-color: #f4f4f4;
outline: none;
}
button:disabled {
color: #999;
}
button:not(:disabled):active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}
.ProseMirror {
color: var(--foreground);
background-color: var(--background);
@ -385,11 +327,3 @@ li.ProseMirror-selectednode::after {
background: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13.1512 0.423856L0.423263 13.1518L2.84763 15.5761L15.5756 2.84822L13.1512 0.423856Z M15.5755 13.1518L2.84763 0.423855L0.423263 2.84822L13.1512 15.5761L15.5755 13.1518Z' fill='%23393840'/%3E%3C/svg%3E%0A")
center no-repeat;
}
.tooltip {
background: #fff;
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
color: #000;
display: flex;
position: absolute;
}

View File

@ -1,226 +0,0 @@
.sidebar-container {
color: rgb(255 255 255 / 50%);
@include font-size(1.6rem);
overflow: hidden;
position: relative;
top: 0;
p {
color: var(--foreground);
}
h4 {
@include font-size(120%);
margin-left: 1rem;
}
button {
height: auto;
min-height: 50px;
padding: 0 1rem;
width: 100%;
}
}
.sidebar-off {
background: #1f1f1f;
height: 100%;
min-height: 100vh;
padding: 40px 20px 20px;
top: 0;
transform: translateX(0);
transition: transform 0.3s;
overflow-y: auto;
scrollbar-width: none;
width: 350px;
.sidebar-container--hidden & {
transform: translateX(100%);
}
::-webkit-scrollbar {
display: none;
}
}
.sidebar-opener {
color: #000;
cursor: pointer;
opacity: 1;
position: absolute;
top: 1em;
transition: opacity 0.3s;
&:hover {
opacity: 0.5;
}
&::after {
background-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cmask id='mask0_1090_23825' style='mask-type:alpha' maskUnits='userSpaceOnUse' x='0' y='14' width='4' height='4'%3E%3Crect y='14.8237' width='3.17647' height='3.17647' fill='%23fff'/%3E%3C/mask%3E%3Cg mask='url(%23mask0_1090_23825)'%3E%3Cpath d='M16.0941 1.05908H0.847027C0.379194 1.05908 0 1.43828 0 1.90611V18.0003L3.38824 14.612H16.0942C16.562 14.612 16.9412 14.2328 16.9412 13.765V1.90614C16.9412 1.43831 16.562 1.05912 16.0942 1.05912L16.0941 1.05908ZM15.2471 12.9179H1.69412V2.7532H15.2471V12.9179Z' fill='black'/%3E%3C/g%3E%3Crect x='1' y='1' width='16' height='12.8235' stroke='black' stroke-width='2'/%3E%3Crect x='4.23535' y='3.17627' width='9.52941' height='2.11765' fill='black'/%3E%3Crect x='4.23535' y='9.5293' width='7.41176' height='2.11765' fill='black'/%3E%3Crect x='4.23535' y='6.35303' width='5.29412' height='2.11765' fill='black'/%3E%3C/svg%3E");
content: '';
height: 18px;
left: 100%;
margin-left: 0.3em;
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 18px;
}
}
.sidebar-closer {
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13.1517 0.423857L0.42375 13.1518L2.84812 15.5761L15.576 2.84822L13.1517 0.423857Z M15.576 13.1518L2.84812 0.423855L0.423751 2.84822L13.1517 15.5761L15.576 13.1518Z' fill='white'/%3E%3C/svg%3E%0A");
cursor: pointer;
height: 16px;
opacity: 1;
position: absolute;
transition: opacity 0.3s;
top: 20px;
width: 16px;
&:hover {
opacity: 0.5;
}
}
.sidebar-label {
color: var(--foreground);
> i {
text-transform: none;
}
}
.sidebar-sub {
margin: 10px 0;
margin-bottom: 30px;
}
.sidebar-container button,
.sidebar-container a,
.sidebar-item {
margin: 0;
outline: none;
display: flex;
align-items: center;
line-height: 24px;
text-align: left;
}
.sidebar-container a,
.sidebar-item {
font-size: 18px;
padding: 2px 0;
width: 100%;
}
.sidebar-link {
background: none;
border: 0;
color: inherit;
cursor: pointer;
font-size: inherit;
justify-content: flex-start;
&:hover {
color: #fff !important;
}
&:active {
> span i {
position: relative;
box-shadow: none;
top: 1px;
}
}
&[disabled] {
color: var(--foreground);
cursor: not-allowed;
}
&.draft {
color: rgb(255 255 255 / 50%);
line-height: 1.4;
margin: 0 0 1em 1.5em;
width: calc(100% - 2rem);
&:hover {
background: none;
}
}
> span {
justify-self: flex-end;
margin-left: auto;
> i {
border: 1px solid;
border-bottom-width: 2px;
border-radius: 0.2rem;
display: inline-block;
color: inherit;
font-size: 13px;
line-height: 1.4;
margin: 0 0.5em 0 0;
padding: 1px 4px;
&:last-child {
text-transform: uppercase;
}
}
}
}
.theme-switcher {
border-bottom: 1px solid rgb(255 255 255 / 30%);
border-top: 1px solid rgb(255 255 255 / 30%);
display: flex;
justify-content: space-between;
margin: 1rem;
padding: 1em 0;
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;
border-radius: 14px;
cursor: pointer;
display: block;
height: 28px;
line-height: 10em;
overflow: hidden;
position: relative;
transition: background-color 0.3s;
width: 46px;
&::before {
background-color: #fff;
border-radius: 100%;
content: '';
height: 16px;
left: 6px;
position: absolute;
top: 6px;
transition: left 0.3s, color 0.3s;
width: 16px;
}
}
&:checked + label {
background-color: #fff;
&::before {
background-color: #1f1f1f;
left: 24px;
}
}
}
}

View File

@ -134,7 +134,7 @@ export const Header = (props: Props) => {
<div class={styles.usernav}>
<div class={clsx(styles.userControl, styles.userControl, 'col')}>
<div class={clsx(styles.userControlItem, styles.userControlItemWritePost)}>
<a href="/create">
<a href="/create" onClick={handleClientRouteLinkClick}>
<span class={styles.textLabel}>{t('Create post')}</span>
<Icon name="pencil" class={styles.icon} />
</a>

View File

@ -3,6 +3,7 @@ import type { JSX } from 'solid-js'
import { getLogger } from '../../utils/logger'
import './Modal.scss'
import { hideModal, useModalStore } from '../../stores/ui'
import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler'
const log = getLogger('modal')
@ -11,10 +12,6 @@ interface ModalProps {
children: JSX.Element
}
const keydownHandler = (e: KeyboardEvent) => {
if (e.key === 'Escape') hideModal()
}
export const Modal = (props: ModalProps) => {
const { modal } = useModalStore()
@ -22,13 +19,7 @@ export const Modal = (props: ModalProps) => {
if (event.target.classList.contains('modalwrap')) hideModal()
}
onMount(() => {
window.addEventListener('keydown', keydownHandler)
onCleanup(() => {
window.removeEventListener('keydown', keydownHandler)
})
})
useEscKeyDownHandler(() => hideModal())
const [visible, setVisible] = createSignal(false)

View File

@ -1,6 +1,7 @@
import { createEffect, createSignal, JSX, onCleanup, onMount, Show } from 'solid-js'
import styles from './Popup.module.scss'
import { clsx } from 'clsx'
import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler'
type HorizontalAnchor = 'center' | 'right'
@ -22,29 +23,18 @@ export const Popup = (props: PopupProps) => {
}
})
let container: HTMLDivElement | undefined
const containerRef: { current: HTMLElement } = { current: null }
const handleClickOutside = (event: MouseEvent & { target: Element }) => {
if (!isVisible()) {
return
}
if (event.target === container || container?.contains(event.target)) {
return
}
setIsVisible(false)
}
onMount(() => {
document.addEventListener('click', handleClickOutside, { capture: true })
onCleanup(() => document.removeEventListener('click', handleClickOutside, { capture: true }))
useOutsideClickHandler({
containerRef,
predicate: () => isVisible(),
handler: () => setIsVisible(false)
})
const toggle = () => setIsVisible((oldVisible) => !oldVisible)
return (
<span class={clsx(styles.container, props.containerCssClass)} ref={container}>
<span class={clsx(styles.container, props.containerCssClass)} ref={(el) => (containerRef.current = el)}>
<span onClick={toggle}>{props.trigger}</span>
<Show when={isVisible()}>
<div

View File

@ -1,11 +1,15 @@
import { newState } from '../Editor/store/context'
import { lazy, Suspense } from 'solid-js'
import { MainLayout } from '../Layouts/MainLayout'
import { CreateView } from '../Views/Create'
import { Loading } from '../Loading'
const CreateView = lazy(() => import('../Views/Create'))
export const CreatePage = () => {
return (
<MainLayout>
<CreateView state={newState()} />
<Suspense fallback={<Loading />}>
<CreateView />
</Suspense>
</MainLayout>
)
}

View File

@ -1,6 +1,6 @@
import { Show, onCleanup, createEffect, onError, onMount, untrack } from 'solid-js'
import { Show, onCleanup, createEffect, onError, onMount, untrack, createSignal } from 'solid-js'
import { createMutable, unwrap } from 'solid-js/store'
import { State, StateContext } from '../Editor/store/context'
import { State, StateContext, newState } from '../Editor/store/context'
import { createCtrl } from '../Editor/store/actions'
import { Layout } from '../Editor/components/Layout'
import { Editor } from '../Editor/components/Editor'
@ -9,16 +9,18 @@ import ErrorView from '../Editor/components/Error'
const matchDark = () => window.matchMedia('(prefers-color-scheme: dark)')
export const CreateView = (props: { state: State }) => {
let isMac = false
export const CreateView = () => {
const [isMounted, setIsMounted] = createSignal(false)
onMount(() => setIsMounted(true))
const onChangeTheme = () => ctrl.updateTheme()
onMount(() => {
isMac = window?.navigator.platform.includes('Mac')
matchDark().addEventListener('change', onChangeTheme)
onCleanup(() => matchDark().removeEventListener('change', onChangeTheme))
})
const [store, ctrl] = createCtrl({ ...props.state, isMac })
const [store, ctrl] = createCtrl(newState())
const mouseEnterCoords = createMutable({ x: 0, y: 0 })
const onMouseEnter = (e: MouseEvent) => {
@ -52,17 +54,21 @@ export const CreateView = (props: { state: State }) => {
}, store.loading)
return (
<StateContext.Provider value={[store, ctrl]}>
<Layout
config={store.config}
data-testid={store.error ? 'error' : store.loading}
onMouseEnter={onMouseEnter}
>
<Show when={!store.error} fallback={<ErrorView />}>
<Editor />
<Sidebar />
</Show>
</Layout>
</StateContext.Provider>
<Show when={isMounted()}>
<StateContext.Provider value={[store, ctrl]}>
<Layout
config={store.config}
data-testid={store.error ? 'error' : store.loading}
onMouseEnter={onMouseEnter}
>
<Show when={!store.error} fallback={<ErrorView />}>
<Editor />
<Sidebar />
</Show>
</Layout>
</StateContext.Provider>
</Show>
)
}
export default CreateView

View File

@ -0,0 +1,15 @@
import { onCleanup, onMount } from 'solid-js'
export const useEscKeyDownHandler = (onEscKeyDown: () => void) => {
const keydownHandler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onEscKeyDown()
}
onMount(() => {
window.addEventListener('keydown', keydownHandler)
onCleanup(() => {
window.removeEventListener('keydown', keydownHandler)
})
})
}

View File

@ -0,0 +1,27 @@
import { onCleanup, onMount } from 'solid-js'
type Options = {
predicate?: () => boolean
containerRef: { current: HTMLElement }
handler: () => void
}
export const useOutsideClickHandler = (options: Options) => {
const { predicate, containerRef, handler } = options
const handleClickOutside = (event: MouseEvent & { target: Element }) => {
if (predicate && !predicate()) {
return
}
if (event.target === containerRef.current || containerRef.current?.contains(event.target)) {
return
}
options.handler()
}
onMount(() => {
document.addEventListener('click', handleClickOutside, { capture: true })
onCleanup(() => document.removeEventListener('click', handleClickOutside, { capture: true }))
})
}