ux: clear-search is now clearer on a phone, and you can edit your search
Massimo Melina committed
Feb 23, 2026 at 23:24 UTC
de5b1ff615d86000c0a5ad97cc8fb2061232517c
3 files changed
+39
-36
frontend/src/index.scss
+5
-3
@@ -408,8 +408,10 @@ ul.dir {
408
padding: min(1vh, 0.5em) 0; // avoid wasting too much vertical space, and no need for horizontal padding as we are using flex to grow
409
}
410
}
411
+#search-clear-button { margin-right: .5em }
412
#searched {
412
- margin: .2em;
413
+ margin-bottom: 0.2em;
414
+ margin-top: 0.3em;
415
}
416
#user-panel {
417
display:flex;
@@ -864,8 +866,8 @@ form label+input { margin-top: .2em; }
866
@media (max-width: 42em) {
867
:root { --ghost-contrast: #8883; } /* phones have different curve */
868
body, button, select { font-size: 14pt; }
867
- #menu-bar, #filter-bar, #clipBar {
868
- button .icon + .label { display: none } /* icons only */
869
+ #menu-bar, #filter-bar, #clipBar, #search-clear-button {
870
+ .icon + .label { display: none } /* icons only */
871
}
872
#filter-bar {
873
margin-top: 0.4em;
frontend/src/menu.ts
+33
-32
@@ -87,7 +87,22 @@ export function MenuPanel() {
87
onClick() {
88
cut(onlyTruthy(Object.keys(selected).map(uri => _.find(state.list, { uri }))))
89
}
90
- } : getSearchProps()),
90
+ } : stopSearch && justStarted ? { // don't change the state of the search button immediately to avoid it flicking at every folder change
91
+ id: 'search-stop-button',
92
+ icon: 'stop',
93
+ label: t`Stop list`,
94
+ className: 'ani-working',
95
+ onClick() {
96
+ stopSearch()
97
+ state.searchManuallyInterrupted = true
98
+ }
99
+ } : {
100
+ id: 'search-button',
101
+ icon: 'search',
102
+ label: t`Search`,
103
+ onClickAnimation: false,
104
+ onClick: searchDialog,
105
+ }),
106
h(Btn, {
107
id: 'options-button',
108
icon: 'options',
@@ -123,37 +138,22 @@ export function MenuPanel() {
138
h(CustomCode, { name: 'appendMenuBar' }),
139
),
140
remoteSearch && h('div', { id: 'searched' },
141
+ !stopSearch && h(Btn, {
142
+ id: 'search-clear-button',
143
+ icon: 'search_off',
144
+ label: t`Clear search`,
145
+ className: 'small',
146
+ onClick() {
147
+ state.remoteSearch = undefined
148
+ }
149
+ }),
150
(stopSearch ? t`Searching` : t`Searched`) + ': ',
127
- _.map({ search: t`Name`, searchComment: t`Comment` }, (v,k) => prefix(v + ': ', remoteSearch[k])).filter(Boolean).join(' and '),
128
- prefix(' (', searchManuallyInterrupted && t`Interrupted`, ')')
151
+ _.map({ search: t`Name`, searchComment: t`Comment` } satisfies { [K in RSK]?: string },
152
+ (v,k) => prefix(v + ': ', remoteSearch[k as RSK])).filter(Boolean).join(' and '),
153
+ prefix(' (', searchManuallyInterrupted && t`Interrupted`, ')'),
154
),
155
)
131
-
132
- function getSearchProps() {
133
- return stopSearch && justStarted ? { // don't change the state of the search button immediately to avoid it flicking at every folder change
134
- id: 'search-stop-button',
135
- icon: 'stop',
136
- label: t`Stop list`,
137
- className: 'ani-working',
138
- onClick() {
139
- stopSearch()
140
- state.searchManuallyInterrupted = true
141
- }
142
- } : state.remoteSearch && !stopSearch ? {
143
- id: 'search-clear-button',
144
- icon: 'search_off',
145
- label: t`Clear search`,
146
- onClick() {
147
- state.remoteSearch = undefined
148
- }
149
- } : {
150
- id: 'search-button',
151
- icon: 'search',
152
- label: t`Search`,
153
- onClickAnimation: false,
154
- onClick: searchDialog,
155
- }
156
- }
156
+ type RSK = keyof typeof remoteSearch
157
}
158
159
function isAllSelected() {
@@ -216,6 +216,7 @@ export async function deleteFiles(uris: string[]) {
216
}
217
218
function searchDialog() {
219
+ const was = state.remoteSearch
220
formDialog({
221
title: t`Search`,
222
dialogProps: { id: 'search-dialog' },
@@ -225,14 +226,14 @@ function searchDialog() {
226
t('search_msg', "Search this folder and sub-folders"),
227
h('div', { className: 'field name' },
228
h('label', { htmlFor: 'name' }, t`Name`),
228
- h('input', { name: 'name', style, autoFocus: true, }),
229
+ h('input', { name: 'name', style, autoFocus: true, defaultValue: was?.search }),
230
),
231
h('div', { className: 'field comment' },
232
h('label', { htmlFor: 'comment' }, t`Comment`),
232
- h('input', { name: 'comment', style, }),
233
+ h('input', { name: 'comment', style, defaultValue: was?.searchComment }),
234
),
235
h('div', { className: 'field wildcards' },
235
- h(Checkbox, { name: 'wild', defaultChecked: true }, t`Wildcards`,
236
+ h(Checkbox, { name: 'wild', defaultChecked: !was?.wild }, t`Wildcards`,
237
h('a', { href: `${WIKI_URL}Wildcards`, target: 'doc' }, hIcon('info'))), // uncontrolled
238
),
239
h('div', { className: 'submit' },
frontend/src/state.ts
+1
-1
@@ -21,7 +21,7 @@ export const state = proxy<typeof FRONTEND_OPTIONS & {
21
patternFilter: string,
22
showFilter: boolean,
23
selected: { [uri:string]: true }, // by using an object instead of an array, Entry components are not rendered when others get selected
24
- remoteSearch: Dict<any> | undefined,
24
+ remoteSearch: { search?: string, searchComment?: string, wild?: string } | undefined,
25
adminUrl?: string,
26
loginRequired?: boolean, // force user to login before proceeding
27
messageOnly?: string, // no gui, just show this message