claude-code-templates 1.27.0 → 1.28.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.
- package/bin/create-claude-config.js +1 -0
- package/package.json +1 -1
- package/src/index.js +71 -11
- package/src/skill-dashboard-web/index.html +326 -0
- package/src/skill-dashboard-web/script.js +445 -0
- package/src/skill-dashboard-web/styles.css +3469 -0
- package/src/skill-dashboard.js +441 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
// State Management
|
|
2
|
+
const state = {
|
|
3
|
+
skills: [],
|
|
4
|
+
filteredSkills: [],
|
|
5
|
+
currentFilter: 'all',
|
|
6
|
+
currentSort: 'name',
|
|
7
|
+
currentView: 'grid',
|
|
8
|
+
searchQuery: '',
|
|
9
|
+
currentSkill: null
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// API Base URL
|
|
13
|
+
const API_BASE = '';
|
|
14
|
+
|
|
15
|
+
// Initialize Dashboard
|
|
16
|
+
async function initDashboard() {
|
|
17
|
+
setupEventListeners();
|
|
18
|
+
await loadSkills();
|
|
19
|
+
renderSkills();
|
|
20
|
+
updateStats();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Setup Event Listeners
|
|
24
|
+
function setupEventListeners() {
|
|
25
|
+
// Sidebar toggle
|
|
26
|
+
document.getElementById('sidebarToggle')?.addEventListener('click', toggleSidebar);
|
|
27
|
+
|
|
28
|
+
// Search
|
|
29
|
+
document.getElementById('skillSearch')?.addEventListener('input', handleSearch);
|
|
30
|
+
|
|
31
|
+
// Refresh
|
|
32
|
+
document.getElementById('refreshBtn')?.addEventListener('click', async () => {
|
|
33
|
+
await loadSkills();
|
|
34
|
+
renderSkills();
|
|
35
|
+
updateStats();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// View toggles
|
|
39
|
+
document.querySelectorAll('.view-btn').forEach(btn => {
|
|
40
|
+
btn.addEventListener('click', (e) => {
|
|
41
|
+
const view = e.currentTarget.dataset.view;
|
|
42
|
+
setView(view);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Source filters (sidebar)
|
|
47
|
+
document.querySelectorAll('.source-filter-btn').forEach(btn => {
|
|
48
|
+
btn.addEventListener('click', (e) => {
|
|
49
|
+
const filter = e.currentTarget.dataset.filter;
|
|
50
|
+
setSourceFilter(filter);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Filter chips
|
|
55
|
+
document.querySelectorAll('.filter-chip').forEach(chip => {
|
|
56
|
+
chip.addEventListener('click', (e) => {
|
|
57
|
+
const filter = e.currentTarget.dataset.filter;
|
|
58
|
+
setFilter(filter);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Sort
|
|
63
|
+
document.getElementById('sortSelect')?.addEventListener('change', (e) => {
|
|
64
|
+
state.currentSort = e.target.value;
|
|
65
|
+
renderSkills();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Modal
|
|
69
|
+
document.getElementById('closeModal')?.addEventListener('click', closeModal);
|
|
70
|
+
document.getElementById('skillModal')?.addEventListener('click', (e) => {
|
|
71
|
+
if (e.target.id === 'skillModal') closeModal();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// Clear filters
|
|
76
|
+
document.getElementById('clearFiltersBtn')?.addEventListener('click', clearFilters);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Load Skills from API
|
|
80
|
+
async function loadSkills() {
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch(`${API_BASE}/api/skills`);
|
|
83
|
+
const data = await response.json();
|
|
84
|
+
state.skills = data.skills || [];
|
|
85
|
+
state.filteredSkills = [...state.skills];
|
|
86
|
+
applyFiltersAndSearch();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Error loading skills:', error);
|
|
89
|
+
showError('Failed to load skills');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Apply Filters and Search
|
|
94
|
+
function applyFiltersAndSearch() {
|
|
95
|
+
let filtered = [...state.skills];
|
|
96
|
+
|
|
97
|
+
// Apply source filter
|
|
98
|
+
if (state.currentFilter !== 'all') {
|
|
99
|
+
filtered = filtered.filter(skill =>
|
|
100
|
+
skill.source.toLowerCase() === state.currentFilter.toLowerCase()
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Apply search
|
|
105
|
+
if (state.searchQuery) {
|
|
106
|
+
const query = state.searchQuery.toLowerCase();
|
|
107
|
+
filtered = filtered.filter(skill =>
|
|
108
|
+
skill.name.toLowerCase().includes(query) ||
|
|
109
|
+
skill.description.toLowerCase().includes(query)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Apply sorting
|
|
114
|
+
filtered.sort((a, b) => {
|
|
115
|
+
switch (state.currentSort) {
|
|
116
|
+
case 'name':
|
|
117
|
+
return a.name.localeCompare(b.name);
|
|
118
|
+
case 'files':
|
|
119
|
+
return (b.fileCount || 0) - (a.fileCount || 0);
|
|
120
|
+
case 'modified':
|
|
121
|
+
return new Date(b.lastModified) - new Date(a.lastModified);
|
|
122
|
+
default:
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
state.filteredSkills = filtered;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Render Skills Grid/List
|
|
131
|
+
function renderSkills() {
|
|
132
|
+
const container = document.getElementById('skillsContainer');
|
|
133
|
+
const emptyState = document.getElementById('emptyState');
|
|
134
|
+
|
|
135
|
+
if (!container) return;
|
|
136
|
+
|
|
137
|
+
if (state.filteredSkills.length === 0) {
|
|
138
|
+
container.style.display = 'none';
|
|
139
|
+
emptyState.style.display = 'flex';
|
|
140
|
+
updateEmptyState();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
container.style.display = 'grid';
|
|
145
|
+
emptyState.style.display = 'none';
|
|
146
|
+
|
|
147
|
+
container.innerHTML = state.filteredSkills.map(skill => createSkillCard(skill)).join('');
|
|
148
|
+
|
|
149
|
+
// Add click listeners
|
|
150
|
+
container.querySelectorAll('.skill-card').forEach((card, index) => {
|
|
151
|
+
card.addEventListener('click', () => openSkillModal(state.filteredSkills[index]));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Create Skill Card HTML
|
|
156
|
+
function createSkillCard(skill) {
|
|
157
|
+
const sourceBadgeClass = skill.source.toLowerCase();
|
|
158
|
+
const lastModified = formatDate(skill.lastModified);
|
|
159
|
+
|
|
160
|
+
return `
|
|
161
|
+
<div class="skill-card">
|
|
162
|
+
<div class="skill-card-header">
|
|
163
|
+
<h3 class="skill-card-title">${escapeHtml(skill.name)}</h3>
|
|
164
|
+
<span class="skill-source-badge ${sourceBadgeClass}">${skill.source}</span>
|
|
165
|
+
</div>
|
|
166
|
+
<p class="skill-card-description">${escapeHtml(skill.description)}</p>
|
|
167
|
+
<div class="skill-card-meta">
|
|
168
|
+
<div class="skill-meta-item">
|
|
169
|
+
<span class="skill-meta-icon">📁</span>
|
|
170
|
+
<span class="skill-meta-value">${skill.fileCount} files</span>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="skill-meta-item">
|
|
173
|
+
<span class="skill-meta-icon">📅</span>
|
|
174
|
+
<span class="skill-meta-value">${lastModified}</span>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Open Skill Modal
|
|
182
|
+
async function openSkillModal(skill) {
|
|
183
|
+
state.currentSkill = skill;
|
|
184
|
+
|
|
185
|
+
// Populate modal header
|
|
186
|
+
document.getElementById('modalSkillName').textContent = skill.name;
|
|
187
|
+
|
|
188
|
+
const sourceBadge = document.getElementById('modalSourceBadge');
|
|
189
|
+
sourceBadge.textContent = skill.source;
|
|
190
|
+
sourceBadge.className = `source-badge skill-source-badge ${skill.source.toLowerCase()}`;
|
|
191
|
+
|
|
192
|
+
// Populate modal footer
|
|
193
|
+
document.getElementById('modalFileCount').textContent = skill.fileCount;
|
|
194
|
+
document.getElementById('modalLastModified').textContent = formatDate(skill.lastModified);
|
|
195
|
+
document.getElementById('modalSource').textContent = skill.source;
|
|
196
|
+
|
|
197
|
+
// Render loading levels
|
|
198
|
+
renderLoadingLevels(skill);
|
|
199
|
+
|
|
200
|
+
// Show modal
|
|
201
|
+
document.getElementById('skillModal').classList.add('active');
|
|
202
|
+
document.body.style.overflow = 'hidden';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Render Loading Levels (new system based on official docs)
|
|
206
|
+
function renderLoadingLevels(skill) {
|
|
207
|
+
// Level 1: Metadata
|
|
208
|
+
document.getElementById('metadataName').textContent = skill.name;
|
|
209
|
+
document.getElementById('metadataDescription').textContent = skill.description;
|
|
210
|
+
|
|
211
|
+
// Allowed tools in metadata
|
|
212
|
+
if (skill.allowedTools && skill.allowedTools.length > 0) {
|
|
213
|
+
const toolsField = document.getElementById('metadataToolsField');
|
|
214
|
+
const toolsContainer = document.getElementById('metadataTools');
|
|
215
|
+
toolsField.style.display = 'flex';
|
|
216
|
+
|
|
217
|
+
const tools = Array.isArray(skill.allowedTools) ? skill.allowedTools : skill.allowedTools.split(',').map(t => t.trim());
|
|
218
|
+
toolsContainer.innerHTML = tools.map(tool =>
|
|
219
|
+
`<span class="tool-chip-small">${escapeHtml(tool)}</span>`
|
|
220
|
+
).join('');
|
|
221
|
+
} else {
|
|
222
|
+
document.getElementById('metadataToolsField').style.display = 'none';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Level 2: Instructions (SKILL.md)
|
|
226
|
+
document.getElementById('level2FileSize').textContent = skill.mainFileSize;
|
|
227
|
+
|
|
228
|
+
// Level 3+: Resources & Code
|
|
229
|
+
const instructionsFiles = [];
|
|
230
|
+
const codeFiles = [];
|
|
231
|
+
const resourceFiles = [];
|
|
232
|
+
|
|
233
|
+
// Categorize files
|
|
234
|
+
const allFiles = [
|
|
235
|
+
...(skill.supportingFiles.onDemand || []),
|
|
236
|
+
...(skill.supportingFiles.progressive || [])
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
allFiles.forEach(file => {
|
|
240
|
+
const ext = file.name.split('.').pop().toLowerCase();
|
|
241
|
+
|
|
242
|
+
if (ext === 'md') {
|
|
243
|
+
instructionsFiles.push(file);
|
|
244
|
+
} else if (['py', 'js', 'ts', 'sh', 'bash'].includes(ext)) {
|
|
245
|
+
codeFiles.push(file);
|
|
246
|
+
} else {
|
|
247
|
+
resourceFiles.push(file);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Render categories
|
|
252
|
+
renderResourceCategory('instructions', instructionsFiles);
|
|
253
|
+
renderResourceCategory('code', codeFiles);
|
|
254
|
+
renderResourceCategory('resources', resourceFiles);
|
|
255
|
+
|
|
256
|
+
// Show empty state if no resources
|
|
257
|
+
const hasResources = instructionsFiles.length > 0 || codeFiles.length > 0 || resourceFiles.length > 0;
|
|
258
|
+
document.getElementById('emptyResources').style.display = hasResources ? 'none' : 'flex';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Render Resource Category
|
|
262
|
+
function renderResourceCategory(categoryName, files) {
|
|
263
|
+
const category = document.getElementById(`${categoryName}Category`);
|
|
264
|
+
const count = document.getElementById(`${categoryName}Count`);
|
|
265
|
+
const filesContainer = document.getElementById(`${categoryName}Files`);
|
|
266
|
+
|
|
267
|
+
if (files.length > 0) {
|
|
268
|
+
category.style.display = 'block';
|
|
269
|
+
count.textContent = files.length;
|
|
270
|
+
|
|
271
|
+
filesContainer.innerHTML = files.map(file => {
|
|
272
|
+
const icon = getFileIcon(file.type);
|
|
273
|
+
return `
|
|
274
|
+
<div class="resource-file">
|
|
275
|
+
<span class="file-icon">${icon}</span>
|
|
276
|
+
<span class="file-name">${escapeHtml(file.relativePath)}</span>
|
|
277
|
+
<span class="file-size">${formatFileSize(file.size)}</span>
|
|
278
|
+
</div>
|
|
279
|
+
`;
|
|
280
|
+
}).join('');
|
|
281
|
+
} else {
|
|
282
|
+
category.style.display = 'none';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Note: File viewing functionality removed as per requirements
|
|
287
|
+
// Modal now focuses on showing the 3-level loading structure
|
|
288
|
+
|
|
289
|
+
// Close Modal
|
|
290
|
+
function closeModal() {
|
|
291
|
+
document.getElementById('skillModal').classList.remove('active');
|
|
292
|
+
document.body.style.overflow = '';
|
|
293
|
+
state.currentSkill = null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Set Filter
|
|
297
|
+
function setFilter(filter) {
|
|
298
|
+
state.currentFilter = filter;
|
|
299
|
+
|
|
300
|
+
// Update UI
|
|
301
|
+
document.querySelectorAll('.filter-chip').forEach(chip => {
|
|
302
|
+
chip.classList.toggle('active', chip.dataset.filter === filter);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
document.querySelectorAll('.source-filter-btn').forEach(btn => {
|
|
306
|
+
btn.classList.toggle('active', btn.dataset.filter === filter);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
applyFiltersAndSearch();
|
|
310
|
+
renderSkills();
|
|
311
|
+
updateStats();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Set Source Filter (sidebar)
|
|
315
|
+
function setSourceFilter(filter) {
|
|
316
|
+
setFilter(filter);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Handle Search
|
|
320
|
+
function handleSearch(e) {
|
|
321
|
+
state.searchQuery = e.target.value;
|
|
322
|
+
applyFiltersAndSearch();
|
|
323
|
+
renderSkills();
|
|
324
|
+
updateStats();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Set View
|
|
328
|
+
function setView(view) {
|
|
329
|
+
state.currentView = view;
|
|
330
|
+
|
|
331
|
+
document.querySelectorAll('.view-btn').forEach(btn => {
|
|
332
|
+
btn.classList.toggle('active', btn.dataset.view === view);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const container = document.getElementById('skillsContainer');
|
|
336
|
+
container.classList.toggle('list-view', view === 'list');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Clear Filters
|
|
340
|
+
function clearFilters() {
|
|
341
|
+
state.currentFilter = 'all';
|
|
342
|
+
state.searchQuery = '';
|
|
343
|
+
document.getElementById('skillSearch').value = '';
|
|
344
|
+
|
|
345
|
+
document.querySelectorAll('.filter-chip').forEach(chip => {
|
|
346
|
+
chip.classList.toggle('active', chip.dataset.filter === 'all');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
applyFiltersAndSearch();
|
|
350
|
+
renderSkills();
|
|
351
|
+
updateStats();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Update Stats
|
|
355
|
+
function updateStats() {
|
|
356
|
+
const total = state.skills.length;
|
|
357
|
+
const personal = state.skills.filter(s => s.source === 'Personal').length;
|
|
358
|
+
const project = state.skills.filter(s => s.source === 'Project').length;
|
|
359
|
+
const plugin = state.skills.filter(s => s.source === 'Plugin').length;
|
|
360
|
+
|
|
361
|
+
// Sidebar stats
|
|
362
|
+
document.getElementById('sidebarTotalSkills').textContent = total;
|
|
363
|
+
document.getElementById('sidebarPersonalSkills').textContent = personal;
|
|
364
|
+
|
|
365
|
+
// Filter counts
|
|
366
|
+
document.getElementById('countAll').textContent = state.filteredSkills.length;
|
|
367
|
+
document.getElementById('countPersonal').textContent = state.filteredSkills.filter(s => s.source === 'Personal').length;
|
|
368
|
+
document.getElementById('countProject').textContent = state.filteredSkills.filter(s => s.source === 'Project').length;
|
|
369
|
+
document.getElementById('countPlugin').textContent = state.filteredSkills.filter(s => s.source === 'Plugin').length;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Update Empty State
|
|
373
|
+
function updateEmptyState() {
|
|
374
|
+
const description = document.getElementById('emptyDescription');
|
|
375
|
+
const clearBtn = document.getElementById('clearFiltersBtn');
|
|
376
|
+
|
|
377
|
+
if (state.searchQuery || state.currentFilter !== 'all') {
|
|
378
|
+
description.textContent = 'No skills match your current filters or search.';
|
|
379
|
+
clearBtn.style.display = 'inline-block';
|
|
380
|
+
} else {
|
|
381
|
+
description.textContent = 'No skills installed. Add skills to ~/.claude/skills or .claude/skills';
|
|
382
|
+
clearBtn.style.display = 'none';
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Toggle Sidebar
|
|
387
|
+
function toggleSidebar() {
|
|
388
|
+
document.querySelector('.sidebar').classList.toggle('collapsed');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Utility Functions
|
|
392
|
+
function formatDate(dateString) {
|
|
393
|
+
if (!dateString) return 'Unknown';
|
|
394
|
+
const date = new Date(dateString);
|
|
395
|
+
const now = new Date();
|
|
396
|
+
const diffTime = Math.abs(now - date);
|
|
397
|
+
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
398
|
+
|
|
399
|
+
if (diffDays === 0) return 'Today';
|
|
400
|
+
if (diffDays === 1) return 'Yesterday';
|
|
401
|
+
if (diffDays < 7) return `${diffDays} days ago`;
|
|
402
|
+
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`;
|
|
403
|
+
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`;
|
|
404
|
+
return `${Math.floor(diffDays / 365)} years ago`;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function formatFileSize(bytes) {
|
|
408
|
+
if (typeof bytes === 'string') return bytes;
|
|
409
|
+
if (!bytes || bytes === 0) return '0 B';
|
|
410
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
411
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
412
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function getFileIcon(type) {
|
|
416
|
+
const icons = {
|
|
417
|
+
markdown: '📝',
|
|
418
|
+
python: '🐍',
|
|
419
|
+
javascript: '📜',
|
|
420
|
+
typescript: '📘',
|
|
421
|
+
shell: '🖥️',
|
|
422
|
+
json: '📋',
|
|
423
|
+
yaml: '⚙️',
|
|
424
|
+
text: '📄',
|
|
425
|
+
html: '🌐',
|
|
426
|
+
css: '🎨',
|
|
427
|
+
unknown: '📄'
|
|
428
|
+
};
|
|
429
|
+
return icons[type] || icons.unknown;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function escapeHtml(text) {
|
|
433
|
+
if (!text) return '';
|
|
434
|
+
const div = document.createElement('div');
|
|
435
|
+
div.textContent = text;
|
|
436
|
+
return div.innerHTML;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function showError(message) {
|
|
440
|
+
console.error(message);
|
|
441
|
+
// Could add a toast notification here
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Initialize on load
|
|
445
|
+
document.addEventListener('DOMContentLoaded', initDashboard);
|