Initial commit, added all backend files

This commit is contained in:
Lucien Cartier-Tilet 2020-05-05 14:49:26 +02:00
commit 8b85ea5952
Signed by: phundrak
GPG Key ID: BD7789E705CB8DCA
13 changed files with 1124 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.css
*.map

19
Dockerfile Normal file
View File

@ -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"]

144
README.org Normal file
View File

@ -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 websites 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 users preferences are kept on their browsers 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 its
really good at its job. I especially enjoy being able to nest blocks within
one another, theres 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 dont
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 youre 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?
Lets 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: <link rel="stylesheet" href="https://org.example.com/style/style.css"/>
,#+HTML_HEAD_EXTRA: <script defer src="https://org.example.com/dart/main.dart.js"></script>
,#+HTML_HEAD_EXTRA: <script src="https://kit.fontawesome.com/yourtokenhere.js" crossorigin="anonymous"></script>
#+END_SRC
You will need to obtain a (free) license on Fontawesome to use fontawesomes
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.

14
analysis_options.yaml Normal file
View File

@ -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/**

10
docker-compose.yml Normal file
View File

@ -0,0 +1,10 @@
version: '3'
services:
web:
build: .
ports:
- 8010:8080
restart: always
volumes:
- ./web/:/app/web/

16
pubspec.yaml Normal file
View File

@ -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 <lucien@phundrak.com>
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

3
start.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
sass --watch web/style/:web/style -tcompressed &
webdev serve --release --hostname 0.0.0.0

9
web/dart/main.dart Normal file
View File

@ -0,0 +1,9 @@
import './reorganize_html.dart' show reorganizeHtml;
import './theme.dart' show enableThemeChanger, setTheme;
Future<void> main() async {
await setTheme();
await reorganizeHtml().then((_) {
enableThemeChanger();
});
}

149
web/dart/navbar.dart Normal file
View File

@ -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<String> classes, [String id]) {
final icon = Element.tag('i')..classes.addAll(classes);
if (id != null) {
icon.attributes['id'] = id;
}
return icon;
}
Future<Element> 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<Element> 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<Element> 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<Element> 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<Element> 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<Element> 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;
}

View File

@ -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<String> getSitemap() async {
const path = 'sitemap.html';
try {
return await HttpRequest.getString(path);
} catch (e) {
print('Couldnt open $path');
}
return 'Error';
}
// Parse the list of elements and detect pages from this list
Map<String, String> detectPages(List<Element> sitemap, [String prefix]) {
final links = <String, String>{};
for (var elem in sitemap) {
if (elem.outerHtml.contains('index')) {
continue;
} else if (elem.innerHtml.startsWith('<a')) {
elem = elem.firstChild;
final url = elem.attributes['href'];
final text = elem.firstChild.text;
links[url] = (prefix == null) ? text : '$text ($prefix)';
} else {
final prefix = elem.firstChild.text;
final ul = elem.children[0].children;
links.addAll(detectPages(ul, prefix));
}
}
return links;
}
// This function returns a Map which contains all links to languages detected
// from the sitemap.
Future<Map<String, String>> parseSitemap() async {
final content = await getSitemap();
final sitemap = parse(content).getElementsByClassName('org-ul')[0].children;
return detectPages(sitemap);
}

View File

@ -0,0 +1,71 @@
import 'dart:html';
import './navbar.dart' show makeNavbar;
Future<Element> 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<void> 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<void> 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 <br> tags from HTML
querySelectorAll('br').forEach((br) => br.remove());
}

53
web/dart/theme.dart Normal file
View File

@ -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<void> 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();
}

591
web/style/style.scss Normal file
View File

@ -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;
}