Skip to content

Commit 52d22c3

Browse files
committed
feat(core): show real per-result icons in unified search
Signed-off-by: Peter Ringelmann <peter.ringelmann@nextcloud.com>
1 parent 0665d59 commit 52d22c3

4 files changed

Lines changed: 89 additions & 59 deletions

File tree

apps/appstore/lib/Search/AppSearch.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ public function search(IUser $user, ISearchQuery $query): SearchResult {
7474
$entry['name'],
7575
'',
7676
$entry['href'],
77-
'icon-confirm'
77+
$entry['icon'],
78+
true,
7879
);
7980
}
8081

apps/settings/lib/Search/SectionSearch.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,20 @@ public function searchSections(ISearchQuery $query, array $sections, string $sub
121121
continue;
122122
}
123123

124-
/**
125-
* We can't use the icon URL at the moment as they don't invert correctly for dark theme
126-
* $iconUrl = $section->getIcon();
127-
*/
124+
// The section's own icon, falling back to a generic cog when it has none.
125+
// These are dark monochrome glyphs; the client inverts them for dark
126+
// themes via --background-invert-if-dark.
127+
$icon = $section->getIcon();
128+
if ($icon === '') {
129+
$icon = $this->urlGenerator->imagePath('settings', 'settings.svg');
130+
}
128131

129132
$result[] = new SearchResultEntry(
130133
'',
131134
$section->getName(),
132135
$subline,
133136
$this->urlGenerator->linkToRouteAbsolute($routeName, ['section' => $section->getID()]),
134-
'icon-settings-dark'
137+
$icon,
135138
);
136139
}
137140
}

core/src/components/AppIcon.vue

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<span
8+
class="app-icon"
9+
:class="{ 'app-icon--outlined': outlined }">
10+
<img
11+
class="app-icon__img"
12+
:src="icon"
13+
alt=""
14+
aria-hidden="true">
15+
<!-- @slot Overlay positioned over the circle, e.g. an unread badge. -->
16+
<slot />
17+
</span>
18+
</template>
19+
20+
<script setup lang="ts">
21+
withDefaults(defineProps<{
22+
/** URL of the app icon. Painted bright on the coloured circle, like the app menu. */
23+
icon: string
24+
/** Render the circle as an outline only (no fill or gradient). */
25+
outlined?: boolean
26+
}>(), {
27+
outlined: false,
28+
})
29+
</script>
30+
31+
<style scoped lang="scss">
32+
.app-icon {
33+
--app-icon-circle-size: calc(var(--default-grid-baseline) * 10);
34+
--app-icon-icon-size: 22px;
35+
box-sizing: border-box;
36+
position: relative;
37+
display: flex;
38+
align-items: center;
39+
justify-content: center;
40+
width: var(--app-icon-circle-size);
41+
height: var(--app-icon-circle-size);
42+
border-radius: 50%;
43+
background-color: var(--color-primary-element);
44+
background-image: linear-gradient(
45+
to bottom,
46+
rgba(255, 255, 255, 0.18) 0%,
47+
rgba(255, 255, 255, 0) 45%,
48+
rgba(0, 0, 0, 0.15) 100%
49+
);
50+
box-shadow:
51+
inset 0 1px 0 0 rgba(255, 255, 255, 0.25),
52+
inset 0 -1px 0 0 rgba(0, 0, 0, 0.2),
53+
0 2px 4px rgba(0, 0, 0, 0.15);
54+
55+
&__img {
56+
width: var(--app-icon-icon-size);
57+
height: var(--app-icon-icon-size);
58+
// App icons are bright by default; flip them to dark when the
59+
// primary color (circle background) is bright (e.g. white in dark mode).
60+
filter: var(--primary-invert-if-bright);
61+
mask: var(--header-menu-icon-mask);
62+
}
63+
64+
// Outlined variant: no fill or gradient.
65+
&--outlined {
66+
background: transparent;
67+
background-image: none;
68+
box-shadow: inset 0 0 0 2px var(--color-border-maxcontrast);
69+
}
70+
71+
&--outlined &__img {
72+
filter: var(--background-invert-if-dark);
73+
mask: none;
74+
}
75+
}
76+
</style>

