diff options
author | John Molakvoæ <skjnldsv@users.noreply.github.com> | 2022-04-22 13:32:14 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-22 13:32:14 +0300 |
commit | 9a76f06ecadf05ef1d26bd735df1bea0dfb15d59 (patch) | |
tree | ebdd458b0969c07d5ce77b21684a8bdf1db8434d /core | |
parent | e1cb1bdce94fa2a6395b2d3a35556944111e66b1 (diff) | |
parent | 850d8ac1cd9e5b28e37668469237d8daa5c5d51d (diff) |
Merge pull request #31751 from nextcloud/theming-providers
Diffstat (limited to 'core')
-rw-r--r-- | core/css/apps.scss | 3 | ||||
-rw-r--r-- | core/css/guest.css | 65 | ||||
-rw-r--r-- | core/css/header.scss | 63 | ||||
-rw-r--r-- | core/css/icons.scss | 8 | ||||
-rw-r--r-- | core/css/styles.scss | 1 | ||||
-rw-r--r-- | core/shipped.json | 1 | ||||
-rw-r--r-- | core/src/components/login/LoginButton.vue | 62 | ||||
-rw-r--r-- | core/src/components/login/LoginForm.vue | 6 | ||||
-rw-r--r-- | core/src/components/login/PasswordLessLoginForm.vue | 5 | ||||
-rw-r--r-- | core/src/components/login/ResetPassword.vue | 21 | ||||
-rw-r--r-- | core/src/components/login/UpdatePassword.vue | 23 | ||||
-rw-r--r-- | core/src/views/Login.vue | 6 | ||||
-rw-r--r-- | core/src/views/UnifiedSearch.vue | 17 | ||||
-rw-r--r-- | core/templates/layout.guest.php | 4 | ||||
-rw-r--r-- | core/templates/layout.user.php | 9 |
15 files changed, 130 insertions, 164 deletions
diff --git a/core/css/apps.scss b/core/css/apps.scss index 15742786b0e..84179a94633 100644 --- a/core/css/apps.scss +++ b/core/css/apps.scss @@ -166,6 +166,7 @@ kbd { &, > a { background-color: var(--color-primary-light); + color: var(--color-primary-text); } } @@ -285,6 +286,8 @@ kbd { margin-right: 11px; width: 16px; height: 16px; + // Legacy invert if bright background + filter: var(--background-invert-if-dark); } /* counter can also be inside the link */ diff --git a/core/css/guest.css b/core/css/guest.css index d48713061ec..a6d2abf9820 100644 --- a/core/css/guest.css +++ b/core/css/guest.css @@ -15,20 +15,18 @@ a, a *, input, input *, select, .button span, label { cursor:pointer; } ul { list-style:none; } body { - background-color: #ffffff; font-weight: normal; /* bring the default font size up to 14px */ font-size: .875em; line-height: 1.6em; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; - color: #000; + color: var(--color-primary-text); text-align: center; - background-color: #0082c9; - background-image: url('../img/background.png?v=2'); - background-image: url('../img/background.png?v=2'), linear-gradient(40deg, #0082c9 0%, rgba(28,175,255,1) 100%); + background-color: var(--color-primary); + background-image: var(--image-background, var(--image-background-plain, url('../../../core/img/background.svg'), linear-gradient(40deg, #0082c9 0%, #30b6ff 100%))); background-position: 50% 50%; background-repeat: repeat; - background-size: 275px, contain; + background-size: var(--image-background-size, 275px, contain); background-attachment: fixed; /* fix background gradient */ min-height: 100%; /* fix sticky footer */ height: auto; @@ -36,7 +34,7 @@ body { /* Various fonts settings */ #body-login a { - color: #fff; + color: var(--color-primary-text); font-weight: 600; } #body-login a:not(.button):hover, @@ -46,7 +44,7 @@ body { } #showAdvanced { - color: #fff; + color: var(--color-primary-text); } em { @@ -80,13 +78,12 @@ body { } #header .logo { - background-image: url('../img/logo/logo.svg?v=1'); + background-image: var(--image-logo, url('../../core/img/logo/logo.svg')); background-repeat: no-repeat; - background-size: 175px; + background-size: contain; background-position: center; - width: 256px; - min-height: 128px; - max-height: 200px; + width: 175px; + height: 130px; margin: 0 auto; position: relative; left: unset; @@ -185,7 +182,7 @@ form #datadirField legend { } .alternative-logins .button { - color: #ffffff; + color: var(--color-text-lighter); padding: 12px 20px; } @@ -263,9 +260,9 @@ select { width: auto; min-width: 25px; padding: 12px; - background-color: white; + background-color: var(--color-main-background); font-weight: bold; - color: #555; + color: var(--color-text-lighter); border: none; border-radius: 100px; /* --border-radius-pill */ cursor: pointer; @@ -280,7 +277,7 @@ input[type='password'], input[type='email'] { width: 249px; background: #fff; - color: #555; + color: var(--color-text-lighter); cursor: text; font-family: inherit; -webkit-appearance: textfield; @@ -338,9 +335,9 @@ input::-moz-focus-inner { input.primary, button.primary, a.primary { - border: 1px solid #fff; - background-color: #0082c9; - color: #fff; + border: 1px solid var(--color-primary-text); + background-color: var(--color-primary); + color: var(--color-primary-text); transition: color 100ms ease-in-out; } @@ -350,8 +347,8 @@ button.primary:not(:disabled):hover, button.primary:not(:disabled):focus, a.primary:not(:disabled):hover, a.primary:not(:disabled):focus { - color: rgba(255, 255, 255, 1); - background-image: linear-gradient(40deg, #0082c9 0%, #30b6ff 100%); + color: var(--color-primary-text); + background-image: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-light) 100%); background-position: initial; } @@ -479,13 +476,13 @@ form fieldset legend, form fieldset .warning-info, form input[type='checkbox']+label { text-align: center; - color: #fff; + color: var(--color-primary-text); } /* overrides another !important statement that sets this to unreadable black */ form .warning input[type='checkbox']:hover+label, form .warning input[type='checkbox']:focus+label, form .warning input[type='checkbox']+label { - color: #fff !important; + color: var(--color-primary-text) !important; } .body-login-container.two-factor { @@ -559,7 +556,7 @@ form .warning input[type='checkbox']+label { display: inline-block; font-weight: normal !important; padding: 12px; - color: #fff; + color: var(--color-primary-text); cursor: pointer; text-shadow: 0 0 2px rgba(0, 0, 0, .4); /* better readability on bright background */ } @@ -569,7 +566,7 @@ form .warning input[type='checkbox']+label { #forgot-password { padding: 11px; float: right; - color: #fff; + color: var(--color-primary-text); } /* Alternative Logins */ @@ -587,16 +584,16 @@ form .warning input[type='checkbox']+label { display: inline-block; text-align: center; box-sizing: border-box; - border: 2px solid #ffffff; - background-color: #0082c9; - color: #ffffff; + border: 2px solid var(--color-primary-text); + background-color: var(--color-primary); + color: var(--color-primary-text); border-radius: 100px; /* --border-radius-pill */ } .alternative-logins a.button:focus, .alternative-logins li a:focus { - border: 2px solid #000000; - background-image: linear-gradient(40deg, #0082c9 0%, #30b6ff 100%); + border: 2px solid var(--color-primary-hover); + background-image: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-light) 100%); background-position: initial; } @@ -854,6 +851,7 @@ p.info { } .loading, .loading-small, .icon-loading, .icon-loading-dark, .icon-loading-small, .icon-loading-small-dark { position: relative; + filter: var(--background-invert-if-dark) } .loading:after, .loading-small:after, .icon-loading:after, .icon-loading-dark:after, .icon-loading-small:after, .icon-loading-small-dark:after { z-index: 2; @@ -871,6 +869,9 @@ p.info { -ms-transform-origin: center; transform-origin: center; } +.primary .loading,.primary+.loading,.primary .loading-small,.primary+.loading-small,.primary .icon-loading,.primary+.icon-loading,.primary .icon-loading-dark,.primary+.icon-loading-dark,.primary .icon-loading-small,.primary+.icon-loading-small,.primary .icon-loading-small-dark,.primary+.icon-loading-small-dark { + filter: var(--primary-invert-if-bright) +} .loading:after, .loading-small:after, .icon-loading:after, .icon-loading-dark:after, .icon-loading-small:after, .icon-loading-small-dark:after { border: 2px solid rgba(150, 150, 150, 0.5); border-top-color: #646464; @@ -920,7 +921,7 @@ img.icon-loading-small-dark, object.icon-loading-small-dark, video.icon-loading- /* FOOTER */ footer { - height: 70px; + min-height: 70px; margin-top: auto; } diff --git a/core/css/header.scss b/core/css/header.scss index 27a8fe289fa..4c107c0d58f 100644 --- a/core/css/header.scss +++ b/core/css/header.scss @@ -165,6 +165,7 @@ margin-right: 10px; height: 16px; width: 16px; + filter: var(--background-invert-if-dark); } } } @@ -172,7 +173,7 @@ } .logo { display: inline-flex; - background-image: var(--image-logo); + background-image: var(--image-logoheader, var(--image-logo, url('../img/logo/logo.svg'))); background-repeat: no-repeat; background-size: contain; background-position: center; @@ -449,6 +450,11 @@ nav[role='navigation'] { // Make sure most app names don’t ellipsize letter-spacing: -0.5px; font-size: 12px; + + // If the primary is too bright, invert the app icons + svg image { + filter: var(--primary-invert-if-bright); + } } /* focused app visual feedback */ @@ -653,61 +659,6 @@ nav[role='navigation'] { } } -/* SEARCHBOX --------------------------------------------------------------- */ -.searchbox { - position: relative; - display: flex; - align-items: center; - input[type='search'] { - position: relative; - font-size: 1.2em; - padding: 3px; - padding-left: 25px; - padding-right: 20px; - background-color: transparent; - color: var(--color-primary-text); - border: 0; - border-radius: var(--border-radius-pill); - height: 34px; - width: 0; - cursor: pointer; - transition: width 100ms, opacity 100ms; - opacity: .6; - &:focus, &:active, &:valid { - background-position-x: 6px; - color: var(--color-primary-text); - width: 155px; - cursor: text; - background-color: transparent !important; - border: 1px solid var(--color-primary-text) !important; - } - &:hover, &:focus, &:active { - opacity: 1; - } - & ~ .icon-close-white { - display: inline; - position: absolute; - width: 30px; - height: 100%; - right: 0; - top: 0; - margin: 0; - &, &:focus, &:active, &:hover { - border: none; - background-color: transparent; - } - } - &:not(:valid) ~ .icon-close-white { - display: none; - } - &::-webkit-search-cancel-button { - -webkit-appearance: none; - } - } - .icon-search-force-white { - @include icon-color('search', 'actions', '#fffffe', 1, true); - } -} /* Empty content messages in the header e.g. notifications, contacts menu, … */ header #emptycontent, diff --git a/core/css/icons.scss b/core/css/icons.scss index 1468c480046..78522895cf5 100644 --- a/core/css/icons.scss +++ b/core/css/icons.scss @@ -64,6 +64,14 @@ transform-origin: center; border: 2px solid var(--color-loading-light); border-top-color: var(--color-loading-dark); + // revert if background is too bright + filter: var(--background-invert-if-dark); + + .primary &, + .primary + & { + // revert if primary is too bright + filter: var(--primary-invert-if-bright); + } } } diff --git a/core/css/styles.scss b/core/css/styles.scss index 8a15cfa19d8..27e5675b53a 100644 --- a/core/css/styles.scss +++ b/core/css/styles.scss @@ -975,6 +975,7 @@ span.ui-icon { background-size: 20px 20px; padding: 14px; cursor: pointer; + filter: var(--primary-invert-if-bright); &:hover, &:focus, diff --git a/core/shipped.json b/core/shipped.json index 14ee28a71d0..8a63cf20248 100644 --- a/core/shipped.json +++ b/core/shipped.json @@ -1,6 +1,5 @@ { "shippedApps": [ - "accessibility", "activity", "admin_audit", "circles", diff --git a/core/src/components/login/LoginButton.vue b/core/src/components/login/LoginButton.vue index f7d426e6c63..e99398ba09a 100644 --- a/core/src/components/login/LoginButton.vue +++ b/core/src/components/login/LoginButton.vue @@ -20,25 +20,33 @@ --> <template> - <div id="submit-wrapper" @click="$emit('click')"> - <input id="submit-form" - type="submit" - class="login primary" + <div class="submit-wrapper" @click="$emit('click')"> + <input type="submit" + class="submit-wrapper__input primary" title="" - :value="!loading ? t('core', 'Log in') : t('core', 'Logging in …')"> - <div class="submit-icon" - :class="{ - 'icon-confirm-white': !loading, - 'icon-loading-small': loading && invertedColors, - 'icon-loading-small-dark': loading && !invertedColors, - }" /> + :value="!loading ? value : valueLoading"> + <div v-if="loading" class="submit-wrapper__icon icon-loading-small-dark" /> + <ArrowRight v-else class="submit-wrapper__icon" /> </div> </template> <script> +import ArrowRight from 'vue-material-design-icons/ArrowRight.vue' + export default { name: 'LoginButton', + components: { + ArrowRight, + }, props: { + value: { + type: String, + default: t('core', 'Log in'), + }, + valueLoading: { + type: String, + default: t('core', 'Logging in …'), + }, loading: { type: Boolean, required: true, @@ -51,6 +59,36 @@ export default { } </script> -<style scoped> +<style scoped lang="scss"> +.submit-wrapper { + display: flex; + align-items: center; + justify-content: center; + padding: 10px 5px; + position: relative; + margin: 0 auto; + &__input { + width: 260px; + height: 50px; + } + + &__icon { + display: flex; + position: absolute; + right: 24px; + transition: right 100ms ease-in-out; + /* The submit icon is positioned on the submit button. + From the user point of view the icon is part of the + button, so the clicks on the icon have to be + applied to the button instead. */ + pointer-events: none; + } + + &__input:hover + &__icon:not(.icon-loading-small-dark), + &__input:focus + &__icon:not(.icon-loading-small-dark), + &__input:active + &__icon:not(.icon-loading-small-dark) { + right: 20px; + } +} </style> diff --git a/core/src/components/login/LoginForm.vue b/core/src/components/login/LoginForm.vue index e702961d50e..9114c0a728a 100644 --- a/core/src/components/login/LoginForm.vue +++ b/core/src/components/login/LoginForm.vue @@ -89,7 +89,7 @@ </a> </p> - <LoginButton :loading="loading" :inverted-colors="invertedColors" /> + <LoginButton :loading="loading" /> <p v-if="invalidPassword" class="warning wrongPasswordMsg"> @@ -158,10 +158,6 @@ export default { type: Number, default: 0, }, - invertedColors: { - type: Boolean, - default: false, - }, autoCompleteAllowed: { type: Boolean, default: true, diff --git a/core/src/components/login/PasswordLessLoginForm.vue b/core/src/components/login/PasswordLessLoginForm.vue index 94e7b35fa0e..3c0013936f0 100644 --- a/core/src/components/login/PasswordLessLoginForm.vue +++ b/core/src/components/login/PasswordLessLoginForm.vue @@ -25,7 +25,6 @@ <LoginButton v-if="validCredentials" :loading="loading" - :inverted-colors="invertedColors" @click="authenticate" /> </fieldset> </form> @@ -74,10 +73,6 @@ export default { type: [String, Boolean], default: false, }, - invertedColors: { - type: Boolean, - default: false, - }, autoCompleteAllowed: { type: Boolean, default: true, diff --git a/core/src/components/login/ResetPassword.vue b/core/src/components/login/ResetPassword.vue index 79589a96aca..7a499baa2f0 100644 --- a/core/src/components/login/ResetPassword.vue +++ b/core/src/components/login/ResetPassword.vue @@ -37,17 +37,7 @@ <label for="user" class="infield">{{ t('core', 'Username or email') }}</label> </p> <div id="reset-password-wrapper"> - <input id="reset-password-submit" - type="submit" - class="login primary" - title="" - :value="t('core', 'Reset password')"> - <div class="submit-icon" - :class="{ - 'icon-confirm-white': !loading, - 'icon-loading-small': loading && invertedColors, - 'icon-loading-small-dark': loading && !invertedColors, - }" /> + <LoginButton :value="t('core', 'Reset password')" /> </div> <p v-if="message === 'send-success'" class="update"> @@ -77,11 +67,14 @@ <script> import axios from '@nextcloud/axios' - import { generateUrl } from '@nextcloud/router' +import LoginButton from './LoginButton.vue' export default { name: 'ResetPassword', + components: { + LoginButton, + }, props: { username: { type: String, @@ -91,10 +84,6 @@ export default { type: String, required: true, }, - invertedColors: { - type: Boolean, - default: false, - }, }, data() { return { diff --git a/core/src/components/login/UpdatePassword.vue b/core/src/components/login/UpdatePassword.vue index 95778b993c8..36a63a6254a 100644 --- a/core/src/components/login/UpdatePassword.vue +++ b/core/src/components/login/UpdatePassword.vue @@ -49,18 +49,9 @@ </label> </div> - <div id="submit-wrapper"> - <input id="submit" - type="submit" - class="login primary" - title="" - :value="!loading ? t('core', 'Reset password') : t('core', 'Resetting password')"> - <div class="submit-icon" - :class="{ - 'icon-loading-small': loading && invertedColors, - 'icon-loading-small-dark': loading && !invertedColors - }" /> - </div> + <LoginButton :loading="loading" + :value="t('core', 'Reset password')" + :value-loading="t('core', 'Resetting password')" /> <p v-if="error && message" :class="{warning: error}"> {{ message }} @@ -71,9 +62,13 @@ <script> import Axios from '@nextcloud/axios' +import LoginButton from './LoginButton.vue' export default { name: 'UpdatePassword', + components: { + LoginButton, + }, props: { username: { type: String, @@ -83,10 +78,6 @@ export default { type: String, required: true, }, - invertedColors: { - type: Boolean, - default: false, - }, }, data() { return { diff --git a/core/src/views/Login.vue b/core/src/views/Login.vue index 70ac8da6f55..e2ca484126c 100644 --- a/core/src/views/Login.vue +++ b/core/src/views/Login.vue @@ -30,7 +30,6 @@ :messages="messages" :errors="errors" :throttle-delay="throttleDelay" - :inverted-colors="invertedColors" :auto-complete-allowed="autoCompleteAllowed" @submit="loading = true" /> <a v-if="canResetPassword && resetPasswordLink !== ''" @@ -68,7 +67,6 @@ class="login-additional"> <PasswordLessLoginForm :username.sync="user" :redirect-url="redirectUrl" - :inverted-colors="invertedColors" :auto-complete-allowed="autoCompleteAllowed" :is-https="isHttps" :is-localhost="isLocalhost" @@ -85,14 +83,12 @@ <ResetPassword v-if="resetPassword" :username.sync="user" :reset-password-link="resetPasswordLink" - :inverted-colors="invertedColors" @abort="resetPassword = false" /> </div> </div> <div v-else-if="resetPasswordTarget !== ''"> <UpdatePassword :username.sync="user" :reset-password-target="resetPasswordTarget" - :inverted-colors="invertedColors" @done="passwordResetFinished" /> </div> </transition> @@ -150,7 +146,6 @@ export default { messages: loadState('core', 'loginMessages', []), redirectUrl: loadState('core', 'loginRedirectUrl', false), throttleDelay: loadState('core', 'loginThrottleDelay', 0), - invertedColors: OCA.Theming && OCA.Theming.inverted, canResetPassword: loadState('core', 'loginCanResetPassword', false), resetPasswordLink: loadState('core', 'loginResetPasswordLink', ''), autoCompleteAllowed: loadState('core', 'loginAutocomplete', true), @@ -165,6 +160,7 @@ export default { hideLoginForm: loadState('core', 'hideLoginForm', false), } }, + methods: { passwordResetFinished() { this.resetPasswordTarget = '' diff --git a/core/src/views/UnifiedSearch.vue b/core/src/views/UnifiedSearch.vue index 89f5d2a0bab..2d2d2d0a254 100644 --- a/core/src/views/UnifiedSearch.vue +++ b/core/src/views/UnifiedSearch.vue @@ -30,7 +30,7 @@ <!-- Header icon --> <template #trigger> <Magnify class="unified-search__trigger" - :size="20" + :size="22/* fit better next to other 20px icons */" fill-color="var(--color-primary-text)" /> </template> @@ -81,15 +81,21 @@ <!-- Loading placeholders --> <SearchResultPlaceholders v-if="isLoading" /> - <EmptyContent v-else-if="isValidQuery" icon="icon-search"> + <EmptyContent v-else-if="isValidQuery"> <Highlight v-if="triggered" :text="t('core', 'No results for {query}', { query })" :search="query" /> <div v-else> {{ t('core', 'Press enter to start searching') }} </div> + <template #icon> + <Magnify /> + </template> </EmptyContent> - <EmptyContent v-else-if="!isLoading || isShortQuery" icon="icon-search"> + <EmptyContent v-else-if="!isLoading || isShortQuery"> {{ t('core', 'Start typing to search') }} + <template #icon> + <Magnify /> + </template> <template v-if="isShortQuery" #desc> {{ n('core', 'Please enter {minSearchLength} character or more to search', @@ -677,11 +683,6 @@ $input-height: 34px; $input-padding: 6px; .unified-search { - &__trigger { - width: 20px; - height: 20px; - } - &__input-wrapper { position: sticky; // above search results diff --git a/core/templates/layout.guest.php b/core/templates/layout.guest.php index b97181d9457..980d9abb7c4 100644 --- a/core/templates/layout.guest.php +++ b/core/templates/layout.guest.php @@ -36,10 +36,6 @@ <h1 class="hidden-visually"> <?php p($theme->getName()); ?> </h1> - <?php if (\OC::$server->getConfig()->getSystemValue('installed', false) - && \OC::$server->getConfig()->getAppValue('theming', 'logoMime', false)): ?> - <img src="<?php p($theme->getLogo()); ?>"/> - <?php endif; ?> </div> </div> </header> diff --git a/core/templates/layout.user.php b/core/templates/layout.user.php index 2b84c89fca6..4efe072a5bb 100644 --- a/core/templates/layout.user.php +++ b/core/templates/layout.user.php @@ -40,7 +40,9 @@ $getUserAvatar = static function (int $size) use ($_): string { <?php emit_script_loading_tags($_); ?> <?php print_unescaped($_['headers']); ?> </head> - <body id="<?php p($_['bodyid']);?>"> + <body id="<?php p($_['bodyid']);?>" <?php foreach ($_['enabledThemes'] as $themeId) { + p("data-theme-$themeId "); + }?>> <?php include 'layout.noscript.warning.php'; ?> <?php foreach ($_['initialStates'] as $app => $initialState) { ?> @@ -64,7 +66,7 @@ $getUserAvatar = static function (int $size) use ($_): string { </div> </a> - <ul id="appmenu" <?php if ($_['themingInvertMenu']) { ?>class="inverted"<?php } ?>> + <ul id="appmenu"> <?php foreach ($_['navigation'] as $entry): ?> <li data-id="<?php p($entry['id']); ?>" class="hidden" tabindex="-1"> <a href="<?php print_unescaped($entry['href']); ?>" @@ -73,13 +75,12 @@ $getUserAvatar = static function (int $size) use ($_): string { aria-label="<?php p($entry['name']); ?>"> <svg width="24" height="20" viewBox="0 0 24 20" alt=""<?php if ($entry['unread'] !== 0) { ?> class="has-unread"<?php } ?>> <defs> - <?php if ($_['themingInvertMenu']) { ?><filter id="invertMenuMain-<?php p($entry['id']); ?>"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0" /></filter><?php } ?> <mask id="hole"> <rect width="100%" height="100%" fill="white"/> <circle r="4.5" cx="21" cy="3" fill="black"/> </mask> </defs> - <image x="2" y="0" width="20" height="20" preserveAspectRatio="xMinYMin meet"<?php if ($_['themingInvertMenu']) { ?> filter="url(#invertMenuMain-<?php p($entry['id']); ?>)"<?php } ?> xlink:href="<?php print_unescaped($entry['icon'] . '?v=' . $_['versionHash']); ?>" style="<?php if ($entry['unread'] !== 0) { ?>mask: url("#hole");<?php } ?>" class="app-icon"></image> + <image x="2" y="0" width="20" height="20" preserveAspectRatio="xMinYMin meet" xlink:href="<?php print_unescaped($entry['icon'] . '?v=' . $_['versionHash']); ?>" style="<?php if ($entry['unread'] !== 0) { ?>mask: url("#hole");<?php } ?>" class="app-icon"></image> <circle class="app-icon-notification" r="3" cx="21" cy="3" fill="red"/> </svg> <div class="unread-counter" aria-hidden="true"><?php p($entry['unread']); ?></div> |