diff --git a/docs/configuration.md b/docs/configuration.md index d3cea7d..3767f71 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -13,6 +13,7 @@ - [Lobsters](#lobsters) - [Reddit](#reddit) - [Search](#search-widget) + - [Group](#group) - [Extension](#extension) - [Weather](#weather) - [Monitor](#monitor) @@ -806,6 +807,50 @@ url: https://store.steampowered.com/search/?term={QUERY} url: https://www.amazon.com/s?k={QUERY} ``` +### Group +Group multiple widgets into one using tabs. Widgets are defined using a `widgets` property exactly as you would on a page column. The only limitation is that you cannot place a group widget within a group widget. + +Example: + +```yaml +- type: group + widgets: + - type: reddit + subreddit: gamingnews + show-thumbnails: true + collapse-after: 6 + - type: reddit + subreddit: games + - type: reddit + subreddit: pcgaming + show-thumbnails: true +``` + +Preview: + + + +#### Sharing properties + +To avoid repetition you can use [YAML anchors](https://support.atlassian.com/bitbucket-cloud/docs/yaml-anchors/) and share properties between widgets. + +Example: + +```yaml +- type: group + define: &shared-properties + type: reddit + show-thumbnails: true + collapse-after: 6 + widgets: + - subreddit: gamingnews + <<: *shared-properties + - subreddit: games + <<: *shared-properties + - subreddit: pcgaming + <<: *shared-properties +``` + ### Extension Display a widget provided by an external source (3rd party). If you want to learn more about developing extensions, checkout the [extensions documentation](extensions.md) (WIP). diff --git a/docs/images/group-widget-preview.png b/docs/images/group-widget-preview.png new file mode 100644 index 0000000..1380937 Binary files /dev/null and b/docs/images/group-widget-preview.png differ diff --git a/internal/assets/static/main.css b/internal/assets/static/main.css index cfc96fc..8b34b78 100644 --- a/internal/assets/static/main.css +++ b/internal/assets/static/main.css @@ -184,6 +184,57 @@ transform: rotate(-90deg); } +.widget-group-header { + overflow-x: auto; + scrollbar-width: thin; +} + +.widget-group-title { + background: none; + font: inherit; + border: none; + color: inherit; + text-transform: uppercase; + border-bottom: 1px solid transparent; + cursor: pointer; + flex-shrink: 0; + padding-bottom: 0.1rem; + transition: color .3s, border-color .3s; +} + +.widget-group-title:hover:not(.widget-group-title-current) { + border-bottom-color: var(--color-text-subdue); + color: var(--color-text-highlight); +} + +.widget-group-title-current { + border-bottom-color: var(--color-primary); + color: var(--color-text-highlight); +} + +.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; } @@ -1393,6 +1444,7 @@ kbd:active { .gap-7 { gap: 0.7rem; } .gap-10 { gap: 1rem; } .gap-15 { gap: 1.5rem; } +.gap-20 { gap: 2rem; } .gap-25 { gap: 2.5rem; } .gap-35 { gap: 3.5rem; } .gap-45 { gap: 4.5rem; } diff --git a/internal/assets/static/main.js b/internal/assets/static/main.js index c8f7324..35ab6a0 100644 --- a/internal/assets/static/main.js +++ b/internal/assets/static/main.js @@ -250,6 +250,46 @@ function setupDynamicRelativeTime() { }); } +function setupGroups() { + const groups = document.getElementsByClassName("widget-type-group"); + + if (groups.length == 0) { + return; + } + + for (let g = 0; g < groups.length; g++) { + const group = groups[g]; + const titles = group.getElementsByClassName("widget-header")[0].children; + const tabs = group.getElementsByClassName("widget-group-contents")[0].children; + let current = 0; + + for (let t = 0; t < titles.length; t++) { + const title = titles[t]; + title.addEventListener("click", () => { + if (t == current) { + return; + } + + for (let i = 0; i < titles.length; i++) { + titles[i].classList.remove("widget-group-title-current"); + tabs[i].classList.remove("widget-group-content-current"); + } + + if (current < t) { + tabs[t].dataset.direction = "right"; + } else { + tabs[t].dataset.direction = "left"; + } + + current = t; + + title.classList.add("widget-group-title-current"); + tabs[t].classList.add("widget-group-content-current"); + }); + } + } +} + function setupLazyImages() { const images = document.querySelectorAll("img[loading=lazy]"); @@ -558,6 +598,7 @@ async function setupPage() { setupSearchBoxes(); setupCollapsibleLists(); setupCollapsibleGrids(); + setupGroups(); setupDynamicRelativeTime(); setupLazyImages(); } finally { diff --git a/internal/assets/templates.go b/internal/assets/templates.go index 53ae871..8274c8c 100644 --- a/internal/assets/templates.go +++ b/internal/assets/templates.go @@ -37,6 +37,7 @@ var ( RepositoryTemplate = compileTemplate("repository.html", "widget-base.html") SearchTemplate = compileTemplate("search.html", "widget-base.html") ExtensionTemplate = compileTemplate("extension.html", "widget-base.html") + GroupTemplate = compileTemplate("group.html", "widget-base.html") ) var globalTemplateFunctions = template.FuncMap{ diff --git a/internal/assets/templates/group.html b/internal/assets/templates/group.html new file mode 100644 index 0000000..fe296fe --- /dev/null +++ b/internal/assets/templates/group.html @@ -0,0 +1,20 @@ +{{ template "widget-base.html" . }} + +{{ define "widget-content-classes" }}widget-content-frameless{{ end }} + +{{ define "widget-content" }} +
+ + + +{{ end }} diff --git a/internal/assets/templates/widget-base.html b/internal/assets/templates/widget-base.html index eed89e1..bdd30b9 100644 --- a/internal/assets/templates/widget-base.html +++ b/internal/assets/templates/widget-base.html @@ -1,4 +1,5 @@