huhaa-myskills 0.2.13 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,8 +15,8 @@
15
15
  <link rel="manifest" href="/site.webmanifest">
16
16
  <meta name="theme-color" content="#7c3aed">
17
17
  <meta name="description" content="一个高效的技能、规则和提示管理工具">
18
- <script type="module" crossorigin src="/assets/index-BPepSb3q.js"></script>
19
- <link rel="stylesheet" crossorigin href="/assets/index-CIGkgT5w.css">
18
+ <script type="module" crossorigin src="/assets/index-Dlvh4-4K.js"></script>
19
+ <link rel="stylesheet" crossorigin href="/assets/index-CaQOPbDU.css">
20
20
  </head>
21
21
  <body>
22
22
  <div id="app"></div>
@@ -12,6 +12,11 @@ const i18n = useI18nStore();
12
12
  const t = i18n.t;
13
13
  const md = new MarkdownIt({ html: false, linkify: true, typographer: true });
14
14
  const viewMode = ref('list');
15
+ const sidebarWidth = ref(180); // 默认 180px(更合理的宽度)
16
+ const isResizing = ref(false);
17
+ const startX = ref(0);
18
+ const startWidth = ref(0);
19
+ const sidebarCollapsed = ref(false); // 侧边栏折叠状态
15
20
 
