SenY commited on
Commit
ba48db7
·
1 Parent(s): 0f4f7f9

fontのフィルタリング改善

Browse files
Files changed (3) hide show
  1. index.html +5 -9
  2. index.js +218 -74
  3. styles.css +56 -0
index.html CHANGED
@@ -30,18 +30,14 @@
30
  <label for="googleFontInput" class="form-label">フォント</label>
31
  <div class="row g-2 mb-2">
32
  <div class="col">
33
- <div class="btn-group w-100" role="group" id="fontTagFilter">
34
- <input type="radio" class="btn-check" name="fontFilter" id="filterAll" value="all" checked>
35
- <label class="btn btn-outline-primary" for="filterAll">すべて表示</label>
36
-
37
- <input type="radio" class="btn-check" name="fontFilter" id="filterJapanese" value="japanese">
38
- <label class="btn btn-outline-primary" for="filterJapanese">日本語フォント</label>
39
-
40
- <input type="radio" class="btn-check" name="fontFilter" id="filterKanji" value="kanji">
41
- <label class="btn btn-outline-primary" for="filterKanji">漢字対応</label>
42
  </div>
43
  </div>
44
  </div>
 
 
 
45
  <select name="googleFontInput" class="form-select" id="googleFontInput">
46
  </select>
47
  </div>
 
30
  <label for="googleFontInput" class="form-label">フォント</label>
31
  <div class="row g-2 mb-2">
32
  <div class="col">
33
+ <div class="btn-group w-100 flex-wrap gap-1" role="group" id="fontTagFilter">
34
+ <!-- フィルターボタンはJavaScriptで動的に生成されます -->
 
 
 
 
 
 
 
35
  </div>
36
  </div>
37
  </div>
38
+ <div id="noFontsMessage" class="alert alert-danger mb-2" style="display: none;">
39
+ 選択した条件に一致するフォントが見つかりませんでした
40
+ </div>
41
  <select name="googleFontInput" class="form-select" id="googleFontInput">
42
  </select>
43
  </div>
