restyle chatbox and some refactoring

This commit is contained in:
Sharon Kennedy 2020-02-23 23:12:47 -05:00
parent 37455346f3
commit dcd4e4a9ba
14 changed files with 385 additions and 385 deletions

View File

@ -10,10 +10,10 @@
</p> </p>
<p style="font-family:sans-serif; padding: 3rem 5rem;">Look down!</p> <p style="font-family:sans-serif; padding: 3rem 5rem;">Look down!</p>
<script src="./widget.js"></script> <script src="./chatbox.js"></script>
<script> <script>
EmbeddableWidget.mount({ EmbeddableChatbox.mount({
termsUrl: "https://tosdr.org/", termsUrl: "https://tosdr.org/",
privacyStatement: "This chat application does not collect any of your personal data or any data from your use of this service.", privacyStatement: "This chat application does not collect any of your personal data or any data from your use of this service.",
matrixServerUrl: "https://matrix.rhok.space", matrixServerUrl: "https://matrix.rhok.space",

View File

@ -1,6 +1,178 @@
@import url('https://fonts.googleapis.com/css?family=Assistant&display=swap');
@keyframes slideInUp {
from {
transform: translate3d(0, 100%, 0);
display: inherit;
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
@keyframes slideOutDown {
from {
transform: translate3d(0, 0, 0);
}
to {
display: none;
visibility: hidden;
transform: translate3d(0, 100%, 0);
}
}
.docked-widget {
position: fixed;
bottom: 10px;
right: 10px;
z-index: 9999;
width: 400px;
}
.dock {
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
width: 400px;
max-width: calc(100vw - 10px);
color: $white;
font-family: $theme-font;
font-size: 1rem;
border: none;
color: $white;
font-size: 1rem;
line-height: 1;
#open-chatbox-label {
background: $theme-color;
padding: 0.75rem 1.5rem;
flex: 1 1 auto;
text-align: left;
margin-right: 0.25rem;
}
.label-icon {
background: $theme-color;
height: 40px;
width: 40px;
border-radius: 40px;
display: flex;
justify-content: center;
align-items: center;
}
}
.widget {
width: 400px;
max-width: calc(100vw - 10px);
border-bottom: none;
animation-duration: 0.2s;
animation-fill-mode: forwards;
&-entering {
animation-name: slideInUp;
}
&-entered {
display: inherit;
visibility: visible;
}
&-exiting {
animation-name: slideOutDown;
}
&-exited {
display: none;
visibility: hidden;
}
&-header {
display: flex;
align-items: center;
margin-bottom: 0.2rem;
justify-content: flex-end;
&-title {
display: flex;
flex-grow: 1;
}
&-minimize {
cursor: pointer;
display: flex;
align-items: center;
justify-content: flex-start;
border: 1px solid $dark-color !important;
background: $white;
color: $dark-color;
flex: 1 1 auto;
font-family: $theme-font;
font-size: 1rem;
padding: 0.5rem;
}
&-close {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid $dark-color !important;
background: $white;
border-radius: 40px;
height: 40px;
width: 40px;
margin-left: 0.2rem;
color: $dark-color;
}
}
&-body {
background: white;
padding: 10px;
height: 150px;
}
&-footer {
background: green;
line-height: 30px;
padding-left: 10px;
}
}
.btn-icon {
font-size: 1.5rem;
line-height: 1;
transform: rotateX(0deg);
transition: all 0.5s linear;
display: flex;
align-items: center;
justify-content: center;
}
.arrow {
margin-right: 0.5rem;
transform: translateY(5px);
&.opened {
color: $dark-color;
transform: rotateX(180deg) translateY(5px);
}
}
@media screen and (max-width: 420px){
.docked-widget {
right: 0;
left: 0;
}
.dock, .widget {
width: 100vw;
max-width: 100vw;
}
}
#ocrcc-chatbox { #ocrcc-chatbox {
font-family: $theme-font; font-family: $theme-font;
@ -9,24 +181,25 @@
height: 60vh; height: 60vh;
max-height: 100vh; max-height: 100vh;
min-height: 180px; min-height: 180px;
border: 1px solid $theme-color;
a { a {
color: inherit; color: inherit;
} }
.message-window { .message-window {
background-color: $light-color; background-color: $white;
border: 1px solid $dark-color;
flex: 1 1 auto; flex: 1 1 auto;
padding: 0.5rem; padding: 0.5rem;
overflow: auto; overflow: auto;
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column-reverse;
justify-content: space-between; justify-content: space-between;
margin-bottom: 0.2rem;
} }
.notices { .notices {
color: $dark-color; color: $gray-color;
font-size: 0.9rem; font-size: 0.9rem;
> div { > div {
@ -48,7 +221,7 @@
} }
&.from-bot { &.from-bot {
color: $dark-color; color: $gray-color;
font-size: 0.9rem; font-size: 0.9rem;
} }
@ -71,8 +244,8 @@
justify-content: flex-start; justify-content: flex-start;
.text { .text {
border: 1px solid $theme-color; border: 1px solid $light-color;
background-color: $white; background-color: $light-color;
color: $dark-color; color: $dark-color;
border-radius: 15px 15px 15px 0; border-radius: 15px 15px 15px 0;
margin-right: 10%; margin-right: 10%;
@ -88,17 +261,18 @@
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 0; margin-bottom: 0;
border-top: 1px solid $theme-color;
} }
input[type="text"] { input[type="text"] {
font-size: 1rem; font-size: 1rem;
padding: 0.5rem 1rem; padding: 0.5rem;
border: none; border: none;
flex: 1 0 auto; flex: 1 0 auto;
border: none; border: 1px solid $dark-color;
background: $light-color;
color: $dark-color; color: $dark-color;
font-family: $theme-font; font-family: $theme-font;
margin-right: 0.2rem;
&:focus { &:focus {
border: none; border: none;
@ -112,7 +286,7 @@
font-size: 1rem; font-size: 1rem;
color: $white; color: $white;
font-weight: bold; font-weight: bold;
border: none; border: 1px solid $theme-color;
font-family: $theme-font; font-family: $theme-font;
cursor: pointer; cursor: pointer;
} }

View File

@ -1,21 +1,23 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.dock {
background: $dark-theme-color;
}
.loader { .loader {
color: $dark-theme-color; color: $dark-theme-color;
} }
#ocrcc-chatbox { #ocrcc-chatbox {
border: 1px solid $dark-theme-color; .btn-icon {
color: $light-text-color;
}
.widget-header { .widget-header-minimize, .widget-header-close {
background-color: $dark-theme-color; background: $dark-background-color;
color: $light-text-color;
border: 1px solid $white;
} }
.message-window { .message-window {
background-color: $dark-background-color; background-color: $dark-background-color;
border: 1px solid $white;
} }
.notices { .notices {
@ -30,9 +32,9 @@
&.from-me { &.from-me {
.text { .text {
background-color: $light-background-color; background-color: $theme-color;
color: $dark-text-color; color: $light-text-color;
border: 1px solid $light-text-color; border: 1px solid $theme-color;
} }
} }
@ -40,24 +42,26 @@
.text { .text {
background-color: $dark-theme-color; background-color: $dark-theme-color;
color: $light-text-color; color: $light-text-color;
border: 1px solid $light-text-color; border: 1px solid $dark-theme-color;
} }
} }
} }
.input-window { .input-window {
form { input[type="text"] {
border-top: 1px solid $dark-theme-color; background-color: $dark-theme-color;
color: $light-text-color;
border: 1px solid $white;
} }
input[type="text"] { ::placeholder {
background-color: $light-background-color; color: transparentize($light-text-color, 0.3);
color: $dark-text-color;
} }
input[type="submit"] { input[type="submit"] {
background-color: $dark-theme-color; background-color: $theme-color;
color: $light-text-color; color: $light-text-color;
border: 1px solid $white;
} }
} }
} }

View File

@ -1,11 +1,16 @@
$light-color: #f6faff; @import url('https://fonts.googleapis.com/css?family=Poppins&display=swap');
$purple: #785BEC;
$charcoal: #828282;
$light-color: #F2F2F2;
$gray-color: $charcoal;
$dark-color: #04090F; $dark-color: #04090F;
$yellow: #FFFACD; $yellow: #FFFACD;
$dark-blue: #2660A4; $dark-blue: #2660A4;
$white: #ffffff; $white: #ffffff;
$highlight-color: $yellow; $highlight-color: $yellow;
$theme-color: $dark-blue; $theme-color: $purple;
$theme-font: 'Assistant', 'Helvetica', sans-serif; $theme-font: 'Poppins', 'Helvetica', sans-serif;
$drop-shadow-color: #BDBEBF; $drop-shadow-color: #BDBEBF;
/* Dark mode colors */ /* Dark mode colors */
@ -14,4 +19,4 @@ $dark-background-color: #0F1116;
$light-background-color: #ffffff; $light-background-color: #ffffff;
$light-text-color: #ffffff; $light-text-color: #ffffff;
$dark-text-color: #0F1116; $dark-text-color: #0F1116;
$dark-theme-color: #333C4B; $dark-theme-color: #4F4F4F;

View File

@ -1,137 +0,0 @@
@keyframes slideInUp {
from {
transform: translate3d(0, 100%, 0);
display: inherit;
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
@keyframes slideOutDown {
from {
transform: translate3d(0, 0, 0);
}
to {
display: none;
visibility: hidden;
transform: translate3d(0, 100%, 0);
}
}
.docked-widget {
position: fixed;
bottom: 0px;
right: 10px;
z-index: 9999;
}
.dock {
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem;
width: 400px;
max-width: calc(100vw - 10px);
background: $theme-color;
color: $white;
font-family: $theme-font;
font-size: 1rem;
border: none;
color: $white;
font-size: 1rem;
line-height: 1;
box-shadow: 0 2px 15px $drop-shadow-color;
}
.widget {
width: 400px;
max-width: calc(100vw - 10px);
border-bottom: none;
animation-duration: 0.2s;
animation-fill-mode: forwards;
box-shadow: 0 2px 15px $drop-shadow-color;
&-entering {
animation-name: slideInUp;
}
&-entered {
display: inherit;
visibility: visible;
}
&-exiting {
animation-name: slideOutDown;
}
&-exited {
display: none;
visibility: hidden;
}
&-header {
background: $theme-color;
color: $white;
padding: 0.5rem;
display: flex;
align-items: center;
&-title {
display: flex;
flex-grow: 1;
}
&-icon {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
}
}
&-body {
background: white;
padding: 10px;
height: 150px;
}
&-footer {
background: green;
line-height: 30px;
padding-left: 10px;
}
}
.arrow {
transform: rotateX(0deg);
transition: all 0.5s linear;
color: $white;
font-size: 1rem;
line-height: 1;
&.opened {
transform: rotateX(180deg);
}
&.closed {
transform: rotateX(0deg) translateY(25%);
}
}
@media screen and (max-width: 420px){
.docked-widget {
right: 0;
left: 0;
}
.dock, .widget {
width: 100vw;
max-width: 100vw;
}
}

View File

@ -1,5 +1,6 @@
import React from "react" import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import { Transition } from 'react-transition-group';
import * as util from "util"; import * as util from "util";
import * as os from "os"; import * as os from "os";
import * as path from "path"; import * as path from "path";
@ -12,6 +13,10 @@ import * as matrix from "matrix-js-sdk";
import {uuid} from "uuidv4" import {uuid} from "uuidv4"
import Message from "./message"; import Message from "./message";
import Dock from "./dock";
import Header from "./header";
import './styles.scss';
const DEFAULT_MATRIX_SERVER = "https://matrix.rhok.space" const DEFAULT_MATRIX_SERVER = "https://matrix.rhok.space"
@ -28,12 +33,9 @@ const DEFAULT_THEME = {
placement: "right" placement: "right"
}; };
const initialState = {
class ChatBox extends React.Component { opened: false,
constructor(props) { showDock: true,
super(props)
const client = matrix.createClient(this.props.matrixServerUrl)
this.state = {
client: null, client: null,
ready: false, ready: false,
accessToken: null, accessToken: null,
@ -44,14 +46,46 @@ class ChatBox extends React.Component {
roomId: null, roomId: null,
typingStatus: null, typingStatus: null,
} }
class ChatBox extends React.Component {
constructor(props) {
super(props)
const client = matrix.createClient(this.props.matrixServerUrl)
this.state = initialState
this.chatboxInput = React.createRef(); this.chatboxInput = React.createRef();
} }
handleToggleOpen = () => {
this.setState((prev) => {
let { showDock } = prev;
if (!prev.opened) {
showDock = false;
}
return {
showDock,
opened: !prev.opened,
};
});
}
handleWidgetExit = () => {
this.setState({
showDock: true,
});
}
handleExitChat = () => {
this.leaveRoom()
.then(() => {
this.setState(initialState)
})
.catch(err => console.log("Error leaving room", err))
}
leaveRoom = () => { leaveRoom = () => {
if (this.state.roomId) { if (this.state.roomId) {
this.state.client.leave(this.state.roomId).then(data => { return this.state.client.leave(this.state.roomId)
console.log("Left room", data)
})
} }
} }
@ -203,13 +237,13 @@ class ChatBox extends React.Component {
handleEscape = (e) => { handleEscape = (e) => {
if (e.keyCode === 27 && this.props.opened) { if (e.keyCode === 27 && this.state.opened) {
this.props.handleToggleOpen() this.handleToggleOpen()
} }
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (prevState.client !== this.state.client) { if (this.state.client && prevState.client !== this.state.client) {
this.createRoom() this.createRoom()
this.state.client.once('sync', (state, prevState, res) => { this.state.client.once('sync', (state, prevState, res) => {
@ -219,19 +253,6 @@ class ChatBox extends React.Component {
}); });
this.state.client.on("Room.timeline", (event, room, toStartOfTimeline) => { this.state.client.on("Room.timeline", (event, room, toStartOfTimeline) => {
// if (event.getType() === "m.room.message") {
// if (event.status === "not sent") {
// return console.log("message not sent!", event)
// }
// if (event.isEncrypted()) {
// return console.log("message encrypted")
// }
// this.handleMessageEvent(event)
// }
if (event.getType() === "m.room.encryption") { if (event.getType() === "m.room.encryption") {
const msgList = [...this.state.messages] const msgList = [...this.state.messages]
const encryptionMsg = { const encryptionMsg = {
@ -263,17 +284,11 @@ class ChatBox extends React.Component {
}); });
} }
// if (prevState.roomId !== this.state.roomId) {
// this.setState({
// isRoomEncrypted: this.state.client.isRoomEncrypted(this.state.roomId)
// })
// }
if (!prevState.ready && this.state.ready) { if (!prevState.ready && this.state.ready) {
this.chatboxInput.current.focus() this.chatboxInput.current.focus()
} }
if (this.state.client === null && prevProps.status !== "entered" && this.props.status === "entered") { if (this.state.client === null && !prevState.opened && this.state.opened) {
this.initializeClient() this.initializeClient()
} }
} }
@ -303,25 +318,16 @@ class ChatBox extends React.Component {
} }
render() { render() {
const { ready, messages, inputValue, userId, typingStatus } = this.state; const { ready, messages, inputValue, userId, roomId, typingStatus, opened, showDock } = this.state;
const { opened, handleToggleOpen, privacyStatement, termsUrl } = this.props;
const inputLabel = 'Send a message...' const inputLabel = 'Send a message...'
return ( return (
<div id="ocrcc-chatbox" aria-haspopup="dialog" aria-label="Open support chat"> <div className="docked-widget" role="complementary">
<div className="widget-header"> <Transition in={opened} timeout={250} onExited={this.handleWidgetExit}>
<div className="widget-header-title"> {(status) => (
Support Chat <div className={`widget widget-${status}`} aria-hidden={!opened}>
</div> <div id="ocrcc-chatbox" aria-haspopup="dialog">
<button <Header handleToggleOpen={this.handleToggleOpen} opened={opened} handleExitChat={this.handleExitChat} />
type="button"
className={`widget-header-icon`}
onClick={handleToggleOpen}
onKeyPress={handleToggleOpen}
>
<span className={`arrow ${opened ? "opened" : "closed"}`}></span>
</button>
</div>
<div className="message-window"> <div className="message-window">
<div className="messages"> <div className="messages">
@ -356,6 +362,12 @@ class ChatBox extends React.Component {
</form> </form>
</div> </div>
</div> </div>
</div>
)}
</Transition>
{showDock && !roomId && <Dock handleToggleOpen={this.handleToggleOpen} />}
{showDock && roomId && <Header handleToggleOpen={this.handleToggleOpen} opened={opened} handleExitChat={this.handleExitChat} />}
</div>
); );
} }
}; };

22
src/components/dock.jsx Normal file
View File

@ -0,0 +1,22 @@
import React from "react"
import PropTypes from "prop-types"
const Dock = ({ handleToggleOpen }) => {
return(
<button
type="button"
className="dock"
onClick={handleToggleOpen}
onKeyPress={handleToggleOpen}
aria-labelledby="open-chatbox-label"
>
<div id="open-chatbox-label">Start a new chat</div>
<div className="label-icon">
<div className={`btn-icon`} aria-label={`Open support chat window`}>+</div>
</div>
</button>
)
}
export default Dock;

33
src/components/header.jsx Normal file
View File

@ -0,0 +1,33 @@
import React from "react"
import PropTypes from "prop-types"
const Header = ({ handleToggleOpen, handleExitChat, opened }) => {
return(
<div className="widget-header">
<button
type="button"
className={`widget-header-minimize`}
onClick={handleToggleOpen}
onKeyPress={handleToggleOpen}
aria-label="Minimize the chat window"
title="Minimize the chat window"
>
<span className={`btn-icon arrow ${opened ? "opened" : "closed"}`}></span>
<span>{`${opened ? "Hide" : "Show"} the chat`}</span>
</button>
<button
type="button"
className={`widget-header-close`}
onClick={handleExitChat}
onKeyPress={handleExitChat}
aria-label="Exit the chat"
title="Exit the chat"
>
<span className={`btn-icon`}>×</span>
</button>
</div>
)
}
export default Header;

View File

@ -1,5 +1,4 @@
@import "variables"; @import "variables";
@import "loader"; @import "loader";
@import "widget";
@import "chat"; @import "chat";
@import "dark_mode"; @import "dark_mode";

View File

@ -1,76 +0,0 @@
import React, { Component } from 'react';
import { Transition } from 'react-transition-group';
import Chatbox from './chatbox';
import './styles.scss';
class Widget extends Component {
constructor(props) {
super(props);
this.state = {
opened: false,
showDock: true,
};
}
handleToggleOpen = () => {
this.setState((prev) => {
let { showDock } = prev;
if (!prev.opened) {
showDock = false;
}
return {
showDock,
opened: !prev.opened,
};
});
}
handleWidgetExit = () => {
this.setState({
showDock: true,
});
}
render() {
const { opened, showDock } = this.state;
return (
<div className="docked-widget" role="complementary">
<Transition in={opened} timeout={250} onExited={this.handleWidgetExit}>
{(status) => (
<div className={`widget widget-${status}`} aria-hidden={!opened}>
<Chatbox
handleToggleOpen={this.handleToggleOpen}
opened={opened}
status={status}
{...this.props} // eslint-disable-line
/>
</div>
)}
</Transition>
{showDock && (
<button
type="button"
className="dock"
onClick={this.handleToggleOpen}
onKeyPress={this.handleToggleOpen}
aria-labelledby="open-chatbox-label"
>
<span id="open-chatbox-label">Open support chat</span>
<span className={`arrow ${opened ? 'opened' : 'closed'}`} aria-label={`${opened ? 'Close' : 'Open'} support chat window`}></span>
</button>
)}
</div>
);
}
}
Widget.propTypes = {
};
Widget.defaultProps = {
};
export default Widget;

View File

@ -1,36 +0,0 @@
import React from 'react';
import { mount } from 'enzyme';
import Widget from './widget';
import { waitForSelection } from '../test-helpers';
describe('<Widget />', () => {
test('open/close', async () => {
const widgetDom = mount(<Widget />);
expect(widgetDom).toMatchSnapshot();
{
const dockAnchorEl = widgetDom.find('button.dock');
expect(dockAnchorEl).toHaveLength(1);
// open widget
dockAnchorEl.simulate('click');
}
expect(widgetDom).toMatchSnapshot();
// dock does not exist anymore
expect(widgetDom.find('a.dock')).toHaveLength(0);
const closeAnchorEl = await waitForSelection(widgetDom, 'button.widget-header-icon');
expect(closeAnchorEl).toHaveLength(1);
// close widget
closeAnchorEl.simulate('click');
{
const dockAnchorEl = await waitForSelection(widgetDom, 'button.dock');
expect(dockAnchorEl).toHaveLength(1);
}
expect(widgetDom).toMatchSnapshot();
});
});

View File

@ -1,12 +1,12 @@
import EmbeddableWidget from './embeddable-widget'; import EmbeddableChatbox from './embeddable-chatbox';
export default function bookmarklet() { export default function bookmarklet() {
if (window.EmbeddableWidget) { if (window.EmbeddableChatbox) {
return; return;
} }
window.EmbeddableWidget = EmbeddableWidget; window.EmbeddableChatbox = EmbeddableChatbox;
EmbeddableWidget.mount({ EmbeddableChatbox.mount({
termsUrl: 'https://tosdr.org/', termsUrl: 'https://tosdr.org/',
privacyStatement: 'This chat application does not collect any of your personal data or any data from your use of this service.', privacyStatement: 'This chat application does not collect any of your personal data or any data from your use of this service.',
matrixServerUrl: 'https://matrix.rhok.space', matrixServerUrl: 'https://matrix.rhok.space',

View File

@ -1,17 +1,17 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Widget from '../components/widget'; import Chatbox from '../components/chatbox';
import '../../vendor/cleanslate.css'; import '../../vendor/cleanslate.css';
export default class EmbeddableWidget { export default class EmbeddableChatbox {
static el; static el;
static mount({ parentElement = null, ...props } = {}) { static mount({ parentElement = null, ...props } = {}) {
const component = <Widget {...props} />; // eslint-disable-line const component = <Chatbox {...props} />; // eslint-disable-line
function doRender() { function doRender() {
if (EmbeddableWidget.el) { if (EmbeddableChatbox.el) {
throw new Error('EmbeddableWidget is already mounted, unmount first'); throw new Error('EmbeddableChatbox is already mounted, unmount first');
} }
const el = document.createElement('div'); const el = document.createElement('div');
@ -26,7 +26,7 @@ export default class EmbeddableWidget {
component, component,
el, el,
); );
EmbeddableWidget.el = el; EmbeddableChatbox.el = el;
} }
if (document.readyState === 'complete') { if (document.readyState === 'complete') {
@ -39,11 +39,11 @@ export default class EmbeddableWidget {
} }
static unmount() { static unmount() {
if (!EmbeddableWidget.el) { if (!EmbeddableChatbox.el) {
throw new Error('EmbeddableWidget is not mounted, mount first'); throw new Error('EmbeddableChatbox is not mounted, mount first');
} }
ReactDOM.unmountComponentAtNode(EmbeddableWidget.el); ReactDOM.unmountComponentAtNode(EmbeddableChatbox.el);
EmbeddableWidget.el.parentNode.removeChild(EmbeddableWidget.el); EmbeddableChatbox.el.parentNode.removeChild(EmbeddableChatbox.el);
EmbeddableWidget.el = null; EmbeddableChatbox.el = null;
} }
} }

View File

@ -77,12 +77,12 @@ const defaultConfig = {
module.exports = [{ module.exports = [{
...defaultConfig, ...defaultConfig,
entry: './src/outputs/embeddable-widget.js', entry: './src/outputs/embeddable-chatbox.js',
output: { output: {
path: distDir, path: distDir,
publicPath: '/', publicPath: '/',
filename: 'widget.js', filename: 'chatbox.js',
library: 'EmbeddableWidget', library: 'EmbeddableChatbox',
libraryExport: 'default', libraryExport: 'default',
libraryTarget: 'window', libraryTarget: 'window',
}, },