ui: drop vditor

This commit is contained in:
undefined committed 2024-06-30 00:24:41 +08:00
1 parent 979b8ce5af
commit 909c927986
16 files changed
+399 -153

No files matched your search

@@ -0,0 +1,235 @@
// Process ![test]( x =100x200)
// ^^^^^^^^ this size specification
function parseNextNumber(str, pos, max) {
const start = pos;
const result = {
ok: false,
pos,
value: '',
};
let code = str.charCodeAt(pos);
while (pos < max && (code >= 0x30 /* 0 */ && code <= 0x39 /* 9 */) || code === 0x25 /* % */) {
code = str.charCodeAt(++pos);
}
result.ok = true;
result.pos = pos;
result.value = str.slice(start, pos);
return result;
}
function parseImageSize(str, pos, max) {
const result = {
ok: false,
pos: 0,
width: '',
height: '',
};
if (pos >= max) { return result; }
let code = str.charCodeAt(pos);
if (code !== 0x3d /* = */) { return result; }
pos++;
code = str.charCodeAt(pos);
if (code !== 0x78 /* x */ && (code < 0x30 || code > 0x39) /* [0-9] */) {
return result;
}
// parse width
const resultW = parseNextNumber(str, pos, max);
pos = resultW.pos;
// next charactor must be 'x'
code = str.charCodeAt(pos);
if (code !== 0x78 /* x */) { return result; }
pos++;
// parse height
const resultH = parseNextNumber(str, pos, max);
pos = resultH.pos;
result.width = resultW.value;
result.height = resultH.value;
result.pos = pos;
result.ok = true;
return result;
}
export default function plugin(md) {
md.inline.ruler.before('emphasis', 'image', (state, silent) => {
let attrs;
let code;
let label;
let pos;
let ref;
let res;
let title;
let width = '';
let height = '';
let token;
let tokens;
let start;
let href = '';
const oldPos = state.pos;
const max = state.posMax;
if (state.src.charCodeAt(state.pos) !== 0x21/* ! */) { return false; }
if (state.src.charCodeAt(state.pos + 1) !== 0x5B/* [ */) { return false; }
const labelStart = state.pos + 2;
const labelEnd = md.helpers.parseLinkLabel(state, state.pos + 1, false);
// parser failed to find ']', so it's not a valid link
if (labelEnd < 0) { return false; }
pos = labelEnd + 1;
if (pos < max && state.src.charCodeAt(pos) === 0x28/* ( */) {
//
// Inline link
//
// [link]( <href> "title" )
// ^^ skipping these spaces
pos++;
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos);
if (code !== 0x20 && code !== 0x0A) { break; }
}
if (pos >= max) { return false; }
// [link]( <href> "title" )
// ^^^^^^ parsing link destination
start = pos;
res = md.helpers.parseLinkDestination(state.src, pos, state.posMax);
if (res.ok) {
href = state.md.normalizeLink(res.str);
if (state.md.validateLink(href)) {
pos = res.pos;
} else {
href = '';
}
}
// [link]( <href> "title" )
// ^^ skipping these spaces
start = pos;
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos);
if (code !== 0x20 && code !== 0x0A) { break; }
}
// [link]( <href> "title" )
// ^^^^^^^ parsing link title
res = md.helpers.parseLinkTitle(state.src, pos, state.posMax);
if (pos < max && start !== pos && res.ok) {
title = res.str;
pos = res.pos;
// [link]( <href> "title" )
// ^^ skipping these spaces
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos);
if (code !== 0x20 && code !== 0x0A) { break; }
}
} else {
title = '';
}
// [link]( <href> "title" =WxH )
// ^^^^ parsing image size
if (pos - 1 >= 0) {
code = state.src.charCodeAt(pos - 1);
// there must be at least one white spaces
// between previous field and the size
if (code === 0x20) {
res = parseImageSize(state.src, pos, state.posMax);
if (res.ok) {
width = res.width;
height = res.height;
pos = res.pos;
// [link]( <href> "title" =WxH )
// ^^ skipping these spaces
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos);
if (code !== 0x20 && code !== 0x0A) { break; }
}
}
}
}
if (pos >= max || state.src.charCodeAt(pos) !== 0x29/* ) */) {
state.pos = oldPos;
return false;
}
pos++;
} else {
//
// Link reference
//
if (typeof state.env.references === 'undefined') { return false; }
// [foo] [bar]
// ^^ optional whitespace (can include newlines)
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos);
if (code !== 0x20 && code !== 0x0A) { break; }
}
if (pos < max && state.src.charCodeAt(pos) === 0x5B/* [ */) {
start = pos + 1;
pos = md.helpers.parseLinkLabel(state, pos);
if (pos >= 0) {
label = state.src.slice(start, pos++);
} else {
pos = labelEnd + 1;
}
} else {
pos = labelEnd + 1;
}
// covers label === '' and label === undefined
// (collapsed reference link and shortcut reference link respectively)
label ||= state.src.slice(labelStart, labelEnd);
ref = state.env.references[md.utils.normalizeReference(label)];
if (!ref) {
state.pos = oldPos;
return false;
}
href = ref.href;
title = ref.title;
}
//
// We found the end of the link, and know for a fact it's a valid link;
// so all that's left to do is to call tokenizer.
//
if (!silent) {
state.pos = labelStart;
state.posMax = labelEnd;
const newState = new state.md.inline.State(
state.src.slice(labelStart, labelEnd),
state.md,
state.env,
tokens = [],
);
newState.md.inline.tokenize(newState);
token = state.push('image', 'img', 0);
token.attrs = attrs = [['src', href],
['alt', '']];
token.children = tokens;
if (title) {
attrs.push(['title', title]);
}
if (width !== '') {
attrs.push(['width', width]);
}
if (height !== '') {
attrs.push(['height', height]);
}
}
state.pos = pos;
state.posMax = max;
return true;
});
};
@@ -1,7 +1,8 @@
// https://github.com/waylonflinn/markdown-it-katex/blob/master/index.js
const katex = require('katex');
import { Logger } from 'hydrooj';
import katex from 'katex';
const logger = new global.Hydro.Logger('katex');
const logger = new Logger('katex');
function isValidDelim(state, pos) {
const max = state.posMax;
@@ -109,8 +110,8 @@ function block(state, start, end, silent) {
return true;
}
module.exports = function plugin(md) {
const options = { throwOnError: false, strict: 'ignore' };
export default function plugin(md) {
const options = { throwOnError: false, strict: 'ignore', displayMode: false };
const render = (latex: string, displayMode = false) => {
options.displayMode = displayMode;
try {
@@ -136,4 +137,4 @@ module.exports = function plugin(md) {
});
md.renderer.rules.math_inline = inlineRenderer;
md.renderer.rules.math_block = blockRenderer;
};
}
@@ -1,8 +1,8 @@
/* eslint-disable max-len */
/* eslint-disable prefer-destructuring */
import { randomUUID } from 'crypto';
import MarkdownIt from 'markdown-it';
import type MarkdownIt from 'markdown-it';
import { v4 as uuid } from 'uuid';
const allowFullScreen = ' webkitallowfullscreen mozallowfullscreen allowfullscreen';
@@ -110,7 +110,7 @@ export function Media(md: MarkdownIt) {
if (service === 'pdf') {
if (src.startsWith('file://') || src.startsWith('./')) src += src.includes('?') ? '&noDisposition=1' : '?noDisposition=1';
return `\
<object classid="clsid:${randomUUID().toUpperCase()}">
<object classid="clsid:${uuid().toUpperCase()}">
<param name="SRC" value="${src}" >
<embed width="100%" style="min-height: 100vh;border: none;" fullscreen="yes" src="${src}">
<noembed></noembed>
+1 -1
View File
@@ -23,7 +23,7 @@ const MergeCells = require('markdown-it-merge-cells');
// @[youtube](https://youtube.com/watch?v=xxx)
// @[pdf](https://foo.com/bar.pdf)
const { Media } = require('./markdown-it-media');
const Katex = require('./markdown-it-katex');
const { default: Katex } = require('./markdown-it-katex');
const { xssProtector } = require('./markdown-it-xss');
class Markdown extends MarkdownIt {
+1 -2
View File
@@ -269,14 +269,13 @@ export default async function (env: { watch?: boolean, production?: boolean, mea
filename: '[name].css?[fullhash:6]',
}),
new WebpackManifestPlugin({}),
new webpack.IgnorePlugin({ resourceRegExp: /(^\.\/locale$|mathjax|abcjs|vditor.+\.d\.ts)/ }),
new webpack.IgnorePlugin({ resourceRegExp: /(^\.\/locale$)/ }),
new CopyWebpackPlugin({
patterns: [
{ from: root('static') },
{ from: root('components/navigation/nav-logo-small_dark.png'), to: 'components/navigation/nav-logo-small_dark.png' },
{ from: root(`${dirname(require.resolve('streamsaver/package.json'))}/mitm.html`), to: 'streamsaver/mitm.html' },
{ from: root(`${dirname(require.resolve('streamsaver/package.json'))}/sw.js`), to: 'streamsaver/sw.js' },
{ from: root(`${dirname(require.resolve('vditor/package.json'))}/dist`), to: 'vditor/dist' },
{ from: root(`${dirname(require.resolve('graphiql/package.json'))}/graphiql.min.css`), to: 'graphiql.min.css' },
{ from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' },
],
-5
View File
@@ -127,11 +127,6 @@ async function main() {
if (fs.existsSync('public/theme.css')) {
fs.copyFileSync('public/theme.css', `public/theme-${pkg.version}.css`);
}
if (argv.options.production) {
for (const f of ['echarts', 'graphviz', 'mermaid', 'mathjax']) {
fs.removeSync(`public/vditor/dist/js/${f}`);
}
}
await Promise.all(globbySync('public/**/*.map').map((i) => fs.remove(i)));
}
process.chdir(dir);
@@ -1,12 +1,3 @@
.vditor
margin-bottom: rem(10px)
.vditor-preview__action
display: none
.vditor ol
list-style: decimal
.decoration-hide
color: grey !important
background-color: grey !important
@@ -1,35 +1,10 @@
import $ from 'jquery';
import { nanoid } from 'nanoid';
import React from 'react';
import ReactDOM from 'react-dom/client';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import Notification from 'vj/components/notification';
import { i18n, request } from 'vj/utils';
export const config = {
toolbar: [
'emoji', 'headings', 'bold', 'italic', 'strike', 'link', '|',
'list', 'ordered-list', 'check', 'outdent', 'indent', '|',
'quote', 'line', 'code', 'inline-code', 'table', '|',
'upload', 'edit-mode', 'fullscreen', 'export',
],
mode: UserContext.preferredEditorType || 'ir',
toolbarConfig: {
pin: true,
},
cdn: `${UiContext.cdn_prefix}vditor`,
counter: {
enable: true,
max: 65536,
},
preview: {
math: {
inlineDigit: true,
},
theme: {
path: `${UiContext.cdn_prefix}vditor/dist/css/content-theme`,
current: 'light',
},
},
};
import { i18n } from 'vj/utils';
import uploadFiles from '../upload';
interface MonacoOptions {
language?: string;
@@ -42,23 +17,23 @@ interface MonacoOptions {
hide?: string[];
lineNumbers?: 'on' | 'off' | 'relative' | 'interval';
}
interface VditorOptions {
theme?: 'classic' | 'dark'
}
type Options = MonacoOptions & VditorOptions;
type Options = MonacoOptions;
export default class Editor extends DOMAttachedObject {
static DOMAttachKey = 'vjEditorInstance';
model: import('../monaco').default.editor.IModel;
editor: import('../monaco').default.editor.IStandaloneCodeEditor;
vditor: import('vditor').default;
markdownEditor: any;
valueCache?: string;
setMarkdownEditorValue?: (v: string) => void;
reactRoot?: ReactDOM.Root;
isValid: boolean;
constructor($dom, public options: Options = {}) {
super($dom);
if (UserContext.preferredEditorType === 'monaco') this.initMonaco();
else if (options.language && options.language !== 'markdown') this.initMonaco();
else this.initVditor();
else this.initMarkdownEditor();
}
async initMonaco() {
@@ -168,87 +143,102 @@ export default class Editor extends DOMAttachedObject {
window.editor = this.editor;
}
async initVditor() {
async initMarkdownEditor() {
const pagename = document.documentElement.getAttribute('data-page');
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
const isProblemEdit = pagename === 'problem_edit';
const { default: Vditor } = await import('vditor');
const that = this;
const { $dom } = this;
const hasFocus = $dom.is(':focus') || $dom.hasClass('autofocus');
const origin = $dom.get(0);
const ele = document.createElement('div');
const value = $dom.val();
const { onChange } = this.options;
await new Promise((resolve) => {
this.vditor = new Vditor(ele, {
...config,
upload: {
multiple: false,
handler: (files) => {
let wrapper = ['', ''];
let ext: string;
const matches = files[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i);
if (matches) {
wrapper = ['![image](', ')'];
[, ext] = matches;
} else if (files[0].type === 'application/x-zip-compressed') {
wrapper = ['[file](', ')'];
ext = 'zip';
}
if (!ext) return i18n('No Supported file type.');
const filename = `${nanoid()}.${ext}`;
const data = new FormData();
data.append('filename', filename);
data.append('file', files[0]);
data.append('operation', 'upload_file');
if (isProblemEdit) data.append('type', 'additional_file');
let progress = 0;
request.postFile(isProblemEdit ? './files' : '/file', data, {
xhr: () => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('loadstart', () => this.vditor.vditor.tip.show(i18n('Uploading...'), 0));
xhr.upload.addEventListener('progress', (e) => {
if (!e.lengthComputable) return;
const percentComplete = Math.round((e.loaded / e.total) * 100);
if (percentComplete === progress) return;
progress = percentComplete;
this.vditor.vditor.tip.show(`${i18n('Uploading...')} ${percentComplete}%`, 0);
}, false);
return xhr;
},
})
.then(() => {
this.vditor.insertValue(`${wrapper.join(`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`)} `);
this.vditor.vditor.tip.hide();
})
.catch((e: { message: string; }) => {
console.error(e);
return `${i18n('Upload Failed')}: ${e.message}`;
});
return null;
},
},
...this.options,
after: () => {
const pos = $(ele).find('button[data-mode="sv"]').get();
const button = $('<button data-mode="monaco">Monaco Editor</button>');
button.on('click', (e) => {
Notification.info('You can select this in PreferenceSettings');
e.preventDefault();
});
button.insertAfter(pos);
resolve(null);
},
input(v) {
const { MdEditor } = await import('./mdeditor');
const renderCallback = (ref) => {
this.markdownEditor = ref;
};
function EditorComponent() {
const [val, setVal] = React.useState(value);
that.setMarkdownEditorValue = setVal;
return <MdEditor
className='textbox'
autoFocus={hasFocus}
codeTheme='github'
codeStyleReverse={false}
ref={renderCallback}
modelValue={val}
noMermaid
noPrettier
autoDetectCode
toolbarsExclude={[
// 'bold',
// 'underline',
// 'italic',
// '-',
// 'strikeThrough',
// 'title',
// 'sub',
// 'sup',
// 'quote',
// 'unorderedList',
// 'orderedList',
// 'task',
// '-',
// 'codeRow',
// 'code',
// 'link',
// 'image',
// 'table',
'mermaid',
// 'katex',
// '-',
// 'revoke',
// 'next',
'save',
// '=',
'pageFullscreen',
'fullscreen',
// 'preview',
'previewOnly',
'htmlPreview',
// 'catalog',
'github',
]}
onChange={(v) => {
that.valueCache = v;
setVal(v);
$dom.val(v);
$dom.text(v);
if (onChange) onChange(v);
},
value,
cache: { id: Math.random().toString() },
});
});
$(ele).addClass('textbox');
onChange?.(v);
}}
onUploadImg={async (files, callback) => {
let ext: string;
const matches = files[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i);
if (matches) {
[, ext] = matches;
} else if (files[0].type === 'application/x-zip-compressed') {
ext = 'zip';
}
if (!ext) return i18n('No Supported file type.');
const filename = `${nanoid()}.${ext}`;
await uploadFiles(isProblemEdit ? './files' : '/file', [files[0]], {
type: isProblemEdit ? 'additional_file' : undefined,
filenameCallback: () => filename,
}).then(() => {
callback([`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`]);
}).catch(() => {
callback([]);
});
return null;
}}
/>;
}
this.reactRoot = ReactDOM.createRoot(ele);
this.reactRoot.render(<EditorComponent />);
$dom.hide();
origin.parentElement.appendChild(ele);
this.isValid = true;
@@ -257,7 +247,7 @@ export default class Editor extends DOMAttachedObject {
destroy() {
this.detach();
if (this.vditor?.destroy) this.vditor.destroy();
if (this.reactRoot) this.reactRoot.unmount();
else if (this.editor?.dispose) this.editor.dispose();
}
@@ -267,13 +257,17 @@ export default class Editor extends DOMAttachedObject {
value(val?: string) {
this.ensureValid();
if (typeof val === 'string') return (this.editor || this.vditor).setValue(val);
return (this.editor || this.vditor).getValue({ lineEnding: '\n', preserveBOM: false });
if (typeof val === 'string') {
if (this.editor) return this.editor.setValue(val);
this.setMarkdownEditorValue?.(val);
this.markdownEditor?.resetHistory?.();
}
if (this.editor) return this.editor.getValue({ lineEnding: '\n', preserveBOM: false });
return this.valueCache;
}
focus() {
this.ensureValid();
this.vditor?.focus();
if (!this.editor || !this.model) return;
this.editor.focus();
const range = this.model.getFullModelRange();
@@ -0,0 +1,39 @@
import 'md-editor-rt/lib/style.css';
import Anchor from 'markdown-it-anchor';
import Footnote from 'markdown-it-footnote';
import Mark from 'markdown-it-mark';
import MergeCells from 'markdown-it-merge-cells';
import TOC from 'markdown-it-table-of-contents';
import { config, MdEditor } from 'md-editor-rt';
import Imsize from '../../backendlib/markdown-it-imsize';
import { Media } from '../../backendlib/markdown-it-media';
import { xssProtector } from '../../backendlib/markdown-it-xss';
const pagename = document.documentElement.getAttribute('data-page');
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
config({
markdownItConfig(mdit) {
mdit.options.html = true;
mdit.options.linkify = true;
mdit.linkify.tlds('.py', false);
mdit.linkify.tlds('.zip', false);
mdit.linkify.tlds('.mov', false);
mdit.use(Media);
mdit.use(Footnote);
mdit.use(Mark);
mdit.use(Imsize);
mdit.use(Anchor);
mdit.use(TOC);
mdit.use(MergeCells);
mdit.use(xssProtector);
if (isProblemPage) {
mdit.core.ruler.before('normalize', 'xss', (state) => {
state.src = state.src.replace(/file:\/\//g, pagename === 'problem_create' ? `/file/${UserContext._id}/` : './file/');
});
}
},
});
export { MdEditor };
+2 -1
View File
@@ -13,6 +13,7 @@ interface UploadOptions {
pjax?: boolean;
sidebar?: boolean;
singleFileUploadCallback?: (file: File) => any;
filenameCallback?: (file: File) => string;
}
export default async function uploadFiles(endpoint = '', files: File[] | FileList = [], options: UploadOptions = {}) {
const dialog = new Dialog({
@@ -38,7 +39,7 @@ export default async function uploadFiles(endpoint = '', files: File[] | FileLis
if (Number.isNaN(+i)) continue;
const file = files[i];
const data = new FormData();
data.append('filename', file.name);
data.append('filename', options.filenameCallback?.(file) || file.name);
data.append('file', file);
if (options.type) data.append('type', options.type);
data.append('operation', 'upload_file');
+6 -4
View File
@@ -25,7 +25,7 @@
"@fontsource/source-code-pro": "^5.0.18",
"@fontsource/ubuntu-mono": "^5.0.20",
"@hydrooj/utils": "workspace:^",
"@sentry/browser": "^8.12.0",
"@sentry/browser": "^8.13.0",
"@sentry/webpack-plugin": "^2.20.1",
"@simplewebauthn/browser": "7.4.0",
"@svgr/webpack": "^8.1.0",
@@ -40,6 +40,7 @@
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/redux-logger": "^3.0.13",
"@types/uuid": "^10",
"@types/webpack-env": "^1.18.5",
"@vscode/codicons": "^0.0.36",
"ajv": "^8.16.0",
@@ -56,7 +57,7 @@
"css-loader": "^6.11.0",
"diff": "^5.2.0",
"diff-dom": "^5.1.3",
"echarts": "^5.5.0",
"echarts": "^5.5.1",
"emojis-keywords": "2.0.0",
"emojis-list": "2.1.0",
"esbuild-loader": "^4.2.0",
@@ -73,9 +74,10 @@
"jquery.easing": "^1.4.1",
"jquery.transit": "^0.9.12",
"matchmedia-polyfill": "^0.3.2",
"md-editor-rt": "^4.17.0",
"mini-css-extract-plugin": "^2.9.0",
"moment": "^2.30.1",
"monaco-editor": "0.46.0",
"monaco-editor": "0.50.0",
"monaco-editor-nls": "^3.1.0",
"monaco-editor-webpack-plugin": "^7.1.0",
"monaco-themes": "^0.4.4",
@@ -118,7 +120,6 @@
"timeago-react": "^3.0.6",
"timeago.js": "^4.0.2",
"ts-loader": "^9.5.1",
"vditor": "^3.10.4",
"vinyl-buffer": "^1.0.1",
"web-streams-polyfill": "^4.0.0",
"webpack": "^5.92.1",
@@ -147,6 +148,7 @@
"nunjucks": "^3.2.4",
"schemastery-jsonschema": "^1.1.0",
"streamsaver": "^2.0.6",
"uuid": "^10.0.0",
"xss": "^1.0.15"
}
}
@@ -154,7 +154,7 @@ async function handleSection(ev, sidebar, type) {
$section.removeClass('animating');
}
export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
export default new NamedPage(['problem_create', 'problem_edit'], () => {
let confirmed = false;
$(document).on('click', '[name="operation"]', (ev) => {
ev.preventDefault();
@@ -240,14 +240,6 @@ export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
await download(`${pdoc.docId} ${pdoc.title}.zip`, targets);
}
setInterval(() => {
$('img').each(function () {
if (this.src.startsWith('file://')) {
$(this).attr('src', $(this).attr('src').replace('file://', (pagename === 'problem_create' ? `/file/${UserContext._id}/` : './file/')));
}
});
}, 500);
const $main = $('textarea[data-editor]');
const $field = $('textarea[data-markdown-upload]');
let content = $field.val();
+1 -1
View File
@@ -89,7 +89,7 @@ self.addEventListener('notificationclick', (event) => {
});
const PRECACHE = `precache-${process.env.VERSION}`;
const DO_NOT_PRECACHE = ['vditor', '.worker.js', 'fonts', 'i.monaco'];
const DO_NOT_PRECACHE = ['.worker.js', 'fonts', 'i.monaco'];
function shouldCachePath(path: string) {
if (!path.split('?')[0].split('/').pop()) return false;
-2
View File
@@ -43,8 +43,6 @@ nav_logo_dark:
default: /components/navigation/nav-logo-small_dark.png
preferredEditorType:
type:
wysiwyg: WYSIWYG
ir: Instant Rendering
sv: Split View
monaco: Monaco Editor
family: markdown
+1 -1
View File
@@ -196,7 +196,7 @@ $hover-background-color = #424242
background-color: #171717
color: #999
pre:not(.vditor-sv), code
pre, code
background-color: #171717
.scratchpad__toolbar
-1
View File
@@ -1,5 +1,4 @@
import 'normalize.css/normalize.css';
import 'vditor/dist/index.css';
import 'allotment/dist/style.css';
import 'pickadate/lib/themes/classic.css';
import 'pickadate/lib/themes/classic.date.css';