index.js CHANGED
@@ -1,70 +1,114 @@
1
  import { applyEffect, getAvailableEffects } from './effects.js';
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  const fontTags = [
 
4
  { name: "Aoboshi One", tags: ["japanese"] },
5
- { name: "BIZ UDGothic", tags: ["japanese", "kanji"] },
6
- { name: "BIZ UDMincho", tags: ["japanese", "kanji"] },
7
- { name: "BIZ UDPGothic", tags: ["japanese", "kanji"] },
8
- { name: "BIZ UDPMincho", tags: ["japanese", "kanji"] },
9
- { name: "Cherry Bomb One", tags: ["japanese"] },
10
- { name: "Chokokutai", tags: ["japanese"] },
11
- { name: "Darumadrop One", tags: ["japanese"] },
12
- { name: "Dela Gothic One", tags: ["japanese", "kanji"] },
13
- { name: "DotGothic16", tags: ["japanese", "kanji"] },
14
- { name: "Hachi Maru Pop", tags: ["japanese", "kanji"] },
15
- { name: "Hina Mincho", tags: ["japanese", "kanji"] },
16
- { name: "IBM Plex Sans JP", tags: ["japanese", "kanji"] },
17
- { name: "Kaisei Decol", tags: ["japanese", "kanji"] },
18
- { name: "Kaisei HarunoUmi", tags: ["japanese", "kanji"] },
19
- { name: "Kaisei Opti", tags: ["japanese", "kanji"] },
20
- { name: "Kaisei Tokumin", tags: ["japanese", "kanji"] },
21
- { name: "Kiwi Maru", tags: ["japanese", "kanji"] },
22
- { name: "Klee One", tags: ["japanese", "kanji"] },
23
- { name: "Kosugi", tags: ["japanese", "kanji"] },
24
- { name: "Kosugi Maru", tags: ["japanese", "kanji"] },
25
- { name: "M PLUS 1", tags: ["japanese", "kanji"] },
26
- { name: "M PLUS 1 Code", tags: ["japanese", "kanji"] },
27
- { name: "M PLUS 1p", tags: ["japanese", "kanji"] },
28
- { name: "M PLUS 2", tags: ["japanese", "kanji"] },
29
- { name: "M PLUS Rounded 1c", tags: ["japanese", "kanji"] },
30
- { name: "Mochiy Pop One", tags: ["japanese", "kanji"] },
31
- { name: "Mochiy Pop P One", tags: ["japanese", "kanji"] },
32
- { name: "Monomaniac One", tags: ["japanese"] },
33
- { name: "Murecho", tags: ["japanese"] },
34
- { name: "New Tegomin", tags: ["japanese", "kanji"] },
35
- { name: "Noto Sans JP", tags: ["japanese", "kanji"] },
36
- { name: "Noto Serif JP", tags: ["japanese", "kanji"] },
37
- { name: "Palette Mosaic", tags: ["japanese"] },
38
- { name: "Potta One", tags: ["japanese", "kanji"] },
39
- { name: "Rampart One", tags: ["japanese", "kanji"] },
40
- { name: "Reggae One", tags: ["japanese", "kanji"] },
41
- { name: "Rock 3D", tags: ["japanese"] },
42
- { name: "RocknRoll One", tags: ["japanese", "kanji"] },
43
- { name: "Sawarabi Gothic", tags: ["japanese", "kanji"] },
44
- { name: "Sawarabi Mincho", tags: ["japanese", "kanji"] },
45
- { name: "Shippori Antique", tags: ["japanese", "kanji"] },
46
- { name: "Shippori Antique B1", tags: ["japanese", "kanji"] },
47
- { name: "Shippori Mincho", tags: ["japanese", "kanji"] },
48
- { name: "Shippori Mincho B1", tags: ["japanese", "kanji"] },
49
- { name: "Shizuru", tags: ["japanese"] },
50
- { name: "Slackside One", tags: ["japanese"] },
51
- { name: "Stick", tags: ["japanese", "kanji"] },
52
- { name: "Train One", tags: ["japanese", "kanji"] },
53
- { name: "Tsukimi Rounded", tags: ["japanese"] },
54
- { name: "Yomogi", tags: ["japanese", "kanji"] },
55
- { name: "Yuji Boku", tags: ["japanese", "kanji"] },
56
- { name: "Yuji Hentaigana Akari", tags: ["japanese"] },
57
- { name: "Yuji Hentaigana Akebono", tags: ["japanese"] },
58
- { name: "Yuji Mai", tags: ["japanese", "kanji"] },
59
- { name: "Yuji Syuku", tags: ["japanese", "kanji"] },
60
- { name: "Yusei Magic", tags: ["japanese", "kanji"] },
61
- { name: "Zen Antique", tags: ["japanese", "kanji"] },
62
- { name: "Zen Antique Soft", tags: ["japanese", "kanji"] },
63
- { name: "Zen Kaku Gothic Antique", tags: ["japanese", "kanji"] },
64
- { name: "Zen Kaku Gothic New", tags: ["japanese", "kanji"] },
65
- { name: "Zen Kurenaido", tags: ["japanese"] },
66
- { name: "Zen Maru Gothic", tags: ["japanese"] },
67
- { name: "Zen Old Mincho", tags: ["japanese", "kanji"] }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  ];
69
 
70
 
