work on weather services; outbound http request caching; styles updates and icons

This commit is contained in:
2023-08-12 19:19:20 -07:00
parent 7c205632cb
commit 85d72b43d2
55 changed files with 2593 additions and 234 deletions

View File

@@ -0,0 +1,60 @@
* {
box-sizing: border-box;
}
fieldset.radio {
width: 30rem;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
column-gap: 2%;
border-color: var(--theme-border-input);
}
fieldset.radio.theme {
margin-block: 1rem 2rem;
}
@media screen and (max-width: 500px) {
fieldset.radio {
width: calc(100vw - 5rem);
}
}
label {
width: 32%;
height: 2rem;
display: block;
position: relative;
}
input {
appearance: none;
display: block;
background: transparent;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
input:not(:checked) {
cursor: pointer;
background: var(--theme-bg-button-primary);
}
input:checked {
background: var(--theme-bg-button-primary-hover);
}
span {
z-index: 2;
position: relative;
pointer-events: none;
text-align: center;
display: block;
height: 100%;
line-height: 2rem;
}

View File

@@ -0,0 +1,7 @@
<a class="popup-open color-theme-contrast" href="#popup-color-theme-contrast">Colors and Contrast</a>
<div class="popup" id="popup-color-theme-contrast" role="dialog">
<div class="popup-content">
<a class="popup-close" href="#">Close</a>
<color-theme-and-contrast-controls></color-theme-and-contrast-controls>
</div>
</div>

View File

@@ -0,0 +1,130 @@
(() => {
const color_theme = 'color_theme';
const color_contrast = 'color_contrast';
const color_theme_attr = 'data-color-theme';
const color_contrast_attr = 'data-color-contrast';
const override_theme = localStorage.getItem(color_theme);
const override_contrast = localStorage.getItem(color_contrast);
if (override_theme) {
document.body.setAttribute(color_theme_attr, override_theme);
}
if (override_contrast) {
document.body.setAttribute(color_contrast_attr, override_contrast);
}
const template = `
<link rel="stylesheet" href="/typography.css">
<link rel="stylesheet" href="/color-theme-controls.css">
<div class="popup" title="Color Theme and Contrast Controls">
<fieldset class="theme radio">
<legend>Color Theme</legend>
<label>
<input type="radio" name="color_theme" value="auto" ${checked_if(! override_theme)}>
<span>Auto</span>
</label>
<label>
<input type="radio" name="color_theme" value="light" ${checked_if(override_theme === 'light')}>
<span>Light</span>
</label>
<label>
<input type="radio" name="color_theme" value="dark" ${checked_if(override_theme === 'dark')}>
<span>Dark</span>
</label>
</fieldset>
<fieldset class="contrast radio">
<legend>Color Contrast</legend>
<label>
<input type="radio" name="color_contrast" value="less" ${checked_if(override_contrast === 'less')}>
<span>Less Contrast</span>
</label>
<label>
<input type="radio" name="color_contrast" value="default" ${checked_if(! override_contrast)}>
<span>Default</span>
</label>
<label>
<input type="radio" name="color_contrast" value="more" ${checked_if(override_contrast === 'more')}>
<span>More Contrast</span>
</label>
</fieldset>
</div>
`;
customElements.define('color-theme-and-contrast-controls',
class ColorThemeToggleButton extends HTMLElement {
#popup = null;
#theme_inputs = null;
#contrast_inputs = null;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = template;
this.#popup = this.shadowRoot.querySelector('.popup');
this.#theme_inputs = this.#popup.querySelectorAll('.theme input');
this.#contrast_inputs = this.#popup.querySelectorAll('.contrast input');
}
connectedCallback() {
this.#popup.addEventListener('change', this.#onChange);
}
disconnectedCallback() {
this.#popup.removeEventListener('change', this.#onChange);
}
#onChange = () => {
const theme = this.#theme_input_value;
const contrast = this.#contrast_input_value;
if (theme === 'auto') {
localStorage.removeItem(color_theme);
document.body.removeAttribute(color_theme_attr);
}
else {
localStorage.setItem(color_theme, theme);
document.body.setAttribute(color_theme_attr, theme);
}
if (contrast === 'default') {
localStorage.removeItem(color_contrast);
document.body.removeAttribute(color_contrast_attr);
}
else {
localStorage.setItem(color_contrast, contrast);
document.body.setAttribute(color_contrast_attr, theme);
}
};
get #theme_input_value() {
return radio_value(this.#theme_inputs, 'auto');
}
get #contrast_input_value() {
return radio_value(this.#contrast_inputs, 'auto');
}
}
);
function checked_if(condition) {
return condition ? 'checked' : '';
}
function radio_value(inputs, default_value) {
inputs = [ ...inputs ];
for (const input of inputs) {
if (input.checked) {
return input.value;
}
}
return default_value;
}
})();

