Initial commit
This commit is contained in:
commit
8863b4c139
135
css/main.css
Normal file
135
css/main.css
Normal file
@ -0,0 +1,135 @@
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
[onClick] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 14pt;
|
||||
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
header {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: min-content min-content 1fr min-content;
|
||||
grid-gap: 16px;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
header #pages {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-gap: 16px;
|
||||
}
|
||||
|
||||
header .page {
|
||||
white-space: nowrap;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#page-label {
|
||||
font-weight: 500;
|
||||
font-size: 1.25em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#theme {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, min-content);
|
||||
grid-gap: 6px;
|
||||
}
|
||||
|
||||
#theme select, #theme button {
|
||||
font-size: 0.85em;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
#sections {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
margin-top: 32px;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.section {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
.section {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.section .name {
|
||||
padding: 3px 6px 6px 6px;
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.section .items {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.section .items .item {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-template-rows:
|
||||
min-content
|
||||
min-content
|
||||
min-content
|
||||
;
|
||||
grid-gap: 4px 10px;
|
||||
cursor: pointer;
|
||||
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.theme-brighter .item:hover {
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
|
||||
.theme-darker .item:hover {
|
||||
filter: brightness(0.85);
|
||||
}
|
||||
|
||||
.section .item .icon {
|
||||
grid-row: 1 / -1;
|
||||
font-size: 1.65em;
|
||||
}
|
||||
|
||||
.section .item .label {
|
||||
align-self: center;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.section .item .description {
|
||||
align-self: center;
|
||||
font-weight: 400;
|
||||
font-size: 0.85em;
|
||||
word-break: normal;
|
||||
overflow-wrap: anywhere;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.section .item .url {
|
||||
align-self: center;
|
||||
font-weight: 300;
|
||||
font-size: 0.85em;
|
||||
}
|
3
css/materialdesignicons.min.css
vendored
Normal file
3
css/materialdesignicons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
fonts/materialdesignicons-webfont.eot
Normal file
BIN
fonts/materialdesignicons-webfont.eot
Normal file
Binary file not shown.
BIN
fonts/materialdesignicons-webfont.ttf
Normal file
BIN
fonts/materialdesignicons-webfont.ttf
Normal file
Binary file not shown.
BIN
fonts/materialdesignicons-webfont.woff
Normal file
BIN
fonts/materialdesignicons-webfont.woff
Normal file
Binary file not shown.
BIN
fonts/materialdesignicons-webfont.woff2
Normal file
BIN
fonts/materialdesignicons-webfont.woff2
Normal file
Binary file not shown.
32
index.html
Normal file
32
index.html
Normal file
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/materialdesignicons.min.css">
|
||||
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"yaml": "/js/yaml/index.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="js/app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div id="page-label"></div>
|
||||
<div id="pages"></div>
|
||||
<div class="spacer"></div>
|
||||
<div id="theme"></div>
|
||||
</header>
|
||||
<div id="sections"></div>
|
||||
</body>
|
||||
</html>
|
277
js/app.js
Normal file
277
js/app.js
Normal file
@ -0,0 +1,277 @@
|
||||
// import * as YAML from 'yaml'
|
||||
import jsYaml from '/js/js-yaml.mjs'
|
||||
|
||||
class App {
|
||||
constructor() {//{{{
|
||||
window._app = this
|
||||
|
||||
this.theme = ''
|
||||
this.themes = { default: Theme.default() }
|
||||
this.sections = []
|
||||
|
||||
this.retrieveThemes()
|
||||
.then(()=>this.retrieveConfig())
|
||||
.then(config=>this.parseConfig(config))
|
||||
.then(()=>this.setTheme(this.theme))
|
||||
.then(()=>this.renderPage())
|
||||
.catch(alert)
|
||||
}//}}}
|
||||
async retrieveThemes() {//{{{
|
||||
return fetch(`/themes.yaml`)
|
||||
.then(res=>{
|
||||
if(res.ok)
|
||||
return res.text()
|
||||
throw `Error when fetching /theme.yaml: ${res.status} ${res.statusText}`
|
||||
})
|
||||
.then(text=>{
|
||||
let themes = jsYaml.load(text)
|
||||
Object.keys(themes).forEach(themeName=>{
|
||||
this.themes[themeName] = new Theme(themeName, themes[themeName])
|
||||
})
|
||||
})
|
||||
}//}}}
|
||||
retrieveConfig() {//{{{
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
let page = urlParams.get('page')
|
||||
if(page === null)
|
||||
page = 'start'
|
||||
|
||||
return fetch(`/${page}.yaml`)
|
||||
.then(res=>{
|
||||
if(res.ok)
|
||||
return res.text()
|
||||
throw `Error when fetching /${page}.yaml: ${res.status} ${res.statusText}`
|
||||
})
|
||||
.then(text=>{
|
||||
return jsYaml.load(text)
|
||||
})
|
||||
}//}}}
|
||||
parseConfig(config) {//{{{
|
||||
// Pages to switch between
|
||||
this.pages = config.pages ? config.pages : []
|
||||
|
||||
// Settings for this page.
|
||||
this.page = config.page
|
||||
|
||||
// Theme to be used for the initial page render.
|
||||
// Can be changed later.
|
||||
this.theme = this.themes[config.page.theme ? config.page.theme : 'default']
|
||||
if(this.theme === undefined)
|
||||
throw new Error(`Theme '${config.page.theme}' isn't defined in themes.yaml.`)
|
||||
|
||||
// Sections are created; items are created within the Section objects.
|
||||
if(config.sections === undefined)
|
||||
config.sections = []
|
||||
this.sections = config.sections.map((d,idx)=>new Section(idx, d))
|
||||
}//}}}
|
||||
renderPage() {//{{{
|
||||
// Page header
|
||||
document.querySelector('header').style.backgroundColor = this.theme.colors.page.header
|
||||
|
||||
// Pages to switch between
|
||||
let pageStyle = `color: ${this.theme.colors.page_select.text}; background: ${this.theme.colors.page_select.background}`
|
||||
let pages = this.pages.map(page=>`<div class="page" style="${pageStyle}" onclick="window.open('/?page=${page.name}', '_self')">${page.label}</div>`)
|
||||
document.getElementById('pages').innerHTML = pages.join('')
|
||||
|
||||
// Page label
|
||||
let pageLabel = document.getElementById('page-label')
|
||||
pageLabel.innerHTML = this.page.label
|
||||
pageLabel.style.color = this.theme.colors.page.text
|
||||
|
||||
// The theme selector is sorted by name for easier usage.
|
||||
let themeOptions = Object.keys(this.themes)
|
||||
.sort((a, b)=>{
|
||||
if(a < b) return -1
|
||||
if(a > b) return 1
|
||||
return 0
|
||||
})
|
||||
.map(themeName=>`<option value="${themeName}" ${this.theme.name == themeName ? 'selected' : ''}>${themeName}</option>`)
|
||||
.join('')
|
||||
|
||||
// Theme selector also has prev and next buttons to
|
||||
// faster switch between themes (probably trying them out).
|
||||
let t = this.theme
|
||||
let themes = document.getElementById('theme')
|
||||
let style = `color: ${t.colors.theme_select.text}; background: ${t.colors.theme_select.background}; border: 1px solid ${t.colors.theme_select.border}`
|
||||
themes.innerHTML = `
|
||||
<button style="${style}" onclick="window._app.previousTheme()"><</button>
|
||||
<select id="theme-select" onchange="window._app.selectTheme()" style="${style}">
|
||||
${themeOptions}
|
||||
</select>
|
||||
<button style="${style}" onclick="window._app.nextTheme()">></button>
|
||||
`
|
||||
|
||||
let sectionHTML = this.sections.map(s=>s.html())
|
||||
document.getElementById('sections').innerHTML = sectionHTML.join('')
|
||||
}//}}}
|
||||
setTheme(theme) {//{{{
|
||||
this.theme = theme
|
||||
document.body.style.color = theme.colors.page.text
|
||||
document.body.style.backgroundColor = theme.colors.page.background
|
||||
|
||||
let sectionClasses = document.getElementById('sections').classList
|
||||
sectionClasses.remove('theme-brighter')
|
||||
sectionClasses.remove('theme-darker')
|
||||
if(theme.hover == 'brighter')
|
||||
sectionClasses.add('theme-brighter')
|
||||
else if(theme.hover == 'darker')
|
||||
sectionClasses.add('theme-darker')
|
||||
}//}}}
|
||||
selectTheme() {//{{{
|
||||
let option = document.querySelector('#theme option:checked')
|
||||
this.setThemeByName(option.value)
|
||||
|
||||
// Focus the theme select box after page render, since
|
||||
// user could be trying themes out and wanting to use the
|
||||
// keyboard up/down.
|
||||
document.querySelector('#theme select').focus()
|
||||
}//}}}
|
||||
setThemeByName(name) {//{{{
|
||||
this.setTheme(this.themes[name])
|
||||
this.renderPage()
|
||||
}//}}}
|
||||
previousTheme() {//{{{
|
||||
let selected = document.querySelector('#theme option:checked')
|
||||
if(!selected)
|
||||
return
|
||||
let prevOption = selected.previousElementSibling
|
||||
if(prevOption) {
|
||||
this.setThemeByName(prevOption.value)
|
||||
}
|
||||
}//}}}
|
||||
nextTheme() {//{{{
|
||||
let selected = document.querySelector('#theme option:checked')
|
||||
if(!selected)
|
||||
return
|
||||
let nextOption = selected.nextElementSibling
|
||||
if(nextOption) {
|
||||
this.setThemeByName(nextOption.value)
|
||||
}
|
||||
}//}}}
|
||||
open(url) {//{{{
|
||||
window.open(url, '_blank')
|
||||
}//}}}
|
||||
}
|
||||
|
||||
class Theme {
|
||||
static default() {//{{{
|
||||
return new Theme('default', {
|
||||
page: {
|
||||
text: '#fff',
|
||||
header: '#333',
|
||||
background: '#fff'
|
||||
},
|
||||
|
||||
page_select: {
|
||||
text: "#ccc",
|
||||
background: "#666"
|
||||
},
|
||||
|
||||
theme_select: {
|
||||
text: "#ccc",
|
||||
background: "#333",
|
||||
border: "#888"
|
||||
},
|
||||
|
||||
section: {
|
||||
background: '#fff',
|
||||
borders: [
|
||||
['#fff', '#22a511']
|
||||
]
|
||||
},
|
||||
|
||||
item: {
|
||||
label: '#333',
|
||||
background: '#eaeaea',
|
||||
description: '#555',
|
||||
url: '#666',
|
||||
icon: '#000',
|
||||
hover: 'darker'
|
||||
}
|
||||
})
|
||||
}//}}}
|
||||
constructor(name, data) {//{{{
|
||||
this.name = name
|
||||
this.hover = data.item.hover
|
||||
this.colors = data
|
||||
}//}}}
|
||||
colorNo(i) {//{{{
|
||||
// Returns a wrapping color from the list of
|
||||
let idx = i % this.colors.section.borders.length
|
||||
return this.colors.section.borders[idx]
|
||||
}//}}}
|
||||
}
|
||||
|
||||
class Section {
|
||||
constructor(idx, data) {//{{{
|
||||
this.section_index = idx
|
||||
this.label = 'Section'
|
||||
this.icon = 'head-question'
|
||||
this.items = []
|
||||
|
||||
Object.keys(data).forEach(key=>
|
||||
this[key] = data[key]
|
||||
)
|
||||
}//}}}
|
||||
html() {//{{{
|
||||
let theme = window._app.theme
|
||||
let colorIndex = this.color ? this.color : this.section_index
|
||||
let color = theme.colorNo(colorIndex)
|
||||
|
||||
let itemHTML = this.items.map(itemData=>
|
||||
new Item(this, itemData).html()
|
||||
)
|
||||
|
||||
return `
|
||||
<div class="section" style="border: 4px solid ${color[0]}; background: ${theme.colors.section.background}">
|
||||
<div class="name" style="background: ${color[0]}; color: ${color[1]}">${this.label}</div>
|
||||
<div class="items" style="background: ${theme.colors.section.background}">
|
||||
${itemHTML.join('')}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}//}}}
|
||||
}
|
||||
|
||||
class Item {
|
||||
constructor(section, data) {//{{{
|
||||
this.section = section
|
||||
this.label = 'Item'
|
||||
this.icon = 'head-question'
|
||||
this.url = 'https://example.com'
|
||||
|
||||
Object.keys(data).forEach(key=>
|
||||
this[key] = data[key]
|
||||
)
|
||||
}//}}}
|
||||
html() {//{{{
|
||||
let theme = window._app.theme
|
||||
let colorIndex = this.section.color ? this.section.color : this.section.section_index
|
||||
let sectionColor = theme.colorNo(colorIndex)[0]
|
||||
|
||||
let colorLabel = theme.colors.item.label
|
||||
if(!colorLabel)
|
||||
colorLabel = sectionColor
|
||||
|
||||
let colorIcon = theme.colors.item.icon
|
||||
if(!colorIcon)
|
||||
colorIcon = sectionColor
|
||||
|
||||
let description = ''
|
||||
if(this.description)
|
||||
description = `<div class="description" style="color: ${theme.colors.item.description}">${this.description}</div>`
|
||||
|
||||
return `
|
||||
<div class="item" style="background: ${theme.colors.item.background}" onclick="window._app.open('${this.url}')">
|
||||
<div class="icon mdi-set mdi-${this.icon}" style="color: ${colorIcon}"></div>
|
||||
<div class="label" style="color: ${colorLabel}">${this.label}</div>
|
||||
${description}
|
||||
<div class="url" style="color: ${theme.colors.item.url}">${this.url}</div>
|
||||
</div>
|
||||
`
|
||||
}//}}}
|
||||
}
|
||||
|
||||
let app = new App()
|
||||
|
||||
// vim: foldmethod=marker
|
3851
js/js-yaml.mjs
Normal file
3851
js/js-yaml.mjs
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user