commit
d903f233cf
|
@ -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 {
|
.error {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -5,3 +63,11 @@
|
||||||
.markdown {
|
.markdown {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
|
||||||
|
color: #000;
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import type { EditorView } from 'prosemirror-view'
|
||||||
import type { EditorState } from 'prosemirror-state'
|
import type { EditorState } from 'prosemirror-state'
|
||||||
import { useState } from '../store/context'
|
import { useState } from '../store/context'
|
||||||
import { ProseMirror } from './ProseMirror'
|
import { ProseMirror } from './ProseMirror'
|
||||||
import '../styles/Editor.scss'
|
|
||||||
import styles from './Editor.module.scss'
|
import styles from './Editor.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
|
@ -14,7 +13,7 @@ export const Editor = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProseMirror
|
<ProseMirror
|
||||||
cssClass={clsx('editor', 'col-md-6', 'shift-content', {
|
cssClass={clsx(styles.editor, 'col-md-6', 'shift-content', {
|
||||||
[styles.error]: store.error,
|
[styles.error]: store.error,
|
||||||
[styles.markdown]: store.markdown
|
[styles.markdown]: store.markdown
|
||||||
})}
|
})}
|
||||||
|
|
52
src/components/Editor/components/Error.module.scss
Normal file
52
src/components/Editor/components/Error.module.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Switch, Match } from 'solid-js'
|
import { Switch, Match } from 'solid-js'
|
||||||
import { useState } from '../store/context'
|
import { useState } from '../store/context'
|
||||||
import '../styles/Button.scss'
|
import styles from './Error.module.scss'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [store] = useState()
|
const [store] = useState()
|
||||||
|
@ -24,8 +24,8 @@ const InvalidState = (props: { title: string }) => {
|
||||||
const onClick = () => ctrl.clean()
|
const onClick = () => ctrl.clean()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="error">
|
<div class={styles.error}>
|
||||||
<div class="container">
|
<div class={styles.container}>
|
||||||
<h1>{props.title}</h1>
|
<h1>{props.title}</h1>
|
||||||
<p>
|
<p>
|
||||||
There is an error with the editor state. This is probably due to an old version in which the data
|
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>
|
<pre>
|
||||||
<code>{JSON.stringify(store.error.props)}</code>
|
<code>{JSON.stringify(store.error.props)}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<button class="primary" onClick={onClick}>
|
<button class={styles.primary} onClick={onClick}>
|
||||||
Clean
|
Clean
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,13 +53,13 @@ const Other = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="error">
|
<div class={styles.error}>
|
||||||
<div class="container">
|
<div class={styles.container}>
|
||||||
<h1>An error occurred.</h1>
|
<h1>An error occurred.</h1>
|
||||||
<pre>
|
<pre>
|
||||||
<code>{getMessage()}</code>
|
<code>{getMessage()}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<button class="primary" onClick={onClick}>
|
<button class={styles.primary} onClick={onClick}>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
border-color: var(--background);
|
border-color: var(--background);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
&.dark {
|
.dark & {
|
||||||
background: var(--foreground);
|
background: var(--foreground);
|
||||||
color: var(--background);
|
color: var(--background);
|
||||||
border-color: var(--foreground);
|
border-color: var(--foreground);
|
|
@ -1,6 +1,7 @@
|
||||||
import type { JSX } from 'solid-js/jsx-runtime'
|
import type { JSX } from 'solid-js/jsx-runtime'
|
||||||
import type { Config } from '../store/context'
|
import type { Config } from '../store/context'
|
||||||
import '../styles/Layout.scss'
|
import { clsx } from 'clsx'
|
||||||
|
import styles from './Layout.module.scss'
|
||||||
|
|
||||||
export type Styled = {
|
export type Styled = {
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
|
@ -12,7 +13,11 @@ export type Styled = {
|
||||||
|
|
||||||
export const Layout = (props: Styled) => {
|
export const Layout = (props: Styled) => {
|
||||||
return (
|
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}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { EditorState, EditorStateConfig, Transaction } from 'prosemirror-state'
|
||||||
import { EditorView } from 'prosemirror-view'
|
import { EditorView } from 'prosemirror-view'
|
||||||
import { Schema } from 'prosemirror-model'
|
import { Schema } from 'prosemirror-model'
|
||||||
import type { NodeViewFn, ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
import type { NodeViewFn, ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
||||||
|
import '../styles/ProseMirror.scss'
|
||||||
|
|
||||||
interface ProseMirrorProps {
|
interface ProseMirrorProps {
|
||||||
cssClass?: string
|
cssClass?: string
|
||||||
|
|
|
@ -1,3 +1,221 @@
|
||||||
.withMargin {
|
.sidebarContainer {
|
||||||
margin-bottom: 10px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { unwrap } from 'solid-js/store'
|
||||||
import { undo, redo } from 'prosemirror-history'
|
import { undo, redo } from 'prosemirror-history'
|
||||||
import { Draft, useState } from '../store/context'
|
import { Draft, useState } from '../store/context'
|
||||||
import { mod } from '../env'
|
|
||||||
import * as remote from '../remote'
|
import * as remote from '../remote'
|
||||||
import { isEmpty } from '../prosemirror/helpers'
|
import { isEmpty } from '../prosemirror/helpers'
|
||||||
import type { Styled } from './Layout'
|
import type { Styled } from './Layout'
|
||||||
import '../styles/Sidebar.scss'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import styles from './Sidebar.module.scss'
|
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 = (
|
const Link = (
|
||||||
props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string }
|
props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string }
|
||||||
) => (
|
) => (
|
||||||
<button
|
<button
|
||||||
class={clsx('sidebar-link', props.className, {
|
class={clsx(styles.sidebarLink, props.className, {
|
||||||
[styles.withMargin]: props.withMargin
|
[styles.withMargin]: props.withMargin
|
||||||
})}
|
})}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
|
@ -33,10 +34,12 @@ const Link = (
|
||||||
export const Sidebar = () => {
|
export const Sidebar = () => {
|
||||||
const [store, ctrl] = useState()
|
const [store, ctrl] = useState()
|
||||||
const [lastAction, setLastAction] = createSignal<string | undefined>()
|
const [lastAction, setLastAction] = createSignal<string | undefined>()
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
document.body.classList.toggle('dark')
|
document.body.classList.toggle('dark')
|
||||||
ctrl.updateConfig({ theme: document.body.className })
|
ctrl.updateConfig({ theme: document.body.className })
|
||||||
}
|
}
|
||||||
|
|
||||||
const collabText = () => {
|
const collabText = () => {
|
||||||
if (store.collab?.started) {
|
if (store.collab?.started) {
|
||||||
return 'Stop'
|
return 'Stop'
|
||||||
|
@ -70,14 +73,12 @@ export const Sidebar = () => {
|
||||||
const onCopyAllAsMd = () =>
|
const onCopyAllAsMd = () =>
|
||||||
remote.copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md'))
|
remote.copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md'))
|
||||||
const onDiscard = () => ctrl.discard()
|
const onDiscard = () => ctrl.discard()
|
||||||
const [isHidden, setIsHidden] = createSignal<boolean | false>()
|
const [isHidden, setIsHidden] = createSignal(true)
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
setIsHidden(!isHidden())
|
setIsHidden((oldIsHidden) => !oldIsHidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSidebar()
|
|
||||||
|
|
||||||
const onCollab = () => {
|
const onCollab = () => {
|
||||||
const state = unwrap(store)
|
const state = unwrap(store)
|
||||||
store.collab?.started ? ctrl.stopCollab(state) : ctrl.startCollab(state)
|
store.collab?.started ? ctrl.stopCollab(state) : ctrl.startCollab(state)
|
||||||
|
@ -117,7 +118,7 @@ export const Sidebar = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line solid/no-react-specific-props
|
// 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 && '📎'}
|
{text()} {p.draft.path && '📎'}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
@ -131,7 +132,7 @@ export const Sidebar = () => {
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setLastAction()
|
setLastAction()
|
||||||
}, store.lastModified)
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!lastAction()) return
|
if (!lastAction()) return
|
||||||
|
@ -141,63 +142,84 @@ export const Sidebar = () => {
|
||||||
onCleanup(() => clearTimeout(id))
|
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 (
|
return (
|
||||||
<div class={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}>
|
<div
|
||||||
<span class="sidebar-opener" onClick={toggleSidebar}>
|
class={clsx(styles.sidebarContainer, {
|
||||||
|
[styles.sidebarContainerHidden]: isHidden()
|
||||||
|
})}
|
||||||
|
ref={(el) => (containerRef.current = el)}
|
||||||
|
>
|
||||||
|
<span class={styles.sidebarOpener} onClick={toggleSidebar}>
|
||||||
Советы и предложения
|
Советы и предложения
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Off onClick={() => editorView().focus()}>
|
<Off onClick={() => editorView().focus()}>
|
||||||
<div class="sidebar-closer" onClick={toggleSidebar} />
|
<div class={styles.sidebarCloser} 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="theme-switcher">
|
<div>
|
||||||
Ночная тема
|
{store.path && (
|
||||||
<input type="checkbox" name="theme" id="theme" onClick={toggleTheme} />
|
<Label>
|
||||||
<label for="theme">Ночная тема</label>
|
<i>({store.path.slice(Math.max(0, store.path.length - 24))})</i>
|
||||||
</div>
|
</Label>
|
||||||
<Link
|
)}
|
||||||
onClick={onDiscard}
|
<Link>Пригласить соавторов</Link>
|
||||||
disabled={!store.path && store.drafts.length === 0 && isEmpty(store.text)}
|
<Link>Настройки публикации</Link>
|
||||||
data-testid="discard"
|
<Link>История правок</Link>
|
||||||
>
|
|
||||||
{discardText()} <Keys keys={[mod, 'w']} />
|
<div class={styles.themeSwitcher}>
|
||||||
</Link>
|
Ночная тема
|
||||||
<Link onClick={onUndo}>
|
<input type="checkbox" name="theme" id="theme" onClick={toggleTheme} />
|
||||||
Undo <Keys keys={[mod, 'z']} />
|
<label for="theme">Ночная тема</label>
|
||||||
</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>
|
</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>
|
</Off>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const dbPromise = async () => {
|
import { openDB } from 'idb'
|
||||||
const { openDB } = await import('idb')
|
|
||||||
|
const dbPromise = () => {
|
||||||
return openDB('discours.io', 2, {
|
return openDB('discours.io', 2, {
|
||||||
upgrade(db) {
|
upgrade(db) {
|
||||||
db.createObjectStore('keyval')
|
db.createObjectStore('keyval')
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
export const isDark = () => (window as any).matchMedia('(prefers-color-scheme: dark)').matches
|
export const isDark = () =>
|
||||||
export const mod = 'Ctrl'
|
typeof window !== undefined && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
export const alt = 'Alt'
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { keymap } from 'prosemirror-keymap'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
import type OrderedMap from 'orderedmap'
|
import type OrderedMap from 'orderedmap'
|
||||||
|
|
||||||
|
import layoutStyles from '../../components/Layout.module.scss'
|
||||||
|
|
||||||
const plainSchema = new Schema({
|
const plainSchema = new Schema({
|
||||||
nodes: {
|
nodes: {
|
||||||
doc: {
|
doc: {
|
||||||
|
@ -53,6 +55,6 @@ export default (plain = false): ProseMirrorExtension => ({
|
||||||
keymap(buildKeymap(schema)),
|
keymap(buildKeymap(schema)),
|
||||||
keymap(baseKeymap),
|
keymap(baseKeymap),
|
||||||
history(),
|
history(),
|
||||||
dropCursor({ class: 'drop-cursor' })
|
dropCursor({ class: layoutStyles.dropCursor })
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,13 +2,14 @@ import { renderGrouped } from 'prosemirror-menu'
|
||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin } from 'prosemirror-state'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
import { buildMenuItems } from './menu'
|
import { buildMenuItems } from './menu'
|
||||||
|
import editorStyles from '../../components/Editor.module.scss'
|
||||||
|
|
||||||
export class SelectionTooltip {
|
export class SelectionTooltip {
|
||||||
tooltip: any
|
tooltip: any
|
||||||
|
|
||||||
constructor(view: any, schema: any) {
|
constructor(view: any, schema: any) {
|
||||||
this.tooltip = document.createElement('div')
|
this.tooltip = document.createElement('div')
|
||||||
this.tooltip.className = 'tooltip'
|
this.tooltip.className = editorStyles.tooltip
|
||||||
view.dom.parentNode.appendChild(this.tooltip)
|
view.dom.parentNode.appendChild(this.tooltip)
|
||||||
const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu as any)
|
const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu as any)
|
||||||
this.tooltip.appendChild(dom)
|
this.tooltip.appendChild(dom)
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { undo as yUndo, redo as yRedo } from 'y-prosemirror'
|
||||||
import debounce from 'lodash/debounce'
|
import debounce from 'lodash/debounce'
|
||||||
import { createSchema, createExtensions, createEmptyText } from '../prosemirror/setup'
|
import { createSchema, createExtensions, createEmptyText } from '../prosemirror/setup'
|
||||||
import { State, Draft, Config, ServiceError, newState, ExtensionsProps, EditorActions } from './context'
|
import { State, Draft, Config, ServiceError, newState, ExtensionsProps, EditorActions } from './context'
|
||||||
import { mod } from '../env'
|
|
||||||
import { serialize, createMarkdownParser } from '../markdown'
|
import { serialize, createMarkdownParser } from '../markdown'
|
||||||
import db from '../db'
|
import db from '../db'
|
||||||
import { isEmpty, isInitialized } from '../prosemirror/helpers'
|
import { isEmpty, isInitialized } from '../prosemirror/helpers'
|
||||||
|
@ -102,13 +101,13 @@ export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const keymap = {
|
const keymap: ExtensionsProps['keymap'] = {
|
||||||
[`${mod}-w`]: discard,
|
[`Mod-w`]: discard,
|
||||||
[`${mod}-z`]: onUndo,
|
[`Mod-z`]: onUndo,
|
||||||
[`Shift-${mod}-z`]: onRedo,
|
[`Shift-Mod-z`]: onRedo,
|
||||||
[`${mod}-y`]: onRedo,
|
[`Mod-y`]: onRedo,
|
||||||
[`${mod}-m`]: toggleMarkdown
|
[`Mod-m`]: toggleMarkdown
|
||||||
} as ExtensionsProps['keymap']
|
}
|
||||||
|
|
||||||
const createTextFromDraft = async (draft: Draft) => {
|
const createTextFromDraft = async (draft: Draft) => {
|
||||||
const state = unwrap(store)
|
const state = unwrap(store)
|
||||||
|
|
|
@ -63,7 +63,6 @@ export interface Collab {
|
||||||
export type LoadingType = 'loading' | 'initialized'
|
export type LoadingType = 'loading' | 'initialized'
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
isMac?: boolean
|
|
||||||
text?: ProseMirrorState
|
text?: ProseMirrorState
|
||||||
editorView?: EditorView
|
editorView?: EditorView
|
||||||
extensions?: ProseMirrorExtension[]
|
extensions?: ProseMirrorExtension[]
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
.index {
|
|
||||||
width: 350px;
|
|
||||||
}
|
|
|
@ -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 {
|
.ProseMirror {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
background-color: var(--background);
|
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")
|
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;
|
center no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
|
|
||||||
color: #000;
|
|
||||||
display: flex;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -134,7 +134,7 @@ export const Header = (props: Props) => {
|
||||||
<div class={styles.usernav}>
|
<div class={styles.usernav}>
|
||||||
<div class={clsx(styles.userControl, styles.userControl, 'col')}>
|
<div class={clsx(styles.userControl, styles.userControl, 'col')}>
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemWritePost)}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemWritePost)}>
|
||||||
<a href="/create">
|
<a href="/create" onClick={handleClientRouteLinkClick}>
|
||||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||||
<Icon name="pencil" class={styles.icon} />
|
<Icon name="pencil" class={styles.icon} />
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { JSX } from 'solid-js'
|
||||||
import { getLogger } from '../../utils/logger'
|
import { getLogger } from '../../utils/logger'
|
||||||
import './Modal.scss'
|
import './Modal.scss'
|
||||||
import { hideModal, useModalStore } from '../../stores/ui'
|
import { hideModal, useModalStore } from '../../stores/ui'
|
||||||
|
import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler'
|
||||||
|
|
||||||
const log = getLogger('modal')
|
const log = getLogger('modal')
|
||||||
|
|
||||||
|
@ -11,10 +12,6 @@ interface ModalProps {
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
const keydownHandler = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape') hideModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Modal = (props: ModalProps) => {
|
export const Modal = (props: ModalProps) => {
|
||||||
const { modal } = useModalStore()
|
const { modal } = useModalStore()
|
||||||
|
|
||||||
|
@ -22,13 +19,7 @@ export const Modal = (props: ModalProps) => {
|
||||||
if (event.target.classList.contains('modalwrap')) hideModal()
|
if (event.target.classList.contains('modalwrap')) hideModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
useEscKeyDownHandler(() => hideModal())
|
||||||
window.addEventListener('keydown', keydownHandler)
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
window.removeEventListener('keydown', keydownHandler)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const [visible, setVisible] = createSignal(false)
|
const [visible, setVisible] = createSignal(false)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { createEffect, createSignal, JSX, onCleanup, onMount, Show } from 'solid-js'
|
import { createEffect, createSignal, JSX, onCleanup, onMount, Show } from 'solid-js'
|
||||||
import styles from './Popup.module.scss'
|
import styles from './Popup.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler'
|
||||||
|
|
||||||
type HorizontalAnchor = 'center' | 'right'
|
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 }) => {
|
useOutsideClickHandler({
|
||||||
if (!isVisible()) {
|
containerRef,
|
||||||
return
|
predicate: () => isVisible(),
|
||||||
}
|
handler: () => setIsVisible(false)
|
||||||
|
|
||||||
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 }))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggle = () => setIsVisible((oldVisible) => !oldVisible)
|
const toggle = () => setIsVisible((oldVisible) => !oldVisible)
|
||||||
|
|
||||||
return (
|
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>
|
<span onClick={toggle}>{props.trigger}</span>
|
||||||
<Show when={isVisible()}>
|
<Show when={isVisible()}>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { newState } from '../Editor/store/context'
|
import { lazy, Suspense } from 'solid-js'
|
||||||
import { MainLayout } from '../Layouts/MainLayout'
|
import { MainLayout } from '../Layouts/MainLayout'
|
||||||
import { CreateView } from '../Views/Create'
|
import { Loading } from '../Loading'
|
||||||
|
|
||||||
|
const CreateView = lazy(() => import('../Views/Create'))
|
||||||
|
|
||||||
export const CreatePage = () => {
|
export const CreatePage = () => {
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<CreateView state={newState()} />
|
<Suspense fallback={<Loading />}>
|
||||||
|
<CreateView />
|
||||||
|
</Suspense>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { 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 { createCtrl } from '../Editor/store/actions'
|
||||||
import { Layout } from '../Editor/components/Layout'
|
import { Layout } from '../Editor/components/Layout'
|
||||||
import { Editor } from '../Editor/components/Editor'
|
import { Editor } from '../Editor/components/Editor'
|
||||||
|
@ -9,16 +9,18 @@ import ErrorView from '../Editor/components/Error'
|
||||||
|
|
||||||
const matchDark = () => window.matchMedia('(prefers-color-scheme: dark)')
|
const matchDark = () => window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
|
||||||
export const CreateView = (props: { state: State }) => {
|
export const CreateView = () => {
|
||||||
let isMac = false
|
const [isMounted, setIsMounted] = createSignal(false)
|
||||||
|
|
||||||
|
onMount(() => setIsMounted(true))
|
||||||
|
|
||||||
const onChangeTheme = () => ctrl.updateTheme()
|
const onChangeTheme = () => ctrl.updateTheme()
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
isMac = window?.navigator.platform.includes('Mac')
|
|
||||||
matchDark().addEventListener('change', onChangeTheme)
|
matchDark().addEventListener('change', onChangeTheme)
|
||||||
onCleanup(() => matchDark().removeEventListener('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 mouseEnterCoords = createMutable({ x: 0, y: 0 })
|
||||||
|
|
||||||
const onMouseEnter = (e: MouseEvent) => {
|
const onMouseEnter = (e: MouseEvent) => {
|
||||||
|
@ -52,17 +54,21 @@ export const CreateView = (props: { state: State }) => {
|
||||||
}, store.loading)
|
}, store.loading)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StateContext.Provider value={[store, ctrl]}>
|
<Show when={isMounted()}>
|
||||||
<Layout
|
<StateContext.Provider value={[store, ctrl]}>
|
||||||
config={store.config}
|
<Layout
|
||||||
data-testid={store.error ? 'error' : store.loading}
|
config={store.config}
|
||||||
onMouseEnter={onMouseEnter}
|
data-testid={store.error ? 'error' : store.loading}
|
||||||
>
|
onMouseEnter={onMouseEnter}
|
||||||
<Show when={!store.error} fallback={<ErrorView />}>
|
>
|
||||||
<Editor />
|
<Show when={!store.error} fallback={<ErrorView />}>
|
||||||
<Sidebar />
|
<Editor />
|
||||||
</Show>
|
<Sidebar />
|
||||||
</Layout>
|
</Show>
|
||||||
</StateContext.Provider>
|
</Layout>
|
||||||
|
</StateContext.Provider>
|
||||||
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default CreateView
|
||||||
|
|
15
src/utils/useEscKeyDownHandler.ts
Normal file
15
src/utils/useEscKeyDownHandler.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
27
src/utils/useOutsideClickHandler.ts
Normal file
27
src/utils/useOutsideClickHandler.ts
Normal 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 }))
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user