feat(core): support unified search on mobile viewports

Signed-off-by: Peter Ringelmann <peter.ringelmann@nextcloud.com>
This commit is contained in:
Peter Ringelmann 2026-07-02 18:44:04 +02:00
parent bb4b70cf3a
commit 755680829b
No known key found for this signature in database
58 changed files with 453 additions and 174 deletions

View file

@ -2,4 +2,4 @@
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/#skip-actions{position:absolute;overflow:hidden;z-index:9999;top:-999px;inset-inline-start:3px;padding:11px;display:flex;flex-wrap:wrap;gap:11px}#skip-actions:focus-within{top:var(--header-height)}#header{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}#header:not(.header-guest){display:inline-flex;position:absolute;top:0;width:100%;z-index:2000;height:var(--header-height);box-sizing:border-box;justify-content:space-between}#header #nextcloud{padding:5px 0;padding-inline-start:86px;position:relative;height:calc(100% - var(--default-grid-baseline));box-sizing:border-box;opacity:1;align-items:center;display:flex;flex-wrap:wrap;overflow:hidden;margin:2px}#header #nextcloud:hover,#header #nextcloud:active{opacity:1}#header #nextcloud .logo{display:inline-flex;background-image:var(--image-logoheader, var(--image-logo, url("../img/logo/logo.svg")));background-repeat:no-repeat;background-size:contain;background-position:center;width:62px;position:absolute;inset-inline-start:12px;top:1px;bottom:1px;filter:var(--image-logoheader-custom, var(--background-image-invert-if-bright))}#header #nextcloud:focus-visible,#header .header-menu button:first-of-type:focus-visible{outline:none}#header #nextcloud:focus-visible::after,#header .header-menu button:first-of-type:focus-visible::after{content:" ";position:absolute;inset-block-end:2px;transform:translateX(-50%);width:12px;height:2px;border-radius:3px;background-color:var(--color-background-plain-text);inset-inline-start:50%;opacity:1}#header .header-start{display:inline-flex;align-items:center;flex:1 0;white-space:nowrap;min-width:0}#header .header-center{display:inline-flex;align-items:center;justify-content:flex-end;flex:0 1 auto;white-space:nowrap;min-width:0}#header .header-end{display:inline-flex;align-items:center;justify-content:flex-end;flex-shrink:1;margin-inline-end:calc(3*var(--default-grid-baseline))}@media(min-width: 512px){#body-user #header .header-end{flex:1 0}}#header .header-appname{color:var(--color-background-plain-text);font-size:16px;font-weight:bold;margin:0;padding:0;padding-inline-end:5px;overflow:hidden;text-overflow:ellipsis;flex:1 1 100%}#header .header-appname .header-info{display:flex;flex-direction:column;overflow:hidden}#header .header-appname .header-info .header-title{overflow:hidden;text-overflow:ellipsis}#header .header-appname .header-info .header-shared-by{color:var(--color-background-plain-text);position:relative;font-weight:300;font-size:var(--font-size-small);line-height:var(--font-size-small);overflow:hidden;text-overflow:ellipsis}/*# sourceMappingURL=header.css.map */
*/#skip-actions{position:absolute;overflow:hidden;z-index:9999;top:-999px;inset-inline-start:3px;padding:11px;display:flex;flex-wrap:wrap;gap:11px}#skip-actions:focus-within{top:var(--header-height)}#header{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}#header:not(.header-guest){display:inline-flex;position:absolute;top:0;width:100%;z-index:2000;height:var(--header-height);box-sizing:border-box;justify-content:space-between}#header #nextcloud{padding:5px 0;padding-inline-start:86px;position:relative;height:calc(100% - var(--default-grid-baseline));box-sizing:border-box;opacity:1;align-items:center;display:flex;flex-wrap:wrap;overflow:hidden;margin:2px}#header #nextcloud:hover,#header #nextcloud:active{opacity:1}#header #nextcloud .logo{display:inline-flex;background-image:var(--image-logoheader, var(--image-logo, url("../img/logo/logo.svg")));background-repeat:no-repeat;background-size:contain;background-position:center;width:62px;position:absolute;inset-inline-start:12px;top:1px;bottom:1px;filter:var(--image-logoheader-custom, var(--background-image-invert-if-bright))}#header #nextcloud:focus-visible,#header .header-menu button:first-of-type:focus-visible{outline:none}#header #nextcloud:focus-visible::after,#header .header-menu button:first-of-type:focus-visible::after{content:" ";position:absolute;inset-block-end:2px;transform:translateX(-50%);width:12px;height:2px;border-radius:3px;background-color:var(--color-background-plain-text);inset-inline-start:50%;opacity:1}#header .header-start{display:inline-flex;align-items:center;flex:1 0;white-space:nowrap;min-width:0}#header .header-center{display:inline-flex;align-items:center;justify-content:flex-end;flex:0 1 auto;white-space:nowrap;min-width:0}#header .header-end{display:inline-flex;align-items:center;justify-content:flex-end;flex-shrink:1;margin-inline-end:calc(3*var(--default-grid-baseline))}@media(min-width: 512px){#body-user #header .header-end,#body-settings #header .header-end{flex:1 0}}#header .header-appname{color:var(--color-background-plain-text);font-size:16px;font-weight:bold;margin:0;padding:0;padding-inline-end:5px;overflow:hidden;text-overflow:ellipsis;flex:1 1 100%}#header .header-appname .header-info{display:flex;flex-direction:column;overflow:hidden}#header .header-appname .header-info .header-title{overflow:hidden;text-overflow:ellipsis}#header .header-appname .header-info .header-shared-by{color:var(--color-background-plain-text);position:relative;font-weight:300;font-size:var(--font-size-small);line-height:var(--font-size-small);overflow:hidden;text-overflow:ellipsis}/*# sourceMappingURL=header.css.map */

