huhaa-myskills 0.2.13 → 0.3.4

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.
Files changed (121) hide show
  1. package/README.md +9 -375
  2. package/bin/huhaa-myskills.mjs +194 -12
  3. package/{scripts → build}/verify.mjs +3 -1
  4. package/docs/Frontend-Spec.md +1149 -0
  5. package/docs/assets/layout-wireframe.png +0 -0
  6. package/docs/assets/theme-reference.png +0 -0
  7. package/docs/scan_rules.md +172 -0
  8. package/package.json +33 -9
  9. package/packages/scanner/src/adapters/directory-skill.mjs +239 -0
  10. package/packages/scanner/src/adapters/skill-adapter.mjs +345 -0
  11. package/packages/scanner/src/icon/brand-map.mjs +84 -0
  12. package/packages/scanner/src/icon/icon-extractor.mjs +246 -0
  13. package/packages/scanner/src/index.mjs +107 -82
  14. package/packages/scanner/src/types.d.ts +18 -0
  15. package/packages/scanner/test/icon.test.mjs +61 -0
  16. package/packages/scanner/test/scanner.test.mjs +37 -41
  17. package/packages/scanner/test/skill-adapter.test.mjs +70 -0
  18. package/packages/scanner/tests/directory-skill.test.mjs +93 -0
  19. package/packages/scanner/tests/scanner-integration.test.mjs +139 -0
  20. package/packages/server/src/index.mjs +164 -12
  21. package/packages/server/src/labels.mjs +62 -0
  22. package/packages/server/test/server.test.mjs +1 -1
  23. package/packages/web/README.md +33 -2
  24. package/packages/web/dist/assets/index-BCY2cRaO.js +175 -0
  25. package/packages/web/dist/assets/index-DALKjJPi.css +1 -0
  26. package/packages/web/dist/favicon-16x16.png +0 -0
  27. package/packages/web/dist/favicon-192x192.png +0 -0
  28. package/packages/web/dist/favicon-32x32.png +0 -0
  29. package/packages/web/dist/favicon-512x512.png +0 -0
  30. package/packages/web/dist/favicon-source.svg +5 -0
  31. package/packages/web/dist/index.html +23 -9
  32. package/packages/web/index.html +22 -8
  33. package/packages/web/package-lock.json +2989 -0
  34. package/packages/web/package.json +21 -8
  35. package/packages/web/postcss.config.js +6 -0
  36. package/packages/web/public/favicon-16x16.png +0 -0
  37. package/packages/web/public/favicon-192x192.png +0 -0
  38. package/packages/web/public/favicon-32x32.png +0 -0
  39. package/packages/web/public/favicon-512x512.png +0 -0
  40. package/packages/web/public/favicon-source.svg +5 -0
  41. package/packages/web/src/App.test.ts +40 -0
  42. package/packages/web/src/App.tsx +177 -0
  43. package/packages/web/src/components/ComingSoon.tsx +14 -0
  44. package/packages/web/src/components/layout/Sidebar.test.tsx +74 -0
  45. package/packages/web/src/components/layout/Sidebar.tsx +114 -0
  46. package/packages/web/src/components/layout/Topbar.tsx +74 -0
  47. package/packages/web/src/components/ui/ActionButton.tsx +48 -0
  48. package/packages/web/src/components/ui/button.tsx +48 -0
  49. package/packages/web/src/components/ui/card.tsx +45 -0
  50. package/packages/web/src/components/views/DashboardView.tsx +122 -0
  51. package/packages/web/src/components/views/OtherSkillsView.test.tsx +126 -0
  52. package/packages/web/src/components/views/OtherSkillsView.tsx +306 -0
  53. package/packages/web/src/components/views/SettingsView.tsx +95 -0
  54. package/packages/web/src/components/views/SkillDetail.test.tsx +57 -0
  55. package/packages/web/src/components/views/SkillDetail.tsx +81 -0
  56. package/packages/web/src/components/views/SkillsView.test.ts +28 -0
  57. package/packages/web/src/components/views/SkillsView.tsx +226 -0
  58. package/packages/web/src/hooks/QUICK_REFERENCE.md +165 -0
  59. package/packages/web/src/hooks/README.md +378 -0
  60. package/packages/web/src/hooks/__tests__/useOtherSkills.test.ts +422 -0
  61. package/packages/web/src/hooks/__tests__/useSkillIcons.test.ts +272 -0
  62. package/packages/web/src/hooks/useLiveReload.ts +20 -0
  63. package/packages/web/src/hooks/useOtherSkills.ts +387 -0
  64. package/packages/web/src/hooks/useSkillIcons.ts +248 -0
  65. package/packages/web/src/hooks/useTheme.ts +36 -0
  66. package/packages/web/src/index.css +161 -0
  67. package/packages/web/src/lib/api.test.ts +47 -0
  68. package/packages/web/src/lib/api.ts +77 -0
  69. package/packages/web/src/lib/cn.ts +7 -0
  70. package/packages/web/src/lib/editors.test.ts +30 -0
  71. package/packages/web/src/lib/editors.ts +70 -0
  72. package/packages/web/src/lib/markdown.test.ts +27 -0
  73. package/packages/web/src/lib/markdown.ts +14 -0
  74. package/packages/web/src/main.tsx +13 -0
  75. package/packages/web/src/types/index.ts +62 -0
  76. package/packages/web/src/types/other-skill.ts +88 -0
  77. package/packages/web/src/types/skill.ts +136 -0
  78. package/packages/web/src/types.ts +47 -0
  79. package/packages/web/src/vite-env.d.ts +1 -0
  80. package/packages/web/tailwind.config.ts +66 -0
  81. package/packages/web/tsconfig.json +28 -0
  82. package/packages/web/vite.config.ts +36 -0
  83. package/packages/web/vitest.config.ts +21 -0
  84. package/packages/web/vitest.setup.ts +1 -0
  85. package/scripts/activate-data-sources.sh +223 -0
  86. package/scripts/brand-completion.py +586 -0
  87. package/scripts/init-nextjs-project.sh +421 -0
  88. package/scripts/install-and-sync.sh +209 -0
  89. package/scripts/start-dev.sh +72 -0
  90. package/docs/GUIDE.md +0 -190
  91. package/docs/PLAN.md +0 -359
  92. package/docs/RULES.md +0 -329
  93. package/docs/RUNBOOK-myskills.md +0 -258
  94. package/docs/SYNC-SKILLS.md +0 -259
  95. package/docs/releases/README.md +0 -47
  96. package/docs/releases/v0.1.0.md +0 -256
  97. package/docs/releases/v0.1.1.md +0 -297
  98. package/docs/releases/v0.1.2.md +0 -207
  99. package/docs/releases/v0.1.3.md +0 -269
  100. package/docs/todo/v0.1.2.md +0 -98
  101. package/docs/todo/v0.1.3.md +0 -40
  102. package/docs/todo/v0.1.4.md +0 -36
  103. package/docs/todo/v0.1.5.md +0 -41
  104. package/docs/todo/v0.1.6.md +0 -43
  105. package/docs/todo/v0.1.7.md +0 -38
  106. package/docs/todo/v0.1.8.md +0 -36
  107. package/docs/todo/v0.1.9.md +0 -54
  108. package/packages/web/dist/assets/index-BPepSb3q.js +0 -36
  109. package/packages/web/dist/assets/index-CIGkgT5w.css +0 -1
  110. package/packages/web/src/App.vue +0 -483
  111. package/packages/web/src/components/AppTree.vue +0 -230
  112. package/packages/web/src/components/DirectoryTree.vue +0 -263
  113. package/packages/web/src/components/SkillTree.vue +0 -208
  114. package/packages/web/src/lib/api.js +0 -43
  115. package/packages/web/src/main.js +0 -6
  116. package/packages/web/src/stores/i18n.js +0 -161
  117. package/packages/web/src/stores/skills.js +0 -287
  118. package/packages/web/src/styles.css +0 -365
  119. package/packages/web/vite.config.js +0 -17
  120. /package/{scripts → build}/build-web.mjs +0 -0
  121. /package/{scripts → build}/prepare-publish.mjs +0 -0
