mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat(mobile): search box in the Select Case picker
The phone case picker had no way to narrow a long case list, so finding one meant scrolling a sheet that showed about six rows at a time. - A search field filters rows by name (every typed word must match, any order, case-insensitive), with a "No matching cases" state. Enter picks the case when exactly one row is left; Escape clears, then closes. - The field is not auto-focused, so opening the picker does not raise the keyboard. The list holds its unfiltered height while searching so the sheet does not jump, and the input is 16px so iOS Safari does not zoom. - Layout: the sheet padded the home-indicator inset on top of the footer already doing so, leaving a dead band under Create New Case; the sheet now grows to 80dvh and the list fills it instead of a separate 50vh cap. - Opening scrolls the list (its own box, not scrollIntoView) to the currently selected case. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,8 @@
|
|||||||
'Manage AI Coding tools in persistent tmux sessions.': '在持久化 tmux 会话中管理 AI 编程工具。',
|
'Manage AI Coding tools in persistent tmux sessions.': '在持久化 tmux 会话中管理 AI 编程工具。',
|
||||||
'Select case': '选择案例',
|
'Select case': '选择案例',
|
||||||
'Select Case': '选择案例',
|
'Select Case': '选择案例',
|
||||||
|
'Search cases': '搜索案例',
|
||||||
|
'No matching cases': '没有匹配的案例',
|
||||||
'All cases': '全部案例',
|
'All cases': '全部案例',
|
||||||
'No directory': '未选择目录',
|
'No directory': '未选择目录',
|
||||||
Run: '运行',
|
Run: '运行',
|
||||||
|
|||||||
@@ -3204,7 +3204,12 @@
|
|||||||
<h3>Select Case</h3>
|
<h3>Select Case</h3>
|
||||||
<button class="modal-close" onclick="app.closeMobileCasePicker()" aria-label="Close case picker">×</button>
|
<button class="modal-close" onclick="app.closeMobileCasePicker()" aria-label="Close case picker">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mobile-case-picker-search">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||||
|
<input type="search" id="mobileCaseSearch" placeholder="Search cases" aria-label="Search cases" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" enterkeyhint="go" oninput="app.filterMobileCases()" onkeydown="app.onMobileCaseSearchKey(event)">
|
||||||
|
</div>
|
||||||
<div class="mobile-case-picker-body">
|
<div class="mobile-case-picker-body">
|
||||||
|
<div class="mobile-case-empty" id="mobileCaseEmpty" hidden>No matching cases</div>
|
||||||
<div class="mobile-case-list" id="mobileCaseList">
|
<div class="mobile-case-list" id="mobileCaseList">
|
||||||
<!-- Cases populated by JS -->
|
<!-- Cases populated by JS -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2165,9 +2165,11 @@ html.mobile-init .file-browser-panel {
|
|||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The footer already reserves the home-indicator inset; padding the sheet too
|
||||||
|
counted it twice and left a dead band under Create New Case. */
|
||||||
.mobile-case-picker-sheet {
|
.mobile-case-picker-sheet {
|
||||||
max-height: 60vh;
|
max-height: 80vh;
|
||||||
padding-bottom: var(--safe-area-bottom);
|
max-height: 80dvh;
|
||||||
animation: slideUp 0.2s ease-out;
|
animation: slideUp 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4506,6 +4506,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
const isSelected = c.name === currentCase;
|
const isSelected = c.name === currentCase;
|
||||||
html += `
|
html += `
|
||||||
<button class="mobile-case-item ${isSelected ? 'selected' : ''}"
|
<button class="mobile-case-item ${isSelected ? 'selected' : ''}"
|
||||||
|
data-search="${escapeHtml(`${c.label} ${c.name}`.toLowerCase())}"
|
||||||
onclick="app.selectMobileCase(${escapeHtml(JSON.stringify(c.name))})">
|
onclick="app.selectMobileCase(${escapeHtml(JSON.stringify(c.name))})">
|
||||||
<span class="mobile-case-item-icon">
|
<span class="mobile-case-item-icon">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
@@ -4528,7 +4529,61 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listContainer.innerHTML = html;
|
listContainer.innerHTML = html;
|
||||||
|
// Every open starts unfiltered. The search box is not focused on purpose:
|
||||||
|
// that would raise the phone keyboard over a list most opens just tap.
|
||||||
|
const search = document.getElementById('mobileCaseSearch');
|
||||||
|
if (search) search.value = '';
|
||||||
|
listContainer.parentElement.style.minHeight = '';
|
||||||
|
this.filterMobileCases();
|
||||||
modal.classList.add('active');
|
modal.classList.add('active');
|
||||||
|
// Bring the current case into view when the list is longer than the sheet.
|
||||||
|
// Scroll the list's own box, never scrollIntoView(), which can also scroll
|
||||||
|
// the document under the fixed header.
|
||||||
|
const body = listContainer.parentElement;
|
||||||
|
const selected = listContainer.querySelector('.mobile-case-item.selected');
|
||||||
|
if (body && selected) {
|
||||||
|
const top = selected.offsetTop - body.offsetTop;
|
||||||
|
if (top + selected.offsetHeight > body.scrollTop + body.clientHeight) {
|
||||||
|
body.scrollTop = top - (body.clientHeight - selected.offsetHeight) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Hide case rows whose name does not contain every word typed in the search box. */
|
||||||
|
filterMobileCases() {
|
||||||
|
const search = document.getElementById('mobileCaseSearch');
|
||||||
|
const words = (search?.value || '').toLowerCase().split(/\s+/).filter(Boolean);
|
||||||
|
// Hold the list at its unfiltered height while searching, so the sheet (and
|
||||||
|
// the input under the thumb) does not jump as rows disappear.
|
||||||
|
const body = document.querySelector('.mobile-case-picker-body');
|
||||||
|
if (body && words.length && !body.style.minHeight) body.style.minHeight = `${body.offsetHeight}px`;
|
||||||
|
let shown = 0;
|
||||||
|
for (const item of document.querySelectorAll('#mobileCaseList .mobile-case-item')) {
|
||||||
|
const hay = item.dataset.search || '';
|
||||||
|
const match = words.every((w) => hay.includes(w));
|
||||||
|
item.hidden = !match;
|
||||||
|
if (match) shown++;
|
||||||
|
}
|
||||||
|
const empty = document.getElementById('mobileCaseEmpty');
|
||||||
|
if (empty) empty.hidden = shown > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Enter picks the case when the search narrows the list to exactly one; Escape clears, then closes. */
|
||||||
|
onMobileCaseSearchKey(event) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
const visible = [...document.querySelectorAll('#mobileCaseList .mobile-case-item:not([hidden])')];
|
||||||
|
if (visible.length === 1) visible[0].click();
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (event.target.value) {
|
||||||
|
event.target.value = '';
|
||||||
|
this.filterMobileCases();
|
||||||
|
} else {
|
||||||
|
this.closeMobileCasePicker();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
closeMobileCasePicker() {
|
closeMobileCasePicker() {
|
||||||
|
|||||||
@@ -7072,12 +7072,61 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-case-picker-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 12px 20px 4px;
|
||||||
|
padding: 0 12px;
|
||||||
|
background: var(--bg-input);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-case-picker-search:focus-within {
|
||||||
|
border-color: var(--accent, #22c55e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-case-picker-search input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 10px 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: var(--text);
|
||||||
|
/* 16px keeps iOS Safari from zooming the page on focus */
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The wrapper draws the focus state; the global input focus ring doubled it */
|
||||||
|
#mobileCaseSearch:focus {
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-case-empty {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-case-empty[hidden],
|
||||||
|
.mobile-case-item[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-case-picker-body {
|
.mobile-case-picker-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
max-height: 50vh;
|
/* Fill the sheet (its max-height is the cap); a separate cap here left the
|
||||||
|
list shorter than the sheet could show. min-height lets flex shrink it. */
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-case-list {
|
.mobile-case-list {
|
||||||
|
|||||||
Reference in New Issue
Block a user