lean-spec 0.2.5-dev.20251124064307 → 0.2.5-dev.20251124073254
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/dist/{chunk-FYYLWDI6.js → chunk-LV7XEQ4D.js} +5 -5
- package/dist/chunk-LV7XEQ4D.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/mcp-server.js +1 -1
- package/package.json +1 -1
- package/templates/examples/dark-theme/README.md +25 -14
- package/templates/examples/dark-theme/package.json +2 -2
- package/templates/examples/dark-theme/src/public/app.js +257 -72
- package/templates/examples/dark-theme/src/public/index.html +205 -18
- package/templates/examples/dark-theme/src/public/style.css +548 -86
- package/templates/examples/dark-theme/src/server.js +3 -2
- package/dist/chunk-FYYLWDI6.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { analyzeCommand, archiveCommand, backfillCommand, boardCommand, checkCommand, compactCommand, createCommand, depsCommand, examplesCommand, filesCommand, ganttCommand, initCommand, linkCommand, listCommand, mcpCommand, migrateCommand, openCommand, searchCommand, splitCommand, statsCommand, templatesCommand, timelineCommand, tokensCommand, uiCommand, unlinkCommand, updateCommand, validateCommand, viewCommand } from './chunk-
|
|
1
|
+
import { analyzeCommand, archiveCommand, backfillCommand, boardCommand, checkCommand, compactCommand, createCommand, depsCommand, examplesCommand, filesCommand, ganttCommand, initCommand, linkCommand, listCommand, mcpCommand, migrateCommand, openCommand, searchCommand, splitCommand, statsCommand, templatesCommand, timelineCommand, tokensCommand, uiCommand, unlinkCommand, updateCommand, validateCommand, viewCommand } from './chunk-LV7XEQ4D.js';
|
|
2
2
|
import './chunk-LVD7ZAVZ.js';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { readFileSync } from 'fs';
|
package/dist/mcp-server.js
CHANGED
package/package.json
CHANGED
|
@@ -4,21 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
## Scenario
|
|
6
6
|
|
|
7
|
-
You're building a
|
|
7
|
+
You're building a professional admin dashboard for a SaaS product. The dashboard looks modern and works great, but users keep requesting a dark mode option for late-night monitoring sessions and to reduce eye strain during extended use. Currently, the dashboard only has a bright light theme.
|
|
8
8
|
|
|
9
9
|
## What's Here
|
|
10
10
|
|
|
11
|
-
A
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
11
|
+
A professional admin dashboard with:
|
|
12
|
+
- Interactive data visualization with charts (Chart.js)
|
|
13
|
+
- Real-time statistics cards with animations
|
|
14
|
+
- Sidebar navigation and top bar
|
|
15
|
+
- Activity feed with recent events
|
|
16
|
+
- Responsive layout
|
|
17
|
+
- Clean, modern light theme
|
|
15
18
|
- No dark mode support (yet!)
|
|
16
19
|
|
|
17
20
|
**Files:**
|
|
18
21
|
- `src/server.js` - Express server for static files
|
|
19
|
-
- `src/public/index.html` -
|
|
20
|
-
- `src/public/style.css` - Current light theme styles
|
|
21
|
-
- `src/public/app.js` -
|
|
22
|
+
- `src/public/index.html` - Dashboard interface with sidebar, charts, and stats
|
|
23
|
+
- `src/public/style.css` - Current light theme styles (CSS custom properties ready)
|
|
24
|
+
- `src/public/app.js` - Dashboard logic, chart initialization, and animations
|
|
22
25
|
|
|
23
26
|
## Getting Started
|
|
24
27
|
|
|
@@ -37,19 +40,27 @@ npm start
|
|
|
37
40
|
|
|
38
41
|
Add dark theme support with automatic switching based on system preferences. Follow the tutorial and ask your AI assistant:
|
|
39
42
|
|
|
40
|
-
> "Help me add dark theme support to this
|
|
43
|
+
> "Help me add dark theme support to this admin dashboard using LeanSpec. It should automatically switch based on the user's system theme preference."
|
|
41
44
|
|
|
42
45
|
The AI will guide you through:
|
|
43
46
|
1. Creating a spec for dark theme support
|
|
44
|
-
2. Designing
|
|
45
|
-
3. Implementing
|
|
46
|
-
4.
|
|
47
|
+
2. Designing CSS custom properties for dark mode colors
|
|
48
|
+
3. Implementing the `@media (prefers-color-scheme: dark)` query
|
|
49
|
+
4. Ensuring charts adapt to the theme
|
|
50
|
+
5. Testing the theme switching
|
|
47
51
|
|
|
48
52
|
## Current Limitations
|
|
49
53
|
|
|
50
54
|
- Only light theme available
|
|
51
55
|
- No manual theme toggle
|
|
52
|
-
-
|
|
53
|
-
- No theme persistence
|
|
56
|
+
- Chart colors don't adapt to theme
|
|
57
|
+
- No theme persistence across sessions
|
|
54
58
|
|
|
55
59
|
These are perfect opportunities to practice spec-driven development!
|
|
60
|
+
|
|
61
|
+
## Tech Stack
|
|
62
|
+
|
|
63
|
+
- **Frontend**: Vanilla HTML, CSS, JavaScript
|
|
64
|
+
- **Charts**: Chart.js 4.4.0
|
|
65
|
+
- **Backend**: Express.js (serving static files)
|
|
66
|
+
- **Styling**: CSS Custom Properties (CSS Variables)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dark-theme-demo",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Professional admin dashboard for LeanSpec Dark Theme Tutorial",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "node src/server.js",
|
|
8
8
|
"dev": "node --watch src/server.js"
|
|
9
9
|
},
|
|
10
|
-
"keywords": ["leanspec", "tutorial", "demo"],
|
|
10
|
+
"keywords": ["leanspec", "tutorial", "demo", "dashboard", "admin"],
|
|
11
11
|
"author": "",
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"dependencies": {
|
|
@@ -1,92 +1,277 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
let taskIdCounter = 1;
|
|
1
|
+
// Admin Dashboard - Interactive data visualization
|
|
2
|
+
// This is a starting point for adding dark theme support
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const taskCount = document.getElementById('taskCount');
|
|
4
|
+
// ============================================
|
|
5
|
+
// Initialize Charts
|
|
6
|
+
// ============================================
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
function initializeCharts() {
|
|
9
|
+
// Revenue Chart
|
|
10
|
+
const revenueCtx = document.getElementById('revenueChart');
|
|
11
|
+
if (revenueCtx) {
|
|
12
|
+
new Chart(revenueCtx, {
|
|
13
|
+
type: 'line',
|
|
14
|
+
data: {
|
|
15
|
+
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
|
16
|
+
datasets: [{
|
|
17
|
+
label: 'Revenue',
|
|
18
|
+
data: [4200, 5100, 4800, 6200, 5800, 7100, 6500],
|
|
19
|
+
borderColor: '#0066cc',
|
|
20
|
+
backgroundColor: 'rgba(0, 102, 204, 0.1)',
|
|
21
|
+
borderWidth: 2,
|
|
22
|
+
tension: 0.4,
|
|
23
|
+
fill: true,
|
|
24
|
+
pointBackgroundColor: '#0066cc',
|
|
25
|
+
pointBorderColor: '#fff',
|
|
26
|
+
pointBorderWidth: 2,
|
|
27
|
+
pointRadius: 4,
|
|
28
|
+
pointHoverRadius: 6
|
|
29
|
+
}]
|
|
30
|
+
},
|
|
31
|
+
options: {
|
|
32
|
+
responsive: true,
|
|
33
|
+
maintainAspectRatio: false,
|
|
34
|
+
plugins: {
|
|
35
|
+
legend: {
|
|
36
|
+
display: false
|
|
37
|
+
},
|
|
38
|
+
tooltip: {
|
|
39
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
40
|
+
padding: 12,
|
|
41
|
+
borderRadius: 8,
|
|
42
|
+
titleColor: '#fff',
|
|
43
|
+
bodyColor: '#fff',
|
|
44
|
+
callbacks: {
|
|
45
|
+
label: function(context) {
|
|
46
|
+
return '$' + context.parsed.y.toLocaleString();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
scales: {
|
|
52
|
+
y: {
|
|
53
|
+
beginAtZero: true,
|
|
54
|
+
grid: {
|
|
55
|
+
color: 'rgba(0, 0, 0, 0.05)'
|
|
56
|
+
},
|
|
57
|
+
ticks: {
|
|
58
|
+
callback: function(value) {
|
|
59
|
+
return '$' + value.toLocaleString();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
x: {
|
|
64
|
+
grid: {
|
|
65
|
+
display: false
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
17
71
|
}
|
|
18
|
-
});
|
|
19
72
|
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
73
|
+
// Traffic Chart
|
|
74
|
+
const trafficCtx = document.getElementById('trafficChart');
|
|
75
|
+
if (trafficCtx) {
|
|
76
|
+
new Chart(trafficCtx, {
|
|
77
|
+
type: 'doughnut',
|
|
78
|
+
data: {
|
|
79
|
+
labels: ['Organic', 'Direct', 'Social', 'Referral'],
|
|
80
|
+
datasets: [{
|
|
81
|
+
data: [42, 28, 18, 12],
|
|
82
|
+
backgroundColor: [
|
|
83
|
+
'#0066cc',
|
|
84
|
+
'#00a854',
|
|
85
|
+
'#7c3aed',
|
|
86
|
+
'#ff8c00'
|
|
87
|
+
],
|
|
88
|
+
borderWidth: 0,
|
|
89
|
+
hoverOffset: 8
|
|
90
|
+
}]
|
|
91
|
+
},
|
|
92
|
+
options: {
|
|
93
|
+
responsive: true,
|
|
94
|
+
maintainAspectRatio: false,
|
|
95
|
+
plugins: {
|
|
96
|
+
legend: {
|
|
97
|
+
position: 'bottom',
|
|
98
|
+
labels: {
|
|
99
|
+
padding: 16,
|
|
100
|
+
usePointStyle: true,
|
|
101
|
+
pointStyle: 'circle',
|
|
102
|
+
font: {
|
|
103
|
+
size: 12
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
tooltip: {
|
|
108
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
109
|
+
padding: 12,
|
|
110
|
+
borderRadius: 8,
|
|
111
|
+
titleColor: '#fff',
|
|
112
|
+
bodyColor: '#fff',
|
|
113
|
+
callbacks: {
|
|
114
|
+
label: function(context) {
|
|
115
|
+
return context.label + ': ' + context.parsed + '%';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
25
122
|
}
|
|
123
|
+
}
|
|
26
124
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
completed: false
|
|
31
|
-
};
|
|
125
|
+
// ============================================
|
|
126
|
+
// Animate Stats on Load
|
|
127
|
+
// ============================================
|
|
32
128
|
|
|
33
|
-
|
|
34
|
-
|
|
129
|
+
function animateValue(element, start, end, duration) {
|
|
130
|
+
const range = end - start;
|
|
131
|
+
const increment = range / (duration / 16);
|
|
132
|
+
let current = start;
|
|
35
133
|
|
|
36
|
-
|
|
134
|
+
const timer = setInterval(() => {
|
|
135
|
+
current += increment;
|
|
136
|
+
if ((increment > 0 && current >= end) || (increment < 0 && current <= end)) {
|
|
137
|
+
element.textContent = formatStatValue(end);
|
|
138
|
+
clearInterval(timer);
|
|
139
|
+
} else {
|
|
140
|
+
element.textContent = formatStatValue(Math.floor(current));
|
|
141
|
+
}
|
|
142
|
+
}, 16);
|
|
37
143
|
}
|
|
38
144
|
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
task.completed = !task.completed;
|
|
43
|
-
renderTasks();
|
|
145
|
+
function formatStatValue(value) {
|
|
146
|
+
if (value >= 1000) {
|
|
147
|
+
return value.toLocaleString();
|
|
44
148
|
}
|
|
149
|
+
return value.toString();
|
|
45
150
|
}
|
|
46
151
|
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
152
|
+
function animateStats() {
|
|
153
|
+
const stats = [
|
|
154
|
+
{ id: 'totalUsers', value: 2847 },
|
|
155
|
+
{ id: 'revenue', value: 45231, prefix: '$' },
|
|
156
|
+
{ id: 'completedTasks', value: 892 },
|
|
157
|
+
{ id: 'activeProjects', value: 24 }
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
stats.forEach(stat => {
|
|
161
|
+
const element = document.getElementById(stat.id);
|
|
162
|
+
if (element) {
|
|
163
|
+
if (stat.prefix) {
|
|
164
|
+
const originalText = element.textContent;
|
|
165
|
+
element.textContent = stat.prefix + '0';
|
|
166
|
+
animateValue(element, 0, stat.value, 1000);
|
|
167
|
+
const timer = setInterval(() => {
|
|
168
|
+
if (element.textContent !== stat.prefix + '0') {
|
|
169
|
+
const currentVal = element.textContent.replace(/[^0-9]/g, '');
|
|
170
|
+
element.textContent = stat.prefix + Number(currentVal).toLocaleString();
|
|
171
|
+
}
|
|
172
|
+
}, 16);
|
|
173
|
+
setTimeout(() => clearInterval(timer), 1000);
|
|
174
|
+
} else {
|
|
175
|
+
element.textContent = '0';
|
|
176
|
+
animateValue(element, 0, stat.value, 1000);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
50
180
|
}
|
|
51
181
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Show empty state if no tasks
|
|
57
|
-
if (tasks.length === 0) {
|
|
58
|
-
taskList.innerHTML = '<div class="empty-state">No tasks yet. Add one above to get started!</div>';
|
|
59
|
-
taskCount.textContent = '0 tasks';
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
182
|
+
// ============================================
|
|
183
|
+
// Recent Activity
|
|
184
|
+
// ============================================
|
|
62
185
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<
|
|
186
|
+
function loadRecentActivity() {
|
|
187
|
+
const activities = [
|
|
188
|
+
{
|
|
189
|
+
type: 'user',
|
|
190
|
+
title: 'New user registration',
|
|
191
|
+
description: 'Sarah Johnson joined the platform',
|
|
192
|
+
time: '5 minutes ago',
|
|
193
|
+
icon: `<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
194
|
+
<path d="M10 11C12.2091 11 14 9.20914 14 7C14 4.79086 12.2091 3 10 3C7.79086 3 6 4.79086 6 7C6 9.20914 7.79086 11 10 11Z" stroke="currentColor" stroke-width="2"/>
|
|
195
|
+
<path d="M3 17C3 14.2386 5.23858 12 8 12H12C14.7614 12 17 14.2386 17 17" stroke="currentColor" stroke-width="2"/>
|
|
196
|
+
</svg>`
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: 'report',
|
|
200
|
+
title: 'Monthly report generated',
|
|
201
|
+
description: 'Q4 2024 performance summary is ready',
|
|
202
|
+
time: '1 hour ago',
|
|
203
|
+
icon: `<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
204
|
+
<rect x="3" y="3" width="14" height="14" rx="2" stroke="currentColor" stroke-width="2"/>
|
|
205
|
+
<path d="M7 8H13M7 12H10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
206
|
+
</svg>`
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
type: 'alert',
|
|
210
|
+
title: 'Server capacity warning',
|
|
211
|
+
description: 'CPU usage exceeded 80% threshold',
|
|
212
|
+
time: '3 hours ago',
|
|
213
|
+
icon: `<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
214
|
+
<path d="M10 6V10M10 14H10.01M10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18Z" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
215
|
+
</svg>`
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
type: 'user',
|
|
219
|
+
title: 'Team member added',
|
|
220
|
+
description: 'Michael Chen joined the Development team',
|
|
221
|
+
time: '5 hours ago',
|
|
222
|
+
icon: `<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
223
|
+
<path d="M10 11C12.2091 11 14 9.20914 14 7C14 4.79086 12.2091 3 10 3C7.79086 3 6 4.79086 6 7C6 9.20914 7.79086 11 10 11Z" stroke="currentColor" stroke-width="2"/>
|
|
224
|
+
<path d="M3 17C3 14.2386 5.23858 12 8 12H12C14.7614 12 17 14.2386 17 17" stroke="currentColor" stroke-width="2"/>
|
|
225
|
+
</svg>`
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
type: 'report',
|
|
229
|
+
title: 'Backup completed',
|
|
230
|
+
description: 'Database backup finished successfully',
|
|
231
|
+
time: '8 hours ago',
|
|
232
|
+
icon: `<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
233
|
+
<path d="M3 12L9 18L21 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
234
|
+
</svg>`
|
|
235
|
+
}
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
const activityList = document.getElementById('activityList');
|
|
239
|
+
if (activityList) {
|
|
240
|
+
activityList.innerHTML = activities.map(activity => `
|
|
241
|
+
<div class="activity-item">
|
|
242
|
+
<div class="activity-icon ${activity.type}">
|
|
243
|
+
${activity.icon}
|
|
244
|
+
</div>
|
|
245
|
+
<div class="activity-content">
|
|
246
|
+
<div class="activity-title">${activity.title}</div>
|
|
247
|
+
<div class="activity-description">${activity.description}</div>
|
|
248
|
+
<div class="activity-time">${activity.time}</div>
|
|
249
|
+
</div>
|
|
73
250
|
</div>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
taskList.appendChild(li);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Update count
|
|
81
|
-
const remaining = tasks.filter(t => !t.completed).length;
|
|
82
|
-
taskCount.textContent = `${remaining} task${remaining !== 1 ? 's' : ''} remaining`;
|
|
251
|
+
`).join('');
|
|
252
|
+
}
|
|
83
253
|
}
|
|
84
254
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return div.innerHTML;
|
|
89
|
-
}
|
|
255
|
+
// ============================================
|
|
256
|
+
// Initialize Dashboard
|
|
257
|
+
// ============================================
|
|
90
258
|
|
|
91
|
-
|
|
92
|
-
|
|
259
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
260
|
+
// Initialize all components
|
|
261
|
+
animateStats();
|
|
262
|
+
initializeCharts();
|
|
263
|
+
loadRecentActivity();
|
|
264
|
+
|
|
265
|
+
// Add subtle hover effects
|
|
266
|
+
document.querySelectorAll('.stat-card').forEach(card => {
|
|
267
|
+
card.addEventListener('mouseenter', (e) => {
|
|
268
|
+
e.currentTarget.style.transform = 'translateY(-4px)';
|
|
269
|
+
});
|
|
270
|
+
card.addEventListener('mouseleave', (e) => {
|
|
271
|
+
e.currentTarget.style.transform = 'translateY(0)';
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
console.log('✓ Dashboard initialized');
|
|
276
|
+
console.log('💡 Try adding dark theme support with CSS custom properties!');
|
|
277
|
+
});
|