16
21
  const viewModeLabel = computed(() => {
17
22
  const labels = {
@@ -23,7 +28,15 @@ const viewModeLabel = computed(() => {
23
28
  return labels[viewMode.value] || '列表';
24
29
  });
25
30
 
26
- onMounted(() => store.load());
31
+ onMounted(() => {
32
+ store.load();
33
+ window.addEventListener('keydown', handleKeydown);
34
+ // 从 localStorage 加载侧栏宽度
35
+ const saved = localStorage.getItem('huhaa-sidebar-width');
36
+ if (saved) {
37
+ sidebarWidth.value = parseInt(saved, 10);
38
+ }
39
+ });
27
40
 
28
41
  const detailHtml = computed(() => {
29
42
  const raw = store.selected?.raw || store.selected?.preview || '';
@@ -164,180 +177,134 @@ function formatBytes(n) {
164
177
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
165
178
  return `${(n / 1024 / 1024).toFixed(1)} MB`;
166
179
  }
167
- </script>
168
180
 
169
- <template>
170
- <div class="shell">
171
- <aside class="sidebar">
172
- <div class="brand-block">
173
- <div class="logo">H</div>
174
- <div>
175
- <h1>HuHaa-MySkills</h1>
176
- <p>{{ t('appSubtitle') }}</p>
177
- </div>
178
- </div>
181
+ function handleKeydown(e) {
182
+ if (e.key === 'Escape' && store.selected) {
183
+ store.selectedId = null;
184
+ }
185
+ }
179
186
 
180
- <div class="lang-switch">
181
- <span>{{ t('language') }}</span>
182
- <button :class="{ active: i18n.locale === 'zh' }" @click="i18n.setLocale('zh')">中文</button>
183
- <button :class="{ active: i18n.locale === 'en' }" @click="i18n.setLocale('en')">EN</button>
184
- </div>
187
+ function startResize(e) {
188
+ isResizing.value = true;
189
+ startX.value = e.clientX;
190
+ startWidth.value = sidebarWidth.value;
191
+ document.addEventListener('mousemove', onResize);
192
+ document.addEventListener('mouseup', stopResize);
193
+ }
185
194
 
186
- <div v-if="store.stats" class="stats-card">
187
- <div class="stat-main">{{ store.stats.total }}</div>
188
- <div class="stat-label">{{ t('skillsLoaded') }}</div>
189
- </div>
195
+ function onResize(e) {
196
+ if (!isResizing.value) return;
197
+ const diff = e.clientX - startX.value;
198
+ const newWidth = Math.max(80, Math.min(300, startWidth.value + diff));
199
+ sidebarWidth.value = newWidth;
200
+ }
190
201
 
191
- <div class="selected-editor-card" v-if="store.filters.editor">
192
- <div>
193
- <span>{{ t('selectedEditor') }}</span>
194
- <strong>{{ store.filters.editor }}</strong>
195
- </div>
196
- <button @click="store.setFilter('editor', '')">×</button>
197
- </div>
202
+ function stopResize() {
203
+ isResizing.value = false;
204
+ document.removeEventListener('mousemove', onResize);
205
+ document.removeEventListener('mouseup', stopResize);
206
+ // 保存到 localStorage
207
+ localStorage.setItem('huhaa-sidebar-width', sidebarWidth.value.toString());
208
+ }
209
+
210
+ onMounted(() => {
211
+ window.addEventListener('keydown', handleKeydown);
212
+ });
213
+ </script>
198
214
 
199
- <section class="filter-section nav-section">
200
- <div class="section-head">
201
- <h2>{{ t('editor') }}</h2>
202
- <small>{{ t('frontSelect') }}</small>
215
+ <template>
216
+ <div class="shell" :style="{ gridTemplateColumns: `${sidebarCollapsed ? 50 : sidebarWidth}px 1fr 380px` }">
217
+ <!-- LEFT SIDEBAR - 筛选项(支持拖拽) -->
218
+ <aside class="sidebar" :class="{ collapsed: sidebarCollapsed }">
219
+ <div class="sidebar-toggle" @click="sidebarCollapsed = !sidebarCollapsed" :title="sidebarCollapsed ? '展开筛选' : '折叠筛选'">
220
+ {{ sidebarCollapsed ? '▶' : '◀' }}
221
+ </div>
222
+
223
+ <div class="sidebar-content" v-show="!sidebarCollapsed">
224
+ <!-- 筛选项 -->
225
+ <div class="filter-compact">
226
+ <label class="filter-item">
227
+ <span>类型</span>
228
+ <select v-model="store.filters.kind" class="filter-select-compact">
229
+ <option value="">全部</option>
230
+ <option v-for="o in store.kinds" :key="o.name" :value="o.name">{{ o.name }}</option>
231
+ </select>
232
+ </label>
233
+
234
+ <label class="filter-item">
235
+ <span>来源</span>
236
+ <select v-model="store.filters.source" class="filter-select-compact">
237
+ <option value="">全部</option>
238
+ <option v-for="o in store.filterSources" :key="o.name" :value="o.name">{{ o.name }}</option>
239
+ </select>
240
+ </label>
241
+
242
+ <label class="filter-item">
243
+ <span>产品</span>
244
+ <select v-model="store.filters.product" class="filter-select-compact">
245
+ <option value="">全部</option>
246
+ <option v-for="o in store.products" :key="o.name" :value="o.name">{{ o.name }}</option>
247
+ </select>
248
+ </label>
249
+
250
+ <label class="filter-item">
251
+ <span>品牌</span>
252
+ <select v-model="store.filters.brand" class="filter-select-compact">
253
+ <option value="">全部</option>
254
+ <option v-for="o in store.brands" :key="o.name" :value="o.name">{{ o.name }}</option>
255
+ </select>
256
+ </label>
257
+
258
+ <label class="filter-item">
259
+ <span>排序</span>
260
+ <select v-model="store.sortKey" class="filter-select-compact">
261
+ <option value="default">默认</option>
262
+ <option value="name">名称</option>
263
+ <option value="updated">最近更新</option>
264
+ <option value="kind">类型</option>
265
+ <option value="source">来源</option>
266
+ <option value="product">产品</option>
267
+ </select>
268
+ </label>
203
269
  </div>
204
- <button
205
- class="pill"
206
- :class="{ active: !store.filters.editor }"
207
- @click="store.setFilter('editor', '')"
208
- >
209
- <span>{{ t('all') }}</span><b>{{ store.stats?.total || 0 }}</b>
210
- </button>
211
- <button
212
- v-for="e in store.editors"
213
- :key="e.name"
214
- class="pill"
215
- :class="{ active: store.filters.editor === e.name }"
216
- @click="store.setFilter('editor', e.name)"
217
- >
218
- <span>{{ e.name }}</span><b>{{ e.count }}</b>
219
- </button>
220
- </section>
270
+ </div>
221
271
 
272
+ <!-- 拖拽调整把手 -->
273
+ <div class="sidebar-resize-handle" @mousedown="startResize" title="拖拽调整宽度"></div>
222
274
  </aside>
223
275
 
276
+ <!-- MAIN CONTENT -->
224
277
  <main class="main">
278
+ <!-- TOPBAR -->
225
279
  <header class="topbar">
226
- <div class="toolbar-card">
227
- <div class="toolbar-main">
228
- <div class="search-box">
229
- <span class="search-icon">⌕</span>
230
- <input
231
- v-model="store.query"
232
- type="search"
233
- :placeholder="t('searchPlaceholder')"
234
- />
235
- </div>
236
- <button class="btn soft" @click="store.clearFilters">{{ t('clear') }}</button>
237
- <button class="btn primary" :disabled="store.reloadState?.scanning" @click="store.reload({ done: t('reloadDone') })">
238
- {{ store.reloadState?.scanning ? t('reloading') : t('reload') }}
239
- </button>
240
- </div>
241
-
242
- <div class="toolbar-facets">
243
- <label class="facet">
244
- <span>{{ t('kind') }}</span>
245
- <select v-model="store.filters.kind">
246
- <option value="">{{ t('all') }}</option>
247
- <option v-for="o in store.kinds" :key="o.name" :value="o.name">{{ o.name }} ({{ o.count }})</option>
248
- </select>
249
- </label>
250
- <label class="facet">
251
- <span>{{ t('source') }}</span>
252
- <select v-model="store.filters.source">
253
- <option value="">{{ t('all') }}</option>
254
- <option v-for="o in store.filterSources" :key="o.name" :value="o.name">{{ o.name }} ({{ o.count }})</option>
255
- </select>
256
- </label>
257
- <label class="facet">
258
- <span>{{ t('product') }}</span>
259
- <select v-model="store.filters.product">
260
- <option value="">{{ t('all') }}</option>
261
- <option v-for="o in store.products" :key="o.name" :value="o.name">{{ o.name }} ({{ o.count }})</option>
262
- </select>
263
- </label>
264
- <label class="facet">
265
- <span>{{ t('brand') }}</span>
266
- <select v-model="store.filters.brand">
267
- <option value="">{{ t('all') }}</option>
268
- <option v-for="o in store.brands" :key="o.name" :value="o.name">{{ o.name }} ({{ o.count }})</option>
269
- </select>
270
- </label>
271
- <label class="facet compact">
272
- <span>{{ t('sort') }}</span>
273
- <select v-model="store.sortKey">
274
- <option value="default">{{ t('sortDefault') }}</option>
275
- <option value="name">{{ t('sortName') }}</option>
276
- <option value="updated">{{ t('sortUpdated') }}</option>
277
- <option value="kind">{{ t('sortKind') }}</option>
278
- <option value="source">{{ t('sortSource') }}</option>
279
- <option value="product">{{ t('sortProduct') }}</option>
280
- </select>
281
- </label>
280
+ <div class="topbar-left">
281
+ <div class="search-box">
282
+ <span class="search-icon">⌕</span>
283
+ <input
284
+ v-model="store.query"
285
+ type="search"
286
+ :placeholder="t('searchPlaceholder')"
287
+ />
282
288
  </div>
289
+ </div>
283
290
 
284
- <div class="toolbar-status">
285
- <span class="filter-chip" :class="{ active: store.activeFilterCount }">
286
- {{ store.activeFilterCount ? `${store.activeFilterCount} ${t('active')}` : t('noFilters') }}
287
- </span>
288
- <div v-if="activeFilterChips.length" class="active-chip-row" :title="activeFilterText">
289
- <button
290
- v-for="chip in activeFilterChips"
291
- :key="`${chip.key}:${chip.value}`"
292
- class="active-chip"
293
- @click="clearFilterChip(chip.key)"
294
- >
295
- <span>{{ chip.label }}</span>
296
- <strong>{{ chip.value }}</strong>
297
- <b>×</b>
298
- </button>
299
- </div>
300
- <span v-else class="active-filter">{{ t('searchHint') }}</span>
301
- <span v-if="store.reloadState?.lastReloadAt" class="reload-state">
302
- {{ t('lastReload') }} {{ new Date(store.reloadState.lastReloadAt).toLocaleTimeString() }}
303
- </span>
304
- </div>
291
+ <div class="topbar-right">
292
+ <button class="btn soft" @click="store.clearFilters">{{ t('clear') }}</button>
293
+ <button class="btn primary" :disabled="store.reloadState?.scanning" @click="store.reload({ done: t('reloadDone') })">
294
+ {{ store.reloadState?.scanning ? t('reloading') : t('reload') }}
295
+ </button>
305
296
  </div>
306
297
  </header>
307
298
 
308
- <div v-if="store.error" class="error">{{ store.error }}</div>
309
- <div v-if="store.loading" class="loading">{{ t('loading') }}</div>
299
+ <!-- CONTENT AREA -->
300
+ <div class="content-wrapper">
301
+ <div v-if="store.error" class="error">{{ store.error }}</div>
302
+ <div v-if="store.loading" class="loading">{{ t('loading') }}</div>
310
303
 
311
- <div class="content">
304
+ <!-- LIST PANEL -->
312
305
  <section class="list-panel">
313
306
  <div class="list-head">
314
307
  <div class="list-view-label">{{ viewModeLabel }}</div>
315
- <div class="list-view-controls">
316
- <button
317
- :class="{ active: viewMode === 'list' }"
318
- class="view-btn"
319
- @click="viewMode = 'list'"
320
- :title="t('list')"
321
- >{{ t('list') }}</button>
322
- <button
323
- :class="{ active: viewMode === 'tree' }"
324
- class="view-btn"
325
- @click="viewMode = 'tree'"
326
- :title="t('category')"
327
- >{{ t('category') }}</button>
328
- <button
329
- :class="{ active: viewMode === 'path-tree' }"
330
- class="view-btn"
331
- @click="viewMode = 'path-tree'"
332
- title="按目录结构"
333
- >目录结构</button>
334
- <button
335
- :class="{ active: viewMode === 'app-tree' }"
336
- class="view-btn"
337
- @click="viewMode = 'app-tree'"
338
- title="按应用分组"
339
- >应用分组</button>
340
- </div>
341
308
  <div>
342
309
  <strong>{{ store.filtered.length }}</strong>
343
310
  <span>{{ t('items') }}</span>
@@ -346,28 +313,28 @@ function formatBytes(n) {
346
313
 
347
314
  <!-- List View -->
348
315
  <template v-if="viewMode === 'list'">
349
- <button
350
- v-for="item in store.filtered"
351
- :key="item.id"
352
- class="skill-card"
353
- :class="{ selected: item.id === store.selectedId }"
354
- @click="store.loadDetail(item.id)"
355
- >
356
- <div class="skill-title">
357
- <span class="src" :class="`src-${item.source}`">{{ item.source }}</span>
358
- <strong v-html="highlighted(item.name)"></strong>
359
- </div>
360
- <div class="skill-chipline">
361
- <span>{{ item.editor || item.source }}</span>
362
- <span>{{ item.kind || '-' }}</span>
363
- <span v-if="item.product">{{ item.product }}</span>
364
- </div>
365
- <div class="skill-meta">
366
- <span>{{ item.category || '-' }}</span>
367
- <span v-if="item.brand">· {{ item.brand }}</span>
368
- </div>
369
- <p v-html="highlighted(item.description)"></p>
370
- </button>
316
+ <button
317
+ v-for="item in store.filtered"
318
+ :key="item.id"
319
+ class="skill-card"
320
+ :class="{ selected: item.id === store.selectedId }"
321
+ @click="store.loadDetail(item.id)"
322
+ >
323
+ <div class="skill-title">
324
+ <span class="src" :class="`src-${item.source}`">{{ item.source }}</span>
325
+ <strong v-html="highlighted(item.name)"></strong>
326
+ </div>
327
+ <div class="skill-chipline">
328
+ <span>{{ item.editor || item.source }}</span>
329
+ <span>{{ item.kind || '-' }}</span>
330
+ <span v-if="item.product">{{ item.product }}</span>
331
+ </div>
332
+ <div class="skill-meta">
333
+ <span>{{ item.category || '-' }}</span>
334
+ <span v-if="item.brand">· {{ item.brand }}</span>
335
+ </div>
336
+ <p v-html="highlighted(item.description)"></p>
337
+ </button>
371
338
  </template>
372
339
 
373
340
  <!-- Tree View (by Source) -->
@@ -379,105 +346,112 @@ function formatBytes(n) {
379
346
  <!-- App Tree View -->
380
347
  <AppTree v-else-if="viewMode === 'app-tree'" />
381
348
  </section>
349
+ </div>
350
+ </main>
382
351
 
383
- <section class="detail-panel" v-if="store.selected">
384
- <div class="detail-head">
385
- <div>
386
- <div class="detail-kicker">
387
- <span class="src" :class="`src-${store.selected.source}`">{{ store.selected.source }}</span>
388
- <span>{{ store.selected.editor || store.selected.source }}</span>
389
- <span>{{ store.selected.category || t('uncategorized') }}</span>
390
- </div>
391
- <h2>{{ i18n.skillText(store.selected, 'name') }}</h2>
392
- <div class="badge-row">
393
- <span
394
- v-for="badge in detailBadges"
395
- :key="badge"
396
- class="badge"
397
- :class="{ danger: badge === t('parseError'), warn: badge === t('redacted') }"
398
- >{{ badge }}</span>
399
- </div>
400
- <p>{{ i18n.skillText(store.selected, 'description') }}</p>
401
- </div>
402
- </div>
352
+ <!-- RIGHT DETAIL PANEL - 固定第三栏(不是浮层) -->
353
+ <aside class="detail-panel-fixed" v-if="store.selected" role="complementary">
354
+ <!-- CLOSE BUTTON -->
355
+ <button class="detail-close" @click="store.selectedId = null" title="Close (Esc)">✕</button>
403
356
 
404
- <div class="actions">
405
- <button @click="store.copySelected('path', { copied: t('copied'), bytes: t('bytes') })">{{ t('copyPath') }}</button>
406
- <button @click="store.copySelected('dir', { copied: t('copied'), bytes: t('bytes') })">{{ t('copyDir') }}</button>
407
- <button @click="store.copySelected('rel', { copied: t('copied'), bytes: t('bytes') })">{{ t('copyRel') }}</button>
408
- <button @click="store.copySelected('prompt', { copied: t('copied'), bytes: t('bytes') })">{{ t('copyPrompt') }}</button>
409
- <button @click="store.copySelected('name', { copied: t('copied'), bytes: t('bytes') })">{{ t('copyName') }}</button>
410
- <button @click="store.copySelected('raw', { copied: t('copied'), bytes: t('bytes') })">{{ t('copyRaw') }}</button>
411
- <button @click="store.openSelected('cursor', { opened: t('opened') })">{{ t('openCursor') }}</button>
412
- <button @click="store.openSelected('finder', { opened: t('opened') })">{{ t('reveal') }}</button>
357
+ <!-- HEADER -->
358
+ <div class="detail-head">
359
+ <div>
360
+ <div class="detail-kicker">
361
+ <span class="src" :class="`src-${store.selected.source}`">{{ store.selected.source }}</span>
362
+ <span>{{ store.selected.editor || store.selected.source }}</span>
363
+ <span>{{ store.selected.category || t('uncategorized') }}</span>
364
+ </div>
365
+ <h2>{{ i18n.skillText(store.selected, 'name') }}</h2>
366
+ <div class="badge-row">
367
+ <span
368
+ v-for="badge in detailBadges"
369
+ :key="badge"
370
+ class="badge"
371
+ :class="{ danger: badge === t('parseError'), warn: badge === t('redacted') }"
372
+ >{{ badge }}</span>
413
373
  </div>
374
+ <p>{{ i18n.skillText(store.selected, 'description') }}</p>
375
+ </div>
376
+ </div>
414
377
 
415
- <div v-if="store.notice" class="notice">{{ store.notice }}</div>
378
+ <!-- ACTIONS -->
379
+ <div class="actions">
380
+ <button @click="store.copySelected('path', { copied: t('copied'), bytes: t('bytes') })" class="action-btn">💬 {{ t('copyPath') }}</button>
381
+ <button @click="store.copySelected('dir', { copied: t('copied'), bytes: t('bytes') })" class="action-btn">📂 {{ t('copyDir') }}</button>
382
+ <button @click="store.copySelected('rel', { copied: t('copied'), bytes: t('bytes') })" class="action-btn">🔗 {{ t('copyRel') }}</button>
383
+ <button @click="store.copySelected('prompt', { copied: t('copied'), bytes: t('bytes') })" class="action-btn">📋 {{ t('copyPrompt') }}</button>
384
+ <button @click="store.openSelected('cursor', { opened: t('opened') })" class="action-btn">🎯 {{ t('openCursor') }}</button>
385
+ <button @click="store.openSelected('finder', { opened: t('opened') })" class="action-btn">🔍 {{ t('reveal') }}</button>
386
+ </div>
416
387
 
417
- <section class="usage-card">
418
- <div class="usage-head">
419
- <div>
420
- <span>{{ t('usage') }}</span>
421
- <h3>{{ t('howToUse') }}</h3>
422
- </div>
423
- <button @click="store.copySelected('prompt', { copied: t('copied'), bytes: t('bytes') })">{{ t('copyPrompt') }}</button>
424
- </div>
425
- <div class="usage-prompt"><code>{{ usagePrompt }}</code></div>
426
- <div class="usage-grid">
427
- <div>
428
- <strong>{{ t('appliesTo') }}</strong>
429
- <p>{{ store.selected.description || '-' }}</p>
430
- </div>
431
- <div>
432
- <strong>{{ t('quickFacts') }}</strong>
433
- <p>{{ store.selected.editor || store.selected.source }} · {{ store.selected.kind }}<span v-if="store.selected.product"> · {{ store.selected.product }}</span></p>
434
- </div>
435
- </div>
436
- <div class="usage-block" v-if="store.selected.params?.length">
437
- <strong>{{ t('params') }}</strong>
438
- <div class="param-table">
439
- <div v-for="p in store.selected.params" :key="p.name">
440
- <code>{{ p.name }}</code>
441
- <span>{{ p.type || '-' }}</span>
442
- <b v-if="p.required">{{ t('required') }}</b>
443
- <p>{{ p.description || p.default || '-' }}</p>
444
- </div>
445
- </div>
446
- </div>
447
- <div class="usage-block" v-if="store.selected.triggers?.length">
448
- <strong>{{ t('triggers') }}</strong>
449
- <span class="tag" v-for="tag in store.selected.triggers.slice(0, 12)" :key="tag">{{ tag }}</span>
450
- </div>
451
- <div class="usage-block" v-if="store.selected.links?.length">
452
- <strong>{{ t('links') }}</strong>
453
- <a class="usage-link" v-for="link in store.selected.links" :key="link.url" :href="link.url" target="_blank" rel="noreferrer">{{ link.label || link.url }}</a>
454
- </div>
455
- </section>
388
+ <div v-if="store.notice" class="notice">{{ store.notice }}</div>
456
389
 
457
- <dl class="kv">
390
+ <!-- SCROLLABLE CONTENT -->
391
+ <div class="detail-scroll">
392
+ <!-- USAGE CARD -->
393
+ <section class="usage-card">
394
+ <div class="usage-head">
395
+ <span>{{ t('usage') }}</span>
396
+ <h3>{{ t('howToUse') }}</h3>
397
+ </div>
398
+ <div class="usage-prompt"><code>{{ usagePrompt }}</code></div>
399
+ <div class="usage-grid">
458
400
  <div>
459
- <dt>{{ t('path') }}</dt>
460
- <dd><code>{{ store.selected.paths?.abs }}</code></dd>
401
+ <strong>{{ t('appliesTo') }}</strong>
402
+ <p>{{ store.selected.description || '-' }}</p>
461
403
  </div>
462
- <div v-for="([label, value]) in detailMeta" :key="label">
463
- <dt>{{ label }}</dt>
464
- <dd>{{ value }}</dd>
465
- </div>
466
- <div v-if="store.selected.parseError" class="kv-error">
467
- <dt>{{ t('parseError') }}</dt>
468
- <dd>{{ store.selected.parseError }}</dd>
404
+ <div>
405
+ <strong>{{ t('quickFacts') }}</strong>
406
+ <p>{{ store.selected.editor || store.selected.source }} · {{ store.selected.kind }}<span v-if="store.selected.product"> · {{ store.selected.product }}</span></p>
469
407
  </div>
470
- <div v-if="store.selected.triggers?.length">
471
- <dt>{{ t('triggers') }}</dt>
472
- <dd>
473
- <span class="tag" v-for="t in store.selected.triggers.slice(0, 8)" :key="t">{{ t }}</span>
474
- </dd>
408
+ </div>
409
+ <div class="usage-block" v-if="store.selected.params?.length">
410
+ <strong>{{ t('params') }}</strong>
411
+ <div class="param-table">
412
+ <div v-for="p in store.selected.params" :key="p.name">
413
+ <code>{{ p.name }}</code>
414
+ <span>{{ p.type || '-' }}</span>
415
+ <b v-if="p.required">{{ t('required') }}</b>
416
+ <p>{{ p.description || p.default || '-' }}</p>
417
+ </div>
475
418
  </div>
476
- </dl>
477
-
478
- <article class="markdown" v-html="detailHtml"></article>
419
+ </div>
420
+ <div class="usage-block" v-if="store.selected.triggers?.length">
421
+ <strong>{{ t('triggers') }}</strong>
422
+ <span class="tag" v-for="tag in store.selected.triggers.slice(0, 12)" :key="tag">{{ tag }}</span>
423
+ </div>
424
+ <div class="usage-block" v-if="store.selected.links?.length">
425
+ <strong>{{ t('links') }}</strong>
426
+ <a class="usage-link" v-for="link in store.selected.links" :key="link.url" :href="link.url" target="_blank" rel="noreferrer">{{ link.label || link.url }}</a>
427
+ </div>
479
428
  </section>
429
+
430
+ <!-- METADATA -->
431
+ <dl class="kv">
432
+ <div>
433
+ <dt>{{ t('path') }}</dt>
434
+ <dd><code>{{ store.selected.paths?.abs }}</code></dd>
435
+ </div>
436
+ <div v-for="([label, value]) in detailMeta" :key="label">
437
+ <dt>{{ label }}</dt>
438
+ <dd>{{ value }}</dd>
439
+ </div>
440
+ <div v-if="store.selected.parseError" class="kv-error">
441
+ <dt>{{ t('parseError') }}</dt>
442
+ <dd>{{ store.selected.parseError }}</dd>
443
+ </div>
444
+ <div v-if="store.selected.triggers?.length">
445
+ <dt>{{ t('triggers') }}</dt>
446
+ <dd>
447
+ <span class="tag" v-for="trig in store.selected.triggers.slice(0, 8)" :key="trig">{{ trig }}</span>
448
+ </dd>
449
+ </div>
450
+ </dl>
451
+
452
+ <!-- MARKDOWN CONTENT -->
453
+ <article class="markdown" v-html="detailHtml"></article>
480
454
  </div>
481
- </main>
455
+ </aside>
482
456
  </div>
483
457
  </template>