From 90b9ea51343028704dc03326b3df9ad80382cf3d Mon Sep 17 00:00:00 2001 From: Daniel Dada Date: Wed, 4 Feb 2026 23:45:25 +0300 Subject: [PATCH] updated list items ui --- list-manager/list-manager.css | 185 ++++++++++++++++++++-- list-manager/list-manager.html | 1 + popup/popup.css | 133 ++++++++++++++-- popup/popup.html | 1 + src/list-manager/ListManagerController.ts | 169 ++++++++++++++++---- src/popup/PopupController.ts | 153 +++++++++++++++--- 6 files changed, 555 insertions(+), 87 deletions(-) diff --git a/list-manager/list-manager.css b/list-manager/list-manager.css index dcb327d..396b022 100644 --- a/list-manager/list-manager.css +++ b/list-manager/list-manager.css @@ -137,40 +137,86 @@ body { .list-hint { font-size: 0.8rem; opacity: 0.7; + margin-top: -4px; +} + +.selection-hint { + font-size: 0.75rem; + opacity: 0.6; + margin-top: 4px; + font-style: italic; } .lists { display: flex; flex-direction: column; - gap: 8px; + gap: 10px; overflow-y: auto; padding-right: 4px; } +/* Custom scrollbar styling */ +.lists::-webkit-scrollbar, +.word-list::-webkit-scrollbar { + width: 8px; +} + +.lists::-webkit-scrollbar-track, +.word-list::-webkit-scrollbar-track { + background: #1a1511; + border-radius: 10px; +} + +.lists::-webkit-scrollbar-thumb, +.word-list::-webkit-scrollbar-thumb { + background: var(--input-border); + border-radius: 10px; + transition: background 0.2s ease; +} + +.lists::-webkit-scrollbar-thumb:hover, +.word-list::-webkit-scrollbar-thumb:hover { + background: #4a3e36; +} + .list-item { background: #1f1813; - border: 1px solid transparent; + border: 2px solid transparent; border-radius: 12px; padding: 10px 12px; display: grid; - grid-template-columns: auto 1fr auto; + grid-template-columns: 1fr auto; gap: 10px; align-items: center; cursor: pointer; + transition: all 0.2s ease; + position: relative; +} + +.list-item:hover { + background: #251f19; + border-color: rgba(242, 168, 101, 0.3); } .list-item.active { border-color: var(--accent); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); + background: #2a2218; +} + +.list-item.selected { + background: rgba(242, 168, 101, 0.15); + border-color: rgba(242, 168, 101, 0.6); +} + +.list-item.selected.active { + border-color: var(--accent); + background: rgba(242, 168, 101, 0.2); } .list-item.drag-over { border-color: var(--accent-hover); -} - -.list-item input[type="checkbox"] { - width: 16px; - height: 16px; + transform: scale(1.02); } .list-meta { @@ -279,7 +325,7 @@ body { background: #1a1511; display: flex; flex-direction: column; - gap: 6px; + gap: 8px; } .empty { @@ -291,20 +337,129 @@ body { .word-item { display: grid; - grid-template-columns: auto 1fr auto auto auto; + grid-template-columns: 1fr auto auto auto auto; gap: 8px; align-items: center; - padding: 6px 8px; + padding: 8px 10px; border-radius: 10px; background: #201915; + border: 2px solid transparent; + cursor: pointer; + transition: all 0.2s ease; + position: relative; +} + +.word-item:hover { + background: #2a2218; + border-color: rgba(242, 168, 101, 0.2); +} + +.word-item.selected { + background: rgba(242, 168, 101, 0.15); + border-color: rgba(242, 168, 101, 0.6); } .word-item.disabled { opacity: 0.5; } -.word-item input[type="text"] { +.word-text { + font-size: 0.9rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + user-select: none; +} + +.word-text.editing { + display: none; +} + +.word-edit-input { + display: none; width: 100%; + background: var(--input-bg); + color: var(--text-color); + border: 1px solid var(--accent); + border-radius: 8px; + padding: 4px 8px; + font-size: 0.9rem; +} + +.word-edit-input.active { + display: block; +} + +.word-actions { + display: flex; + gap: 4px; + align-items: center; + pointer-events: auto; +} + +.word-actions > * { + pointer-events: auto; +} + +.icon-btn { + background: transparent; + border: none; + color: var(--text-color); + opacity: 0.6; + cursor: pointer; + padding: 4px 6px; + border-radius: 6px; + transition: all 0.2s ease; + font-size: 0.85rem; + display: flex; + align-items: center; + justify-content: center; + min-width: 28px; + min-height: 28px; +} + +.icon-btn:hover { + opacity: 1; + background: rgba(242, 168, 101, 0.15); + color: var(--accent); +} + +.icon-btn i { + pointer-events: none; +} + +.toggle-btn { + width: 40px; + height: 22px; + background: rgba(255, 255, 255, 0.1); + border: 1px solid var(--input-border); + border-radius: 11px; + position: relative; + cursor: pointer; + transition: all 0.2s ease; + padding: 0; +} + +.toggle-btn::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + background: var(--text-color); + border-radius: 50%; + top: 2px; + left: 2px; + transition: all 0.2s ease; +} + +.toggle-btn.active { + background: var(--accent); + border-color: var(--accent); +} + +.toggle-btn.active::after { + left: 20px; + background: var(--accent-text); } button { @@ -379,6 +534,7 @@ input[type="color"]::-webkit-color-swatch { width: 38px; height: 28px; border-radius: 8px; + cursor: pointer; } .word-item input[type="color"]::-webkit-color-swatch-wrapper { @@ -389,6 +545,11 @@ input[type="color"]::-webkit-color-swatch { border-radius: 6px; } +.word-item input[type="color"]:hover { + transform: scale(1.05); + transition: transform 0.2s ease; +} + .stats { font-size: 0.85rem; opacity: 0.7; diff --git a/list-manager/list-manager.html b/list-manager/list-manager.html index 99ef70a..e96a2f0 100644 --- a/list-manager/list-manager.html +++ b/list-manager/list-manager.html @@ -85,6 +85,7 @@ +
Click to select • Ctrl/Cmd+Click for multi-select • Click edit icon to rename
diff --git a/popup/popup.css b/popup/popup.css index 0e7cb7c..4cd8176 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -272,10 +272,18 @@ body.light { overflow: hidden; min-height: 0; display: grid; - grid-template-rows: auto auto auto 1fr; + grid-template-rows: auto auto auto 1fr auto; row-gap: 6px; } +.selection-hint { + font-size: 0.7em; + opacity: 0.6; + text-align: center; + font-style: italic; + padding: 4px 0 0 0; +} + .section[data-section="addwords"] textarea { height: 44px; } @@ -589,23 +597,29 @@ input[type="file"] { left: 4px; height: 34px; display: grid; - grid-template-columns: 18px 1fr 26px 26px; + grid-template-columns: 1fr auto auto auto auto; align-items: center; gap: 6px; padding: 6px 8px; box-sizing: border-box; border-radius: 8px; background: var(--input-bg); - border: 1px solid var(--input-border); + border: 2px solid transparent; transition: all 0.2s; + cursor: pointer; } #wordList .word-item:hover { background: var(--highlight-tag); - border-color: var(--accent); + border-color: rgba(242, 168, 101, 0.3); box-shadow: var(--shadow-sm); } +#wordList .word-item.selected { + background: rgba(242, 168, 101, 0.15); + border-color: rgba(242, 168, 101, 0.6); +} + #wordList .word-item.disabled { opacity: 0.5; background: var(--section-bg); @@ -615,18 +629,30 @@ input[type="file"] { opacity: 0.65; } -#wordList .word-item.disabled input[type="text"] { - color: var(--text-color) !important; - opacity: 0.6; +#wordList .word-item.disabled .word-text { text-decoration: line-through; } -#wordList input[type="text"] { +#wordList .word-text { + font-size: 0.8em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + user-select: none; + min-width: 0; +} + +#wordList .word-text.editing { + display: none; +} + +#wordList .word-edit-input { + display: none; min-width: 0; width: 100%; background-color: var(--section-bg) !important; color: var(--text-color) !important; - border: 1px solid var(--input-border) !important; + border: 1px solid var(--accent) !important; padding: 4px 6px; border-radius: 6px; font-size: 0.8em; @@ -634,11 +660,88 @@ input[type="file"] { margin: 0; } -#wordList input[type="text"]:focus { +#wordList .word-edit-input.active { + display: block; +} + +#wordList .word-edit-input:focus { border-color: var(--accent) !important; outline: none; } +#wordList .word-actions { + display: flex; + gap: 4px; + align-items: center; + pointer-events: auto; +} + +#wordList .word-actions > * { + pointer-events: auto; +} + +#wordList .icon-btn { + background: transparent; + border: none; + color: var(--text-color); + opacity: 0.6; + cursor: pointer; + padding: 3px 5px; + border-radius: 4px; + transition: all 0.2s ease; + font-size: 0.75em; + display: flex; + align-items: center; + justify-content: center; + min-width: 22px; + min-height: 22px; +} + +#wordList .icon-btn:hover { + opacity: 1; + background: rgba(242, 168, 101, 0.15); + color: var(--accent); +} + +#wordList .icon-btn i { + pointer-events: none; +} + +#wordList .toggle-btn { + width: 32px; + height: 18px; + background: rgba(255, 255, 255, 0.1); + border: 1px solid var(--input-border); + border-radius: 9px; + position: relative; + cursor: pointer; + transition: all 0.2s ease; + padding: 0; + flex-shrink: 0; +} + +#wordList .toggle-btn::after { + content: ''; + position: absolute; + width: 12px; + height: 12px; + background: var(--text-color); + border-radius: 50%; + top: 2px; + left: 2px; + transition: all 0.2s ease; +} + +#wordList .toggle-btn.active { + background: var(--accent); + border-color: var(--accent); +} + +#wordList .toggle-btn.active::after { + left: 16px; + background: var(--accent-text); +} + #wordList input[type="color"] { width: 22px; height: 22px; @@ -646,18 +749,14 @@ input[type="file"] { border-radius: 3px; border: 1px solid var(--input-border); cursor: pointer; - transition: border-color 0.2s; + transition: all 0.2s; align-self: center; + flex-shrink: 0; } #wordList input[type="color"]:hover { border-color: var(--accent); -} - -#wordList input[type="checkbox"] { - margin: 0; - width: 16px; - height: 16px; + transform: scale(1.05); } #wordCount { diff --git a/popup/popup.html b/popup/popup.html index cccacf1..9d24836 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -101,6 +101,7 @@
+
Click to select • Ctrl/Cmd+Click for multi-select
diff --git a/src/list-manager/ListManagerController.ts b/src/list-manager/ListManagerController.ts index 41510a3..939ea70 100644 --- a/src/list-manager/ListManagerController.ts +++ b/src/list-manager/ListManagerController.ts @@ -64,15 +64,16 @@ export class ListManagerController { const listsContainer = document.getElementById('listsContainer'); listsContainer?.addEventListener('click', (e) => this.handleListClick(e)); - listsContainer?.addEventListener('change', (e) => this.handleListCheckboxChange(e)); listsContainer?.addEventListener('dragstart', (e) => this.handleDragStart(e)); listsContainer?.addEventListener('dragover', (e) => this.handleDragOver(e)); listsContainer?.addEventListener('drop', (e) => this.handleDrop(e)); listsContainer?.addEventListener('dragend', () => this.clearDragState()); const wordList = document.getElementById('wordList'); + wordList?.addEventListener('click', (e) => this.handleWordListClick(e)); wordList?.addEventListener('change', (e) => this.handleWordListChange(e)); wordList?.addEventListener('keydown', (e) => this.handleWordListKeydown(e)); + wordList?.addEventListener('blur', (e) => this.handleWordListBlur(e), true); } private setupStorageSync(): void { @@ -313,29 +314,28 @@ export class ListManagerController { const listItem = target.closest('.list-item') as HTMLElement | null; if (!listItem) return; - if (target.tagName === 'INPUT' || target.tagName === 'BUTTON') return; - const index = Number(listItem.dataset.index); if (Number.isNaN(index)) return; + const mouseEvent = event as MouseEvent; + // Ctrl/Cmd + click for multi-select + if (mouseEvent.ctrlKey || mouseEvent.metaKey) { + if (this.selectedLists.has(index)) { + this.selectedLists.delete(index); + } else { + this.selectedLists.add(index); + } + this.renderLists(); + return; + } + + // Regular click - set as current and clear multi-selection this.currentListIndex = index; + this.selectedLists.clear(); this.selectedWords.clear(); this.render(); } - private handleListCheckboxChange(event: Event): void { - const target = event.target as HTMLInputElement; - if (!target.classList.contains('list-checkbox')) return; - const index = Number(target.dataset.index); - if (Number.isNaN(index)) return; - - if (target.checked) { - this.selectedLists.add(index); - } else { - this.selectedLists.delete(index); - } - } - private handleDragStart(event: DragEvent): void { const target = (event.target as HTMLElement).closest('.list-item') as HTMLElement | null; if (!target) return; @@ -384,23 +384,84 @@ export class ListManagerController { document.querySelectorAll('.list-item.drag-over').forEach(item => item.classList.remove('drag-over')); } - private handleWordListChange(event: Event): void { - const target = event.target as HTMLInputElement; + private handleWordListClick(event: Event): void { + const target = event.target as HTMLElement; const list = this.lists[this.currentListIndex]; if (!list) return; - if (target.classList.contains('word-checkbox') && target.dataset.index != null) { + const editBtn = target.closest('.edit-word-btn') as HTMLElement | null; + if (editBtn) { + event.stopPropagation(); + const index = Number(editBtn.dataset.index); + if (Number.isNaN(index)) return; + this.startEditingWord(index); + return; + } + + // Handle toggle button click + if (target.classList.contains('toggle-btn')) { + event.stopPropagation(); const index = Number(target.dataset.index); - if (target.checked) { - this.selectedWords.add(index); - } else { + if (Number.isNaN(index)) return; + const word = list.words[index]; + if (word) { + word.active = !word.active; + this.save(); + } + return; + } + + // Don't select if clicking on color inputs or edit input + if (target.tagName === 'INPUT') { + if ((target as HTMLInputElement).type === 'color') { + event.stopPropagation(); + return; + } + if (target.classList.contains('word-edit-input')) { + event.stopPropagation(); + return; + } + } + + // Don't select if clicking inside word-actions area (except on the word item itself) + if (target.closest('.word-actions') && !target.classList.contains('word-item')) { + return; + } + + // Handle word item selection + const wordItem = target.closest('.word-item') as HTMLElement | null; + if (!wordItem) return; + + const index = Number(wordItem.dataset.index); + if (Number.isNaN(index)) return; + + const mouseEvent = event as MouseEvent; + // Ctrl/Cmd + click for multi-select + if (mouseEvent.ctrlKey || mouseEvent.metaKey) { + if (this.selectedWords.has(index)) { this.selectedWords.delete(index); + } else { + this.selectedWords.add(index); } this.renderWords(); return; } - const editIndex = Number(target.dataset.bgEdit ?? target.dataset.fgEdit ?? target.dataset.activeEdit ?? -1); + // Regular click - toggle selection + if (this.selectedWords.has(index)) { + this.selectedWords.delete(index); + } else { + this.selectedWords.add(index); + } + this.renderWords(); + } + + private handleWordListChange(event: Event): void { + const target = event.target as HTMLInputElement; + const list = this.lists[this.currentListIndex]; + if (!list) return; + + const editIndex = Number(target.dataset.bgEdit ?? target.dataset.fgEdit ?? -1); if (Number.isNaN(editIndex) || editIndex < 0) return; const word = list.words[editIndex]; @@ -408,14 +469,28 @@ export class ListManagerController { if (target.dataset.bgEdit != null) word.background = target.value; if (target.dataset.fgEdit != null) word.foreground = target.value; - if (target.dataset.activeEdit != null) word.active = target.checked; this.save(); } - private handleWordListKeydown(event: KeyboardEvent): void { - if (event.key !== 'Enter') return; + private startEditingWord(index: number): void { + const wordItem = document.querySelector(`.word-item[data-index="${index}"]`); + if (!wordItem) return; + + const textSpan = wordItem.querySelector('.word-text') as HTMLElement; + const input = wordItem.querySelector('.word-edit-input') as HTMLInputElement; + if (!textSpan || !input) return; + + textSpan.classList.add('editing'); + input.classList.add('active'); + input.focus(); + input.select(); + } + + private handleWordListBlur(event: Event): void { const target = event.target as HTMLInputElement; + if (!target.classList.contains('word-edit-input')) return; + const list = this.lists[this.currentListIndex]; if (!list) return; @@ -425,8 +500,26 @@ export class ListManagerController { const word = list.words[index]; if (!word) return; - word.wordStr = target.value; - this.save(); + const newValue = target.value.trim(); + if (newValue && newValue !== word.wordStr) { + word.wordStr = newValue; + this.save(); + } else { + this.renderWords(); + } + } + + private handleWordListKeydown(event: KeyboardEvent): void { + const target = event.target as HTMLInputElement; + if (!target.classList.contains('word-edit-input')) return; + + if (event.key === 'Enter') { + event.preventDefault(); + target.blur(); + } else if (event.key === 'Escape') { + event.preventDefault(); + this.renderWords(); + } } private getSelectedListIndices(): number[] { @@ -459,10 +552,10 @@ export class ListManagerController { const item = document.createElement('div'); item.className = 'list-item'; if (index === this.currentListIndex) item.classList.add('active'); + if (this.selectedLists.has(index)) item.classList.add('selected'); item.draggable = true; item.dataset.index = index.toString(); item.innerHTML = ` -
${DOMUtils.escapeHtml(list.name)}
${total} words • ${activeCount} active
@@ -519,13 +612,19 @@ export class ListManagerController { wordList.innerHTML = entries.map(entry => { const word = entry.word; const index = entry.index; + const isSelected = this.selectedWords.has(index); return ` -
- - - - - +
+ ${DOMUtils.escapeHtml(word.wordStr)} + +
+ + + + +
`; }).join(''); diff --git a/src/popup/PopupController.ts b/src/popup/PopupController.ts index af8bebb..80ba384 100644 --- a/src/popup/PopupController.ts +++ b/src/popup/PopupController.ts @@ -201,17 +201,79 @@ export class PopupController { } private setupWordListEvents(wordList: HTMLDivElement): void { - wordList.addEventListener('change', (e) => { - const target = e.target as HTMLInputElement; - if (target.type === 'checkbox' && target.dataset.index != null) { - const index = +target.dataset.index; - if (target.checked) { - this.selectedCheckboxes.add(index); - } else { + wordList.addEventListener('click', (e) => { + const target = e.target as HTMLElement; + const list = this.lists[this.currentListIndex]; + if (!list) return; + + // Handle edit button click + const editBtn = target.closest('.icon-btn.edit-word-btn') as HTMLElement | null; + if (editBtn) { + e.stopPropagation(); + const index = Number(editBtn.dataset.index); + if (!Number.isNaN(index)) { + this.startEditingWord(index); + } + return; + } + + // Handle toggle button click + if (target.classList.contains('toggle-btn')) { + e.stopPropagation(); + const index = Number(target.dataset.index); + if (!Number.isNaN(index)) { + const word = list.words[index]; + if (word) { + word.active = !word.active; + this.save(); + } + } + return; + } + + // Don't select if clicking on color inputs or edit input + if (target.tagName === 'INPUT') { + if ((target as HTMLInputElement).type === 'color') { + e.stopPropagation(); + return; + } + if (target.classList.contains('word-edit-input')) { + e.stopPropagation(); + return; + } + } + + // Don't select if clicking inside word-actions area + if (target.closest('.word-actions') && !target.classList.contains('word-item')) { + return; + } + + // Handle word item selection + const wordItem = target.closest('.word-item') as HTMLElement | null; + if (!wordItem) return; + + const index = Number(wordItem.dataset.index); + if (Number.isNaN(index)) return; + + const mouseEvent = e as MouseEvent; + // Ctrl/Cmd + click for multi-select + if (mouseEvent.ctrlKey || mouseEvent.metaKey) { + if (this.selectedCheckboxes.has(index)) { this.selectedCheckboxes.delete(index); + } else { + this.selectedCheckboxes.add(index); } this.renderWords(); + return; } + + // Regular click - toggle selection + if (this.selectedCheckboxes.has(index)) { + this.selectedCheckboxes.delete(index); + } else { + this.selectedCheckboxes.add(index); + } + this.renderWords(); }); wordList.addEventListener('change', (e) => { @@ -227,19 +289,40 @@ export class PopupController { }); wordList.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - const target = e.target as HTMLInputElement; - const index = +(target.dataset.wordEdit ?? -1); - if (index === -1) return; + const target = e.target as HTMLInputElement; + if (!target.classList.contains('word-edit-input')) return; - const word = this.lists[this.currentListIndex].words[index]; - if (target.dataset.wordEdit != null) { - word.wordStr = target.value; - this.save(); - } + if (e.key === 'Enter') { + e.preventDefault(); + target.blur(); + } else if (e.key === 'Escape') { + e.preventDefault(); + this.renderWords(); } }); + wordList.addEventListener('blur', (e) => { + const target = e.target as HTMLInputElement; + if (!target.classList.contains('word-edit-input')) return; + + const list = this.lists[this.currentListIndex]; + if (!list) return; + + const index = Number(target.dataset.wordEdit ?? -1); + if (Number.isNaN(index) || index < 0) return; + + const word = list.words[index]; + if (!word) return; + + const newValue = target.value.trim(); + if (newValue && newValue !== word.wordStr) { + word.wordStr = newValue; + this.save(); + } else { + this.renderWords(); + } + }, true); + let scrolling = false; wordList.addEventListener('scroll', () => { if (scrolling) return; @@ -251,6 +334,20 @@ export class PopupController { }); } + private startEditingWord(index: number): void { + const wordItem = document.querySelector(`.word-item[data-index="${index}"]`); + if (!wordItem) return; + + const textSpan = wordItem.querySelector('.word-text') as HTMLElement; + const input = wordItem.querySelector('.word-edit-input') as HTMLInputElement; + if (!textSpan || !input) return; + + textSpan.classList.add('editing'); + input.classList.add('active'); + input.focus(); + input.select(); + } + private setupWordSelection(): void { document.getElementById('selectAllBtn')?.addEventListener('click', () => { const list = this.lists[this.currentListIndex]; @@ -670,7 +767,7 @@ export class PopupController { } const itemHeight = 32; - const itemSpacing = 2; + const itemSpacing = 4; const totalItemHeight = itemHeight + itemSpacing; const containerHeight = wordList.clientHeight || 250; const scrollTop = wordList.scrollTop; @@ -682,7 +779,7 @@ export class PopupController { const spacer = document.createElement('div'); spacer.style.position = 'relative'; - spacer.style.height = `${filteredWords.length * totalItemHeight}px`; + spacer.style.height = `${filteredWords.length * totalItemHeight - itemSpacing}px`; spacer.style.width = '100%'; for (let i = startIndex; i < endIndex; i++) { @@ -705,21 +802,31 @@ export class PopupController { private createWordItem(word: HighlightWord, realIndex: number, displayIndex: number, itemHeight: number): HTMLDivElement { const container = document.createElement('div'); container.className = 'word-item'; + if (this.selectedCheckboxes.has(realIndex)) { + container.classList.add('selected'); + } if (word.active === false) { container.classList.add('disabled'); } container.style.cssText = ` position: absolute; - top: ${displayIndex * (itemHeight + 2)}px; + top: ${displayIndex * (itemHeight + 4)}px; `; + container.dataset.index = realIndex.toString(); const list = this.lists[this.currentListIndex]; container.innerHTML = ` - - - - + ${DOMUtils.escapeHtml(word.wordStr)} + +
+ + + + +
`; return container;