1212 :href =" resourceUrl "
1313 target="_self">
1414 <template #icon >
15+ <AppIcon
16+ v-if =" isAppIcon "
17+ class="result-item__app-icon"
18+ :icon =" icon " />
1519 <div
20+ v-else
1621 aria-hidden =" true"
1722 class =" result-item__icon"
1823 :class =" {
1924 'result-item__icon--rounded': rounded,
20- 'result-item__icon--no-preview': !isValidIconOrPreviewUrl(thumbnailUrl),
21- 'result-item__icon--with-thumbnail': isValidIconOrPreviewUrl(thumbnailUrl),
22- [icon]: !isValidIconOrPreviewUrl(icon),
23- }"
24- :style =" {
25- backgroundImage: isValidIconOrPreviewUrl(icon) ? `url(${icon})` : '',
25+ 'result-item__icon--with-thumbnail': hasThumbnail,
26+ [icon]: !iconIsUrl && !hasThumbnail,
2627 }" >
2728 <img
28- v-if =" isValidIconOrPreviewUrl(thumbnailUrl) && !thumbnailHasError "
29+ v-if =" hasThumbnail "
2930 :src =" thumbnailUrl"
3031 @error =" thumbnailErrorHandler" >
32+ <img
33+ v-else-if =" iconIsUrl"
34+ class =" result-item__icon-img"
35+ :src =" icon"
36+ alt =" "
37+ aria-hidden =" true" >
3138 </div >
3239 </template >
3340 <template #subname >
3845
3946<script >
4047import NcListItem from ' @nextcloud/vue/components/NcListItem'
48+ import AppIcon from ' ../AppIcon.vue'
4149
4250export default {
4351 name: ' SearchResult' ,
4452 components: {
53+ AppIcon,
4554 NcListItem,
4655 },
4756
@@ -108,6 +117,26 @@ export default {
108117 }
109118 },
110119
120+ computed: {
121+ /** A usable thumbnail image (a preview/avatar), not errored. */
122+ hasThumbnail () {
123+ return this .isValidIconOrPreviewUrl (this .thumbnailUrl ) && ! this .thumbnailHasError
124+ },
125+
126+ /** The icon is a real URL we can put in an <img>, not a legacy CSS class string. */
127+ iconIsUrl () {
128+ return this .isValidIconOrPreviewUrl (this .icon )
129+ },
130+
131+ /**
132+ * App-style icon (bright glyph on a primary circle, like the app menu). Providers
133+ * flag it by marking the entry rounded with an icon URL and no thumbnail.
134+ */
135+ isAppIcon () {
136+ return this .rounded && this .iconIsUrl && ! this .hasThumbnail
137+ },
138+ },
139+
111140 watch: {
112141 thumbnailUrl () {
113142 this .thumbnailHasError = false
@@ -128,58 +157,96 @@ export default {
128157
129158<style lang="scss" scoped>
130159.result-item {
160+ padding-inline : 0 ;
161+
131162 :deep (a ) {
132163 border : 2px solid transparent ;
133164 border-radius : var (--border-radius-large ) !important ;
134165
166+ // Hover/press: neutral gray fill only, no border.
135167 & :active ,
136- & :hover ,
137- & :focus {
168+ & :hover {
138169 background-color : var (--color-background-hover );
139- border : 2px solid var (--color-border-maxcontrast );
170+ }
171+
172+ // Plain Tab into a result keeps a visible focus ring (a11y). Normally the combobox
173+ // keeps focus in the input and drives selection via `active` below.
174+ & :focus-visible {
175+ background-color : var (--color-background-hover );
176+ border-color : var (--color-border-maxcontrast );
140177 }
141178
142179 * {
143180 cursor : pointer ;
144181 }
145182 }
146183
184+ // NcListItem's `active` state paints a primary fill, white text and a blue stripe.
185+ // We want a neutral look: the gray hover fill plus a maxcontrast border, readable text.
186+ & .list-item__wrapper--active {
187+ :deep (.list-item ) {
188+ background-color : var (--color-background-hover );
189+
190+ & :hover {
191+ background-color : var (--color-background-hover );
192+ }
193+ }
194+
195+ // Undo the forced active text colour. Chain through the anchor to outrank
196+ // NcListItem's own !important rule.
197+ :deep(.list-item__anchor .list-item-content__name ),
198+ :deep(.list-item__anchor .list-item-content__subname ),
199+ :deep(.list-item__anchor .list-item-content__details ),
200+ :deep(.list-item__anchor .list-item-details__details ) {
201+ color : var (--color-main-text ) !important ;
202+ }
203+ }
204+
147205 & __icon {
206+ display : flex ;
207+ align-items : center ;
208+ justify-content : center ;
148209 overflow : hidden ;
149210 width : var (--default-clickable-area );
150211 height : var (--default-clickable-area );
151212 border-radius : var (--border-radius );
152- background-repeat : no-repeat ;
153- background-position : center center ;
154- background-size : 32px ;
213+ margin-inline-start : var (--default-grid-baseline );
155214
156215 & --rounded {
157216 border-radius : calc (var (--default-clickable-area ) / 2 );
158217 }
159218
160- & --no-preview {
161- background-size : 32px ;
162- }
163-
164- & --with-thumbnail {
165- background-size : cover ;
166- }
167-
168219 & --with-thumbnail :not (#{&}--rounded) {
169220 border : 1px solid var (--color-border );
170221 // compensate for border
171222 max-height : calc (var (--default-clickable-area ) - 2px );
172223 max-width : calc (var (--default-clickable-area ) - 2px );
173224 }
174225
175- img {
226+ // A full-bleed thumbnail (preview or avatar) fills the box.
227+ & --with-thumbnail img {
176228 // Make sure to keep ratio
177229 width : 100% ;
178230 height : 100% ;
179231
180232 object-fit : cover ;
181233 object-position : center ;
182234 }
235+
236+ // A small monochrome glyph (e.g. a settings section), not a thumbnail.
237+ & -img {
238+ width : 20px ;
239+ height : 20px ;
240+ object-fit : contain ;
241+ // Dark monochrome icons invert to light in dark themes.
242+ filter : var (--background-invert-if-dark );
243+ }
244+ }
245+
246+ // App results reuse the app-menu tile (AppIcon); size its circle to the icon column.
247+ & __app-icon {
248+ --app-icon-circle-size : var (--default-clickable-area );
249+ margin-inline-start : var (--default-grid-baseline );
183250 }
184251}
185252 </style >
0 commit comments