@@ -159,20 +203,119 @@ document.addEventListener('DOMContentLoaded', async () => {
159
  const fontSizeInput = document.getElementById('fontSize');
160
  const verticalTextInput = document.getElementById('verticalText');
161
  const effectGrid = document.querySelector('.effect-grid');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
  // フォントオプションの初期化と絞り込み機能の実装
164
- function initializeFontOptions(filter = 'all') {
165
  // 現在選択されているフォントを保持
166
  const currentFont = fontSelect.value;
167
 
 
 
 
 
168
  // 既存のオプションをクリア
169
  fontSelect.innerHTML = '';
170
 
171
  // フィルタリングされたフォントのリストを作成
172
- const filteredFonts = fontTags.filter(font => {
173
- if (filter === 'all') return true;
174
- return font.tags.includes(filter);
175
- });
 
 
 
 
 
 
 
 
 
 
 
176
 
177
  // フォントオプションを追加
178
  filteredFonts.forEach((font, index) => {
@@ -191,17 +334,18 @@ document.addEventListener('DOMContentLoaded', async () => {
191
  }
192
 
193
  // タグフィルターの変更イベントを設定
194
- fontTagFilter.querySelectorAll('input[type="radio"]').forEach(radio => {
195
- radio.addEventListener('change', async (e) => {
196
- if (e.target.checked) {
197
- await initializeFontOptions(e.target.value);
198
  await renderAllPresets();
199
  }
200
- });
201
  });
202
 
203
  // 初期化
204
- await initializeFontOptions('all');
 
205
  await loadGoogleFont(fontSelect.value);
206
 
207
  // 縦書きモードの状態をグリッドに反映
 
1
  import { applyEffect, getAvailableEffects } from './effects.js';
2
 
3
+ const tagDisplayNames = {
4
+ japanese: "日本語",
5
+ english: "英語",
6
+ kanji: "漢字対応",
7
+ business: "ビジネス",
8
+ fancy: "装飾的",
9
+ playful: "遊び心",
10
+ display: "ディスプレイ",
11
+ handwritten: "手書き",
12
+ retro: "レトロ",
13
+ calm: "落ち着いた",
14
+ cute: "かわいい",
15
+ script: "筆記体",
16
+ bold: "太字",
17
+ horror: "ホラー",
18
+ comic: "コミック"
19
+ };
20
+
21
  const fontTags = [
22
+ // 日本語フォント
23
  { name: "Aoboshi One", tags: ["japanese"] },
24
+ { name: "BIZ UDGothic", tags: ["japanese", "kanji", "business"] },
25
+ { name: "BIZ UDMincho", tags: ["japanese", "kanji", "business"] },
26
+ { name: "BIZ UDPGothic", tags: ["japanese", "kanji", "business"] },
27
+ { name: "BIZ UDPMincho", tags: ["japanese", "kanji", "business"] },
28
+ { name: "Cherry Bomb One", tags: ["japanese", "cute"] },
29
+ { name: "Chokokutai", tags: ["japanese", "fancy"] },
30
+ { name: "Darumadrop One", tags: ["japanese", "playful"] },
31
+ { name: "Dela Gothic One", tags: ["japanese", "kanji", "display"] },
32
+ { name: "DotGothic16", tags: ["japanese", "kanji", "retro"] },
33
+ { name: "Hachi Maru Pop", tags: ["japanese", "kanji", "cute"] },
34
+ { name: "Hina Mincho", tags: ["japanese", "kanji", "fancy"] },
35
+ { name: "IBM Plex Sans JP", tags: ["japanese", "kanji", "business"] },
36
+ { name: "Kaisei Decol", tags: ["japanese", "kanji", "fancy"] },
37
+ { name: "Kaisei HarunoUmi", tags: ["japanese", "kanji", "fancy"] },
38
+ { name: "Kaisei Opti", tags: ["japanese", "kanji", "business"] },
39
+ { name: "Kaisei Tokumin", tags: ["japanese", "kanji", "business"] },
40
+ { name: "Kiwi Maru", tags: ["japanese", "kanji", "cute"] },
41
+ { name: "Klee One", tags: ["japanese", "kanji", "handwritten"] },
42
+ { name: "Kosugi", tags: ["japanese", "kanji", "business"] },
43
+ { name: "Kosugi Maru", tags: ["japanese", "kanji", "calm"] },
44
+ { name: "M PLUS 1", tags: ["japanese", "kanji", "business"] },
45
+ { name: "M PLUS 1 Code", tags: ["japanese", "kanji", "display"] },
46
+ { name: "M PLUS 1p", tags: ["japanese", "kanji", "business"] },
47
+ { name: "M PLUS 2", tags: ["japanese", "kanji", "business"] },
48
+ { name: "M PLUS Rounded 1c", tags: ["japanese", "kanji", "calm"] },
49
+ { name: "Mochiy Pop One", tags: ["japanese", "kanji", "playful"] },
50
+ { name: "Mochiy Pop P One", tags: ["japanese", "kanji", "playful"] },
51
+ { name: "Monomaniac One", tags: ["japanese", "display"] },
52
+ { name: "Murecho", tags: ["japanese", "business"] },
53
+ { name: "New Tegomin", tags: ["japanese", "kanji", "fancy"] },
54
+ { name: "Noto Sans JP", tags: ["japanese", "kanji", "business"] },
55
+ { name: "Noto Serif JP", tags: ["japanese", "kanji", "business"] },
56
+ { name: "Palette Mosaic", tags: ["japanese", "display"] },
57
+ { name: "Potta One", tags: ["japanese", "kanji", "playful"] },
58
+ { name: "Rampart One", tags: ["japanese", "kanji", "display"] },
59
+ { name: "Reggae One", tags: ["japanese", "kanji", "display"] },
60
+ { name: "Rock 3D", tags: ["japanese", "display"] },
61
+ { name: "RocknRoll One", tags: ["japanese", "kanji", "playful"] },
62
+ { name: "Sawarabi Gothic", tags: ["japanese", "kanji", "business"] },
63
+ { name: "Sawarabi Mincho", tags: ["japanese", "kanji", "business"] },
64
+ { name: "Shippori Antique", tags: ["japanese", "kanji", "retro"] },
65
+ { name: "Shippori Antique B1", tags: ["japanese", "kanji", "retro"] },
66
+ { name: "Shippori Mincho", tags: ["japanese", "kanji", "business"] },
67
+ { name: "Shippori Mincho B1", tags: ["japanese", "kanji", "business"] },
68
+ { name: "Shizuru", tags: ["japanese", "display"] },
69
+ { name: "Slackside One", tags: ["japanese", "handwritten"] },
70
+ { name: "Stick", tags: ["japanese", "kanji", "display"] },
71
+ { name: "Train One", tags: ["japanese", "kanji", "display"] },
72
+ { name: "Tsukimi Rounded", tags: ["japanese", "calm"] },
73
+ { name: "Yomogi", tags: ["japanese", "kanji", "handwritten"] },
74
+ { name: "Yuji Boku", tags: ["japanese", "kanji", "fancy"] },
75
+ { name: "Yuji Hentaigana Akari", tags: ["japanese", "fancy"] },
76
+ { name: "Yuji Hentaigana Akebono", tags: ["japanese", "fancy"] },
77
+ { name: "Yuji Mai", tags: ["japanese", "kanji", "fancy"] },
78
+ { name: "Yuji Syuku", tags: ["japanese", "kanji", "fancy"] },
79
+ { name: "Yusei Magic", tags: ["japanese", "kanji", "playful"] },
80
+ { name: "Zen Antique", tags: ["japanese", "kanji", "retro"] },
81
+ { name: "Zen Antique Soft", tags: ["japanese", "kanji", "retro"] },
82
+ { name: "Zen Kaku Gothic Antique", tags: ["japanese", "kanji", "business"] },
83
+ { name: "Zen Kaku Gothic New", tags: ["japanese", "kanji", "business"] },
84
+ { name: "Zen Kurenaido", tags: ["japanese", "calm"] },
85
+ { name: "Zen Maru Gothic", tags: ["japanese", "calm"] },
86
+ { name: "Zen Old Mincho", tags: ["japanese", "kanji", "retro"] },
87
+
88
+ // 英語フォント - ビジネス/フォーマル
89
+ { name: "Montserrat", tags: ["english", "business"] },
90
+ { name: "Playfair Display", tags: ["english", "business", "fancy"] },
91
+ { name: "Roboto", tags: ["english", "business"] },
92
+ { name: "Lato", tags: ["english", "business"] },
93
+
94
+ // デコラティブ/ファンシー
95
+ { name: "Pacifico", tags: ["english", "fancy", "script"] },
96
+ { name: "Great Vibes", tags: ["english", "fancy", "script"] },
97
+ { name: "Lobster", tags: ["english", "fancy"] },
98
+ { name: "Dancing Script", tags: ["english", "fancy", "script"] },
99
+ { name: "Satisfy", tags: ["english", "fancy", "script"] },
100
+
101
+ // ディスプレイ/特殊
102
+ { name: "Righteous", tags: ["english", "display"] },
103
+ { name: "Permanent Marker", tags: ["english", "display", "handwritten"] },
104
+ { name: "Press Start 2P", tags: ["english", "display", "retro"] },
105
+ { name: "Fredoka One", tags: ["english", "display", "playful"] },
106
+ { name: "Creepster", tags: ["english", "display", "horror"] },
107
+ { name: "Bangers", tags: ["english", "display", "comic"] },
108
+ { name: "Rubik Mono One", tags: ["english", "display", "bold"] },
109
+ { name: "Bungee", tags: ["english", "display", "bold"] },
110
+ { name: "Bungee Shade", tags: ["english", "display", "fancy"] },
111
+ { name: "Monoton", tags: ["english", "display", "retro"] }
112
  ];
113
 
114
 
 
203
  const fontSizeInput = document.getElementById('fontSize');
204
  const verticalTextInput = document.getElementById('verticalText');
205
  const effectGrid = document.querySelector('.effect-grid');
206
+ const noFontsMessage = document.getElementById('noFontsMessage');
207
+
208
+ // 利用可能なタグを収集し、使用頻度をカウント
209
+ function getTagsWithCount() {
210
+ const tagCount = new Map();
211
+ fontTags.forEach(font => {
212
+ font.tags.forEach(tag => {
213
+ tagCount.set(tag, (tagCount.get(tag) || 0) + 1);
214
+ });
215
+ });
216
+ return tagCount;
217
+ }
218
+
219
+ // 言語関連のタグかどうかを判定
220
+ function isLanguageTag(tag) {
221
+ return ['japanese', 'english', 'kanji'].includes(tag);
222
+ }
223
+
224
+ // フィルターボタンを動的に生成
225
+ function createFilterButtons() {
226
+ const tagCount = getTagsWithCount();
227
+ fontTagFilter.innerHTML = '';
228
+
229
+ // タグを言語関連とその他に分類し、個数でソート
230
+ const languageTags = [...tagCount.entries()]
231
+ .filter(([tag]) => isLanguageTag(tag))
232
+ .sort((a, b) => b[1] - a[1]);
233
+
234
+ const otherTags = [...tagCount.entries()]
235
+ .filter(([tag]) => !isLanguageTag(tag))
236
+ .sort((a, b) => b[1] - a[1]);
237
+
238
+ // 言語関連のタグを追加
239
+ if (languageTags.length > 0) {
240
+ const langGroup = document.createElement('div');
241
+ langGroup.className = 'filter-group mb-2';
242
+ langGroup.innerHTML = '<div class="filter-group-label mb-1">言語</div>';
243
+
244
+ const langButtonGroup = document.createElement('div');
245
+ langButtonGroup.className = 'btn-group-wrapper';
246
+
247
+ languageTags.forEach(([tag, count]) => {
248
+ const displayName = tagDisplayNames[tag] || tag;
249
+ const button = document.createElement('div');
250
+ button.className = 'btn-check-wrapper';
251
+ button.innerHTML = `
252
+ <input type="checkbox" class="btn-check" name="fontFilter" id="filter${tag}" value="${tag}">
253
+ <label class="btn btn-outline-primary" for="filter${tag}">
254
+ ${displayName} <span class="tag-count">(${count})</span>
255
+ </label>
256
+ `;
257
+ langButtonGroup.appendChild(button);
258
+ });
259
+
260
+ langGroup.appendChild(langButtonGroup);
261
+ fontTagFilter.appendChild(langGroup);
262
+ }
263
+
264
+ // その他のタグを追加
265
+ if (otherTags.length > 0) {
266
+ const otherGroup = document.createElement('div');
267
+ otherGroup.className = 'filter-group';
268
+ otherGroup.innerHTML = '<div class="filter-group-label mb-1">スタイル</div>';
269
+
270
+ const otherButtonGroup = document.createElement('div');
271
+ otherButtonGroup.className = 'btn-group-wrapper';
272
+
273
+ otherTags.forEach(([tag, count]) => {
274
+ const displayName = tagDisplayNames[tag] || tag;
275
+ const button = document.createElement('div');
276
+ button.className = 'btn-check-wrapper';
277
+ button.innerHTML = `
278
+ <input type="checkbox" class="btn-check" name="fontFilter" id="filter${tag}" value="${tag}">
279
+ <label class="btn btn-outline-primary" for="filter${tag}">
280
+ ${displayName} <span class="tag-count">(${count})</span>
281
+ </label>
282
+ `;
283
+ otherButtonGroup.appendChild(button);
284
+ });
285
+
286
+ otherGroup.appendChild(otherButtonGroup);
287
+ fontTagFilter.appendChild(otherGroup);
288
+ }
289
+ }
290
 
291
  // フォントオプションの初期化と絞り込み機能の実装
292
+ function initializeFontOptions() {
293
  // 現在選択されているフォントを保持
294
  const currentFont = fontSelect.value;
295
 
296
+ // 選択されているフィルターを取得
297
+ const selectedFilters = Array.from(fontTagFilter.querySelectorAll('input[type="checkbox"]:checked'))
298
+ .map(checkbox => checkbox.value);
299
+
300
  // 既存のオプションをクリア
301
  fontSelect.innerHTML = '';
302
 
303
  // フィルタリングされたフォントのリストを作成
304
+ const filteredFonts = selectedFilters.length === 0
305
+ ? fontTags
306
+ : fontTags.filter(font =>
307
+ selectedFilters.every(filter => font.tags.includes(filter))
308
+ );
309
+
310
+ // フォルター結果が0件の場合
311
+ if (filteredFonts.length === 0) {
312
+ noFontsMessage.style.display = 'block';
313
+ fontSelect.disabled = true;
314
+ return Promise.resolve(); // フォントの読み込みは不要
315
+ } else {
316
+ noFontsMessage.style.display = 'none';
317
+ fontSelect.disabled = false;
318
+ }
319
 
320
  // フォントオプションを追加
321
  filteredFonts.forEach((font, index) => {
 
334
  }
335
 
336
  // タグフィルターの変更イベントを設定
337
+ fontTagFilter.addEventListener('change', async (e) => {
338
+ if (e.target.type === 'checkbox') {
339
+ await initializeFontOptions();
340
+ if (!fontSelect.disabled) { // フォントが選択可能な場合のみプレビューを更新
341
  await renderAllPresets();
342
  }
343
+ }
344
  });
345
 
346
  // 初期化
347
+ createFilterButtons();
348
+ await initializeFontOptions();
349
  await loadGoogleFont(fontSelect.value);
350
 
351
  // 縦書きモードの状態をグリッドに反映
styles.css CHANGED
@@ -114,4 +114,60 @@
114
  0% { transform: translateY(0) translateX(0); }
115
  50% { transform: translateY(-10px) translateX(5px); }
116
  100% { transform: translateY(0) translateX(0); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  }
 
114
  0% { transform: translateY(0) translateX(0); }
115
  50% { transform: translateY(-10px) translateX(5px); }
116
  100% { transform: translateY(0) translateX(0); }
117
+ }
118
+
119
+ .btn-check-wrapper {
120
+ display: inline-block;
121
+ margin: 2px;
122
+ }
123
+
124
+ .btn-check-wrapper .btn {
125
+ margin: 0;
126
+ white-space: nowrap;
127
+ }
128
+
129
+ #fontTagFilter {
130
+ display: flex;
131
+ flex-wrap: wrap;
132
+ gap: 0.25rem;
133
+ }
134
+
135
+ #fontTagFilter .btn {
136
+ border-radius: 0.25rem;
137
+ font-size: 0.875rem;
138
+ padding: 0.25rem 0.5rem;
139
+ }
140
+
141
+ /* チェックされた状態のスタイル */
142
+ .btn-check:checked + .btn-outline-primary {
143
+ background-color: #0d6efd;
144
+ color: white;
145
+ }
146
+
147
+ .filter-group {
148
+ margin-bottom: 1rem;
149
+ }
150
+
151
+ .filter-group-label {
152
+ font-size: 0.875rem;
153
+ font-weight: 600;
154
+ color: #6c757d;
155
+ margin-bottom: 0.5rem;
156
+ }
157
+
158
+ .btn-group-wrapper {
159
+ display: flex;
160
+ flex-wrap: wrap;
161
+ gap: 0.25rem;
162
+ }
163
+
164
+ .tag-count {
165
+ font-size: 0.75rem;
166
+ opacity: 0.8;
167
+ }
168
+
169
+ /* ボタンのホバー時もタグカウントの視認性を保持 */
170
+ .btn-check:checked + .btn-outline-primary .tag-count,
171
+ .btn-outline-primary:hover .tag-count {
172
+ opacity: 1;
173
  }