editor styles isolated, editor sidebar esc and outside click handling
This commit is contained in:
parent
b2387543fc
commit
0f0b2645aa
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,20 +5,21 @@ import { Draft, useState } from '../store/context'
|
||||||
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 { isServer } from 'solid-js/web'
|
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
|
||||||
|
@ -147,14 +148,30 @@ export const Sidebar = () => {
|
||||||
setMod(navigator.platform.includes('Mac') ? 'Cmd' : 'Ctrl')
|
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} />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{store.path && (
|
{store.path && (
|
||||||
|
@ -166,7 +183,7 @@ export const Sidebar = () => {
|
||||||
<Link>Настройки публикации</Link>
|
<Link>Настройки публикации</Link>
|
||||||
<Link>История правок</Link>
|
<Link>История правок</Link>
|
||||||
|
|
||||||
<div class="theme-switcher">
|
<div class={styles.themeSwitcher}>
|
||||||
Ночная тема
|
Ночная тема
|
||||||
<input type="checkbox" name="theme" id="theme" onClick={toggleTheme} />
|
<input type="checkbox" name="theme" id="theme" onClick={toggleTheme} />
|
||||||
<label for="theme">Ночная тема</label>
|
<label for="theme">Ночная тема</label>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,4 +1,4 @@
|
||||||
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, newState } from '../Editor/store/context'
|
import { State, StateContext, newState } from '../Editor/store/context'
|
||||||
import { createCtrl } from '../Editor/store/actions'
|
import { createCtrl } from '../Editor/store/actions'
|
||||||
|
@ -10,6 +10,10 @@ 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 = () => {
|
export const CreateView = () => {
|
||||||
|
const [isMounted, setIsMounted] = createSignal(false)
|
||||||
|
|
||||||
|
onMount(() => setIsMounted(true))
|
||||||
|
|
||||||
const onChangeTheme = () => ctrl.updateTheme()
|
const onChangeTheme = () => ctrl.updateTheme()
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
matchDark().addEventListener('change', onChangeTheme)
|
matchDark().addEventListener('change', onChangeTheme)
|
||||||
|
@ -50,18 +54,20 @@ export const CreateView = () => {
|
||||||
}, 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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