diff --git a/config.js b/config.js index 68ecd09cc..e3e9c022b 100644 --- a/config.js +++ b/config.js @@ -1,6 +1,7 @@ module.exports = { catalogUrl: null, catalogTitle: "STAC Browser", + catalogImage: null, allowExternalAccess: true, // Must be true if catalogUrl is not given allowedDomains: [], detectLocaleFromBrowser: true, diff --git a/docs/options.md b/docs/options.md index 04e2c4fb9..b664d1337 100644 --- a/docs/options.md +++ b/docs/options.md @@ -17,6 +17,7 @@ The following ways to set config options are possible: - [catalogUrl](#catalogurl) - [catalogTitle](#catalogtitle) +- [catalogImage](#catalogimage) - [allowExternalAccess](#allowexternalaccess) - [allowedDomains](#alloweddomains) - [apiCatalogPriority](#apicatalogpriority) @@ -71,6 +72,11 @@ If `catalogUrl` is empty or set to `null` STAC Browser switches to a mode where The default title shown if no title can be read from the root STAC catalog. +## catalogImage + +URL to an image to use as a logo with the title. +Should be an image that browsers can display, e.g. PNG, JPEG, WebP, or SVG. + ## allowExternalAccess This allows or disallows loading and browsing external STAC data. diff --git a/src/StacBrowser.vue b/src/StacBrowser.vue index 2876f97e8..e63c02019 100644 --- a/src/StacBrowser.vue +++ b/src/StacBrowser.vue @@ -5,8 +5,76 @@
- - + + +
+ +

+ + +

+ + {{ $t('server') }} + +
+ + +
+
+ + +
+ +

{{ title }}

+ +
+ +
+
@@ -17,32 +85,45 @@ + + + + - + - diff --git a/src/components/StacHeader.vue b/src/components/StacHeader.vue deleted file mode 100644 index bd4009a8c..000000000 --- a/src/components/StacHeader.vue +++ /dev/null @@ -1,192 +0,0 @@ - - - - - diff --git a/src/i18n.js b/src/i18n.js index 8a9295a78..d34010ef4 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -7,6 +7,7 @@ import Utils from './utils'; Vue.use(VueI18n); export const API_LANGUAGE_CONFORMANCE = ['https://api.stacspec.org/v1.*/language']; +export const STAC_LANGUAGE_EXT = 'https://stac-extensions.github.io/language/v1.*/schema.json' const LOCALE_CONFIG = {}; diff --git a/src/locales/en/texts.json b/src/locales/en/texts.json index 5e0e20b0c..e20645ecf 100644 --- a/src/locales/en/texts.json +++ b/src/locales/en/texts.json @@ -132,7 +132,7 @@ "in": "in {catalog}", "index": { "api": "API", - "catalog": "Catalog", + "catalog": "Static Catalog", "load": "Load", "selectStacIndex": "... or select one from {stacIndex}:", "specifyCatalog": "Please specify a STAC Catalog or API...", @@ -267,6 +267,7 @@ "title": "Search", "useInItemSearch": "Open Collection in Item Search | Open {count} Collections in Item Search" }, + "server": "Server", "showMore": "Show more...", "sidebar": { "switchCatalog": "Switch Catalog" diff --git a/src/store/index.js b/src/store/index.js index ea439247f..b44bf059b 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -99,8 +99,6 @@ function getStore(config, router) { } }, - displayCatalogTitle: (state, getters) => STAC.getDisplayTitle(getters.root, state.catalogTitle), - isCollection: state => state.data?.isCollection() || false, isCatalog: state => state.data?.isCatalog() || false, isCatalogLike: state => state.data?.isCatalogLike() || false, @@ -109,20 +107,20 @@ function getStore(config, router) { root: (_, getters) => getters.getStac(getters.rootLink), rootLink: state => { - let link = state.data?.getStacLinkWithRel('root'); + if (state.catalogUrl) { + return Utils.createLink(state.catalogUrl, 'root', state.catalogTitle); + } + const link = state.data?.getStacLinkWithRel('root'); if (link) { return link; } - else if (state.catalogUrl) { - return Utils.createLink(state.catalogUrl, 'root'); - } else if (state.url && state.data instanceof STAC && state.data.getLinksWithRels(['conformance', 'service-desc', 'service-doc', 'data', 'search']).length > 0) { - return Utils.createLink(state.url, 'root'); + return Utils.createLink(state.url, 'root', state.title); } else if (state.url) { // Fallback: If we detect OGC API like paths, try to guess the paths - let uri = URI(state.url); - let path = uri.segment(-2); + const uri = URI(state.url); + const path = uri.segment(-2); if (['collections', 'items'].includes(path)) { uri.segment(-1, ""); uri.segment(-1, ""); @@ -130,7 +128,7 @@ function getStore(config, router) { uri.segment(-1, ""); uri.segment(-1, ""); } - return Utils.createLink(uri.toString(), 'root'); + return Utils.createLink(uri.toString(), 'root', state.catalogTitle); } } return null; diff --git a/src/theme/page.scss b/src/theme/page.scss index 4b3cb8389..88447d3a7 100644 --- a/src/theme/page.scss +++ b/src/theme/page.scss @@ -1,264 +1,296 @@ -html, body { - height: 100%; - width: 100%; +html, +body { + height: 100%; + width: 100%; } body { - margin-top: $header-margin; + margin-top: $header-margin; } #stac-browser { - display: flex; - flex-direction: column; - max-width: 100%; - height: 100%; - min-height: 100%; - gap: $block-margin; + display: flex; + flex-direction: column; + max-width: 100%; + height: 100%; + min-height: 100%; + gap: $block-margin; + + @include media-breakpoint-only(xxxl) { + max-width: 75vw; + } - @include media-breakpoint-only(xxxl) { - max-width: 75vw; - } + > header { + .site { + border-bottom: 3px solid map-get($theme-colors, "primary"); - > header { - padding-top: $block-margin; + > .col-md-12 { + > .title { + font-size: 1.5rem; + font-weight: 600; + } - .lead { - color: map-get($theme-colors, 'secondary'); + .logo { + max-height: $logo-image-height; } + } } - .logo { - @if $logo == 'image' { - // Hide text - color: transparent; - font-size: 0; - // Show image - background-image: url($logo-image); - background-position: center; - background-repeat: no-repeat; - background-size: contain; - height: $logo-image-height; - margin-bottom: $block-margin; - } @else if $logo == 'none' { - display: none; + .site, + .page { + padding: $block-margin 0; + + > .col-md-12 { + display: flex; + gap: 1rem; + align-items: center; + + > .title { + word-break: break-word; + display: flex; + flex-grow: 1; + margin: 0; + padding: 0; + align-items: center; + gap: 1rem; + + h1, h2 { + margin: 0; + padding: 0; + } } + } } - > main { - flex: 1; - } - > footer { - padding-bottom: $block-margin; - text-align: center; - } + } + > main { + flex: 1; + } + > footer { + padding-bottom: $block-margin; + text-align: center; + } - .map { - height: 350px; - background-color: transparent; - @include border-radius($border-radius); - position: relative; - z-index: 0; + .popover-large { + width: 80%; + max-width: 800px; + + .popover-body { + overflow-y: auto; + overflow-x: hidden; + max-height: 80vh; } + } + + .map { + height: 350px; + background-color: transparent; + @include border-radius($border-radius); + position: relative; + z-index: 0; + } + h1 { + font-weight: 700; + display: flex; + align-items: center; + font-size: 2.2rem; + } + @include media-breakpoint-down(xl) { h1 { - font-weight: 700; - display: flex; - align-items: center; - font-size: 2.2rem; - } - @include media-breakpoint-down(xl) { - h1 { - font-size: 2rem; - } + font-size: 2rem; } - @include media-breakpoint-down(md) { - h1 { - font-size: 1.75rem; - } + } + @include media-breakpoint-down(md) { + h1 { + font-size: 1.75rem; } + } - h2 { - color: map-get($theme-colors, 'secondary'); - font-weight: 600; - } + h2 { + color: map-get($theme-colors, "secondary"); + font-weight: 600; + } - .maps-preview { - position: static; + .maps-preview { + position: static; - .nav-pills { - margin: 0; - padding: 0; - - > li { - margin: 0 0.5rem; - &:only-child { - display: none; - } - &:first-of-type { - margin-top: 0.5rem; - } - &:last-of-type { - margin-bottom: 0.5rem; - } - } + .nav-pills { + margin: 0; + padding: 0; + + > li { + margin: 0 0.5rem; + &:only-child { + display: none; + } + &:first-of-type { + margin-top: 0.5rem; + } + &:last-of-type { + margin-bottom: 0.5rem; } + } } + } - > .popover .items .card-columns.count-1 { - column-count: 1; - } + > .popover .items .card-columns.count-1 { + column-count: 1; + } - .service { - &.bsky { - @include button-variant(#1185fe, #1185fe); - } - &.mastodon { - @include button-variant(#6364FF, #6364FF); - } - &.x { - @include button-variant(#000000, #000000); - } + .service { + &.bsky { + @include button-variant(#1185fe, #1185fe); } - - .icon { - max-width: $max-icon-size; - max-height: $max-icon-size; + &.mastodon { + @include button-variant(#6364ff, #6364ff); } - h1 > .icon { - max-width: 1.2em; - max-height: 1.2em; + &.x { + @include button-variant(#000000, #000000); } + } - .global-error { - position: fixed; - bottom: 0; - right: 0; - z-index: 5000; - opacity: 0.9; - max-width: 50vh; - margin: 1rem; - } + .icon { + max-width: $max-icon-size; + max-height: $max-icon-size; + } + h1 > .icon { + max-width: 1.2em; + max-height: 1.2em; + } - abbr[title], abbr[data-original-title] { - text-decoration: none; - border-bottom: 1px dotted $body-color; - } + .global-error { + position: fixed; + bottom: 0; + right: 0; + z-index: 5000; + opacity: 0.9; + max-width: 50vh; + margin: 1rem; + } - [class*='col'] { - position: static; - } - .card { - background: transparent; - } + abbr[title], + abbr[data-original-title] { + text-decoration: none; + border-bottom: 1px dotted $body-color; + } - input:invalid { - border-color: map-get($theme-colors, "danger"); - } + [class*="col"] { + position: static; + } + .card { + background: transparent; + } - a.list-group-item { - color: $link-color; - text-decoration: $link-decoration; + input:invalid { + border-color: map-get($theme-colors, "danger"); + } - &:hover { - color: $link-hover-color; - text-decoration: $link-hover-decoration; - } + a.list-group-item { + color: $link-color; + text-decoration: $link-decoration; + + &:hover { + color: $link-hover-color; + text-decoration: $link-hover-decoration; } + } - .btn-group, .btn-group-vertical { - .btn-primary, .btn-secondary { - border-color: darken(map-get($theme-colors, "primary"), 15%); - &:hover { - border-color: darken(map-get($theme-colors, "primary"), 30%); - } - } - .btn-secondary { - background-color: lighten(desaturate(map-get($theme-colors, "primary"), 50%), 20%); - &:hover { - background-color: darken(map-get($theme-colors, "primary"), 5%); - } - } + .btn-group, + .btn-group-vertical { + .btn-primary, + .btn-secondary { + border-color: darken(map-get($theme-colors, "primary"), 15%); + &:hover { + border-color: darken(map-get($theme-colors, "primary"), 30%); + } } - .btn-group-vertical.actions { - .btn { - text-align: left - } + .btn-secondary { + background-color: lighten( + desaturate(map-get($theme-colors, "primary"), 50%), + 20% + ); + &:hover { + background-color: darken(map-get($theme-colors, "primary"), 5%); + } } - + } + .btn-group-vertical.actions { .btn { - &.disabled, - &:disabled { - cursor: not-allowed; - filter: grayscale(1); - } + text-align: left; } + } - .button-label { - display: none; + .btn { + &.disabled, + &:disabled { + cursor: not-allowed; + filter: grayscale(1); } - @include media-breakpoint-up(sm) { - .button-label.prio { - display: inline; - } + } + + .button-label { + display: none; + } + @include media-breakpoint-up(lg) { + .button-label { + display: inline-block; + margin-left: 0.25rem; } - @include media-breakpoint-up(md) { - .button-label { - display: inline; - } + } + + .expandable-card > .card-header { + padding: 0; + background-color: transparent; + } + .expandable-card > .card-header > .btn, + .mimic-expandable-card > .list-group-item { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.25rem; + padding: 0.5rem; + background-color: rgba(0, 0, 0, 0.03); + + > .title { + text-align: left; + flex-grow: 9; } + > .badges { + flex-grow: 1; + text-align: right; - .expandable-card > .card-header { - padding: 0; - background-color: transparent; + .badge { + margin: 0.125rem; + text-transform: uppercase; + } } - .expandable-card > .card-header > .btn, - .mimic-expandable-card > .list-group-item { - display: flex; - justify-content: space-between; - align-items: center; - gap: 0.25rem; - padding: 0.5rem; - background-color: rgba(0,0,0,0.03); - - > .title { - text-align: left; - flex-grow: 9; - } - > .badges { - flex-grow: 1; - text-align: right; - - .badge { - margin: 0.125rem; - text-transform: uppercase; - } - } + } + + .items, + .catalogs { + > .list { + position: relative; } - - .items, .catalogs { - > .list { - position: relative; - } - - > header { - margin-bottom: 0.5rem; - - > h2.title { - vertical-align: middle; - display: inline-block; - margin-bottom: 0.25rem; - } - .badge { - vertical-align: middle; - } - .btn-group { - vertical-align: middle; - margin: 0.25rem 0; - } - } + > header { + margin-bottom: 0.5rem; + + > h2.title { + vertical-align: middle; + display: inline-block; + margin-bottom: 0.25rem; + } + .badge { + vertical-align: middle; + } + .btn-group { + vertical-align: middle; + margin: 0.25rem 0; + } } + } } -@import '~vue-multiselect/dist/vue-multiselect.min.css'; +@import "~vue-multiselect/dist/vue-multiselect.min.css"; // Datepicker related style $default-color: map-get($theme-colors, "secondary"); @@ -266,7 +298,6 @@ $primary-color: map-get($theme-colors, "primary"); // Multiselect related style #stac-browser { - .multiselect__tags:focus-within { border-color: #48cce1; outline: 0; @@ -277,7 +308,7 @@ $primary-color: map-get($theme-colors, "primary"); color: #495057; border-color: #495057 transparent transparent; } - + .multiselect__tags, .multiselect__single { border-color: #ccc; @@ -315,7 +346,7 @@ $primary-color: map-get($theme-colors, "primary"); left: 0; width: 100vw; height: 100vh; - background-color: rgba(0,0,0, 0.5); + background-color: rgba(0, 0, 0, 0.5); margin: auto; display: flex; justify-content: center; @@ -323,12 +354,12 @@ $primary-color: map-get($theme-colors, "primary"); z-index: 9999; > form { - min-width: 200px; - width: 50vw; - border-radius: $border-radius; + min-width: 200px; + width: 50vw; + border-radius: $border-radius; - > .card { - background-color: #fff; - } + > .card { + background-color: #fff; + } } } diff --git a/src/theme/variables.scss b/src/theme/variables.scss index b5b27b713..0bc24fe36 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -3,11 +3,7 @@ // Most of the variables below are Bootstrap variables, see https://getbootstrap.com/docs/4.0/getting-started/theming/ for details. // For simplicity we just provide some common options as default, but you can override all Bootstrap Sass variables. -// Header / Logo, either image (see below), text (shows CATALOG_TITLE) or none -$logo: 'none'; -// If 'image' has been chosen above -$logo-image: 'https://raw.githubusercontent.com/radiantearth/stac-site/master/images/logo/stac-030-long.png'; -$logo-image-height: 100px; +$logo-image-height: 2rem; // Background and text default colors $body-bg: white; @@ -70,4 +66,4 @@ $header-margin: 0px; $border-radius: 0.25rem; // Max. icon size -$max-icon-size: 32px; \ No newline at end of file +$max-icon-size: 32px; diff --git a/src/utils.js b/src/utils.js index 3e8dee3eb..9ce37d936 100644 --- a/src/utils.js +++ b/src/utils.js @@ -444,8 +444,8 @@ export default class Utils { return searchterm[fn](term => target.includes(term)); } - static createLink(href, rel) { - return { href, rel }; + static createLink(href, rel, title) { + return { href, rel, title }; } static supportsExtension(data, pattern) { diff --git a/src/views/Catalog.vue b/src/views/Catalog.vue index 4c6e611e4..745a38aba 100644 --- a/src/views/Catalog.vue +++ b/src/views/Catalog.vue @@ -10,6 +10,7 @@ +