Initial commit

This commit is contained in:
Magnus Åhall 2023-07-25 11:01:17 +00:00
commit 8863b4c139
10 changed files with 4298 additions and 0 deletions

0
README.md Normal file
View File

135
css/main.css Normal file
View 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

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

32
index.html Normal file
View 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
View 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()">&lt;</button>
<select id="theme-select" onchange="window._app.selectTheme()" style="${style}">
${themeOptions}
</select>
<button style="${style}" onclick="window._app.nextTheme()">&gt;</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

File diff suppressed because it is too large Load Diff