tags.md (6750B)
1 --- 2 source: 'docs/tags.md' 3 title: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline; margin-bottom:-2px; margin-right:6px;"><path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="currentColor"/></svg> Tags' 4 --- 5 6 <script setup> 7 import { ref, computed } from 'vue' 8 import { data as tagsData } from './.vitepress/tags.data.ts' 9 10 const tagNames = Object.keys(tagsData).sort() 11 12 // Store an array of active tags instead of a single string 13 const activeFilters = ref([]) 14 15 // Compute pages that contain ALL selected tags (AND logic) 16 const filteredPages = computed(() => { 17 if (activeFilters.value.length === 0) return [] 18 19 // Start with the pages of the first selected tag 20 let commonPages = tagsData[activeFilters.value[0]] 21 22 // Intersect with pages of the other selected tags 23 for (let i = 1; i < activeFilters.value.length; i++) { 24 const nextTagPages = tagsData[activeFilters.value[i]] 25 commonPages = commonPages.filter(page => 26 nextTagPages.some(p => p.url === page.url) 27 ) 28 } 29 30 return commonPages 31 }) 32 33 function toggleFilter(tag) { 34 const index = activeFilters.value.indexOf(tag) 35 if (index > -1) { 36 // Tag is currently active, remove it 37 activeFilters.value.splice(index, 1) 38 } else { 39 // Tag is not active, add it 40 activeFilters.value.push(tag) 41 } 42 } 43 44 function humanize(tag) { 45 return tag.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) 46 } 47 </script> 48 49 # Tags 50 51 Browse pages grouped by tag. Click tags below to filter results, or click a tag chip anywhere on the site to jump straight here. 52 53 <div v-if="!tagNames.length" class="bsc-tags-empty"> 54 No tags found yet. 55 </div> 56 57 <div v-else class="bsc-filter-bar"> 58 <button 59 v-for="tag in tagNames" 60 :key="tag" 61 class="bsc-filter-chip" 62 :class="{ 'bsc-filter-chip-active': activeFilters.includes(tag) }" 63 @click="toggleFilter(tag)" 64 >{{ humanize(tag) }}</button> 65 </div> 66 67 <p v-if="activeFilters.length" class="bsc-filter-status"> 68 Showing pages with <strong>all</strong> selected tags: 69 <strong style="color: var(--vp-c-brand-1);">{{ activeFilters.map(humanize).join(' + ') }}</strong> — 70 <a href="#" class="bsc-filter-clear" @click.prevent="activeFilters = []">clear filters</a> 71 </p> 72 73 <!-- If filters are active, show the combined intersection of pages --> 74 <div v-if="activeFilters.length" class="bsc-tag-section"> 75 <h2 class="bsc-tag-heading"> 76 Filtered Results 77 <span class="bsc-tag-count">{{ filteredPages.length }}</span> 78 </h2> 79 80 <ul v-if="filteredPages.length" class="bsc-tag-pages"> 81 <li v-for="page in filteredPages" :key="page.url"> 82 <a :href="page.url">{{ page.title }}</a> 83 </li> 84 </ul> 85 <div v-else class="bsc-tags-empty" style="margin-top: 16px;"> 86 No pages match all selected tags. 87 </div> 88 </div> 89 90 <!-- If no filters are active, show all standard tag sections --> 91 <template v-else> 92 <div v-for="tag in tagNames" :key="tag" :id="tag" class="bsc-tag-section"> 93 <h2 class="bsc-tag-heading"> 94 <a class="bsc-tag" :href="`#${tag}`">{{ humanize(tag) }}</a> 95 <span class="bsc-tag-count">{{ tagsData[tag].length }}</span> 96 </h2> 97 <ul class="bsc-tag-pages"> 98 <li v-for="page in tagsData[tag]" :key="page.url"> 99 <a :href="page.url">{{ page.title }}</a> 100 </li> 101 </ul> 102 </div> 103 </template> 104 105 <style scoped> 106 .bsc-tags-empty { 107 color: var(--vp-c-text-2); 108 font-size: 14px; 109 } 110 111 .bsc-filter-bar { 112 display: flex; 113 flex-wrap: wrap; 114 gap: 8px; 115 margin: 16px 0 8px; 116 } 117 118 .bsc-filter-chip { 119 font: inherit; 120 font-size: 13px; 121 font-weight: 500; 122 padding: 5px 12px; 123 color: var(--vp-c-text-2); 124 background: var(--vp-c-bg-soft); 125 border: 1px solid var(--vp-c-divider); 126 border-radius: 16px; 127 cursor: pointer; 128 -webkit-tap-highlight-color: transparent; 129 } 130 131 /* Only change to blue text on hover if the chip is NOT active */ 132 .bsc-filter-chip:not(.bsc-filter-chip-active):hover { 133 color: var(--vp-c-brand-1); 134 border-color: var(--vp-c-brand-1); 135 } 136 137 /* Lock the active state to white text so it never disappears on hover */ 138 .bsc-filter-chip-active { 139 color: #ffffff !important; 140 background: var(--vp-c-brand-1) !important; 141 border-color: var(--vp-c-brand-1) !important; 142 } 143 144 .bsc-filter-status { 145 font-size: 13px; 146 color: var(--vp-c-text-2); 147 margin: 0 0 12px; 148 } 149 150 .bsc-filter-clear { 151 color: var(--vp-c-text-3); 152 text-decoration: underline; 153 cursor: pointer; 154 transition: color 0.2s ease; 155 } 156 157 .bsc-filter-clear:hover { 158 color: var(--vp-c-brand-1); 159 } 160 161 .bsc-tag-section { 162 margin: 28px 0; 163 padding-top: 8px; 164 border-top: 1px solid var(--vp-c-divider); 165 } 166 167 .bsc-tag-heading { 168 display: flex; 169 align-items: center; 170 gap: 10px; 171 margin: 16px 0 10px; 172 border: none; 173 padding: 0; 174 } 175 176 .bsc-tag-count { 177 font-size: 12px; 178 color: var(--vp-c-text-3); 179 font-weight: 400; 180 } 181 182 .bsc-tag { 183 position: relative; 184 display: inline-flex; 185 align-items: center; 186 height: 28px; 187 padding: 0 14px 0 10px; 188 font-size: 14px; 189 font-weight: 500; 190 line-height: 1; 191 text-decoration: none; 192 white-space: nowrap; 193 194 color: var(--vp-c-brand-1); 195 background: var(--vp-c-bg-soft); 196 197 border: 1px solid var(--vp-c-divider); 198 border-left: none; 199 border-radius: 0 5px 5px 0; 200 201 margin-left: 14px; 202 } 203 204 /* The Pointed Left Angle */ 205 .bsc-tag::after { 206 content: ''; 207 position: absolute; 208 left: -10px; 209 top: 50%; 210 margin-top: -10px; 211 width: 20px; 212 height: 20px; 213 214 background: inherit; 215 216 border-left: 1px solid var(--vp-c-divider); 217 border-bottom: 1px solid var(--vp-c-divider); 218 border-radius: 0 0 0 3px; 219 220 transform: rotate(45deg); 221 z-index: -1; 222 } 223 224 /* The Hole Punch Dot */ 225 .bsc-tag::before { 226 content: ''; 227 position: absolute; 228 left: -3px; 229 top: 50%; 230 margin-top: -2px; 231 width: 4px; 232 height: 4px; 233 border-radius: 50%; 234 background: var(--vp-c-brand-1); 235 z-index: 1; 236 } 237 238 /* Hover States */ 239 .bsc-tag:hover { 240 color: #fff; 241 background: var(--vp-c-brand-1); 242 border-color: var(--vp-c-brand-1); 243 } 244 245 .bsc-tag:hover::after { 246 border-color: var(--vp-c-brand-1); 247 } 248 249 .bsc-tag:hover::before { 250 background: #fff; 251 } 252 253 .bsc-tag-pages { 254 margin: 0; 255 padding-left: 4px; 256 list-style: none; 257 display: flex; 258 flex-wrap: wrap; 259 align-items: center; 260 row-gap: 10px; 261 column-gap: 20px; 262 } 263 264 .bsc-tag-pages li { 265 margin: 0 !important; 266 padding: 0; 267 line-height: 1.6; 268 } 269 270 .bsc-tag-pages a { 271 display: inline-block; 272 color: var(--vp-c-brand-1); 273 text-decoration: none; 274 font-size: 14px; 275 line-height: 1.6; 276 } 277 278 .bsc-tag-pages a:hover { 279 text-decoration: underline; 280 } 281 </style>