@@ -1,230 +0,0 @@
1
- <script setup>
2
- import { computed, reactive } from 'vue';
3
- import { useSkillsStore } from '../stores/skills.js';
4
-
5
- const store = useSkillsStore();
6
- const treeState = reactive(new Map());
7
-
8
- const appGroups = computed(() => {
9
- const groups = new Map();
10
-
11
- for (const skill of store.filtered) {
12
- const app = skill.editor || skill.source || 'Other';
13
- if (!groups.has(app)) groups.set(app, []);
14
- groups.get(app).push(skill);
15
- }
16
-
17
- return [...groups.entries()].sort((a, b) => a[0].localeCompare(b[0], 'zh-CN'));
18
- });
19
-
20
- function toggleApp(app) {
21
- treeState.set(app, !treeState.get(app));
22
- }
23
-
24
- function getAppIcon(app) {
25
- const icons = {
26
- 'cursor': '🖱️',
27
- 'vs-code': '⚙️',
28
- 'vscode': '⚙️',
29
- 'code': '⚙️',
30
- 'vim': '🎹',
31
- 'neovim': '🎹',
32
- 'nvim': '🎹',
33
- 'emacs': '📝',
34
- 'sublime': '🎨',
35
- 'sublime-text': '🎨',
36
- 'zed': '⚡',
37
- 'helix': '⚡',
38
- 'hermes': '🤖',
39
- 'claude-code': '💬',
40
- 'codex': '📖',
41
- 'atom': '⚛️',
42
- 'jetbrains': '🦾',
43
- 'kate': '📄',
44
- 'gedit': '📝',
45
- 'textmate': '🎸',
46
- 'bbedit': '✏️',
47
- 'openclaw': '🦾',
48
- 'windsurf': '🌊',
49
- 'trae': '🎯',
50
- 'trae-cn': '🎯',
51
- };
52
-
53
- const lower = String(app || '').toLowerCase();
54
- for (const [key, icon] of Object.entries(icons)) {
55
- if (lower.includes(key)) return icon;
56
- }
57
- return '📦';
58
- }
59
- </script>
60
-
61
- <template>
62
- <div class="app-tree">
63
- <div v-for="[app, skills] in appGroups" :key="app" class="tree-group">
64
- <button
65
- class="tree-group-header"
66
- :class="{ expanded: treeState.get(app) }"
67
- @click="toggleApp(app)"
68
- :title="`${app} (${skills.length})`"
69
- >
70
- <span class="tree-arrow">{{ treeState.get(app) ? '▼' : '▶' }}</span>
71
- <span class="app-icon">{{ getAppIcon(app) }}</span>
72
- <span class="app-name">{{ app }}</span>
73
- <span class="skill-count">{{ skills.length }}</span>
74
- </button>
75
-
76
- <div v-if="treeState.get(app)" class="tree-group-items">
77
- <button
78
- v-for="skill in skills"
79
- :key="skill.id"
80
- class="skill-tree-item"
81
- :class="{ active: skill.id === store.selectedId }"
82
- @click="store.loadDetail(skill.id)"
83
- :title="skill.description"
84
- >
85
- <span class="icon">{{ getAppIcon(skill.source) }}</span>
86
- <span class="name">{{ skill.name }}</span>
87
- <span v-if="skill.source" class="source-tag">{{ skill.source }}</span>
88
- </button>
89
- </div>
90
- </div>
91
-
92
- <div v-if="appGroups.length === 0" class="empty-tree">
93
- <p>没有找到匹配的技能</p>
94
- </div>
95
- </div>
96
- </template>
97
-
98
- <style scoped>
99
- .app-tree {
100
- display: flex;
101
- flex-direction: column;
102
- overflow-y: auto;
103
- font-size: 13px;
104
- user-select: none;
105
- }
106
-
107
- .tree-group {
108
- border-bottom: 1px solid #f0f0f0;
109
- }
110
-
111
- .tree-group-header {
112
- width: 100%;
113
- padding: 8px 10px;
114
- background: #f8f8f8;
115
- border: none;
116
- border-bottom: 1px solid #e8e8e8;
117
- font-weight: 600;
118
- cursor: pointer;
119
- display: flex;
120
- align-items: center;
121
- gap: 6px;
122
- font-size: 12px;
123
- text-align: left;
124
- justify-content: flex-start;
125
- transition: all 0.15s ease;
126
- }
127
-
128
- .tree-group-header:hover {
129
- background: #f0f0f0;
130
- }
131
-
132
- .tree-group-header.expanded {
133
- background: #e8e8e8;
134
- }
135
-
136
- .tree-arrow {
137
- width: 16px;
138
- text-align: center;
139
- flex-shrink: 0;
140
- font-size: 11px;
141
- color: #666;
142
- }
143
-
144
- .app-icon {
145
- width: 16px;
146
- text-align: center;
147
- flex-shrink: 0;
148
- font-size: 14px;
149
- }
150
-
151
- .app-name {
152
- flex: 1;
153
- overflow: hidden;
154
- text-overflow: ellipsis;
155
- white-space: nowrap;
156
- }
157
-
158
- .skill-count {
159
- margin-left: auto;
160
- color: #999;
161
- font-size: 11px;
162
- font-weight: normal;
163
- flex-shrink: 0;
164
- }
165
-
166
- .tree-group-items {
167
- background: #fafafa;
168
- display: flex;
169
- flex-direction: column;
170
- }
171
-
172
- .skill-tree-item {
173
- display: flex;
174
- align-items: center;
175
- gap: 6px;
176
- width: 100%;
177
- padding: 6px 16px;
178
- border: none;
179
- background: transparent;
180
- cursor: pointer;
181
- font-size: 12px;
182
- text-align: left;
183
- transition: background-color 0.1s ease;
184
- color: inherit;
185
- justify-content: flex-start;
186
- }
187
-
188
- .skill-tree-item:hover {
189
- background: #f0f0f0;
190
- }
191
-
192
- .skill-tree-item.active {
193
- background: #e3f2fd;
194
- color: #1976d2;
195
- font-weight: 500;
196
- }
197
-
198
- .icon {
199
- width: 16px;
200
- text-align: center;
201
- flex-shrink: 0;
202
- font-size: 12px;
203
- }
204
-
205
- .name {
206
- flex: 1;
207
- overflow: hidden;
208
- text-overflow: ellipsis;
209
- white-space: nowrap;
210
- }
211
-
212
- .source-tag {
213
- margin-left: 4px;
214
- padding: 1px 4px;
215
- background: #f0f0f0;
216
- border-radius: 2px;
217
- font-size: 10px;
218
- color: #666;
219
- flex-shrink: 0;
220
- }
221
-
222
- .empty-tree {
223
- display: flex;
224
- align-items: center;
225
- justify-content: center;
226
- height: 100px;
227
- color: #999;
228
- font-size: 12px;
229
- }
230
- </style>
@@ -1,263 +0,0 @@
1
- <script setup>
2
- import { computed, reactive, ref } from 'vue';
3
- import { useSkillsStore } from '../stores/skills.js';
4
-
5
- const store = useSkillsStore();
6
- const expandedPaths = reactive(new Set());
7
- const selectedPath = ref(null);
8
-
9
- const directoryTree = computed(() => {
10
- const tree = new Map();
11
-
12
- for (const skill of store.filtered) {
13
- if (!skill.paths?.abs) continue;
14
-
15
- const parts = skill.paths.abs.split('/');
16
- let current = tree;
17
-
18
- for (let i = 0; i < parts.length - 1; i++) {
19
- const part = parts[i];
20
- if (!current.has(part)) {
21
- current.set(part, new Map());
22
- }
23
- current = current.get(part);
24
- }
25
-
26
- if (!current.has('__skills__')) {
27
- current.set('__skills__', []);
28
- }
29
- current.get('__skills__').push(skill);
30
- }
31
-
32
- return tree;
33
- });
34
-
35
- function togglePath(path) {
36
- if (expandedPaths.has(path)) {
37
- expandedPaths.delete(path);
38
- } else {
39
- expandedPaths.add(path);
40
- }
41
- }
42
-
43
- function selectPath(path) {
44
- selectedPath.value = selectedPath.value === path ? null : path;
45
- }
46
-
47
- function countSkills(tree) {
48
- let count = 0;
49
- for (const [key, value] of tree.entries()) {
50
- if (key === '__skills__') {
51
- count += value.length;
52
- } else if (value instanceof Map) {
53
- count += countSkills(value);
54
- }
55
- }
56
- return count;
57
- }
58
-
59
- function getSkillsInPath(path) {
60
- let current = directoryTree.value;
61
- const parts = path.split('/');
62
-
63
- for (const part of parts) {
64
- if (!current.has(part)) return [];
65
- current = current.get(part);
66
- }
67
-
68
- return current.get('__skills__') || [];
69
- }
70
-
71
- const flatTree = computed(() => {
72
- const dirs = [];
73
-
74
- function traverse(tree, depth = 0, pathPrefix = '') {
75
- for (const [key, value] of tree.entries()) {
76
- if (key === '__skills__') continue;
77
-
78
- const currentPath = pathPrefix ? `${pathPrefix}/${key}` : key;
79
- const isExpanded = expandedPaths.has(currentPath);
80
-
81
- dirs.push({
82
- name: key,
83
- path: currentPath,
84
- depth,
85
- expanded: isExpanded,
86
- skillCount: countSkills(value),
87
- });
88
-
89
- if (isExpanded) {
90
- traverse(value, depth + 1, currentPath);
91
- }
92
- }
93
- }
94
-
95
- traverse(directoryTree.value);
96
- return dirs;
97
- });
98
-
99
- function getPaddingLeft(depth) {
100
- return `${depth * 16 + 8}px`;
101
- }
102
-
103
- function getSkillPaddingLeft(depth) {
104
- return `${(depth + 1) * 16 + 24}px`;
105
- }
106
- </script>
107
-
108
- <template>
109
- <div class="directory-tree">
110
- <div v-if="store.filtered.length === 0" class="empty-tree">
111
- <p>没有找到匹配的技能</p>
112
- </div>
113
-
114
- <template v-else>
115
- <!-- Directory structure -->
116
- <div v-for="dir in flatTree" :key="dir.path" class="tree-item">
117
- <button
118
- class="tree-dir"
119
- :style="{ paddingLeft: getPaddingLeft(dir.depth) }"
120
- :class="{ expanded: dir.expanded, selected: dir.path === selectedPath }"
121
- @click="selectPath(dir.path)"
122
- @dblclick="togglePath(dir.path)"
123
- :title="`单击选择,双击展开`"
124
- >
125
- <span class="tree-arrow">{{ dir.expanded ? '▼' : '▶' }}</span>
126
- <span class="tree-icon">📁</span>
127
- <span class="tree-name">{{ dir.name }}</span>
128
- <span class="tree-count">{{ dir.skillCount }}</span>
129
- </button>
130
-
131
- <!-- Skills in this directory -->
132
- <div v-if="dir.expanded" class="tree-skills">
133
- <button
134
- v-for="skill in getSkillsInPath(dir.path)"
135
- :key="skill.id"
136
- class="tree-skill-item"
137
- :style="{ paddingLeft: getSkillPaddingLeft(dir.depth) }"
138
- :class="{ active: skill.id === store.selectedId }"
139
- @click.stop="store.loadDetail(skill.id)"
140
- :title="skill.description"
141
- >
142
- <span class="tree-icon">📄</span>
143
- <span class="tree-name">{{ skill.name }}</span>
144
- </button>
145
- </div>
146
- </div>
147
- </template>
148
- </div>
149
- </template>
150
-
151
- <style scoped>
152
- .directory-tree {
153
- display: flex;
154
- flex-direction: column;
155
- overflow-y: auto;
156
- font-size: 13px;
157
- user-select: none;
158
- }
159
-
160
- .tree-item {
161
- display: flex;
162
- flex-direction: column;
163
- }
164
-
165
- .tree-dir {
166
- padding: 6px 10px;
167
- background: transparent;
168
- border: none;
169
- cursor: pointer;
170
- display: flex;
171
- align-items: center;
172
- gap: 6px;
173
- font-size: 12px;
174
- text-align: left;
175
- color: inherit;
176
- transition: background-color 0.1s ease;
177
- width: 100%;
178
- justify-content: flex-start;
179
- }
180
-
181
- .tree-dir:hover {
182
- background: #f0f0f0;
183
- }
184
-
185
- .tree-dir.selected {
186
- background: #e8f4f8;
187
- color: #0d5a7c;
188
- font-weight: 500;
189
- }
190
-
191
- .tree-arrow {
192
- width: 16px;
193
- text-align: center;
194
- flex-shrink: 0;
195
- font-size: 10px;
196
- color: #666;
197
- }
198
-
199
- .tree-icon {
200
- width: 16px;
201
- text-align: center;
202
- flex-shrink: 0;
203
- font-size: 12px;
204
- }
205
-
206
- .tree-name {
207
- flex: 1;
208
- overflow: hidden;
209
- text-overflow: ellipsis;
210
- white-space: nowrap;
211
- font-weight: 500;
212
- }
213
-
214
- .tree-count {
215
- margin-left: auto;
216
- padding: 0 4px;
217
- background: #e8e8e8;
218
- border-radius: 2px;
219
- font-size: 10px;
220
- color: #666;
221
- flex-shrink: 0;
222
- }
223
-
224
- .tree-skills {
225
- display: flex;
226
- flex-direction: column;
227
- }
228
-
229
- .tree-skill-item {
230
- padding: 6px 10px;
231
- background: transparent;
232
- border: none;
233
- cursor: pointer;
234
- display: flex;
235
- align-items: center;
236
- gap: 6px;
237
- font-size: 12px;
238
- text-align: left;
239
- color: inherit;
240
- transition: background-color 0.1s ease;
241
- width: 100%;
242
- justify-content: flex-start;
243
- }
244
-
245
- .tree-skill-item:hover {
246
- background: #f5f5f5;
247
- }
248
-
249
- .tree-skill-item.active {
250
- background: #e3f2fd;
251
- color: #1976d2;
252
- font-weight: 500;
253
- }
254
-
255
- .empty-tree {
256
- display: flex;
257
- align-items: center;
258
- justify-content: center;
259
- height: 100px;
260
- color: #999;
261
- font-size: 12px;
262
- }
263
- </style>
@@ -1,208 +0,0 @@
1
- <script setup>
2
- import { computed, reactive } from 'vue';
3
- import { useSkillsStore } from '../stores/skills.js';
4
-
5
- const store = useSkillsStore();
6
- const treeState = reactive(new Map());
7
-
8
- const sourceGroups = computed(() => {
9
- const groups = new Map();
10
- for (const skill of store.filtered) {
11
- const source = skill.source || 'other';
12
- if (!groups.has(source)) groups.set(source, []);
13
- groups.get(source).push(skill);
14
- }
15
- return [...groups.entries()].sort((a, b) => a[0].localeCompare(b[0], 'zh-CN'));
16
- });
17
-
18
- function toggleGroup(source) {
19
- treeState.set(source, !treeState.get(source));
20
- }
21
-
22
- function getSourceIcon(source) {
23
- const icons = {
24
- 'hermes': '🤖',
25
- 'claude-code': '💬',
26
- 'cursor': '🖱️',
27
- 'mcp-config': '⚙️',
28
- 'mcp': '⚙️',
29
- 'codex': '📖',
30
- 'project-runbook': '📚',
31
- 'hermes-plugin': '🔌',
32
- 'skills': '✨',
33
- };
34
- return icons[source] || '📄';
35
- }
36
- </script>
37
-
38
- <template>
39
- <div class="skill-tree">
40
- <div v-for="[source, skills] in sourceGroups" :key="source" class="tree-group">
41
- <button
42
- class="tree-group-header"
43
- :class="{ expanded: treeState.get(source) }"
44
- @click="toggleGroup(source)"
45
- :title="`${source} (${skills.length})`"
46
- >
47
- <span class="tree-arrow">{{ treeState.get(source) ? '▼' : '▶' }}</span>
48
- <span class="source-icon">{{ getSourceIcon(source) }}</span>
49
- <span class="source-name">{{ source }}</span>
50
- <span class="skill-count">{{ skills.length }}</span>
51
- </button>
52
-
53
- <div v-if="treeState.get(source)" class="tree-group-items">
54
- <button
55
- v-for="skill in skills"
56
- :key="skill.id"
57
- class="skill-tree-item"
58
- :class="{ active: skill.id === store.selectedId }"
59
- @click="store.loadDetail(skill.id)"
60
- :title="skill.description"
61
- >
62
- <span class="icon">{{ getSourceIcon(skill.source) }}</span>
63
- <span class="name">{{ skill.name }}</span>
64
- <span v-if="skill.category" class="category-tag">{{ skill.category }}</span>
65
- </button>
66
- </div>
67
- </div>
68
-
69
- <div v-if="sourceGroups.length === 0" class="empty-tree">
70
- <p>没有找到匹配的技能</p>
71
- </div>
72
- </div>
73
- </template>
74
-
75
- <style scoped>
76
- .skill-tree {
77
- display: flex;
78
- flex-direction: column;
79
- overflow-y: auto;
80
- font-size: 13px;
81
- border-right: 1px solid #ddd;
82
- user-select: none;
83
- }
84
-
85
- .tree-group {
86
- border-bottom: 1px solid #f0f0f0;
87
- }
88
-
89
- .tree-group-header {
90
- width: 100%;
91
- padding: 8px 10px;
92
- background: #f8f8f8;
93
- border: none;
94
- border-bottom: 1px solid #e8e8e8;
95
- font-weight: 600;
96
- cursor: pointer;
97
- display: flex;
98
- align-items: center;
99
- gap: 6px;
100
- font-size: 12px;
101
- text-align: left;
102
- justify-content: flex-start;
103
- transition: all 0.15s ease;
104
- }
105
-
106
- .tree-group-header:hover {
107
- background: #f0f0f0;
108
- }
109
-
110
- .tree-group-header.expanded {
111
- background: #e8e8e8;
112
- }
113
-
114
- .tree-arrow {
115
- width: 16px;
116
- text-align: center;
117
- flex-shrink: 0;
118
- font-size: 11px;
119
- color: #666;
120
- }
121
-
122
- .source-icon {
123
- width: 16px;
124
- text-align: center;
125
- flex-shrink: 0;
126
- font-size: 14px;
127
- }
128
-
129
- .source-name {
130
- flex: 1;
131
- overflow: hidden;
132
- text-overflow: ellipsis;
133
- white-space: nowrap;
134
- }
135
-
136
- .skill-count {
137
- margin-left: auto;
138
- color: #999;
139
- font-size: 11px;
140
- font-weight: normal;
141
- flex-shrink: 0;
142
- }
143
-
144
- .tree-group-items {
145
- background: #fafafa;
146
- display: flex;
147
- flex-direction: column;
148
- }
149
-
150
- .skill-tree-item {
151
- display: flex;
152
- align-items: center;
153
- gap: 6px;
154
- width: 100%;
155
- padding: 6px 16px;
156
- border: none;
157
- background: transparent;
158
- cursor: pointer;
159
- font-size: 12px;
160
- transition: background-color 0.1s ease;
161
- text-align: left;
162
- color: inherit;
163
- justify-content: flex-start;
164
- }
165
-
166
- .skill-tree-item:hover {
167
- background: #f0f0f0;
168
- }
169
-
170
- .skill-tree-item.active {
171
- background: #e3f2fd;
172
- color: #1976d2;
173
- font-weight: 500;
174
- }
175
-
176
- .icon {
177
- width: 16px;
178
- text-align: center;
179
- flex-shrink: 0;
180
- font-size: 12px;
181
- }
182
-
183
- .name {
184
- flex: 1;
185
- overflow: hidden;
186
- text-overflow: ellipsis;
187
- white-space: nowrap;
188
- }
189
-
190
- .category-tag {
191
- margin-left: 4px;
192
- padding: 1px 4px;
193
- background: #f0f0f0;
194
- border-radius: 2px;
195
- font-size: 10px;
196
- color: #666;
197
- flex-shrink: 0;
198
- }
199
-
200
- .empty-tree {
201
- display: flex;
202
- align-items: center;
203
- justify-content: center;
204
- height: 100px;
205
- color: #999;
206
- font-size: 12px;
207
- }
208
- </style>