feature: Dashboards, at long last (#224)
* feature: Dashboards, at long last * fmt: action button IDs now use hypens. Removed ;
This commit is contained in:
parent
1b13a2bc4b
commit
6892a679ee
|
|
@ -10,7 +10,7 @@ message Action {
|
|||
string icon = 3;
|
||||
bool can_exec = 4;
|
||||
repeated ActionArgument arguments = 5;
|
||||
bool popup_on_start = 6;
|
||||
string popup_on_start = 6;
|
||||
}
|
||||
|
||||
message ActionArgument {
|
||||
|
|
@ -39,10 +39,18 @@ message GetDashboardComponentsResponse {
|
|||
string title = 1;
|
||||
repeated Action actions = 2;
|
||||
repeated Entity entities = 3;
|
||||
repeated DashboardItem dashboards = 4;
|
||||
}
|
||||
|
||||
message GetDashboardComponentsRequest {}
|
||||
|
||||
message DashboardItem {
|
||||
string title = 1;
|
||||
string type = 2;
|
||||
repeated DashboardItem contents = 3;
|
||||
string link = 4;
|
||||
}
|
||||
|
||||
message StartActionRequest {
|
||||
string action_name = 1;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,15 +12,15 @@ describe('config: multipleDropdowns', function () {
|
|||
|
||||
it('Multiple dropdowns are possible', async function() {
|
||||
await webdriver.get(runner.baseUrl())
|
||||
await webdriver.manage().setTimeouts({ implicit: 2000 });
|
||||
await webdriver.manage().setTimeouts({ implicit: 2000 })
|
||||
|
||||
const button = await webdriver.findElement(By.id('actionButton_bdc45101bbd12c1397557790d9f3e059')).findElement(By.tagName('button'));
|
||||
const button = await webdriver.findElement(By.id('actionButton-bdc45101bbd12c1397557790d9f3e059')).findElement(By.tagName('button'))
|
||||
|
||||
expect(button).to.not.be.undefined;
|
||||
expect(button).to.not.be.undefined
|
||||
|
||||
await button.click()
|
||||
|
||||
const dialog = await webdriver.findElement(By.id('argument-popup'));
|
||||
const dialog = await webdriver.findElement(By.id('argument-popup'))
|
||||
|
||||
await webdriver.wait(until.elementIsVisible(dialog), 2000)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ type Action struct {
|
|||
ExecOnFileChangedInDir []string
|
||||
MaxConcurrent int
|
||||
Arguments []ActionArgument
|
||||
PopupOnStart bool
|
||||
PopupOnStart string
|
||||
}
|
||||
|
||||
// ActionArgument objects appear on Actions.
|
||||
|
|
@ -72,6 +72,7 @@ type Config struct {
|
|||
ExternalRestAddress string
|
||||
LogLevel string
|
||||
Actions []Action `mapstructure:"actions"`
|
||||
Dashboards []DashboardItem `mapstructure:"dashboards"`
|
||||
Entities []Entity `mapstructure:"entities"`
|
||||
CheckForUpdates bool
|
||||
PageTitle string
|
||||
|
|
@ -89,6 +90,14 @@ type Config struct {
|
|||
AccessControlLists []AccessControlList
|
||||
WebUIDir string
|
||||
CronSupportForSeconds bool
|
||||
SectionNavigationStyle string
|
||||
}
|
||||
|
||||
type DashboardItem struct {
|
||||
Title string
|
||||
Type string
|
||||
Link string
|
||||
Contents []DashboardItem
|
||||
}
|
||||
|
||||
// DefaultConfig gets a new Config structure with sensible default values.
|
||||
|
|
@ -112,6 +121,7 @@ func DefaultConfig() *Config {
|
|||
config.AuthJwtClaimUserGroup = "group"
|
||||
config.WebUIDir = "./webui"
|
||||
config.CronSupportForSeconds = false
|
||||
config.SectionNavigationStyle = "sidebar"
|
||||
|
||||
return &config
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,6 +214,8 @@ func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashb
|
|||
|
||||
log.Tracef("GetDashboardComponents: %v", res)
|
||||
|
||||
dashboardCfgToPb(res, cfg.Dashboards)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
package grpcapi
|
||||
|
||||
import (
|
||||
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
)
|
||||
|
||||
func dashboardCfgToPb(res *pb.GetDashboardComponentsResponse, dashboards []config.DashboardItem) {
|
||||
for _, dashboard := range dashboards {
|
||||
res.Dashboards = append(res.Dashboards, &pb.DashboardItem{
|
||||
Type: "dashboard",
|
||||
Title: dashboard.Title,
|
||||
Contents: getDashboardContents(&dashboard),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getDashboardContents(dashboard *config.DashboardItem) []*pb.DashboardItem {
|
||||
ret := make([]*pb.DashboardItem, 0)
|
||||
|
||||
for _, subitem := range dashboard.Contents {
|
||||
newitem := &pb.DashboardItem{
|
||||
Title: subitem.Title,
|
||||
Type: subitem.Type,
|
||||
}
|
||||
|
||||
if len(subitem.Contents) > 0 {
|
||||
if newitem.Type != "fieldset" {
|
||||
newitem.Type = "directory"
|
||||
}
|
||||
|
||||
newitem.Contents = getDashboardContents(&subitem)
|
||||
} else {
|
||||
newitem.Type = "link"
|
||||
newitem.Link = subitem.Link
|
||||
}
|
||||
|
||||
ret = append(ret, newitem)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ type webUISettings struct {
|
|||
AvailableVersion string
|
||||
CurrentVersion string
|
||||
PageTitle string
|
||||
SectionNavigationStyle string
|
||||
}
|
||||
|
||||
func findWebuiDir() string {
|
||||
|
|
@ -57,6 +58,7 @@ func generateWebUISettings(w http.ResponseWriter, r *http.Request) {
|
|||
AvailableVersion: updatecheck.AvailableVersion,
|
||||
CurrentVersion: updatecheck.CurrentVersion,
|
||||
PageTitle: cfg.PageTitle,
|
||||
SectionNavigationStyle: cfg.SectionNavigationStyle,
|
||||
})
|
||||
|
||||
_, err := w.Write([]byte(jsonRet))
|
||||
|
|
|
|||
|
|
@ -24,16 +24,16 @@
|
|||
<h1 id = "page-title">OliveTin</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<div id = "content-sidebar">
|
||||
<input type = "checkbox" id = "hide-sidebar-checkbox" hidden checked />
|
||||
<aside>
|
||||
<ul>
|
||||
<nav hidden>
|
||||
<ul id = "navigation-links">
|
||||
<li><a id = "showActions">Actions</a></li>
|
||||
</ul>
|
||||
|
||||
<ul id = "supplemental-links">
|
||||
<li><a id = "showLogs">Logs</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section id = "contentLogs" title = "Logs" hidden>
|
||||
<div class = "toolbar">
|
||||
|
|
@ -53,7 +53,8 @@
|
|||
</section>
|
||||
|
||||
<section id = "contentActions" title = "Actions" hidden >
|
||||
<fieldset id = "root-group" title = "Dashboard of buttons">
|
||||
<fieldset id = "root-group" title = "Actions">
|
||||
<legend hidden>Actions</legend>
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class ActionButton extends window.HTMLElement {
|
|||
this.domTitle.innerText = this.btn.title
|
||||
this.domIcon.innerHTML = this.unicodeIcon
|
||||
|
||||
this.setAttribute('id', 'actionButton_' + json.id)
|
||||
this.setAttribute('id', 'actionButton-' + json.id)
|
||||
}
|
||||
|
||||
updateFromJson (json) {
|
||||
|
|
@ -68,7 +68,6 @@ class ActionButton extends window.HTMLElement {
|
|||
startAction (actionArgs) {
|
||||
// this.btn.disabled = true
|
||||
// this.isWaiting = true
|
||||
// this.updateDom()
|
||||
this.btn.classList = [] // Removes old animation classes
|
||||
|
||||
if (actionArgs === undefined) {
|
||||
|
|
|
|||
|
|
@ -1,32 +1,321 @@
|
|||
import './ActionButton.js' // To define action-button
|
||||
|
||||
export function marshalActionButtonsJsonToHtml (json) {
|
||||
export function marshalDashboardComponentsJsonToHtml (json) {
|
||||
marshalActionsJsonToHtml(json)
|
||||
marshalDashboardStructureToHtml(json)
|
||||
|
||||
window.changeDirectory = changeDirectory
|
||||
window.showSection = showSection
|
||||
|
||||
changeDirectory(null)
|
||||
}
|
||||
|
||||
function marshalActionsJsonToHtml (json) {
|
||||
const currentIterationTimestamp = Date.now()
|
||||
|
||||
for (const jsonButton of json.actions) {
|
||||
let htmlButton = document.querySelector('#execution-' + jsonButton.id)
|
||||
window.actionButtons = {}
|
||||
|
||||
if (htmlButton == null) {
|
||||
for (const jsonButton of json.actions) {
|
||||
let htmlButton = window.actionButtons[jsonButton.id]
|
||||
|
||||
if (typeof htmlButton === 'undefined') {
|
||||
htmlButton = document.createElement('action-button')
|
||||
htmlButton.constructFromJson(jsonButton)
|
||||
|
||||
document.getElementById('root-group').appendChild(htmlButton)
|
||||
} else {
|
||||
htmlButton.updateFromJson(jsonButton)
|
||||
htmlButton.updateDom()
|
||||
window.actionButtons[jsonButton.title] = htmlButton
|
||||
}
|
||||
|
||||
htmlButton.updateFromJson(jsonButton)
|
||||
htmlButton.updateIterationTimestamp = currentIterationTimestamp
|
||||
}
|
||||
|
||||
// Remove existing, but stale buttons (that were not updated in this round)
|
||||
for (const existingButton of document.querySelector('#contentActions').querySelectorAll('action-button')) {
|
||||
for (const existingButton of document.querySelectorAll('action-button')) {
|
||||
if (existingButton.updateIterationTimestamp !== currentIterationTimestamp) {
|
||||
existingButton.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showSection (title) {
|
||||
for (const section of document.querySelectorAll('section')) {
|
||||
if (section.title === title) {
|
||||
section.style.display = 'block'
|
||||
} else {
|
||||
section.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
for (const otherName of ['Actions', 'Logs']) {
|
||||
document.getElementById('show' + otherName).classList.remove('activeSection')
|
||||
document.getElementById('content' + otherName).hidden = true
|
||||
}
|
||||
|
||||
// document.getElementById('show' + name).classList.add('activeSection')
|
||||
// document.getElementById('content' + name).hidden = false
|
||||
|
||||
document.getElementById('hide-sidebar-checkbox').checked = true
|
||||
|
||||
changeDirectory(null)
|
||||
}
|
||||
|
||||
export function setupSectionNavigation (style) {
|
||||
const nav = document.querySelector('nav')
|
||||
|
||||
if (style === 'sidebar') {
|
||||
nav.classList += 'sidebar'
|
||||
|
||||
document.body.classList += 'has-sidebar'
|
||||
} else {
|
||||
nav.classList += 'topbar'
|
||||
|
||||
document.body.classList += 'has-topbar'
|
||||
}
|
||||
|
||||
nav.hidden = false
|
||||
|
||||
document.getElementById('showActions').onclick = () => { showSection('Actions') }
|
||||
document.getElementById('showLogs').onclick = () => { showSection('Logs') }
|
||||
}
|
||||
|
||||
function marshalDashboardStructureToHtml (json) {
|
||||
const nav = document.getElementById('navigation-links')
|
||||
|
||||
for (const dashboard of json.dashboards) {
|
||||
const oldsection = document.querySelector('section[title="' + dashboard.title + '"]')
|
||||
|
||||
if (oldsection != null) {
|
||||
oldsection.remove()
|
||||
}
|
||||
|
||||
const section = document.createElement('section')
|
||||
section.title = dashboard.title
|
||||
|
||||
const def = createFieldset('default', section)
|
||||
section.appendChild(def)
|
||||
|
||||
document.getElementsByTagName('main')[0].appendChild(section)
|
||||
marshalContainerContents(dashboard, section, def, dashboard.title)
|
||||
|
||||
const oldLi = nav.querySelector('li[title="' + dashboard.title + '"]')
|
||||
|
||||
if (oldLi != null) {
|
||||
oldLi.remove()
|
||||
}
|
||||
|
||||
const navigationA = document.createElement('a')
|
||||
navigationA.title = dashboard.title
|
||||
navigationA.innerText = dashboard.title
|
||||
navigationA.onclick = () => {
|
||||
showSection(dashboard.title)
|
||||
}
|
||||
|
||||
const navigationLi = document.createElement('li')
|
||||
navigationLi.appendChild(navigationA)
|
||||
navigationLi.title = dashboard.title
|
||||
|
||||
document.getElementById('navigation-links').appendChild(navigationLi)
|
||||
}
|
||||
|
||||
if (json.dashboards.length === 0) {
|
||||
showSection('Actions')
|
||||
} else {
|
||||
showSection(json.dashboards[0].title)
|
||||
}
|
||||
|
||||
const rootGroup = document.querySelector('#root-group')
|
||||
|
||||
let hasRootActions = false
|
||||
|
||||
for (const btn of Object.values(window.actionButtons)) {
|
||||
if (btn.parentElement === null) {
|
||||
rootGroup.appendChild(btn)
|
||||
hasRootActions = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasRootActions) {
|
||||
nav.querySelector('li[title="Actions"]').style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
function marshalLink (item, fieldset) {
|
||||
let btn = window.actionButtons[item.link]
|
||||
|
||||
if (typeof btn === 'undefined') {
|
||||
btn = document.createElement('button')
|
||||
btn.innerText = 'Action not found: ' + item.link
|
||||
btn.classList.add('error')
|
||||
}
|
||||
|
||||
fieldset.appendChild(btn)
|
||||
}
|
||||
|
||||
function marshalContainerContents (json, section, fieldset, parentDashboard) {
|
||||
for (const item of json.contents) {
|
||||
switch (item.type) {
|
||||
case 'fieldset':
|
||||
marshalFieldset(item, section, parentDashboard)
|
||||
break
|
||||
case 'directory':
|
||||
marshalDirectoryButton(item, fieldset)
|
||||
marshalDirectory(item, section)
|
||||
break
|
||||
case 'link':
|
||||
marshalLink(item, fieldset)
|
||||
break
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createFieldset (title, parentDashboard) {
|
||||
const legend = document.createElement('legend')
|
||||
legend.innerText = title
|
||||
|
||||
const fs = document.createElement('fieldset')
|
||||
fs.title = title
|
||||
fs.appendChild(legend)
|
||||
|
||||
if (typeof parentDashboard === 'undefined') {
|
||||
fs.setAttribute('parent-dashboard', '')
|
||||
} else {
|
||||
fs.setAttribute('parent-dashboard', parentDashboard)
|
||||
}
|
||||
|
||||
return fs
|
||||
}
|
||||
|
||||
function marshalFieldset (item, section, parentDashboard) {
|
||||
const fs = createFieldset(item.title, parentDashboard)
|
||||
|
||||
marshalContainerContents(item, section, fs)
|
||||
|
||||
section.appendChild(fs)
|
||||
}
|
||||
|
||||
function changeDirectory (selected) {
|
||||
if (selected === '') {
|
||||
selected = null
|
||||
}
|
||||
|
||||
if (selected === null) {
|
||||
window.directoryNavigation = []
|
||||
} else if (selected === '..') {
|
||||
window.directoryNavigation.pop()
|
||||
|
||||
if (window.directoryNavigation.length > 0) {
|
||||
selected = window.directoryNavigation[window.directoryNavigation.length - 1]
|
||||
} else {
|
||||
selected = null
|
||||
}
|
||||
} else {
|
||||
// If the selected item is already in the nav list, pop elements until we get
|
||||
// "back" to the existing nav item
|
||||
while (window.directoryNavigation.includes(selected)) {
|
||||
window.directoryNavigation.pop()
|
||||
}
|
||||
|
||||
window.directoryNavigation.push(selected)
|
||||
}
|
||||
|
||||
for (const fieldset of document.querySelectorAll('fieldset')) {
|
||||
if (selected === null) {
|
||||
if ((fieldset.id === 'root-group' || fieldset.getAttribute('parent-dashboard') !== '') && fieldset.children.length > 1) {
|
||||
fieldset.style.display = 'grid'
|
||||
} else {
|
||||
fieldset.style.display = 'none'
|
||||
}
|
||||
} else {
|
||||
if (fieldset.title === selected) {
|
||||
fieldset.style.display = 'grid'
|
||||
} else {
|
||||
fieldset.style.display = 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const title = document.querySelector('h1')
|
||||
title.innerHTML = ''
|
||||
|
||||
const rootLink = createDirectoryBreadcrumb('OliveTin', null)
|
||||
title.appendChild(rootLink)
|
||||
|
||||
for (const dir of window.directoryNavigation) {
|
||||
const sep = document.createElement('span')
|
||||
sep.innerHTML = ' » '
|
||||
title.append(sep)
|
||||
|
||||
if (dir === selected) {
|
||||
title.append(selected)
|
||||
} else {
|
||||
title.appendChild(createDirectoryBreadcrumb(dir))
|
||||
}
|
||||
}
|
||||
|
||||
document.title = title.innerText
|
||||
|
||||
if (selected === null) {
|
||||
window.location.hash = null
|
||||
|
||||
window.history.pushState({ dir: null }, null, '#')
|
||||
} else {
|
||||
window.location.hash = selected
|
||||
|
||||
window.history.pushState({ dir: selected }, null, '#' + selected)
|
||||
}
|
||||
}
|
||||
|
||||
function createDirectoryBreadcrumb (title, link) {
|
||||
const a = document.createElement('a')
|
||||
a.innerText = title
|
||||
a.title = title
|
||||
|
||||
if (typeof link === 'undefined') {
|
||||
link = title
|
||||
}
|
||||
|
||||
if (link === null) {
|
||||
a.href = '#'
|
||||
} else {
|
||||
a.href = '#' + link
|
||||
}
|
||||
|
||||
a.onclick = () => {
|
||||
changeDirectory(link)
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
function marshalDirectoryButton (item, fieldset) {
|
||||
const directoryButton = document.createElement('button')
|
||||
directoryButton.innerHTML = '<span class = "icon">📁</span> ' + item.title
|
||||
directoryButton.onclick = () => {
|
||||
changeDirectory(item.title)
|
||||
}
|
||||
|
||||
fieldset.appendChild(directoryButton)
|
||||
}
|
||||
|
||||
function marshalDirectory (item, section) {
|
||||
const fs = createFieldset(item.title)
|
||||
fs.style.display = 'none'
|
||||
|
||||
const directoryBackButton = document.createElement('button')
|
||||
directoryBackButton.innerHTML = '«'
|
||||
directoryBackButton.title = 'Go back one directory'
|
||||
directoryBackButton.onclick = () => {
|
||||
changeDirectory('..')
|
||||
}
|
||||
|
||||
fs.appendChild(directoryBackButton)
|
||||
|
||||
marshalContainerContents(item, section, fs)
|
||||
|
||||
section.appendChild(fs)
|
||||
}
|
||||
|
||||
export function marshalLogsJsonToHtml (json) {
|
||||
for (const logEntry of json.logs) {
|
||||
const tpl = document.getElementById('tplLogRow')
|
||||
|
|
@ -69,3 +358,10 @@ export function marshalLogsJsonToHtml (json) {
|
|||
document.querySelector('#logTableBody').prepend(row)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', (e) => {
|
||||
e.preventDefault()
|
||||
if (e.state != null && typeof e.state.dir !== 'undefined') {
|
||||
changeDirectory(e.state.dir)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use strict'
|
||||
|
||||
import { marshalActionButtonsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js'
|
||||
import { setupSectionNavigation, marshalDashboardComponentsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js'
|
||||
import { checkWebsocketConnection } from './js/websocket.js'
|
||||
|
||||
function searchLogs (e) {
|
||||
|
|
@ -24,25 +24,6 @@ function searchLogsClear () {
|
|||
document.getElementById('logSearchBox').value = ''
|
||||
}
|
||||
|
||||
function showSection (name) {
|
||||
for (const otherName of ['Actions', 'Logs']) {
|
||||
document.getElementById('show' + otherName).classList.remove('activeSection')
|
||||
document.getElementById('content' + otherName).hidden = true
|
||||
}
|
||||
|
||||
document.getElementById('show' + name).classList.add('activeSection')
|
||||
document.getElementById('content' + name).hidden = false
|
||||
|
||||
document.getElementById('hide-sidebar-checkbox').checked = true
|
||||
}
|
||||
|
||||
function setupSections () {
|
||||
document.getElementById('showActions').onclick = () => { showSection('Actions') }
|
||||
document.getElementById('showLogs').onclick = () => { showSection('Logs') }
|
||||
|
||||
showSection('Actions')
|
||||
}
|
||||
|
||||
function setupLogSearchBox () {
|
||||
document.getElementById('logSearchBox').oninput = searchLogs
|
||||
document.getElementById('searchLogsClear').onclick = searchLogsClear
|
||||
|
|
@ -91,7 +72,7 @@ function fetchGetDashboardComponents () {
|
|||
}
|
||||
|
||||
window.restAvailable = true
|
||||
marshalActionButtonsJsonToHtml(res)
|
||||
marshalDashboardComponentsJsonToHtml(res)
|
||||
|
||||
refreshServerConnectionLabel() // in-case it changed, update the label quicker
|
||||
}).catch((err) => { // err is 1st arg
|
||||
|
|
@ -142,8 +123,8 @@ function processWebuiSettingsJson (settings) {
|
|||
}
|
||||
|
||||
function main () {
|
||||
setupSections()
|
||||
setupLogSearchBox()
|
||||
setupSectionNavigation('sidebar')
|
||||
|
||||
window.fetch('webUiSettings.json').then(res => {
|
||||
return res.json()
|
||||
|
|
|
|||
|
|
@ -1,28 +1,25 @@
|
|||
body {
|
||||
background-color: #dee3e7;
|
||||
color: black;
|
||||
text-align: center;
|
||||
font-family: sans-serif;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
dialog {
|
||||
box-shadow: 0 0 6px 0 #444;
|
||||
max-width: 600px;
|
||||
text-align: left;
|
||||
padding: 1em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
fieldset#root-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, 160px);
|
||||
grid-template-rows: auto auto auto auto;
|
||||
grid-gap: 1em;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
border: 0;
|
||||
}
|
||||
|
|
@ -62,48 +59,67 @@ footer a {
|
|||
color: black;
|
||||
}
|
||||
|
||||
aside {
|
||||
nav.sidebar {
|
||||
background-color: white;
|
||||
position: absolute;
|
||||
width: 180px;
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
left: 0;
|
||||
top: 0;
|
||||
padding-top: 4em;
|
||||
transition: 0.5s ease;
|
||||
background-color: white;
|
||||
border: 0 0 10px 0;
|
||||
box-shadow: 0 0 10px 0 #444;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
input:checked ~ aside {
|
||||
#navigation-links {
|
||||
padding-top: 4em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#supplemental-links {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
input:checked ~ nav.sidebar {
|
||||
left: -250px;
|
||||
}
|
||||
|
||||
aside ul {
|
||||
nav ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
aside ul li {
|
||||
nav ul li {
|
||||
list-style: none;
|
||||
text-align: left;
|
||||
border-bottom: 1px inset black;
|
||||
}
|
||||
|
||||
aside ul li a {
|
||||
nav ul li a {
|
||||
display: block;
|
||||
padding-left: 1em;
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
aside ul li a:hover {
|
||||
nav ul li a:hover {
|
||||
color: black;
|
||||
background-color: #efefef;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
nav.topbar {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
nav.topbar ul li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
table {
|
||||
background-color: white;
|
||||
border-collapse: collapse;
|
||||
|
|
@ -204,8 +220,8 @@ action-button {
|
|||
}
|
||||
|
||||
action-button button {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
|
|
@ -307,6 +323,9 @@ img.logo {
|
|||
|
||||
main {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
body.has-sidebar main {
|
||||
padding-top: 3em;
|
||||
}
|
||||
|
||||
|
|
@ -417,6 +436,10 @@ div.toolbar * {
|
|||
}
|
||||
|
||||
@media screen and (width <= 600px) {
|
||||
fieldset {
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
}
|
||||
|
||||
label {
|
||||
text-align: left;
|
||||
margin-bottom: .6em;
|
||||
|
|
@ -464,7 +487,7 @@ div.toolbar * {
|
|||
color: white;
|
||||
}
|
||||
|
||||
aside {
|
||||
nav {
|
||||
background-color: #111;
|
||||
color: white;
|
||||
}
|
||||
|
|
@ -474,7 +497,7 @@ div.toolbar * {
|
|||
color: gray;
|
||||
}
|
||||
|
||||
aside ul li a:hover {
|
||||
nav ul li a:hover {
|
||||
background-color: #666;
|
||||
color: white;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue