Skip to content

Commit b74e28c

Browse files
authored
Merge pull request #62605 from nextcloud/feat/search-results-presentation
feat(core): redesign unified search result presentation
2 parents 00a85d0 + 7808932 commit b74e28c

23 files changed

Lines changed: 1464 additions & 240 deletions

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: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,18 @@ 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, or a generic cog fallback.
125+
$icon = $section->getIcon();
126+
if ($icon === '') {
127+
$icon = $this->urlGenerator->imagePath('settings', 'settings.svg');
128+
}
128129

129130
$result[] = new SearchResultEntry(
130131
'',
131132
$section->getName(),
132133
$subline,
133134
$this->urlGenerator->linkToRouteAbsolute($routeName, ['section' => $section->getID()]),
134-
'icon-settings-dark'
135+
$icon,
135136
);
136137
}
137138
}

core/src/components/AppIcon.vue

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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). */
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; flip to dark when the circle background is bright (e.g. white in dark mode).
59+
filter: var(--primary-invert-if-bright);
60+
mask: var(--header-menu-icon-mask);
61+
}
62+
63+
&--outlined {
64+
background: transparent;
65+
background-image: none;
66+
box-shadow: inset 0 0 0 2px var(--color-border-maxcontrast);
67+
}
68+
69+
&--outlined &__img {
70+
filter: var(--background-invert-if-dark);
71+
mask: none;
72+
}
73+
}
74+
</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)