Merge 62e9c32082
into 0d6966726e
This commit is contained in:
commit
07f7f88a7b
8 changed files with 352 additions and 22 deletions
|
@ -269,25 +269,41 @@ Example:
|
|||
|
||||
```yaml
|
||||
theme:
|
||||
background-color: 100 20 10
|
||||
primary-color: 40 90 40
|
||||
contrast-multiplier: 1.1
|
||||
background-color: 186 21 20
|
||||
contrast-multiplier: 1.2
|
||||
primary-color: 97 13 80
|
||||
|
||||
presets:
|
||||
my-custom-dark-theme:
|
||||
background-color: 229 19 23
|
||||
contrast-multiplier: 1.2
|
||||
primary-color: 222 74 74
|
||||
positive-color: 96 44 68
|
||||
negative-color: 359 68 71
|
||||
my-custom-light-theme:
|
||||
light: true
|
||||
background-color: 220 23 95
|
||||
contrast-multiplier: 1.0
|
||||
primary-color: 220 91 54
|
||||
positive-color: 109 58 40
|
||||
negative-color: 347 87 44
|
||||
```
|
||||
|
||||
### Themes
|
||||
If you don't want to spend time configuring your own theme, there are [several available themes](themes.md) which you can simply copy the values for.
|
||||
|
||||
### Properties
|
||||
| Name | Type | Required | Default |
|
||||
| ---- | ---- | -------- | ------- |
|
||||
| light | boolean | no | false |
|
||||
| background-color | HSL | no | 240 8 9 |
|
||||
| primary-color | HSL | no | 43 50 70 |
|
||||
| positive-color | HSL | no | same as `primary-color` |
|
||||
| negative-color | HSL | no | 0 70 70 |
|
||||
| contrast-multiplier | number | no | 1 |
|
||||
| text-saturation-multiplier | number | no | 1 |
|
||||
| custom-css-file | string | no | |
|
||||
| Name | Type | Required | Default |
|
||||
| ---- |-------|----------| ------- |
|
||||
| light | boolean | no | false |
|
||||
| background-color | HSL | no | 240 8 9 |
|
||||
| primary-color | HSL | no | 43 50 70 |
|
||||
| positive-color | HSL | no | same as `primary-color` |
|
||||
| negative-color | HSL | no | 0 70 70 |
|
||||
| contrast-multiplier | number | no | 1 |
|
||||
| text-saturation-multiplier | number | no | 1 |
|
||||
| custom-css-file | string | no | |
|
||||
| presets | array | no | |
|
||||
|
||||
#### `light`
|
||||
Whether the scheme is light or dark. This does not change the background color, it inverts the text colors so that they look appropriately on a light background.
|
||||
|
@ -320,6 +336,26 @@ theme:
|
|||
custom-css-file: /assets/my-style.css
|
||||
```
|
||||
|
||||
#### `presets`
|
||||
Define theme presets that can be selected from a dropdown menu in the webpage. Example:
|
||||
```yaml
|
||||
theme:
|
||||
presets:
|
||||
my-custom-dark-theme: # This will be displayed in the dropdown menu to select this theme
|
||||
background-color: 229 19 23
|
||||
contrast-multiplier: 1.2
|
||||
primary-color: 222 74 74
|
||||
positive-color: 96 44 68
|
||||
negative-color: 359 68 71
|
||||
my-custom-light-theme: # This will be displayed in the dropdown menu to select this theme
|
||||
light: true
|
||||
background-color: 220 23 95
|
||||
contrast-multiplier: 1.0
|
||||
primary-color: 220 91 54
|
||||
positive-color: 109 58 40
|
||||
negative-color: 347 87 44
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> Because Glance uses a lot of utility classes it might be difficult to target some elements. To make it easier to style specific widgets, each widget has a `widget-type-{name}` class, so for example if you wanted to make the links inside just the RSS widget bigger you could use the following selector:
|
||||
|
|
|
@ -17,6 +17,16 @@ import (
|
|||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type CssProperties struct {
|
||||
BackgroundColor *hslColorField `yaml:"background-color"`
|
||||
PrimaryColor *hslColorField `yaml:"primary-color"`
|
||||
PositiveColor *hslColorField `yaml:"positive-color"`
|
||||
NegativeColor *hslColorField `yaml:"negative-color"`
|
||||
Light bool `yaml:"light"`
|
||||
ContrastMultiplier float32 `yaml:"contrast-multiplier"`
|
||||
TextSaturationMultiplier float32 `yaml:"text-saturation-multiplier"`
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Server struct {
|
||||
Host string `yaml:"host"`
|
||||
|
@ -31,6 +41,7 @@ type config struct {
|
|||
} `yaml:"document"`
|
||||
|
||||
Theme struct {
|
||||
// Todo : Find a way to use CssProperties struct to avoid duplicates
|
||||
BackgroundColor *hslColorField `yaml:"background-color"`
|
||||
PrimaryColor *hslColorField `yaml:"primary-color"`
|
||||
PositiveColor *hslColorField `yaml:"positive-color"`
|
||||
|
@ -39,6 +50,8 @@ type config struct {
|
|||
ContrastMultiplier float32 `yaml:"contrast-multiplier"`
|
||||
TextSaturationMultiplier float32 `yaml:"text-saturation-multiplier"`
|
||||
CustomCSSFile string `yaml:"custom-css-file"`
|
||||
|
||||
Presets map[string]CssProperties `yaml:"presets"`
|
||||
} `yaml:"theme"`
|
||||
|
||||
Branding struct {
|
||||
|
|
|
@ -3,6 +3,7 @@ package glance
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
|
@ -125,8 +126,9 @@ func (a *application) transformUserDefinedAssetPath(path string) string {
|
|||
}
|
||||
|
||||
type pageTemplateData struct {
|
||||
App *application
|
||||
Page *page
|
||||
App *application
|
||||
Page *page
|
||||
Presets string
|
||||
}
|
||||
|
||||
func (a *application) handlePageRequest(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -137,9 +139,20 @@ func (a *application) handlePageRequest(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
presets := a.Config.Theme.Presets
|
||||
keys := make([]string, 0, len(presets))
|
||||
for key := range presets {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
presetsAsJSON, jsonErr := json.Marshal(presets)
|
||||
if jsonErr != nil {
|
||||
log.Fatalf("Erreur lors de la conversion en JSON : %v", jsonErr)
|
||||
}
|
||||
|
||||
pageData := pageTemplateData{
|
||||
Page: page,
|
||||
App: a,
|
||||
App: a,
|
||||
Page: page,
|
||||
Presets: string(presetsAsJSON),
|
||||
}
|
||||
|
||||
var responseBytes bytes.Buffer
|
||||
|
|
|
@ -2,6 +2,30 @@ import { setupPopovers } from './popover.js';
|
|||
import { setupMasonries } from './masonry.js';
|
||||
import { throttledDebounce, isElementVisible, openURLInNewTab } from './utils.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const theme = localStorage.getItem('theme');
|
||||
|
||||
if (!theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html = document.querySelector('html');
|
||||
const jsonTheme = JSON.parse(theme);
|
||||
if (jsonTheme.themeScheme === 'light') {
|
||||
html.classList.remove('dark-scheme');
|
||||
html.classList.add('light-scheme');
|
||||
} else if (jsonTheme.themeScheme === 'dark') {
|
||||
html.classList.add('dark-scheme');
|
||||
html.classList.remove('light-scheme');
|
||||
}
|
||||
|
||||
html.classList.add(jsonTheme.theme);
|
||||
document.querySelector('[name=color-scheme]').setAttribute('content', jsonTheme.themeScheme);
|
||||
Array.from(document.querySelectorAll('.dropdown-button span')).forEach((button) => {
|
||||
button.textContent = jsonTheme.theme;
|
||||
})
|
||||
})
|
||||
|
||||
async function fetchPageContent(pageData) {
|
||||
// TODO: handle non 200 status codes/time outs
|
||||
// TODO: add retries
|
||||
|
@ -649,6 +673,101 @@ function setupTruncatedElementTitles() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} HslColorField
|
||||
* @property {number} Hue
|
||||
* @property {number} Saturation
|
||||
* @property {number} Lightness
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Theme
|
||||
* @property {HslColorField} BackgroundColor
|
||||
* @property {HslColorField} PrimaryColor
|
||||
* @property {HslColorField} PositiveColor
|
||||
* @property {HslColorField} NegativeColor
|
||||
* @property {boolean} Light
|
||||
* @property {number} ContrastMultiplier
|
||||
* @property {number} TextSaturationMultiplier
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Record<string, Theme>} ThemeCollection
|
||||
*/
|
||||
function setupThemeSwitcher() {
|
||||
const presetsContainers = Array.from(document.querySelectorAll('.custom-presets'));
|
||||
const userThemesKeys = Object.keys(userThemes);
|
||||
|
||||
presetsContainers.forEach((presetsContainer) => {
|
||||
userThemesKeys.forEach(preset => {
|
||||
const presetElement = document.createElement('div');
|
||||
presetElement.className = 'theme-option';
|
||||
presetElement.setAttribute('data-theme', preset);
|
||||
presetElement.setAttribute('data-scheme', userThemes[preset].Light ? 'light' : 'dark');
|
||||
presetElement.textContent = preset;
|
||||
presetsContainer.appendChild(presetElement);
|
||||
});
|
||||
});
|
||||
|
||||
const dropdownButtons = Array.from(document.querySelectorAll('.dropdown-button'));
|
||||
const dropdownContents = Array.from(document.querySelectorAll('.dropdown-content'));
|
||||
|
||||
dropdownButtons.forEach((dropdownButton) => {
|
||||
dropdownButton.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
dropdownContents.forEach((dropdownContent) => {
|
||||
dropdownContent.classList.toggle('show');
|
||||
});
|
||||
dropdownButton.classList.toggle('active');
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.theme-dropdown')) {
|
||||
dropdownContents.forEach((dropdownContent) => {
|
||||
dropdownContent.classList.remove('show');
|
||||
});
|
||||
dropdownButtons.forEach((dropdownButton) => {
|
||||
dropdownButton.classList.remove('active');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.theme-option').forEach(option => {
|
||||
option.addEventListener('click', () => {
|
||||
const selectedTheme = option.getAttribute('data-theme');
|
||||
const selectedThemeScheme = option.getAttribute('data-scheme');
|
||||
const previousTheme = localStorage.getItem('theme');
|
||||
dropdownContents.forEach((dropdownContent) => {
|
||||
dropdownContent.classList.remove('show');
|
||||
});
|
||||
dropdownButtons.forEach((dropdownButton) => {
|
||||
const html = document.querySelector('html');
|
||||
if (previousTheme) {
|
||||
html.classList.remove(JSON.parse(previousTheme).theme);
|
||||
}
|
||||
dropdownButton.classList.remove('active');
|
||||
dropdownButton.querySelector('span').textContent = option.textContent;
|
||||
html.classList.add(selectedTheme);
|
||||
|
||||
if (selectedThemeScheme === 'light') {
|
||||
html.classList.remove('dark-scheme');
|
||||
html.classList.add('light-scheme');
|
||||
} else if (selectedThemeScheme === 'dark') {
|
||||
html.classList.add('dark-scheme');
|
||||
html.classList.remove('light-scheme');
|
||||
}
|
||||
|
||||
document.querySelector('[name=color-scheme]').setAttribute('content', selectedThemeScheme);
|
||||
localStorage.setItem('theme', JSON.stringify({
|
||||
theme: selectedTheme,
|
||||
themeScheme: selectedThemeScheme
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function setupPage() {
|
||||
const pageElement = document.getElementById("page");
|
||||
const pageContentElement = document.getElementById("page-content");
|
||||
|
@ -657,6 +776,7 @@ async function setupPage() {
|
|||
pageContentElement.innerHTML = pageContent;
|
||||
|
||||
try {
|
||||
setupThemeSwitcher();
|
||||
setupPopovers();
|
||||
setupClocks()
|
||||
await setupCalendars();
|
||||
|
|
|
@ -56,6 +56,28 @@
|
|||
--font-size-h6: 1.1rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--scheme: ;
|
||||
--bgh: 240;
|
||||
--bgs: 8%;
|
||||
--bgl: 9%;
|
||||
--bghs: var(--bgh), var(--bgs);
|
||||
--cm: 1;
|
||||
--tsm: 1;
|
||||
}
|
||||
|
||||
.light {
|
||||
--scheme: 100% -;
|
||||
--bgh: 240;
|
||||
--bgs: 50%;
|
||||
--bgl: 98%;
|
||||
--bghs: var(--bgh), var(--bgs);
|
||||
--cm: 1;
|
||||
--tsm: 1;
|
||||
--color-primary: hsl(43, 50%, 70%);
|
||||
}
|
||||
|
||||
|
||||
.light-scheme {
|
||||
--scheme: 100% -;
|
||||
}
|
||||
|
@ -1844,7 +1866,6 @@ details[open] .summary::after {
|
|||
padding: 15px var(--content-bounds-padding);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
@ -2095,3 +2116,83 @@ details[open] .summary::after {
|
|||
.list-gap-20 { --list-half-gap: 1rem; }
|
||||
.list-gap-24 { --list-half-gap: 1.2rem; }
|
||||
.list-gap-34 { --list-half-gap: 1.7rem; }
|
||||
|
||||
/*
|
||||
### Theme Dropdown ###
|
||||
*/
|
||||
.theme-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.dropdown-button {
|
||||
padding: 10px 15px;
|
||||
background: var(--color-widget-background);
|
||||
border: 1px solid var(--color-widget-content-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 150px;
|
||||
transition: border-color .2s;
|
||||
color: var(--color-text-highlight);
|
||||
}
|
||||
|
||||
.dropdown-button:hover {
|
||||
border-color: var(--color-text-subdue);
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background: var(--color-widget-content-border);
|
||||
min-width: 150px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
border-radius: 4px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.mobile-navigation-page-links .dropdown-content {
|
||||
top: unset;
|
||||
bottom: 38px;
|
||||
}
|
||||
|
||||
.dropdown-content.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.theme-option {
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.theme-option:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: #dee2e6;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
border: solid #666;
|
||||
border-width: 0 2px 2px 0;
|
||||
display: inline-block;
|
||||
padding: 3px;
|
||||
transform: rotate(45deg);
|
||||
transition: transform 0.2s ease;
|
||||
margin-left: auto;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.dropdown-button.active .arrow {
|
||||
transform: rotate(-135deg);
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
<title>{{ block "document-title" . }}{{ end }}</title>
|
||||
<script>if (navigator.platform === 'iPhone') document.documentElement.classList.add('ios');</script>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<meta name="color-scheme" content="light">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
|
|
@ -8,6 +8,11 @@
|
|||
slug: "{{ .Page.Slug }}",
|
||||
baseURL: "{{ .App.Config.Server.BaseURL }}",
|
||||
};
|
||||
|
||||
/**
|
||||
* @type ThemeCollection
|
||||
*/
|
||||
const userThemes = JSON.parse("{{ .Presets }}");
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
|
@ -29,6 +34,23 @@
|
|||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "theme-switcher" }}
|
||||
<div class="theme-dropdown">
|
||||
<button class="dropdown-button">
|
||||
<span>Theme</span>
|
||||
<i class="arrow"></i>
|
||||
</button>
|
||||
<div class="dropdown-content">
|
||||
<div class="theme-option" data-theme="default" data-scheme="dark">default</div>
|
||||
<div class="theme-option" data-theme="light" data-scheme="light">light</div>
|
||||
<div class="theme-option" data-theme="dark" data-scheme="dark">dark</div>
|
||||
<div class="separator"></div>
|
||||
<div class="custom-presets">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ define "document-body" }}
|
||||
<div class="flex flex-column body-content">
|
||||
{{ if not .Page.HideDesktopNavigation }}
|
||||
|
@ -39,6 +61,9 @@
|
|||
<div class="nav flex grow">
|
||||
{{ template "navigation-links" . }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
{{ template "theme-switcher" . }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
@ -52,7 +77,12 @@
|
|||
<label class="mobile-navigation-label"><input type="checkbox" class="mobile-navigation-page-links-input" autocomplete="on"{{ if .Page.ExpandMobilePageNavigation }} checked{{ end }}><div class="hamburger-icon"></div></label>
|
||||
</div>
|
||||
<div class="mobile-navigation-page-links">
|
||||
{{ template "navigation-links" . }}
|
||||
<div class="flex grow">
|
||||
{{ template "navigation-links" . }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
{{ template "theme-switcher" . }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,14 +1,31 @@
|
|||
<style>
|
||||
:root {
|
||||
.default {
|
||||
{{ if .BackgroundColor }}
|
||||
--bgh: {{ .BackgroundColor.Hue }};
|
||||
--bgs: {{ .BackgroundColor.Saturation }}%;
|
||||
--bgl: {{ .BackgroundColor.Lightness }}%;
|
||||
{{ end }}
|
||||
|
||||
{{ if ne 0.0 .ContrastMultiplier }}--cm: {{ .ContrastMultiplier }};{{ end }}
|
||||
{{ if ne 0.0 .TextSaturationMultiplier }}--tsm: {{ .TextSaturationMultiplier }};{{ end }}
|
||||
{{ if .PrimaryColor }}--color-primary: {{ .PrimaryColor.String | safeCSS }};{{ end }}
|
||||
{{ if .PositiveColor }}--color-positive: {{ .PositiveColor.String | safeCSS }};{{ end }}
|
||||
{{ if .NegativeColor }}--color-negative: {{ .NegativeColor.String | safeCSS }};{{ end }}
|
||||
}
|
||||
</style>
|
||||
|
||||
{{ range $name,$theme := .Presets }}
|
||||
.{{ $name }} {
|
||||
{{ if .BackgroundColor }}
|
||||
--bgh: {{ $theme.BackgroundColor.Hue }};
|
||||
--bgs: {{ $theme.BackgroundColor.Saturation }}%;
|
||||
--bgl: {{ $theme.BackgroundColor.Lightness }}%;
|
||||
{{ end }}
|
||||
|
||||
{{ if ne 0.0 $theme.ContrastMultiplier }}--cm: {{ $theme.ContrastMultiplier }};{{ end }}
|
||||
{{ if ne 0.0 $theme.TextSaturationMultiplier }}--tsm: {{ $theme.TextSaturationMultiplier }};{{ end }}
|
||||
{{ if $theme.PrimaryColor }}--color-primary: {{ $theme.PrimaryColor.String | safeCSS }};{{ end }}
|
||||
{{ if $theme.PositiveColor }}--color-positive: {{ $theme.PositiveColor.String | safeCSS }};{{ end }}
|
||||
{{ if $theme.NegativeColor }}--color-negative: {{ $theme.NegativeColor.String | safeCSS }};{{ end }}
|
||||
}
|
||||
{{ end }}
|
||||
</style>
|
Loading…
Add table
Reference in a new issue