Compare commits
15 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 |
Binary file not shown.
18
package.json
18
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@fwdekker/template",
|
"name": "@fwdekker/template",
|
||||||
"version": "3.4.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,26 +1,39 @@
|
||||||
/* pico.css overrides, based on https://picocss.com/docs/customization.html */
|
/* pico.css overrides, based on https://picocss.com/docs/customization.html */
|
||||||
|
|
||||||
|
/* Light (default) */
|
||||||
.fwd-nav,
|
.fwd-nav,
|
||||||
:root[data-theme="light"] {
|
[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-focus-dark: rgb(0, 29, 114) !important;
|
||||||
--primary-inverse: white !important;
|
--primary-inverse: white !important;
|
||||||
|
|
||||||
--form-element-active-border-color: var(--primary) !important;
|
|
||||||
--form-element-focus-color: var(--primary-focus) !important;
|
|
||||||
--switch-color: var(--primary-inverse) !important;
|
|
||||||
--switch-checked-background-color: var(--primary) !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) {
|
:root[data-theme="dark"]:not(.fwd-nav) {
|
||||||
--primary: #1e88e5 !important;
|
--primary: #1e88e5 !important;
|
||||||
--primary-hover: #2196f3 !important;
|
--primary-hover: #2196f3 !important;
|
||||||
--primary-focus: rgba(30, 136, 229, 0.25) !important;
|
--primary-focus: rgba(30, 136, 229, 0.25) !important;
|
||||||
--primary-focus-opaque: rgb(30, 136, 229) !important;
|
--primary-focus-opaque: rgb(30, 136, 229) !important;
|
||||||
--primary-inverse: white !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;
|
||||||
|
|
|
@ -53,6 +53,11 @@ a[target="_blank"]::after {
|
||||||
.grid-with-sidebar aside {
|
.grid-with-sidebar aside {
|
||||||
max-width: var(--aside-width);
|
max-width: var(--aside-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-with-sidebar aside .sticky {
|
||||||
|
position: sticky;
|
||||||
|
top: var(--block-spacing-vertical);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -11,11 +11,15 @@
|
||||||
--muted-color: hsl(205deg, 12%, 59%) !important;
|
--muted-color: hsl(205deg, 12%, 59%) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* pico.css: Bold labels, except for checkbox/radio labels */
|
/* pico.css: Bold <label> and <th>, except for checkbox/radio labels */
|
||||||
:root {
|
:root {
|
||||||
--form-label-font-weight: bold;
|
--form-label-font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tr th {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
input:where([type="checkbox"], [type="radio"]) + label {
|
input:where([type="checkbox"], [type="radio"]) + label {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
@ -65,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,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/";
|
||||||
|
|
||||||
|
|
|
@ -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"] ?? "";
|
||||||
|
|
|
@ -61,8 +61,8 @@
|
||||||
<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>
|
||||||
|
|
Loading…
Reference in New Issue