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