View file

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["header.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA,GAYA,cACC,kBACA,gBACA,aACA,WACA,uBACA,aACA,aACA,eACA,SAEA,2BACC,yBAKF,QAEC,yBACA,sBACA,qBACA,iBAGA,2BACC,oBACA,kBACA,MACA,WACA,aACA,4BACA,sBACA,8BAID,mBACC,cACA,0BACA,kBACA,iDACA,sBACA,UACA,mBACA,aACA,eACA,gBACA,WAEA,mDAEC,UAID,yBACC,oBACA,yFACA,4BACA,wBACA,2BACA,WACA,kBACA,wBACA,QACA,WAEA,gFAMF,yFAEC,aAEA,uGACC,YACA,kBACA,oBACA,2BACA,WACA,WACA,kBACA,oDACA,uBACA,UAOF,sBACC,oBACA,mBACA,SACA,mBACA,YAKD,uBACC,oBACA,mBACA,yBACA,cACA,mBACA,YAKD,oBACC,oBACA,mBACA,yBACA,cAEA,uDAQA,yBACC,+BACC,UAOH,wBACC,yCACA,eACA,iBACA,SACA,UACA,uBACA,gBACA,uBAEA,cAGA,qCACC,aACA,sBACA,gBAEA,mDACC,gBACA,uBAGD,uDACC,yCACA,kBACA,gBACA,iCACA,mCACA,gBACA","file":"header.css"}
{"version":3,"sourceRoot":"","sources":["header.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA,GAYA,cACC,kBACA,gBACA,aACA,WACA,uBACA,aACA,aACA,eACA,SAEA,2BACC,yBAKF,QAEC,yBACA,sBACA,qBACA,iBAGA,2BACC,oBACA,kBACA,MACA,WACA,aACA,4BACA,sBACA,8BAID,mBACC,cACA,0BACA,kBACA,iDACA,sBACA,UACA,mBACA,aACA,eACA,gBACA,WAEA,mDAEC,UAID,yBACC,oBACA,yFACA,4BACA,wBACA,2BACA,WACA,kBACA,wBACA,QACA,WAEA,gFAMF,yFAEC,aAEA,uGACC,YACA,kBACA,oBACA,2BACA,WACA,WACA,kBACA,oDACA,uBACA,UAOF,sBACC,oBACA,mBACA,SACA,mBACA,YAKD,uBACC,oBACA,mBACA,yBACA,cACA,mBACA,YAKD,oBACC,oBACA,mBACA,yBACA,cAEA,uDASA,yBACC,kEAEC,UAOH,wBACC,yCACA,eACA,iBACA,SACA,UACA,uBACA,gBACA,uBAEA,cAGA,qCACC,aACA,sBACA,gBAEA,mDACC,gBACA,uBAGD,uDACC,yCACA,kBACA,gBACA,iCACA,mCACA,gBACA","file":"header.css"}

View file

@ -134,14 +134,16 @@
// Add some spacing so the last entry looks ok
margin-inline-end: calc(3 * var(--default-grid-baseline));
// Only grow to match header-start on desktop, and only
// in the user layout where header-center exists.
// Below the mobile breakpoint the search collapses to an icon,
// and header-end must stay content-sized so header-start pushes
// that icon flush to the right edge. The public layout has
// no centered search, so it keeps the content-sized header-end.
// Grow to match header-start (which always grows) so the centered search
// stays centered. Only on desktop and only in the layouts that render
// header-center: the user layout and the settings layout (#body-settings).
// Below the mobile breakpoint the search collapses to an icon, and
// header-end must stay content-sized so header-start pushes that icon flush
// to the right edge. The public layout has no centered search, so it keeps
// the content-sized header-end.
@media (min-width: 512px) {
#body-user & {
#body-user &,
#body-settings & {
flex: 1 0;
}
}

View file

@ -22,7 +22,7 @@
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/#skip-actions{position:absolute;overflow:hidden;z-index:9999;top:-999px;inset-inline-start:3px;padding:11px;display:flex;flex-wrap:wrap;gap:11px}#skip-actions:focus-within{top:var(--header-height)}#header{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}#header:not(.header-guest){display:inline-flex;position:absolute;top:0;width:100%;z-index:2000;height:var(--header-height);box-sizing:border-box;justify-content:space-between}#header #nextcloud{padding:5px 0;padding-inline-start:86px;position:relative;height:calc(100% - var(--default-grid-baseline));box-sizing:border-box;opacity:1;align-items:center;display:flex;flex-wrap:wrap;overflow:hidden;margin:2px}#header #nextcloud:hover,#header #nextcloud:active{opacity:1}#header #nextcloud .logo{display:inline-flex;background-image:var(--image-logoheader, var(--image-logo, url("../img/logo/logo.svg")));background-repeat:no-repeat;background-size:contain;background-position:center;width:62px;position:absolute;inset-inline-start:12px;top:1px;bottom:1px;filter:var(--image-logoheader-custom, var(--background-image-invert-if-bright))}#header #nextcloud:focus-visible,#header .header-menu button:first-of-type:focus-visible{outline:none}#header #nextcloud:focus-visible::after,#header .header-menu button:first-of-type:focus-visible::after{content:" ";position:absolute;inset-block-end:2px;transform:translateX(-50%);width:12px;height:2px;border-radius:3px;background-color:var(--color-background-plain-text);inset-inline-start:50%;opacity:1}#header .header-start{display:inline-flex;align-items:center;flex:1 0;white-space:nowrap;min-width:0}#header .header-center{display:inline-flex;align-items:center;justify-content:flex-end;flex:0 1 auto;white-space:nowrap;min-width:0}#header .header-end{display:inline-flex;align-items:center;justify-content:flex-end;flex-shrink:1;margin-inline-end:calc(3*var(--default-grid-baseline))}@media(min-width: 512px){#body-user #header .header-end{flex:1 0}}#header .header-appname{color:var(--color-background-plain-text);font-size:16px;font-weight:bold;margin:0;padding:0;padding-inline-end:5px;overflow:hidden;text-overflow:ellipsis;flex:1 1 100%}#header .header-appname .header-info{display:flex;flex-direction:column;overflow:hidden}#header .header-appname .header-info .header-title{overflow:hidden;text-overflow:ellipsis}#header .header-appname .header-info .header-shared-by{color:var(--color-background-plain-text);position:relative;font-weight:300;font-size:var(--font-size-small);line-height:var(--font-size-small);overflow:hidden;text-overflow:ellipsis}/*!
*/#skip-actions{position:absolute;overflow:hidden;z-index:9999;top:-999px;inset-inline-start:3px;padding:11px;display:flex;flex-wrap:wrap;gap:11px}#skip-actions:focus-within{top:var(--header-height)}#header{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}#header:not(.header-guest){display:inline-flex;position:absolute;top:0;width:100%;z-index:2000;height:var(--header-height);box-sizing:border-box;justify-content:space-between}#header #nextcloud{padding:5px 0;padding-inline-start:86px;position:relative;height:calc(100% - var(--default-grid-baseline));box-sizing:border-box;opacity:1;align-items:center;display:flex;flex-wrap:wrap;overflow:hidden;margin:2px}#header #nextcloud:hover,#header #nextcloud:active{opacity:1}#header #nextcloud .logo{display:inline-flex;background-image:var(--image-logoheader, var(--image-logo, url("../img/logo/logo.svg")));background-repeat:no-repeat;background-size:contain;background-position:center;width:62px;position:absolute;inset-inline-start:12px;top:1px;bottom:1px;filter:var(--image-logoheader-custom, var(--background-image-invert-if-bright))}#header #nextcloud:focus-visible,#header .header-menu button:first-of-type:focus-visible{outline:none}#header #nextcloud:focus-visible::after,#header .header-menu button:first-of-type:focus-visible::after{content:" ";position:absolute;inset-block-end:2px;transform:translateX(-50%);width:12px;height:2px;border-radius:3px;background-color:var(--color-background-plain-text);inset-inline-start:50%;opacity:1}#header .header-start{display:inline-flex;align-items:center;flex:1 0;white-space:nowrap;min-width:0}#header .header-center{display:inline-flex;align-items:center;justify-content:flex-end;flex:0 1 auto;white-space:nowrap;min-width:0}#header .header-end{display:inline-flex;align-items:center;justify-content:flex-end;flex-shrink:1;margin-inline-end:calc(3*var(--default-grid-baseline))}@media(min-width: 512px){#body-user #header .header-end,#body-settings #header .header-end{flex:1 0}}#header .header-appname{color:var(--color-background-plain-text);font-size:16px;font-weight:bold;margin:0;padding:0;padding-inline-end:5px;overflow:hidden;text-overflow:ellipsis;flex:1 1 100%}#header .header-appname .header-info{display:flex;flex-direction:column;overflow:hidden}#header .header-appname .header-info .header-title{overflow:hidden;text-overflow:ellipsis}#header .header-appname .header-info .header-shared-by{color:var(--color-background-plain-text);position:relative;font-weight:300;font-size:var(--font-size-small);line-height:var(--font-size-small);overflow:hidden;text-overflow:ellipsis}/*!
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later

