lean-spec 0.2.5-dev.20251124064307 → 0.2.5-dev.20251124070920

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/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-FYYLWDI6.js';
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';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- export { createMcpServer } from './chunk-FYYLWDI6.js';
2
+ export { createMcpServer } from './chunk-LV7XEQ4D.js';
3
3
  import './chunk-LVD7ZAVZ.js';
4
4
  //# sourceMappingURL=mcp-server.js.map
5
5
  //# sourceMappingURL=mcp-server.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lean-spec",
3
- "version": "0.2.5-dev.20251124064307",
3
+ "version": "0.2.5-dev.20251124070920",
4
4
  "description": "Specification-driven development made simple",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,21 +4,24 @@
4
4
 
5
5
  ## Scenario
6
6
 
7
- You're building a personal task manager web app. The app works great, but users keep requesting a dark mode option for late-night productivity sessions. Currently, the app only has a bright light theme that can strain eyes in low-light environments.
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 minimal single-page task manager with:
12
- - Task creation and listing interface
13
- - Simple Express.js server serving static files
14
- - Clean light theme CSS
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` - Task manager interface
20
- - `src/public/style.css` - Current light theme styles
21
- - `src/public/app.js` - Task management logic
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 app using LeanSpec. It should automatically switch based on the user's system theme preference."
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 the CSS for dark mode
45
- 3. Implementing system preference detection
46
- 4. Testing the theme switching
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
- - Colors may not be fully accessible
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": "Simple web app for LeanSpec Dark Theme Tutorial",
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
- // Task Manager - Simple task tracking
2
- let tasks = [];
3
- let taskIdCounter = 1;
1
+ // Admin Dashboard - Interactive data visualization
2
+ // This is a starting point for adding dark theme support
4
3
 
5
- const taskInput = document.getElementById('taskInput');
6
- const addButton = document.getElementById('addButton');
7
- const taskList = document.getElementById('taskList');
8
- const taskCount = document.getElementById('taskCount');
4
+ // ============================================
5
+ // Initialize Charts
6
+ // ============================================
9
7
 
10
- // Add task on button click
11
- addButton.addEventListener('click', addTask);
12
-
13
- // Add task on Enter key
14
- taskInput.addEventListener('keypress', (e) => {
15
- if (e.key === 'Enter') {
16
- addTask();
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
- function addTask() {
21
- const text = taskInput.value.trim();
22
-
23
- if (!text) {
24
- return;
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
- const task = {
28
- id: taskIdCounter++,
29
- text: text,
30
- completed: false
31
- };
125
+ // ============================================
126
+ // Animate Stats on Load
127
+ // ============================================
32
128
 
33
- tasks.push(task);
34
- taskInput.value = '';
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
- renderTasks();
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 toggleTask(id) {
40
- const task = tasks.find(t => t.id === id);
41
- if (task) {
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 deleteTask(id) {
48
- tasks = tasks.filter(t => t.id !== id);
49
- renderTasks();
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
- function renderTasks() {
53
- // Clear list
54
- taskList.innerHTML = '';
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
- // Render each task
64
- tasks.forEach(task => {
65
- const li = document.createElement('li');
66
- li.className = `task-item ${task.completed ? 'completed' : ''}`;
67
-
68
- li.innerHTML = `
69
- <div class="task-content">
70
- <input type="checkbox" class="task-checkbox" ${task.completed ? 'checked' : ''}
71
- onchange="toggleTask(${task.id})">
72
- <span class="task-text">${escapeHtml(task.text)}</span>
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
- <button class="delete-button" onclick="deleteTask(${task.id})">Delete</button>
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
- function escapeHtml(text) {
86
- const div = document.createElement('div');
87
- div.textContent = text;
88
- return div.innerHTML;
89
- }
255
+ // ============================================
256
+ // Initialize Dashboard
257
+ // ============================================
90
258
 
91
- // Initial render
92
- renderTasks();
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
+ });