core/src/components/AppItem.vue

Lines changed: 3 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
class="app-item"
99
:class="{
1010
'app-item--active': app.active,
11-
'app-item--outlined': outlined,
1211
}"
1312
:href="app.href"
1413
:target="newTab ? '_blank' : undefined"
@@ -17,17 +16,12 @@
1716
:tabindex="tabindex"
1817
:title="app.name"
1918
role="menuitem">
20-
<span class="app-item__circle">
21-
<img
22-
class="app-item__icon"
23-
:src="app.icon"
24-
alt=""
25-
aria-hidden="true">
19+
<AppIcon :icon="app.icon" :outlined="outlined">
2620
<span
2721
v-if="app.unread"
2822
class="app-item__unread"
2923
aria-hidden="true" />
30-
</span>
24+
</AppIcon>
3125
<span class="app-item__label">
3226
{{ app.name }}
3327
<span v-if="app.unread" class="hidden-visually">, {{ unreadLabel }}</span>
@@ -40,6 +34,7 @@ import type { INavigationEntry } from '../types/navigation.d.ts'
4034
4135
import { n } from '@nextcloud/l10n'
4236
import { computed } from 'vue'
37+
import AppIcon from './AppIcon.vue'
4338
4439
const props = withDefaults(defineProps<{
4540
app: INavigationEntry
@@ -73,8 +68,6 @@ const unreadLabel = computed(() => {
7368

7469
<style scoped lang="scss">
7570
.app-item {
76-
--app-item-circle-size: calc(var(--default-grid-baseline) * 10);
77-
--app-item-icon-size: 22px;
7871
display: flex;
7972
flex-direction: column;
8073
align-items: center;
@@ -100,37 +93,6 @@ const unreadLabel = computed(() => {
10093
box-shadow: inset 0 0 0 2px var(--color-primary-element);
10194
}
10295
103-
&__circle {
104-
box-sizing: border-box;
105-
position: relative;
106-
width: var(--app-item-circle-size);
107-
height: var(--app-item-circle-size);
108-
border-radius: 50%;
109-
background-color: var(--color-primary-element);
110-
background-image: linear-gradient(
111-
to bottom,
112-
rgba(255, 255, 255, 0.18) 0%,
113-
rgba(255, 255, 255, 0) 45%,
114-
rgba(0, 0, 0, 0.15) 100%
115-
);
116-
box-shadow:
117-
inset 0 1px 0 0 rgba(255, 255, 255, 0.25),
118-
inset 0 -1px 0 0 rgba(0, 0, 0, 0.2),
119-
0 2px 4px rgba(0, 0, 0, 0.15);
120-
display: flex;
121-
align-items: center;
122-
justify-content: center;
123-
}
124-
125-
&__icon {
126-
width: var(--app-item-icon-size);
127-
height: var(--app-item-icon-size);
128-
// App icons are bright by default; flip them to dark when the
129-
// primary color (circle background) is bright (e.g. white in dark mode).
130-
filter: var(--primary-invert-if-bright);
131-
mask: var(--header-menu-icon-mask);
132-
}
133-
13496
&__unread {
13597
position: absolute;
13698
top: 0;
@@ -160,17 +122,5 @@ const unreadLabel = computed(() => {
160122
&--active &__label {
161123
font-weight: bold;
162124
}
163-
164-
// Outlined variant: no fill or gradient.
165-
&--outlined &__circle {
166-
background: transparent;
167-
background-image: none;
168-
box-shadow: inset 0 0 0 2px var(--color-border-maxcontrast);
169-
}
170-
171-
&--outlined &__icon {
172-
filter: var(--background-invert-if-dark);
173-
mask: none;
174-
}
175125
}
176126
</style>

0 commit comments

Comments
 (0)