diff options
author | Julius Härtl <jus@bitgrid.net> | 2022-08-27 10:57:13 +0200 |
---|---|---|
committer | Julius Härtl <jus@bitgrid.net> | 2022-08-31 10:24:03 +0200 |
commit | 5b4708c5be100c3a4bbb2fd32151ae2a7420df2d (patch) | |
tree | 181dc1020643ecc5434fa77bfac5606be3826ed4 /core | |
parent | 23bb4f16f9056e7a79116129c7de5b59cf84f8be (diff) | |
download | nextcloud-server-5b4708c5be100c3a4bbb2fd32151ae2a7420df2d.tar.gz nextcloud-server-5b4708c5be100c3a4bbb2fd32151ae2a7420df2d.zip |
Move app menu to vue
Signed-off-by: Julius Härtl <jus@bitgrid.net>
Diffstat (limited to 'core')
-rw-r--r-- | core/css/header.scss | 301 | ||||
-rw-r--r-- | core/src/components/AppMenu.vue | 268 | ||||
-rw-r--r-- | core/src/components/MainMenu.js | 98 | ||||
-rw-r--r-- | core/src/init.js | 81 | ||||
-rw-r--r-- | core/templates/layout.user.php | 65 |
5 files changed, 284 insertions, 529 deletions
diff --git a/core/css/header.scss b/core/css/header.scss index 5f3c4356d32..8ab0b019437 100644 --- a/core/css/header.scss +++ b/core/css/header.scss @@ -16,7 +16,6 @@ /* prevent ugly selection effect on accidental selection */ #header, -#navigation, #expanddiv { -webkit-user-select: none; -moz-user-select: none; @@ -72,7 +71,6 @@ /* Header menu */ $header-menu-entry-height: 44px; - .header-left > nav > .menu, .header-right > div > .menu { background-color: var(--color-main-background); filter: drop-shadow(0 1px 5px var(--color-box-shadow)); @@ -103,7 +101,6 @@ right: 10px; } - #apps > ul, & > div, & > ul { overflow-y: auto; @@ -111,8 +108,7 @@ @include header-menu-height(); } - /* Use by the apps menu and the settings right menu */ - #apps > ul, + /* Use by the settings right menu */ &.settings-menu > ul { li { a { @@ -182,16 +178,6 @@ padding-right: 10px; flex-shrink: 0; } - /* show caret indicator next to logo to make clear it is tappable */ - .icon-caret { - display: inline-block; - width: 12px; - height: 12px; - margin: 0; - margin-top: -21px; - padding: 0; - vertical-align: middle; - } #header-left, .header-left, #header-right, .header-right { @@ -245,27 +231,6 @@ opacity: .75; } -.menutoggle { - .icon-caret { - opacity: .75; - } - &:hover { - .header-appname, .icon-caret { - opacity: 1; - } - } - &:focus { - .header-appname, .icon-caret { - opacity: 1; - } - } - &.active { - .header-appname, .icon-caret { - opacity: 1; - } - } -} - /* TODO: move into minimal css file for public shared template */ /* only used for public share pages now as we have the app icons when logged in */ .header-appname { @@ -291,56 +256,6 @@ text-overflow: ellipsis; } -/* do not show menu toggle on public share links as there is no menu */ -#body-public #header .icon-caret { - display: none; -} - -/* NAVIGATION --------------------------------------------------------------- */ -nav[role='navigation'] { - display: inline-block; - width: variables.$header-height; - height: variables.$header-height; - margin-left: -#{variables.$header-height}; - position: relative; -} - -#header .header-left > nav > #navigation { - position: relative; - left: 25px; /* half the togglemenu */ - transform: translateX(-50%); - width: 160px; -} - -#header .header-left > nav > #navigation, -.ui-datepicker, -.ui-timepicker.ui-widget { - background-color: var(--color-main-background); - filter: drop-shadow(0 1px 10px var(--color-box-shadow)); - &:after { - /* position of dropdown arrow */ - left: 50%; - bottom: 100%; - border: solid transparent; - content: ' '; - height: 0; - width: 0; - position: absolute; - pointer-events: none; - border-color: rgba(0, 0, 0, 0); - border-bottom-color: var(--color-main-background); - border-width: 10px; - margin-left: -10px; /* border width */ - } -} - -#navigation { - box-sizing: border-box; - .in-header { - display: none; - } -} - /* USER MENU -----------------------------------------------------------------*/ #settings { display: inline-block; @@ -420,220 +335,6 @@ nav[role='navigation'] { } } -/* Apps menu */ -#appmenu { - display: inline-flex; - min-width: variables.$header-height; - z-index: 2; - - li { - position: relative; - cursor: pointer; - padding: 0 2px; - display: flex; - justify-content: center; - - a { - position: relative; - display: flex; - margin: 0; - height: calc(variables.$header-height - 6px); - width: variables.$header-height; - align-items: center; - justify-content: center; - opacity: .85; - // Make sure most app names don’t ellipsize - letter-spacing: -0.5px; - font-size: 12px; - margin: 2px; - } - - /* focused app visual feedback */ - &:hover a, - a:focus, - a.active { - opacity: 1; - font-weight: bold; - } - - // Text size back to normal for hover/focus - &:hover a, - a:focus { - font-size: 14px; - } - - &:hover a + span, - a:focus + span, - &:hover span, - &:focus span, - a:focus span, - a.active span { - display: inline-block; - text-overflow: initial; - width: auto; - overflow: hidden; - padding: 0 5px; - z-index: 2; - } - - /* hidden apps menu */ - img, - .icon-more-white { - display: inline-block; - width: 20px; - height: 20px; - } - - .icon-more-white { - background-image: url('../img/actions/more-white.svg?v=1'); - } - - /* App title */ - span { - opacity: 0; - position: absolute; - color: var(--color-primary-text); - bottom: 2px; - width: 100%; - text-align: center; - overflow: hidden; - text-overflow: ellipsis; - transition: all var(--animation-quick) ease; - pointer-events: none; - } - - /* Set up transitions for showing app titles on hover */ - /* App icon */ - svg, - .icon-more-white { - transition: transform var(--animation-quick) ease; - // If the primary is too bright, invert the app icons - filter: var(--primary-invert-if-bright); - } - - /* Triangle */ - a::before { - transition: border var(--animation-quick) ease; - } - } - - /* Show all app titles on hovering app menu area */ - &:hover { - li { - /* Move up app icon */ - svg, - .icon-more, - .icon-more-white, - .icon-loading-small, - .icon-loading-small-dark { - transform: translateY(-7px); - } - - /* Show app title */ - span { - opacity: 1; - bottom: 2px; - z-index: -1; /* fix clickability issue - otherwise we need to move the span into the link */ - } - - /* Prominent app title for current and hovered/focused app */ - &:hover span, - &:focus span, - .active + span { - opacity: 1; - } - - /* Smaller triangle because of limited space */ - a::before { - border-width: 5px; - } - } - } - - /* Also show app title on focusing single entry (showing all on focus is only possible with CSS4 and parent selectors) */ - li a:focus { - /* Move up app icon */ - svg, - .icon-more, - .icon-more-white, - .icon-loading-small, - .icon-loading-small-dark { - transform: translateY(-7px); - } - - /* Show app title */ - & + span, - span { - opacity: 1; - bottom: 2px; - } - - /* Smaller triangle because of limited space */ - &::before { - border-width: 5px; - } - } - - /* show triangle below active app */ - li a::before { - content: ' '; - height: 0; - width: 0; - position: absolute; - pointer-events: none; - border: 0 solid transparent; - border-bottom-color: var(--color-main-background); - border-width: 10px; - transform: translateX(-50%); - left: 50%; - bottom: -5px; - display: none; - } - - /* triangle focus feedback */ - li a.active::before, - li:hover a::before, - li:hover a.active::before, - li a:focus::before { - display: block; - } - li a.active::before { - z-index: 99; - } - li:hover a::before, - li a.active:hover::before, - li a:focus::before { - z-index: 101; - } - - li.hidden { - display: none; - } - - #more-apps { - z-index: 3; - } -} - -.unread-counter { - display: none; -} -#apps .app-icon-notification, -#appmenu .app-icon-notification { - fill: var(--color-error); -} - -#apps svg:not(.has-unread), -#appmenu svg:not(.has-unread) { - .app-icon-notification-mask { - display: none; - } - .app-icon-notification { - display: none; - } -} - - /* Skip navigation links – show only on keyboard focus */ #skip-actions { position: absolute; diff --git a/core/src/components/AppMenu.vue b/core/src/components/AppMenu.vue new file mode 100644 index 00000000000..a8ea7250852 --- /dev/null +++ b/core/src/components/AppMenu.vue @@ -0,0 +1,268 @@ +<!-- + - @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> + - + - @author Julius Härtl <jus@bitgrid.net> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + --> + +<template> + <nav class="app-menu"> + <ul class="app-menu-main"> + <li v-for="app in mainAppList" + :key="app.id" + :data-app-id="app.id" + class="app-menu-entry" + :class="{ 'app-menu-entry__active': app.active }"> + <a :href="app.href" :class="{ 'has-unread': app.unread > 0 }" :aria-label="appLabel(app)"> + <img :src="app.icon" alt=""> + <div class="app-menu-entry--label"> + {{ app.name }} + <span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span> + </div> + </a> + </li> + </ul> + <NcActions class="app-menu-more" :aria-label="t('core', 'More apps')"> + <NcActionLink v-for="app in popoverAppList" + :key="app.id" + :aria-label="appLabel(app)" + :href="app.href" + class="app-menu-popover-entry"> + <template #icon> + <div class="app-icon" :class="{ 'has-unread': app.unread > 0 }"> + <img :src="app.icon" alt=""> + </div> + </template> + {{ app.name }} + <span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span> + </NcActionLink> + </NcActions> + </nav> +</template> + +<script> +import { loadState } from '@nextcloud/initial-state' +import NcActions from '@nextcloud/vue/dist/Components/NcActions' +import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink' + +export default { + name: 'AppMenu', + components: { + NcActions, NcActionLink, + }, + data() { + return { + apps: loadState('core', 'apps', {}), + appLimit: 0, + } + }, + computed: { + appList() { + return Object.values(this.apps) + }, + mainAppList() { + return this.appList.slice(0, this.appLimit) + }, + popoverAppList() { + return this.appList.slice(this.appLimit) + }, + appLabel() { + return (app) => app.name + + (app.active ? ' (' + t('core', 'Currently open') + ')' : '') + + (app.unread > 0 ? ' (' + n('core', '{count} notification', '{count} notifications', app.unread, { count: app.unread }) + ')' : '') + }, + }, + mounted() { + window.addEventListener('resize', this.resize) + this.resize() + }, + methods: { + setNavigationCounter(id, counter) { + this.$set(this.apps[id], 'unread', counter) + }, + resize() { + const availableWidth = this.$el.offsetWidth + let appCount = Math.floor(availableWidth / 50) - 1 + const popoverAppCount = this.appList.length - appCount + if (popoverAppCount === 1) { + appCount-- + } + if (appCount < 1) { + appCount = 0 + } + this.appLimit = appCount + }, + }, +} +</script> + +<style lang="scss" scoped> +$header-icon-size: 20px; + +.app-menu { + width: 100%; + display: flex; + flex-shrink: 1; + flex-wrap: wrap; +} +.app-menu-main { + display: flex; + flex-wrap: nowrap; + + .app-menu-entry { + width: 50px; + height: 50px; + position: relative; + display: flex; + opacity: .7; + + &.app-menu-entry__active { + opacity: 1; + + &::before { + content: " "; + position: absolute; + pointer-events: none; + border: 8px solid transparent; + border-bottom-color: var(--color-main-background); + transform: translateX(-50%); + left: 50%; + bottom: 0; + display: block; + transition: all 0.1s ease-in-out; + opacity: 1; + } + + .app-menu-entry--label { + font-weight: bold; + } + } + + a { + width: 100%; + height: 100%; + color: var(--color-primary-text); + position: relative; + } + + img { + transition: margin 0.1s ease-in-out; + width: $header-icon-size; + height: $header-icon-size; + padding: calc((100% - $header-icon-size) / 2); + filter: var(--primary-invert-if-bright); + } + + .app-menu-entry--label { + opacity: 0; + position: absolute; + font-size: 12px; + color: var(--color-primary-text); + text-align: center; + bottom: -5px; + left: 50%; + display: block; + min-width: 100%; + transform: translateX(-50%); + transition: all 0.1s ease-in-out; + width: 100%; + text-overflow: ellipsis; + overflow: hidden; + } + + &:hover, + &:focus-within { + opacity: 1; + .app-menu-entry--label { + opacity: 1; + font-weight: bold; + font-size: 14px; + bottom: 0; + width: auto; + overflow: visible; + } + } + + } + + // Show labels + &:hover, + &:focus-within, + .app-menu-entry:hover, + .app-menu-entry:focus { + opacity: 1; + + img { + margin-top: -6px; + } + + .app-menu-entry--label { + opacity: 1; + bottom: 0; + } + + &::before, .app-menu-entry::before { + border-width: 3px; + } + } +} + +::v-deep .app-menu-more .button-vue--vue-tertiary { + color: var(--color-primary-text); + opacity: .7; + margin: 3px; + + &:hover { + opacity: 1; + background-color: transparent !important; + } +} + +.app-menu-popover-entry { + .app-icon { + position: relative; + height: 44px; + + &.has-unread::after { + background-color: var(--color-main-text); + } + + img { + filter: var(--background-invert-if-bright); + width: $header-icon-size; + height: $header-icon-size; + padding: calc((50px - $header-icon-size) / 2); + } + } +} + +.has-unread::after { + content: ""; + width: 8px; + height: 8px; + background-color: var(--color-primary-text); + border-radius: 50%; + position: absolute; + display: block; + top: 10px; + right: 10px; +} + +.unread-counter { + display: none; +} +</style> diff --git a/core/src/components/MainMenu.js b/core/src/components/MainMenu.js index 603338d05b3..267a3d9a361 100644 --- a/core/src/components/MainMenu.js +++ b/core/src/components/MainMenu.js @@ -22,99 +22,27 @@ * */ -import $ from 'jquery' +import { translate as t, translatePlural as n } from '@nextcloud/l10n' +import Vue from 'vue' -import OC from '../OC' +import AppMenu from './AppMenu.vue' -/** - * Set up the main menu toggle to react to media query changes. - * If the screen is small enough, the main menu becomes a toggle. - * If the screen is bigger, the main menu is not a toggle any more. - */ export const setUp = () => { - Object.assign(OC, { - setNavigationCounter(id, counter) { - const appmenuElement = document.getElementById('appmenu').querySelector('[data-id="' + id + '"] svg') - const appsElement = document.getElementById('apps').querySelector('[data-id="' + id + '"] svg') - if (counter === 0) { - appmenuElement.classList.remove('has-unread') - appsElement.classList.remove('has-unread') - appmenuElement.getElementsByTagName('image')[0].style.mask = '' - appsElement.getElementsByTagName('image')[0].style.mask = '' - } else { - appmenuElement.classList.add('has-unread') - appsElement.classList.add('has-unread') - appmenuElement.getElementsByTagName('image')[0].style.mask = 'url(#hole-appmenu-' + id + ')' - appsElement.getElementsByTagName('image')[0].style.mask = 'url(#hole-' + id + ')' - } - document.getElementById('appmenu').querySelector('[data-id="' + id + '"] .unread-counter').textContent = counter - document.getElementById('apps').querySelector('[data-id="' + id + '"] .unread-counter').textContent = counter + Vue.mixin({ + methods: { + t, + n, }, }) - // init the more-apps menu - OC.registerMenu($('#more-apps > a'), $('#navigation')) - - // toggle the navigation - const $toggle = $('#header .header-appname-container') - const $navigation = $('#navigation') - const $appmenu = $('#appmenu') - - // init the menu - OC.registerMenu($toggle, $navigation) - $toggle.data('oldhref', $toggle.attr('href')) - $toggle.attr('href', '#') - $navigation.hide() - // show loading feedback on more apps list - $navigation.delegate('a', 'click', event => { - let $app = $(event.target) - if (!$app.is('a')) { - $app = $app.closest('a') - } - if (event.which === 1 && !event.ctrlKey && !event.metaKey && $app.attr('target') !== '_blank') { - $app.find('svg').remove() - $app.find('div').remove() // prevent odd double-clicks - // no need for theming, loader is already inverted on dark mode - // but we need it over the primary colour - $app.prepend($('<div></div>').addClass('icon-loading-small')) - } else { - // Close navigation when opening app in - // a new tab - OC.hideMenus(() => false) - } - }) + const AppMenuApp = Vue.extend(AppMenu) + const appMenu = new AppMenuApp({}).$mount('#header-left__appmenu') - $navigation.delegate('a', 'mouseup', event => { - if (event.which === 2) { - // Close navigation when opening app in - // a new tab via middle click - OC.hideMenus(() => false) - } + Object.assign(OC, { + setNavigationCounter(id, counter) { + appMenu.setNavigationCounter(id, counter) + }, }) - // show loading feedback on visible apps list - $appmenu.delegate('li:not(#more-apps) > a', 'click', event => { - let $app = $(event.target) - if (!$app.is('a')) { - $app = $app.closest('a') - } - - if (event.which === 1 && !event.ctrlKey && !event.metaKey && $app.parent('#more-apps').length === 0 && $app.attr('target') !== '_blank') { - $app.find('svg').remove() - $app.find('div').remove() // prevent odd double-clicks - $app.prepend($('<div></div>').addClass( - OCA.Theming && OCA.Theming.inverted - ? 'icon-loading-small' - : 'icon-loading-small-dark' - )) - // trigger redirect - // needed for ie, but also works for every browser - window.location = $app.attr('href') - } else { - // Close navigation when opening app in - // a new tab - OC.hideMenus(() => false) - } - }) } diff --git a/core/src/init.js b/core/src/init.js index 507f5bbb35f..487114051ae 100644 --- a/core/src/init.js +++ b/core/src/init.js @@ -36,62 +36,7 @@ import { setUp as setUpMainMenu } from './components/MainMenu' import { setUp as setUpUserMenu } from './components/UserMenu' import PasswordConfirmation from './OC/password-confirmation' -// keep in sync with core/css/variables.scss -const breakpointMobileWidth = 1024 - -const resizeMenu = () => { - const appList = $('#appmenu li') - const rightHeaderWidth = $('.header-right').outerWidth() - const headerWidth = $('header').outerWidth() - const usePercentualAppMenuLimit = 0.67 - const minAppsDesktop = 12 - let availableWidth = headerWidth - $('#nextcloud').outerWidth() - (rightHeaderWidth > 210 ? rightHeaderWidth : 210) - const isMobile = $(window).width() < breakpointMobileWidth - if (!isMobile) { - availableWidth = availableWidth * usePercentualAppMenuLimit - } - let appCount = Math.floor((availableWidth / $(appList).width())) - if (isMobile && appCount > minAppsDesktop) { - appCount = minAppsDesktop - } - if (!isMobile && appCount < minAppsDesktop) { - appCount = minAppsDesktop - } - - // show at least 2 apps in the popover - if (appList.length - 1 - appCount >= 1) { - appCount-- - } - - $('#more-apps a').removeClass('active') - let lastShownApp - for (let k = 0; k < appList.length - 1; k++) { - const name = $(appList[k]).data('id') - if (k < appCount) { - $(appList[k]).removeClass('hidden') - $('#apps li[data-id=' + name + ']').addClass('in-header') - lastShownApp = appList[k] - } else { - $(appList[k]).addClass('hidden') - $('#apps li[data-id=' + name + ']').removeClass('in-header') - // move active app to last position if it is active - if (appCount > 0 && $(appList[k]).children('a').hasClass('active')) { - $(lastShownApp).addClass('hidden') - $('#apps li[data-id=' + $(lastShownApp).data('id') + ']').removeClass('in-header') - $(appList[k]).removeClass('hidden') - $('#apps li[data-id=' + name + ']').addClass('in-header') - } - } - } - - // show/hide more apps icon - if ($('#apps li:not(.in-header)').length === 0) { - $('#more-apps').hide() - $('#navigation').hide() - } else { - $('#more-apps').show() - } -} +const breakpointMobileWidth = getComputedStyle(document.documentElement).getPropertyValue('--breakpoint-mobile') const initLiveTimestamps = () => { // Update live timestamps every 30 seconds @@ -179,30 +124,6 @@ export const initCore = () => { setUpUserMenu() setUpContactsMenu() - // move triangle of apps dropdown to align with app name triangle - // 2 is the additional offset between the triangles - if ($('#navigation').length) { - $('#header #nextcloud + .menutoggle').on('click', () => { - $('#menu-css-helper').remove() - const caretPosition = $('.header-appname + .icon-caret').offset().left - 2 - if (caretPosition > 255) { - // if the app name is longer than the menu, just put the triangle in the middle - - } else { - $('head').append('<style id="menu-css-helper">#navigation:after { left: ' + caretPosition + 'px }</style>') - } - }) - $('#header #appmenu .menutoggle').on('click', () => { - $('#appmenu').toggleClass('menu-open') - if ($('#appmenu').is(':visible')) { - $('#menu-css-helper').remove() - } - }) - } - - $(window).resize(resizeMenu) - setTimeout(resizeMenu, 0) - // just add snapper for logged in users // and if the app doesn't handle the nav slider itself if ($('#app-navigation').length && !$('html').hasClass('lte9') diff --git a/core/templates/layout.user.php b/core/templates/layout.user.php index c7eb2fde5ad..05698b3aa67 100644 --- a/core/templates/layout.user.php +++ b/core/templates/layout.user.php @@ -65,70 +65,7 @@ $getUserAvatar = static function (int $size) use ($_): string { </div> </a> - <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']); ?>" - <?php if (isset($entry['target']) && $entry['target']): ?> target="_blank" rel="noreferrer noopener"<?php endif; ?> - <?php if ($entry['active']): ?> class="active"<?php endif; ?> - aria-label="<?php p($entry['name']); ?>"> - <svg width="24" height="20" viewBox="0 0 24 20"<?php if ($entry['unread'] !== 0) { ?> class="has-unread"<?php } ?>> - <defs> - <mask id="hole-appmenu-<?php p($entry['id']); ?>"> - <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" 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> - <span> - <?php p($entry['name']); ?> - </span> - </a> - </li> - <?php endforeach; ?> - <li id="more-apps" class="menutoggle" - aria-haspopup="true" aria-controls="navigation" aria-expanded="false"> - <a href="#" aria-label="<?php p($l->t('More apps')); ?>"> - <div class="icon-more-white"></div> - <span><?php p($l->t('More')); ?></span> - </a> - </li> - </ul> - - <nav role="navigation"> - <div id="navigation" style="display: none;" aria-label="<?php p($l->t('More apps menu')); ?>"> - <div id="apps"> - <ul> - <?php foreach ($_['navigation'] as $entry): ?> - <li data-id="<?php p($entry['id']); ?>"> - <a href="<?php print_unescaped($entry['href']); ?>" - <?php if (isset($entry['target']) && $entry['target']): ?> target="_blank" rel="noreferrer noopener"<?php endif; ?> - <?php if ($entry['active']): ?> class="active"<?php endif; ?> - aria-label="<?php p($entry['name']); ?>"> - <svg width="20" height="20" viewBox="0 0 16 16"<?php if ($entry['unread'] !== 0) { ?> class="has-unread"<?php } ?>> - <defs> - <filter id="invertMenuMore-<?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"></feColorMatrix></filter> - <mask id="hole-<?php p($entry['id']); ?>"> - <rect width="100%" height="100%" fill="white"/> - <circle r="4.5" cx="17" cy="3" fill="black"/> - </mask> - </defs> - <image x="0" y="0" width="16" height="16" preserveAspectRatio="xMinYMin meet" filter="url(#invertMenuMore-<?php p($entry['id']); ?>)" 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="17" cy="3" fill="red"/> - </svg> - <div class="unread-counter" aria-hidden="true"><?php p($entry['unread']); ?></div> - <span class="app-title"><?php p($entry['name']); ?></span> - </a> - </li> - <?php endforeach; ?> - </ul> - </div> - </div> - </nav> - + <nav id="header-left__appmenu"></nav> </div> <div class="header-right"> |