Compare commits

...

21 Commits
v3.3.5 ... main

Author SHA1 Message Date
Florine W. Dekker 164a76d9fd
Change default license branch to main 2024-04-24 11:15:06 +02:00
Florine W. Dekker a6faa1ebe8
Bump dependencies 2023-12-01 13:01:41 +01:00
Florine W. Dekker a1d3384e32
Update dependencies 2023-03-02 14:41:21 +01:00
Florine W. Dekker eb12f0473a
Correctly load dark theme if dark is preferred 2022-12-17 19:20:12 +01:00
Florine W. Dekker 09cacd241f
Increase inline button padding 2022-12-16 22:39:16 +01:00
Florine W. Dekker c495714867
Let form declare status card 2022-12-16 21:39:05 +01:00
Florine W. Dekker 080c1f852c
Move some form styling around from other projects 2022-12-14 21:26:05 +01:00
Florine W. Dekker 7ecd9ec165
Load all hints and add inline button 2022-12-14 21:17:41 +01:00
Florine W. Dekker e5e6a0d347
Bold table headers 2022-12-14 20:45:27 +01:00
Florine W. Dekker cba23b4911
Remove top margin from form validation in footers 2022-12-14 20:39:37 +01:00
Florine W. Dekker 46da788e86
Fix sticky sidebar query 2022-12-12 17:58:55 +01:00
Florine W. Dekker 0ec60b3480
Add sticky sidebar class 2022-12-12 17:53:54 +01:00
Florine W. Dekker 7648e49999
Fix status card close button location and margin 2022-12-08 19:19:25 +01:00
Florine W. Dekker 201efeca8c
Implement storage/retrieval of strings 2022-11-26 15:36:09 +01:00
Florine W. Dekker c8c99373a3
Enforce custom colours when no theme is specified 2022-11-26 14:38:13 +01:00
Florine W. Dekker 9220ac6224
Add support for dark theme
Fixes #33.
2022-11-26 14:34:01 +01:00
Florine W. Dekker 6c8215bc0c
Do not override theme if already set 2022-11-26 14:07:33 +01:00
Florine W. Dekker 0be8fc9b97
Restore tab index for linkless nav elements 2022-11-24 18:17:47 +01:00
Florine W. Dekker c71fab3342
Give normal weight to checkbox and radio labels 2022-11-24 18:14:00 +01:00
Florine W. Dekker a208f13f4e
Highlight logo if on main page 2022-11-24 17:49:22 +01:00
Florine W. Dekker 2e33b8d762
Use mouse-enabled navbar for mobile
Works towards fixing #35.
2022-11-24 17:38:08 +01:00
12 changed files with 186 additions and 78 deletions

BIN
package-lock.json generated

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "@fwdekker/template",
"version": "3.3.5",
"version": "3.6.5",
"description": "The base template for pages on fwdekker.com.",
"author": "Florine W. Dekker",
"license": "MIT",
@ -24,20 +24,20 @@
"deploy": "grunt deploy"
},
"dependencies": {
"@picocss/pico": "^1.5.6"
"@picocss/pico": "^1.5.10"
},
"devDependencies": {
"grunt": "^1.5.3",
"grunt": "^1.6.1",
"grunt-cli": "^1.4.3",
"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-focus": "^1.0.0",
"grunt-webpack": "^5.0.0",
"ts-loader": "^9.4.1",
"grunt-webpack": "^6.0.0",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.1",
"typescript": "^4.9.3",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0"
"typescript": "^5.3.2",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
}
}

View File

@ -4,5 +4,5 @@
@import "snippets/colors.css";
@import "snippets/common.css";
@import "snippets/forms.css";
@import "snippets/nav.css";
@import "snippets/validation.css";

View File

@ -1,12 +1,39 @@
/* 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-hover: rgb(0, 61, 245) !important;
--primary-focus: rgba(0, 41, 163, 0.125) !important;
--primary-focus-opaque: rgb(0, 41, 163) !important;
--primary-focus-dark: rgb(0, 29, 114) !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-focus-color: var(--primary-focus) !important;
--switch-color: var(--primary-inverse) !important;

View File

@ -53,6 +53,11 @@ a[target="_blank"]::after {
.grid-with-sidebar aside {
max-width: var(--aside-width);
}
.grid-with-sidebar aside .sticky {
position: sticky;
top: var(--block-spacing-vertical);
}
}
@media (max-width: 992px) {
@ -75,7 +80,7 @@ noscript.fwd-js-notice p {
/* Header */
header.fwd-header a[href="."] {
color: black;
color: unset;
}

View File

