Various AT Protocol integrations with obsidian

better styling of filters

+102 -84
+76 -72
src/views/bookmarks.ts
··· 229 229 230 230 private renderFilters(container: HTMLElement) { 231 231 const filtersEl = container.createEl("div", { cls: "atmosphere-filters" }); 232 + const toolbarRow = filtersEl.createEl("div", { cls: "atmosphere-filter-toolbar" }); 232 233 233 234 const collectionSources = (["semble", "margin"] as SourceName[]).filter(s => this.activeSources.has(s)); 234 235 if (collectionSources.length > 0) { 235 - void this.renderCollectionsFilter(filtersEl, collectionSources); 236 + this.renderCollectionsFilterBtn(toolbarRow, collectionSources); 236 237 } 237 238 238 239 const tagSources = (["margin", "bookmark"] as SourceName[]).filter(s => this.activeSources.has(s)); 239 240 if (tagSources.length > 0) { 240 - void this.renderTagsFilter(filtersEl, tagSources); 241 + this.renderTagsFilterBtn(toolbarRow, tagSources); 241 242 } 242 243 243 - const searchInput = new SearchComponent(filtersEl) 244 - searchInput.setValue(this.searchQuery) 244 + const searchInput = new SearchComponent(toolbarRow); 245 + searchInput.setValue(this.searchQuery); 245 246 searchInput.setPlaceholder("Search bookmarks..."); 246 247 searchInput.onChange(() => { 247 248 this.searchQuery = searchInput.getValue(); 248 249 this.renderGrid(this.cachedItems); 249 - }) 250 - } 250 + }); 251 251 252 - private async fetchAllCollections(sources: SourceName[]): Promise<(SourceFilter & { source: SourceName })[]> { 253 - const results = await Promise.all( 254 - sources.map(async s => { 255 - const items = await (this.sources.get(s)?.getAvailableCollections?.() ?? Promise.resolve([])); 256 - return items.map(c => ({ ...c, source: s })); 257 - }) 258 - ); 259 - const seen = new Set<string>(); 260 - return results.flat().filter(c => !seen.has(c.value) && Boolean(seen.add(c.value))); 261 - } 262 - 263 - private async fetchAllTags(sources: SourceName[]): Promise<(SourceFilter & { source: SourceName })[]> { 264 - const results = await Promise.all( 265 - sources.map(async s => { 266 - const items = await (this.sources.get(s)?.getAvilableTags?.() ?? Promise.resolve([])); 267 - return items.map(t => ({ ...t, source: s })); 268 - }) 269 - ); 270 - const seen = new Set<string>(); 271 - return results.flat().filter(t => !seen.has(t.value) && Boolean(seen.add(t.value))); 252 + if (this.selectedCollections.size > 0 || this.selectedTags.size > 0) { 253 + const chipsRow = filtersEl.createEl("div", { cls: "atmosphere-active-chips-row" }); 254 + if (this.selectedCollections.size > 0 && collectionSources.length > 0) { 255 + void this.renderActiveCollectionChips(chipsRow, collectionSources); 256 + } 257 + if (this.selectedTags.size > 0 && tagSources.length > 0) { 258 + void this.renderActiveTagChips(chipsRow, tagSources); 259 + } 260 + } 272 261 } 273 262 274 - private async renderCollectionsFilter(container: HTMLElement, collectionSources: SourceName[]) { 275 - const section = container.createEl("div", { cls: "atmosphere-filter-section" }); 263 + private renderCollectionsFilterBtn(toolbar: HTMLElement, collectionSources: SourceName[]) { 264 + const group = toolbar.createEl("div", { cls: "atmosphere-filter-btn-group" }); 276 265 277 - const titleRow = section.createEl("div", { cls: "atmosphere-filter-title-row" }); 278 - 279 - const pickerBtn = titleRow.createEl("button", { 266 + const pickerBtn = group.createEl("button", { 280 267 cls: "atmosphere-filter-picker-btn", 281 268 attr: { "aria-label": "Filter collections" }, 282 269 }); 283 270 setIcon(pickerBtn, "folder"); 284 271 pickerBtn.createEl("span", { text: "Collections", cls: "atmosphere-filter-title" }); 272 + pickerBtn.addEventListener("click", (e) => void this.showCollectionsMenu(e, collectionSources)); 285 273 286 - const btn = titleRow.createEl("button", { 274 + const btn = group.createEl("button", { 287 275 cls: "atmosphere-filter-create-btn", 288 276 attr: { "aria-label": "New collection" }, 289 277 }); ··· 296 284 () => void this.refresh() 297 285 ).open(); 298 286 }); 299 - pickerBtn.addEventListener("click", (e) => void this.showCollectionsMenu(e, collectionSources)); 300 - 301 - if (this.selectedCollections.size > 0) { 302 - const chipsRow = section.createEl("div", { cls: "atmosphere-filter-active-chips" }); 303 - const collections = await this.fetchAllCollections(collectionSources); 304 - for (const c of collections) { 305 - if (!this.selectedCollections.has(c.value)) continue; 306 - const chip = chipsRow.createEl("span", { cls: "atmosphere-chip atmosphere-chip-active atmosphere-chip-removable" }); 307 - setIcon(chip, sourceIconId(c.source)); 308 - chip.createEl("span", { text: c.label ?? c.value }); 309 - const x = chip.createEl("button", { cls: "atmosphere-chip-remove-btn", attr: { "aria-label": `Remove ${c.label ?? c.value}` } }); 310 - setIcon(x, "x"); 311 - x.addEventListener("click", () => { 312 - this.selectedCollections.delete(c.value); 313 - void this.render(); 314 - }); 315 - } 316 - } 317 287 } 318 288 319 - private async renderTagsFilter(container: HTMLElement, tagSources: SourceName[]) { 320 - const section = container.createEl("div", { cls: "atmosphere-filter-section" }); 321 - 322 - const titleRow = section.createEl("div", { cls: "atmosphere-filter-title-row" }); 289 + private renderTagsFilterBtn(toolbar: HTMLElement, tagSources: SourceName[]) { 290 + const group = toolbar.createEl("div", { cls: "atmosphere-filter-btn-group" }); 323 291 324 - const pickerBtn = titleRow.createEl("button", { 292 + const pickerBtn = group.createEl("button", { 325 293 cls: "atmosphere-filter-picker-btn", 326 294 attr: { "aria-label": "Filter tags" }, 327 295 }); 328 296 setIcon(pickerBtn, "tag"); 329 297 pickerBtn.createEl("span", { text: "Tags", cls: "atmosphere-filter-title" }); 298 + pickerBtn.addEventListener("click", (e) => void this.showTagsMenu(e, tagSources)); 330 299 331 300 if (tagSources.includes("bookmark")) { 332 - const btn = titleRow.createEl("button", { 301 + const btn = group.createEl("button", { 333 302 cls: "atmosphere-filter-create-btn", 334 303 attr: { "aria-label": "New tag" }, 335 304 }); 336 305 setIcon(btn, "plus"); 337 306 btn.addEventListener("click", (e) => { e.stopPropagation(); new CreateTagModal(this.plugin, () => void this.refresh()).open(); }); 338 307 } 339 - pickerBtn.addEventListener("click", (e) => void this.showTagsMenu(e, tagSources)); 308 + } 340 309 341 - if (this.selectedTags.size > 0) { 342 - const chipsRow = section.createEl("div", { cls: "atmosphere-filter-active-chips" }); 343 - const tags = await this.fetchAllTags(tagSources); 344 - for (const t of tags) { 345 - if (!this.selectedTags.has(t.value)) continue; 346 - const chip = chipsRow.createEl("span", { cls: "atmosphere-chip atmosphere-chip-active atmosphere-chip-removable" }); 347 - setIcon(chip, sourceIconId(t.source)) 310 + private async fetchAllCollections(sources: SourceName[]): Promise<(SourceFilter & { source: SourceName })[]> { 311 + const results = await Promise.all( 312 + sources.map(async s => { 313 + const items = await (this.sources.get(s)?.getAvailableCollections?.() ?? Promise.resolve([])); 314 + return items.map(c => ({ ...c, source: s })); 315 + }) 316 + ); 317 + const seen = new Set<string>(); 318 + return results.flat().filter(c => !seen.has(c.value) && Boolean(seen.add(c.value))); 319 + } 348 320 349 - chip.createEl("span", { text: t.label ?? t.value }); 350 - const x = chip.createEl("button", { cls: "atmosphere-chip-remove-btn", attr: { "aria-label": `Remove ${t.label ?? t.value}` } }); 351 - setIcon(x, "x"); 352 - x.addEventListener("click", () => { 353 - this.selectedTags.delete(t.value); 354 - void this.render(); 355 - }); 356 - } 321 + private async fetchAllTags(sources: SourceName[]): Promise<(SourceFilter & { source: SourceName })[]> { 322 + const results = await Promise.all( 323 + sources.map(async s => { 324 + const items = await (this.sources.get(s)?.getAvilableTags?.() ?? Promise.resolve([])); 325 + return items.map(t => ({ ...t, source: s })); 326 + }) 327 + ); 328 + const seen = new Set<string>(); 329 + return results.flat().filter(t => !seen.has(t.value) && Boolean(seen.add(t.value))); 330 + } 331 + 332 + private async renderActiveCollectionChips(chipsRow: HTMLElement, collectionSources: SourceName[]) { 333 + const collections = await this.fetchAllCollections(collectionSources); 334 + for (const c of collections) { 335 + if (!this.selectedCollections.has(c.value)) continue; 336 + const chip = chipsRow.createEl("span", { cls: "atmosphere-chip atmosphere-chip-active atmosphere-chip-removable" }); 337 + setIcon(chip, sourceIconId(c.source)); 338 + chip.createEl("span", { text: c.label ?? c.value }); 339 + const x = chip.createEl("button", { cls: "atmosphere-chip-remove-btn", attr: { "aria-label": `Remove ${c.label ?? c.value}` } }); 340 + setIcon(x, "x"); 341 + x.addEventListener("click", () => { 342 + this.selectedCollections.delete(c.value); 343 + void this.render(); 344 + }); 345 + } 346 + } 347 + 348 + private async renderActiveTagChips(chipsRow: HTMLElement, tagSources: SourceName[]) { 349 + const tags = await this.fetchAllTags(tagSources); 350 + for (const t of tags) { 351 + if (!this.selectedTags.has(t.value)) continue; 352 + const chip = chipsRow.createEl("span", { cls: "atmosphere-chip atmosphere-chip-active atmosphere-chip-removable" }); 353 + setIcon(chip, sourceIconId(t.source)); 354 + chip.createEl("span", { text: "#" + (t.label ?? t.value) }); 355 + const x = chip.createEl("button", { cls: "atmosphere-chip-remove-btn", attr: { "aria-label": `Remove ${t.label ?? t.value}` } }); 356 + setIcon(x, "x"); 357 + x.addEventListener("click", () => { 358 + this.selectedTags.delete(t.value); 359 + void this.render(); 360 + }); 357 361 } 358 362 } 359 363
+26 -12
styles.css
··· 103 103 .atmosphere-filters { 104 104 display: flex; 105 105 flex-direction: column; 106 - gap: 16px; 106 + gap: 8px; 107 107 margin-bottom: 16px; 108 + container-type: inline-size; 108 109 } 109 110 110 - .atmosphere-filter-section { 111 + .atmosphere-filter-toolbar { 111 112 display: flex; 112 - flex-direction: column; 113 - gap: 6px; 113 + align-items: center; 114 + gap: 8px; 115 + flex-wrap: wrap; 116 + } 117 + 118 + .atmosphere-filter-btn-group { 119 + display: inline-flex; 120 + align-items: center; 121 + gap: 2px; 114 122 } 115 123 116 - .atmosphere-filter-title-row { 124 + .atmosphere-active-chips-row { 117 125 display: flex; 118 - align-items: center; 126 + flex-wrap: wrap; 119 127 gap: 6px; 120 - margin-bottom: 2px; 121 128 } 122 129 123 130 ··· 1224 1231 gap: 12px; 1225 1232 } 1226 1233 1227 - .atmosphere-filter-section { 1228 - margin-bottom: 12px; 1234 + .atmosphere-filter-toolbar { 1235 + flex-wrap: wrap; 1236 + gap: 6px; 1229 1237 } 1230 1238 } 1231 1239 ··· 1590 1598 height: 14px; 1591 1599 } 1592 1600 1593 - .atmosphere-filters .search-input-container { 1594 - width: 180px; 1595 - margin: 0 auto; 1601 + .atmosphere-filter-toolbar .search-input-container { 1602 + width: 160px; 1603 + margin-left: auto; 1604 + } 1605 + 1606 + @container (max-width: 380px) { 1607 + .atmosphere-filter-toolbar .search-input-container { 1608 + margin: 0 auto; 1609 + } 1596 1610 }