View File

@@ -0,0 +1,22 @@
<aside class="controls">
<div>
{{# user }}
<p>Logged in as
{{# user.name }}{{ user.name }}{{/ user.name }}
{{^ user.name }}{{ user.username }}{{/ user.name }}
-
</p>
<form action="/logout" method="POST">
<button type="submit">Logout</button>
</form>
{{/ user }}
{{^ user }}
<form action="/login" method="POST">
<button type="submit">Login with OpenID Connect</button>
</form>
{{/ user }}
</div>
{{> color_theme_controls }}
</aside>

143
templates/forms.css Normal file
View File

@@ -0,0 +1,143 @@
fieldset {
border-color: var(--theme-border-input);
}
button, input:is([type='button'], [type='submit'], [type='reset']) {
appearance: none;
cursor: pointer;
border: 0;
padding: 0.5rem 1rem;
}
/*
label {
margin-block-start: 1rem;
}
label:not(:first-of-type) {
margin-block-start: 2rem;
}
label.radio {
display: flex;
margin-block: 1rem;
justify-content: flex-start;
}
input,
textarea {
display: block;
margin-block-start: 0.35rem;
margin-block-end: 1rem;
border: 0.125rem var(--theme-border-input) solid;
border-radius: 0.25rem;
background-color: var(--theme-bg-input);
padding: 0.5rem;
width: 20rem;
}
input[disabled],
textarea[disabled] {
opacity: 0.4;
cursor: default;
pointer-events: none;
}
input[type='text']:invalid,
input[type='password']:invalid,
textarea:invalid {
border-color: var(--theme-border-input-invalid);
}
input[type='text'][readonly] {
color: var(--theme-text-light);
border-color: var(--theme-line);
}
input[type='checkbox'] {
width: 1rem;
height: 1rem;
padding: 0;
}
input[type='checkbox'] + label {
margin-inline-start: 0.5rem;
margin-inline-end: 2rem;
}
input[type='radio'] {
flex: 0 0 2rem;
margin: 0.2rem;
}
textarea {
line-height: 1.7rem;
font-family: var(--font-monospace);
}
::-webkit-input-placeholder {
color: var(--theme-text-light);
}
button {
padding: 0.5rem 1.5rem;
border-radius: 0.5rem;
font-size: 1rem;
margin-block-start: 1rem;
margin-inline-end: 1rem;
cursor: pointer;
user-select: none;
appearance: none;
color: var(--theme-text-button-primary);
background: var(--theme-bg-button-primary);
text-decoration: none;
font-size: 1rem;
font-family: var(--font-body);
font-weight: 700;
line-height: 2rem;
display: flex;
align-items: center;
}
button[disabled] {
opacity: 0.4;
cursor: default;
pointer-events: none;
}
select {
display: block;
margin-block-start: 0.35rem;
margin-block-end: 1rem;
border: 0.125rem var(--theme-border-input) solid;
border-radius: 0.25rem;
background-color: var(--theme-bg-input);
padding: 0.5rem;
width: 20rem;
cursor: pointer;
}
select[disabled] {
opacity: 0.4;
cursor: default;
pointer-events: none;
}
select option {
background-color: var(--theme-bg-input);
padding: 0.5rem;
}
:is(table, .table) :is(input, select) {
margin-block: 0;
}
*/

View File

@@ -0,0 +1,3 @@
<section data-widget="local-clock" title="Local-time clock">
{{! }}
</section>

View File

@@ -0,0 +1,5 @@
<aside class="error">
<h2>Login Failed</h2>
<p><b>Error Code:</b> {{ login_error_code }}</p>
<p><b>Error Message:</b> {{ login_error.message }}</p>
</aside>

View File

@@ -1,21 +0,0 @@
<!doctype html>
<html>
<head>
<title>Login</title>
<link rel="stylesheet" type="text/css" href="/themes.css">
<link rel="stylesheet" type="text/css" href="/typography.css">
</head>
<body>
<form action="/login" method="POST">
<button type="submit">Login with OpenID Connect</button>
</form>
{{# error_code }}
<div>
<h4>Login failed</h4>
<b>Error Code:</b> {{ error_code }}<br />
<b>Error Message:</b> {{ error.message }}
</div>
{{/ error_code }}
</body>
</html>

View File

@@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<title>{{ page_title }}</title>
<link rel="stylesheet" href="/themes.css">
<link rel="stylesheet" href="/typography.css">
<link rel="stylesheet" href="/forms.css">
<link rel="stylesheet" href="/popup.css">
<link rel="stylesheet" href="/structure.css">
<script src="/color-theme-controls.js" async></script>
</head>
<body>
{{> controls }}
{{> page_content }}
</body>
</html>

37
templates/popup.css Normal file
View File

@@ -0,0 +1,37 @@
.popup {
z-index: 1;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
user-select: none;
pointer-events: none;
display: flex;
opacity: 0;
transition: opacity 0.125s linear;
background: var(--theme-bg-popup-mask);
}
.popup:target {
opacity: 1;
user-select: unset;
pointer-events: unset;
}
.popup .popup-content {
margin: auto;
position: relative;
background: var(--theme-bg-main);
border: 0.125rem solid var(--theme-border-input);
border-radius: 1rem;
padding: 2rem;
}
.popup .popup-close {
position: absolute;
top: 1rem;
right: 2rem;
font-size: 0.8rem;
}

View File

@@ -1,22 +1,9 @@
<!doctype html>
<html>
<head>
<title>Dashboard</title>
<link rel="stylesheet" type="text/css" href="/themes.css">
<link rel="stylesheet" type="text/css" href="/typography.css">
</head>
<body>
<header>
<h1>Dashboard</h1>
</header>
{{# user }}
<p>Logged in as {{ user.name }} ({{ user.username }})</p>
<form action="/logout" method="POST">
<button type="submit">Logout</button>
</form>
{{/ user }}
{{^ user }}
<a href="/login">Login Page</a>
{{/ user }}
</body>
</html>
<main>
{{# rendered_widgets }}
{{{ . }}}
{{/ rendered_widgets }}
</main>

64
templates/structure.css Normal file
View File

@@ -0,0 +1,64 @@
* {
box-sizing: border-box;
}
html, body {
background: var(--theme-bg-main);
}
/* ===== Primary Controls ===== */
aside.controls {
position: absolute;
top: 1rem;
right: 1rem;
text-align: right;
}
aside.controls :is(a, p, button) {
display: inline;
font-size: 0.8rem;
margin-block: 0.5rem;
}
aside.controls form {
display: contents;
}
aside.controls button {
margin: 0;
padding: 0;
border: 0;
background: transparent;
color: var(--theme-text-link);
text-decoration: underline;
}
aside.controls button:hover {
background: transparent;
}
/* ===== Error Box ===== */
aside.error {
margin-block: 4rem;
margin-inline: 1rem;
padding: 1rem;
border: 0.1rem solid var(--theme-border-error-box);
background: var(--theme-bg-error-box);
}
aside.error h2 {
font-size: 1.2rem;
margin-block-start: 0;
}
aside.error p {
margin-block: 0;
color: var(--theme-text-error-box);
}

View File

@@ -3,76 +3,60 @@
color-scheme: light dark;
}
{{! ===== Default Themes ===== }}
body {
{{> default_light }}
}
body[data-color-scheme='dark'] {
body[data-color-theme='dark'] {
{{> default_dark }}
}
{{# less_contrast }}
body[data-color-contrast='less'] {
{{> less_contrast_light }}
}
body[data-color-theme='dark'][data-color-contrast='less'] {
{{> less_contrast_dark }}
}
{{/ less_contrast }}
{{# more_contrast }}
body[data-color-contrast='more'] {
{{> more_contrast_light }}
}
body[data-color-theme='dark'][data-color-contrast='more'] {
{{> more_contrast_dark }}
}
{{/ more_contrast }}
@media (prefers-color-scheme: dark) {
body {
{{> default_dark }}
}
body[data-color-scheme='light'] {
body[data-color-theme='light'] {
{{> default_light }}
}
}
{{! ===== High Contrast Themes ===== }}
{{# more_contrast }}
@media (prefers-contrast: more) {
body {
{{> more_contrast_light }}
}
body[data-color-scheme='dark'] {
{{> more_contrast_dark }}
}
@media (prefers-color-scheme: dark) {
body {
{{> more_contrast_dark }}
}
body[data-color-scheme='light'] {
{{> more_contrast_light }}
}
}
}
{{/ more_contrast }}
{{! ===== Low Contrast Themes ===== }}
{{# less_contrast }}
@media (prefers-contrast: less) {
body {
{{> less_contrast_light }}
}
body[data-color-scheme='dark'] {
{{# less_contrast }}
body[data-color-contrast='less'] {
{{> less_contrast_dark }}
}
@media (prefers-color-scheme: dark) {
body {
{{> less_contrast_dark }}
}
body[data-color-scheme='light'] {
{{> less_contrast_light }}
}
body[data-color-theme='light'][data-color-contrast='less'] {
{{> less_contrast_light }}
}
{{/ less_contrast }}
{{# more_contrast }}
body[data-color-contrast='more'] {
{{> more_contrast_dark }}
}
body[data-color-theme='light'][data-color-contrast='more'] {
{{> more_contrast_light }}
}
{{/ more_contrast }}
}
{{/ less_contrast }}

View File

@@ -1,17 +1,36 @@
:root {
--font-heading: 'Open Sans', sans-serif;
--font-body: 'Open Sans', sans-serif;
--font-heading: Verdana, sans-serif;
--font-body: Verdana, sans-serif;
--font-monospace: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 16px;
font-family: var(--font-body);
}
@media screen and (max-width: 800px) {
:root {
font-size: 14px;
}
}
@media screen and (min-width: 2000px) {
:root {
font-size: 18px;
}
}
/* ===== Font Families ===== */
h1, h2, h3, h4, h5, h6,
th, dt {
font-family: var(--font-heading);
}
p, td, dd, figcaption, li, blockquote {
p, td, dd, figcaption, li, blockquote,
input, textarea, select, option, optgroup, legend, fieldset, label, button {
font-family: var(--font-body);
}
@@ -24,3 +43,110 @@ b, i, u, q,
strong, em, mark, cite {
font-family: inherit;
}
/* ===== Font Sizes ===== */
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 1.8rem;
}
h3 {
font-size: 1.5rem;
}
h4 {
font-size: 1.2rem;
}
h5 {
font-size: 1rem;
}
h6 {
font-size: 0.8rem;
}
th, td, dt, dd,
p, figcaption, li, blockquote,
pre,
input, textarea, select, option, optgroup, legend, fieldset, label, button {
font-size: 1rem;
}
a, span,
b, i, u, q,
strong, em, mark, cite,
code, samp {
font-size: inherit;
}
/* ===== Colors ===== */
::selection {
color: var(--theme-text-selection);
background: var(--theme-bg-text-selection);
}
h1, h2, h3, h4, h5, h6,
th, dt {
color: var(--theme-text-heading)
}
p, td, dd, figcaption, li, blockquote,
input, textarea, select, option, optgroup, legend, fieldset, label {
color: var(--theme-text-body)
}
button, input:is([type='button'], [type='submit'], [type='reset']) {
color: var(--theme-text-button-primary);
background: var(--theme-bg-button-primary);
}
:is(button, input:is([type='button'], [type='submit'], [type='reset'])):hover {
background: var(--theme-bg-button-primary-hover);
}
:is(button, input:is([type='button'], [type='submit'], [type='reset'])).secondary {
color: var(--theme-text-button-secondary);
background: var(--theme-bg-button-secondary);
}
:is(button, input:is([type='button'], [type='submit'], [type='reset'])).secondary:hover {
background: var(--theme-bg-button-secondary-hover);
}
pre, code, samp {
color: var(--theme-code-normal);
}
a {
color: var(--theme-text-link);
}
a:active {
color: var(--theme-text-link-active);
}
a:visited {
color: var(--theme-text-link-visited);
}
mark {
color: var(--theme-text-highlight);
background: var(--theme-bg-text-highlight);
}
span,
b, i, u, q,
strong, em, cite {
color: inherit;
}

View File

@@ -0,0 +1,78 @@
<section data-widget="weather-gov-forecast" title="Weather Forecast for {{ location.name }}">
<link rel="stylesheet" href="/weather.gov/styles.css">
<div class="flex-row">
<div class="today">
{{# forecast_today }}
<div class="{{# isDaytime }}day{{/ isDaytime }}{{^ isDaytime }}night{{/ isDaytime }}">
<h2>{{ name }}</h2>
<p class="condition">{{ shortForecast }}</p>
<p class="temp">
{{{ icons.thermometer }}}
<span>{{ temperature }}<sup>{{ temperatureUnit }}</sup></span>
</p>
<p class="wind">
{{{ icons.wind }}}
<span>{{ windSpeed }} <!-- / {{ windDirection }} --></span>
</p>
</div>
{{/ forecast_today }}
</div>
<table class="future">
<tr class="day">
{{# forecast_days }}
<td>
{{# . }}
<h3>{{ name }}</h3>
<p class="condition">{{ shortForecast }}</p>
<p class="temp">
{{{ icons.thermometer }}}
<span>{{ temperature }}<sup>{{ temperatureUnit }}</sup></span>
</p>
<p class="wind">
{{{ icons.wind }}}
<span>{{ windSpeed }} <!-- / {{ windDirection }} --></span>
</p>
{{/ . }}
</td>
{{/ forecast_days }}
</tr>
<tr class="night">
{{# forecast_nights }}
<td>
{{# . }}
<h3>{{ name }}</h3>
<p class="condition">{{ shortForecast }}</p>
<p class="temp">
{{{ icons.thermometer }}}
<span>{{ temperature }}<sup>{{ temperatureUnit }}</sup></span>
</p>
<p class="wind">
{{{ icons.wind }}}
<span>{{ windSpeed }} <!-- / {{ windDirection }} --></span>
</p>
{{/ . }}
</td>
{{/ forecast_nights }}
</tr>
</table>
</div>
{{# alerts.length }}
<ul class="alerts" title="Alerts">
{{# alerts }}
<li>
<h2>{{ event }}</h2>
<p class="headline">{{ headline }}</p>
<p class="description">{{ description }}</p>
<p class="instruction">{{ instruction }}</p>
</li>
{{/ alerts }}
</ul>
{{/ alerts.length }}
<p class="powered-by">
Powered by <a href="https://www.weather.gov/documentation/services-web-api" rel="external nofollow noreferrer">weather.gov</a>
</p>
</section>

View File

@@ -0,0 +1,83 @@
[data-widget='weather-gov-forecast'] {
padding: 1rem;
max-width: 70rem;
}
[data-widget='weather-gov-forecast'] .alerts {
margin-block: 1rem;
padding-inline: 1rem;
border: 0.1rem solid var(--theme-border-error-box);
background: var(--theme-bg-error-box);
list-style: none;
}
[data-widget='weather-gov-forecast'] .alerts li {
margin-block: 1rem;
}
[data-widget='weather-gov-forecast'] .alerts p {
color: var(--theme-text-error-box);
}
[data-widget='weather-gov-forecast'] .alerts p.headline {
font-weight: 700;
}
[data-widget='weather-gov-forecast'] .flex-row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
[data-widget='weather-gov-forecast'] .today {
display: inline-flex;
flex-direction: column;
row-gap: 2rem;
padding: 1rem;
margin-block-end: 1rem;
background: var(--theme-bg-light);
}
[data-widget='weather-gov-forecast'] svg.icon {
width: 1rem;
height: 1rem;
flex: 0 0 1rem;
}
[data-widget='weather-gov-forecast'] h2 {
font-size: 1.2rem;
margin-block: 0;
}
[data-widget='weather-gov-forecast'] .today p {
font-size: 1rem;
}
[data-widget='weather-gov-forecast'] .today svg.icon {
width: 1.2rem;
height: 1.2rem;
}
[data-widget='weather-gov-forecast'] h3 {
font-size: 0.8rem;
margin-block-end: 0;
}
[data-widget='weather-gov-forecast'] p {
font-size: 0.8rem;
margin-block: 0.25rem;
}
[data-widget='weather-gov-forecast'] p:is(.wind, .temp) {
display: flex;
column-gap: 0.5rem;
}
[data-widget='weather-gov-forecast'] table {
margin-block-end: 1rem;
}
[data-widget='weather-gov-forecast'] td {
padding-inline: 1rem;
}

View File

@@ -0,0 +1,21 @@
<section data-widget="weatherapi-com-current" title="Current Weather for {{ location.name }}">
<link rel="stylesheet" href="/weatherapi.com/styles.css">
<p class="condition">
<span>{{ weather.current.condition.text }}</span>
</p>
<p class="temp">
{{{ icons.thermometer }}}
<span>{{ weather.current.temp_f }}<sup>F</sup> / {{ weather.current.temp_c }}<sup>C</sup></span>
</p>
<p class="wind">
{{{ icons.wind }}}
<span>{{ weather.current.wind_mph }} mph / {{ weather.current.wind_kph }} km/h / {{ weather.current.wind_dir }}</span>
</p>
<p class="powered-by">
Powered by <a href="https://www.weatherapi.com/docs/" rel="external nofollow noreferrer">WeatherAPI.com</a>
</p>
</section>

View File

@@ -0,0 +1,32 @@
[data-widget='weatherapi-com-current'] {
width: 20rem;
padding: 1rem;
}
[data-widget='weatherapi-com-current'] svg.icon {
width: 1.2rem;
height: 1.2rem;
flex: 0 0 1.2rem;
}
[data-widget='weatherapi-com-current'] h2 {
font-size: 1.2rem;
margin-block: 0;
}
[data-widget='weatherapi-com-current'] p:is(.condition, .wind, .temp) {
display: flex;
column-gap: 0.5rem;
font-size: 1rem;
margin-block: 0.25rem;
}
[data-widget='weatherapi-com-current'] p.condition {
margin-block-end: 1rem;
}
[data-widget='weatherapi-com-current'] p.powered-by {
font-size: 0.8rem;
margin-block-start: 1rem;
}