Merge pull request #60665 from nextcloud/feat/59888-nav-redesign-header-search-launcher

feat(core): Add centered search input to top bar
This commit is contained in:
F. E Noel Nfebe 2026-05-27 10:27:22 +01:00 committed by GitHub
commit 9ecf114443
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 201 additions and 27 deletions

View file

@ -11,9 +11,9 @@
:triggers="[]"
placement="bottom-start"
:skidding="popoverSkidding"
:setReturnFocus="returnFocusTarget"
popoverBaseClass="app-menu__popover-base"
popupRole="menu"
:set-return-focus="returnFocusTarget"
popover-base-class="app-menu__popover-base"
popup-role="menu"
@update:shown="opened = $event">
<template #trigger>
<NcButton
@ -40,7 +40,7 @@
ref="items"
:app="item"
:outlined="item.id === 'more-apps' || item.id === 'app-store'"
:newTab="item.id === 'app-store'"
:new-tab="item.id === 'app-store'"
:tabindex="i === focusedIndex ? 0 : -1" />
</div>
</div>
@ -431,8 +431,7 @@ export default defineComponent({
min-width: 0;
}
// Hide on small screens (matches $breakpoint-small-mobile in @nextcloud/vue).
@media only screen and (max-width: 512px) {
@media only screen and (max-width: 1024px) {
display: none !important;
}
}

View file

@ -0,0 +1,185 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<search class="unified-search-input" :class="[{ 'unified-search-input--mobile': isSmallMobile }]">
<NcHeaderButton
v-if="isSmallMobile"
:aria-label="placeholderText"
aria-haspopup="dialog"
:aria-expanded="expanded ? 'true' : 'false'"
@click="$emit('click', $event)">
<template #icon>
<IconMagnify :size="20" />
</template>
</NcHeaderButton>
<button
v-else
type="button"
class="unified-search-input__button"
aria-haspopup="dialog"
:aria-expanded="expanded ? 'true' : 'false'"
@click="$emit('click', $event)">
<IconMagnify
class="unified-search-input__icon"
:size="20"
aria-hidden="true" />
<span class="unified-search-input__label">
{{ placeholderText }}
</span>
</button>
</search>
</template>
<script setup lang="ts">
import { t } from '@nextcloud/l10n'
import { useIsSmallMobile } from '@nextcloud/vue/composables/useIsMobile'
import NcHeaderButton from '@nextcloud/vue/components/NcHeaderButton'
import IconMagnify from 'vue-material-design-icons/Magnify.vue'
/**
* First phase of the unified-search input: a button styled to look like an
* input field that opens the unified-search modal on click. A later phase
* will replace the button with a real input that filters results inline.
*
* Implemented as a custom component because no `@nextcloud/vue` component
* fits the design role here: NcInputField is a real input whose styling
* assumes a light page background and clashes with the themed header,
* and NcTextField has the same issue. On narrow viewports the trigger
* collapses to a standard NcHeaderButton so it matches the visual
* language of the other header items.
*/
defineProps<{
/** Whether the popup the input controls is currently open. Bound to aria-expanded. */
expanded?: boolean
}>()
defineEmits<{
click: [mouseEvent: MouseEvent]
}>()
const isSmallMobile = useIsSmallMobile()
const placeholderText = t('core', 'Search apps, files, tags, messages …')
</script>
<style lang="scss" scoped>
.unified-search-input {
&:not(.unified-search-input--mobile) {
position: absolute;
top: 0;
bottom: 0;
inset-inline: 0;
margin-inline: auto;
display: flex;
align-items: center;
width: clamp(200px, 35vw, 600px);
max-width: calc(100% - 32px);
pointer-events: none;
}
&--mobile {
display: contents;
}
&__button {
pointer-events: auto;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
height: calc(var(--default-clickable-area) - 8px);
padding: 0 12px;
border: none;
border-radius: var(--border-radius-element, 8px);
background-color: rgba(0, 0, 0, 0.15);
-webkit-backdrop-filter: var(--filter-background-blur);
backdrop-filter: var(--filter-background-blur);
box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.12);
color: var(--color-background-plain-text);
cursor: pointer;
text-align: center;
font: inherit;
transition: background-color var(--animation-quick) ease-in-out;
&:hover {
background-color: rgba(0, 0, 0, 0.22);
}
&:focus-visible {
background-color: rgba(0, 0, 0, 0.22);
outline: 2px solid var(--color-background-plain-text);
outline-offset: 2px;
}
&:active {
background-color: rgba(0, 0, 0, 0.28) !important;
color: var(--color-background-plain-text) !important;
outline: none;
}
}
&__icon {
flex-shrink: 0;
display: flex;
align-items: center;
}
&__label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.unified-search-input--mobile :deep(.header-menu) {
height: var(--default-clickable-area);
}
.unified-search-input--mobile :deep(.header-menu__trigger) {
--button-size: var(--default-clickable-area) !important;
height: var(--default-clickable-area) !important;
}
.unified-search-input--mobile :deep(.button-vue) {
--color-main-text: var(--color-background-plain-text);
color: var(--color-background-plain-text);
border-radius: var(--border-radius-element) !important;
&:hover:not(:disabled) {
background-color: rgba(0, 0, 0, 0.1) !important;
}
&:active:not(:disabled) {
background-color: rgba(0, 0, 0, 0.15) !important;
}
&:focus-visible {
background-color: rgba(0, 0, 0, 0.1) !important;
outline: none !important;
box-shadow: inset 0 0 0 2px var(--color-background-plain-text) !important;
}
}
[data-theme-dark] .unified-search-input__button,
[data-theme-dark-highcontrast] .unified-search-input__button {
background-color: color-mix(in srgb, var(--color-primary-element) 16%, transparent);
&:hover {
background-color: color-mix(in srgb, var(--color-primary-element) 22%, transparent);
}
&:focus-visible {
background-color: color-mix(in srgb, var(--color-primary-element) 22%, transparent);
}
&:active {
background-color: color-mix(in srgb, var(--color-primary-element) 28%, transparent) !important;
color: var(--color-background-plain-text) !important;
outline: none;
}
}
</style>

View file

@ -4,15 +4,9 @@
-->
<template>
<div class="unified-search-menu">
<NcHeaderButton
v-show="!showLocalSearch"
id="unified-search"
:aria-label="t('core', 'Unified search')"
@click="toggleUnifiedSearch">
<template #icon>
<NcIconSvgWrapper :path="mdiMagnify" />
</template>
</NcHeaderButton>
<UnifiedSearchInput
:expanded="showUnifiedSearch || showLocalSearch"
@click="toggleUnifiedSearch" />
<UnifiedSearchLocalSearchBar
v-if="supportsLocalSearch"
:open.sync="showLocalSearch"
@ -26,14 +20,12 @@
</template>
<script lang="ts">
import { mdiMagnify } from '@mdi/js'
import { emit, subscribe } from '@nextcloud/event-bus'
import { t } from '@nextcloud/l10n'
import { useBrowserLocation } from '@vueuse/core'
import debounce from 'debounce'
import { defineComponent } from 'vue'
import NcHeaderButton from '@nextcloud/vue/components/NcHeaderButton'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import UnifiedSearchInput from '../components/UnifiedSearch/UnifiedSearchInput.vue'
import UnifiedSearchLocalSearchBar from '../components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue'
import UnifiedSearchModal from '../components/UnifiedSearch/UnifiedSearchModal.vue'
import logger from '../logger.js'
@ -42,10 +34,9 @@ export default defineComponent({
name: 'UnifiedSearch',
components: {
NcHeaderButton,
NcIconSvgWrapper,
UnifiedSearchModal,
UnifiedSearchLocalSearchBar,
UnifiedSearchInput,
},
setup() {
@ -54,7 +45,6 @@ export default defineComponent({
return {
currentLocation,
mdiMagnify,
t,
}
},

4
dist/core-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long