@@ -451,7 +513,7 @@ import { useRouter } from 'vue-router'
import { dialog } from '@/composables/dialog'
import { notify } from '@/composables/notify'
-import { useScreen } from '@/composables/screen'
+import { useMinWidth, useScreen } from '@/composables/screen'
import { useDark } from '@/composables/dark'
import { useCommonStore } from '@/stores/common'
@@ -533,6 +595,12 @@ const state = reactive({
searchIsFocused: false,
currentFolderId: null,
currentFileId: null,
+ /**
+ * Whether the folder tree has been opened. Only consulted while it overlays the list — beside it, it
+ * is simply there. Deliberately NOT one of the remembered view options: those describe how a list is
+ * drawn, and this is a panel that is open at the moment.
+ */
+ treeOpen: false,
treeNodes: {},
treeRoots: [],
displayMode: 'title',
@@ -590,6 +658,34 @@ const treeComp = ref(null)
const insertMode = computed(() => siteStore.overlayOpts?.insertMode ?? false)
+/**
+ * Whether the folder tree is a panel over the list rather than a column beside it.
+ *
+ * 1024 is `WDrawer`'s own default `overlayBelow`, which is what actually decides how the drawer draws
+ * itself — this is the same question asked from the outside, so that the toolbar knows whether to offer a
+ * way in. The two have to agree.
+ */
+const isAtLeastMd = useMinWidth(1024)
+const isTreeOverlay = computed(() => !isAtLeastMd.value)
+
+/**
+ * Whether the details pane is beside the list. It is a 350px column with no overlay form, so below 1440px
+ * there is simply no room for it -- which is also why the Insert button it holds needs a second home; see
+ * the toolbar.
+ */
+const detailsPaneShown = computed(() => screen.gt.md)
+
+/**
+ * The tree drawer's open state: always open where it has a column of its own, and the reader's to decide
+ * where it overlays. The setter is what the drawer's scrim reaches when it is tapped.
+ */
+const treeDrawerOpen = computed({
+ get: () => !isTreeOverlay.value || state.treeOpen,
+ set: (val) => {
+ state.treeOpen = val
+ }
+})
+
const folderPath = computed(() => {
if (!state.currentFolderId) {
return '/'
@@ -716,12 +812,35 @@ const currentFileDetails = computed(() => {
watch(
() => state.currentFolderId,
async (newValue) => {
+ /*
+ Picking a folder is what the tree is open FOR, so it closes behind the choice -- the contents of that
+ folder are in the list underneath, which the panel is covering. Only while it overlays; beside the
+ list it is a column and there is nothing to close.
+ */
+ state.treeOpen = false
await loadTree({ parentId: newValue })
}
)
// METHODS
+/**
+ * Put the folder tree away when the list behind it is tapped.
+ *
+ * A no-op unless the tree is actually overlaying and open, so an ordinary click on a file — which is what
+ * this handler mostly receives — costs nothing and behaves as it always did.
+ */
+function dismissTreeOverlay(ev) {
+ if (!isTreeOverlay.value || !state.treeOpen) {
+ return
+ }
+ // -> The toolbar's own button is what opened it; closing here would undo that on the way back up
+ if (ev?.target?.closest?.('.fileman-toolbar')) {
+ return
+ }
+ state.treeOpen = false
+}
+
function close() {
siteStore.overlay = null
}
@@ -1375,7 +1494,51 @@ onBeforeUnmount(() => {
diff --git a/frontend/src/pages/ProfileAvatar.vue b/frontend/src/pages/ProfileAvatar.vue
index 6629c4202..1103b3499 100644
--- a/frontend/src/pages/ProfileAvatar.vue
+++ b/frontend/src/pages/ProfileAvatar.vue
@@ -1,8 +1,17 @@
-
-
+
+
+
+
-
+
{{ t('profile.avatarUploadTitle') }}
{{ t('profile.avatarUploadHint') }}
@@ -35,7 +44,7 @@
@click="clearImage" />
-
+
{{ t('profile.avatarUploadDisabled') }}
diff --git a/frontend/src/pages/Search.vue b/frontend/src/pages/Search.vue
index f591ffc73..74bfd27e7 100644
--- a/frontend/src/pages/Search.vue
+++ b/frontend/src/pages/Search.vue
@@ -14,7 +14,28 @@
t('common.actions.goback')
}}
-
+
+
+
+
+
{
const state = reactive({
loading: 0,
+ /** Whether the sort/filter panel is open. Only consulted below 900px, where it is a disclosure. */
+ filtersOpen: false,
params: {
filterPath: '',
filterLocale: [],
@@ -284,6 +308,18 @@ const state = reactive({
total: 0
})
+/**
+ * Below 900px, where the filter panel stops being a column beside the results and becomes a disclosure
+ * above them.
+ *
+ * This layout's own breakpoint rather than one of the app's, and the same one `ProfileLayout` uses for its
+ * nav: the two screens are the same shape -- a card with a 300px sidebar -- so they run out of room at the
+ * same width. The stylesheet has to agree with it; `$filters-collapse-max` is the same boundary from the
+ * other side.
+ */
+const isAtLeast900 = useMinWidth(900)
+const isFiltersCollapsed = computed(() => !isAtLeast900.value)
+
const orderByOptions = computed(() => {
return [
{ label: t('search.sortByRelevance'), value: 'relevancy', icon: 'la:stream' },
@@ -336,6 +372,10 @@ function humanizeDate(val) {
return userStore.formatDateTime(t, val)
}
+function toggleFilters() {
+ state.filtersOpen = !state.filtersOpen
+}
+
function setOrderBy(val) {
if (val === state.params.orderBy) {
state.params.orderByDirection = state.params.orderByDirection === 'desc' ? 'asc' : 'desc'
@@ -460,6 +500,17 @@ onUnmounted(() => {