Finish initial client main view
This commit is contained in:
parent
ef3f4a20f2
commit
29adf50ae0
9 changed files with 370 additions and 111 deletions
|
@ -72,7 +72,7 @@ export async function uploadPlugin(data, id) {
|
||||||
let resp
|
let resp
|
||||||
if (id) {
|
if (id) {
|
||||||
resp = await fetch(`${BASE_PATH}/plugin/${id}`, {
|
resp = await fetch(`${BASE_PATH}/plugin/${id}`, {
|
||||||
headers: getHeaders("applcation/zip"),
|
headers: getHeaders("application/zip"),
|
||||||
body: data,
|
body: data,
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
})
|
})
|
||||||
|
@ -96,9 +96,27 @@ export async function getClient(id) {
|
||||||
return await resp.json()
|
return await resp.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function uploadAvatar(id, data, mime) {
|
||||||
|
const resp = await fetch(`${BASE_PATH}/client/${id}/avatar`, {
|
||||||
|
headers: getHeaders(mime),
|
||||||
|
body: data,
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
return await resp.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function putClient(client) {
|
||||||
|
const resp = await fetch(`${BASE_PATH}/client/${client.id}`, {
|
||||||
|
headers: getHeaders(),
|
||||||
|
body: JSON.stringify(client),
|
||||||
|
method: "PUT",
|
||||||
|
})
|
||||||
|
return await resp.json()
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
login, ping,
|
login, ping,
|
||||||
getInstances, getInstance,
|
getInstances, getInstance,
|
||||||
getPlugins, getPlugin, uploadPlugin,
|
getPlugins, getPlugin, uploadPlugin,
|
||||||
getClients, getClient,
|
getClients, getClient, uploadAvatar, putClient,
|
||||||
}
|
}
|
||||||
|
|
55
maubot/management/frontend/src/components/PreferenceTable.js
Normal file
55
maubot/management/frontend/src/components/PreferenceTable.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// maubot - A plugin-based Matrix bot system.
|
||||||
|
// Copyright (C) 2018 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
import React from "react"
|
||||||
|
import Switch from "./Switch"
|
||||||
|
|
||||||
|
export const PrefTable = ({ children, wrapperClass }) => {
|
||||||
|
if (wrapperClass) {
|
||||||
|
return (
|
||||||
|
<div className={wrapperClass}>
|
||||||
|
<div className="preference-table">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="preference-table">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrefRow = ({ name, children }) => (
|
||||||
|
<div className="row">
|
||||||
|
<div className="key">{name}</div>
|
||||||
|
<div className="value">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const PrefInput = ({ rowName, ...args }) => (
|
||||||
|
<PrefRow name={rowName}>
|
||||||
|
<input {...args}/>
|
||||||
|
</PrefRow>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const PrefSwitch = ({ rowName, ...args }) => (
|
||||||
|
<PrefRow name={rowName}>
|
||||||
|
<Switch {...args}/>
|
||||||
|
</PrefRow>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default PrefTable
|
|
@ -15,11 +15,16 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import React, { Component } from "react"
|
import React, { Component } from "react"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import Switch from "../../components/Switch"
|
|
||||||
import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg"
|
import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg"
|
||||||
import { ReactComponent as UploadButton } from "../../res/upload.svg"
|
import { ReactComponent as UploadButton } from "../../res/upload.svg"
|
||||||
|
import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTable"
|
||||||
|
import Spinner from "../../components/Spinner"
|
||||||
|
import api from "../../api"
|
||||||
|
|
||||||
function getAvatarURL(client) {
|
function getAvatarURL(client) {
|
||||||
|
if (!client.avatar_url) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
const id = client.avatar_url.substr("mxc://".length)
|
const id = client.avatar_url.substr("mxc://".length)
|
||||||
return `${client.homeserver}/_matrix/media/r0/download/${id}`
|
return `${client.homeserver}/_matrix/media/r0/download/${id}`
|
||||||
}
|
}
|
||||||
|
@ -45,68 +50,136 @@ class Client extends Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = props
|
this.state = Object.assign(this.initialState, props.client)
|
||||||
|
}
|
||||||
|
|
||||||
|
get initialState() {
|
||||||
|
return {
|
||||||
|
id: "",
|
||||||
|
displayname: "",
|
||||||
|
homeserver: "",
|
||||||
|
avatar_url: "",
|
||||||
|
access_token: "",
|
||||||
|
sync: true,
|
||||||
|
autojoin: true,
|
||||||
|
enabled: true,
|
||||||
|
started: false,
|
||||||
|
|
||||||
|
uploadingAvatar: false,
|
||||||
|
saving: false,
|
||||||
|
startingOrStopping: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
this.setState(nextProps)
|
this.setState(Object.assign(this.initialState, nextProps.client))
|
||||||
}
|
}
|
||||||
|
|
||||||
inputChange = event => {
|
inputChange = event => {
|
||||||
|
if (!event.target.name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.setState({ [event.target.name]: event.target.value })
|
this.setState({ [event.target.name]: event.target.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async readFile(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
reader.onload = evt => resolve(evt.target.result)
|
||||||
|
reader.onerror = err => reject(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarUpload = async event => {
|
||||||
|
const file = event.target.files[0]
|
||||||
|
this.setState({
|
||||||
|
uploadingAvatar: true,
|
||||||
|
})
|
||||||
|
const data = await this.readFile(file)
|
||||||
|
const resp = await api.uploadAvatar(this.state.id, data, file.type)
|
||||||
|
this.setState({
|
||||||
|
uploadingAvatar: false,
|
||||||
|
avatar_url: resp.content_uri,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
save = async () => {
|
||||||
|
this.setState({ saving: true })
|
||||||
|
const resp = await api.putClient(this.state)
|
||||||
|
if (resp.id) {
|
||||||
|
resp.saving = false
|
||||||
|
this.setState(resp)
|
||||||
|
} else {
|
||||||
|
console.error(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startOrStop = async () => {
|
||||||
|
this.setState({ startingOrStopping: true })
|
||||||
|
const resp = await api.putClient({
|
||||||
|
id: this.state.id,
|
||||||
|
started: !this.state.started,
|
||||||
|
})
|
||||||
|
if (resp.id) {
|
||||||
|
resp.startingOrStopping = false
|
||||||
|
this.setState(resp)
|
||||||
|
} else {
|
||||||
|
console.error(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div className="client">
|
return <div className="client">
|
||||||
<div className="avatar-container">
|
<div className="sidebar">
|
||||||
<img className="avatar" src={getAvatarURL(this.state)} alt="Avatar"/>
|
<div className={`avatar-container ${this.state.avatar_url ? "" : "no-avatar"}
|
||||||
<UploadButton className="upload"/>
|
${this.state.uploadingAvatar ? "uploading" : ""}`}>
|
||||||
|
<img className="avatar" src={getAvatarURL(this.state)} alt="Avatar"/>
|
||||||
|
<UploadButton className="upload"/>
|
||||||
|
<input className="file-selector" type="file" accept="image/png, image/jpeg"
|
||||||
|
onChange={this.avatarUpload} disabled={this.state.uploadingAvatar}/>
|
||||||
|
{this.state.uploadingAvatar && <Spinner/>}
|
||||||
|
</div>
|
||||||
|
{this.props.client && (<>
|
||||||
|
<div className="started-container">
|
||||||
|
<span className={`started ${this.state.started}`}/>
|
||||||
|
<span className="text">{this.state.started ? "Started" : "Stopped"}</span>
|
||||||
|
</div>
|
||||||
|
<button className="save" onClick={this.startOrStop}
|
||||||
|
disabled={this.state.saving || this.state.startingOrStopping}>
|
||||||
|
{this.state.startingOrStopping ? <Spinner/>
|
||||||
|
: (this.state.started ? "Stop" : "Start")}
|
||||||
|
</button>
|
||||||
|
</>)}
|
||||||
</div>
|
</div>
|
||||||
<div className="info-container">
|
<div className="info-container">
|
||||||
<div className="row">
|
<PrefTable>
|
||||||
<div className="key">User ID</div>
|
<PrefInput rowName="User ID" type="text" disabled={!!this.props.client}
|
||||||
<div className="value">
|
name={this.props.client ? "" : "id"}
|
||||||
<input type="text" disabled value={this.props.id}
|
value={this.state.id} placeholder="@fancybot:example.com"
|
||||||
onChange={this.inputChange}/>
|
onChange={this.inputChange}/>
|
||||||
</div>
|
<PrefInput rowName="Display name" type="text" name="displayname"
|
||||||
</div>
|
value={this.state.displayname} placeholder="My fancy bot"
|
||||||
<div className="row">
|
|
||||||
<div className="key">Display name</div>
|
|
||||||
<div className="value">
|
|
||||||
<input type="text" name="displayname" value={this.state.displayname}
|
|
||||||
onChange={this.inputChange}/>
|
onChange={this.inputChange}/>
|
||||||
</div>
|
<PrefInput rowName="Homeserver" type="text" name="homeserver"
|
||||||
</div>
|
value={this.state.homeserver} placeholder="https://example.com"
|
||||||
<div className="row">
|
|
||||||
<div className="key">Homeserver</div>
|
|
||||||
<div className="value">
|
|
||||||
<input type="text" name="homeserver" value={this.state.homeserver}
|
|
||||||
onChange={this.inputChange}/>
|
onChange={this.inputChange}/>
|
||||||
</div>
|
<PrefInput rowName="Access token" type="text" name="access_token"
|
||||||
</div>
|
value={this.state.access_token} onChange={this.inputChange}
|
||||||
<div className="row">
|
placeholder="MDAxYWxvY2F0aW9uIG1hdHJpeC5sb2NhbAowMDEzaWRlbnRpZmllc"/>
|
||||||
<div className="key">Access token</div>
|
<PrefSwitch rowName="Sync" active={this.state.sync}
|
||||||
<div className="value">
|
|
||||||
<input type="text" name="access_token" value={this.state.access_token}
|
|
||||||
onChange={this.inputChange}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="key">Sync</div>
|
|
||||||
<div className="value">
|
|
||||||
<Switch active={this.state.sync}
|
|
||||||
onToggle={sync => this.setState({ sync })}/>
|
onToggle={sync => this.setState({ sync })}/>
|
||||||
</div>
|
<PrefSwitch rowName="Autojoin" active={this.state.autojoin}
|
||||||
</div>
|
onToggle={autojoin => this.setState({ autojoin })}/>
|
||||||
<div className="row">
|
<PrefSwitch rowName="Enabled" active={this.state.enabled}
|
||||||
<div className="key">Enabled</div>
|
|
||||||
<div className="value">
|
|
||||||
<Switch active={this.state.enabled}
|
|
||||||
onToggle={enabled => this.setState({ enabled })}/>
|
onToggle={enabled => this.setState({ enabled })}/>
|
||||||
</div>
|
</PrefTable>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<button className="save" onClick={this.save}
|
||||||
|
disabled={this.state.saving || this.state.startingOrStopping}>
|
||||||
|
{this.state.saving ? <Spinner/> : "Save"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,11 +58,19 @@ class Dashboard extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderView(field, type, id) {
|
renderView(field, type, id) {
|
||||||
const entry = this.state[field + "s"][id]
|
const stateField = field + "s"
|
||||||
|
const entry = this.state[stateField][id]
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return "Not found :("
|
return "Not found :("
|
||||||
}
|
}
|
||||||
return React.createElement(type, entry)
|
return React.createElement(type, {
|
||||||
|
[field]: entry,
|
||||||
|
onChange: newEntry => this.setState({
|
||||||
|
[stateField]: Object.assign({}, this.state[stateField], {
|
||||||
|
[id]: newEntry,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -72,7 +80,9 @@ class Dashboard extends Component {
|
||||||
Maubot Manager
|
Maubot Manager
|
||||||
</Link>
|
</Link>
|
||||||
<div className="topbar">
|
<div className="topbar">
|
||||||
{localStorage.username}
|
<div className="user">
|
||||||
|
{localStorage.username}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav className="sidebar">
|
<nav className="sidebar">
|
||||||
<div className="instances list">
|
<div className="instances list">
|
||||||
|
|
|
@ -25,10 +25,12 @@
|
||||||
color: $inverted-text-color
|
color: $inverted-text-color
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
font-size: 1rem
|
font-size: 1rem
|
||||||
cursor: pointer
|
|
||||||
|
|
||||||
&:hover
|
&:not(:disabled)
|
||||||
background-color: darken($background, 10%)
|
cursor: pointer
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background-color: darken($background, 10%)
|
||||||
|
|
||||||
=link-button()
|
=link-button()
|
||||||
display: inline-block
|
display: inline-block
|
||||||
|
@ -37,7 +39,7 @@
|
||||||
|
|
||||||
=main-color-button()
|
=main-color-button()
|
||||||
background-color: $primary
|
background-color: $primary
|
||||||
&:hover
|
&:hover:not(:disabled)
|
||||||
background-color: $primary-dark
|
background-color: $primary-dark
|
||||||
|
|
||||||
.button
|
.button
|
||||||
|
|
|
@ -14,9 +14,12 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
@import lib/spinner
|
@import lib/spinner
|
||||||
|
|
||||||
@import base/vars
|
@import base/vars
|
||||||
@import base/body
|
@import base/body
|
||||||
@import base/elements
|
@import base/elements
|
||||||
|
|
||||||
|
@import lib/preferencetable
|
||||||
@import lib/switch
|
@import lib/switch
|
||||||
|
|
||||||
@import pages/login
|
@import pages/login
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
// maubot - A plugin-based Matrix bot system.
|
||||||
|
// Copyright (C) 2018 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
.preference-table
|
||||||
|
display: table
|
||||||
|
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
> .row
|
||||||
|
display: table-row
|
||||||
|
|
||||||
|
> .key, > .value
|
||||||
|
display: table-cell
|
||||||
|
padding-bottom: .5rem
|
||||||
|
|
||||||
|
> .key
|
||||||
|
width: 7rem
|
||||||
|
|
||||||
|
> .value
|
||||||
|
margin: .5rem
|
||||||
|
|
||||||
|
> .switch
|
||||||
|
width: auto
|
||||||
|
height: 2rem
|
||||||
|
|
||||||
|
> input
|
||||||
|
border: none
|
||||||
|
height: 2rem
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
box-sizing: border-box
|
||||||
|
|
||||||
|
padding: .375rem 0
|
||||||
|
background-color: $background
|
||||||
|
|
||||||
|
font-size: 1rem
|
||||||
|
|
||||||
|
border-bottom: 1px dotted $primary
|
||||||
|
|
||||||
|
&:hover:not(:disabled)
|
||||||
|
border-bottom: 1px solid $primary
|
||||||
|
|
||||||
|
&:focus:not(:disabled)
|
||||||
|
border-bottom: 2px solid $primary
|
|
@ -16,85 +16,116 @@
|
||||||
|
|
||||||
> .client
|
> .client
|
||||||
margin: 1rem
|
margin: 1rem
|
||||||
|
display: flex
|
||||||
|
|
||||||
div.avatar-container
|
> div.sidebar
|
||||||
position: relative
|
|
||||||
display: inline-block
|
|
||||||
width: 8rem
|
|
||||||
height: 8rem
|
|
||||||
border-radius: 100%
|
|
||||||
cursor: pointer
|
|
||||||
vertical-align: top
|
vertical-align: top
|
||||||
|
text-align: center
|
||||||
|
|
||||||
> img.avatar
|
> div
|
||||||
display: block
|
margin-bottom: 1rem
|
||||||
max-width: 8rem
|
|
||||||
max-height: 8rem
|
|
||||||
border-radius: 100%
|
|
||||||
position: absolute
|
|
||||||
left: 50%
|
|
||||||
top: 50%
|
|
||||||
-webkit-transform: translateY(-50%) translateX(-50%)
|
|
||||||
|
|
||||||
> svg.upload
|
> div.avatar-container
|
||||||
position: absolute
|
position: relative
|
||||||
display: block
|
width: 8rem
|
||||||
visibility: hidden
|
height: 8rem
|
||||||
|
border-radius: 50%
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
width: 6rem
|
display: flex
|
||||||
height: 6rem
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
padding: 1rem
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
> img.avatar
|
> img.avatar
|
||||||
opacity: .25
|
position: absolute
|
||||||
|
display: block
|
||||||
|
max-width: 8rem
|
||||||
|
max-height: 8rem
|
||||||
|
user-select: none
|
||||||
|
|
||||||
> svg.upload
|
> svg.upload
|
||||||
visibility: visible
|
position: absolute
|
||||||
|
display: block
|
||||||
|
visibility: hidden
|
||||||
|
|
||||||
div.info-container
|
width: 6rem
|
||||||
display: inline-table
|
height: 6rem
|
||||||
vertical-align: top
|
|
||||||
|
|
||||||
margin: 1rem 2rem
|
padding: 1rem
|
||||||
|
user-select: none
|
||||||
|
|
||||||
> .row
|
> input.file-selector
|
||||||
display: table-row
|
position: absolute
|
||||||
|
width: 8rem
|
||||||
|
height: 8rem
|
||||||
|
user-select: none
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
> .key, > .value
|
> div.spinner
|
||||||
display: table-cell
|
+thick-spinner
|
||||||
padding-bottom: .5rem
|
|
||||||
|
|
||||||
> .key
|
&:not(.uploading)
|
||||||
width: 6.5rem
|
> input.file-selector
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
> .value
|
&:hover
|
||||||
|
> img.avatar
|
||||||
|
opacity: .25
|
||||||
|
|
||||||
|
> svg.upload
|
||||||
|
visibility: visible
|
||||||
|
|
||||||
|
&.no-avatar
|
||||||
|
> img.avatar
|
||||||
|
visibility: hidden
|
||||||
|
|
||||||
|
> svg.upload
|
||||||
|
visibility: visible
|
||||||
|
opacity: .5
|
||||||
|
|
||||||
|
&.uploading
|
||||||
|
> img.avatar
|
||||||
|
opacity: .25
|
||||||
|
|
||||||
|
> div.started-container
|
||||||
|
display: inline-flex
|
||||||
|
|
||||||
|
> span.started
|
||||||
|
display: inline-block
|
||||||
|
height: 0
|
||||||
|
width: 0
|
||||||
|
border-radius: 50%
|
||||||
margin: .5rem
|
margin: .5rem
|
||||||
|
|
||||||
> .value > .switch
|
&.true
|
||||||
width: auto
|
background-color: $primary
|
||||||
height: 2rem
|
box-shadow: 0 0 .75rem .75rem $primary
|
||||||
|
|
||||||
> .value > input
|
&.false
|
||||||
border: none
|
background-color: $error-light
|
||||||
height: 2rem
|
box-shadow: 0 0 .75rem .75rem $error-light
|
||||||
width: 100%
|
|
||||||
|
|
||||||
box-sizing: border-box
|
> span.text
|
||||||
|
display: inline-block
|
||||||
|
margin-left: 1rem
|
||||||
|
|
||||||
padding: .375rem 0
|
> div.info-container
|
||||||
background-color: $background
|
vertical-align: top
|
||||||
|
|
||||||
font-size: 1rem
|
margin: 0 1rem
|
||||||
|
flex: 1
|
||||||
|
|
||||||
border-bottom: 1px solid transparent
|
button.save
|
||||||
|
+button
|
||||||
|
+main-color-button
|
||||||
|
width: 100%
|
||||||
|
height: 2.5rem
|
||||||
|
padding: 0
|
||||||
|
|
||||||
&:hover:not(:disabled)
|
> .spinner
|
||||||
border-bottom: 1px solid $primary
|
+thick-spinner
|
||||||
|
+white-spinner
|
||||||
&:focus:not(:disabled)
|
width: 2rem
|
||||||
border-bottom: 2px solid $primary
|
|
||||||
|
|
||||||
//> .client
|
//> .client
|
||||||
display: table
|
display: table
|
||||||
|
|
|
@ -46,9 +46,19 @@
|
||||||
grid-area: topbar
|
grid-area: topbar
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
justify-content: center
|
justify-content: right
|
||||||
background-color: $primary
|
background-color: $primary
|
||||||
box-shadow: 0 .25rem .25rem rgba(0, 0, 0, .2)
|
box-shadow: 0 .25rem .25rem rgba(0, 0, 0, .2)
|
||||||
|
padding: .5rem 1rem
|
||||||
|
|
||||||
|
> div.user
|
||||||
|
display: inline-flex
|
||||||
|
align-items: center
|
||||||
|
height: 100%
|
||||||
|
padding: 0 1rem
|
||||||
|
box-sizing: border-box
|
||||||
|
background-color: $primary-dark
|
||||||
|
border-radius: .25rem
|
||||||
|
|
||||||
|
|
||||||
@import "sidebar"
|
@import "sidebar"
|
||||||
|
|
Loading…
Reference in a new issue