From 8b85ea5952d7b264ebb1f3f3d541fe0991c98fca Mon Sep 17 00:00:00 2001 From: Lucien Cartier-Tilet Date: Tue, 5 May 2020 14:49:26 +0200 Subject: [PATCH] Initial commit, added all backend files --- .gitignore | 2 + Dockerfile | 19 ++ README.org | 144 +++++++++ analysis_options.yaml | 14 + docker-compose.yml | 10 + pubspec.yaml | 16 + start.sh | 3 + web/dart/main.dart | 9 + web/dart/navbar.dart | 149 +++++++++ web/dart/parse_sitemap.dart | 43 +++ web/dart/reorganize_html.dart | 71 ++++ web/dart/theme.dart | 53 +++ web/style/style.scss | 591 ++++++++++++++++++++++++++++++++++ 13 files changed, 1124 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.org create mode 100644 analysis_options.yaml create mode 100644 docker-compose.yml create mode 100644 pubspec.yaml create mode 100755 start.sh create mode 100644 web/dart/main.dart create mode 100644 web/dart/navbar.dart create mode 100644 web/dart/parse_sitemap.dart create mode 100644 web/dart/reorganize_html.dart create mode 100644 web/dart/theme.dart create mode 100644 web/style/style.scss diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06b34da --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.css +*.map diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8fe9775 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM google/dart:2.7 + +WORKDIR /app + +# Get Dart dependencies +RUN mkdir -p /pub-cache +ENV PUB_CACHE=/pub-cache +ENV PATH="${PATH}:/pub-cache/bin" +RUN pub global activate webdev +ADD pubspec.* /app/ +RUN pub get +RUN pub get --offline + +RUN apt update && apt install ruby-sass ruby-dev build-essential -y +RUN gem install sass-listen + +ADD . /app/ + +CMD ["./start.sh"] diff --git a/README.org b/README.org new file mode 100644 index 0000000..6ccd9eb --- /dev/null +++ b/README.org @@ -0,0 +1,144 @@ +#+title: Org Website Backend +#+author: Lucien Cartier-Tilet +#+STARTUP: content + +* Org Website Backend +** Introduction + Org Website Backend, or OWB, is a backend I created for my org-generated + static websites. It was originally developed as a backend for my linguistics + website [[https://langue.phundrak.com][langue.phundrak.com]], but eventually I began using its Dart and Scss + source code with other generated websites and pages of mine, such as my + configuration website hosted on [[https://phundrak.com/config][phundrak.com/config]]. I now want to just have + it stand on its own, while my org-generated websites stay Dart and Scss code + free. + +** What this backend does + This has one goal: provide my org-generated websites a beautiful and unified + interface. This is achieved by reorganizing the HTML generated by Emacs when + publishing my org files, and by reading dynamically the website’s sitemap in + order to generate some user menus so they can navigate freely on the website + without the need to go back to the main page. + + Visually, it also provides the user three themes: + - a light theme, enabled by default + - a dark theme, easier on the eyes + - a black theme, easier on smartphones’ battery if they have an AMOLED screen + The user’s preferences are kept on their browser’s local storage, so no + cookies are used. + +** Why Dart? + Dart is a programming language developed by Google, which aims to be + compilable as native code or as Javascript code. In this case, I use it + compiled as Javascript. Why not Javascript then? I personally find Dart much + easier to work with, and to be a way saner language than Javascript is. It + also ensures type-safety and —to some extent— some compile-time code + verification. The dart compiler also performs some optimization at + compile-time, which is really benificial. + +** Why SCSS? + SCSS is a superset of CSS which aims at simplifying CSS users’ life, and it’s + really good at its job. I especially enjoy being able to nest blocks within + one another, there’s no more need to rewrite endlessly some lines that could + simply be generated by SCSS. Why SCSS and not SASS? The answer is simple: I + have a buggy SASS installation, but SCSS works fine. Yep, simple as that. + + Another thing is that I use the Ruby implementation of SASS. The reason for + that is also simple: it is the only one that provides a ~--watch~ option so + it automatically recompiles SCSS code to CSS when the SCSS code is changed. + +** How to run this backend + This backend delivers only two main files: + - =/dart/main.dart.js= The main dart file compiled to Javascript (you don’t + need to worry about the others), + - =/style/style.css= The main style file compiled to CSS. + This is everything you need for beautiful org-generated websites. + +*** Running locally + You could install Dart on your machine, as well as the Ruby implementation + of SASS with its dependencies. Then, all you have to do is run ~start.sh~, + and you’re good to go! Content will be delivered on the 8080 port. If you + wish to deliver content to another port, you can edit this file. + +*** Docker + A Dockerfile is also provided so you can run this server inside a Docker + container, and thus you can avoid the hassle of installing Dart and Ruby + Sass. In order to run OWB, you can run the following: + #+BEGIN_SRC sh + $ docker build . --tag owb:1.0 + $ docker run \ + -p 8080:8080 \ + -e RELEASE="release" \ + --restart always \ + --detach \ + --name owb \ + owb:1.0 + #+END_SRC + + If you wish to run this in development mode, you could attach a volume so + the Dart and SCSS code are linked between your container and your + filesystem. This will also make ~webserver~ run in development mode; expect + shorter Dart compilation time, but slower Dart code execution. + #+BEGIN_SRC sh + $ docker run \ + -p 8080:8080 \ + -v ./web:/app/web \ + -e RELEASE="debug" \ + --restart always \ + --detach \ + --name owb \ + owb:1.0 + #+END_SRC + +*** Docker-compose + This repository also provides a ~docker-compose.yml~ file for easier Docker + usage with ~docker-compose~. If you wish to run your backend in + release-mode, simply run the following: + #+BEGIN_SRC sh + docker-compose up --detach + #+END_SRC + + If, as above, you wish to enter development mode, you could modify the + ~docker-compose.yml~ file like so: + #+BEGIN_SRC yaml + version: '3' + + services: + web: + build: . + environment: + - RELEASE="debug" + ports: + - 8080:8080 + restart: always + volumes: + - ./web/:/app/web/ + #+END_SRC + +** How can I use this in my org files? + Let’s say you serve your files on org.example.com, add the following lines to + the top of your org file: + #+BEGIN_SRC org + ,#+HTML_HEAD_EXTRA: + ,#+HTML_HEAD_EXTRA: + ,#+HTML_HEAD_EXTRA: + #+END_SRC + + You will need to obtain a (free) license on Fontawesome to use fontawesome’s + icons. Then, once you have this license, use your token provided by them to + edit the third header above. + + Another option is to redirect any request of your website directed to ~/dart~ + or ~/style~ to your running instance with the help of your reverse proxy, + such as Nginx. You could have for example the following lines: + #+BEGIN_SRC nginx + location /dart { + root http://localhost:8080/dart; + } + + location /style { + root http://localhost:8080/style; + } + #+END_SRC + + Of course, be careful to write the same port in the rules above as the port + your backend is serving on. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..a686c1b --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,14 @@ +# Defines a default set of lint rules enforced for +# projects at Google. For details and rationale, +# see https://github.com/dart-lang/pedantic#enabled-lints. +include: package:pedantic/analysis_options.yaml + +# For lint rules and documentation, see http://dart-lang.github.io/linter/lints. +# Uncomment to specify additional rules. +# linter: +# rules: +# - camel_case_types + +analyzer: +# exclude: +# - path/to/excluded/files/** diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..48b9d02 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3' + +services: + web: + build: . + ports: + - 8010:8080 + restart: always + volumes: + - ./web/:/app/web/ diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..96b0265 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,16 @@ +name: languephundrakcom +description: A bare-bone server for my linguistics website. +version: 1.0.0 +homepage: https://langue.phundrak.com +author: Lucien Cartier-Tilet + +environment: + sdk: '>=2.5.0 <3.0.0' + +dependencies: + html: '^0.14.0+3' + +dev_dependencies: + build_runner: ^1.8.0 + build_web_compilers: ^2.9.0 + pedantic: ^1.9.0 diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..c655788 --- /dev/null +++ b/start.sh @@ -0,0 +1,3 @@ +#!/bin/bash +sass --watch web/style/:web/style -tcompressed & +webdev serve --release --hostname 0.0.0.0 diff --git a/web/dart/main.dart b/web/dart/main.dart new file mode 100644 index 0000000..5b17663 --- /dev/null +++ b/web/dart/main.dart @@ -0,0 +1,9 @@ +import './reorganize_html.dart' show reorganizeHtml; +import './theme.dart' show enableThemeChanger, setTheme; + +Future main() async { + await setTheme(); + await reorganizeHtml().then((_) { + enableThemeChanger(); + }); +} diff --git a/web/dart/navbar.dart b/web/dart/navbar.dart new file mode 100644 index 0000000..4e404af --- /dev/null +++ b/web/dart/navbar.dart @@ -0,0 +1,149 @@ +import 'dart:html'; + +import './parse_sitemap.dart' show parseSitemap; + +// Returns the title of the current webpage +String getPageTitle() { + return querySelector('title').text; +} + +Element makeIcon(List classes, [String id]) { + final icon = Element.tag('i')..classes.addAll(classes); + if (id != null) { + icon.attributes['id'] = id; + } + return icon; +} + +Future makeToc() async { + return Element.li() + ..attributes['id'] = 'toc-drop' + ..classes.addAll(['nav-item', 'has-dropdown']) + ..append(Element.a() + ..attributes['href'] = 'javascript:void(0)' + ..append(makeIcon(['fas', 'fa-list-ol'], 'tocBtn'))); +} + +Future makePages() async { + var pages = Element.ul() + ..attributes['id'] = 'drop-page' + ..classes.add('dropdown'); + await parseSitemap().then((final sitemap) => { + sitemap.forEach((url, name) { + final link = Element.li() + ..classes.add('dropdown-item') + ..insertAdjacentElement( + 'afterBegin', + Element.a() + ..attributes['href'] = url + ..innerText = name); + pages.insertAdjacentElement('beforeEnd', link); + }) + }); + return Element.li() + ..append(Element.a() + ..attributes['href'] = 'javascript:void(0)' + ..append(makeIcon(['fas', 'fa-flag']))) + ..classes.addAll(['nav-item', 'has-dropdown']) + ..insertAdjacentElement('beforeEnd', pages); +} + +Element makeShareLink(Element icon, String url) { + return Element.li() + ..classes.add('dropdown-item') + ..append(Element.a() + ..attributes['href'] = url + ..attributes['target'] = '_blank' + ..append(icon)); +} + +Future makeShare() async { + return Element.li() + ..classes.addAll(['nav-item', 'has-dropdown']) + ..append(Element.a() + ..attributes['href'] = 'javascript:void(0)' + ..append(makeIcon(['fas', 'fa-share-alt']))) + ..append(Element.ul() + ..classes.add('dropdown') + ..attributes['id'] = 'drop-share' + ..append(makeShareLink( + makeIcon(['fab', 'fa-twitter-square']), + 'https://twitter.com/share?text=${getPageTitle()}' + '&url=${window.location.href}')) + ..append(makeShareLink(makeIcon(['fab', 'fa-reddit-square']), + 'https://www.reddit.com/submit?title=${getPageTitle()}s&url=${window.location.href}')) + ..append(makeShareLink(makeIcon(['fas', 'fa-envelope-square']), + 'mailto:?subject=${getPageTitle}&body=${window.location.href}')) + ..append(makeShareLink( + makeIcon(['fab', 'fa-linkedin']), + 'https://www.linkedin.com/shareArticle?mini=true&url=${window.location.href}' + '&title=${getPageTitle()}')) + ..append(makeShareLink(makeIcon(['fab', 'fa-facebook-square']), + 'https://www.facebook.com/sharer/sharer.php?u=${window.location.href}'))); +} + +Future makeThemeChanger() async { + Element makeThemeItem(String t_btnId, Element t_icon, String t_text) { + return Element.li() + ..classes.add('dropdown-item') + ..append(Element.span() + ..attributes['id'] = t_btnId + ..append(t_icon) + ..appendText(' $t_text')); + } + + return Element.li() + ..classes.addAll(['nav-item', 'has-dropdown']) + ..append(Element.a() + ..attributes['href'] = 'javascript:void(0)' + ..append(Element.span() + ..classes.add('fa-stack') + ..style.verticalAlign = 'top' + ..append(makeIcon(['fas', 'fa-sun', 'fa-stack-1x']) + ..style.fontSize = '0.9em') + ..append(makeIcon(['fas', 'fa-moon', 'fa-stack-1x'])))) + ..append(Element.ul() + ..classes.add('dropdown') + ..attributes['id'] = 'theme-dropdown' + ..append(makeThemeItem('lightBtn', makeIcon(['fas', 'fa-sun']), 'Clair')) + ..append( + makeThemeItem('darkBtn', makeIcon(['fas', 'fa-lightbulb']), 'Sombre')) + ..append( + makeThemeItem('blackBtn', makeIcon(['fas', 'fa-moon']), 'Noir'))); +} + +Future makeHome() async { + return Element.li() + ..classes.add('nav-item') + ..insertAdjacentElement( + 'afterBegin', + Element.a() + ..attributes['href'] = '/' + ..append(makeIcon(['fas', 'fa-home']))); +} + +// Add a navbar atop of the HTML body, containing two buttons: +// - One back to home +// - A dropdown to each page detected in the sitemap +Future makeNavbar() async { + final navbar_content = Element.ul()..classes.add('navbar-nav'); + final home = await makeHome(); + final pages = await makePages(); + final toc = await makeToc(); + final share = await makeShare(); + final theme = await makeThemeChanger(); + + navbar_content + ..append(home) + ..append(pages) + ..append(toc) + ..append(share) + ..append(theme); + + // Navbar content added to navbar + final navbar = Element.nav() + ..classes.add('navbar') + ..append(navbar_content); + + return navbar; +} diff --git a/web/dart/parse_sitemap.dart b/web/dart/parse_sitemap.dart new file mode 100644 index 0000000..33ccd94 --- /dev/null +++ b/web/dart/parse_sitemap.dart @@ -0,0 +1,43 @@ +import 'dart:html' show HttpRequest; + +import 'package:html/parser.dart' show parse; +import 'package:html/dom.dart' show Element; + +// Get the sitemap content +Future getSitemap() async { + const path = 'sitemap.html'; + try { + return await HttpRequest.getString(path); + } catch (e) { + print('Couldn’t open $path'); + } + return 'Error'; +} + +// Parse the list of elements and detect pages from this list +Map detectPages(List sitemap, [String prefix]) { + final links = {}; + for (var elem in sitemap) { + if (elem.outerHtml.contains('index')) { + continue; + } else if (elem.innerHtml.startsWith('> parseSitemap() async { + final content = await getSitemap(); + final sitemap = parse(content).getElementsByClassName('org-ul')[0].children; + return detectPages(sitemap); +} diff --git a/web/dart/reorganize_html.dart b/web/dart/reorganize_html.dart new file mode 100644 index 0000000..a16988a --- /dev/null +++ b/web/dart/reorganize_html.dart @@ -0,0 +1,71 @@ +import 'dart:html'; + +import './navbar.dart' show makeNavbar; + +Future makeHeader() async { + var header = Element.tag('header'); + header + ..append(Element.img() + ..attributes['src'] = + 'https://cdn.phundrak.com/img/mahakala-monochrome.png' + ..attributes['alt'] = 'Logo de Phundrak' + ..attributes['heigh'] = '150px' + ..attributes['width'] = '150px') + ..append(querySelector('h1')); + var subt = querySelector('.subtitle'); + if (subt != null) { + header.append(subt); + } + return header; +} + +Future wrapTables() async { + for (var table in querySelectorAll('table')) { + var largetable = DivElement()..className = 'largetable'; + table.before(largetable); + largetable.children.add(table); + } +} + +// All images that are not nested inside a link will be linkified to themselves. +void linkifyImg() { + querySelectorAll('img').forEach((img) { + print(img.attributes['src']); + print(img.parent.tagName); + if (img.parent.tagName == 'P') { + final link = Element.a()..attributes['href'] = img.attributes['src']; + img.insertAdjacentElement('beforeBegin', link); + link.append(img); + } + }); +} + +Future reorganizeHtml() async { + final content = querySelector('#content'); + + // Make navbar + await makeNavbar().then((navbar) { + querySelector('body').insertAdjacentElement('afterBegin', navbar); + }); + + // Make header + await makeHeader().then((header) { + content.insertAdjacentElement('beforeBegin', header); + final subtitle = querySelector('.subtitle'); + if (subtitle != null) { + header.append(subtitle); + } + }); + + // wrap tables in container for better SCSS display + await wrapTables(); + + linkifyImg(); + + // Add correct class to TOC + querySelector('#toc-drop') + .append(querySelector('#table-of-contents')..classes.add('dropdown')); + + // Remove all
tags from HTML + querySelectorAll('br').forEach((br) => br.remove()); +} diff --git a/web/dart/theme.dart b/web/dart/theme.dart new file mode 100644 index 0000000..823a3f9 --- /dev/null +++ b/web/dart/theme.dart @@ -0,0 +1,53 @@ +import 'dart:html'; + +class Theme { + String _name; + String _icon; + + Theme(String t_name, String t_icon) { + _name = t_name; + _icon = t_icon; + } + + String getIcon() => _icon; + String getName() => _name; +} + +final themes = { + 'light': Theme('light', 'fa-sun'), + 'dark': Theme('dark', 'fa-lightbulb'), + 'black': Theme('black', 'fa-moon') +}; + +final localStorage = window.localStorage; + +var currentTheme = themes[localStorage['theme']]; + +void enableThemeChanger() { + final darkBtn = querySelector('#darkBtn'); + final lightBtn = querySelector('#lightBtn'); + final blackBtn = querySelector('#blackBtn'); + + lightBtn.onClick.listen((_) => switchTheme(themes['light'])); + darkBtn.onClick.listen((_) => switchTheme(themes['dark'])); + blackBtn.onClick.listen((_) => switchTheme(themes['black'])); +} + +Future setTheme() async { + if (currentTheme == null) { + currentTheme = themes['light']; + localStorage['theme'] = currentTheme.getName(); + } + querySelector('body') + ..classes.clear() + ..classes.add(currentTheme.getName()); +} + +void switchTheme(Theme currentTheme) { + // Set HTML theme + querySelector('body') + ..classes.clear() + ..classes.add(currentTheme.getName()); + // Set storage theme + localStorage['theme'] = currentTheme.getName(); +} diff --git a/web/style/style.scss b/web/style/style.scss new file mode 100644 index 0000000..e4181ec --- /dev/null +++ b/web/style/style.scss @@ -0,0 +1,591 @@ +@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic|Roboto+Slab:400,700|Inconsolata:400,700); +@font-face { + font-family: "DoulosSIL"; + font-display: swap; + src: url("/fonts/DoulosSIL-R.woff"); +} +@font-face { + font-family: "Noto Sans Runes"; + font-display: swap; + src: url("../fonts/NotoSansRunic-Regular.ttf"); +} +@font-face { + font-family: "Helvetica Neue"; + font-display: swap; + src: url("../fonts/HelveticaNeue.ttf"); +} + +/* Variables *****************************************************************/ + +$switch-small-screen: "only screen and (max-width: 600px)"; +$switch-smaller-screen: "only screen and (max-width: 400px)"; + +$navbar-height: 70px; +$postamble-height: 55px; + +// Themes ///////////////////////////////////////////////////////////////////// + +$dark: rgba( 52, 73, 94, 1); +$black: rgba( 0, 0, 0, 1); +$accent1: rgba( 93, 115, 126, 1); +$accent2: rgba( 92, 172, 126, 1); +$accent3: rgba(197, 193, 155, 1); +$light: #eee; +$grey1: #f8f8f8; +$grey2: #dbe1e8; +$grey3: #b2becd; +$grey4: #6c7983; +$grey5: #454e56; +$grey6: #12181b; + +// Accent 1 +// Black +$gradient-accent1-black-left: linear-gradient(to left, $black, $accent1, $accent1); +$gradient-accent1-black-right: linear-gradient(to right, $black, $accent1, $accent1); +// Dark +$gradient-accent1-dark-left: linear-gradient(to left, $dark, $accent1); +$gradient-accent1-dark-right: linear-gradient(to right, $dark, $accent1); +// Light +$gradient-accent1-light-left: linear-gradient(to left, $light, $accent1); +$gradient-accent1-light-right: linear-gradient(to right, $light, $accent1); +// Accent 2 +// Black +$gradient-accent2-black-left: linear-gradient(to left, $black, $accent2, $accent2); +$gradient-accent2-black-right: linear-gradient(to right, $black, $accent2, $accent2); +// Dark +$gradient-accent2-dark-left: linear-gradient(to left, $dark, $accent2); +$gradient-accent2-dark-right: linear-gradient(to right, $dark, $accent2); +// Light +$gradient-accent2-light-left: linear-gradient(to left, $light, $accent2); +$gradient-accent2-light-right: linear-gradient(to right, $light, $accent2); +// Accent 3 +// Black +$gradient-accent3-black-left: linear-gradient(to left, $black, $accent3, $accent3); +$gradient-accent3-black-right: linear-gradient(to right, $black, $accent3, $accent3); +// Dark +$gradient-accent3-dark-left: linear-gradient(to left, $dark, $accent3); +$gradient-accent3-dark-right: linear-gradient(to right, $dark, $accent3); +// Light +$gradient-accent3-light-left: linear-gradient(to left, $light, $accent3); +$gradient-accent3-light-right: linear-gradient(to right, $light, $accent3); + +.light { + $bg-nav: $gradient-accent3-light-right; + $border-color: $accent1; + + color: $dark; + background: $light; + + transition: background 500ms ease-in-out, color 1s ease-in-out; + + pre { + box-shadow: 3px 3px $dark; + border-color: $light; + } + + pre.src { + &::before { + background-color: $light; + color: $dark; + } + } + + .navbar, header { + background: $bg-nav; + } + + .status { + background: $gradient-accent3-light-left; + color: $dark; + } + + .tooltip { + border-bottom: 1px dotted $accent3; + + .tooltiptext { + background-color: $accent3; + color: $dark; + + &::after { + border-color: $accent3 transparent transparent transparent; + } + } + } + + .dropdown { + background: $accent3; + color: $dark; + } + + #content { + a { + box-shadow: inset 0 -3px 0 $accent3; + transition: box-shadow 300ms ease-in-out; + + &:hover { + box-shadow: inset 0 -23px 0 $accent3; + transition: box-shadow 300ms ease-in-out; + } + } + } + + table, th, td { + border: 1px solid $dark; + } + + th { + background: darken($light, 5%); + } + + .gentree { + filter: invert(0%); + transition: filter 1s ease-in-out; + } +} + +.dark, .black { + $bg-nav: $gradient-accent2-dark-right; + $border-color: $dark; + + color: $light; + background: $dark; + + transition: background 500ms ease-in-out, color 1s ease-in-out; + + pre { + box-shadow: 3px 3px $dark; + border: none; + } + + pre.src { + &::before { + background-color: $dark; + color: $light; + } + } + + .navbar, header { + background: $bg-nav; + } + + .status { + background: $gradient-accent2-dark-left; + color: $light; + } + + .tooltip { + border-bottom: 1px dotted $accent1; + + .tooltiptext { + background-color: $accent1; + color: $light; + + &::after { + border-color: $accent1 transparent transparent transparent; + } + } + } + + .dropdown { + background: $accent3; + color: $dark; + } + + #content { + a { + box-shadow: inset 0 -3px 0 $accent2; + transition: box-shadow 300ms ease-in-out; + + &:hover { + box-shadow: inset 0 -23px 0 $accent2; + transition: box-shadow 300ms ease-in-out; + } + } + } + + table, th, td { + border: 1px solid $accent1; + } + + th { + background: darken($dark, 2.5%); + } + + .gentree { + filter: invert(100%); + transition: filter 1s ease-in-out; + } +} + +.black { + $bg-nav: $gradient-accent1-black-right; + + background: $black; + + pre { + box-shadow: 3px 3px $light; + border: none; + } + + pre.src { + &::before { + background-color: $black; + color: $light; + } + } + + .navbar, header { + background: $bg-nav; + } + + .status { + background: $gradient-accent1-black-left; + } + + .dropdown { + background: $dark; + color: $light; + } + + #content { + a { + box-shadow: inset 0 -3px 0 $accent1; + transition: box-shadow 300ms ease-in-out; + + &:hover { + box-shadow: inset 0 -23px 0 $accent1; + transition: box-shadow 300ms ease-in-out; + } + } + } + + table, th, td { + border: 1px solid $light; + } + + th { + background: lighten($black, 15%); + } +} + +/* Style *********************************************************************/ + +* { + outline: none; +} + +body { + margin: 0; + padding: 0; + font-family: "Noto Sans Runes", "DoulosSIL", "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + font-size: 1.2em; + + transition: background 500ms ease-in-out, color 1s ease-in-out; + + header, .navbar { + transition: background 500ms ease-in-out, color 1s ease-in-out; + ul { + list-style-type: none; + margin: 0; + padding: 0; + } + } +} + +a { + color: currentColor; + text-decoration: none; +} + +.navbar { + position: fixed; + top: 0; + z-index: 4; + height: $navbar-height; + width: 100%; +} + +.navbar-nav { + display: flex; + align-items: center; + justify-content: space-evenly; + height: 100%; +} + +header { + padding: 1em; + margin-top: $navbar-height; + margin-bottom: 1em; + padding-bottom: 6em; + text-align: center; + clip-path: polygon(50% 0%, 100% 0, 100% 80%, 50% 100%, 0 80%, 0 0); + transition: background 500ms ease-in-out; + + .title { + font-size: 5em; + margin: 0; + @media #{$switch-small-screen} { + font-size: 3em; + } + } + + .subtitle { + font-size: 1.2em; + } +} + +.dropdown { + position: absolute; + opacity: 0; + z-index: -1; + + border-bottom-right-radius: 8px; + border-bottom-left-radius: 8px; + + display: flex; + align-items: center; + justify-content: space-around; + min-height: 3rem; + margin-top: 1rem; + padding: 0.5rem; + top: 0; + + box-shadow: rgba(2, 8, 20, 0.1) 0px 0.175em 0.5em; + transform: translateX(-40%); + + transition: opacity 500ms ease-in-out, top 500ms ease-in-out; +} + +.has-dropdown { + &:focus-within { + .dropdown { + opacity: 1; + top: $navbar-height; + z-index: 5; + pointer-events: auto; + } + + #table-of-contents { + top: $navbar-height / 1.3; + opacity: 1; + z-index: 5; + height: 500%; + pointer-events: auto; + } + } +} + +#theme-dropdown { + width: 250px; + flex-direction: row; + transform: translateX(-75%); +} + +#drop-page { + flex-direction: column; + transform: translateX(-40%); + li { + padding: 5px; + } +} + +#drop-share { + li { + padding: 10px; + } +} + +.dropdown-item { + cursor: pointer; + + a { + width: 100%; + height: 100%; + size: 0.7rem; + padding-left: 10px; + padding-right: 10px; + font-weight: bold; + } +} + +#table-of-contents { + flex-direction: column; + padding: 20px; + float: right; + overflow-y: auto; + height: 0%; + width: 75%; + min-width: 350px; + transform: translateX(-45%); + font-size: 0.9em; + top: -40px; + + transition: height 500ms ease-in-out, opacity 500ms ease-in-out, top 500ms ease-in-out; + + li { + padding: 5px 0; + } + + h2 { + font-size: 1.2em; + } + + #text-table-of-contents { + height: 100%; + } +} + +#content { + padding: 50px; + padding-top: 0; + max-width: 1000px; + margin: 0 auto; + text-align: justify; + + a { + font-style: italic; + } +} + +#postamble { + display: grid; + grid-template-areas: 'author email date'; + + @media #{$switch-small-screen} { + grid-template-areas: 'author date' 'email email'; + } + + @media #{$switch-smaller-screen} { + grid-template-areas: 'author' 'date' 'email'; + } + + font-size: 0.8em; + align-content: space-evenly; + text-align: center; + + p { + margin: 10px 0; + } + + .author { + grid-area: author; + } + + .email { + grid-area: email; + } + + .date { + grid-area: date; + } +} + +h1, h2, h3, h4, h5, h6 { + text-align: center; +} + +h2 { + font-size: 2.5em; + @media #{$switch-small-screen} { + font-size: 1.75em; + } +} + +h3 { + font-size: 2em; + + @media #{$switch-small-screen} { + font-size: 1.5em; + } +} + +h4 { + font-size: 1.5em; + + @media #{$switch-small-screen} { + font-size: 1.3em; + } +} + +h5 { + font-size: 1.25em; + + @media #{$switch-small-screen} { + font-size: 1.2em; + } +} + +.tooltip { + position: relative; + display: inline-block; + + .tooltiptext { + visibility: hidden; + + margin-left: -60px; /* Half the width */ + bottom: 100%; + left: 10%; + padding: 5px 10px; + text-align: center; + border-radius: 6px; + + position: absolute; + z-index: 5; + + + &::after { + content: " "; + position: absolute; + top: 100%; /* At the bottom of the tooltip */ + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + } + } + + &:hover { + .tooltiptext { + visibility: visible; + } + } +} + +blockquote { + padding: 0; + margin: 20px; + font-style: italic; +} + +.largetable { + overflow-y: auto; + margin-top:20px; + margin-bottom: 20px; +} + +table { + margin: 0 auto; +} + +th, td { + white-space: nowrap; + padding: 10px; +} + +img { + max-height: 600px; + max-width: 100%; +} + +.figure { + padding: 0; +} + +ul { + padding-inline-start: 20px; +} + +pre.src { + overflow-y: auto; + +}