File diff suppressed because one or more lines are too long

View file

@ -8,7 +8,8 @@
:class="{ 'unified-search-input--mobile': isSmallMobile }">
<NcHeaderButton
v-if="isSmallMobile"
:aria-label="placeholderText"
id="unified-search-trigger"
:ariaLabel="placeholderText"
aria-haspopup="dialog"
:aria-expanded="expanded ? 'true' : 'false'"
@click="$emit('click', $event)">
@ -20,14 +21,16 @@
v-else
class="unified-search-input__field"
:class="{ 'unified-search-input__field--active': isActive }">
<!-- Resting overlay: centred magnifier + placeholder, purely decorative.
A full-width input cannot group an icon with its own placeholder, so we
paint the resting look on top and let clicks fall through to the input. -->
<div class="unified-search-input__resting" aria-hidden="true">
<!-- Decorative overlay: an input can't group an icon with its own placeholder,
so we paint the magnifier + placeholder on top and let clicks fall through.
It slides to the leading edge on focus (see styles). -->
<div
class="unified-search-input__resting"
:class="{ 'unified-search-input__resting--filled': query.length > 0 }"
aria-hidden="true">
<IconMagnify :size="20" />
<span>{{ placeholderText }}</span>
<span class="unified-search-input__label">{{ placeholderText }}</span>
</div>
<IconMagnify class="unified-search-input__icon" :size="20" aria-hidden="true" />
<input
ref="inputRef"
type="text"
@ -36,9 +39,8 @@
aria-autocomplete="list"
:aria-expanded="expanded ? 'true' : 'false'"
:aria-label="placeholderText"
:placeholder="isActive ? placeholderText : ''"
:value="query"
@focus="onFocus"
@focus="isFocused = true"
@blur="isFocused = false"
@input="onInput">
<NcButton
@ -83,30 +85,18 @@ const props = defineProps<{
const emit = defineEmits<{
click: [mouseEvent: MouseEvent]
focus: [focusEvent: FocusEvent]
'update:query': [query: string]
}>()
const isSmallMobile = useIsSmallMobile()
const placeholderText = t('core', 'Search apps, files, tags, messages ')
const placeholderText = t('core', 'Apps, files, messages, and more')
const inputRef = ref<HTMLInputElement>()
const isFocused = ref(false)
/** Active = focused or holding a query; drives the input-vs-button styling. */
/** Active = focused or holding a query; drives the resting-vs-active styling. */
const isActive = computed(() => isFocused.value || props.query.length > 0)
/**
* Track focus for the active styling and bubble the event so the parent can
* react to it.
*
* @param event The focus event
*/
function onFocus(event: FocusEvent) {
isFocused.value = true
emit('focus', event)
}
/**
* Relay the typed value upward.
*
@ -121,15 +111,12 @@ function clearQuery() {
emit('update:query', '')
inputRef.value?.focus()
}
defineExpose({
getInputElement: (): HTMLInputElement | null => inputRef.value ?? null,
})
</script>
<style lang="scss" scoped>
.unified-search-input {
// Positioned so z-index applies and the input paints above the search scrim
// Paints above the modal root (z-index: 50) so the header input stays clickable
// over the scrim while the popover is open. Keep 51 one above that value.
position: relative;
z-index: 51;
@ -147,7 +134,18 @@ defineExpose({
&__field {
--resting-background: rgba(0, 0, 0, 0.15);
--resting-background-hover: rgba(0, 0, 0, 0.22);
// Shared geometry: the resting group and the input's leading padding read the
// same tokens so the placeholder and the typed value line up.
--search-icon-pad: 12px;
--search-icon-size: 20px;
--search-icon-gap: 8px;
// One shared timing for every focus transition (background, the icon/label
// slide, the recolour) so they move together. easeOutQuart = soft landing.
--search-anim-duration: 240ms;
--search-anim-easing: cubic-bezier(0.22, 1, 0.36, 1);
position: relative;
// Query container so the resting group can centre itself with cqi units
container-type: inline-size;
display: flex;
align-items: center;
// Match the default clickable area so the inner <input> (which the global
@ -160,7 +158,10 @@ defineExpose({
background-color: var(--resting-background);
-webkit-backdrop-filter: var(--filter-background-blur);
backdrop-filter: var(--filter-background-blur);
transition: background-color var(--animation-quick) ease-in-out;
// Blue tint -> white surface on the shared timing, in step with the slide.
transition:
background-color var(--search-anim-duration) var(--search-anim-easing),
box-shadow var(--search-anim-duration) var(--search-anim-easing);
&:hover:not(.unified-search-input__field--active) {
background-color: var(--resting-background-hover);
@ -173,44 +174,53 @@ defineExpose({
}
}
// Resting look: magnifier + placeholder centred as a group, painted over the
// (empty, pointer-transparent) input. Fades out as the field becomes active.
// Anchored at the leading edge and translated to the centre while at rest; on
// focus (--active) the translate goes to 0 and it slides into place. Centre offset
// is pure CSS: half the field (50cqi) minus half the group (50%) minus the pad, so
// it self-corrects for any placeholder length or field width.
&__resting {
--slide-sign: 1;
position: absolute;
inset: 0;
inset-block: 0;
inset-inline-start: var(--search-icon-pad);
max-width: calc(100% - 2 * var(--search-icon-pad));
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding-inline: 12px;
gap: var(--search-icon-gap);
pointer-events: none;
color: color-mix(in srgb, var(--color-background-plain-text) 70%, var(--color-background-plain));
transition: opacity var(--animation-quick) ease-in-out;
span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
transform: translateX(calc(var(--slide-sign) * (50cqi - 50% - var(--search-icon-pad))));
transition:
transform var(--search-anim-duration) var(--search-anim-easing),
color var(--search-anim-duration) var(--search-anim-easing);
.unified-search-input__field--active & {
opacity: 0;
transform: translateX(0);
color: var(--color-text-maxcontrast);
}
}
// Left magnifier for the active state; hidden while the resting overlay shows
&__icon {
position: absolute;
inset-inline-start: 12px;
display: flex;
color: var(--color-main-text);
opacity: 0;
pointer-events: none;
transition: opacity var(--animation-quick) ease-in-out;
// Placeholder text inside the resting group. Ellipsised, and hidden once typing
// starts so it doesn't overlap the value. Scoped to the label class so the sibling
// magnifier (also rendered as a <span>) stays visible.
&__label {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
transition: opacity var(--search-anim-duration) var(--search-anim-easing);
}
.unified-search-input__field--active & {
opacity: 1;
}
&__resting--filled &__label {
opacity: 0;
}
// The material-design icon <svg> is inline (baseline-aligned), which leaves a
// descender gap and makes the glyph sit high even when its box is centred.
// Render it as a block so it fills its box, then nudge 1px down to sit on the
// text's optical centre (a geometrically centred glyph reads slightly high).
&__resting :deep(.material-design-icon__svg) {
display: block;
transform: translateY(1px);
}
// Only visible once active (at rest it's empty and covered by the overlay),
@ -220,8 +230,9 @@ defineExpose({
min-width: 0;
height: 100%;
margin: 0;
// Leading space for the active magnifier
padding-inline: calc(var(--default-clickable-area) - var(--default-grid-baseline)) 12px;
// Leading space so the placeholder/value starts one gap past the magnifier,
// matching the resting group exactly. Trailing padding mirrors the leading pad.
padding-inline: calc(var(--search-icon-pad) + var(--search-icon-size) + var(--search-icon-gap)) var(--search-icon-pad);
// Opt out of NC's global input chrome (core/css/inputs.scss adds a border,
// radius and focus box-shadow to any text input not in its exclusion list).
// !important because that global focus rule outweighs a scoped class.
@ -256,6 +267,21 @@ defineExpose({
--resting-background-hover: color-mix(in srgb, var(--color-primary-element) 22%, transparent);
}
// translateX is physical, so flip the resting slide under RTL to keep it moving
// toward the leading (right) edge.
[dir=rtl] .unified-search-input__resting {
--slide-sign: -1;
}
// Respect reduced-motion: keep the end states but drop the slide/fade so nothing
// animates on focus.
@media (prefers-reduced-motion: reduce) {
.unified-search-input__resting,
.unified-search-input__resting span {
transition: none;
}
}
// Mobile: NcHeaderButton styling to match the other header items
.unified-search-input--mobile :deep(.header-menu) {
height: var(--default-clickable-area);

View file

@ -14,11 +14,29 @@
@set:customDateRange="setCustomDateRange"
@update:isOpen="showDateRangeModal = $event" />
<div ref="panel" class="unified-search-modal-container">
<div ref="panel" class="unified-search-modal__container">
<!-- Unified search form -->
<div class="unified-search-modal__header">
<div v-if="isSmallMobile" class="unified-search-modal__mobile-input">
<NcTextField
type="search"
:label="t('core', 'Apps, files, messages, and more')"
:modelValue="searchQuery"
:showTrailingButton="searchQuery.length > 0"
:trailingButtonLabel="t('core', 'Clear search')"
@update:modelValue="onMobileSearchInput"
@trailing-button-click="searchQuery = ''" />
<NcButton
variant="tertiary"
:aria-label="t('core', 'Close search')"
@click="onUpdateOpen(false)">
<template #icon>
<IconClose :size="20" />
</template>
</NcButton>
</div>
<div class="unified-search-modal__filters" data-cy-unified-search-filters>
<NcActions v-model:open="providerActionMenuIsOpen" :menuName="t('core', 'Places')" data-cy-unified-search-filter="places">
<NcActions :open.sync="providerActionMenuIsOpen" :menu-name="t('core', 'Places')" data-cy-unified-search-filter="places">
<template #icon>
<IconListBox :size="20" />
</template>
@ -35,7 +53,7 @@
{{ provider.name }}
</NcActionButton>
</NcActions>
<NcActions v-model:open="dateActionMenuIsOpen" :menuName="t('core', 'Date')" data-cy-unified-search-filter="date">
<NcActions :open.sync="dateActionMenuIsOpen" :menu-name="t('core', 'Date')" data-cy-unified-search-filter="date">
<template #icon>
<IconCalendarRange :size="20" />
</template>
@ -182,28 +200,33 @@
</template>
</div>
</div>
<div class="unified-search-modal-scrim" @click="onUpdateOpen(false)" />
<div class="unified-search-modal__scrim" @click="onUpdateOpen(false)" />
</div>
</transition>
</template>
<script lang="ts">
import type { FocusTrap } from 'focus-trap'
import { subscribe } from '@nextcloud/event-bus'
import { loadState } from '@nextcloud/initial-state'
import { getCanonicalLocale, n, t } from '@nextcloud/l10n'
import { useIsSmallMobile } from '@nextcloud/vue/composables/useIsMobile'
import { useBrowserLocation } from '@vueuse/core'
import debounce from 'debounce'
import { type FocusTrap,createFocusTrap } from 'focus-trap'
import { type PropType,defineComponent, markRaw } from 'vue'
import { createFocusTrap } from 'focus-trap'
import { defineComponent, markRaw } from 'vue'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import NcActions from '@nextcloud/vue/components/NcActions'
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import NcTextField from '@nextcloud/vue/components/NcTextField'
import IconAccountGroup from 'vue-material-design-icons/AccountGroupOutline.vue'
import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue'
import IconCalendarRange from 'vue-material-design-icons/CalendarRangeOutline.vue'
import IconClose from 'vue-material-design-icons/Close.vue'
import IconDotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
import IconFilter from 'vue-material-design-icons/Filter.vue'
import IconListBox from 'vue-material-design-icons/ListBox.vue'
@ -222,6 +245,7 @@ export default defineComponent({
IconArrowRight,
IconAccountGroup,
IconCalendarRange,
IconClose,
IconDotsHorizontal,
IconFilter,
IconListBox,
@ -235,6 +259,7 @@ export default defineComponent({
NcButton,
NcEmptyContent,
NcCheckboxRadioSwitch,
NcTextField,
SearchableList,
SearchResult,
},
@ -263,14 +288,6 @@ export default defineComponent({
type: Boolean,
default: false,
},
/**
* The header input element (lives in a sibling component), for the popover's focus trap
*/
inputElement: {
type: (null as unknown) as PropType<HTMLElement | null>,
default: null,
},
},
emits: ['update:open', 'update:query'],
@ -281,11 +298,13 @@ export default defineComponent({
*/
const currentLocation = useBrowserLocation()
const searchStore = useSearchStore()
const isSmallMobile = useIsSmallMobile()
return {
t,
currentLocation,
externalFilters: searchStore.externalFilters,
isSmallMobile,
}
},
@ -314,7 +333,6 @@ export default defineComponent({
results: [],
contacts: [],
showDateRangeModal: false,
internalIsVisible: this.open,
initialized: false,
searchExternalResources: false,
minSearchLength: loadState('unified-search', 'min-search-length', 1),
@ -378,6 +396,12 @@ export default defineComponent({
return this.filters.some((filter) => filter.type === 'date' || filter.type === 'person')
},
// Any teleported overlay (a filter menu or the date-range dialog) that renders
// outside the focus trap and needs it paused while open.
isOverlayOpen() {
return this.providerActionMenuIsOpen || this.dateActionMenuIsOpen || this.showDateRangeModal
},
filteredResults() {
const isInFolderAtRoot = (result) => {
if (result.id !== 'in-folder') {
@ -447,19 +471,7 @@ export default defineComponent({
}
},
// Filter menus and the date-range dialog teleport outside the trap; pause it
// while any is open so focus can move into them, then resume.
providerActionMenuIsOpen() {
this.toggleFocusTrapForOverlays()
},
dateActionMenuIsOpen() {
this.toggleFocusTrapForOverlays()
},
showDateRangeModal() {
this.toggleFocusTrapForOverlays()
},
isOverlayOpen: 'syncFocusTrapToOverlays',
query: {
immediate: true,
@ -471,7 +483,12 @@ export default defineComponent({
searchQuery: {
handler() {
this.$emit('update:query', this.searchQuery)
this.debouncedFind(this.searchQuery)
// Only search while open: the query prop keeps flowing from the header even
// when closed (e.g. the local search bar on deck), so a hidden modal must
// not fire background searches.
if (this.open) {
this.debouncedFind(this.searchQuery)
}
},
},
@ -499,6 +516,16 @@ export default defineComponent({
}
},
/**
* NcTextField's `update:modelValue` is typed `string | number`; the search
* query is always a string, so normalize here rather than casting in the template.
*
* @param value The new value from the mobile search field
*/
onMobileSearchInput(value: string | number) {
this.searchQuery = String(value)
},
/**
* Close the search on Escape. Sub-menus / sub-dialogs (filter actions, the
* date-range dialog) handle Escape themselves, so only close the popover
@ -510,7 +537,7 @@ export default defineComponent({
if (event.key !== 'Escape') {
return
}
if (this.providerActionMenuIsOpen || this.dateActionMenuIsOpen || this.showDateRangeModal) {
if (this.isOverlayOpen) {
return
}
event.preventDefault()
@ -529,13 +556,17 @@ export default defineComponent({
if (!panel) {
return
}
// focus-trap collects tabbables *inside* each container, not the container
// element itself. The bare <input> is a leaf, so pass its wrapper (which
// holds the input and the clear button); input first to match DOM order.
const inputContainer = (this.inputElement?.closest('.unified-search-input') ?? null) as HTMLElement | null
// focus-trap collects tabbables *inside* each container. The header input is
// in a sibling component under the shared .unified-search-menu ancestor, so we
// query its wrapper from the DOM at activation (not via a prop, which goes
// stale across the mobile/desktop input swap). Input container first for DOM order.
const menu = (this.$el as HTMLElement)?.closest?.('.unified-search-menu') ?? null
const inputContainer = (menu?.querySelector('.unified-search-input') ?? null) as HTMLElement | null
const containers = inputContainer ? [inputContainer, panel] : [panel]
this.focusTrap = markRaw(createFocusTrap(containers, {
initialFocus: this.inputElement ?? panel,
// Prefer the mobile search field (rendered inside the panel) when present;
// falls back to the header input, then the panel itself.
initialFocus: () => panel.querySelector('input[type="search"]') ?? inputContainer?.querySelector('input') ?? panel,
// We own closing via Escape (onEscapeKey) and scrim click
escapeDeactivates: false,
// Let scrim clicks reach their handler instead of being swallowed
@ -552,15 +583,12 @@ export default defineComponent({
this.focusTrap = null
},
/**
* Pause the trap while a teleported menu / dialog is open so focus can move
* into it, and resume once they have all closed.
*/
toggleFocusTrapForOverlays() {
// Filter menus and the date-range dialog teleport outside the trap; pause it
// while any is open so focus can move into them, then resume once all close.
syncFocusTrapToOverlays(overlayOpen: boolean) {
if (!this.focusTrap) {
return
}
const overlayOpen = this.providerActionMenuIsOpen || this.dateActionMenuIsOpen || this.showDateRangeModal
if (overlayOpen) {
this.focusTrap.pause()
} else {
@ -969,15 +997,17 @@ export default defineComponent({
position: absolute;
inset-block-start: 100%;
inset-inline: 0;
// One below the header input (z-index: 51) and above the page. !important wins
// the stacking cascade inside the themed #header.
z-index: 50 !important;
margin-block-start: 8px;
margin-block-start: 6px;
display: flex;
justify-content: center;
}
// Backdrop, mirrors NcModal's .modal-mask. Fixed so it covers the whole viewport
// regardless of the anchored root.
.unified-search-modal-scrim {
.unified-search-modal__scrim {
position: fixed;
inset: 0;
z-index: 0;
@ -987,7 +1017,7 @@ export default defineComponent({
// Dialog panel: NcModal's "normal" chrome, but width-matched to the header input
// and anchored under it, growing downward and scrolling internally when tall.
.unified-search-modal-container {
.unified-search-modal__container {
position: relative;
z-index: 1;
display: flex;
@ -1005,13 +1035,24 @@ export default defineComponent({
background-color: var(--color-main-background);
color: var(--color-main-text);
box-shadow: 0 0 40px rgba(0, 0, 0, 0.2);
// The panel slides down into place; the enter/leave classes set the start offset
transition: transform 100ms ease-out;
// The panel slides down into place; the enter/leave classes set the start offset.
// Same easeOutQuart curve as the header input so the whole search UI moves in step.
transition: transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
}
// Fullscreen on small viewports, mirrors NcModal's responsive breakpoint
@media only screen and ((max-width: 512px) or (max-height: 400px)) {
.unified-search-modal-container {
.unified-search-modal-root {
// Fill the viewport below the header bar, leaving it visible and interactive
// (matches the previous unified search and the rest of the mobile chrome).
position: fixed;
inset-block-start: var(--header-height);
inset-inline: 0;
inset-block-end: 0;
margin-block-start: 0;
}
.unified-search-modal__container {
width: 100%;
max-width: initial;
height: 100%;
@ -1031,11 +1072,24 @@ export default defineComponent({
opacity: 0;
}
.unified-search-modal-enter .unified-search-modal-container,
.unified-search-modal-leave-to .unified-search-modal-container {
.unified-search-modal-enter .unified-search-modal__container,
.unified-search-modal-leave-to .unified-search-modal__container {
transform: translateY(-6px);
}
// Respect reduced-motion: keep the backdrop cross-fade (opacity is not motion) but
// drop the panel slide so nothing moves on open/close.
@media (prefers-reduced-motion: reduce) {
.unified-search-modal__container {
transition: none;
}
.unified-search-modal-enter .unified-search-modal__container,
.unified-search-modal-leave-to .unified-search-modal__container {
transform: none;
}
}
.unified-search-modal {
&__header {
// Add background to prevent leaking scrolled content (because of sticky position)
@ -1049,6 +1103,17 @@ export default defineComponent({
top: 6px;
}
&__mobile-input {
display: flex;
align-items: center;
gap: 4px;
margin-block-end: 8px;
:deep(.input-field) {
flex: 1 1 auto;
}
}
&__filters {
display: flex;
flex-wrap: wrap;

View file

@ -0,0 +1,65 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { shallowMount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
const mobile = ref(false)
vi.mock('@nextcloud/vue/composables/useIsMobile', () => ({
useIsSmallMobile: () => mobile,
useIsMobile: () => ref(false),
}))
vi.mock('@nextcloud/event-bus', () => ({ emit: vi.fn(), subscribe: vi.fn() }))
import UnifiedSearch from '../../views/UnifiedSearch.vue'
function factory() {
return shallowMount(UnifiedSearch, {
global: { mocks: { t: (_: string, s: string) => s, OCP: {} } },
})
}
beforeEach(() => {
mobile.value = false
window.OCP = { Accessibility: { disableKeyboardShortcuts: () => true } }
})
afterEach(() => vi.clearAllMocks())
describe('UnifiedSearch open-state model', () => {
it('desktop: typing opens, clearing closes', async () => {
const wrapper = factory()
wrapper.vm.queryText = 'abc'
await wrapper.vm.$nextTick()
expect(wrapper.vm.showUnifiedSearch).toBe(true)
wrapper.vm.queryText = ''
await wrapper.vm.$nextTick()
expect(wrapper.vm.showUnifiedSearch).toBe(false)
})
it('mobile: typing does NOT open the modal', async () => {
mobile.value = true
const wrapper = factory()
wrapper.vm.queryText = 'abc'
await wrapper.vm.$nextTick()
expect(wrapper.vm.showUnifiedSearch).toBe(false)
})
it('mobile: header button click opens the modal', async () => {
mobile.value = true
const wrapper = factory()
wrapper.findComponent({ name: 'UnifiedSearchInput' }).vm.$emit('click')
await wrapper.vm.$nextTick()
expect(wrapper.vm.showUnifiedSearch).toBe(true)
})
it('mobile: clearing the query does NOT close an open modal', async () => {
mobile.value = true
const wrapper = factory()
wrapper.vm.showUnifiedSearch = true
wrapper.vm.queryText = ''
await wrapper.vm.$nextTick()
expect(wrapper.vm.showUnifiedSearch).toBe(true)
})
})

View file

@ -0,0 +1,75 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { shallowMount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
const mobile = ref(false)
vi.mock('@nextcloud/vue/composables/useIsMobile', () => ({
useIsSmallMobile: () => mobile,
useIsMobile: () => ref(false),
}))
vi.mock('@nextcloud/event-bus', () => ({ subscribe: vi.fn(), emit: vi.fn() }))
vi.mock('@nextcloud/initial-state', () => ({ loadState: vi.fn(() => ([])) }))
vi.mock('../../services/UnifiedSearchService.js', () => ({
getProviders: vi.fn(() => Promise.resolve([])),
getContacts: vi.fn(() => Promise.resolve([])),
search: vi.fn(() => ({ request: Promise.resolve({ data: { ocs: { data: { entries: [] } } } }), cancel: vi.fn() })),
}))
vi.mock('../../store/unified-search-external-filters.js', () => ({
useSearchStore: () => ({ externalFilters: [], scopeToApp: false }),
}))
import UnifiedSearchModal from '../../components/UnifiedSearch/UnifiedSearchModal.vue'
function factory(open = true) {
return shallowMount(UnifiedSearchModal, {
propsData: { open, query: '', localSearch: false },
global: { mocks: { t: (_: string, s: string) => s, n: (_: string, s: string) => s } },
})
}
beforeEach(() => {
mobile.value = false
})
afterEach(() => vi.clearAllMocks())
describe('UnifiedSearchModal mobile input', () => {
it('renders the search field only on mobile', () => {
mobile.value = true
expect(factory().findComponent({ name: 'NcTextField' }).exists()).toBe(true)
})
it('has no in-modal search field on desktop', () => {
expect(factory().findComponent({ name: 'NcTextField' }).exists()).toBe(false)
})
it('mobile close button requests close', async () => {
mobile.value = true
const wrapper = factory()
wrapper.find('.unified-search-modal__mobile-input').findComponent({ name: 'NcButton' }).vm.$emit('click')
await wrapper.vm.$nextTick()
expect(wrapper.emitted('update:open')?.at(-1)).toEqual([false])
})
// Guards the kebab event binding: Vue 2.7 does not normalize v-on names, so a
// camelCase listener would silently miss NcTextField's `trailing-button-click`.
it('mobile clear (trailing) button empties the query', async () => {
mobile.value = true
const wrapper = factory()
wrapper.vm.searchQuery = 'hello'
await wrapper.vm.$nextTick()
wrapper.findComponent({ name: 'NcTextField' }).vm.$emit('trailing-button-click')
await wrapper.vm.$nextTick()
expect(wrapper.vm.searchQuery).toBe('')
})
it('onMobileSearchInput normalises the emitted value to a string', () => {
mobile.value = true
const wrapper = factory()
wrapper.vm.onMobileSearchInput(5)
expect(wrapper.vm.searchQuery).toBe('5')
})
})

View file

@ -5,9 +5,9 @@
<template>
<div class="unified-search-menu">
<UnifiedSearchInput
ref="searchInput"
:query="queryText"
:expanded="showUnifiedSearch || showLocalSearch"
@click="openModal"
@update:query="queryText = $event" />
<UnifiedSearchLocalSearchBar
v-if="supportsLocalSearch"
@ -18,7 +18,6 @@
@update:query="queryText = $event" />
<UnifiedSearchModal
:localSearch="supportsLocalSearch"
:inputElement="inputElement"
:query="queryText"
:open="showUnifiedSearch"
@update:query="queryText = $event"
@ -29,6 +28,7 @@
<script lang="ts">
import { emit, subscribe } from '@nextcloud/event-bus'
import { t } from '@nextcloud/l10n'
import { useIsSmallMobile } from '@nextcloud/vue/composables/useIsMobile'
import { useBrowserLocation } from '@vueuse/core'
import debounce from 'debounce'
import { defineComponent } from 'vue'
@ -48,9 +48,11 @@ export default defineComponent({
setup() {
const currentLocation = useBrowserLocation()
const isSmallMobile = useIsSmallMobile()
return {
currentLocation,
isSmallMobile,
t,
}
@ -64,8 +66,6 @@ export default defineComponent({
showUnifiedSearch: false,
/** Open state of the local search bar */
showLocalSearch: false,
/** The header input element, read from the child and handed to the modal's focus trap */
inputElement: null as HTMLElement | null,
}
},
@ -104,8 +104,9 @@ export default defineComponent({
*/
queryText() {
this.debouncedQueryUpdate()
// Open the search popover only once something is typed; close it when cleared.
if (!this.supportsLocalSearch) {
// Desktop opens/closes the popover as you type; mobile is driven by the
// header button + the modal close paths, so clearing must not collapse it.
if (!this.supportsLocalSearch && !this.isSmallMobile) {
this.showUnifiedSearch = this.queryText.length > 0
}
},
@ -131,9 +132,6 @@ export default defineComponent({
emit('nextcloud:unified-search.search', { query })
})
// Read the header input element from the child so the modal's focus trap can include it
this.inputElement = (this.$refs.searchInput as { getInputElement?: () => HTMLElement | null })?.getInputElement?.() ?? null
// all done
logger.debug('Unified search initialized!')
},

4
dist/2426-2426.js vendored

File diff suppressed because one or more lines are too long

View file

@ -128,6 +128,9 @@ This file is generated from multiple sources. Included packages:
- escape-html
- version: 1.0.3
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hookable
- version: 5.5.3
- license: MIT

File diff suppressed because one or more lines are too long

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

View file

@ -360,6 +360,9 @@ This file is generated from multiple sources. Included packages:
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hast-to-hyperscript
- version: 10.0.3
- license: MIT

File diff suppressed because one or more lines are too long

4
dist/core-login.js vendored

File diff suppressed because one or more lines are too long

View file

@ -221,6 +221,9 @@ This file is generated from multiple sources. Included packages:
- filter-obj
- version: 5.1.0
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hookable
- version: 5.5.3
- license: MIT

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
(()=>{"use strict";var e,r,t,o={43925(e,r,t){var o=t(21777),a=t(81222),n=t(85471);t.nc=(0,o.aV)();const l=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(2311)]).then(t.bind(t,72311))),i=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(2426)]).then(t.bind(t,32426))),c=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(9811)]).then(t.bind(t,59811))),d=(0,a.C)("core","loginFlowState");new n.Ay({render:e=>e("auth"===d?l:"grant"===d?i:c)}).$mount("#core-loginflow")}},a={};function n(e){var r=a[e];if(void 0!==r)return r.exports;var t=a[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,n),t.loaded=!0,t.exports}n.m=o,e=[],n.O=(r,t,o,a)=>{if(!t){var l=1/0;for(u=0;u<e.length;u++){for(var[t,o,a]=e[u],i=!0,c=0;c<t.length;c++)(!1&a||l>=a)&&Object.keys(n.O).every(e=>n.O[e](t[c]))?t.splice(c--,1):(i=!1,a<l&&(l=a));if(i){e.splice(u--,1);var d=o();void 0!==d&&(r=d)}}return r}a=a||0;for(var u=e.length;u>0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[t,o,a]},n.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return n.d(r,{a:r}),r},n.d=(e,r)=>{for(var t in r)n.o(r,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce((r,t)=>(n.f[t](e,r),r),[])),n.u=e=>e+"-"+e+".js?v="+{2311:"851ce89957dd56c95c18",2426:"579790c86922198cbd2a",9811:"975f353a252dd0095921"}[e],n.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud-ui-legacy:",n.l=(e,o,a,l)=>{if(r[e])r[e].push(o);else{var i,c;if(void 0!==a)for(var d=document.getElementsByTagName("script"),u=0;u<d.length;u++){var s=d[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==t+a){i=s;break}}i||(c=!0,(i=document.createElement("script")).charset="utf-8",n.nc&&i.setAttribute("nonce",n.nc),i.setAttribute("data-webpack",t+a),i.src=e),r[e]=[o];var p=(t,o)=>{i.onerror=i.onload=null,clearTimeout(f);var a=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),a&&a.forEach(e=>e(o)),t)return t(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=p.bind(null,i.onerror),i.onload=p.bind(null,i.onload),c&&document.head.appendChild(i)}},n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),n.j=3181,(()=>{var e;globalThis.importScripts&&(e=globalThis.location+"");var r=globalThis.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var o=t.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=t[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),n.p=e})(),(()=>{n.b="undefined"!=typeof document&&document.baseURI||self.location.href;var e={3181:0};n.f.j=(r,t)=>{var o=n.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else{var a=new Promise((t,a)=>o=e[r]=[t,a]);t.push(o[2]=a);var l=n.p+n.u(r),i=new Error;n.l(l,t=>{if(n.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var a=t&&("load"===t.type?"missing":t.type),l=t&&t.target&&t.target.src;i.message="Loading chunk "+r+" failed.\n("+a+": "+l+")",i.name="ChunkLoadError",i.type=a,i.request=l,o[1](i)}},"chunk-"+r,r)}},n.O.j=r=>0===e[r];var r=(r,t)=>{var o,a,[l,i,c]=t,d=0;if(l.some(r=>0!==e[r])){for(o in i)n.o(i,o)&&(n.m[o]=i[o]);if(c)var u=c(n)}for(r&&r(t);d<l.length;d++)a=l[d],n.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return n.O(u)},t=globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),n.nc=void 0;var l=n.O(void 0,[4208],()=>n(43925));l=n.O(l)})();
//# sourceMappingURL=core-login_flow.js.map?v=36a4298a97527bc500f9
(()=>{"use strict";var e,r,t,o={43925(e,r,t){var o=t(21777),a=t(81222),n=t(85471);t.nc=(0,o.aV)();const l=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(2311)]).then(t.bind(t,72311))),i=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(2426)]).then(t.bind(t,32426))),c=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(9811)]).then(t.bind(t,59811))),d=(0,a.C)("core","loginFlowState");new n.Ay({render:e=>e("auth"===d?l:"grant"===d?i:c)}).$mount("#core-loginflow")}},a={};function n(e){var r=a[e];if(void 0!==r)return r.exports;var t=a[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,n),t.loaded=!0,t.exports}n.m=o,e=[],n.O=(r,t,o,a)=>{if(!t){var l=1/0;for(u=0;u<e.length;u++){for(var[t,o,a]=e[u],i=!0,c=0;c<t.length;c++)(!1&a||l>=a)&&Object.keys(n.O).every(e=>n.O[e](t[c]))?t.splice(c--,1):(i=!1,a<l&&(l=a));if(i){e.splice(u--,1);var d=o();void 0!==d&&(r=d)}}return r}a=a||0;for(var u=e.length;u>0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[t,o,a]},n.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return n.d(r,{a:r}),r},n.d=(e,r)=>{for(var t in r)n.o(r,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce((r,t)=>(n.f[t](e,r),r),[])),n.u=e=>e+"-"+e+".js?v="+{2311:"851ce89957dd56c95c18",2426:"ad80104d3c7b9fa0e6ae",9811:"975f353a252dd0095921"}[e],n.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud-ui-legacy:",n.l=(e,o,a,l)=>{if(r[e])r[e].push(o);else{var i,c;if(void 0!==a)for(var d=document.getElementsByTagName("script"),u=0;u<d.length;u++){var s=d[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==t+a){i=s;break}}i||(c=!0,(i=document.createElement("script")).charset="utf-8",n.nc&&i.setAttribute("nonce",n.nc),i.setAttribute("data-webpack",t+a),i.src=e),r[e]=[o];var p=(t,o)=>{i.onerror=i.onload=null,clearTimeout(f);var a=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),a&&a.forEach(e=>e(o)),t)return t(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=p.bind(null,i.onerror),i.onload=p.bind(null,i.onload),c&&document.head.appendChild(i)}},n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),n.j=3181,(()=>{var e;globalThis.importScripts&&(e=globalThis.location+"");var r=globalThis.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var o=t.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=t[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),n.p=e})(),(()=>{n.b="undefined"!=typeof document&&document.baseURI||self.location.href;var e={3181:0};n.f.j=(r,t)=>{var o=n.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else{var a=new Promise((t,a)=>o=e[r]=[t,a]);t.push(o[2]=a);var l=n.p+n.u(r),i=new Error;n.l(l,t=>{if(n.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var a=t&&("load"===t.type?"missing":t.type),l=t&&t.target&&t.target.src;i.message="Loading chunk "+r+" failed.\n("+a+": "+l+")",i.name="ChunkLoadError",i.type=a,i.request=l,o[1](i)}},"chunk-"+r,r)}},n.O.j=r=>0===e[r];var r=(r,t)=>{var o,a,[l,i,c]=t,d=0;if(l.some(r=>0!==e[r])){for(o in i)n.o(i,o)&&(n.m[o]=i[o]);if(c)var u=c(n)}for(r&&r(t);d<l.length;d++)a=l[d],n.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return n.O(u)},t=globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),n.nc=void 0;var l=n.O(void 0,[4208],()=>n(43925));l=n.O(l)})();
//# sourceMappingURL=core-login_flow.js.map?v=5e97ca85bd50d607a4bd

File diff suppressed because one or more lines are too long

4
dist/core-main.js vendored

File diff suppressed because one or more lines are too long

View file

@ -253,6 +253,9 @@ This file is generated from multiple sources. Included packages:
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hookable
- version: 5.5.3
- license: MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -129,6 +129,9 @@ This file is generated from multiple sources. Included packages:
- escape-html
- version: 1.0.3
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hookable
- version: 5.5.3
- license: MIT

File diff suppressed because one or more lines are too long

View file

@ -115,6 +115,9 @@ This file is generated from multiple sources. Included packages:
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- ieee754
- version: 1.2.1
- license: BSD-3-Clause

File diff suppressed because one or more lines are too long

View file

@ -217,6 +217,9 @@ This file is generated from multiple sources. Included packages:
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hookable
- version: 5.5.3
- license: MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -191,6 +191,9 @@ This file is generated from multiple sources. Included packages:
- escape-html
- version: 1.0.3
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hookable
- version: 5.5.3
- license: MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -217,6 +217,9 @@ This file is generated from multiple sources. Included packages:
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hookable
- version: 5.5.3
- license: MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -294,6 +294,9 @@ This file is generated from multiple sources. Included packages:
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hast-to-hyperscript
- version: 10.0.3
- license: MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -218,6 +218,9 @@ This file is generated from multiple sources. Included packages:
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hookable
- version: 5.5.3
- license: MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -230,6 +230,9 @@ This file is generated from multiple sources. Included packages:
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hookable
- version: 5.5.3
- license: MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -205,6 +205,9 @@ This file is generated from multiple sources. Included packages:
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hookable
- version: 5.5.3
- license: MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -196,6 +196,9 @@ This file is generated from multiple sources. Included packages:
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hookable
- version: 5.5.3
- license: MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -322,6 +322,9 @@ This file is generated from multiple sources. Included packages:
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hast-to-hyperscript
- version: 10.0.3
- license: MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -229,6 +229,9 @@ This file is generated from multiple sources. Included packages:
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 8.2.2
- license: MIT
- hookable
- version: 5.5.3
- license: MIT

File diff suppressed because one or more lines are too long