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.
@@ -0,0 +1,551 @@
1
+ <script setup>
2
+ import { useSkillsStore } from '../stores/skills.js';
3
+ import { useI18nStore } from '../stores/i18n.js';
4
+
5
+ const props = defineProps({
6
+ detailHtml: String,
7
+ detailMeta: Array,
8
+ detailBadges: Array,
9
+ usagePrompt: String,
10
+ });
11
+
12
+ const emit = defineEmits(['close']);
13
+
14
+ const store = useSkillsStore();
15
+ const i18n = useI18nStore();
16
+ const t = i18n.t;
17
+
18
+ function handleEscape(e) {
19
+ if (e.key === 'Escape') {
20
+ emit('close');
21
+ }
22
+ }
23
+ </script>
24
+
25
+ <template>
26
+ <div class="detail-panel-wrapper">
27
+ <div class="detail-panel-overlay" @click="emit('close')"></div>
28
+ <div class="detail-panel" role="dialog" @keydown="handleEscape" tabindex="0">
29
+ <!-- CLOSE BUTTON -->
30
+ <button class="detail-close" @click="emit('close')" title="Close (Esc)">✕</button>
31
+
32
+ <!-- HEADER -->
33
+ <div class="detail-head">
34
+ <div>
35
+ <div class="detail-kicker">
36
+ <span class="src" :class="`src-${store.selected.source}`">{{ store.selected.source }}</span>
37
+ <span>{{ store.selected.editor || store.selected.source }}</span>
38
+ <span>{{ store.selected.category || t('uncategorized') }}</span>
39
+ </div>
40
+ <h2>{{ i18n.skillText(store.selected, 'name') }}</h2>
41
+ <div class="badge-row">
42
+ <span
43
+ v-for="badge in detailBadges"
44
+ :key="badge"
45
+ class="badge"
46
+ :class="{ danger: badge === t('parseError'), warn: badge === t('redacted') }"
47
+ >{{ badge }}</span>
48
+ </div>
49
+ <p>{{ i18n.skillText(store.selected, 'description') }}</p>
50
+ </div>
51
+ </div>
52
+
53
+ <!-- ACTIONS -->
54
+ <div class="actions">
55
+ <button @click="store.copySelected('path', { copied: t('copied'), bytes: t('bytes') })" class="action-btn">💬 {{ t('copyPath') }}</button>
56
+ <button @click="store.copySelected('dir', { copied: t('copied'), bytes: t('bytes') })" class="action-btn">📂 {{ t('copyDir') }}</button>
57
+ <button @click="store.copySelected('rel', { copied: t('copied'), bytes: t('bytes') })" class="action-btn">🔗 {{ t('copyRel') }}</button>
58
+ <button @click="store.copySelected('prompt', { copied: t('copied'), bytes: t('bytes') })" class="action-btn">📋 {{ t('copyPrompt') }}</button>
59
+ <button @click="store.openSelected('cursor', { opened: t('opened') })" class="action-btn">🎯 {{ t('openCursor') }}</button>
60
+ <button @click="store.openSelected('finder', { opened: t('opened') })" class="action-btn">🔍 {{ t('reveal') }}</button>
61
+ </div>
62
+
63
+ <div v-if="store.notice" class="notice">{{ store.notice }}</div>
64
+
65
+ <!-- SCROLLABLE CONTENT -->
66
+ <div class="detail-scroll">
67
+ <!-- USAGE CARD -->
68
+ <section class="usage-card">
69
+ <div class="usage-head">
70
+ <span>{{ t('usage') }}</span>
71
+ <h3>{{ t('howToUse') }}</h3>
72
+ </div>
73
+ <div class="usage-prompt"><code>{{ usagePrompt }}</code></div>
74
+ <div class="usage-grid">
75
+ <div>
76
+ <strong>{{ t('appliesTo') }}</strong>
77
+ <p>{{ store.selected.description || '-' }}</p>
78
+ </div>
79
+ <div>
80
+ <strong>{{ t('quickFacts') }}</strong>
81
+ <p>{{ store.selected.editor || store.selected.source }} · {{ store.selected.kind }}<span v-if="store.selected.product"> · {{ store.selected.product }}</span></p>
82
+ </div>
83
+ </div>
84
+ <div class="usage-block" v-if="store.selected.params?.length">
85
+ <strong>{{ t('params') }}</strong>
86
+ <div class="param-table">
87
+ <div v-for="p in store.selected.params" :key="p.name">
88
+ <code>{{ p.name }}</code>
89
+ <span>{{ p.type || '-' }}</span>
90
+ <b v-if="p.required">{{ t('required') }}</b>
91
+ <p>{{ p.description || p.default || '-' }}</p>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ <div class="usage-block" v-if="store.selected.triggers?.length">
96
+ <strong>{{ t('triggers') }}</strong>
97
+ <span class="tag" v-for="tag in store.selected.triggers.slice(0, 12)" :key="tag">{{ tag }}</span>
98
+ </div>
99
+ <div class="usage-block" v-if="store.selected.links?.length">
100
+ <strong>{{ t('links') }}</strong>
101
+ <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>
102
+ </div>
103
+ </section>
104
+
105
+ <!-- METADATA -->
106
+ <dl class="kv">
107
+ <div>
108
+ <dt>{{ t('path') }}</dt>
109
+ <dd><code>{{ store.selected.paths?.abs }}</code></dd>
110
+ </div>
111
+ <div v-for="([label, value]) in detailMeta" :key="label">
112
+ <dt>{{ label }}</dt>
113
+ <dd>{{ value }}</dd>
114
+ </div>
115
+ <div v-if="store.selected.parseError" class="kv-error">
116
+ <dt>{{ t('parseError') }}</dt>
117
+ <dd>{{ store.selected.parseError }}</dd>
118
+ </div>
119
+ <div v-if="store.selected.triggers?.length">
120
+ <dt>{{ t('triggers') }}</dt>
121
+ <dd>
122
+ <span class="tag" v-for="trig in store.selected.triggers.slice(0, 8)" :key="trig">{{ trig }}</span>
123
+ </dd>
124
+ </div>
125
+ </dl>
126
+
127
+ <!-- MARKDOWN CONTENT -->
128
+ <article class="markdown" v-html="detailHtml"></article>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </template>
133
+
134
+ <style scoped>
135
+ .detail-panel-wrapper {
136
+ position: fixed;
137
+ right: 0;
138
+ top: 0;
139
+ width: 100%;
140
+ height: 100vh;
141
+ z-index: 100;
142
+ pointer-events: none;
143
+ }
144
+
145
+ .detail-panel-overlay {
146
+ position: absolute;
147
+ left: 0;
148
+ top: 0;
149
+ width: 100%;
150
+ height: 100%;
151
+ background: rgba(0, 0, 0, 0.25);
152
+ pointer-events: auto;
153
+ animation: fadeIn 0.2s ease-out;
154
+ }
155
+
156
+ .detail-panel {
157
+ position: absolute;
158
+ right: 0;
159
+ top: 0;
160
+ height: 100vh;
161
+ width: 380px;
162
+ background: white;
163
+ box-shadow: -4px 0 16px rgba(0, 0, 0, 0.15);
164
+ display: flex;
165
+ flex-direction: column;
166
+ pointer-events: auto;
167
+ animation: slideIn 0.3s ease-out;
168
+ }
169
+
170
+ @keyframes slideIn {
171
+ from {
172
+ transform: translateX(100%);
173
+ }
174
+ to {
175
+ transform: translateX(0);
176
+ }
177
+ }
178
+
179
+ @keyframes fadeIn {
180
+ from {
181
+ opacity: 0;
182
+ }
183
+ to {
184
+ opacity: 1;
185
+ }
186
+ }
187
+
188
+ .detail-close {
189
+ position: absolute;
190
+ top: 12px;
191
+ right: 12px;
192
+ width: 32px;
193
+ height: 32px;
194
+ border: 0;
195
+ border-radius: 8px;
196
+ background: #f3f4f6;
197
+ color: #6b7280;
198
+ font-size: 18px;
199
+ cursor: pointer;
200
+ z-index: 10;
201
+ transition: all 0.2s ease;
202
+ }
203
+
204
+ .detail-close:hover {
205
+ background: #e5e7eb;
206
+ color: #1f2937;
207
+ }
208
+
209
+ .detail-head {
210
+ flex: 0 0 auto;
211
+ padding: 16px 16px 0;
212
+ border-bottom: 1px solid #e5e7eb;
213
+ }
214
+
215
+ .detail-head > div > div:first-child {
216
+ margin-bottom: 8px;
217
+ }
218
+
219
+ .detail-kicker {
220
+ display: flex;
221
+ gap: 6px;
222
+ align-items: center;
223
+ font-size: 11px;
224
+ margin-bottom: 4px;
225
+ }
226
+
227
+ .detail-kicker span {
228
+ padding: 2px 6px;
229
+ border-radius: 4px;
230
+ background: #f3f4f6;
231
+ color: #6b7280;
232
+ }
233
+
234
+ .detail-kicker .src {
235
+ background: none;
236
+ font-weight: 600;
237
+ }
238
+
239
+ .detail-head h2 {
240
+ margin: 0;
241
+ font-size: 18px;
242
+ margin-bottom: 8px;
243
+ }
244
+
245
+ .badge-row {
246
+ display: flex;
247
+ gap: 4px;
248
+ margin-bottom: 8px;
249
+ flex-wrap: wrap;
250
+ }
251
+
252
+ .badge {
253
+ display: inline-block;
254
+ padding: 2px 8px;
255
+ border-radius: 4px;
256
+ font-size: 11px;
257
+ font-weight: 600;
258
+ background: #e0e7ff;
259
+ color: #3730a3;
260
+ }
261
+
262
+ .badge.danger {
263
+ background: #fee2e2;
264
+ color: #b91c1c;
265
+ }
266
+
267
+ .badge.warn {
268
+ background: #fef3c7;
269
+ color: #b45309;
270
+ }
271
+
272
+ .detail-head p {
273
+ margin: 0;
274
+ font-size: 13px;
275
+ color: #4b5563;
276
+ line-height: 1.5;
277
+ }
278
+
279
+ .actions {
280
+ flex: 0 0 auto;
281
+ padding: 12px 16px;
282
+ border-bottom: 1px solid #e5e7eb;
283
+ display: grid;
284
+ grid-template-columns: repeat(3, 1fr);
285
+ gap: 6px;
286
+ }
287
+
288
+ .action-btn {
289
+ padding: 8px 12px;
290
+ font-size: 12px;
291
+ border: 1px solid #d1d5db;
292
+ border-radius: 8px;
293
+ background: white;
294
+ color: #374151;
295
+ cursor: pointer;
296
+ transition: all 0.2s ease;
297
+ white-space: nowrap;
298
+ }
299
+
300
+ .action-btn:hover {
301
+ background: #f9fafb;
302
+ border-color: #9ca3af;
303
+ }
304
+
305
+ .notice {
306
+ flex: 0 0 auto;
307
+ padding: 8px 16px;
308
+ background: #fef3c7;
309
+ border-bottom: 1px solid #fcd34d;
310
+ font-size: 12px;
311
+ color: #92400e;
312
+ }
313
+
314
+ .detail-scroll {
315
+ flex: 1 1 auto;
316
+ overflow-y: auto;
317
+ padding: 16px;
318
+ }
319
+
320
+ .usage-card {
321
+ margin-bottom: 24px;
322
+ }
323
+
324
+ .usage-head {
325
+ margin-bottom: 12px;
326
+ }
327
+
328
+ .usage-head span {
329
+ display: block;
330
+ font-size: 11px;
331
+ color: #9ca3af;
332
+ text-transform: uppercase;
333
+ letter-spacing: 0.08em;
334
+ margin-bottom: 4px;
335
+ }
336
+
337
+ .usage-head h3 {
338
+ margin: 0;
339
+ font-size: 14px;
340
+ font-weight: 600;
341
+ }
342
+
343
+ .usage-prompt {
344
+ margin-bottom: 12px;
345
+ padding: 10px 12px;
346
+ background: #f3f4f6;
347
+ border-radius: 8px;
348
+ border-left: 3px solid #8b5cf6;
349
+ }
350
+
351
+ .usage-prompt code {
352
+ font-size: 11px;
353
+ color: #374151;
354
+ word-break: break-word;
355
+ }
356
+
357
+ .usage-grid {
358
+ display: grid;
359
+ grid-template-columns: 1fr 1fr;
360
+ gap: 8px;
361
+ margin-bottom: 12px;
362
+ }
363
+
364
+ .usage-grid > div {
365
+ padding: 8px 10px;
366
+ background: #f9fafb;
367
+ border-radius: 6px;
368
+ }
369
+
370
+ .usage-grid strong {
371
+ display: block;
372
+ font-size: 11px;
373
+ color: #6b7280;
374
+ margin-bottom: 4px;
375
+ }
376
+
377
+ .usage-grid p {
378
+ margin: 0;
379
+ font-size: 12px;
380
+ color: #374151;
381
+ line-height: 1.4;
382
+ }
383
+
384
+ .usage-block {
385
+ margin-bottom: 12px;
386
+ }
387
+
388
+ .usage-block strong {
389
+ display: block;
390
+ font-size: 12px;
391
+ margin-bottom: 6px;
392
+ }
393
+
394
+ .tag {
395
+ display: inline-block;
396
+ padding: 3px 8px;
397
+ margin: 2px 4px 2px 0;
398
+ border-radius: 4px;
399
+ background: #e0e7ff;
400
+ color: #3730a3;
401
+ font-size: 11px;
402
+ }
403
+
404
+ .param-table {
405
+ display: grid;
406
+ gap: 8px;
407
+ font-size: 12px;
408
+ }
409
+
410
+ .param-table > div {
411
+ padding: 8px;
412
+ background: #f9fafb;
413
+ border-radius: 6px;
414
+ }
415
+
416
+ .param-table code {
417
+ display: block;
418
+ font-size: 11px;
419
+ color: #dc2626;
420
+ margin-bottom: 2px;
421
+ }
422
+
423
+ .param-table b {
424
+ display: inline-block;
425
+ padding: 1px 4px;
426
+ background: #fef3c7;
427
+ color: #b45309;
428
+ border-radius: 2px;
429
+ font-size: 10px;
430
+ margin-left: 4px;
431
+ }
432
+
433
+ .usage-link {
434
+ display: block;
435
+ padding: 6px 0;
436
+ font-size: 12px;
437
+ color: #0ea5e9;
438
+ text-decoration: none;
439
+ border-bottom: 1px solid #e0e7ff;
440
+ word-break: break-word;
441
+ }
442
+
443
+ .usage-link:hover {
444
+ color: #0284c7;
445
+ }
446
+
447
+ .kv {
448
+ margin-bottom: 24px;
449
+ }
450
+
451
+ .kv > div {
452
+ display: grid;
453
+ grid-template-columns: 140px 1fr;
454
+ gap: 12px;
455
+ margin-bottom: 10px;
456
+ padding-bottom: 10px;
457
+ border-bottom: 1px solid #f3f4f6;
458
+ font-size: 12px;
459
+ }
460
+
461
+ .kv dt {
462
+ font-weight: 600;
463
+ color: #6b7280;
464
+ word-break: break-word;
465
+ }
466
+
467
+ .kv dd {
468
+ margin: 0;
469
+ color: #374151;
470
+ word-break: break-word;
471
+ }
472
+
473
+ .kv code {
474
+ display: block;
475
+ padding: 6px;
476
+ background: #f3f4f6;
477
+ border-radius: 4px;
478
+ font-size: 11px;
479
+ color: #374151;
480
+ overflow-x: auto;
481
+ }
482
+
483
+ .kv-error dd {
484
+ color: #b91c1c;
485
+ }
486
+
487
+ .markdown {
488
+ margin-bottom: 20px;
489
+ font-size: 13px;
490
+ line-height: 1.6;
491
+ color: #374151;
492
+ }
493
+
494
+ .markdown h1, .markdown h2, .markdown h3 {
495
+ margin-top: 16px;
496
+ margin-bottom: 8px;
497
+ font-weight: 600;
498
+ }
499
+
500
+ .markdown p {
501
+ margin: 8px 0;
502
+ }
503
+
504
+ .markdown code {
505
+ background: #f3f4f6;
506
+ padding: 2px 4px;
507
+ border-radius: 3px;
508
+ font-size: 11px;
509
+ color: #dc2626;
510
+ }
511
+
512
+ .markdown pre {
513
+ background: #1f2937;
514
+ padding: 10px;
515
+ border-radius: 6px;
516
+ overflow-x: auto;
517
+ margin: 8px 0;
518
+ }
519
+
520
+ .markdown pre code {
521
+ background: none;
522
+ color: #e5e7eb;
523
+ padding: 0;
524
+ }
525
+
526
+ /* 响应式 - 平板 */
527
+ @media (max-width: 1199px) {
528
+ .detail-panel {
529
+ width: 320px;
530
+ }
531
+ }
532
+
533
+ /* 响应式 - 手机 */
534
+ @media (max-width: 767px) {
535
+ .detail-panel {
536
+ width: 100%;
537
+ }
538
+
539
+ .actions {
540
+ grid-template-columns: repeat(2, 1fr);
541
+ }
542
+
543
+ .usage-grid {
544
+ grid-template-columns: 1fr;
545
+ }
546
+
547
+ .kv > div {
548
+ grid-template-columns: 1fr;
549
+ }
550
+ }
551
+ </style>
@@ -11,6 +11,9 @@ const messages = {
11
11
  product: '产品',
12
12
  category: '分类',
13
13
  brand: '品牌',
14
+ directory: '目录',
15
+ application: '应用',
16
+ view: '视图',
14
17
  filters: '筛选',
15
18
  active: '已启用',
16
19
  selectedEditor: '当前编辑器',
@@ -75,6 +78,9 @@ const messages = {
75
78
  product: 'Product',
76
79
  category: 'Category',
77
80
  brand: 'Brand',
81
+ directory: 'Directory',
82
+ application: 'Application',
83
+ view: 'View',
78
84
  filters: 'Filters',
79
85
  active: 'active',
80
86
  selectedEditor: 'Selected editor',
@@ -23,6 +23,10 @@ export const useSkillsStore = defineStore('skills', {
23
23
  brand: '',
24
24
  },
25
25
  notice: '',
26
+ // 目录树相关状态
27
+ directoryTree: null,
28
+ selectedPaths: new Set(),
29
+ expandedPaths: new Set(),
26
30
  }),
27
31
 
28
32
  getters: {
@@ -62,6 +66,15 @@ export const useSkillsStore = defineStore('skills', {
62
66
  if (product) arr = arr.filter(x => x.product === product);
63
67
  if (brand) arr = arr.filter(x => x.brand === brand);
64
68
 
69
+ // 目录路径过滤
70
+ if (state.selectedPaths.size > 0) {
71
+ arr = arr.filter(x => {
72
+ return Array.from(state.selectedPaths).some(path =>
73
+ x.paths?.abs?.startsWith(path)
74
+ );
75
+ });
76
+ }
77
+
65
78
  const parsed = parseQuery(state.query);
66
79
  arr = applyStructuredFilters(arr, parsed.filters);
67
80
  const q = parsed.text;
@@ -113,6 +126,8 @@ export const useSkillsStore = defineStore('skills', {
113
126
  applySnapshot(skills, stats) {
114
127
  this.skills = skills;
115
128
  this.stats = stats;
129
+ // 构建目录树
130
+ this.buildDirectory();
116
131
  },
117
132
  async refreshSnapshot() {
118
133
  const currentId = this.selectedId;
@@ -206,6 +221,43 @@ export const useSkillsStore = defineStore('skills', {
206
221
  if (this.notice === msg) this.notice = '';
207
222
  }, 1600);
208
223
  },
224
+ // 目录树相关 actions
225
+ buildDirectory() {
226
+ this.directoryTree = buildDirectoryTree(this.skills);
227
+ },
228
+ togglePath(path) {
229
+ if (this.expandedPaths.has(path)) {
230
+ this.expandedPaths.delete(path);
231
+ } else {
232
+ this.expandedPaths.add(path);
233
+ }
234
+ },
235
+ expandAllPaths() {
236
+ function walk(node) {
237
+ this.expandedPaths.add(node.path);
238
+ if (node.children) {
239
+ for (const child of node.children.values()) {
240
+ walk.call(this, child);
241
+ }
242
+ }
243
+ }
244
+ if (this.directoryTree) {
245
+ walk.call(this, this.directoryTree);
246
+ }
247
+ },
248
+ collapseAllPaths() {
249
+ this.expandedPaths.clear();
250
+ },
251
+ toggleDirectoryPath(path) {
252
+ if (this.selectedPaths.has(path)) {
253
+ this.selectedPaths.delete(path);
254
+ } else {
255
+ this.selectedPaths.add(path);
256
+ }
257
+ },
258
+ clearDirectoryPaths() {
259
+ this.selectedPaths.clear();
260
+ },
209
261
  },
210
262
  });
211
263
 
@@ -285,3 +337,78 @@ function sortItems(items, sortKey) {
285
337
  return arr.sort((a, b) => `${text(a.editor || a.source)}\u0000${text(a.name)}`.localeCompare(`${text(b.editor || b.source)}\u0000${text(b.name)}`, 'zh-CN'));
286
338
  }
287
339
  }
340
+
341
+ /**
342
+ * 构建目录树
343
+ */
344
+ function buildDirectoryTree(skills) {
345
+ const root = {
346
+ path: '/',
347
+ name: '/',
348
+ children: new Map(),
349
+ skills: [],
350
+ depth: 0,
351
+ };
352
+
353
+ for (const skill of skills) {
354
+ const absPath = skill.paths?.abs;
355
+ if (!absPath) continue;
356
+
357
+ const parts = absPath.split('/').filter(Boolean);
358
+ if (parts.length < 1) continue;
359
+
360
+ let current = root;
361
+
362
+ for (let i = 0; i < parts.length - 1; i++) {
363
+ const part = parts[i];
364
+ const pathSoFar = '/' + parts.slice(0, i + 1).join('/');
365
+
366
+ if (!current.children.has(part)) {
367
+ current.children.set(part, {
368
+ path: pathSoFar,
369
+ name: part,
370
+ children: new Map(),
371
+ skills: [],
372
+ depth: i + 1,
373
+ });
374
+ }
375
+
376
+ current = current.children.get(part);
377
+ }
378
+
379
+ current.skills.push(skill);
380
+ }
381
+
382
+ return root;
383
+ }
384
+
385
+ /**
386
+ * 获取顶级目录
387
+ */
388
+ function getTopLevelDirectories(tree) {
389
+ const dirs = [];
390
+ const seen = new Set();
391
+
392
+ function walk(node) {
393
+ if (!node.children) return;
394
+ for (const [name, child] of node.children) {
395
+ if (!seen.has(child.path)) {
396
+ seen.add(child.path);
397
+ dirs.push(child);
398
+ }
399
+ if (child.depth <= 2) {
400
+ walk(child);
401
+ }
402
+ }
403
+ }
404
+
405
+ walk(tree);
406
+ return dirs;
407
+ }
408
+
409
+ /**
410
+ * 计算路径下的技能数量
411
+ */
412
+ function countSkillsInPath(path, skills) {
413
+ return skills.filter(s => s.paths?.abs?.startsWith(path)).length;
414
+ }