mirror of
https://github.com/shadoll/sLogos.git
synced 2026-02-04 11:03:24 +00:00
feat: enhance tag filtering with search functionality and update compact mode toggle
This commit is contained in:
@@ -22,6 +22,18 @@
|
|||||||
export let setCompactMode = () => {};
|
export let setCompactMode = () => {};
|
||||||
|
|
||||||
let searchInput; // Reference to the search input element
|
let searchInput; // Reference to the search input element
|
||||||
|
let tagSearchQuery = ""; // Search query for filtering tags
|
||||||
|
|
||||||
|
// Filter available tags based on search query
|
||||||
|
$: filteredAvailableTags = allTags.filter((t) =>
|
||||||
|
!selectedTags.includes(t.text) &&
|
||||||
|
t.text.toLowerCase().includes(tagSearchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter all tags based on search query (both selected and unselected)
|
||||||
|
$: filteredAllTags = allTags.filter((t) =>
|
||||||
|
t.text.toLowerCase().includes(tagSearchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Add global keydown listener for the / hotkey
|
// Add global keydown listener for the / hotkey
|
||||||
@@ -214,13 +226,26 @@
|
|||||||
{#if tagDropdownOpen}
|
{#if tagDropdownOpen}
|
||||||
<div class="filter-dropdown-panel" on:click|stopPropagation>
|
<div class="filter-dropdown-panel" on:click|stopPropagation>
|
||||||
<div class="filter-options">
|
<div class="filter-options">
|
||||||
<div class="filter-option">
|
<button
|
||||||
<label>
|
class="filter-option-item"
|
||||||
<input
|
class:selected={compactMode}
|
||||||
type="checkbox"
|
on:click={() => setCompactMode(!compactMode)}
|
||||||
checked={compactMode}
|
aria-label={compactMode ? "Disable group by brand" : "Enable group by brand"}
|
||||||
on:change={(e) => setCompactMode(e.target.checked)}
|
>
|
||||||
/>
|
<span class="option-icon">
|
||||||
|
<span class="permanent-icon">
|
||||||
|
{#if compactMode}
|
||||||
|
✔️
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span class="hover-icon">
|
||||||
|
{#if compactMode}
|
||||||
|
❌
|
||||||
|
{:else}
|
||||||
|
✔️
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
<span class="option-label">
|
<span class="option-label">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M10 11L3 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
<path d="M10 11L3 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
@@ -228,28 +253,68 @@
|
|||||||
<path d="M14 13.5L16.1 16L20 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M14 13.5L16.1 16L20 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M3 6L13.5 6M20 6L17.75 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
<path d="M3 6L13.5 6M20 6L17.75 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
Compact mode
|
Group by brand
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if allTags.filter((t) => !selectedTags.includes(t.text)).length > 0}
|
{#if filteredAvailableTags.length > 0 || tagSearchQuery}
|
||||||
<div class="filter-separator"></div>
|
<div class="filter-separator"></div>
|
||||||
<div class="filter-tags-section">
|
<div class="filter-tags-section">
|
||||||
<div class="filter-section-title">Tags</div>
|
<div class="filter-section-title">Tags</div>
|
||||||
<div class="filter-tags-grid">
|
|
||||||
{#each allTags.filter((t) => !selectedTags.includes(t.text)) as tagObj}
|
<div class="tags-search-bar">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search tags..."
|
||||||
|
bind:value={tagSearchQuery}
|
||||||
|
class="tags-search-input"
|
||||||
|
/>
|
||||||
|
{#if tagSearchQuery}
|
||||||
<button
|
<button
|
||||||
class="filter-tag"
|
class="tags-search-clear"
|
||||||
style={tagObj.color ? `background: ${tagObj.color}; color: #fff;` : ""}
|
on:click|stopPropagation={() => tagSearchQuery = ""}
|
||||||
on:click={() => addTag(tagObj.text)}
|
aria-label="Clear tag search"
|
||||||
aria-label={`Add tag: ${tagObj.text}`}
|
|
||||||
>
|
>
|
||||||
{tagObj.text}
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if filteredAllTags.length > 0}
|
||||||
|
<div class="filter-tags-list">
|
||||||
|
{#each filteredAllTags as tagObj}
|
||||||
|
{@const isSelected = selectedTags.includes(tagObj.text)}
|
||||||
|
<button
|
||||||
|
class="filter-tag-item"
|
||||||
|
class:selected={isSelected}
|
||||||
|
on:click={() => isSelected ? removeTag(tagObj.text) : addTag(tagObj.text)}
|
||||||
|
aria-label={isSelected ? `Remove tag: ${tagObj.text}` : `Add tag: ${tagObj.text}`}
|
||||||
|
> <span class="tag-icon">
|
||||||
|
<span class="permanent-icon">
|
||||||
|
{#if isSelected}
|
||||||
|
✔️
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span class="hover-icon">
|
||||||
|
{#if isSelected}
|
||||||
|
❌
|
||||||
|
{:else}
|
||||||
|
✔️
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="tag-text" style={tagObj.color ? `color: ${tagObj.color};` : ""}>{tagObj.text}</span>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="no-tags">
|
||||||
|
{tagSearchQuery ? "No tags match your search" : "No available tags"}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -268,15 +333,20 @@
|
|||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if compactMode}
|
{#if compactMode}
|
||||||
<span class="compact-indicator">
|
<button
|
||||||
|
class="compact-indicator"
|
||||||
|
on:click={() => setCompactMode(false)}
|
||||||
|
aria-label="Disable group by brand"
|
||||||
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M10 11L3 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
<path d="M10 11L3 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
<path d="M10 16H3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
<path d="M10 16H3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
<path d="M14 13.5L16.1 16L20 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M14 13.5L16.1 16L20 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M3 6L13.5 6M20 6L17.75 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
<path d="M3 6L13.5 6M20 6L17.75 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
Compact
|
Group by brand
|
||||||
</span>
|
<span class="close">×</span>
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="view-toggle">
|
<div class="view-toggle">
|
||||||
@@ -357,8 +427,7 @@
|
|||||||
x="4"
|
x="4"
|
||||||
y="13"
|
y="13"
|
||||||
width="12"
|
width="12"
|
||||||
height="2"
|
height="2" fill="currentColor"
|
||||||
fill="currentColor"
|
|
||||||
/></svg
|
/></svg
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
@@ -472,7 +541,7 @@
|
|||||||
background: var(--color-accent, #4f8cff);
|
background: var(--color-accent, #4f8cff);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
padding: 0.2em 0.8em 0.2em 0.8em;
|
padding: 0.2em 0.8em 0.2em 0.8em;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -501,7 +570,8 @@
|
|||||||
.compact-indicator {
|
.compact-indicator {
|
||||||
background: var(--color-border);
|
background: var(--color-border);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
border-radius: 12px;
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
padding: 0.2em 0.8em;
|
padding: 0.2em 0.8em;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -509,6 +579,27 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4em;
|
gap: 0.4em;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, color 0.2s, opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-indicator:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: var(--color-text);
|
||||||
|
color: var(--color-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-indicator .close {
|
||||||
|
margin-left: 0.4em;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-indicator .close:hover {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-dropdown {
|
.filter-dropdown {
|
||||||
@@ -564,7 +655,7 @@
|
|||||||
|
|
||||||
.filter-dropdown-panel {
|
.filter-dropdown-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
left: 0;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
@@ -591,25 +682,35 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-option label {
|
.filter-option-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
position: relative;
|
||||||
|
padding: 0.4rem 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-option input[type="checkbox"] {
|
.filter-option-label input[type="checkbox"] {
|
||||||
width: 16px;
|
opacity: 0;
|
||||||
height: 16px;
|
position: absolute;
|
||||||
margin: 0;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-label {
|
.filter-option-label .checkmark {
|
||||||
display: flex;
|
opacity: 0;
|
||||||
align-items: center;
|
transition: opacity 0.2s;
|
||||||
gap: 0.4rem;
|
}
|
||||||
|
|
||||||
|
.filter-option-label input[type="checkbox"]:checked + .checkmark {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-text {
|
||||||
|
margin-left: 0.5rem;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,31 +733,214 @@
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-tags-grid {
|
.tags-search-bar {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 2.5rem 0.5rem 0.5rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
background: var(--color-card);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-search-clear {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.5rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #888;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
align-items: center;
|
||||||
gap: 0.3rem;
|
justify-content: center;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-search-clear:hover {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tags-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-tag {
|
.filter-tag-item {
|
||||||
background: var(--color-accent, #4f8cff);
|
background: none;
|
||||||
color: #fff;
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 12px;
|
padding: 0.4rem 0.5rem;
|
||||||
padding: 0.2em 0.8em;
|
display: flex;
|
||||||
font-size: 0.85em;
|
align-items: center;
|
||||||
font-weight: 500;
|
gap: 0.5rem;
|
||||||
letter-spacing: 0.02em;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0.85;
|
border-radius: 4px;
|
||||||
transition: background 0.2s, color 0.2s, opacity 0.2s;
|
transition: background 0.2s;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-tag:hover {
|
.filter-tag-item:hover {
|
||||||
|
background: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tag-item.selected {
|
||||||
|
background: none;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tag-item.selected:hover {
|
||||||
|
background: var(--color-border);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(-1px);
|
}
|
||||||
|
|
||||||
|
.filter-tag-item.selected .tag-checkbox {
|
||||||
|
border-color: var(--color-border);
|
||||||
|
background: var(--color-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tag-item .tag-icon .hover-icon {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tag-item:hover .tag-icon .hover-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tag-item:hover .tag-icon .permanent-icon {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option-item:hover .option-icon .hover-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option-item:hover .option-icon .permanent-icon {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-text {
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-tags {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.85em;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-container input[type="checkbox"] {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 0.9em;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option-item {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.4rem 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
text-align: left;
|
||||||
|
color: var(--color-text);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option-item:hover {
|
||||||
|
background: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option-item .option-icon .hover-icon {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option-item:hover .option-icon .hover-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option-item:hover .option-icon > *:not(.hover-icon) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permanent-icon {
|
||||||
|
position: absolute;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-icon {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user