From 43b8f8f31b2201d5eec96641646e7c3c511e45f0 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Thu, 20 Mar 2025 13:08:39 +0000 Subject: [PATCH] Split CSS into multiple files This shouldn't result in anything breaking, will require thorough testing --- internal/glance/config.go | 2 +- internal/glance/embed.go | 90 + internal/glance/glance.go | 18 +- internal/glance/static/css/forum-posts.css | 19 + internal/glance/static/css/main.css | 63 + internal/glance/static/css/mobile.css | 223 ++ internal/glance/static/css/popover.css | 65 + internal/glance/static/css/site.css | 295 +++ internal/glance/static/css/utils.css | 561 +++++ .../glance/static/css/widget-bookmarks.css | 31 + .../glance/static/css/widget-calendar.css | 71 + internal/glance/static/css/widget-clock.css | 7 + .../glance/static/css/widget-dns-stats.css | 120 + .../static/css/widget-docker-containers.css | 26 + internal/glance/static/css/widget-group.css | 49 + internal/glance/static/css/widget-markets.css | 13 + internal/glance/static/css/widget-monitor.css | 36 + internal/glance/static/css/widget-reddit.css | 22 + .../glance/static/css/widget-releases.css | 6 + internal/glance/static/css/widget-rss.css | 56 + internal/glance/static/css/widget-search.css | 79 + .../glance/static/css/widget-server-stats.css | 81 + internal/glance/static/css/widget-twitch.css | 47 + internal/glance/static/css/widget-videos.css | 13 + internal/glance/static/css/widget-weather.css | 139 ++ internal/glance/static/css/widgets.css | 88 + internal/glance/static/main.css | 2115 ----------------- internal/glance/templates/document.html | 8 +- 28 files changed, 2222 insertions(+), 2121 deletions(-) create mode 100644 internal/glance/static/css/forum-posts.css create mode 100644 internal/glance/static/css/main.css create mode 100644 internal/glance/static/css/mobile.css create mode 100644 internal/glance/static/css/popover.css create mode 100644 internal/glance/static/css/site.css create mode 100644 internal/glance/static/css/utils.css create mode 100644 internal/glance/static/css/widget-bookmarks.css create mode 100644 internal/glance/static/css/widget-calendar.css create mode 100644 internal/glance/static/css/widget-clock.css create mode 100644 internal/glance/static/css/widget-dns-stats.css create mode 100644 internal/glance/static/css/widget-docker-containers.css create mode 100644 internal/glance/static/css/widget-group.css create mode 100644 internal/glance/static/css/widget-markets.css create mode 100644 internal/glance/static/css/widget-monitor.css create mode 100644 internal/glance/static/css/widget-reddit.css create mode 100644 internal/glance/static/css/widget-releases.css create mode 100644 internal/glance/static/css/widget-rss.css create mode 100644 internal/glance/static/css/widget-search.css create mode 100644 internal/glance/static/css/widget-server-stats.css create mode 100644 internal/glance/static/css/widget-twitch.css create mode 100644 internal/glance/static/css/widget-videos.css create mode 100644 internal/glance/static/css/widget-weather.css create mode 100644 internal/glance/static/css/widgets.css delete mode 100644 internal/glance/static/main.css diff --git a/internal/glance/config.go b/internal/glance/config.go index 779713d..f28548b 100644 --- a/internal/glance/config.go +++ b/internal/glance/config.go @@ -113,7 +113,7 @@ var configVariablePattern = regexp.MustCompile(`(^|.)\$\{(?:([a-zA-Z]+):)?([a-zA // ${API_KEY} - gets replaced with the value of the API_KEY environment variable // \${API_KEY} - escaped, gets used as is without the \ in the config // ${secret:api_key} - value gets loaded from /run/secrets/api_key -// ${loadFileFromEnv:PATH_TO_SECRET} - value gets loaded from the file path specified in the environment variable PATH_TO_SECRET +// ${readFileFromEnv:PATH_TO_SECRET} - value gets loaded from the file path specified in the environment variable PATH_TO_SECRET // // TODO: don't match against commented out sections, not sure exactly how since // variables can be placed anywhere and used to modify the YAML structure itself diff --git a/internal/glance/embed.go b/internal/glance/embed.go index 7bb07c9..e9ed009 100644 --- a/internal/glance/embed.go +++ b/internal/glance/embed.go @@ -1,13 +1,19 @@ package glance import ( + "bytes" "crypto/md5" "embed" "encoding/hex" + "errors" + "fmt" "io" "io/fs" "log" + "path/filepath" + "regexp" "strconv" + "strings" "time" ) @@ -20,6 +26,19 @@ var _templateFS embed.FS var staticFS, _ = fs.Sub(_staticFS, "static") var templateFS, _ = fs.Sub(_templateFS, "templates") +func readAllFromStaticFS(path string) ([]byte, error) { + // For some reason fs.FS only works with forward slashes, so in case we're + // running on Windows or pass paths with backslashes we need to replace them. + path = strings.ReplaceAll(path, "\\", "/") + + file, err := staticFS.Open(path) + if err != nil { + return nil, err + } + + return io.ReadAll(file) +} + var staticFSHash = func() string { hash, err := computeFSHash(staticFS) if err != nil { @@ -60,3 +79,74 @@ func computeFSHash(files fs.FS) (string, error) { return hex.EncodeToString(hash.Sum(nil))[:10], nil } + +var cssImportPattern = regexp.MustCompile(`(?m)^@import "(.*?)";$`) +var cssSingleLineCommentPattern = regexp.MustCompile(`(?m)^\s*\/\*.*?\*\/$`) +var whitespaceAtBeginningOfLinePattern = regexp.MustCompile(`(?m)^\s+`) + +// Yes, we bundle at runtime, give comptime pls +var bundledCSSContents = func() []byte { + const mainFilePath = "css/main.css" + + var recursiveParseImports func(path string, depth int) ([]byte, error) + recursiveParseImports = func(path string, depth int) ([]byte, error) { + if depth > 20 { + return nil, errors.New("maximum import depth reached, is one of your imports circular?") + } + + mainFileContents, err := readAllFromStaticFS(path) + if err != nil { + return nil, err + } + + // Normalize line endings, otherwise the \r's make the regex not match + mainFileContents = bytes.ReplaceAll(mainFileContents, []byte("\r\n"), []byte("\n")) + + mainFileDir := filepath.Dir(path) + var importLastErr error + + parsed := cssImportPattern.ReplaceAllFunc(mainFileContents, func(match []byte) []byte { + if importLastErr != nil { + return nil + } + + matches := cssImportPattern.FindSubmatch(match) + if len(matches) != 2 { + importLastErr = fmt.Errorf( + "import didn't return expected number of capture groups: %s, expected 2, got %d", + match, len(matches), + ) + return nil + } + + importFilePath := filepath.Join(mainFileDir, string(matches[1])) + importContents, err := recursiveParseImports(importFilePath, depth+1) + if err != nil { + importLastErr = err + return nil + } + + return importContents + }) + + if importLastErr != nil { + return nil, importLastErr + } + + return parsed, nil + } + + contents, err := recursiveParseImports(mainFilePath, 0) + if err != nil { + panic(fmt.Sprintf("building CSS bundle: %v", err)) + } + + // We could strip a bunch more unnecessary characters, but the biggest + // win comes from removing the whitepsace at the beginning of lines + // since that's at least 4 bytes per property, which yielded a ~20% reduction. + contents = cssSingleLineCommentPattern.ReplaceAll(contents, nil) + contents = whitespaceAtBeginningOfLinePattern.ReplaceAll(contents, nil) + contents = bytes.ReplaceAll(contents, []byte("\n"), []byte("")) + + return contents +}() diff --git a/internal/glance/glance.go b/internal/glance/glance.go index 8fb3e40..5419176 100644 --- a/internal/glance/glance.go +++ b/internal/glance/glance.go @@ -20,6 +20,8 @@ var ( pageThemeStyleTemplate = mustParseTemplate("theme-style.gotmpl") ) +const STATIC_ASSETS_CACHE_DURATION = 24 * time.Hour + type application struct { Version string Config config @@ -230,9 +232,23 @@ func (a *application) server() (func() error, func() error) { mux.Handle( fmt.Sprintf("GET /static/%s/{path...}", staticFSHash), - http.StripPrefix("/static/"+staticFSHash, fileServerWithCache(http.FS(staticFS), 24*time.Hour)), + http.StripPrefix( + "/static/"+staticFSHash, + fileServerWithCache(http.FS(staticFS), STATIC_ASSETS_CACHE_DURATION), + ), ) + cssBundleCacheControlValue := fmt.Sprintf( + "public, max-age=%d", + int(STATIC_ASSETS_CACHE_DURATION.Seconds()), + ) + + mux.HandleFunc(fmt.Sprintf("GET /static/%s/css/bundle.css", staticFSHash), func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Cache-Control", cssBundleCacheControlValue) + w.Header().Add("Content-Type", "text/css; charset=utf-8") + w.Write(bundledCSSContents) + }) + var absAssetsPath string if a.Config.Server.AssetsPath != "" { absAssetsPath, _ = filepath.Abs(a.Config.Server.AssetsPath) diff --git a/internal/glance/static/css/forum-posts.css b/internal/glance/static/css/forum-posts.css new file mode 100644 index 0000000..e58ac6e --- /dev/null +++ b/internal/glance/static/css/forum-posts.css @@ -0,0 +1,19 @@ +.forum-post-list-thumbnail { + flex-shrink: 0; + width: 6rem; + height: 4.1rem; + border-radius: var(--border-radius); + object-fit: cover; + border: 1px solid var(--color-separator); + margin-top: 0.1rem; +} + +.forum-post-tags-container { + transform: translateY(-0.15rem); +} + +@container widget (max-width: 550px) { + .forum-post-autohide { + display: none; + } +} diff --git a/internal/glance/static/css/main.css b/internal/glance/static/css/main.css new file mode 100644 index 0000000..4bb3096 --- /dev/null +++ b/internal/glance/static/css/main.css @@ -0,0 +1,63 @@ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('../fonts/JetBrainsMono-Regular.woff2') format('woff2'); +} + +:root { + font-size: 10px; + + --scheme: ; + --bgh: 240; + --bgs: 8%; + --bgl: 9%; + --bghs: var(--bgh), var(--bgs); + --cm: 1; + --tsm: 1; + + --widget-gap: 23px; + --widget-content-vertical-padding: 15px; + --widget-content-horizontal-padding: 17px; + --widget-content-padding: var(--widget-content-vertical-padding) var(--widget-content-horizontal-padding); + --content-bounds-padding: 15px; + --border-radius: 5px; + --mobile-navigation-height: 50px; + + --color-primary: hsl(43, 50%, 70%); + --color-positive: var(--color-primary); + --color-negative: hsl(0, 70%, 70%); + --color-background: hsl(var(--bghs), var(--bgl)); + --color-widget-background-hsl-values: var(--bghs), calc(var(--bgl) + 1%); + --color-widget-background: hsl(var(--color-widget-background-hsl-values)); + --color-separator: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 4% * var(--cm)))); + --color-widget-content-border: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 4%))); + --color-widget-background-highlight: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 4%))); + --color-popover-background: hsl(var(--bgh), calc(var(--bgs) + 3%), calc(var(--bgl) + 3%)); + --color-popover-border: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 12%))); + --color-progress-border: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 10% * var(--cm)))); + --color-progress-value: hsl(var(--bgh), calc(var(--bgs) * var(--tsm)), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 30% * var(--cm)))); + --color-graph-gridlines: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 6% * var(--cm)))); + + --ths: var(--bgh), calc(var(--bgs) * var(--tsm)); + --color-text-highlight: hsl(var(--ths), calc(var(--scheme) var(--cm) * 85%)); + --color-text-paragraph: hsl(var(--ths), calc(var(--scheme) var(--cm) * 73%)); + --color-text-base: hsl(var(--ths), calc(var(--scheme) var(--cm) * 58%)); + --color-text-base-muted: hsl(var(--ths), calc(var(--scheme) var(--cm) * 52%)); + --color-text-subdue: hsl(var(--ths), calc(var(--scheme) var(--cm) * 35%)); + + --font-size-h1: 1.7rem; + --font-size-h2: 1.6rem; + --font-size-h3: 1.5rem; + --font-size-h4: 1.4rem; + --font-size-base: 1.3rem; + --font-size-h5: 1.2rem; + --font-size-h6: 1.1rem; +} + +@import "site.css"; +@import "widgets.css"; +@import "popover.css"; +@import "mobile.css"; +@import "utils.css"; diff --git a/internal/glance/static/css/mobile.css b/internal/glance/static/css/mobile.css new file mode 100644 index 0000000..da5024f --- /dev/null +++ b/internal/glance/static/css/mobile.css @@ -0,0 +1,223 @@ +@media (max-width: 1190px) { + .header-container { + display: none; + } + + .page-column-small .size-title-dynamic { + font-size: var(--font-size-h3); + } + + .page-column-small { + width: 100%; + flex-shrink: 1; + } + + .page-column { + display: none; + animation: columnEntrance .0s cubic-bezier(0.25, 1, 0.5, 1) backwards; + } + + .page-columns-transitioned .page-column { + animation-duration: .3s; + } + + @keyframes columnEntrance { + from { + opacity: 0; + transform: scaleX(0.95); + } + } + + .mobile-navigation-offset { + height: var(--mobile-navigation-height); + flex-shrink: 0; + } + + .mobile-navigation { + display: block; + position: fixed; + bottom: 0; + transform: translateY(calc(100% - var(--mobile-navigation-height))); + left: var(--content-bounds-padding); + right: var(--content-bounds-padding); + z-index: 10; + background-color: var(--color-widget-background); + border: 1px solid var(--color-widget-content-border); + border-bottom: 0; + border-radius: var(--border-radius) var(--border-radius) 0 0; + transition: transform .3s; + } + + .mobile-navigation:has(.mobile-navigation-page-links-input:checked) .hamburger-icon { + --spacing: 7px; + color: var(--color-primary); + height: 2px; + } + + .mobile-navigation:has(.mobile-navigation-page-links-input:checked) { + transform: translateY(0); + } + + .mobile-navigation-page-links { + border-top: 1px solid var(--color-widget-content-border); + padding: 15px var(--content-bounds-padding); + display: flex; + align-items: center; + overflow-x: auto; + scrollbar-width: thin; + gap: 2.5rem; + } + + .mobile-navigation-icons { + display: flex; + justify-content: space-around; + align-items: center; + } + + body:has(.mobile-navigation-input[value="0"]:checked) .page-columns > :nth-child(1), + body:has(.mobile-navigation-input[value="1"]:checked) .page-columns > :nth-child(2), + body:has(.mobile-navigation-input[value="2"]:checked) .page-columns > :nth-child(3) { + display: block; + } + + .mobile-navigation-label { + display: flex; + flex: 1; + max-width: 50px; + height: var(--mobile-navigation-height); + justify-content: center; + align-items: center; + cursor: pointer; + font-size: 15px; + line-height: var(--mobile-navigation-height); + } + + .mobile-navigation-pill { + display: block; + background: var(--color-text-base); + height: 10px; + width: 10px; + border-radius: 10px; + transition: width .3s, background-color .3s; + } + + .mobile-navigation-label:hover > .mobile-navigation-pill { + background-color: var(--color-text-highlight); + } + + .mobile-navigation-label:hover { + color: var(--color-text-highlight); + } + + .mobile-navigation-input:checked + .mobile-navigation-pill { + background: var(--color-primary); + width: 30px; + } + + .mobile-navigation-input, .mobile-navigation-page-links-input { + display: none; + } + + .hamburger-icon { + --spacing: 4px; + width: 1em; + height: 1px; + background-color: currentColor; + transition: color .3s, box-shadow .3s; + box-shadow: 0 calc(var(--spacing) * -1) 0 0 currentColor, 0 var(--spacing) 0 0 currentColor; + } + + .expand-toggle-button.container-expanded { + bottom: var(--mobile-navigation-height); + } + + .cards-grid + .expand-toggle-button.container-expanded { + /* hides content that peeks through the rounded borders of the mobile navigation */ + box-shadow: 0 var(--border-radius) 0 0 var(--color-background); + } + + .weather-column-rain::before { + background-size: 7px 7px; + } + + .ios .search-input { + /* so that iOS Safari does not zoom the page when the input is focused */ + font-size: 16px; + } +} + +@media (max-width: 1190px) and (display-mode: standalone) { + :root { + --safe-area-inset-bottom: env(safe-area-inset-bottom, 0); + } + + .ios .body-content { + height: 100dvh; + } + + .expand-toggle-button.container-expanded { + bottom: calc(var(--mobile-navigation-height) + var(--safe-area-inset-bottom)); + } + + .mobile-navigation { + transform: translateY(calc(100% - var(--mobile-navigation-height) - var(--safe-area-inset-bottom))); + padding-bottom: var(--safe-area-inset-bottom); + } + + .mobile-navigation-icons { + padding-bottom: var(--safe-area-inset-bottom); + transition: padding-bottom .3s; + } + + .mobile-navigation-offset { + height: calc(var(--mobile-navigation-height) + var(--safe-area-inset-bottom)); + } + + .mobile-navigation-icons:has(.mobile-navigation-page-links-input:checked) { + padding-bottom: 0; + } +} + +@media (display-mode: standalone) { + body { + padding-top: env(safe-area-inset-top, 0); + } +} + +@media (max-width: 550px) { + :root { + font-size: 9px; + --widget-gap: 15px; + --widget-content-vertical-padding: 10px; + --widget-content-horizontal-padding: 10px; + --content-bounds-padding: 10px; + } + + .dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; } + + .row-reverse-on-mobile { + flex-direction: row-reverse; + } + + .hide-on-mobile, .thumbnail-container:has(> .hide-on-mobile) { + display: none + } + + .mobile-reachability-header { + display: block; + font-size: 3rem; + padding: 10vh 1rem; + text-align: center; + color: var(--color-text-highlight); + animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards; + } + + .rss-detailed-thumbnail > * { + height: 6rem; + } + + .rss-detailed-description { + line-clamp: 3; + -webkit-line-clamp: 3; + } +} diff --git a/internal/glance/static/css/popover.css b/internal/glance/static/css/popover.css new file mode 100644 index 0000000..232f29c --- /dev/null +++ b/internal/glance/static/css/popover.css @@ -0,0 +1,65 @@ +.popover-container, [data-popover-html] { + display: none; +} + +.popover-container { + --triangle-size: 10px; + --triangle-offset: 50%; + --triangle-margin: calc(var(--triangle-size) + 3px); + --entrance-y-offset: 8px; + --entrance-direction: calc(var(--entrance-y-offset) * -1); + + z-index: 20; + position: absolute; + padding-top: var(--triangle-margin); + padding-inline: var(--content-bounds-padding); +} + +.popover-container.position-above { + --entrance-direction: var(--entrance-y-offset); + padding-top: 0; + padding-bottom: var(--triangle-margin); +} + +.popover-frame { + --shadow-properties: 0 15px 20px -10px; + --shadow-color: hsla(var(--bghs), calc(var(--bgl) * 0.2), 0.5); + position: relative; + padding: 10px; + background: var(--color-popover-background); + border: 1px solid var(--color-popover-border); + border-radius: 5px; + animation: popoverFrameEntrance 0.3s backwards cubic-bezier(0.16, 1, 0.3, 1); + box-shadow: var(--shadow-properties) var(--shadow-color); +} + +.popover-frame::before { + content: ''; + position: absolute; + width: var(--triangle-size); + height: var(--triangle-size); + transform: rotate(45deg); + background-color: var(--color-popover-background); + border-top-left-radius: 2px; + border-left: 1px solid var(--color-popover-border); + border-top: 1px solid var(--color-popover-border); + left: calc(var(--triangle-offset) - (var(--triangle-size) / 2)); + top: calc(var(--triangle-size) / 2 * -1 - 1px); +} + +.popover-container.position-above .popover-frame::before { + transform: rotate(-135deg); + top: auto; + bottom: calc(var(--triangle-size) / 2 * -1 - 1px); +} + +.popover-container.position-above .popover-frame { + --shadow-properties: 0 10px 20px -10px; +} + +@keyframes popoverFrameEntrance { + from { + opacity: 0; + transform: translateY(var(--entrance-direction)); + } +} diff --git a/internal/glance/static/css/site.css b/internal/glance/static/css/site.css new file mode 100644 index 0000000..4604fc6 --- /dev/null +++ b/internal/glance/static/css/site.css @@ -0,0 +1,295 @@ +.light-scheme { + --scheme: 100% -; +} + +.page { + height: 100%; + padding-block: var(--widget-gap); +} + +.page-content, .page.content-ready .page-loading-container { + display: none; +} + +.page.content-ready > .page-content { + display: block; +} + +.page-column-small .size-title-dynamic { + font-size: var(--font-size-h4); +} + +.page-column-full .size-title-dynamic { + font-size: var(--font-size-h3); +} + +pre { + font: inherit; +} + +::selection { + background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 20%))); + color: var(--color-text-highlight); +} + +::-webkit-scrollbar-thumb { + background: var(--color-text-subdue); + border-radius: var(--border-radius); +} + +::-webkit-scrollbar { + background: var(--color-background); + height: 5px; + width: 10px; +} + +*:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 0.1rem; + border-radius: var(--border-radius); +} + +*, *::before, *::after { + box-sizing: border-box; +} + +* { + padding: 0; + margin: 0; +} + +hr { + border: 0; + height: 1px; + background-color: var(--color-separator); +} + +img, svg { + display: block; + max-width: 100%; +} + +img[loading=lazy].loaded:not(.finished-transition) { + transition: opacity .4s; +} + +img[loading=lazy].cached:not(.finished-transition) { + transition: none; +} + +img[loading=lazy]:not(.loaded, .cached) { + opacity: 0; +} + +html { + scrollbar-color: var(--color-text-subdue) transparent; + scroll-behavior: smooth; +} + +html, body, .body-content { + height: 100%; +} + +h1, h2, h3, h4, h5 { + font: inherit; +} + +a { + text-decoration: none; + color: inherit; + overflow-wrap: break-word; +} + +ul { + list-style: none; +} + +body { + font-size: 1.3rem; + font-family: 'JetBrains Mono', monospace; + font-variant-ligatures: none; + line-height: 1.6; + color: var(--color-text-base); + background-color: var(--color-background); + overflow-y: scroll; +} + +.page-column-small { + width: 300px; + flex-shrink: 0; +} + +.page-column-full { + width: 100%; + min-width: 0; +} + +.page-columns { + display: flex; + gap: var(--widget-gap); + animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards; +} + +@keyframes pageColumnsEntrance { + from { + opacity: 0; + transform: translateY(10px); + } +} + +.page-loading-container { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + animation: loadingContainerEntrance 200ms backwards; + animation-delay: 150ms; + font-size: 2rem; +} + +.page-loading-container > .loading-icon { + translate: 0 -250%; +} + +@keyframes loadingContainerEntrance { + from { + opacity: 0; + } +} + +.loading-icon { + min-width: 1.5em; + width: 1.5em; + height: 1.5em; + border: 0.25em solid hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 12%))); + border-top-color: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 40%))); + border-radius: 50%; + animation: loadingIconSpin 800ms infinite linear; +} + +@keyframes loadingIconSpin { + to { + transform: rotate(360deg); + } +} + +.notice-icon { + width: 0.7rem; + height: 0.7rem; + border-radius: 50%; +} + +.notice-icon-major { + background: var(--color-negative); +} + +.notice-icon-minor { + border: 1px solid var(--color-negative); +} + +kbd { + font: inherit; + padding: 0.1rem 0.8rem; + border-radius: var(--border-radius); + border: 2px solid var(--color-widget-background-highlight); + box-shadow: 0 2px 0 var(--color-widget-background-highlight); + user-select: none; + transition: transform .1s, box-shadow .1s; + font-size: var(--font-size-h5); + cursor: pointer; +} + +kbd:active { + transform: translateY(2px); + box-shadow: 0 0 0 0 var(--color-widget-background-highlight); +} + +.content-bounds { + max-width: 1600px; + width: 100%; + margin-inline: auto; + padding: 0 var(--content-bounds-padding); +} + +.page-width-wide .content-bounds { + max-width: 1920px; +} + +.page-width-slim .content-bounds { + max-width: 1100px; +} + +.page-center-vertically .page { + display: flex; + justify-content: center; + flex-direction: column; +} + +.header-container { + margin-top: calc(var(--widget-gap) / 2); + --header-height: 45px; + --header-items-gap: 2.5rem; +} + +.header { + display: flex; + height: var(--header-height); + gap: var(--header-items-gap); +} + +.logo { + height: 100%; + line-height: var(--header-height); + font-size: 2rem; + color: var(--color-text-highlight); + border-right: 1px solid var(--color-widget-content-border); + padding-right: var(--widget-content-horizontal-padding); +} + +.logo:has(img) { + display: flex; + align-items: center; +} + +.logo img { + max-height: 2.7rem; +} + +.nav { + height: 100%; + gap: var(--header-items-gap); +} + +.nav .nav-item { + line-height: var(--header-height); +} + +.footer { + padding-bottom: calc(var(--widget-gap) * 1.5); + padding-top: calc(var(--widget-gap) / 2); + animation: loadingContainerEntrance 200ms backwards; + animation-delay: 150ms; +} + +.mobile-navigation, .mobile-reachability-header { + display: none; +} + +.nav-item { + display: block; + height: 100%; + border-bottom: 2px solid transparent; + transition: color .3s, border-color .3s; + font-size: var(--font-size-h3); + flex-shrink: 0; +} + +.nav-item:not(.nav-item-current):hover { + border-bottom-color: var(--color-text-subdue); + color: var(--color-text-highlight); +} + +.nav-item.nav-item-current { + border-bottom-color: var(--color-primary); + color: var(--color-text-highlight); +} diff --git a/internal/glance/static/css/utils.css b/internal/glance/static/css/utils.css new file mode 100644 index 0000000..42aef5a --- /dev/null +++ b/internal/glance/static/css/utils.css @@ -0,0 +1,561 @@ +.masonry { + display: flex; + gap: var(--widget-gap); +} + +.masonry-column { + flex: 1; + display: flex; + flex-direction: column; +} + +.widget-small-content-bounds { + max-width: 350px; + margin: 0 auto; +} + +.visually-hidden { + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} + +.list-horizontal-text { + display: flex; + list-style: none; + flex-wrap: wrap; + align-items: center; +} + +.list-horizontal-text > *:not(:last-child)::after { + content: '•' / ""; + color: var(--color-text-subdue); + margin: 0 0.4rem; + position: relative; + top: 0.1rem; +} + +.summary { + width: 100%; + cursor: pointer; + word-spacing: -0.18em; + user-select: none; + list-style: none; + position: relative; + display: flex; + z-index: 1; +} + +.summary::-webkit-details-marker { + display: none; +} + +.details[open] .summary { + margin-bottom: .8rem; +} + +.summary::before { + content: ""; + position: absolute; + inset: -.3rem -.8rem; + border-radius: var(--border-radius); + background-color: var(--color-widget-background-highlight); + opacity: 0; + transition: opacity 0.2s; + z-index: -1; +} + +.details[open] .summary::before, .summary:hover::before { + opacity: 1; +} + +.details:not([open]) .list-with-transition { + display: none; +} + +.summary::after { + content: "◀" / ""; + font-size: 1.2em; + position: absolute; + top: 0; + bottom: 0; + line-height: 1.3em; + right: 0; + transition: rotate .5s cubic-bezier(0.22, 1, 0.36, 1); +} + +details[open] .summary::after { + rotate: -90deg; +} + +/* TODO: refactor, otherwise I hope I never have to change dynamic columns again */ +.dynamic-columns { + --list-half-gap: 0.5rem; + gap: var(--widget-content-vertical-padding) var(--widget-content-horizontal-padding); + display: grid; + grid-template-columns: repeat(var(--columns-per-row), 1fr); +} + +.dynamic-columns > * { + padding-left: var(--widget-content-horizontal-padding); + border-left: 1px solid var(--color-separator); + min-width: 0; +} + +.dynamic-columns > *:first-child { + padding-top: 0; + border-top: none; + border-left: none; +} + +.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; } +.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; } +.dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; } +.dynamic-columns:has(> :nth-child(4)) { --columns-per-row: 4; } +.dynamic-columns:has(> :nth-child(5)) { --columns-per-row: 5; } + +@container widget (max-width: 599px) { + .dynamic-columns { gap: 0; } + .dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; } + .dynamic-columns > * { + border-left: none; + padding-left: 0; + } + .dynamic-columns > *:not(:first-child) { + margin-top: calc(var(--list-half-gap) * 2); + } + .dynamic-columns.list-with-separator > *:not(:first-child) { + margin-top: var(--list-half-gap); + border-top: 1px solid var(--color-separator); + padding-top: var(--list-half-gap); + } +} +@container widget (min-width: 600px) and (max-width: 849px) { + .dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; } + .dynamic-columns > :nth-child(2n-1) { + border-left: none; + padding-left: 0; + } +} +@container widget (min-width: 850px) and (max-width: 1249px) { + .dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; } + .dynamic-columns > :nth-child(3n+1) { + border-left: none; + padding-left: 0; + } +} +@container widget (min-width: 1250px) and (max-width: 1499px) { + .dynamic-columns:has(> :nth-child(4)) { --columns-per-row: 4; } + .dynamic-columns > :nth-child(4n+1) { + border-left: none; + padding-left: 0; + } +} +@container widget (min-width: 1500px) { + .dynamic-columns:has(> :nth-child(5)) { --columns-per-row: 5; } + .dynamic-columns > :nth-child(5n+1) { + border-left: none; + padding-left: 0; + } +} + +.cards-vertical { + flex-direction: column; +} + +.cards-horizontal { + --cards-per-row: 6.5; +} + +.cards-horizontal, .cards-vertical { + --cards-gap: calc(var(--widget-content-vertical-padding) * 0.7); + display: flex; + gap: var(--cards-gap); +} + +.card { + display: flex; + flex-direction: column; +} + +.cards-horizontal .card { + flex-shrink: 0; + width: calc(100% / var(--cards-per-row) - var(--cards-gap) * (var(--cards-per-row) - 1) / var(--cards-per-row)); +} + +.cards-grid .card { + min-width: 0; +} + +.cards-horizontal { + overflow-x: auto; + scrollbar-width: thin; + padding-bottom: 1rem; +} + +.cards-grid { + --cards-per-row: 6; + display: grid; + grid-template-columns: repeat(var(--cards-per-row), 1fr); + gap: calc(var(--widget-content-vertical-padding) * 0.7); +} + +@container widget (max-width: 1300px) { .cards-horizontal { --cards-per-row: 5.5; } } +@container widget (max-width: 1100px) { .cards-horizontal { --cards-per-row: 4.5; } } +@container widget (max-width: 850px) { .cards-horizontal { --cards-per-row: 3.5; } } +@container widget (max-width: 750px) { .cards-horizontal { --cards-per-row: 3.5; } } +@container widget (max-width: 650px) { .cards-horizontal { --cards-per-row: 2.5; } } +@container widget (max-width: 450px) { .cards-horizontal { --cards-per-row: 2.3; } } + +@container widget (max-width: 1300px) { .cards-grid { --cards-per-row: 5; } } +@container widget (max-width: 1100px) { .cards-grid { --cards-per-row: 4; } } +@container widget (max-width: 850px) { .cards-grid { --cards-per-row: 3; } } +@container widget (max-width: 750px) { .cards-grid { --cards-per-row: 3; } } +@container widget (max-width: 650px) { .cards-grid { --cards-per-row: 2; } } + +.text-truncate, +.single-line-titles .title +{ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.single-line-titles .title { + display: block; +} + +.text-truncate-2-lines, .text-truncate-3-lines { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; +} + +.text-truncate-3-lines { line-clamp: 3; -webkit-line-clamp: 3; } +.text-truncate-2-lines { line-clamp: 2; -webkit-line-clamp: 2; } + +.visited-indicator:not(.text-truncate)::after, +.visited-indicator.text-truncate::before { + content: '↗' / ""; + margin-left: 0.5em; + display: inline-block; + position: relative; + top: 0.15em; + color: var(--color-text-base); +} + +.visited-indicator.text-truncate { + direction: rtl; + text-align: left; +} + +.visited-indicator:not(:visited)::before, .visited-indicator:not(:visited)::after { + color: var(--color-primary); +} + +.page-columns-transitioned .list-with-transition > * { animation: collapsibleItemReveal .25s backwards; } +.list-with-transition > *:nth-child(2) { animation-delay: 30ms; } +.list-with-transition > *:nth-child(3) { animation-delay: 60ms; } +.list-with-transition > *:nth-child(4) { animation-delay: 90ms; } +.list-with-transition > *:nth-child(5) { animation-delay: 120ms; } +.list-with-transition > *:nth-child(6) { animation-delay: 150ms; } +.list-with-transition > *:nth-child(7) { animation-delay: 180ms; } +.list-with-transition > *:nth-child(8) { animation-delay: 210ms; } + +.list > *:not(:first-child) { + margin-top: calc(var(--list-half-gap) * 2); +} + +.list.list-with-separator > *:not(:first-child) { + margin-top: var(--list-half-gap); + border-top: 1px solid var(--color-separator); + padding-top: var(--list-half-gap); +} + +.collapsible-container:not(.container-expanded) > .collapsible-item { + display: none; +} + +.collapsible-item { + animation: collapsibleItemReveal .25s backwards; +} + +@keyframes collapsibleItemReveal { + from { + opacity: 0; + transform: translateY(10px); + } +} + +.expand-toggle-button { + font: inherit; + border: 0; + cursor: pointer; + display: block; + width: 100%; + text-align: left; + color: var(--color-text-base); + text-transform: uppercase; + font-size: var(--font-size-h4); + padding: var(--widget-content-vertical-padding) 0; + background: var(--color-widget-background); +} + +.expand-toggle-button.container-expanded { + position: sticky; + /* -1px to hide 1px gap on chrome */ + bottom: -1px; +} + +.expand-toggle-button-icon { + display: inline-block; + margin-left: 1rem; + position: relative; + top: -.2rem; +} + +.expand-toggle-button-icon::before { + content: '' / ""; + font-size: 0.8rem; + transform: rotate(90deg); + line-height: 1; + display: inline-block; + transition: transform 0.3s; +} + +.expand-toggle-button.container-expanded .expand-toggle-button-icon::before { + transform: rotate(-90deg); +} + +.cards-grid.collapsible-container + .expand-toggle-button { + text-align: center; + margin-top: 0.5rem; + background-color: var(--color-background); +} + +.widget-content:has(.expand-toggle-button:last-child) { + padding-bottom: 0; +} + +.carousel-container { + position: relative; +} + +.carousel-container::before, .carousel-container::after { + content: ''; + position: absolute; + width: 2rem; + top: 0; + bottom: 1rem; + z-index: 10; + opacity: 0; + pointer-events: none; + transition-duration: 0.2s; +} + +.carousel-container::before { + background: linear-gradient(to right, var(--color-background), transparent); +} + +.carousel-container::after { + right: 0; + background: linear-gradient(to left, var(--color-background), transparent); +} + +.carousel-container.show-left-cutoff::before, .carousel-container.show-right-cutoff::after { + opacity: 1; +} + +.attachments { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +:root:not(.light-scheme) .flat-icon { + filter: invert(1); +} + +.attachments > * { + border-radius: var(--border-radius); + padding: 0.1rem 0.5rem; + font-size: var(--font-size-h6); + background-color: var(--color-separator); +} + +.progress-bar { + border: 1px solid var(--color-progress-border); + border-radius: var(--border-radius); + display: flex; + flex-direction: column; + gap: 2px; + padding: 2px; + height: 1.5rem; + /* naughty, but oh so beautiful */ + margin-inline: -3px; +} + +.progress-bar-combined { + height: 3rem; +} + +.popover-active > .progress-bar { + transition: border-color .3s; + border-color: var(--color-text-subdue); +} + +.progress-value { + --half-border-radius: calc(var(--border-radius) / 2); + border-radius: 0 var(--half-border-radius) var(--half-border-radius) 0; + background: var(--color-progress-value); + width: calc(var(--percent) * 1%); + min-width: 1px; + flex: 1; +} + +.progress-value:first-child { + border-top-left-radius: var(--half-border-radius); +} + +.progress-value:last-child { + border-bottom-left-radius: var(--half-border-radius); +} + +.progress-value-notice { + background: linear-gradient(to right, var(--color-progress-value) 65%, var(--color-negative)); +} + +.value-separator { + min-width: 2rem; + margin-inline: 0.8rem; + flex: 1; + height: calc(1em * 1.1); + border-bottom: 1px dotted var(--color-text-subdue); +} + +.thumbnail { + filter: grayscale(0.2) contrast(0.9); + opacity: 0.8; + transition: filter 0.2s, opacity .2s; +} + +.thumbnail-container { + flex-shrink: 0; + border: 1px solid var(--color-separator); + border-radius: var(--border-radius); +} + +.thumbnail-container > * { + border-radius: var(--border-radius); + object-fit: cover; +} + +.thumbnail-parent:hover .thumbnail { + opacity: 1; + filter: none; +} + +.size-h1 { font-size: var(--font-size-h1); } +.size-h2 { font-size: var(--font-size-h2); } +.size-h3 { font-size: var(--font-size-h3); } +.size-h4 { font-size: var(--font-size-h4); } +.size-base { font-size: var(--font-size-base); } +.size-h5 { font-size: var(--font-size-h5); } +.size-h6 { font-size: var(--font-size-h6); } + +.color-highlight { color: var(--color-text-highlight); } +.color-paragraph { color: var(--color-text-paragraph); } +.color-base { color: var(--color-text-base); } +.color-subdue { color: var(--color-text-subdue); } +.color-negative { color: var(--color-negative); } +.color-positive { color: var(--color-positive); } +.color-primary { color: var(--color-primary); } + +.color-primary-if-not-visited:not(:visited) { + color: var(--color-primary); +} + +.cursor-help { cursor: help; } +.break-all { word-break: break-all; } +.text-left { text-align: left; } +.text-right { text-align: right; } +.text-center { text-align: center; } +.text-elevate { margin-top: -0.2em; } +.text-compact { word-spacing: -0.18em; } +.text-very-compact { word-spacing: -0.35em; } +.rtl { direction: rtl; } +.shrink { flex-shrink: 1; } +.shrink-0 { flex-shrink: 0; } +.min-width-0 { min-width: 0; } +.max-width-100 { max-width: 100%; } +.block { display: block; } +.inline-block { display: inline-block; } +.overflow-hidden { overflow: hidden; } +.relative { position: relative; } +.flex { display: flex; } +.flex-1 { flex: 1; } +.flex-wrap { flex-wrap: wrap; } +.flex-nowrap { flex-wrap: nowrap; } +.justify-between { justify-content: space-between; } +.justify-stretch { justify-content: stretch; } +.justify-evenly { justify-content: space-evenly; } +.justify-center { justify-content: center; } +.justify-end { justify-content: end; } +.uppercase { text-transform: uppercase; } +.grow { flex-grow: 1; } +.flex-column { flex-direction: column; } +.items-center { align-items: center; } +.items-start { align-items: start; } +.items-end { align-items: end; } +.gap-5 { gap: 0.5rem; } +.gap-7 { gap: 0.7rem; } +.gap-10 { gap: 1rem; } +.gap-12 { gap: 1.2rem; } +.gap-15 { gap: 1.5rem; } +.gap-20 { gap: 2rem; } +.gap-25 { gap: 2.5rem; } +.gap-35 { gap: 3.5rem; } +.gap-45 { gap: 4.5rem; } +.gap-55 { gap: 5.5rem; } +.margin-left-auto { margin-left: auto; } +.margin-top-3 { margin-top: 0.3rem; } +.margin-top-5 { margin-top: 0.5rem; } +.margin-top-7 { margin-top: 0.7rem; } +.margin-top-10 { margin-top: 1rem; } +.margin-top-15 { margin-top: 1.5rem; } +.margin-top-20 { margin-top: 2rem; } +.margin-top-25 { margin-top: 2.5rem; } +.margin-top-35 { margin-top: 3.5rem; } +.margin-top-40 { margin-top: 4rem; } +.margin-top-auto { margin-top: auto; } +.margin-block-3 { margin-block: 0.3rem; } +.margin-block-5 { margin-block: 0.5rem; } +.margin-block-7 { margin-block: 0.7rem; } +.margin-block-8 { margin-block: 0.8rem; } +.margin-block-10 { margin-block: 1rem; } +.margin-block-15 { margin-block: 1.5rem; } +.margin-bottom-3 { margin-bottom: 0.3rem; } +.margin-bottom-5 { margin-bottom: 0.5rem; } +.margin-bottom-7 { margin-bottom: 0.7rem; } +.margin-bottom-10 { margin-bottom: 1rem; } +.margin-bottom-15 { margin-bottom: 1.5rem; } +.margin-bottom-auto { margin-bottom: auto; } +.margin-bottom-widget { margin-bottom: var(--widget-content-vertical-padding); } +.padding-widget { padding: var(--widget-content-padding); } +.padding-block-widget { padding-block: var(--widget-content-vertical-padding); } +.padding-inline-widget { padding-inline: var(--widget-content-horizontal-padding); } +.padding-block-5 { padding-block: 0.5rem; } +.scale-half { transform: scale(0.5); } +.list { --list-half-gap: 0rem; } +.list-gap-2 { --list-half-gap: 0.1rem; } +.list-gap-4 { --list-half-gap: 0.2rem; } +.list-gap-8 { --list-half-gap: 0.4rem; } +.list-gap-10 { --list-half-gap: 0.5rem; } +.list-gap-14 { --list-half-gap: 0.7rem; } +.list-gap-20 { --list-half-gap: 1rem; } +.list-gap-24 { --list-half-gap: 1.2rem; } +.list-gap-34 { --list-half-gap: 1.7rem; } diff --git a/internal/glance/static/css/widget-bookmarks.css b/internal/glance/static/css/widget-bookmarks.css new file mode 100644 index 0000000..7f2dabd --- /dev/null +++ b/internal/glance/static/css/widget-bookmarks.css @@ -0,0 +1,31 @@ +.bookmarks-group { + --bookmarks-group-color: var(--color-primary); +} + +.bookmarks-group-title { + color: var(--bookmarks-group-color); +} + +.bookmarks-link:not(.bookmarks-link-no-arrow)::after { + content: '↗' / ""; + margin-left: 0.5em; + display: inline-block; + position: relative; + top: 0.15em; + color: var(--bookmarks-group-color); +} + +.bookmarks-icon-container { + margin-block: 0.1rem; + background-color: var(--color-widget-background-highlight); + border-radius: var(--border-radius); + padding: 0.5rem; + opacity: 0.7; + flex-shrink: 0; +} + +.bookmarks-icon { + width: 20px; + height: 20px; + opacity: 0.8; +} diff --git a/internal/glance/static/css/widget-calendar.css b/internal/glance/static/css/widget-calendar.css new file mode 100644 index 0000000..f9b0d9d --- /dev/null +++ b/internal/glance/static/css/widget-calendar.css @@ -0,0 +1,71 @@ +.old-calendar-day { + width: calc(100% / 7); + text-align: center; + padding: 0.6rem 0; +} + +.old-calendar-day-today { + border-radius: var(--border-radius); + background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) (var(--bgl)) + 6%))); + color: var(--color-text-highlight); +} + +.calendar-dates { + text-align: center; + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 2px; +} + +.calendar-date { + padding: 0.4rem 0; + color: var(--color-text-base); + position: relative; + border-radius: var(--border-radius); + background: none; + border: none; + font: inherit; +} + +.calendar-current-date { + border-radius: var(--border-radius); + background-color: var(--color-popover-border); + color: var(--color-text-highlight); +} + +.calendar-spillover-date { + color: var(--color-text-subdue); +} + +.calendar-header-button { + position: relative; + cursor: pointer; + width: 2rem; + height: 2rem; + z-index: 1; + background: none; + border: none; +} + +.calendar-header-button::before { + content: ''; + position: absolute; + inset: -0.2rem; + border-radius: var(--border-radius); + background-color: var(--color-text-subdue); + opacity: 0; + transition: opacity 0.2s; + z-index: -1; +} + +.calendar-header-button:hover::before { + opacity: 0.4; +} + +.calendar-undo-button { + display: inline-block; + vertical-align: text-top; + width: 2rem; + height: 2rem; + margin-left: 0.7rem; +} diff --git a/internal/glance/static/css/widget-clock.css b/internal/glance/static/css/widget-clock.css new file mode 100644 index 0000000..47a1a2f --- /dev/null +++ b/internal/glance/static/css/widget-clock.css @@ -0,0 +1,7 @@ +.clock-time { + min-width: 8ch; +} + +.clock-time span { + color: var(--color-text-highlight); +} diff --git a/internal/glance/static/css/widget-dns-stats.css b/internal/glance/static/css/widget-dns-stats.css new file mode 100644 index 0000000..e94d349 --- /dev/null +++ b/internal/glance/static/css/widget-dns-stats.css @@ -0,0 +1,120 @@ +.dns-stats-totals { + transition: opacity .3s; + transition-delay: 50ms; +} + +.dns-stats:has(.dns-stats-graph .popover-active) .dns-stats-totals { + opacity: 0.1; + transition-delay: 0s; +} + +.dns-stats-graph { + --graph-height: 70px; + height: var(--graph-height); + position: relative; + margin-bottom: 2.5rem; +} + +.dns-stats-graph-gridlines-container { + position: absolute; + inset: 0; +} + +.dns-stats-graph-gridlines { + height: 100%; + width: 100%; +} + +.dns-stats-graph-columns { + display: flex; + height: 100%; +} + +.dns-stats-graph-column { + display: flex; + justify-content: flex-end; + align-items: center; + flex-direction: column; + width: calc(100% / 8); + position: relative; +} + +.dns-stats-graph-column::before { + content: ''; + position: absolute; + inset: 1px 0; + opacity: 0; + background: var(--color-text-base); + transition: opacity .2s; +} + +.dns-stats-graph-column:hover::before { + opacity: 0.05; +} + +.dns-stats-graph-bar { + width: 14px; + height: calc((var(--bar-height) / 100) * var(--graph-height)); + border: 1px solid var(--color-progress-border); + border-radius: var(--border-radius) var(--border-radius) 0 0; + display: flex; + background: var(--color-widget-background); + padding: 2px 2px 0 2px; + flex-direction: column; + gap: 2px; + transition: border-color .2s; + min-height: 10px; +} + +.dns-stats-graph-column.popover-active .dns-stats-graph-bar { + border-color: var(--color-text-subdue); + border-bottom-color: var(--color-progress-border); +} + +.dns-stats-graph-bar > * { + border-radius: 2px; + background: var(--color-progress-value); + min-height: 1px; +} + +.dns-stats-graph-bar > .queries { + flex-grow: 1; +} + +.dns-stats-graph-bar > *:last-child { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.dns-stats-graph-bar > .blocked { + background-color: var(--color-negative); + flex-basis: calc(var(--percent) - 1px); +} + +.dns-stats-graph-column:nth-child(even) .dns-stats-graph-time { + opacity: 1; + transform: translateY(0); +} + +.dns-stats-graph-time, .dns-stats-graph-columns:hover .dns-stats-graph-time { + position: absolute; + font-size: var(--font-size-h6); + inset-inline: 0; + text-align: center; + height: 2.5rem; + line-height: 2.5rem; + top: 100%; + user-select: none; + opacity: 0; + transform: translateY(-0.5rem); + transition: opacity .2s, transform .2s; +} + +.dns-stats-graph-column:hover .dns-stats-graph-time { + opacity: 1; + transform: translateY(0); +} + +.dns-stats-graph-columns:hover .dns-stats-graph-column:not(:hover) .dns-stats-graph-time { + opacity: 0; +} diff --git a/internal/glance/static/css/widget-docker-containers.css b/internal/glance/static/css/widget-docker-containers.css new file mode 100644 index 0000000..ae08788 --- /dev/null +++ b/internal/glance/static/css/widget-docker-containers.css @@ -0,0 +1,26 @@ +.docker-container-icon { + display: block; + filter: grayscale(0.4); + object-fit: contain; + aspect-ratio: 1 / 1; + width: 2.7rem; + opacity: 0.8; + transition: filter 0.3s, opacity 0.3s; +} + +.docker-container-icon.flat-icon { + opacity: 0.7; +} + +.docker-container:hover .docker-container-icon { + opacity: 1; +} + +.docker-container:hover .docker-container-icon:not(.flat-icon) { + filter: grayscale(0); +} + +.docker-container-status-icon { + width: 2rem; + height: 2rem; +} diff --git a/internal/glance/static/css/widget-group.css b/internal/glance/static/css/widget-group.css new file mode 100644 index 0000000..378853b --- /dev/null +++ b/internal/glance/static/css/widget-group.css @@ -0,0 +1,49 @@ +.widget-group-header { + overflow-x: auto; + scrollbar-width: thin; +} + +.widget-group-title { + background: none; + font: inherit; + border: none; + text-transform: uppercase; + border-bottom: 1px dotted transparent; + cursor: pointer; + flex-shrink: 0; + transition: color .3s, border-color .3s; + color: var(--color-text-subdue); + line-height: calc(1.6em - 1px); +} + +.widget-group-title:hover:not(.widget-group-title-current) { + color: var(--color-text-base); +} + +.widget-group-title-current { + border-bottom-color: var(--color-text-base-muted); + color: var(--color-text-base); +} + +.widget-group-content { + animation: widgetGroupContentEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards; +} + +.widget-group-content[data-direction="right"] { + --direction: 5px; +} + +.widget-group-content[data-direction="left"] { + --direction: -5px; +} + +@keyframes widgetGroupContentEntrance { + from { + opacity: 0; + transform: translateX(var(--direction)); + } +} + +.widget-group-content:not(.widget-group-content-current) { + display: none; +} diff --git a/internal/glance/static/css/widget-markets.css b/internal/glance/static/css/widget-markets.css new file mode 100644 index 0000000..f670a25 --- /dev/null +++ b/internal/glance/static/css/widget-markets.css @@ -0,0 +1,13 @@ +.market-chart { + margin-left: auto; + width: 6.5rem; + flex-shrink: 0; +} + +.market-chart svg { + width: 100%; +} + +.market-values { + min-width: 8rem; +} diff --git a/internal/glance/static/css/widget-monitor.css b/internal/glance/static/css/widget-monitor.css new file mode 100644 index 0000000..8bc629b --- /dev/null +++ b/internal/glance/static/css/widget-monitor.css @@ -0,0 +1,36 @@ +.monitor-site-icon { + display: block; + opacity: 0.8; + filter: grayscale(0.4); + object-fit: contain; + aspect-ratio: 1 / 1; + width: 3.2rem; + position: relative; + top: -0.1rem; + transition: filter 0.3s, opacity 0.3s; +} + +.monitor-site-icon.flat-icon { + opacity: 0.7; +} + +.monitor-site:hover .monitor-site-icon { + opacity: 1; +} + +.monitor-site:hover .monitor-site-icon:not(.flat-icon) { + filter: grayscale(0); +} + +.monitor-site-status-icon { + flex-shrink: 0; + margin-left: auto; + width: 2rem; + height: 2rem; +} + +.monitor-site-status-icon-compact { + width: 1.8rem; + height: 1.8rem; + flex-shrink: 0; +} diff --git a/internal/glance/static/css/widget-reddit.css b/internal/glance/static/css/widget-reddit.css new file mode 100644 index 0000000..5b62e0e --- /dev/null +++ b/internal/glance/static/css/widget-reddit.css @@ -0,0 +1,22 @@ +.reddit-card-thumbnail { + width: 100%; + height: 100%; + object-fit: cover; + object-position: 0% 20%; + opacity: 0.15; + filter: blur(1px); +} + +.reddit-card-thumbnail-container { + position: absolute; + inset: 0; + overflow: hidden; + border-radius: var(--border-radius); +} + +.reddit-card-thumbnail-container::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(0deg, var(--color-widget-background) 10%, transparent); +} diff --git a/internal/glance/static/css/widget-releases.css b/internal/glance/static/css/widget-releases.css new file mode 100644 index 0000000..7def4cf --- /dev/null +++ b/internal/glance/static/css/widget-releases.css @@ -0,0 +1,6 @@ +.release-source-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + opacity: 0.4; +} diff --git a/internal/glance/static/css/widget-rss.css b/internal/glance/static/css/widget-rss.css new file mode 100644 index 0000000..34b7114 --- /dev/null +++ b/internal/glance/static/css/widget-rss.css @@ -0,0 +1,56 @@ +.rss-card-image { + height: var(--rss-thumbnail-height, 10rem); + object-fit: cover; + border-radius: var(--border-radius) var(--border-radius) 0 0; +} + +.rss-card-2 { + position: relative; + height: var(--rss-card-height, 27rem); + overflow: hidden; +} + +.rss-card-2::before { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background-image: linear-gradient( + 0deg, + var(--color-widget-background), + hsla(var(--color-widget-background-hsl-values), 0.8) 6rem, transparent 14rem + ); + z-index: 2; +} + +.rss-card-2-image { + position: absolute; + width: 100%; + height: 100%; + object-fit: cover; + /* +1px is required to fix some weird graphical bug where the image overflows on the bottom in firefox */ + border-radius: calc(var(--border-radius) + 1px); + opacity: 0.9; + z-index: 1; +} + +.rss-card-2-content { + position: absolute; + inset-inline: 0; + bottom: var(--widget-content-vertical-padding); + z-index: 3; +} + +.rss-detailed-description { + max-width: 55rem; + color: var(--color-text-base-muted); +} + +.rss-detailed-thumbnail { + margin-top: 0.3rem; +} + +.rss-detailed-thumbnail > * { + aspect-ratio: 3 / 2; + height: 8.7rem; +} diff --git a/internal/glance/static/css/widget-search.css b/internal/glance/static/css/widget-search.css new file mode 100644 index 0000000..ebf5cbb --- /dev/null +++ b/internal/glance/static/css/widget-search.css @@ -0,0 +1,79 @@ +.search-icon { + width: 2.3rem; +} + +.search-icon-container { + position: relative; + flex-shrink: 0; +} + +/* gives a wider hit area for the 3 people that will notice the animation : ) */ +.search-icon-container::before { + content: ''; + position: absolute; + inset: -1rem; +} + +.search-icon-container:hover > .search-icon { + animation: searchIconHover 2.9s forwards; +} + +@keyframes searchIconHover { + 0%, 39% { translate: 0 0; } + 20% { scale: 1.3; } + 40% { scale: 1; } + 50% { translate: -30% 30%; } + 70% { translate: 30% -30%; } + 90% { translate: -30% -30%; } + 100% { translate: 0 0; } +} + +.search { + transition: border-color .2s; + position: relative; +} + +.search:hover { + border-color: var(--color-text-subdue); +} + +.search:focus-within { + border-color: var(--color-primary); +} + +.search-input { + border: 0; + background: none; + width: 100%; + height: 6rem; + font: inherit; + outline: none; + color: var(--color-text-highlight); +} + +.search-input::placeholder { + color: var(--color-text-base-muted); + opacity: 1; +} + +.search-bangs { display: none; } + +.search-bang { + border-radius: calc(var(--border-radius) * 2); + background: var(--color-widget-background-highlight); + padding: 0.3rem 1rem; + flex-shrink: 0; + font-size: var(--font-size-h5); + animation: searchBangsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards; +} + +@keyframes searchBangsEntrance { + 0% { + opacity: 0; + transform: translateX(-10px); + } +} + +.search-bang:empty { + display: none; +} diff --git a/internal/glance/static/css/widget-server-stats.css b/internal/glance/static/css/widget-server-stats.css new file mode 100644 index 0000000..11f9ab5 --- /dev/null +++ b/internal/glance/static/css/widget-server-stats.css @@ -0,0 +1,81 @@ +.widget-type-server-info { + position: relative; +} + +.server + .server { + margin-top: 3rem; +} + +.server { + gap: 1rem; + display: flex; + flex-direction: column; +} + +.server-info { + align-items: center; + display: flex; + justify-content: space-between; + gap: 1.5rem; + flex-shrink: 1; + min-width: 0; +} + +.server-details { + min-width: 0; +} + +.server-icon { + height: 3rem; + width: 3rem; +} + +.server-spicy-cpu-icon { + height: 1em; + align-self: center; + margin-left: 0.4em; + margin-bottom: 0.2rem; +} + +.server-stats { + display: flex; + gap: 1.5rem; + margin-top: 0.5rem; +} + +.server-stat-unavailable { + opacity: 0.5; +} + +@container widget (min-width: 650px) { + .server { + gap: 2rem; + flex-direction: row; + align-items: center; + } + + .server + .server { + margin-top: 1rem; + } + + .server-info { + flex-direction: row-reverse; + justify-content: unset; + margin-right: auto; + z-index: 1; + } + + .server-stats { + flex-direction: row; + justify-content: right; + min-width: 450px; + margin-top: 0; + gap: 2rem; + padding-bottom: 0.8rem; + z-index: 1; + } + + .server-stats > * { + max-width: 200px; + } +} diff --git a/internal/glance/static/css/widget-twitch.css b/internal/glance/static/css/widget-twitch.css new file mode 100644 index 0000000..d109756 --- /dev/null +++ b/internal/glance/static/css/widget-twitch.css @@ -0,0 +1,47 @@ +.twitch-category-thumbnail { + width: 5rem; + aspect-ratio: 3 / 4; + border-radius: var(--border-radius); +} + +.twitch-channel-avatar { + aspect-ratio: 1; + border-radius: 50%; +} + +.twitch-channel-avatar-container { + width: 4.4rem; + height: 4.4rem; + border: 2px solid var(--color-text-subdue); + padding: 2px; + border-radius: 50%; + position: relative; + flex-shrink: 0; +} + +.twitch-channel-live .twitch-channel-avatar-container { + border: 2px solid var(--color-positive); + margin-bottom: 1rem; +} + +.twitch-channel-live .twitch-channel-avatar-container::after { + content: 'LIVE'; + position: absolute; + background: var(--color-positive); + color: var(--color-widget-background); + font-size: var(--font-size-h6); + left: 50%; + bottom: -35%; + border-radius: var(--border-radius); + padding-inline: 0.3rem; + transform: translate(-50%); + border: 2px solid var(--color-widget-background); +} + +.twitch-stream-preview { + max-width: 100%; + width: 400px; + aspect-ratio: 16 / 9; + border-radius: var(--border-radius); + object-fit: cover; +} diff --git a/internal/glance/static/css/widget-videos.css b/internal/glance/static/css/widget-videos.css new file mode 100644 index 0000000..8d07801 --- /dev/null +++ b/internal/glance/static/css/widget-videos.css @@ -0,0 +1,13 @@ +.video-thumbnail { + width: 100%; + aspect-ratio: 16 / 8.9; + object-fit: cover; + border-radius: var(--border-radius) var(--border-radius) 0 0; +} + +.video-horizontal-list-thumbnail { + height: 4rem; + aspect-ratio: 16 / 8.9; + object-fit: cover; + border-radius: var(--border-radius); +} diff --git a/internal/glance/static/css/widget-weather.css b/internal/glance/static/css/widget-weather.css new file mode 100644 index 0000000..84d50ba --- /dev/null +++ b/internal/glance/static/css/widget-weather.css @@ -0,0 +1,139 @@ +.weather-column { + position: relative; + display: flex; + align-items: center; + justify-content: end; + flex-direction: column; + width: calc(100% / 12); + padding-top: 3px; +} + +.weather-column-value, .weather-columns:hover .weather-column-value { + font-size: 13px; + color: var(--color-text-highlight); + letter-spacing: -0.1rem; + margin-right: 0.1rem; + position: relative; + margin-bottom: 0.3rem; + opacity: 0; + transform: translateY(0.5rem); + transition: opacity .2s, transform .2s; + user-select: none; +} + +.weather-column-current .weather-column-value, .weather-column:hover .weather-column-value { + opacity: 1; + transform: translateY(0); +} + +.weather-column-value::after { + position: absolute; + content: '°'; + left: 100%; + color: var(--color-text-subdue); +} + +.weather-column-value.weather-column-value-negative::before { + position: absolute; + content: '-'; + right: 100%; +} + +.weather-bar, .weather-columns:hover .weather-bar { + height: calc(20px + var(--weather-bar-height) * 40px); + width: 6px; + background-color: hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 18%))); + border: 1px solid hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 24%))); + border-bottom: 0; + border-radius: 6px 6px 0 0; + mask-image: linear-gradient(0deg, transparent 0, #000 10px); + -webkit-mask-image: linear-gradient(0deg, transparent 0, #000 10px); + transition: background-color .2s, border-color .2s, width .2s; +} + +.weather-column-current .weather-bar, .weather-column:hover .weather-bar { + width: 10px; + background-color: hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 40%))); + border: 1px solid hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 50%))); +} + +.weather-column-rain { + position: absolute; + inset: 0; + bottom: 20%; + overflow: hidden; + mask-image: linear-gradient(0deg, transparent 40%, #000); + -webkit-mask-image: linear-gradient(0deg, transparent 40%, #000); +} + +.weather-column-rain::before { + content: ''; + position: absolute; + /* TODO: figure out a way to make it look continuous between columns, right now */ + /* depending on the width of the page the rain inside two columns next to each other */ + /* can overlap and look bad */ + background: radial-gradient(circle at 4px 4px, hsl(200, 90%, 70%, 0.4) 1px, transparent 0); + background-size: 8px 8px; + transform: rotate(45deg) translate(-50%, 25%); + height: 130%; + aspect-ratio: 1; + left: 55%; +} + +.weather-column:nth-child(3) .weather-column-time, +.weather-column:nth-child(7) .weather-column-time, +.weather-column:nth-child(11) .weather-column-time { + opacity: 1; + transform: translateY(0); +} + +.weather-column-time, .weather-columns:hover .weather-column-time { + margin-top: 0.3rem; + font-size: var(--font-size-h6); + opacity: 0; + transform: translateY(-0.5rem); + transition: opacity .2s, transform .2s; + user-select: none; +} + +.weather-column:hover .weather-column-time { + opacity: 1; + transform: translateY(0); +} + +.weather-column-daylight { + position: absolute; + inset: 0; + background: linear-gradient(0deg, transparent 30px, hsl(50, 50%, 30%, 0.2)); +} + +.weather-column-daylight-sunrise { + border-radius: 20px 0 0 0; +} + +.weather-column-daylight-sunset { + border-radius: 0 20px 0 0; +} + +.location-icon { + width: 0.8em; + height: 0.8em; + border-radius: 0 50% 50% 50%; + background-color: currentColor; + transform: rotate(225deg) translate(.1em, .1em); + position: relative; + flex-shrink: 0; +} + +.location-icon::after { + content: ''; + position: absolute; + z-index: 2; + width: .4em; + height: .4em; + border-radius: 50%; + background-color: var(--color-widget-background); + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} diff --git a/internal/glance/static/css/widgets.css b/internal/glance/static/css/widgets.css new file mode 100644 index 0000000..9c9e8ee --- /dev/null +++ b/internal/glance/static/css/widgets.css @@ -0,0 +1,88 @@ +@import "widget-bookmarks.css"; +@import "widget-calendar.css"; +@import "widget-clock.css"; +@import "widget-dns-stats.css"; +@import "widget-docker-containers.css"; +@import "widget-group.css"; +@import "widget-markets.css"; +@import "widget-monitor.css"; +@import "widget-reddit.css"; +@import "widget-releases.css"; +@import "widget-rss.css"; +@import "widget-search.css"; +@import "widget-server-stats.css"; +@import "widget-twitch.css"; +@import "widget-videos.css"; +@import "widget-weather.css"; + +@import "forum-posts.css"; + +.widget-error-header { + display: flex; + align-items: center; + justify-content: space-between; + position: relative; + margin-bottom: 1.8rem; + z-index: 1; +} + +.widget-error-header::before { + content: ''; + position: absolute; + inset: calc(0rem - (var(--widget-content-vertical-padding) / 2)) calc(0rem - (var(--widget-content-horizontal-padding) / 2)); + background: var(--color-negative); + opacity: 0.05; + border-radius: var(--border-radius); + z-index: -1; +} + +.widget-error-icon { + width: 2.4rem; + height: 2.4rem; + flex-shrink: 0; + stroke: var(--color-negative); + opacity: 0.6; +} + +.widget-content { + container-type: inline-size; + container-name: widget; +} + +.widget-content:not(.widget-content-frameless) { + padding: var(--widget-content-padding); +} + +.widget-content:not(.widget-content-frameless), .widget-content-frame { + background: var(--color-widget-background); + border-radius: var(--border-radius); + border: 1px solid var(--color-widget-content-border); + box-shadow: 0px 3px 0px 0px hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl)) - 0.5%)); +} + +.widget-header { + padding: 0 calc(var(--widget-content-horizontal-padding) + 1px); + font-size: var(--font-size-h4); + margin-bottom: 0.9rem; + display: flex; + align-items: center; + gap: 1rem; +} + +.widget-beta-icon { + width: 1.6rem; + height: 1.6rem; + flex-shrink: 0; + transition: transform .45s, opacity .45s, stroke .45s; + opacity: 0.7; +} + +.widget-beta-icon:hover, .widget-header .popover-active > .widget-beta-icon { + fill: var(--color-text-highlight); + transform: translateY(-10%) scale(1.3); + opacity: 1; +} + +.widget + .widget { + margin-top: var(--widget-gap); +} diff --git a/internal/glance/static/main.css b/internal/glance/static/main.css deleted file mode 100644 index 7b7b592..0000000 --- a/internal/glance/static/main.css +++ /dev/null @@ -1,2115 +0,0 @@ -@font-face { - font-family: 'JetBrains Mono'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('fonts/JetBrainsMono-Regular.woff2') format('woff2'); -} - -:root { - font-size: 10px; - - --scheme: ; - --bgh: 240; - --bgs: 8%; - --bgl: 9%; - --bghs: var(--bgh), var(--bgs); - --cm: 1; - --tsm: 1; - - --widget-gap: 23px; - --widget-content-vertical-padding: 15px; - --widget-content-horizontal-padding: 17px; - --widget-content-padding: var(--widget-content-vertical-padding) var(--widget-content-horizontal-padding); - --content-bounds-padding: 15px; - --border-radius: 5px; - --mobile-navigation-height: 50px; - - --color-primary: hsl(43, 50%, 70%); - --color-positive: var(--color-primary); - --color-negative: hsl(0, 70%, 70%); - --color-background: hsl(var(--bghs), var(--bgl)); - --color-widget-background-hsl-values: var(--bghs), calc(var(--bgl) + 1%); - --color-widget-background: hsl(var(--color-widget-background-hsl-values)); - --color-separator: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 4% * var(--cm)))); - --color-widget-content-border: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 4%))); - --color-widget-background-highlight: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 4%))); - --color-popover-background: hsl(var(--bgh), calc(var(--bgs) + 3%), calc(var(--bgl) + 3%)); - --color-popover-border: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 12%))); - --color-progress-border: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 10% * var(--cm)))); - --color-progress-value: hsl(var(--bgh), calc(var(--bgs) * var(--tsm)), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 30% * var(--cm)))); - --color-graph-gridlines: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 6% * var(--cm)))); - - --ths: var(--bgh), calc(var(--bgs) * var(--tsm)); - --color-text-highlight: hsl(var(--ths), calc(var(--scheme) var(--cm) * 85%)); - --color-text-paragraph: hsl(var(--ths), calc(var(--scheme) var(--cm) * 73%)); - --color-text-base: hsl(var(--ths), calc(var(--scheme) var(--cm) * 58%)); - --color-text-base-muted: hsl(var(--ths), calc(var(--scheme) var(--cm) * 52%)); - --color-text-subdue: hsl(var(--ths), calc(var(--scheme) var(--cm) * 35%)); - - --font-size-h1: 1.7rem; - --font-size-h2: 1.6rem; - --font-size-h3: 1.5rem; - --font-size-h4: 1.4rem; - --font-size-base: 1.3rem; - --font-size-h5: 1.2rem; - --font-size-h6: 1.1rem; -} - -.light-scheme { - --scheme: 100% -; -} - -.page { - height: 100%; - padding-block: var(--widget-gap); -} - -.page-content, .page.content-ready .page-loading-container { - display: none; -} - -.page.content-ready > .page-content { - display: block; -} - -.page-column-small .size-title-dynamic { - font-size: var(--font-size-h4); -} - -.page-column-full .size-title-dynamic { - font-size: var(--font-size-h3); -} - -.color-primary-if-not-visited:not(:visited) { - color: var(--color-primary); -} - -.text-truncate, -.single-line-titles .title -{ - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.single-line-titles .title { - display: block; -} - -.text-truncate-2-lines, .text-truncate-3-lines { - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-box-orient: vertical; -} - -.text-truncate-3-lines { line-clamp: 3; -webkit-line-clamp: 3; } -.text-truncate-2-lines { line-clamp: 2; -webkit-line-clamp: 2; } - -.visited-indicator:not(.text-truncate)::after, -.visited-indicator.text-truncate::before, -.bookmarks-link:not(.bookmarks-link-no-arrow)::after { - content: '↗' / ""; - margin-left: 0.5em; - display: inline-block; - position: relative; - top: 0.15em; - color: var(--color-text-base); -} - -.visited-indicator.text-truncate { - direction: rtl; - text-align: left; -} - -.visited-indicator:not(:visited)::before, .visited-indicator:not(:visited)::after { - color: var(--color-primary); -} - -.page-columns-transitioned .list-with-transition > * { animation: collapsibleItemReveal .25s backwards; } -.list-with-transition > *:nth-child(2) { animation-delay: 30ms; } -.list-with-transition > *:nth-child(3) { animation-delay: 60ms; } -.list-with-transition > *:nth-child(4) { animation-delay: 90ms; } -.list-with-transition > *:nth-child(5) { animation-delay: 120ms; } -.list-with-transition > *:nth-child(6) { animation-delay: 150ms; } -.list-with-transition > *:nth-child(7) { animation-delay: 180ms; } -.list-with-transition > *:nth-child(8) { animation-delay: 210ms; } - -.list > *:not(:first-child) { - margin-top: calc(var(--list-half-gap) * 2); -} - -.list.list-with-separator > *:not(:first-child) { - margin-top: var(--list-half-gap); - border-top: 1px solid var(--color-separator); - padding-top: var(--list-half-gap); -} - -.collapsible-container:not(.container-expanded) > .collapsible-item { - display: none; -} - -.collapsible-item { - animation: collapsibleItemReveal .25s backwards; -} - -@keyframes collapsibleItemReveal { - from { - opacity: 0; - transform: translateY(10px); - } -} - -.expand-toggle-button { - font: inherit; - border: 0; - cursor: pointer; - display: block; - width: 100%; - text-align: left; - color: var(--color-text-base); - text-transform: uppercase; - font-size: var(--font-size-h4); - padding: var(--widget-content-vertical-padding) 0; - background: var(--color-widget-background); -} - -.expand-toggle-button.container-expanded { - position: sticky; - /* -1px to hide 1px gap on chrome */ - bottom: -1px; -} - -.expand-toggle-button-icon { - display: inline-block; - margin-left: 1rem; - position: relative; - top: -.2rem; -} - -.expand-toggle-button-icon::before { - content: '' / ""; - font-size: 0.8rem; - transform: rotate(90deg); - line-height: 1; - display: inline-block; - transition: transform 0.3s; -} - -.expand-toggle-button.container-expanded .expand-toggle-button-icon::before { - transform: rotate(-90deg); -} - -.widget-group-header { - overflow-x: auto; - scrollbar-width: thin; -} - -.widget-group-title { - background: none; - font: inherit; - border: none; - text-transform: uppercase; - border-bottom: 1px dotted transparent; - cursor: pointer; - flex-shrink: 0; - transition: color .3s, border-color .3s; - color: var(--color-text-subdue); - line-height: calc(1.6em - 1px); -} - -.widget-group-title:hover:not(.widget-group-title-current) { - color: var(--color-text-base); -} - -.widget-group-title-current { - border-bottom-color: var(--color-text-base-muted); - color: var(--color-text-base); -} - -.widget-group-content { - animation: widgetGroupContentEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards; -} - -.widget-group-content[data-direction="right"] { - --direction: 5px; -} - -.widget-group-content[data-direction="left"] { - --direction: -5px; -} - -@keyframes widgetGroupContentEntrance { - from { - opacity: 0; - transform: translateX(var(--direction)); - } -} - -.widget-group-content:not(.widget-group-content-current) { - display: none; -} - -.widget-content:has(.expand-toggle-button:last-child) { - padding-bottom: 0; -} - -.cards-grid.collapsible-container + .expand-toggle-button { - text-align: center; - margin-top: 0.5rem; - background-color: var(--color-background); -} - -.attachments { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; -} - -.attachments > * { - border-radius: var(--border-radius); - padding: 0.1rem 0.5rem; - font-size: var(--font-size-h6); - background-color: var(--color-separator); -} - -pre { - font: inherit; -} - -::selection { - background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 20%))); - color: var(--color-text-highlight); -} - -::-webkit-scrollbar-thumb { - background: var(--color-text-subdue); - border-radius: var(--border-radius); -} - -::-webkit-scrollbar { - background: var(--color-background); - height: 5px; - width: 10px; -} - -*:focus-visible { - outline: 2px solid var(--color-primary); - outline-offset: 0.1rem; - border-radius: var(--border-radius); -} - -*, *::before, *::after { - box-sizing: border-box; -} - -* { - padding: 0; - margin: 0; -} - -hr { - border: 0; - height: 1px; - background-color: var(--color-separator); -} - -img, svg { - display: block; - max-width: 100%; -} - -img[loading=lazy].loaded:not(.finished-transition) { - transition: opacity .4s; -} - -img[loading=lazy].cached:not(.finished-transition) { - transition: none; -} - -img[loading=lazy]:not(.loaded, .cached) { - opacity: 0; -} - -html { - scrollbar-color: var(--color-text-subdue) transparent; - scroll-behavior: smooth; -} - -html, body, .body-content { - height: 100%; -} - -h1, h2, h3, h4, h5 { - font: inherit; -} - -.visually-hidden { - clip-path: inset(50%); - height: 1px; - overflow: hidden; - position: absolute; - white-space: nowrap; - width: 1px; -} - -a { - text-decoration: none; - color: inherit; - overflow-wrap: break-word; -} - -ul { - list-style: none; -} - -body { - font-size: 1.3rem; - font-family: 'JetBrains Mono', monospace; - font-variant-ligatures: none; - line-height: 1.6; - color: var(--color-text-base); - background-color: var(--color-background); - overflow-y: scroll; -} - -.page-column-small { - width: 300px; - flex-shrink: 0; -} - -.page-column-full { - width: 100%; - min-width: 0; -} - -.page-columns { - display: flex; - gap: var(--widget-gap); - animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards; -} - -@keyframes pageColumnsEntrance { - from { - opacity: 0; - transform: translateY(10px); - } -} - -.page-loading-container { - height: 100%; - display: flex; - align-items: center; - justify-content: center; - animation: loadingContainerEntrance 200ms backwards; - animation-delay: 150ms; - font-size: 2rem; -} - -.page-loading-container > .loading-icon { - translate: 0 -250%; -} - -@keyframes loadingContainerEntrance { - from { - opacity: 0; - } -} - -.loading-icon { - min-width: 1.5em; - width: 1.5em; - height: 1.5em; - border: 0.25em solid hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 12%))); - border-top-color: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 40%))); - border-radius: 50%; - animation: loadingIconSpin 800ms infinite linear; -} - -@keyframes loadingIconSpin { - to { - transform: rotate(360deg); - } -} - -.notice-icon { - width: 0.7rem; - height: 0.7rem; - border-radius: 50%; -} - -.notice-icon-major { - background: var(--color-negative); -} - -.notice-icon-minor { - border: 1px solid var(--color-negative); -} - -kbd { - font: inherit; - padding: 0.1rem 0.8rem; - border-radius: var(--border-radius); - border: 2px solid var(--color-widget-background-highlight); - box-shadow: 0 2px 0 var(--color-widget-background-highlight); - user-select: none; - transition: transform .1s, box-shadow .1s; - font-size: var(--font-size-h5); - cursor: pointer; -} - -kbd:active { - transform: translateY(2px); - box-shadow: 0 0 0 0 var(--color-widget-background-highlight); -} - -.masonry { - display: flex; - gap: var(--widget-gap); -} - -.masonry-column { - flex: 1; - display: flex; - flex-direction: column; -} - -.popover-container, [data-popover-html] { - display: none; -} - -.popover-container { - --triangle-size: 10px; - --triangle-offset: 50%; - --triangle-margin: calc(var(--triangle-size) + 3px); - --entrance-y-offset: 8px; - --entrance-direction: calc(var(--entrance-y-offset) * -1); - - z-index: 20; - position: absolute; - padding-top: var(--triangle-margin); - padding-inline: var(--content-bounds-padding); -} - -.popover-container.position-above { - --entrance-direction: var(--entrance-y-offset); - padding-top: 0; - padding-bottom: var(--triangle-margin); -} - -.popover-frame { - --shadow-properties: 0 15px 20px -10px; - --shadow-color: hsla(var(--bghs), calc(var(--bgl) * 0.2), 0.5); - position: relative; - padding: 10px; - background: var(--color-popover-background); - border: 1px solid var(--color-popover-border); - border-radius: 5px; - animation: popoverFrameEntrance 0.3s backwards cubic-bezier(0.16, 1, 0.3, 1); - box-shadow: var(--shadow-properties) var(--shadow-color); -} - -.popover-frame::before { - content: ''; - position: absolute; - width: var(--triangle-size); - height: var(--triangle-size); - transform: rotate(45deg); - background-color: var(--color-popover-background); - border-top-left-radius: 2px; - border-left: 1px solid var(--color-popover-border); - border-top: 1px solid var(--color-popover-border); - left: calc(var(--triangle-offset) - (var(--triangle-size) / 2)); - top: calc(var(--triangle-size) / 2 * -1 - 1px); -} - -.popover-container.position-above .popover-frame::before { - transform: rotate(-135deg); - top: auto; - bottom: calc(var(--triangle-size) / 2 * -1 - 1px); -} - -.popover-container.position-above .popover-frame { - --shadow-properties: 0 10px 20px -10px; -} - -@keyframes popoverFrameEntrance { - from { - opacity: 0; - transform: translateY(var(--entrance-direction)); - } -} - -.summary { - width: 100%; - cursor: pointer; - word-spacing: -0.18em; - user-select: none; - list-style: none; - position: relative; - display: flex; - z-index: 1; -} - -.summary::-webkit-details-marker { - display: none; -} - -.details[open] .summary { - margin-bottom: .8rem; -} - -.summary::before { - content: ""; - position: absolute; - inset: -.3rem -.8rem; - border-radius: var(--border-radius); - background-color: var(--color-widget-background-highlight); - opacity: 0; - transition: opacity 0.2s; - z-index: -1; -} - -.details[open] .summary::before, .summary:hover::before { - opacity: 1; -} - -.details:not([open]) .list-with-transition { - display: none; -} - -.summary::after { - content: "◀" / ""; - font-size: 1.2em; - position: absolute; - top: 0; - bottom: 0; - line-height: 1.3em; - right: 0; - transition: rotate .5s cubic-bezier(0.22, 1, 0.36, 1); -} - -details[open] .summary::after { - rotate: -90deg; -} - -.content-bounds { - max-width: 1600px; - width: 100%; - margin-inline: auto; - padding: 0 var(--content-bounds-padding); -} - -.page-width-wide .content-bounds { - max-width: 1920px; -} - -.page-width-slim .content-bounds { - max-width: 1100px; -} - -.page-center-vertically .page { - display: flex; - justify-content: center; - flex-direction: column; -} - -/* TODO: refactor, otherwise I hope I never have to change dynamic columns again */ -.dynamic-columns { - --list-half-gap: 0.5rem; - gap: var(--widget-content-vertical-padding) var(--widget-content-horizontal-padding); - display: grid; - grid-template-columns: repeat(var(--columns-per-row), 1fr); -} - -.dynamic-columns > * { - padding-left: var(--widget-content-horizontal-padding); - border-left: 1px solid var(--color-separator); - min-width: 0; -} - -.dynamic-columns > *:first-child { - padding-top: 0; - border-top: none; - border-left: none; -} - -.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; } -.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; } -.dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; } -.dynamic-columns:has(> :nth-child(4)) { --columns-per-row: 4; } -.dynamic-columns:has(> :nth-child(5)) { --columns-per-row: 5; } - -@container widget (max-width: 599px) { - .dynamic-columns { gap: 0; } - .dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; } - .dynamic-columns > * { - border-left: none; - padding-left: 0; - } - .dynamic-columns > *:not(:first-child) { - margin-top: calc(var(--list-half-gap) * 2); - } - .dynamic-columns.list-with-separator > *:not(:first-child) { - margin-top: var(--list-half-gap); - border-top: 1px solid var(--color-separator); - padding-top: var(--list-half-gap); - } -} -@container widget (min-width: 600px) and (max-width: 849px) { - .dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; } - .dynamic-columns > :nth-child(2n-1) { - border-left: none; - padding-left: 0; - } -} -@container widget (min-width: 850px) and (max-width: 1249px) { - .dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; } - .dynamic-columns > :nth-child(3n+1) { - border-left: none; - padding-left: 0; - } -} -@container widget (min-width: 1250px) and (max-width: 1499px) { - .dynamic-columns:has(> :nth-child(4)) { --columns-per-row: 4; } - .dynamic-columns > :nth-child(4n+1) { - border-left: none; - padding-left: 0; - } -} -@container widget (min-width: 1500px) { - .dynamic-columns:has(> :nth-child(5)) { --columns-per-row: 5; } - .dynamic-columns > :nth-child(5n+1) { - border-left: none; - padding-left: 0; - } -} - -.cards-vertical { - flex-direction: column; -} - -.cards-horizontal { - --cards-per-row: 6.5; -} - -.cards-horizontal, .cards-vertical { - --cards-gap: calc(var(--widget-content-vertical-padding) * 0.7); - display: flex; - gap: var(--cards-gap); -} - -.card { - display: flex; - flex-direction: column; -} - -.cards-horizontal .card { - flex-shrink: 0; - width: calc(100% / var(--cards-per-row) - var(--cards-gap) * (var(--cards-per-row) - 1) / var(--cards-per-row)); -} - -.cards-grid .card { - min-width: 0; -} - -.cards-horizontal { - overflow-x: auto; - scrollbar-width: thin; - padding-bottom: 1rem; -} - -.cards-grid { - --cards-per-row: 6; - display: grid; - grid-template-columns: repeat(var(--cards-per-row), 1fr); - gap: calc(var(--widget-content-vertical-padding) * 0.7); -} - -@container widget (max-width: 1300px) { .cards-horizontal { --cards-per-row: 5.5; } } -@container widget (max-width: 1100px) { .cards-horizontal { --cards-per-row: 4.5; } } -@container widget (max-width: 850px) { .cards-horizontal { --cards-per-row: 3.5; } } -@container widget (max-width: 750px) { .cards-horizontal { --cards-per-row: 3.5; } } -@container widget (max-width: 650px) { .cards-horizontal { --cards-per-row: 2.5; } } -@container widget (max-width: 450px) { .cards-horizontal { --cards-per-row: 2.3; } } - -@container widget (max-width: 1300px) { .cards-grid { --cards-per-row: 5; } } -@container widget (max-width: 1100px) { .cards-grid { --cards-per-row: 4; } } -@container widget (max-width: 850px) { .cards-grid { --cards-per-row: 3; } } -@container widget (max-width: 750px) { .cards-grid { --cards-per-row: 3; } } -@container widget (max-width: 650px) { .cards-grid { --cards-per-row: 2; } } - -.widget-small-content-bounds { - max-width: 350px; - margin: 0 auto; -} - -.widget-error-header { - display: flex; - align-items: center; - justify-content: space-between; - position: relative; - margin-bottom: 1.8rem; - z-index: 1; -} - -.widget-error-header::before { - content: ''; - position: absolute; - inset: calc(0rem - (var(--widget-content-vertical-padding) / 2)) calc(0rem - (var(--widget-content-horizontal-padding) / 2)); - background: var(--color-negative); - opacity: 0.05; - border-radius: var(--border-radius); - z-index: -1; -} - -.widget-error-icon { - width: 2.4rem; - height: 2.4rem; - flex-shrink: 0; - stroke: var(--color-negative); - opacity: 0.6; -} - -.widget-content { - container-type: inline-size; - container-name: widget; -} - -.widget-content:not(.widget-content-frameless) { - padding: var(--widget-content-padding); -} - -.widget-content:not(.widget-content-frameless), .widget-content-frame { - background: var(--color-widget-background); - border-radius: var(--border-radius); - border: 1px solid var(--color-widget-content-border); - box-shadow: 0px 3px 0px 0px hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl)) - 0.5%)); -} - -.padding-widget { - padding: var(--widget-content-padding); -} - -.margin-bottom-widget { - margin-bottom: var(--widget-content-vertical-padding); -} - -.padding-block-widget { - padding-block: var(--widget-content-vertical-padding); -} - -.padding-inline-widget { - padding-inline: var(--widget-content-horizontal-padding); -} - -.widget-header { - padding: 0 calc(var(--widget-content-horizontal-padding) + 1px); - font-size: var(--font-size-h4); - margin-bottom: 0.9rem; - display: flex; - align-items: center; - gap: 1rem; -} - -.widget-beta-icon { - width: 1.6rem; - height: 1.6rem; - flex-shrink: 0; - transition: transform .45s, opacity .45s, stroke .45s; - opacity: 0.7; -} - -.widget-beta-icon:hover, .widget-header .popover-active > .widget-beta-icon { - fill: var(--color-text-highlight); - transform: translateY(-10%) scale(1.3); - opacity: 1; -} - -.widget + .widget { - margin-top: var(--widget-gap); -} - -.list-horizontal-text { - display: flex; - list-style: none; - flex-wrap: wrap; - align-items: center; -} - -.list-horizontal-text > *:not(:last-child)::after { - content: '•' / ""; - color: var(--color-text-subdue); - margin: 0 0.4rem; - position: relative; - top: 0.1rem; -} - -.header-container { - margin-top: calc(var(--widget-gap) / 2); - --header-height: 45px; - --header-items-gap: 2.5rem; -} - -.header { - display: flex; - height: var(--header-height); - gap: var(--header-items-gap); -} - -.logo { - height: 100%; - line-height: var(--header-height); - font-size: 2rem; - color: var(--color-text-highlight); - border-right: 1px solid var(--color-widget-content-border); - padding-right: var(--widget-content-horizontal-padding); -} - -.logo:has(img) { - display: flex; - align-items: center; -} - -.logo img { - max-height: 2.7rem; -} - -.nav { - height: 100%; - gap: var(--header-items-gap); -} - -.nav .nav-item { - line-height: var(--header-height); -} - -.footer { - padding-bottom: calc(var(--widget-gap) * 1.5); - padding-top: calc(var(--widget-gap) / 2); - animation: loadingContainerEntrance 200ms backwards; - animation-delay: 150ms; -} - -.mobile-navigation, .mobile-reachability-header { - display: none; -} - -.nav-item { - display: block; - height: 100%; - border-bottom: 2px solid transparent; - transition: color .3s, border-color .3s; - font-size: var(--font-size-h3); - flex-shrink: 0; -} - -.nav-item:not(.nav-item-current):hover { - border-bottom-color: var(--color-text-subdue); - color: var(--color-text-highlight); -} - -.nav-item.nav-item-current { - border-bottom-color: var(--color-primary); - color: var(--color-text-highlight); -} - -.release-source-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - opacity: 0.4; -} - -.market-chart { - margin-left: auto; - width: 6.5rem; - flex-shrink: 0; -} - -.market-chart svg { - width: 100%; -} - -.market-values { - min-width: 8rem; -} - -.carousel-container { - position: relative; -} - -.carousel-container::before, .carousel-container::after { - content: ''; - position: absolute; - width: 2rem; - top: 0; - bottom: 1rem; - z-index: 10; - opacity: 0; - pointer-events: none; - transition-duration: 0.2s; -} - -.carousel-container::before { - left: 0; - background: linear-gradient(to right, var(--color-background), transparent); -} - -.carousel-container::after { - right: 0; - background: linear-gradient(to left, var(--color-background), transparent); -} - -.carousel-container.show-left-cutoff::before, .carousel-container.show-right-cutoff::after { - opacity: 1; -} - -.video-thumbnail { - width: 100%; - aspect-ratio: 16 / 8.9; - object-fit: cover; - border-radius: var(--border-radius) var(--border-radius) 0 0; -} - -.video-horizontal-list-thumbnail { - height: 4rem; - aspect-ratio: 16 / 8.9; - object-fit: cover; - border-radius: var(--border-radius); -} - -.search-icon { - width: 2.3rem; -} - -.search-icon-container { - position: relative; - flex-shrink: 0; -} - -/* gives a wider hit area for the 3 people that will notice the animation : ) */ -.search-icon-container::before { - content: ''; - position: absolute; - inset: -1rem; -} - -.search-icon-container:hover > .search-icon { - animation: searchIconHover 2.9s forwards; -} - -@keyframes searchIconHover { - 0%, 39% { translate: 0 0; } - 20% { scale: 1.3; } - 40% { scale: 1; } - 50% { translate: -30% 30%; } - 70% { translate: 30% -30%; } - 90% { translate: -30% -30%; } - 100% { translate: 0 0; } -} - -.search { - transition: border-color .2s; - position: relative; -} - -.search:hover { - border-color: var(--color-text-subdue); -} - -.search:focus-within { - border-color: var(--color-primary); -} - -.search-input { - border: 0; - background: none; - width: 100%; - height: 6rem; - font: inherit; - outline: none; - color: var(--color-text-highlight); -} - -.search-input::placeholder { - color: var(--color-text-base-muted); - opacity: 1; -} - -.search-bangs { display: none; } - -.search-bang { - border-radius: calc(var(--border-radius) * 2); - background: var(--color-widget-background-highlight); - padding: 0.3rem 1rem; - flex-shrink: 0; - font-size: var(--font-size-h5); - animation: searchBangsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards; -} - -@keyframes searchBangsEntrance { - 0% { - opacity: 0; - transform: translateX(-10px); - } -} - -.search-bang:empty { - display: none; -} - -.forum-post-list-thumbnail { - flex-shrink: 0; - width: 6rem; - height: 4.1rem; - border-radius: var(--border-radius); - object-fit: cover; - border: 1px solid var(--color-separator); - margin-top: 0.1rem; -} - -.forum-post-tags-container { - transform: translateY(-0.15rem); -} - -@container widget (max-width: 550px) { - .forum-post-autohide { - display: none; - } -} - -.bookmarks-group { - --bookmarks-group-color: var(--color-primary); -} - -.bookmarks-group-title { - color: var(--bookmarks-group-color); -} - -.bookmarks-group .bookmarks-link::after { - color: var(--bookmarks-group-color); -} - -.bookmarks-icon-container { - margin-block: 0.1rem; - background-color: var(--color-widget-background-highlight); - border-radius: var(--border-radius); - padding: 0.5rem; - opacity: 0.7; - flex-shrink: 0; -} - -.bookmarks-icon { - width: 20px; - height: 20px; - opacity: 0.8; -} - -:root:not(.light-scheme) .flat-icon { - filter: invert(1); -} - -.old-calendar-day { - width: calc(100% / 7); - text-align: center; - padding: 0.6rem 0; -} - -.old-calendar-day-today { - border-radius: var(--border-radius); - background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) (var(--bgl)) + 6%))); - color: var(--color-text-highlight); -} - -.calendar-dates { - text-align: center; - display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 2px; -} - -.calendar-date { - padding: 0.4rem 0; - color: var(--color-text-base); - position: relative; - border-radius: var(--border-radius); - background: none; - border: none; - font: inherit; -} - -.calendar-current-date { - border-radius: var(--border-radius); - background-color: var(--color-popover-border); - color: var(--color-text-highlight); -} - -.calendar-spillover-date { - color: var(--color-text-subdue); -} - -.calendar-header-button { - position: relative; - cursor: pointer; - width: 2rem; - height: 2rem; - z-index: 1; - background: none; - border: none; -} - -.calendar-header-button::before { - content: ''; - position: absolute; - inset: -0.2rem; - border-radius: var(--border-radius); - background-color: var(--color-text-subdue); - opacity: 0; - transition: opacity 0.2s; - z-index: -1; -} - -.calendar-header-button:hover::before { - opacity: 0.4; -} - -.calendar-undo-button { - display: inline-block; - vertical-align: text-top; - width: 2rem; - height: 2rem; - margin-left: 0.7rem; -} - -.dns-stats-totals { - transition: opacity .3s; - transition-delay: 50ms; -} - -.dns-stats:has(.dns-stats-graph .popover-active) .dns-stats-totals { - opacity: 0.1; - transition-delay: 0s; -} - -.dns-stats-graph { - --graph-height: 70px; - height: var(--graph-height); - position: relative; - margin-bottom: 2.5rem; -} - -.dns-stats-graph-gridlines-container { - position: absolute; - inset: 0; -} - -.dns-stats-graph-gridlines { - height: 100%; - width: 100%; -} - -.dns-stats-graph-columns { - display: flex; - height: 100%; -} - -.dns-stats-graph-column { - display: flex; - justify-content: flex-end; - align-items: center; - flex-direction: column; - width: calc(100% / 8); - position: relative; -} - -.dns-stats-graph-column::before { - content: ''; - position: absolute; - inset: 1px 0; - opacity: 0; - background: var(--color-text-base); - transition: opacity .2s; -} - -.dns-stats-graph-column:hover::before { - opacity: 0.05; -} - -.dns-stats-graph-bar { - width: 14px; - height: calc((var(--bar-height) / 100) * var(--graph-height)); - border: 1px solid var(--color-progress-border); - border-radius: var(--border-radius) var(--border-radius) 0 0; - display: flex; - background: var(--color-widget-background); - padding: 2px 2px 0 2px; - flex-direction: column; - gap: 2px; - transition: border-color .2s; - min-height: 10px; -} - -.dns-stats-graph-column.popover-active .dns-stats-graph-bar { - border-color: var(--color-text-subdue); - border-bottom-color: var(--color-progress-border); -} - -.dns-stats-graph-bar > * { - border-radius: 2px; - background: var(--color-progress-value); - min-height: 1px; -} - -.dns-stats-graph-bar > .queries { - flex-grow: 1; -} - -.dns-stats-graph-bar > *:last-child { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} - -.dns-stats-graph-bar > .blocked { - background-color: var(--color-negative); - flex-basis: calc(var(--percent) - 1px); -} - -.dns-stats-graph-column:nth-child(even) .dns-stats-graph-time { - opacity: 1; - transform: translateY(0); -} - -.dns-stats-graph-time, .dns-stats-graph-columns:hover .dns-stats-graph-time { - position: absolute; - font-size: var(--font-size-h6); - inset-inline: 0; - text-align: center; - height: 2.5rem; - line-height: 2.5rem; - top: 100%; - user-select: none; - opacity: 0; - transform: translateY(-0.5rem); - transition: opacity .2s, transform .2s; -} - -.dns-stats-graph-column:hover .dns-stats-graph-time { - opacity: 1; - transform: translateY(0); -} - -.dns-stats-graph-columns:hover .dns-stats-graph-column:not(:hover) .dns-stats-graph-time { - opacity: 0; -} - -.weather-column { - position: relative; - display: flex; - align-items: center; - justify-content: end; - flex-direction: column; - width: calc(100% / 12); - padding-top: 3px; -} - -.weather-column-value, .weather-columns:hover .weather-column-value { - font-size: 13px; - color: var(--color-text-highlight); - letter-spacing: -0.1rem; - margin-right: 0.1rem; - position: relative; - margin-bottom: 0.3rem; - opacity: 0; - transform: translateY(0.5rem); - transition: opacity .2s, transform .2s; - user-select: none; -} - -.weather-column-current .weather-column-value, .weather-column:hover .weather-column-value { - opacity: 1; - transform: translateY(0); -} - -.weather-column-value::after { - position: absolute; - content: '°'; - left: 100%; - color: var(--color-text-subdue); -} - -.weather-column-value.weather-column-value-negative::before { - position: absolute; - content: '-'; - right: 100%; -} - -.weather-bar, .weather-columns:hover .weather-bar { - height: calc(20px + var(--weather-bar-height) * 40px); - width: 6px; - background-color: hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 18%))); - border: 1px solid hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 24%))); - border-bottom: 0; - border-radius: 6px 6px 0 0; - mask-image: linear-gradient(0deg, transparent 0, #000 10px); - -webkit-mask-image: linear-gradient(0deg, transparent 0, #000 10px); - transition: background-color .2s, border-color .2s, width .2s; -} - -.weather-column-current .weather-bar, .weather-column:hover .weather-bar { - width: 10px; - background-color: hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 40%))); - border: 1px solid hsl(var(--ths), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 50%))); -} - -.weather-column-rain { - position: absolute; - inset: 0; - bottom: 20%; - overflow: hidden; - mask-image: linear-gradient(0deg, transparent 40%, #000); - -webkit-mask-image: linear-gradient(0deg, transparent 40%, #000); -} - -.weather-column-rain::before { - content: ''; - position: absolute; - /* TODO: figure out a way to make it look continuous between columns, right now - depending on the width of the page the rain inside two columns next to each other - can overlap and look bad */ - background: radial-gradient(circle at 4px 4px, hsl(200, 90%, 70%, 0.4) 1px, transparent 0); - background-size: 8px 8px; - transform: rotate(45deg) translate(-50%, 25%); - height: 130%; - aspect-ratio: 1; - left: 55%; -} - -.weather-column:nth-child(3) .weather-column-time, -.weather-column:nth-child(7) .weather-column-time, -.weather-column:nth-child(11) .weather-column-time { - opacity: 1; - transform: translateY(0); -} - -.weather-column-time, .weather-columns:hover .weather-column-time { - margin-top: 0.3rem; - font-size: var(--font-size-h6); - opacity: 0; - transform: translateY(-0.5rem); - transition: opacity .2s, transform .2s; - user-select: none; -} - -.weather-column:hover .weather-column-time { - opacity: 1; - transform: translateY(0); -} - -.weather-column-daylight { - position: absolute; - inset: 0; - background: linear-gradient(0deg, transparent 30px, hsl(50, 50%, 30%, 0.2)); -} - -.weather-column-daylight-sunrise { - border-radius: 20px 0 0 0; -} - -.weather-column-daylight-sunset { - border-radius: 0 20px 0 0; -} - -.location-icon { - width: 0.8em; - height: 0.8em; - border-radius: 0 50% 50% 50%; - background-color: currentColor; - transform: rotate(225deg) translate(.1em, .1em); - position: relative; - flex-shrink: 0; -} - -.location-icon::after { - content: ''; - position: absolute; - z-index: 2; - width: .4em; - height: .4em; - border-radius: 50%; - background-color: var(--color-widget-background); - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - -.clock-time { - min-width: 8ch; -} - -.clock-time span { - color: var(--color-text-highlight); -} - -.monitor-site-icon { - display: block; - opacity: 0.8; - filter: grayscale(0.4); - object-fit: contain; - aspect-ratio: 1 / 1; - width: 3.2rem; - position: relative; - top: -0.1rem; - transition: filter 0.3s, opacity 0.3s; -} - -.monitor-site-icon.flat-icon { - opacity: 0.7; -} - -.monitor-site:hover .monitor-site-icon { - opacity: 1; -} - -.monitor-site:hover .monitor-site-icon:not(.flat-icon) { - filter: grayscale(0); -} - -.monitor-site-status-icon { - flex-shrink: 0; - margin-left: auto; - width: 2rem; - height: 2rem; -} - -.monitor-site-status-icon-compact { - width: 1.8rem; - height: 1.8rem; - flex-shrink: 0; -} - -.docker-container-icon { - display: block; - filter: grayscale(0.4); - object-fit: contain; - aspect-ratio: 1 / 1; - width: 2.7rem; - opacity: 0.8; - transition: filter 0.3s, opacity 0.3s; -} - -.docker-container-icon.flat-icon { - opacity: 0.7; -} - -.docker-container:hover .docker-container-icon { - opacity: 1; -} - -.docker-container:hover .docker-container-icon:not(.flat-icon) { - filter: grayscale(0); -} - -.docker-container-status-icon { - width: 2rem; - height: 2rem; -} - -.widget-type-server-info { - position: relative; -} - -.server + .server { - margin-top: 3rem; -} - -.server { - gap: 1rem; - display: flex; - flex-direction: column; -} - -.server-info { - align-items: center; - display: flex; - justify-content: space-between; - gap: 1.5rem; - flex-shrink: 1; - min-width: 0; -} - -.server-details { - min-width: 0; -} - -.server-icon { - height: 3rem; - width: 3rem; -} - -.server-spicy-cpu-icon { - height: 1em; - align-self: center; - margin-left: 0.4em; - margin-bottom: 0.2rem; -} - -.server-stats { - display: flex; - gap: 1.5rem; - margin-top: 0.5rem; -} - -.server-stat-unavailable { - opacity: 0.5; -} - -.progress-bar { - border: 1px solid var(--color-progress-border); - border-radius: var(--border-radius); - display: flex; - flex-direction: column; - gap: 2px; - padding: 2px; - height: 1.5rem; - margin-inline: -3px; /* naughty, but oh so beautiful */ -} - -.progress-bar-combined { - height: 3rem; -} - -.popover-active > .progress-bar { - transition: border-color .3s; - border-color: var(--color-text-subdue); -} - -.progress-value { - --half-border-radius: calc(var(--border-radius) / 2); - border-radius: 0 var(--half-border-radius) var(--half-border-radius) 0; - background: var(--color-progress-value); - width: calc(var(--percent) * 1%); - min-width: 1px; - flex: 1; -} - -.progress-value:first-child { - border-top-left-radius: var(--half-border-radius); -} - -.progress-value:last-child { - border-bottom-left-radius: var(--half-border-radius); -} - -.progress-value-notice { - background: linear-gradient(to right, var(--color-progress-value) 65%, var(--color-negative)); -} - -.value-separator { - min-width: 2rem; - margin-inline: 0.8rem; - flex: 1; - height: calc(1em * 1.1); - border-bottom: 1px dotted var(--color-text-subdue); -} - -@container widget (min-width: 650px) { - .server { - gap: 2rem; - flex-direction: row; - align-items: center; - } - - .server + .server { - margin-top: 1rem; - } - - .server-info { - flex-direction: row-reverse; - justify-content: unset; - margin-right: auto; - z-index: 1; - } - - .server-stats { - flex-direction: row; - justify-content: right; - min-width: 450px; - margin-top: 0; - gap: 2rem; - padding-bottom: 0.8rem; - z-index: 1; - } - - .server-stats > * { - max-width: 200px; - } -} - -.thumbnail { - filter: grayscale(0.2) contrast(0.9); - opacity: 0.8; - transition: filter 0.2s, opacity .2s; -} - -.thumbnail-container { - flex-shrink: 0; - border: 1px solid var(--color-separator); - border-radius: var(--border-radius); -} - -.thumbnail-container > * { - border-radius: var(--border-radius); - object-fit: cover; -} - -.thumbnail-parent:hover .thumbnail { - opacity: 1; - filter: none; -} - -.rss-card-image { - height: var(--rss-thumbnail-height, 10rem); - object-fit: cover; - border-radius: var(--border-radius) var(--border-radius) 0 0; -} - -.rss-card-2 { - position: relative; - height: var(--rss-card-height, 27rem); - overflow: hidden; -} - -.rss-card-2::before { - content: ''; - position: absolute; - inset: 0; - pointer-events: none; - background-image: linear-gradient( - 0deg, - var(--color-widget-background), - hsla(var(--color-widget-background-hsl-values), 0.8) 6rem, transparent 14rem - ); - z-index: 2; -} - -.rss-card-2-image { - position: absolute; - width: 100%; - height: 100%; - object-fit: cover; - /* +1px is required to fix some weird graphical bug where the image overflows on the bottom in firefox */ - border-radius: calc(var(--border-radius) + 1px); - opacity: 0.9; - z-index: 1; -} - -.rss-card-2-content { - position: absolute; - inset-inline: 0; - bottom: var(--widget-content-vertical-padding); - z-index: 3; -} - -.rss-detailed-description { - max-width: 55rem; - color: var(--color-text-base-muted); -} - -.rss-detailed-thumbnail { - margin-top: 0.3rem; -} - -.rss-detailed-thumbnail > * { - aspect-ratio: 3 / 2; - height: 8.7rem; -} - -.twitch-category-thumbnail { - width: 5rem; - aspect-ratio: 3 / 4; - border-radius: var(--border-radius); -} - -.twitch-channel-avatar { - aspect-ratio: 1; - border-radius: 50%; -} - -.twitch-channel-avatar-container { - width: 4.4rem; - height: 4.4rem; - border: 2px solid var(--color-text-subdue); - padding: 2px; - border-radius: 50%; - position: relative; - flex-shrink: 0; -} - -.twitch-channel-live .twitch-channel-avatar-container { - border: 2px solid var(--color-positive); - margin-bottom: 1rem; -} - -.twitch-channel-live .twitch-channel-avatar-container::after { - content: 'LIVE'; - position: absolute; - background: var(--color-positive); - color: var(--color-widget-background); - font-size: var(--font-size-h6); - left: 50%; - bottom: -35%; - border-radius: var(--border-radius); - padding-inline: 0.3rem; - transform: translate(-50%); - border: 2px solid var(--color-widget-background); -} - -.twitch-stream-preview { - max-width: 100%; - width: 400px; - aspect-ratio: 16 / 9; - border-radius: var(--border-radius); - object-fit: cover; -} - -.reddit-card-thumbnail { - width: 100%; - height: 100%; - object-fit: cover; - object-position: 0% 20%; - opacity: 0.15; - filter: blur(1px); -} - -.reddit-card-thumbnail-container { - position: absolute; - inset: 0; - overflow: hidden; - border-radius: var(--border-radius); -} - -.reddit-card-thumbnail-container::after { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient(0deg, var(--color-widget-background) 10%, transparent); -} - -@media (max-width: 1190px) { - .header-container { - display: none; - } - - .page-column-small .size-title-dynamic { - font-size: var(--font-size-h3); - } - - .page-column-small { - width: 100%; - flex-shrink: 1; - } - - .page-column { - display: none; - animation: columnEntrance .0s cubic-bezier(0.25, 1, 0.5, 1) backwards; - } - - .page-columns-transitioned .page-column { - animation-duration: .3s; - } - - @keyframes columnEntrance { - from { - opacity: 0; - transform: scaleX(0.95); - } - } - - .mobile-navigation-offset { - height: var(--mobile-navigation-height); - flex-shrink: 0; - } - - .mobile-navigation { - display: block; - position: fixed; - bottom: 0; - transform: translateY(calc(100% - var(--mobile-navigation-height))); - left: var(--content-bounds-padding); - right: var(--content-bounds-padding); - z-index: 10; - background-color: var(--color-widget-background); - border: 1px solid var(--color-widget-content-border); - border-bottom: 0; - border-radius: var(--border-radius) var(--border-radius) 0 0; - transition: transform .3s; - } - - .mobile-navigation:has(.mobile-navigation-page-links-input:checked) .hamburger-icon { - --spacing: 7px; - color: var(--color-primary); - height: 2px; - } - - .mobile-navigation:has(.mobile-navigation-page-links-input:checked) { - transform: translateY(0); - } - - .mobile-navigation-page-links { - border-top: 1px solid var(--color-widget-content-border); - padding: 15px var(--content-bounds-padding); - display: flex; - align-items: center; - overflow-x: auto; - scrollbar-width: thin; - gap: 2.5rem; - } - - .mobile-navigation-icons { - display: flex; - justify-content: space-around; - align-items: center; - } - - body:has(.mobile-navigation-input[value="0"]:checked) .page-columns > :nth-child(1), - body:has(.mobile-navigation-input[value="1"]:checked) .page-columns > :nth-child(2), - body:has(.mobile-navigation-input[value="2"]:checked) .page-columns > :nth-child(3) { - display: block; - } - - .mobile-navigation-label { - display: flex; - flex: 1; - max-width: 50px; - height: var(--mobile-navigation-height); - justify-content: center; - align-items: center; - cursor: pointer; - font-size: 15px; - line-height: var(--mobile-navigation-height); - } - - .mobile-navigation-pill { - display: block; - background: var(--color-text-base); - height: 10px; - width: 10px; - border-radius: 10px; - transition: width .3s, background-color .3s; - } - - .mobile-navigation-label:hover > .mobile-navigation-pill { - background-color: var(--color-text-highlight); - } - - .mobile-navigation-label:hover { - color: var(--color-text-highlight); - } - - .mobile-navigation-input:checked + .mobile-navigation-pill { - background: var(--color-primary); - width: 30px; - } - - .mobile-navigation-input, .mobile-navigation-page-links-input { - display: none; - } - - .hamburger-icon { - --spacing: 4px; - width: 1em; - height: 1px; - background-color: currentColor; - transition: color .3s, box-shadow .3s; - box-shadow: 0 calc(var(--spacing) * -1) 0 0 currentColor, 0 var(--spacing) 0 0 currentColor; - } - - .expand-toggle-button.container-expanded { - bottom: var(--mobile-navigation-height); - } - - .cards-grid + .expand-toggle-button.container-expanded { - /* hides content that peeks through the rounded borders of the mobile navigation */ - box-shadow: 0 var(--border-radius) 0 0 var(--color-background); - } - - .weather-column-rain::before { - background-size: 7px 7px; - } - - .ios .search-input { - /* so that iOS Safari does not zoom the page when the input is focused */ - font-size: 16px; - } -} - -@media (max-width: 1190px) and (display-mode: standalone) { - :root { - --safe-area-inset-bottom: env(safe-area-inset-bottom, 0); - } - - .ios .body-content { - height: 100dvh; - } - - .expand-toggle-button.container-expanded { - bottom: calc(var(--mobile-navigation-height) + var(--safe-area-inset-bottom)); - } - - .mobile-navigation { - transform: translateY(calc(100% - var(--mobile-navigation-height) - var(--safe-area-inset-bottom))); - padding-bottom: var(--safe-area-inset-bottom); - } - - .mobile-navigation-icons { - padding-bottom: var(--safe-area-inset-bottom); - transition: padding-bottom .3s; - } - - .mobile-navigation-offset { - height: calc(var(--mobile-navigation-height) + var(--safe-area-inset-bottom)); - } - - .mobile-navigation-icons:has(.mobile-navigation-page-links-input:checked) { - padding-bottom: 0; - } -} - -@media (display-mode: standalone) { - body { - padding-top: env(safe-area-inset-top, 0); - } -} - -@media (max-width: 550px) { - :root { - font-size: 9px; - --widget-gap: 15px; - --widget-content-vertical-padding: 10px; - --widget-content-horizontal-padding: 10px; - --content-bounds-padding: 10px; - } - - .dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; } - - .row-reverse-on-mobile { - flex-direction: row-reverse; - } - - .hide-on-mobile, .thumbnail-container:has(> .hide-on-mobile) { - display: none - } - - .mobile-reachability-header { - display: block; - font-size: 3rem; - padding: 10vh 1rem; - text-align: center; - color: var(--color-text-highlight); - animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards; - } - - .rss-detailed-thumbnail > * { - height: 6rem; - } - - .rss-detailed-description { - line-clamp: 3; - -webkit-line-clamp: 3; - } -} - -.size-h1 { font-size: var(--font-size-h1); } -.size-h2 { font-size: var(--font-size-h2); } -.size-h3 { font-size: var(--font-size-h3); } -.size-h4 { font-size: var(--font-size-h4); } -.size-base { font-size: var(--font-size-base); } -.size-h5 { font-size: var(--font-size-h5); } -.size-h6 { font-size: var(--font-size-h6); } - -.color-highlight { color: var(--color-text-highlight); } -.color-paragraph { color: var(--color-text-paragraph); } -.color-base { color: var(--color-text-base); } -.color-subdue { color: var(--color-text-subdue); } -.color-negative { color: var(--color-negative); } -.color-positive { color: var(--color-positive); } -.color-primary { color: var(--color-primary); } - -.cursor-help { cursor: help; } -.break-all { word-break: break-all; } -.text-left { text-align: left; } -.text-right { text-align: right; } -.text-center { text-align: center; } -.text-elevate { margin-top: -0.2em; } -.text-compact { word-spacing: -0.18em; } -.text-very-compact { word-spacing: -0.35em; } -.rtl { direction: rtl; } -.shrink { flex-shrink: 1; } -.shrink-0 { flex-shrink: 0; } -.min-width-0 { min-width: 0; } -.max-width-100 { max-width: 100%; } -.block { display: block; } -.inline-block { display: inline-block; } -.overflow-hidden { overflow: hidden; } -.relative { position: relative; } -.flex { display: flex; } -.flex-1 { flex: 1; } -.flex-wrap { flex-wrap: wrap; } -.flex-nowrap { flex-wrap: nowrap; } -.justify-between { justify-content: space-between; } -.justify-stretch { justify-content: stretch; } -.justify-evenly { justify-content: space-evenly; } -.justify-center { justify-content: center; } -.justify-end { justify-content: end; } -.uppercase { text-transform: uppercase; } -.grow { flex-grow: 1; } -.flex-column { flex-direction: column; } -.items-center { align-items: center; } -.items-start { align-items: start; } -.items-end { align-items: end; } -.gap-5 { gap: 0.5rem; } -.gap-7 { gap: 0.7rem; } -.gap-10 { gap: 1rem; } -.gap-12 { gap: 1.2rem; } -.gap-15 { gap: 1.5rem; } -.gap-20 { gap: 2rem; } -.gap-25 { gap: 2.5rem; } -.gap-35 { gap: 3.5rem; } -.gap-45 { gap: 4.5rem; } -.gap-55 { gap: 5.5rem; } -.margin-left-auto { margin-left: auto; } -.margin-top-3 { margin-top: 0.3rem; } -.margin-top-5 { margin-top: 0.5rem; } -.margin-top-7 { margin-top: 0.7rem; } -.margin-top-10 { margin-top: 1rem; } -.margin-top-15 { margin-top: 1.5rem; } -.margin-top-20 { margin-top: 2rem; } -.margin-top-25 { margin-top: 2.5rem; } -.margin-top-35 { margin-top: 3.5rem; } -.margin-top-40 { margin-top: 4rem; } -.margin-top-auto { margin-top: auto; } -.margin-block-3 { margin-block: 0.3rem; } -.margin-block-5 { margin-block: 0.5rem; } -.margin-block-7 { margin-block: 0.7rem; } -.margin-block-8 { margin-block: 0.8rem; } -.margin-block-10 { margin-block: 1rem; } -.margin-block-15 { margin-block: 1.5rem; } -.margin-bottom-3 { margin-bottom: 0.3rem; } -.margin-bottom-5 { margin-bottom: 0.5rem; } -.margin-bottom-7 { margin-bottom: 0.7rem; } -.margin-bottom-10 { margin-bottom: 1rem; } -.margin-bottom-15 { margin-bottom: 1.5rem; } -.margin-bottom-auto { margin-bottom: auto; } -.padding-block-5 { padding-block: 0.5rem; } -.scale-half { transform: scale(0.5); } -.list { --list-half-gap: 0rem; } -.list-gap-2 { --list-half-gap: 0.1rem; } -.list-gap-4 { --list-half-gap: 0.2rem; } -.list-gap-8 { --list-half-gap: 0.4rem; } -.list-gap-10 { --list-half-gap: 0.5rem; } -.list-gap-14 { --list-half-gap: 0.7rem; } -.list-gap-20 { --list-half-gap: 1rem; } -.list-gap-24 { --list-half-gap: 1.2rem; } -.list-gap-34 { --list-half-gap: 1.7rem; } diff --git a/internal/glance/templates/document.html b/internal/glance/templates/document.html index a26f854..a6896e0 100644 --- a/internal/glance/templates/document.html +++ b/internal/glance/templates/document.html @@ -12,11 +12,11 @@ - - + + - - + + {{ block "document-head-after" . }}{{ end }}