@ -1,14 +1,21 @@
/* Status card */
.status-card {
font-weight: bold;
position: relative;
}
.status-card output {
display: block;
margin-right: var(--block-spacing-horizontal);
}
.status-card .close {
float: right;
display: block;
position: absolute;
right: var(--block-spacing-horizontal);
top: calc(var(--block-spacing-vertical) / 2);
width: 1rem;
height: 1rem;
margin: 0 0 calc(var(--block-spacing-vertical) / 2) calc(var(--block-spacing-vertical) / 2);
background-image: var(--icon-close);
background-position: center;
@ -46,6 +53,7 @@
}
/* Input validation */
label.invalid,
*[data-label-for].invalid,
input.invalid,
@ -59,3 +67,21 @@ input.valid,
*[data-hint-for].valid {
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;
}

View File

@ -29,10 +29,6 @@ nav.fwd-nav li > :first-child {
margin: 0;
}
nav.fwd-nav a {
cursor: pointer;
}
nav.fwd-nav ul ul {
display: none;
@ -44,7 +40,7 @@ nav.fwd-nav ul ul {
box-shadow: var(--fwd-nav-box-shadow);
}
nav.fwd-nav li:where(:active, :focus-within, :hover) > ul {
nav.fwd-nav li:where(:active, :focus-within, :hover, .fwd-nav-active) > ul {
display: flex;
flex-direction: column;
align-items: start;
@ -70,15 +66,15 @@ nav.fwd-nav ul {
background-color: var(--primary);
}
nav.fwd-nav li.border-above {
nav.fwd-nav li.fwd-nav-separator {
border-top: 1px solid #ccc;
}
nav.fwd-nav li:where(:active, :focus-within, :hover) {
nav.fwd-nav li:where(:active, :focus-within, :hover, .fwd-nav-active) {
background-color: var(--primary-focus-dark);
}
nav.fwd-nav li.current-page:not(:where(:active, :focus-within, :hover)) {
nav.fwd-nav li.fwd-nav-highlighted:not(:where(:active, :focus-within, :hover, .fwd-nav-active)) {
background-color: var(--primary-focus-opaque);
}
@ -125,7 +121,7 @@ nav.fwd-nav #fwd-nav-hamburger-label {
color: var(--primary-inverse);
}
nav.fwd-nav #fwd-nav-hamburger-label:where(:active, :focus-within, :hover) {
nav.fwd-nav #fwd-nav-hamburger-label:where(:active, :focus-within, :hover, .fwd-nav-active) {
background-color: var(--primary-focus-dark);
}

View File

@ -1,14 +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;
}
/* pico.css: Bold labels */
: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 */
h1 {
--typography-spacing-vertical: 1.5rem;
@ -54,8 +69,12 @@ article > header {
}
article > footer {
margin-bottom: 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,

View File

@ -20,7 +20,6 @@ export interface Storage {
*
* @param name the name of the array to store
* @param value the array to store under the given name
* @protected
*/
setArray(name: string, value: any[]): void;
@ -29,7 +28,6 @@ export interface Storage {
*
* @param name the name of the boolean to retrieve
* @param def the value to return if no boolean is stored with the given name
* @protected
*/
getBoolean(name: string, def: boolean): boolean;
@ -38,7 +36,6 @@ export interface Storage {
*
* @param name the name of the boolean to store
* @param value the boolean to store under the given name
* @protected
*/
setBoolean(name: string, value: boolean): void;
@ -47,7 +44,6 @@ export interface Storage {
*
* @param name the name of the number to retrieve
* @param def the value to return if no number is stored with the given name
* @protected
*/
getNumber(name: string, def: number): number;
@ -56,9 +52,24 @@ export interface Storage {
*
* @param name the name of the number to store
* @param value the number to store under the given name
* @protected
*/
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 {
const item = this.read();
item[name] = JSON.stringify(value);
this.write(item);
this.setString(name, JSON.stringify(value));
}
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 {
const item = this.read();
item[name] = "" + value;
this.write(item);
this.setString(name, "" + value);
}
getNumber(name: string, def: number = 0): number {
return +(this.read()[name] ?? def);
return +this.getString(name, "" + def);
}
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();
item[name] = "" + value;
item[name] = value;
this.write(item);
}
}
@ -175,4 +190,12 @@ export class MemoryStorage implements Storage {
getNumber(name: string, def: number): number {
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;
}
}

View File

@ -86,15 +86,23 @@ function nav(highlightPath?: string, cb?: (json: any) => void): HTMLElement {
const checkbox = stringToHtml(`<input id="fwd-nav-hamburger-checkbox" type="checkbox" hidden />`);
nav.appendChild(checkbox);
const base = stringToHtml(`<ul><li><a id="logo" href="https://fwdekker.com/">FWDekker</a></span></li></ul>`);
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/")
.then(it => it.json())
.then(json => {
if (cb !== undefined)
cb(json);
json.entries.forEach(
(entry: any) => base.appendChild(stringToHtml(unpackEntry(entry, "/", highlightPath)))
json.entries.forEach((it: any) => base.appendChild(unpackEntry(it, "/", highlightPath)));
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));
@ -113,38 +121,42 @@ function nav(highlightPath?: string, cb?: (json: any) => void): HTMLElement {
* @param parentPath the current path traversed, found by joining the names of the entries with `/`s; always starts and
* ends with a `/`
* @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, parentPath: string = "/", highlightPath?: string): string {
function unpackEntry(entry: any, parentPath: string = "/", highlightPath?: string): HTMLLIElement {
const path = `${parentPath + entry.name}/`;
const hasChildren = entry.entries.length !== 0;
const classList = [];
if (highlightPath?.startsWith(path) ?? false) classList.push("current-page");
if (entry.border) classList.push("border-above");
const classString = classList.length === 0 ? "" : `class="${classList.join(" ")}"`;
const li = document.createElement("li");
if (highlightPath === path) li.setAttribute("aria-current", "page");
if (highlightPath?.startsWith(path) ?? false) li.classList.add("fwd-nav-highlighted");
if (entry.border) li.classList.add("fwd-nav-separator");
const a = document.createElement("a");
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;
let hrefString;
if (entry.link == null) {
hrefString = "";
} else {
hrefString = `href="${entry.link}"`;
if (entry.link !== "#" && !/^https:\/\/.*fwdekker.com/i.test(entry.link))
hrefString += ` target="_blank"`;
a.target = "_blank";
}
li.addEventListener("click", () => li.classList.add("fwd-nav-active"));
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);
}
if (entry.entries.length === 0)
return `<li ${classString}><a ${hrefString}>${entry.name}</a></li>`;
const depth = parentPath.split("/").length - 2; // -1 because count parts, then another -1 because of leading `/`
const arrow = depth === 0 ? "&#9662;" : "&#9656;";
return "" +
`<li ${classString}>` +
/**/`<a ${hrefString}>${entry.name} ${arrow}</a>` +
/**/`<ul>` +
/**//**/entry.entries.map((it: any) => unpackEntry(it, path, highlightPath)).join("") +
/**/`</ul>` +
`</li>`;
return li;
}
/**
@ -187,7 +199,7 @@ function footer(
if (author === undefined) author = "Florine&nbsp;W.&nbsp;Dekker";
if (authorURL === undefined) authorURL = "https://fwdekker.com/";
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 (privacyPolicyURL === undefined) privacyPolicyURL = "https://fwdekker.com/privacy/";
@ -231,8 +243,6 @@ function footerLink(prefix: string, text: string | null | undefined, url: string
* not a valid value as a parameter for that function, its value is considered `undefined`.
*/
doAfterLoad(() => {
$("html")!.dataset["theme"] = "light";
const navTarget = $(getMetaProperty("fwd:nav:target"));
if (navTarget != null) {
navTarget.parentElement?.replaceChild(

View File

@ -27,21 +27,23 @@ export function showMessageType(card: HTMLElement | HTMLFormElement,
message?: string,
type?: "busy" | "error" | "info" | "success" | "warning"): void {
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;
card = formCard;
}
const output = $("output", card)!;
card.removeAttribute("aria-busy");
output.removeAttribute("aria-busy");
card.classList.remove("hidden", "error", "info", "success", "warning");
if (message == null || type == null) {
card.classList.add("hidden");
output.innerHTML = "";
} else {
if (type === "busy") card.setAttribute("aria-busy", "true");
if (type === "busy") output.setAttribute("aria-busy", "true");
else card.classList.add(type);
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;
hint.innerHTML = hint.dataset["hint"] ?? "";

View File

@ -11,7 +11,7 @@
<meta name="fwd:nav:target" content="#nav" />
<meta name="fwd:nav:highlight-path" content="/Tools/Dice/" />
<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:validation:load-forms" />
@ -61,8 +61,8 @@
<h3>Already have an account? Welcome back!</h3>
</hgroup>
</header>
<form id="test-form" novalidate>
<article class="status-card hidden" data-status-for="test-form">
<form id="test-form" data-status-card="test-status-card" novalidate>
<article id="test-status-card" class="status-card hidden">
<output>Congrats!</output>
<a class="close" href="#" aria-label="Close"></a>
</article>