Compare commits
28 Commits
Author | SHA1 | Date |
---|---|---|
Florine W. Dekker | 164a76d9fd | |
Florine W. Dekker | a6faa1ebe8 | |
Florine W. Dekker | a1d3384e32 | |
Florine W. Dekker | eb12f0473a | |
Florine W. Dekker | 09cacd241f | |
Florine W. Dekker | c495714867 | |
Florine W. Dekker | 080c1f852c | |
Florine W. Dekker | 7ecd9ec165 | |
Florine W. Dekker | e5e6a0d347 | |
Florine W. Dekker | cba23b4911 | |
Florine W. Dekker | 46da788e86 | |
Florine W. Dekker | 0ec60b3480 | |
Florine W. Dekker | 7648e49999 | |
Florine W. Dekker | 201efeca8c | |
Florine W. Dekker | c8c99373a3 | |
Florine W. Dekker | 9220ac6224 | |
Florine W. Dekker | 6c8215bc0c | |
Florine W. Dekker | 0be8fc9b97 | |
Florine W. Dekker | c71fab3342 | |
Florine W. Dekker | a208f13f4e | |
Florine W. Dekker | 2e33b8d762 | |
Florine W. Dekker | eb83cfa97c | |
Florine W. Dekker | dc50ad5859 | |
Florine W. Dekker | 570e9c5764 | |
Florine W. Dekker | 4d0f1dc5b4 | |
Florine W. Dekker | df1198b361 | |
Florine W. Dekker | 4aa27c567a | |
Florine W. Dekker | 2d46bf1969 |
|
@ -12,6 +12,7 @@ All JavaScript functionalities are exposed using the `window.fwdekker` object.
|
||||||
Some functionalities are automatically executed after configuring some `<meta>` tags in the HTML.
|
Some functionalities are automatically executed after configuring some `<meta>` tags in the HTML.
|
||||||
For example, set `<meta name="fwd:nav:target" content="#nav" />` to automatically put the navigation bar in the `#nav`
|
For example, set `<meta name="fwd:nav:target" content="#nav" />` to automatically put the navigation bar in the `#nav`
|
||||||
element.
|
element.
|
||||||
|
All meta-tag behaviour is opt-in.
|
||||||
|
|
||||||
Read the files' individual documentation for more information.
|
Read the files' individual documentation for more information.
|
||||||
|
|
||||||
|
|
Binary file not shown.
18
package.json
18
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@fwdekker/template",
|
"name": "@fwdekker/template",
|
||||||
"version": "3.3.0",
|
"version": "3.6.5",
|
||||||
"description": "The base template for pages on fwdekker.com.",
|
"description": "The base template for pages on fwdekker.com.",
|
||||||
"author": "Florine W. Dekker",
|
"author": "Florine W. Dekker",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -24,20 +24,20 @@
|
||||||
"deploy": "grunt deploy"
|
"deploy": "grunt deploy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@picocss/pico": "^1.5.6"
|
"@picocss/pico": "^1.5.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"grunt": "^1.5.3",
|
"grunt": "^1.6.1",
|
||||||
"grunt-cli": "^1.4.3",
|
"grunt-cli": "^1.4.3",
|
||||||
"grunt-contrib-clean": "^2.0.1",
|
"grunt-contrib-clean": "^2.0.1",
|
||||||
"grunt-contrib-cssmin": "^4.0.0",
|
"grunt-contrib-cssmin": "^5.0.0",
|
||||||
"grunt-contrib-watch": "^1.1.0",
|
"grunt-contrib-watch": "^1.1.0",
|
||||||
"grunt-focus": "^1.0.0",
|
"grunt-focus": "^1.0.0",
|
||||||
"grunt-webpack": "^5.0.0",
|
"grunt-webpack": "^6.0.0",
|
||||||
"ts-loader": "^9.4.1",
|
"ts-loader": "^9.5.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.9.3",
|
"typescript": "^5.3.2",
|
||||||
"webpack": "^5.75.0",
|
"webpack": "^5.89.0",
|
||||||
"webpack-cli": "^5.0.0"
|
"webpack-cli": "^5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,5 +4,5 @@
|
||||||
@import "snippets/colors.css";
|
@import "snippets/colors.css";
|
||||||
|
|
||||||
@import "snippets/common.css";
|
@import "snippets/common.css";
|
||||||
|
@import "snippets/forms.css";
|
||||||
@import "snippets/nav.css";
|
@import "snippets/nav.css";
|
||||||
@import "snippets/validation.css";
|
|
||||||
|
|
|
@ -1,11 +1,39 @@
|
||||||
/* pico.css overrides, based on https://picocss.com/docs/customization.html */
|
/* pico.css overrides, based on https://picocss.com/docs/customization.html */
|
||||||
:root {
|
|
||||||
|
/* Light (default) */
|
||||||
|
.fwd-nav,
|
||||||
|
[data-theme="light"],
|
||||||
|
:root:not([data-theme="dark"]) {
|
||||||
--primary: rgb(0, 51, 204) !important;
|
--primary: rgb(0, 51, 204) !important;
|
||||||
--primary-hover: rgb(0, 61, 245) !important;
|
--primary-hover: rgb(0, 61, 245) !important;
|
||||||
--primary-focus: rgba(0, 41, 163, 0.125) !important;
|
--primary-focus: rgba(0, 41, 163, 0.125) !important;
|
||||||
--primary-focus-opaque: rgb(0, 41, 163) !important;
|
--primary-focus-opaque: rgb(0, 41, 163) !important;
|
||||||
|
--primary-focus-dark: rgb(0, 29, 114) !important;
|
||||||
--primary-inverse: white !important;
|
--primary-inverse: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark (auto) */
|
||||||
|
@media only screen and (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]):not(.fwd-nav) {
|
||||||
|
--primary: #1e88e5 !important;
|
||||||
|
--primary-hover: #2196f3 !important;
|
||||||
|
--primary-focus: rgba(30, 136, 229, 0.25) !important;
|
||||||
|
--primary-focus-opaque: rgb(30, 136, 229) !important;
|
||||||
|
--primary-inverse: white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark (forced) */
|
||||||
|
:root[data-theme="dark"]:not(.fwd-nav) {
|
||||||
|
--primary: #1e88e5 !important;
|
||||||
|
--primary-hover: #2196f3 !important;
|
||||||
|
--primary-focus: rgba(30, 136, 229, 0.25) !important;
|
||||||
|
--primary-focus-opaque: rgb(30, 136, 229) !important;
|
||||||
|
--primary-inverse: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Common */
|
||||||
|
:root {
|
||||||
--form-element-active-border-color: var(--primary) !important;
|
--form-element-active-border-color: var(--primary) !important;
|
||||||
--form-element-focus-color: var(--primary-focus) !important;
|
--form-element-focus-color: var(--primary-focus) !important;
|
||||||
--switch-color: var(--primary-inverse) !important;
|
--switch-color: var(--primary-inverse) !important;
|
||||||
|
|
|
@ -38,41 +38,30 @@ a[target="_blank"]::after {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Container with aside, e.g. for table of contents */
|
/* Container with sidebar, e.g. for table of contents */
|
||||||
@media (min-width: 576px) {
|
@media (min-width: 992px) {
|
||||||
:root {
|
:root {
|
||||||
--aside-width: 200px;
|
--aside-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-with-toc,
|
.grid-with-sidebar {
|
||||||
.container-with-aside-left,
|
|
||||||
.container-with-aside-right {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-column-gap: calc(var(--block-spacing-horizontal) * 3);
|
grid-column-gap: calc(var(--block-spacing-horizontal) * 3);
|
||||||
}
|
|
||||||
|
|
||||||
.container-with-toc,
|
|
||||||
.container-with-aside-left {
|
|
||||||
grid-template-columns: var(--aside-width) auto;
|
grid-template-columns: var(--aside-width) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-with-aside-right {
|
.grid-with-sidebar aside {
|
||||||
grid-template-columns: auto var(--aside-width);
|
max-width: var(--aside-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-with-toc nav,
|
.grid-with-sidebar aside .sticky {
|
||||||
.container-with-aside-left nav,
|
position: sticky;
|
||||||
.container-with-aside-right nav
|
top: var(--block-spacing-vertical);
|
||||||
{
|
|
||||||
max-width: var(--aside-width);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 992px) {
|
||||||
.container-with-toc aside,
|
.grid-with-sidebar aside {
|
||||||
.container-with-aside-left aside,
|
|
||||||
.container-with-aside-right aside
|
|
||||||
{
|
|
||||||
margin-bottom: var(--block-spacing-vertical);
|
margin-bottom: var(--block-spacing-vertical);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +80,7 @@ noscript.fwd-js-notice p {
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
header.fwd-header a[href="."] {
|
header.fwd-header a[href="."] {
|
||||||
color: black;
|
color: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
|
/* Status card */
|
||||||
.status-card {
|
.status-card {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card output {
|
||||||
|
display: block;
|
||||||
|
margin-right: var(--block-spacing-horizontal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-card .close {
|
.status-card .close {
|
||||||
float: right;
|
position: absolute;
|
||||||
display: block;
|
right: var(--block-spacing-horizontal);
|
||||||
|
top: calc(var(--block-spacing-vertical) / 2);
|
||||||
|
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
margin: 0 0 calc(var(--block-spacing-vertical) / 2) calc(var(--block-spacing-vertical) / 2);
|
|
||||||
|
|
||||||
background-image: var(--icon-close);
|
background-image: var(--icon-close);
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
@ -46,6 +53,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Input validation */
|
||||||
label.invalid,
|
label.invalid,
|
||||||
*[data-label-for].invalid,
|
*[data-label-for].invalid,
|
||||||
input.invalid,
|
input.invalid,
|
||||||
|
@ -59,3 +67,21 @@ input.valid,
|
||||||
*[data-hint-for].valid {
|
*[data-hint-for].valid {
|
||||||
color: var(--form-element-valid-border-color) !important;
|
color: var(--form-element-valid-border-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Enable hint-like styling on any element */
|
||||||
|
.input-hint {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
/*noinspection CssUnresolvedCustomProperty*/
|
||||||
|
margin-top: calc(var(--spacing) * -.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Custom components */
|
||||||
|
.inline-button {
|
||||||
|
display: inline-block;
|
||||||
|
width: unset;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.3em;
|
||||||
|
}
|
|
@ -1,140 +1,138 @@
|
||||||
/* Base elements */
|
/* Base elements */
|
||||||
nav.fwd-nav {
|
nav.fwd-nav {
|
||||||
display: block;
|
--fwd-nav-box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.75);
|
||||||
z-index: 10;
|
box-shadow: var(--fwd-nav-box-shadow);
|
||||||
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
background-color: var(--primary);
|
|
||||||
border-bottom: 1px solid var(--nav-border-color);
|
|
||||||
|
|
||||||
--padding: calc(2em / 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.fwd-nav * {
|
nav.fwd-nav > ul {
|
||||||
vertical-align: middle;
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.fwd-nav a,
|
nav.fwd-nav > ul > li {
|
||||||
nav.fwd-nav a:link,
|
flex-grow: 0;
|
||||||
nav.fwd-nav a:visited,
|
flex-basis: 0;
|
||||||
nav.fwd-nav a:hover,
|
|
||||||
nav.fwd-nav a:active {
|
|
||||||
/* Ensures whole li is clickable */
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.fwd-nav a,
|
nav.fwd-nav li {
|
||||||
nav.fwd-nav a:link,
|
|
||||||
nav.fwd-nav a:visited,
|
|
||||||
nav.fwd-nav a:hover,
|
|
||||||
nav.fwd-nav a:active,
|
|
||||||
nav.fwd-nav #nav-hamburger-label {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0;
|
|
||||||
padding: calc(var(--padding)) calc(var(--padding));
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
color: var(--primary-inverse);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.fwd-nav #nav-hamburger-label {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.fwd-nav a[target="_blank"]::after {
|
|
||||||
margin-bottom: 0.2rem;
|
|
||||||
background-color: var(--primary-inverse);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Logo */
|
|
||||||
nav.fwd-nav .logo {
|
|
||||||
width: calc(1em + var(--padding));
|
|
||||||
height: calc(1em + var(--padding));
|
|
||||||
|
|
||||||
vertical-align: middle;
|
|
||||||
filter: brightness(0) invert(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.fwd-nav div.logo {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: calc(1em / 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* First level nesting */
|
|
||||||
nav.fwd-nav ul {
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.fwd-nav ul li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: var(--primary);
|
width: 100%;
|
||||||
|
padding: var(--nav-element-spacing-horizontal);
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.fwd-nav ul li:hover,
|
nav.fwd-nav li > :first-child {
|
||||||
nav.fwd-nav ul li:focus-within,
|
display: inline-block;
|
||||||
nav.fwd-nav #nav-hamburger-label:hover,
|
width: 100%;
|
||||||
nav.fwd-nav #nav-hamburger-label:focus-within {
|
margin: 0;
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--primary-hover);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.fwd-nav li.currentPage {
|
nav.fwd-nav ul ul {
|
||||||
background-color: var(--primary-focus-opaque);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Second level nesting */
|
|
||||||
nav.fwd-nav ul li ul {
|
|
||||||
z-index: 11;
|
|
||||||
|
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
box-shadow: var(--fwd-nav-box-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.fwd-nav ul li ul li ul {
|
nav.fwd-nav li:where(:active, :focus-within, :hover, .fwd-nav-active) > ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.fwd-nav ul ul ul {
|
||||||
left: 100%;
|
left: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.fwd-nav ul li:hover > ul,
|
/* z-index */
|
||||||
nav.fwd-nav ul li:focus-within > ul,
|
nav.fwd-nav a {
|
||||||
nav.fwd-nav ul li ul:hover {
|
position: relative;
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.fwd-nav ul li ul li {
|
nav.fwd-nav > ul > li > ul {
|
||||||
min-width: 7em;
|
z-index: 10;
|
||||||
width: 100%;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Colors and optional styling */
|
||||||
|
nav.fwd-nav,
|
||||||
|
nav.fwd-nav ul {
|
||||||
|
background-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
/* Hide hamburger-related elements */
|
nav.fwd-nav li.fwd-nav-separator {
|
||||||
nav.fwd-nav input[type="checkbox"] {
|
border-top: 1px solid #ccc;
|
||||||
display: none;
|
}
|
||||||
|
|
||||||
|
nav.fwd-nav li:where(:active, :focus-within, :hover, .fwd-nav-active) {
|
||||||
|
background-color: var(--primary-focus-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.fwd-nav li.fwd-nav-highlighted:not(:where(:active, :focus-within, :hover, .fwd-nav-active)) {
|
||||||
|
background-color: var(--primary-focus-opaque);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.fwd-nav a {
|
||||||
|
color: var(--primary-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.fwd-nav a::after {
|
||||||
|
background-color: var(--primary-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo */
|
||||||
|
nav.fwd-nav #logo {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.fwd-nav #logo::before {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
width: calc(1em * var(--line-height));
|
||||||
|
height: calc(1em * var(--line-height));
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
--mask-image: url("https://fwdekker.com/favicon.png");
|
||||||
|
-webkit-mask-image: var(--mask-image);
|
||||||
|
-webkit-mask-size: cover;
|
||||||
|
mask-image: var(--mask-image);
|
||||||
|
mask-size: cover;
|
||||||
|
background-repeat: no-repeat no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
background-size: cover;
|
||||||
|
background-color: var(--primary-inverse);
|
||||||
|
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hamburger */
|
||||||
|
nav.fwd-nav #fwd-nav-hamburger-label {
|
||||||
|
height: fit-content;
|
||||||
|
margin: 0;
|
||||||
|
padding: calc(var(--nav-element-spacing-horizontal) + var(--nav-link-spacing-vertical));
|
||||||
|
|
||||||
|
color: var(--primary-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.fwd-nav #fwd-nav-hamburger-label:where(:active, :focus-within, :hover, .fwd-nav-active) {
|
||||||
|
background-color: var(--primary-focus-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 576px) {
|
@media (min-width: 576px) {
|
||||||
nav.fwd-nav #nav-hamburger-label {
|
nav.fwd-nav #fwd-nav-hamburger-label {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
nav.fwd-nav input[type="checkbox"]:not(:checked) ~ ul li:not(:first-child) {
|
nav.fwd-nav #fwd-nav-hamburger-checkbox:not(:checked) ~ ul li:not(:first-child) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,29 @@
|
||||||
|
/* pico.css: Improved text contrast (see also picocss/pico#276) */
|
||||||
|
:root {
|
||||||
|
--code-color: var(--color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] {
|
||||||
|
--muted-color: hsl(205deg, 15%, 41%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--muted-color: hsl(205deg, 12%, 59%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* pico.css: Bold <label> and <th>, except for checkbox/radio labels */
|
||||||
|
:root {
|
||||||
|
--form-label-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr th {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:where([type="checkbox"], [type="radio"]) + label {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
/* pico.css: Halve header margins */
|
/* pico.css: Halve header margins */
|
||||||
h1 {
|
h1 {
|
||||||
--typography-spacing-vertical: 1.5rem;
|
--typography-spacing-vertical: 1.5rem;
|
||||||
|
@ -43,8 +69,12 @@ article > header {
|
||||||
}
|
}
|
||||||
|
|
||||||
article > footer {
|
article > footer {
|
||||||
margin-bottom: calc(var(--block-spacing-vertical) / -2);
|
|
||||||
margin-top: calc(var(--block-spacing-vertical) / 2);
|
margin-top: calc(var(--block-spacing-vertical) / 2);
|
||||||
|
margin-bottom: calc(var(--block-spacing-vertical) / -2);
|
||||||
|
}
|
||||||
|
|
||||||
|
article > footer > form > article {
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
article > header > hgroup,
|
article > header > hgroup,
|
||||||
|
|
|
@ -20,7 +20,6 @@ export interface Storage {
|
||||||
*
|
*
|
||||||
* @param name the name of the array to store
|
* @param name the name of the array to store
|
||||||
* @param value the array to store under the given name
|
* @param value the array to store under the given name
|
||||||
* @protected
|
|
||||||
*/
|
*/
|
||||||
setArray(name: string, value: any[]): void;
|
setArray(name: string, value: any[]): void;
|
||||||
|
|
||||||
|
@ -29,7 +28,6 @@ export interface Storage {
|
||||||
*
|
*
|
||||||
* @param name the name of the boolean to retrieve
|
* @param name the name of the boolean to retrieve
|
||||||
* @param def the value to return if no boolean is stored with the given name
|
* @param def the value to return if no boolean is stored with the given name
|
||||||
* @protected
|
|
||||||
*/
|
*/
|
||||||
getBoolean(name: string, def: boolean): boolean;
|
getBoolean(name: string, def: boolean): boolean;
|
||||||
|
|
||||||
|
@ -38,7 +36,6 @@ export interface Storage {
|
||||||
*
|
*
|
||||||
* @param name the name of the boolean to store
|
* @param name the name of the boolean to store
|
||||||
* @param value the boolean to store under the given name
|
* @param value the boolean to store under the given name
|
||||||
* @protected
|
|
||||||
*/
|
*/
|
||||||
setBoolean(name: string, value: boolean): void;
|
setBoolean(name: string, value: boolean): void;
|
||||||
|
|
||||||
|
@ -47,7 +44,6 @@ export interface Storage {
|
||||||
*
|
*
|
||||||
* @param name the name of the number to retrieve
|
* @param name the name of the number to retrieve
|
||||||
* @param def the value to return if no number is stored with the given name
|
* @param def the value to return if no number is stored with the given name
|
||||||
* @protected
|
|
||||||
*/
|
*/
|
||||||
getNumber(name: string, def: number): number;
|
getNumber(name: string, def: number): number;
|
||||||
|
|
||||||
|
@ -56,9 +52,24 @@ export interface Storage {
|
||||||
*
|
*
|
||||||
* @param name the name of the number to store
|
* @param name the name of the number to store
|
||||||
* @param value the number to store under the given name
|
* @param value the number to store under the given name
|
||||||
* @protected
|
|
||||||
*/
|
*/
|
||||||
setNumber(name: string, value: number): void;
|
setNumber(name: string, value: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a string from storage.
|
||||||
|
*
|
||||||
|
* @param name the name of the string to retrieve
|
||||||
|
* @param def the value to return if no string is stored with the given name
|
||||||
|
*/
|
||||||
|
getString(name: string, def: string): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a string.
|
||||||
|
*
|
||||||
|
* @param name the name of the string to store
|
||||||
|
* @param value the number to store under the given name
|
||||||
|
*/
|
||||||
|
setString(name: string, value: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -115,28 +126,32 @@ export class LocalStorage implements Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
setArray(name: string, value: any[]): void {
|
setArray(name: string, value: any[]): void {
|
||||||
const item = this.read();
|
this.setString(name, JSON.stringify(value));
|
||||||
item[name] = JSON.stringify(value);
|
|
||||||
this.write(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getBoolean(name: string, def: boolean = false): boolean {
|
getBoolean(name: string, def: boolean = false): boolean {
|
||||||
return (this.read()[name] ?? `${def}`) === "true";
|
return this.getString(name, def ? "true" : "false") === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
setBoolean(name: string, value: boolean): void {
|
setBoolean(name: string, value: boolean): void {
|
||||||
const item = this.read();
|
this.setString(name, "" + value);
|
||||||
item[name] = "" + value;
|
|
||||||
this.write(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getNumber(name: string, def: number = 0): number {
|
getNumber(name: string, def: number = 0): number {
|
||||||
return +(this.read()[name] ?? def);
|
return +this.getString(name, "" + def);
|
||||||
}
|
}
|
||||||
|
|
||||||
setNumber(name: string, value: number): void {
|
setNumber(name: string, value: number): void {
|
||||||
|
this.setString(name, "" + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
getString(name: string, def: string = ""): string {
|
||||||
|
return this.read()[name] ?? def;
|
||||||
|
}
|
||||||
|
|
||||||
|
setString(name: string, value: string): void {
|
||||||
const item = this.read();
|
const item = this.read();
|
||||||
item[name] = "" + value;
|
item[name] = value;
|
||||||
this.write(item);
|
this.write(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -175,4 +190,12 @@ export class MemoryStorage implements Storage {
|
||||||
getNumber(name: string, def: number): number {
|
getNumber(name: string, def: number): number {
|
||||||
return this.storage[name] ?? def;
|
return this.storage[name] ?? def;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setString(name: string, value: string): void {
|
||||||
|
this.storage[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getString(name: string, def: string): string {
|
||||||
|
return this.storage[name] ?? def;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
* Converts the given string to an HTML element.
|
* Converts the given string to an HTML element.
|
||||||
*
|
*
|
||||||
* @param string the string to convert to an HTML element
|
* @param string the string to convert to an HTML element
|
||||||
* @param query the type of element to return
|
* @param query the type of element to return, or `undefined` to return the first element
|
||||||
* @returns the HTML element described by the given string, or `null` if `query` did not match any element
|
* @returns the HTML element described by the given string
|
||||||
*/
|
*/
|
||||||
export function stringToHtml(string: string, query: string): HTMLElement | null {
|
export function stringToHtml(string: string, query: string = "*"): HTMLElement {
|
||||||
return (new DOMParser()).parseFromString(string, "text/html").body.querySelector(query);
|
return (new DOMParser()).parseFromString(string, "text/html").body.querySelector(query)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -81,35 +81,36 @@ export function getMetaProperty(name: string): string | null | undefined {
|
||||||
* @returns a base navigation element that will eventually be filled with contents
|
* @returns a base navigation element that will eventually be filled with contents
|
||||||
*/
|
*/
|
||||||
function nav(highlightPath?: string, cb?: (json: any) => void): HTMLElement {
|
function nav(highlightPath?: string, cb?: (json: any) => void): HTMLElement {
|
||||||
const base = stringToHtml(
|
const nav = stringToHtml(`<nav class="fwd-nav"></nav>`);
|
||||||
`<ul><li><a href="https://fwdekker.com/">` +
|
|
||||||
`<div class="logo"><img class="logo" src="https://fwdekker.com/favicon.png" alt="FWDekker" /></div>` +
|
|
||||||
`<b>FWDekker</b>` +
|
|
||||||
`</a></li></ul>`,
|
|
||||||
"ul"
|
|
||||||
)!;
|
|
||||||
|
|
||||||
|
const checkbox = stringToHtml(`<input id="fwd-nav-hamburger-checkbox" type="checkbox" hidden />`);
|
||||||
|
nav.appendChild(checkbox);
|
||||||
|
|
||||||
|
const base = stringToHtml(
|
||||||
|
`<ul><li ${highlightPath === "/" ? `class="fwd-nav-highlighted" aria-current="page"` : ""}>` +
|
||||||
|
`<a id="logo" href="https://fwdekker.com/">FWDekker</a>` +
|
||||||
|
`</li></ul>`
|
||||||
|
);
|
||||||
fetch("https://fwdekker.com/api/nav/")
|
fetch("https://fwdekker.com/api/nav/")
|
||||||
.then(it => it.json())
|
.then(it => it.json())
|
||||||
.then(json => {
|
.then(json => {
|
||||||
if (cb !== undefined)
|
if (cb !== undefined)
|
||||||
cb(json);
|
cb(json);
|
||||||
|
|
||||||
json.entries.forEach(
|
json.entries.forEach((it: any) => base.appendChild(unpackEntry(it, "/", highlightPath)));
|
||||||
(entry: any) =>
|
|
||||||
base.appendChild(stringToHtml(unpackEntry(entry, "/", highlightPath), "li")!)
|
document.body.addEventListener(
|
||||||
|
"click",
|
||||||
|
() => $a("li", base).forEach(it => it.classList.remove("fwd-nav-active")),
|
||||||
|
{capture: true}
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(error => console.error("Failed to fetch navigation elements", error));
|
.catch(error => console.error("Failed to fetch navigation elements", error));
|
||||||
|
|
||||||
const nav = stringToHtml(
|
|
||||||
`<nav class="fwd-nav">` +
|
|
||||||
`<input id="nav-hamburger-checkbox" type="checkbox" hidden />` +
|
|
||||||
`<label id="nav-hamburger-label" for="nav-hamburger-checkbox">☰</label>` +
|
|
||||||
`</nav>`,
|
|
||||||
"nav"
|
|
||||||
)!;
|
|
||||||
nav.appendChild(base);
|
nav.appendChild(base);
|
||||||
|
|
||||||
|
const label = stringToHtml(`<label id="fwd-nav-hamburger-label" for="fwd-nav-hamburger-checkbox">☰</label>`);
|
||||||
|
nav.appendChild(label);
|
||||||
|
|
||||||
return nav;
|
return nav;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,29 +118,45 @@ function nav(highlightPath?: string, cb?: (json: any) => void): HTMLElement {
|
||||||
* Unpacks a navigation entry returned from the navigation API into an HTML element.
|
* Unpacks a navigation entry returned from the navigation API into an HTML element.
|
||||||
*
|
*
|
||||||
* @param entry the entry to unpack
|
* @param entry the entry to unpack
|
||||||
* @param path the current path traversed, found by joining the names of the entries with `/`s; always starts and ends
|
* @param parentPath the current path traversed, found by joining the names of the entries with `/`s; always starts and
|
||||||
* with a `/`
|
* ends with a `/`
|
||||||
* @param highlightPath the path to highlight together with its parents, or `undefined` if no path should be highlighted
|
* @param highlightPath the path to highlight together with its parents, or `undefined` if no path should be highlighted
|
||||||
* @returns the navigation list entry as HTML, described by its children
|
* @returns the navigation list entry
|
||||||
*/
|
*/
|
||||||
function unpackEntry(entry: any, path: string = "/", highlightPath?: string): string {
|
function unpackEntry(entry: any, parentPath: string = "/", highlightPath?: string): HTMLLIElement {
|
||||||
const shouldHighlight = highlightPath?.startsWith(`${path + entry.name}/`) ?? false;
|
const path = `${parentPath + entry.name}/`;
|
||||||
const isExternalLink = !(/^https:\/\/.*fwdekker.com/i.test(entry.link)) && entry.link !== "#";
|
const hasChildren = entry.entries.length !== 0;
|
||||||
|
|
||||||
if (entry.entries.length === 0)
|
const li = document.createElement("li");
|
||||||
return "" +
|
if (highlightPath === path) li.setAttribute("aria-current", "page");
|
||||||
`<li ${shouldHighlight ? "class=\"currentPage\"" : ""}>` +
|
if (highlightPath?.startsWith(path) ?? false) li.classList.add("fwd-nav-highlighted");
|
||||||
`<a href="${entry.link}" ${isExternalLink ? "target=\"_blank\"" : ""}>${entry.name}</a>` +
|
if (entry.border) li.classList.add("fwd-nav-separator");
|
||||||
`</li>`;
|
|
||||||
|
|
||||||
const depth = path.split("/").length - 2; // -1 because count parts, then another -1 because of leading `/`
|
const a = document.createElement("a");
|
||||||
const arrow = depth === 0 ? "▾" : "▸";
|
a.innerText = entry.name;
|
||||||
|
a.tabIndex = 0;
|
||||||
|
if (hasChildren) {
|
||||||
|
const depth = parentPath.split("/").length - 2; // -1 because count parts, then another -1 because of leading /
|
||||||
|
a.innerText += ` ${depth === 0 ? "▾" : "▸"}`;
|
||||||
|
}
|
||||||
|
if (entry.link != null) {
|
||||||
|
a.href = entry.link;
|
||||||
|
|
||||||
return "" +
|
if (entry.link !== "#" && !/^https:\/\/.*fwdekker.com/i.test(entry.link))
|
||||||
`<li class="${shouldHighlight ? "currentPage" : ""}">` +
|
a.target = "_blank";
|
||||||
`<a href="${entry.link}">${entry.name} ${arrow}</a>` +
|
}
|
||||||
`<ul>${entry.entries.map((it: any) => unpackEntry(it, `${path + entry.name}/`, highlightPath)).join("")}</ul>` +
|
li.addEventListener("click", () => li.classList.add("fwd-nav-active"));
|
||||||
`</li>`;
|
li.appendChild(a);
|
||||||
|
|
||||||
|
if (hasChildren) {
|
||||||
|
const ul = document.createElement("ul");
|
||||||
|
entry.entries
|
||||||
|
.map((it: any) => unpackEntry(it, path, highlightPath))
|
||||||
|
.forEach((it: HTMLLIElement) => ul.appendChild(it));
|
||||||
|
li.appendChild(ul);
|
||||||
|
}
|
||||||
|
|
||||||
|
return li;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -182,7 +199,7 @@ function footer(
|
||||||
if (author === undefined) author = "Florine W. Dekker";
|
if (author === undefined) author = "Florine W. Dekker";
|
||||||
if (authorURL === undefined) authorURL = "https://fwdekker.com/";
|
if (authorURL === undefined) authorURL = "https://fwdekker.com/";
|
||||||
if (license === undefined) license = "MIT";
|
if (license === undefined) license = "MIT";
|
||||||
if (licenseURL === undefined && vcsURL !== undefined) licenseURL = `${vcsURL}src/branch/master/LICENSE`;
|
if (licenseURL === undefined && vcsURL !== undefined) licenseURL = `${vcsURL}src/branch/main/LICENSE`;
|
||||||
if (vcs === undefined && vcsURL !== undefined) vcs = "git";
|
if (vcs === undefined && vcsURL !== undefined) vcs = "git";
|
||||||
if (privacyPolicyURL === undefined) privacyPolicyURL = "https://fwdekker.com/privacy/";
|
if (privacyPolicyURL === undefined) privacyPolicyURL = "https://fwdekker.com/privacy/";
|
||||||
|
|
||||||
|
@ -192,9 +209,8 @@ function footer(
|
||||||
footerLink("Licensed ", license, licenseURL, ". ") +
|
footerLink("Licensed ", license, licenseURL, ". ") +
|
||||||
footerLink("Source and support on ", vcs, vcsURL, ". ") +
|
footerLink("Source and support on ", vcs, vcsURL, ". ") +
|
||||||
footerLink("Read the ", privacyPolicyURL && "privacy policy", privacyPolicyURL, ". ") +
|
footerLink("Read the ", privacyPolicyURL && "privacy policy", privacyPolicyURL, ". ") +
|
||||||
`</small><div id="fwd-footer-version"><small>${version || ""}</small></div></footer>`,
|
`</small><div id="fwd-footer-version"><small>${version || ""}</small></div></footer>`
|
||||||
"footer"
|
);
|
||||||
)!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -27,21 +27,23 @@ export function showMessageType(card: HTMLElement | HTMLFormElement,
|
||||||
message?: string,
|
message?: string,
|
||||||
type?: "busy" | "error" | "info" | "success" | "warning"): void {
|
type?: "busy" | "error" | "info" | "success" | "warning"): void {
|
||||||
if (card instanceof HTMLFormElement) {
|
if (card instanceof HTMLFormElement) {
|
||||||
const formCard = $(`article[data-status-for="${card.id}"]`);
|
if (card.dataset.statusCard == null) return;
|
||||||
|
|
||||||
|
const formCard = $(`#${card.dataset.statusCard}`);
|
||||||
if (formCard == null) return;
|
if (formCard == null) return;
|
||||||
|
|
||||||
card = formCard;
|
card = formCard;
|
||||||
}
|
}
|
||||||
const output = $("output", card)!;
|
const output = $("output", card)!;
|
||||||
|
|
||||||
card.removeAttribute("aria-busy");
|
output.removeAttribute("aria-busy");
|
||||||
card.classList.remove("hidden", "error", "info", "success", "warning");
|
card.classList.remove("hidden", "error", "info", "success", "warning");
|
||||||
|
|
||||||
if (message == null || type == null) {
|
if (message == null || type == null) {
|
||||||
card.classList.add("hidden");
|
card.classList.add("hidden");
|
||||||
output.innerHTML = "";
|
output.innerHTML = "";
|
||||||
} else {
|
} else {
|
||||||
if (type === "busy") card.setAttribute("aria-busy", "true");
|
if (type === "busy") output.setAttribute("aria-busy", "true");
|
||||||
else card.classList.add(type);
|
else card.classList.add(type);
|
||||||
|
|
||||||
output.innerHTML = message;
|
output.innerHTML = message;
|
||||||
|
@ -200,7 +202,7 @@ doAfterLoad(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$a("input + small[data-hint]").forEach((hint: Element) => {
|
$a("small[data-hint]").forEach((hint: Element) => {
|
||||||
if (!(hint instanceof HTMLElement)) return;
|
if (!(hint instanceof HTMLElement)) return;
|
||||||
|
|
||||||
hint.innerHTML = hint.dataset["hint"] ?? "";
|
hint.innerHTML = hint.dataset["hint"] ?? "";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="light">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
<meta name="fwd:nav:target" content="#nav" />
|
<meta name="fwd:nav:target" content="#nav" />
|
||||||
<meta name="fwd:nav:highlight-path" content="/Tools/Dice/" />
|
<meta name="fwd:nav:highlight-path" content="/Tools/Dice/" />
|
||||||
<meta name="fwd:footer:target" content="#footer" />
|
<meta name="fwd:footer:target" content="#footer" />
|
||||||
<meta name="fwd:footer:vcs-url" content="https://git.fwdekker.com/FWDekker/fwdekker-template/" />
|
<meta name="fwd:footer:vcs-url" content="https://git.fwdekker.com/fwdekker.com/template/" />
|
||||||
<meta name="fwd:footer:version" content="vTEST" />
|
<meta name="fwd:footer:version" content="vTEST" />
|
||||||
<meta name="fwd:validation:load-forms" />
|
<meta name="fwd:validation:load-forms" />
|
||||||
|
|
||||||
|
@ -29,7 +29,10 @@
|
||||||
</p>
|
</p>
|
||||||
</noscript>
|
</noscript>
|
||||||
<nav id="nav"></nav>
|
<nav id="nav"></nav>
|
||||||
<main class="container container-with-aside-right">
|
<main class="container grid-with-sidebar">
|
||||||
|
<aside>
|
||||||
|
Table of contents test
|
||||||
|
</aside>
|
||||||
<div role="document">
|
<div role="document">
|
||||||
<section>
|
<section>
|
||||||
<header class="fwd-header">
|
<header class="fwd-header">
|
||||||
|
@ -48,6 +51,7 @@
|
||||||
<p>This <a href="./" target="_blank">is an external link</a> in a sentence.</p>
|
<p>This <a href="./" target="_blank">is an external link</a> in a sentence.</p>
|
||||||
<p>These are some more contents.</p>
|
<p>These are some more contents.</p>
|
||||||
<p>These are the page contents.</p>
|
<p>These are the page contents.</p>
|
||||||
|
<p><code>This is a code block</code></p>
|
||||||
|
|
||||||
|
|
||||||
<article>
|
<article>
|
||||||
|
@ -57,40 +61,26 @@
|
||||||
<h3>Already have an account? Welcome back!</h3>
|
<h3>Already have an account? Welcome back!</h3>
|
||||||
</hgroup>
|
</hgroup>
|
||||||
</header>
|
</header>
|
||||||
<form id="test-form" novalidate>
|
<form id="test-form" data-status-card="test-status-card" novalidate>
|
||||||
<article class="status-card hidden" data-status-for="test-form">
|
<article id="test-status-card" class="status-card hidden">
|
||||||
<output>Congrats!</output>
|
<output>Congrats!</output>
|
||||||
<a class="close" href="#" aria-label="Close"></a>
|
<a class="close" href="#" aria-label="Close"></a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<fieldset>
|
<label for="test-email">Email</label>
|
||||||
<label for="test-email">Email</label>
|
<input id="test-email" type="email" name="email" autocomplete="on" />
|
||||||
<input id="test-email" type="email" name="email" autocomplete="on" />
|
<small id="test-email-hint" data-hint-for="test-email" data-hint="Test hint"></small>
|
||||||
<small id="test-email-hint" data-hint-for="test-email" data-hint="Test hint"></small>
|
|
||||||
|
|
||||||
<label for="test-password">Password</label>
|
<label for="test-password">Password</label>
|
||||||
<input id="test-password" type="password" name="password" />
|
<input id="test-password" type="password" name="password" />
|
||||||
<small id="test-password-hint" data-hint-for="test-password"></small>
|
<small id="test-password-hint" data-hint-for="test-password"></small>
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset>
|
<button id="login-button">Log in</button>
|
||||||
<button id="login-button">Log in</button>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
<footer id="footer"></footer>
|
<footer id="footer"></footer>
|
||||||
</div>
|
</div>
|
||||||
<aside>
|
|
||||||
<nav>
|
|
||||||
<b class="hidden-no-mobile">Table of contents</b>
|
|
||||||
<ul>
|
|
||||||
<li>Item 1</li>
|
|
||||||
<li>Item 2</li>
|
|
||||||
</ul>
|
|
||||||
<hr class="hidden-no-mobile" />
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!--suppress HtmlUnknownTarget -->
|
<!--suppress HtmlUnknownTarget -->
|
||||||
|
|
Loading…
Reference in New Issue