claude-code-templates 1.26.4 → 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.
@@ -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);