|
1 | 1 | <script> |
2 | 2 | export let allRows = [] |
3 | 3 | export let filters = {} |
| 4 | + export let clusterLabel = '' |
4 | 5 | export let onReset |
5 | 6 |
|
6 | 7 | $: grandAlliances = ['', ...unique(allRows, 'Grand Alliance')] |
|
13 | 14 | $: types = unique(allRows, 'Type') |
14 | 15 | $: mechanics = ['', ...unique(allRows, 'Mechanic').filter(m => m && m !== 'no effect')] |
15 | 16 |
|
| 17 | + const COSTS = ['[Double]', '[Triple]', '[Quad]'] |
| 18 | +
|
16 | 19 | function unique(rows, key) { |
17 | 20 | return [...new Set(rows.map(r => r[key]).filter(Boolean))].sort() |
18 | 21 | } |
19 | 22 |
|
20 | | - function toggle(type) { |
| 23 | + function toggleType(type) { |
21 | 24 | const sel = new Set(filters.types || []) |
22 | 25 | sel.has(type) ? sel.delete(type) : sel.add(type) |
23 | 26 | filters = { ...filters, types: [...sel] } |
24 | 27 | } |
25 | 28 |
|
26 | | - function allSelected() { |
27 | | - return !filters.types || filters.types.length === 0 || filters.types.length === types.length |
28 | | - } |
29 | | -
|
30 | | - function toggleAll() { |
31 | | - filters = { ...filters, types: [] } |
| 29 | + function toggleCost(cost) { |
| 30 | + const sel = new Set(filters.costs || []) |
| 31 | + sel.has(cost) ? sel.delete(cost) : sel.add(cost) |
| 32 | + filters = { ...filters, costs: [...sel] } |
32 | 33 | } |
33 | 34 | </script> |
34 | 35 |
|
35 | 36 | <aside class="filter-bar"> |
36 | 37 | <div class="filter-group"> |
37 | 38 | <label class="filter-label">Search</label> |
38 | 39 | <input |
39 | | - class="search-input" |
| 40 | + class="text-input" |
40 | 41 | type="search" |
41 | 42 | placeholder="Name or description…" |
42 | 43 | value={filters.search || ''} |
|
80 | 81 | </select> |
81 | 82 | </div> |
82 | 83 |
|
| 84 | + <div class="filter-group"> |
| 85 | + <label class="filter-label">Duplicates</label> |
| 86 | + <div class="check-list"> |
| 87 | + <label class="check-item" class:checked={filters.hasSimilar} title="Show only entries that have documented similar abilities in other warbands"> |
| 88 | + <input |
| 89 | + type="checkbox" |
| 90 | + checked={filters.hasSimilar || false} |
| 91 | + on:change={() => filters = { ...filters, hasSimilar: !filters.hasSimilar, gapsOnly: false }} |
| 92 | + /> |
| 93 | + <span>≈ Has similar</span> |
| 94 | + </label> |
| 95 | + <label class="check-item" class:checked={filters.gapsOnly} title="Show entries that share mechanic and cost with others but have no documented similarities yet — potential undocumented duplicates"> |
| 96 | + <input |
| 97 | + type="checkbox" |
| 98 | + checked={filters.gapsOnly || false} |
| 99 | + on:change={() => filters = { ...filters, gapsOnly: !filters.gapsOnly, hasSimilar: false }} |
| 100 | + /> |
| 101 | + <span>? Gaps only</span> |
| 102 | + </label> |
| 103 | + </div> |
| 104 | + </div> |
| 105 | + |
| 106 | + <div class="filter-group"> |
| 107 | + <label class="filter-label">Cost</label> |
| 108 | + <div class="check-list"> |
| 109 | + {#each COSTS as cost} |
| 110 | + <label class="check-item" class:checked={(filters.costs || []).includes(cost)}> |
| 111 | + <input |
| 112 | + type="checkbox" |
| 113 | + checked={(filters.costs || []).includes(cost)} |
| 114 | + on:change={() => toggleCost(cost)} |
| 115 | + /> |
| 116 | + <span>{cost}</span> |
| 117 | + </label> |
| 118 | + {/each} |
| 119 | + </div> |
| 120 | + </div> |
| 121 | + |
83 | 122 | <div class="filter-group"> |
84 | 123 | <label class="filter-label">Type</label> |
85 | | - <div class="type-chips"> |
86 | | - <button |
87 | | - class="chip {allSelected() ? 'active' : ''}" |
88 | | - on:click={toggleAll} |
89 | | - >All</button> |
| 124 | + <div class="check-list"> |
90 | 125 | {#each types as type} |
91 | | - <button |
92 | | - class="chip {(filters.types || []).includes(type) ? 'active' : ''}" |
93 | | - on:click={() => toggle(type)} |
94 | | - >{type}</button> |
| 126 | + <label class="check-item" class:checked={(filters.types || []).includes(type)}> |
| 127 | + <input |
| 128 | + type="checkbox" |
| 129 | + checked={(filters.types || []).includes(type)} |
| 130 | + on:change={() => toggleType(type)} |
| 131 | + /> |
| 132 | + <span>{type}</span> |
| 133 | + </label> |
95 | 134 | {/each} |
96 | 135 | </div> |
97 | 136 | </div> |
98 | 137 |
|
99 | 138 | {#if filters.similarCluster} |
100 | 139 | <div class="filter-group"> |
101 | 140 | <label class="filter-label">Showing cluster</label> |
102 | | - <button class="cluster-tag" on:click={() => filters = {}}> |
103 | | - {filters._clusterLabel} <span class="dismiss">×</span> |
| 141 | + <button class="cluster-btn" on:click={() => filters = {}}> |
| 142 | + {clusterLabel} <span class="dismiss">×</span> |
104 | 143 | </button> |
105 | 144 | </div> |
106 | 145 | {/if} |
107 | 146 |
|
| 147 | + <details class="help"> |
| 148 | + <summary>How to use</summary> |
| 149 | + <div class="help-body"> |
| 150 | + <p><strong>≈</strong> on a row means similar abilities in other warbands are documented. Expand the row to see them, or click <em>Show all similar</em> to filter to that cluster.</p> |
| 151 | + <p><strong>?</strong> means this entry shares mechanic and cost with others but has no documented similarities yet — a potential duplicate to investigate.</p> |
| 152 | + <p>Use <strong>⊕</strong> to pin up to two rows for side-by-side description comparison.</p> |
| 153 | + </div> |
| 154 | + </details> |
| 155 | + |
108 | 156 | <button class="reset-btn" on:click={onReset}>Reset filters</button> |
109 | 157 | </aside> |
110 | 158 |
|
111 | 159 | <style> |
112 | 160 | .filter-bar { |
113 | 161 | display: flex; |
114 | 162 | flex-direction: column; |
115 | | - gap: 1.25rem; |
116 | | - padding: 1.25rem; |
| 163 | + gap: 1.75rem; |
| 164 | + padding: 1.5rem; |
117 | 165 | background: var(--surface); |
118 | 166 | border-right: 1px solid var(--border); |
119 | | - min-width: 220px; |
120 | | - width: 220px; |
| 167 | + min-width: 240px; |
| 168 | + width: 240px; |
121 | 169 | overflow-y: auto; |
122 | 170 | } |
123 | 171 |
|
|
128 | 176 | letter-spacing: 0.08em; |
129 | 177 | text-transform: uppercase; |
130 | 178 | color: var(--text-muted); |
131 | | - margin-bottom: 0.4rem; |
| 179 | + margin-bottom: 0.6rem; |
132 | 180 | } |
133 | 181 |
|
134 | | - .search-input, |
| 182 | + .text-input, |
135 | 183 | select { |
136 | 184 | width: 100%; |
137 | 185 | padding: 0.45rem 0.6rem; |
|
140 | 188 | background: var(--bg); |
141 | 189 | color: var(--text); |
142 | 190 | font-size: 0.875rem; |
143 | | - box-sizing: border-box; |
144 | 191 | } |
145 | 192 |
|
146 | | - .search-input:focus, |
| 193 | + .text-input:focus, |
147 | 194 | select:focus { |
148 | 195 | outline: 2px solid var(--accent); |
149 | 196 | outline-offset: -1px; |
150 | 197 | } |
151 | 198 |
|
152 | | - .type-chips { |
| 199 | + .check-list { |
153 | 200 | display: flex; |
154 | | - flex-wrap: wrap; |
155 | | - gap: 0.35rem; |
| 201 | + flex-direction: column; |
| 202 | + gap: 0.5rem; |
156 | 203 | } |
157 | 204 |
|
158 | | - .chip { |
159 | | - padding: 0.25rem 0.6rem; |
160 | | - border-radius: 99px; |
161 | | - border: 1px solid var(--border); |
162 | | - background: var(--bg); |
| 205 | + .check-item { |
| 206 | + display: flex; |
| 207 | + align-items: center; |
| 208 | + gap: 0.55rem; |
| 209 | + padding: 0.1rem 0; |
163 | 210 | color: var(--text-muted); |
164 | | - font-size: 0.75rem; |
| 211 | + font-size: 0.875rem; |
165 | 212 | cursor: pointer; |
166 | | - transition: background 0.15s, color 0.15s, border-color 0.15s; |
167 | | - white-space: nowrap; |
| 213 | + transition: color 0.15s; |
| 214 | + user-select: none; |
168 | 215 | } |
169 | 216 |
|
170 | | - .chip.active { |
171 | | - background: var(--accent); |
172 | | - border-color: var(--accent); |
173 | | - color: #fff; |
| 217 | + .check-item:hover { |
| 218 | + color: var(--text); |
174 | 219 | } |
175 | 220 |
|
176 | | - .chip:hover:not(.active) { |
177 | | - border-color: var(--accent); |
178 | | - color: var(--accent); |
| 221 | + .check-item.checked { |
| 222 | + color: var(--text); |
179 | 223 | } |
180 | 224 |
|
181 | | - .reset-btn { |
182 | | - margin-top: auto; |
183 | | - padding: 0.5rem; |
184 | | - background: transparent; |
185 | | - border: 1px solid var(--border); |
186 | | - border-radius: 6px; |
187 | | - color: var(--text-muted); |
188 | | - font-size: 0.8rem; |
| 225 | + .check-item input[type="checkbox"] { |
| 226 | + accent-color: var(--accent); |
| 227 | + width: 14px; |
| 228 | + height: 14px; |
| 229 | + margin: 0; |
189 | 230 | cursor: pointer; |
190 | | - transition: border-color 0.15s, color 0.15s; |
191 | | - } |
192 | | -
|
193 | | - .reset-btn:hover { |
194 | | - border-color: var(--accent); |
195 | | - color: var(--accent); |
| 231 | + flex-shrink: 0; |
196 | 232 | } |
197 | 233 |
|
198 | | - .cluster-tag { |
| 234 | + .cluster-btn { |
199 | 235 | display: flex; |
200 | 236 | align-items: center; |
201 | 237 | justify-content: space-between; |
202 | 238 | width: 100%; |
203 | 239 | padding: 0.4rem 0.6rem; |
204 | | - background: var(--accent); |
205 | | - border: none; |
| 240 | + background: var(--bg); |
| 241 | + border: 1px solid var(--accent); |
206 | 242 | border-radius: 6px; |
207 | | - color: #fff; |
208 | | - font-size: 0.8rem; |
| 243 | + color: var(--accent-hi); |
| 244 | + font-size: 0.875rem; |
209 | 245 | cursor: pointer; |
210 | | - text-align: left; |
211 | 246 | gap: 0.5rem; |
| 247 | + transition: background 0.15s; |
212 | 248 | } |
213 | 249 |
|
214 | | - .cluster-tag:hover { |
215 | | - opacity: 0.85; |
| 250 | + .cluster-btn:hover { |
| 251 | + background: var(--surface-2); |
216 | 252 | } |
217 | 253 |
|
218 | 254 | .dismiss { |
219 | 255 | font-size: 1rem; |
220 | 256 | line-height: 1; |
221 | 257 | opacity: 0.8; |
222 | 258 | } |
| 259 | +
|
| 260 | + .help { |
| 261 | + margin-top: auto; |
| 262 | + border: 1px solid var(--border); |
| 263 | + border-radius: 6px; |
| 264 | + padding: 0 0.6rem; |
| 265 | + } |
| 266 | +
|
| 267 | + .help summary { |
| 268 | + padding: 0.45rem 0; |
| 269 | + font-size: 0.75rem; |
| 270 | + font-weight: 700; |
| 271 | + letter-spacing: 0.06em; |
| 272 | + text-transform: uppercase; |
| 273 | + color: var(--text-muted); |
| 274 | + cursor: pointer; |
| 275 | + user-select: none; |
| 276 | + list-style: none; |
| 277 | + } |
| 278 | +
|
| 279 | + .help summary::before { |
| 280 | + content: '▸ '; |
| 281 | + font-size: 0.65rem; |
| 282 | + } |
| 283 | +
|
| 284 | + .help[open] summary::before { |
| 285 | + content: '▾ '; |
| 286 | + } |
| 287 | +
|
| 288 | + .help-body { |
| 289 | + padding: 0 0 0.6rem; |
| 290 | + display: flex; |
| 291 | + flex-direction: column; |
| 292 | + gap: 0.5rem; |
| 293 | + } |
| 294 | +
|
| 295 | + .help-body p { |
| 296 | + margin: 0; |
| 297 | + font-size: 0.82rem; |
| 298 | + line-height: 1.5; |
| 299 | + color: var(--text-muted); |
| 300 | + } |
| 301 | +
|
| 302 | + .help-body strong { |
| 303 | + color: var(--accent-hi); |
| 304 | + } |
| 305 | +
|
| 306 | + .reset-btn { |
| 307 | + padding: 0.5rem 0.6rem; |
| 308 | + background: transparent; |
| 309 | + border: 1px solid var(--border); |
| 310 | + border-radius: 6px; |
| 311 | + color: var(--text-muted); |
| 312 | + font-size: 0.875rem; |
| 313 | + cursor: pointer; |
| 314 | + transition: border-color 0.15s, color 0.15s; |
| 315 | + } |
| 316 | +
|
| 317 | + .reset-btn:hover { |
| 318 | + border-color: var(--accent); |
| 319 | + color: var(--text); |
| 320 | + } |
223 | 321 | </style> |
0 commit comments