feat(core): show real per-result icons in unified search

Signed-off-by: Peter Ringelmann <peter.ringelmann@nextcloud.com>
pull/62605/head
Peter Ringelmann 1 month ago
parent 07483662c1
commit e05c609a2f
No known key found for this signature in database
  1. 3
      apps/appstore/lib/Search/AppSearch.php
  2. 13
      apps/settings/lib/Search/SectionSearch.php
  3. 76
      core/src/components/AppIcon.vue
  4. 56
      core/src/components/AppItem.vue

@ -74,7 +74,8 @@ final readonly class AppSearch implements IProvider {
$entry['name'],
'',
$entry['href'],
'icon-confirm'
$entry['icon'],
true,
);
}

@ -121,17 +121,20 @@ class SectionSearch implements IProvider {
continue;
}
/**
* We can't use the icon URL at the moment as they don't invert correctly for dark theme
* $iconUrl = $section->getIcon();
*/
// The section's own icon, falling back to a generic cog when it has none.
// These are dark monochrome glyphs; the client inverts them for dark
// themes via --background-invert-if-dark.
$icon = $section->getIcon();
if ($icon === '') {
$icon = $this->urlGenerator->imagePath('settings', 'settings.svg');
}
$result[] = new SearchResultEntry(
'',
$section->getName(),
$subline,
$this->urlGenerator->linkToRouteAbsolute($routeName, ['section' => $section->getID()]),
'icon-settings-dark'
$icon,
);
}
}

@ -0,0 +1,76 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<span
class="app-icon"
:class="{ 'app-icon--outlined': outlined }">
<img
class="app-icon__img"
:src="icon"
alt=""
aria-hidden="true">
<!-- @slot Overlay positioned over the circle, e.g. an unread badge. -->
<slot />
</span>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
/** URL of the app icon. Painted bright on the coloured circle, like the app menu. */
icon: string
/** Render the circle as an outline only (no fill or gradient). */
outlined?: boolean
}>(), {
outlined: false,
})
</script>
<style scoped lang="scss">
.app-icon {
--app-icon-circle-size: calc(var(--default-grid-baseline) * 10);
--app-icon-icon-size: 22px;
box-sizing: border-box;
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: var(--app-icon-circle-size);
height: var(--app-icon-circle-size);
border-radius: 50%;
background-color: var(--color-primary-element);
background-image: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.18) 0%,
rgba(255, 255, 255, 0) 45%,
rgba(0, 0, 0, 0.15) 100%
);
box-shadow:
inset 0 1px 0 0 rgba(255, 255, 255, 0.25),
inset 0 -1px 0 0 rgba(0, 0, 0, 0.2),
0 2px 4px rgba(0, 0, 0, 0.15);
&__img {
width: var(--app-icon-icon-size);
height: var(--app-icon-icon-size);
// App icons are bright by default; flip them to dark when the
// primary color (circle background) is bright (e.g. white in dark mode).
filter: var(--primary-invert-if-bright);
mask: var(--header-menu-icon-mask);
}
// Outlined variant: no fill or gradient.
&--outlined {
background: transparent;
background-image: none;
box-shadow: inset 0 0 0 2px var(--color-border-maxcontrast);
}
&--outlined &__img {
filter: var(--background-invert-if-dark);
mask: none;
}
}
</style>

@ -8,7 +8,6 @@
class="app-item"
:class="{
'app-item--active': app.active,
'app-item--outlined': outlined,
}"
:href="app.href"
:target="newTab ? '_blank' : undefined"
@ -17,17 +16,12 @@
:tabindex="tabindex"
:title="app.name"
role="menuitem">
<span class="app-item__circle">
<img
class="app-item__icon"
:src="app.icon"
alt=""
aria-hidden="true">
<AppIcon :icon="app.icon" :outlined="outlined">
<span
v-if="app.unread"
class="app-item__unread"
aria-hidden="true" />
</span>
</AppIcon>
<span class="app-item__label">
{{ app.name }}
<span v-if="app.unread" class="hidden-visually">, {{ unreadLabel }}</span>
@ -40,6 +34,7 @@ import type { INavigationEntry } from '../types/navigation.d.ts'
import { n } from '@nextcloud/l10n'
import { computed } from 'vue'
import AppIcon from './AppIcon.vue'
const props = withDefaults(defineProps<{
app: INavigationEntry
@ -73,8 +68,6 @@ const unreadLabel = computed(() => {
<style scoped lang="scss">
.app-item {
--app-item-circle-size: calc(var(--default-grid-baseline) * 10);
--app-item-icon-size: 22px;
display: flex;
flex-direction: column;
align-items: center;
@ -100,37 +93,6 @@ const unreadLabel = computed(() => {
box-shadow: inset 0 0 0 2px var(--color-primary-element);
}
&__circle {
box-sizing: border-box;
position: relative;
width: var(--app-item-circle-size);
height: var(--app-item-circle-size);
border-radius: 50%;
background-color: var(--color-primary-element);
background-image: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.18) 0%,
rgba(255, 255, 255, 0) 45%,
rgba(0, 0, 0, 0.15) 100%
);
box-shadow:
inset 0 1px 0 0 rgba(255, 255, 255, 0.25),
inset 0 -1px 0 0 rgba(0, 0, 0, 0.2),
0 2px 4px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
justify-content: center;
}
&__icon {
width: var(--app-item-icon-size);
height: var(--app-item-icon-size);
// App icons are bright by default; flip them to dark when the
// primary color (circle background) is bright (e.g. white in dark mode).
filter: var(--primary-invert-if-bright);
mask: var(--header-menu-icon-mask);
}
&__unread {
position: absolute;
top: 0;
@ -160,17 +122,5 @@ const unreadLabel = computed(() => {
&--active &__label {
font-weight: bold;
}
// Outlined variant: no fill or gradient.
&--outlined &__circle {
background: transparent;
background-image: none;
box-shadow: inset 0 0 0 2px var(--color-border-maxcontrast);
}
&--outlined &__icon {
filter: var(--background-invert-if-dark);
mask: none;
}
}
</style>

Loading…
Cancel
Save