sillyspec 3.7.13 → 3.7.15
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/.claude/skills/sillyspec-archive/SKILL.md +77 -0
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +591 -0
- package/.claude/skills/sillyspec-continue/SKILL.md +44 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +233 -0
- package/.claude/skills/sillyspec-explore/SKILL.md +96 -0
- package/.claude/skills/sillyspec-export/SKILL.md +53 -0
- package/.claude/skills/sillyspec-init/SKILL.md +171 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +263 -0
- package/.claude/skills/sillyspec-propose/SKILL.md +248 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +102 -0
- package/.claude/skills/sillyspec-resume/SKILL.md +111 -0
- package/{templates/scan.md → .claude/skills/sillyspec-scan/SKILL.md} +22 -60
- package/.claude/skills/sillyspec-state/SKILL.md +54 -0
- package/.claude/skills/sillyspec-status/SKILL.md +131 -0
- package/.claude/skills/sillyspec-verify/SKILL.md +146 -0
- package/.claude/skills/sillyspec-workspace/SKILL.md +149 -0
- package/.sillyspec/changes/run-command-design/design.md +1230 -0
- package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
- package/.sillyspec/knowledge/INDEX.md +8 -0
- package/.sillyspec/knowledge/uncategorized.md +3 -0
- package/.sillyspec/projects/sillyspec.yaml +3 -0
- package/README.md +4 -0
- package/logo.jpg +0 -0
- package/package.json +7 -1
- package/packages/dashboard/dist/assets/index-Bx0cgoK_.js +7446 -0
- package/packages/dashboard/dist/assets/index-DbkUSsNO.css +1 -0
- package/packages/dashboard/dist/favicon.jpg +0 -0
- package/packages/dashboard/dist/index.html +2 -2
- package/packages/dashboard/dist/logo.jpg +0 -0
- package/packages/dashboard/package-lock.json +220 -0
- package/packages/dashboard/package.json +8 -5
- package/packages/dashboard/public/favicon.jpg +0 -0
- package/packages/dashboard/public/logo.jpg +0 -0
- package/packages/dashboard/server/index.js +92 -4
- package/packages/dashboard/server/parser.js +252 -28
- package/packages/dashboard/src/App.vue +139 -9
- package/packages/dashboard/src/components/ActionBar.vue +23 -39
- package/packages/dashboard/src/components/CommandPalette.vue +40 -65
- package/packages/dashboard/src/components/DetailPanel.vue +68 -53
- package/packages/dashboard/src/components/DocPreview.vue +137 -20
- package/packages/dashboard/src/components/DocTree.vue +48 -26
- package/packages/dashboard/src/components/LogStream.vue +12 -32
- package/packages/dashboard/src/components/PipelineStage.vue +8 -8
- package/packages/dashboard/src/components/PipelineView.vue +35 -43
- package/packages/dashboard/src/components/ProjectList.vue +52 -77
- package/packages/dashboard/src/components/ProjectOverview.vue +178 -0
- package/packages/dashboard/src/components/StageBadge.vue +13 -13
- package/packages/dashboard/src/components/StepCard.vue +11 -11
- package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -0
- package/packages/dashboard/src/components/detail/GitDetail.vue +61 -0
- package/packages/dashboard/src/components/detail/TechDetail.vue +43 -0
- package/packages/dashboard/src/main.js +4 -1
- package/packages/dashboard/src/style.css +13 -13
- package/src/index.js +55 -8
- package/src/init.js +66 -196
- package/src/migrate.js +1 -18
- package/src/progress.js +279 -281
- package/src/run.js +320 -0
- package/src/setup.js +1 -9
- package/src/stages/brainstorm.js +210 -0
- package/src/stages/execute.js +190 -0
- package/src/stages/index.js +22 -0
- package/src/stages/plan.js +118 -0
- package/src/stages/propose.js +115 -0
- package/src/stages/verify.js +98 -0
- package/packages/dashboard/dist/assets/index-5Rrvs0Rl.css +0 -1
- package/packages/dashboard/dist/assets/index-ZNToqi9V.js +0 -17
- package/templates/archive.md +0 -121
- package/templates/brainstorm.md +0 -246
- package/templates/commit.md +0 -123
- package/templates/continue.md +0 -32
- package/templates/execute.md +0 -314
- package/templates/explore.md +0 -60
- package/templates/export.md +0 -21
- package/templates/init.md +0 -61
- package/templates/plan.md +0 -157
- package/templates/progress-format.md +0 -90
- package/templates/propose.md +0 -73
- package/templates/quick.md +0 -135
- package/templates/resume-dialog.md +0 -55
- package/templates/resume.md +0 -53
- package/templates/scan-quick.md +0 -49
- package/templates/skills/playwright-e2e/SKILL.md +0 -340
- package/templates/status.md +0 -72
- package/templates/verify.md +0 -253
- package/templates/workspace-sync.md +0 -89
- package/templates/workspace.md +0 -67
- /package/.sillyspec/{docs/sillyspec/brainstorm → changes/brainstorm-archive}/2026-04-05-dashboard-design.md +0 -0
- /package/.sillyspec/{docs/sillyspec/brainstorm → changes/brainstorm-archive}/2026-04-05-unified-docs-design.md +0 -0
- /package/.sillyspec/{specs/2026-04-05-dashboard-design.md → changes/dashboard/design.md.braindraft} +0 -0
- /package/.sillyspec/{specs/2026-04-05-unified-docs-design.md → changes/unified-docs-design/design.md} +0 -0
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<div class="flex items-center gap-2.5 mb-3">
|
|
17
17
|
<span
|
|
18
18
|
class="text-[12px] font-semibold font-[JetBrains_Mono,monospace] tracking-tight"
|
|
19
|
-
:style="{ color: isActive ? '#
|
|
19
|
+
:style="{ color: isActive ? '#D97706' : '#1C1C1E' }"
|
|
20
20
|
>
|
|
21
21
|
{{ title }}
|
|
22
22
|
</span>
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
<!-- Steps -->
|
|
27
27
|
<div class="space-y-1">
|
|
28
|
-
<div v-if="steps.length === 0" class="text-[11px] italic py-1" style="color: #
|
|
28
|
+
<div v-if="steps.length === 0" class="text-[11px] italic py-1" style="color: #6B7280;">
|
|
29
29
|
No steps yet
|
|
30
30
|
</div>
|
|
31
31
|
<StepCard
|
|
@@ -58,13 +58,13 @@ const props = defineProps({
|
|
|
58
58
|
const emit = defineEmits(['select-step'])
|
|
59
59
|
|
|
60
60
|
const nodeStyle = computed(() => {
|
|
61
|
-
if (props.isActive) return { background: '#
|
|
61
|
+
if (props.isActive) return { background: '#D97706', boxShadow: '0 0 8px rgba(251,191,36,0.4)' }
|
|
62
62
|
const colors = {
|
|
63
|
-
'completed': '#
|
|
64
|
-
'in-progress': '#
|
|
65
|
-
'blocked': '#
|
|
66
|
-
'failed': '#
|
|
67
|
-
'pending': '#
|
|
63
|
+
'completed': '#16A34A',
|
|
64
|
+
'in-progress': '#D97706',
|
|
65
|
+
'blocked': '#EA580C',
|
|
66
|
+
'failed': '#DC2626',
|
|
67
|
+
'pending': '#E5E5EA'
|
|
68
68
|
}
|
|
69
69
|
return { background: colors[props.status] || colors.pending }
|
|
70
70
|
})
|
|
@@ -1,77 +1,69 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="flex flex-col h-full" style="background: #
|
|
2
|
+
<div class="flex flex-col h-full" style="background: #F5F5F7;">
|
|
3
3
|
<!-- Header with Tabs -->
|
|
4
|
-
<div class="px-6 pt-4 pb-0" style="border-bottom: 1px solid #
|
|
4
|
+
<div class="px-6 pt-4 pb-0" style="border-bottom: 1px solid #F0F0F3;">
|
|
5
5
|
<div class="flex items-center gap-6">
|
|
6
|
-
<h2 class="text-[11px] font-semibold uppercase tracking-[0.2em] font-[JetBrains_Mono,monospace]" style="color: #
|
|
6
|
+
<h2 class="text-[11px] font-semibold uppercase tracking-[0.2em] font-[JetBrains_Mono,monospace]" style="color: #6B7280;">
|
|
7
7
|
{{ project?.name || '项目' }}
|
|
8
8
|
</h2>
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
:style="{ color: activeTab === 'pipeline' ? '#FBBF24' : '#525252', background: activeTab === 'pipeline' ? 'rgba(251,191,36,0.08)' : 'transparent' }"
|
|
14
|
-
>流水线</button>
|
|
15
|
-
<button
|
|
16
|
-
@click="$emit('switch-tab', 'docs')"
|
|
17
|
-
class="px-3 py-1.5 text-[11px] font-[JetBrains_Mono,monospace] transition-colors rounded"
|
|
18
|
-
:style="{ color: activeTab === 'docs' ? '#FBBF24' : '#525252', background: activeTab === 'docs' ? 'rgba(251,191,36,0.08)' : 'transparent' }"
|
|
19
|
-
>文档</button>
|
|
20
|
-
</div>
|
|
9
|
+
<n-tabs :value="activeTab" type="segment" size="small" @update:value="$emit('switch-tab', $event)" style="max-width: 200px;">
|
|
10
|
+
<n-tab name="pipeline">流水线</n-tab>
|
|
11
|
+
<n-tab name="docs">文档</n-tab>
|
|
12
|
+
</n-tabs>
|
|
21
13
|
</div>
|
|
22
14
|
</div>
|
|
23
15
|
|
|
24
16
|
<!-- Pipeline Tab -->
|
|
25
17
|
<div v-if="activeTab === 'pipeline'" class="flex flex-col flex-1 overflow-hidden">
|
|
26
|
-
<div class="px-6 pt-4 pb-2">
|
|
27
|
-
<p
|
|
28
|
-
<span style="color: #
|
|
18
|
+
<div v-if="project && currentStage" class="px-6 pt-4 pb-2">
|
|
19
|
+
<p class="text-[12px] font-[JetBrains_Mono,monospace]" style="color: #636366;">
|
|
20
|
+
<span style="color: #D97706;">{{ currentStage }}</span>
|
|
29
21
|
</p>
|
|
30
22
|
</div>
|
|
31
23
|
|
|
32
24
|
<!-- Empty state -->
|
|
33
|
-
<
|
|
34
|
-
<div class="text-center">
|
|
35
|
-
<div class="w-14 h-14 mx-auto mb-4 rounded-md flex items-center justify-center" style="border: 1px dashed #2A2A2D; transform: rotate(45deg);">
|
|
36
|
-
<svg class="w-5 h-5" style="color: #525252; transform: rotate(-45deg);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
37
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
|
38
|
-
</svg>
|
|
39
|
-
</div>
|
|
40
|
-
<p class="text-[12px] font-[JetBrains_Mono,monospace]" style="color: #525252;">选择一个项目查看流水线</p>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
25
|
+
<n-empty v-if="!project || !project.state" description="选择一个项目查看流水线" style="margin: auto;" />
|
|
43
26
|
|
|
44
27
|
<!-- Stages -->
|
|
45
28
|
<div v-else class="flex-1 overflow-y-auto px-6 pb-5 space-y-5">
|
|
46
29
|
<PipelineStage name="brainstorm" title="头脑风暴" :steps="getStageSteps('brainstorm')" :status="getStageStatus('brainstorm')" :is-active="currentStage === 'brainstorm'" :active-step="activeStep" @select-step="handleSelectStep" />
|
|
47
|
-
<div v-if="hasStage('plan')" class="flex items-center pl-[3px]"><div class="w-px h-4" style="background: #
|
|
30
|
+
<div v-if="hasStage('plan')" class="flex items-center pl-[3px]"><div class="w-px h-4" style="background: #F0F0F3;" /></div>
|
|
48
31
|
<PipelineStage name="plan" title="规划" :steps="getStageSteps('plan')" :status="getStageStatus('plan')" :is-active="currentStage === 'plan'" :active-step="activeStep" @select-step="handleSelectStep" />
|
|
49
|
-
<div v-if="hasStage('execute')" class="flex items-center pl-[3px]"><div class="w-px h-4" style="background: #
|
|
32
|
+
<div v-if="hasStage('execute')" class="flex items-center pl-[3px]"><div class="w-px h-4" style="background: #F0F0F3;" /></div>
|
|
50
33
|
<PipelineStage name="execute" title="执行" :steps="getStageSteps('execute')" :status="getStageStatus('execute')" :is-active="currentStage === 'execute'" :active-step="activeStep" @select-step="handleSelectStep" />
|
|
51
|
-
<div v-if="hasStage('verify')" class="flex items-center pl-[3px]"><div class="w-px h-4" style="background: #
|
|
34
|
+
<div v-if="hasStage('verify')" class="flex items-center pl-[3px]"><div class="w-px h-4" style="background: #F0F0F3;" /></div>
|
|
52
35
|
<PipelineStage name="verify" title="验证" :steps="getStageSteps('verify')" :status="getStageStatus('verify')" :is-active="currentStage === 'verify'" :active-step="activeStep" @select-step="handleSelectStep" />
|
|
53
36
|
</div>
|
|
54
37
|
|
|
55
38
|
<!-- Activity Log -->
|
|
56
|
-
<div v-if="project?.state?.progress" style="border-top: 1px solid #
|
|
39
|
+
<div v-if="project?.state?.progress" style="border-top: 1px solid #F0F0F3; background: rgba(0,0,0,0.03);">
|
|
57
40
|
<div class="px-6 py-2.5 flex items-center justify-between">
|
|
58
|
-
<div class="text-[9px] font-semibold uppercase tracking-[0.25em] font-[JetBrains_Mono,monospace]" style="color: #
|
|
59
|
-
<div class="text-[10px] font-mono-log" style="color: #
|
|
41
|
+
<div class="text-[9px] font-semibold uppercase tracking-[0.25em] font-[JetBrains_Mono,monospace]" style="color: #6B7280;">活动日志</div>
|
|
42
|
+
<div class="text-[10px] font-mono-log" style="color: #D1D1D6;">{{ activityLogs.length }}</div>
|
|
60
43
|
</div>
|
|
61
|
-
<div class="px-6 pb-3
|
|
62
|
-
<
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
44
|
+
<div class="px-6 pb-3 max-h-32 overflow-y-auto">
|
|
45
|
+
<n-timeline size="small">
|
|
46
|
+
<n-timeline-item
|
|
47
|
+
v-for="(log, i) in activityLogs"
|
|
48
|
+
:key="i"
|
|
49
|
+
:type="log.status === 'completed' ? 'success' : 'warning'"
|
|
50
|
+
>
|
|
51
|
+
<template #default>
|
|
52
|
+
<span class="text-[11px]" style="color: #636366;">{{ log.description }}</span>
|
|
53
|
+
</template>
|
|
54
|
+
<template #time>
|
|
55
|
+
<span class="text-[10px] font-mono-log" style="color: #D1D1D6;">{{ log.time }}</span>
|
|
56
|
+
</template>
|
|
57
|
+
</n-timeline-item>
|
|
58
|
+
</n-timeline>
|
|
59
|
+
<div v-if="activityLogs.length === 0" class="text-[10px] py-1" style="color: #D1D1D6;">暂无活动记录</div>
|
|
68
60
|
</div>
|
|
69
61
|
</div>
|
|
70
62
|
</div>
|
|
71
63
|
|
|
72
64
|
<!-- Docs Tab -->
|
|
73
65
|
<div v-if="activeTab === 'docs'" class="flex-1 flex overflow-hidden">
|
|
74
|
-
<div class="w-[200px] flex-shrink-0 overflow-hidden" style="border-right: 1px solid #
|
|
66
|
+
<div class="w-[200px] flex-shrink-0 overflow-hidden" style="border-right: 1px solid #F0F0F3;">
|
|
75
67
|
<DocTree :groups="docs.groups" :selected-file="selectedDocFile" @select-file="$emit('select-doc-file', $event)" />
|
|
76
68
|
</div>
|
|
77
69
|
<div class="flex-1 overflow-hidden">
|
|
@@ -99,7 +91,7 @@ const props = defineProps({
|
|
|
99
91
|
|
|
100
92
|
const emit = defineEmits(['select-step', 'switch-tab', 'select-doc-file'])
|
|
101
93
|
|
|
102
|
-
const currentStage = computed(() => props.project?.state?.currentStage || '
|
|
94
|
+
const currentStage = computed(() => props.project?.state?.currentStage || '')
|
|
103
95
|
const progress = computed(() => props.project?.state?.progress || {})
|
|
104
96
|
const stages = computed(() => progress.value.stages || {})
|
|
105
97
|
|
|
@@ -3,138 +3,109 @@
|
|
|
3
3
|
<!-- Header -->
|
|
4
4
|
<div class="relative z-10 px-5 pt-5 pb-4">
|
|
5
5
|
<div class="flex items-center gap-3">
|
|
6
|
-
<div class="w-8 h-8 rounded-md flex items-center justify-center" style="background: linear-gradient(135deg, #
|
|
6
|
+
<div class="w-8 h-8 rounded-md flex items-center justify-center" style="background: linear-gradient(135deg, #D97706 0%, #F59E0B 100%); clip-path: polygon(0 0, 100% 0, 85% 100%, 15% 100%);">
|
|
7
7
|
<span class="text-[10px] font-bold text-black font-[JetBrains_Mono,monospace]">S</span>
|
|
8
8
|
</div>
|
|
9
|
+
<img src="/logo.jpg" style="width:28px;height:28px;border-radius:6px;margin-right:8px;">
|
|
9
10
|
<div class="flex-1">
|
|
10
|
-
<h1 class="text-[13px] font-semibold tracking-tight font-[JetBrains_Mono,monospace]" style="color: #
|
|
11
|
+
<h1 class="text-[13px] font-semibold tracking-tight font-[JetBrains_Mono,monospace]" style="color: #1C1C1E;">
|
|
11
12
|
SillySpec
|
|
12
13
|
</h1>
|
|
13
|
-
<p class="text-[10px] tracking-widest uppercase" style="color: #
|
|
14
|
+
<p class="text-[10px] tracking-widest uppercase" style="color: #6B7280;">控制台</p>
|
|
14
15
|
</div>
|
|
15
16
|
<!-- Scan paths gear button -->
|
|
16
|
-
<button
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
25
|
-
</svg>
|
|
26
|
-
</button>
|
|
17
|
+
<n-button quaternary size="tiny" @click="showScanPanel = !showScanPanel" :type="showScanPanel ? 'primary' : 'default'">
|
|
18
|
+
<template #icon>
|
|
19
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
20
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
|
21
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
22
|
+
</svg>
|
|
23
|
+
</template>
|
|
24
|
+
</n-button>
|
|
27
25
|
</div>
|
|
28
26
|
|
|
29
27
|
<!-- Scan paths panel (inline) -->
|
|
30
28
|
<Transition name="slide">
|
|
31
|
-
<div v-if="showScanPanel" class="mt-3 rounded-md p-3" style="background: #
|
|
32
|
-
<div class="text-[10px] font-semibold uppercase tracking-[0.15em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #
|
|
29
|
+
<div v-if="showScanPanel" class="mt-3 rounded-md p-3" style="background: #FFFFFF; border: 1px solid #F0F0F3;">
|
|
30
|
+
<div class="text-[10px] font-semibold uppercase tracking-[0.15em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">扫描路径</div>
|
|
33
31
|
|
|
34
|
-
<div v-if="scanPaths.length === 0" class="text-[10px] py-1" style="color: #
|
|
32
|
+
<div v-if="scanPaths.length === 0" class="text-[10px] py-1" style="color: #D1D1D6;">暂无自定义路径</div>
|
|
35
33
|
<div v-else class="space-y-1 mb-2">
|
|
36
34
|
<div v-for="(p, i) in scanPaths" :key="i" class="flex items-center gap-2 text-[10px] group">
|
|
37
|
-
<span class="flex-1 truncate font-mono-log" style="color: #
|
|
38
|
-
<button
|
|
39
|
-
@click="removePath(p)"
|
|
40
|
-
class="opacity-0 group-hover:opacity-100 transition-opacity text-[9px] px-1 rounded-sm"
|
|
41
|
-
style="color: #EF4444; background: rgba(239,68,68,0.08);"
|
|
42
|
-
>✕</button>
|
|
35
|
+
<span class="flex-1 truncate font-mono-log" style="color: #636366;">{{ p }}</span>
|
|
36
|
+
<n-button quaternary size="tiny" type="error" @click="removePath(p)">✕</n-button>
|
|
43
37
|
</div>
|
|
44
38
|
</div>
|
|
45
39
|
|
|
46
40
|
<!-- Add path -->
|
|
47
41
|
<div v-if="showAddInput" class="flex items-center gap-2">
|
|
48
|
-
<input
|
|
49
|
-
|
|
50
|
-
v-model="newPath"
|
|
51
|
-
type="text"
|
|
52
|
-
placeholder="输入目录路径..."
|
|
53
|
-
class="flex-1 px-2 py-1 rounded-sm text-[10px] font-mono-log outline-none"
|
|
54
|
-
style="background: #0A0A0B; border: 1px solid #2A2A2D; color: #E4E4E7;"
|
|
55
|
-
@keydown.enter="addPath"
|
|
56
|
-
@keydown.escape="showAddInput = false"
|
|
57
|
-
/>
|
|
58
|
-
<button @click="addPath" class="px-2 py-1 text-[10px] rounded-sm" style="background: rgba(251,191,36,0.1); color: #FBBF24; border: 1px solid rgba(251,191,36,0.2);">添加</button>
|
|
42
|
+
<n-input v-model:value="newPath" size="tiny" placeholder="输入目录路径..." @keydown.enter="addPath" @keydown.escape="showAddInput = false" ref="pathInput" />
|
|
43
|
+
<n-button size="tiny" type="primary" @click="addPath">添加</n-button>
|
|
59
44
|
</div>
|
|
60
|
-
<button
|
|
61
|
-
v-else
|
|
62
|
-
@click="showAddInput = true"
|
|
63
|
-
class="text-[10px] px-2 py-1 rounded-sm transition-colors duration-100"
|
|
64
|
-
style="color: #525252; border: 1px dashed #2A2A2D;"
|
|
65
|
-
>
|
|
66
|
-
+ 添加目录
|
|
67
|
-
</button>
|
|
45
|
+
<n-button v-else size="tiny" dashed @click="showAddInput = true">+ 添加目录</n-button>
|
|
68
46
|
</div>
|
|
69
47
|
</Transition>
|
|
70
48
|
</div>
|
|
71
49
|
|
|
72
50
|
<!-- Divider -->
|
|
73
|
-
<div class="mx-4 h-px" style="background: linear-gradient(90deg, transparent, #
|
|
51
|
+
<div class="mx-4 h-px" style="background: linear-gradient(90deg, transparent, #E5E5EA, transparent);"></div>
|
|
74
52
|
|
|
75
53
|
<!-- Projects List -->
|
|
76
54
|
<div class="flex-1 overflow-y-auto py-3 relative z-10">
|
|
77
55
|
<!-- Loading skeleton -->
|
|
78
56
|
<div v-if="isLoading" class="px-4 space-y-2">
|
|
79
|
-
<
|
|
80
|
-
<
|
|
81
|
-
<
|
|
82
|
-
</
|
|
83
|
-
<p class="text-center text-[10px] mt-4 font-[JetBrains_Mono,monospace]" style="color: #
|
|
57
|
+
<n-card v-for="i in 4" :key="i" size="small" :bordered="false">
|
|
58
|
+
<n-skeleton text :width="80" size="small" />
|
|
59
|
+
<n-skeleton text :width="140" size="small" style="margin-top: 6px;" />
|
|
60
|
+
</n-card>
|
|
61
|
+
<p class="text-center text-[10px] mt-4 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">
|
|
84
62
|
正在扫描项目...
|
|
85
63
|
</p>
|
|
86
64
|
</div>
|
|
87
65
|
|
|
88
66
|
<!-- Empty state -->
|
|
89
|
-
<
|
|
90
|
-
<div class="w-10 h-10 mx-auto mb-3 rounded-full flex items-center justify-center" style="border: 1px dashed #2A2A2D;">
|
|
91
|
-
<svg class="w-4 h-4" style="color: #525252;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
92
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
93
|
-
</svg>
|
|
94
|
-
</div>
|
|
95
|
-
<p class="text-[11px]" style="color: #8B8B8E;">未发现 SillySpec 项目</p>
|
|
96
|
-
<p class="text-[10px] mt-1" style="color: #525252;">正在扫描项目目录...</p>
|
|
97
|
-
</div>
|
|
67
|
+
<n-empty v-else-if="projects.length === 0" description="未发现 SillySpec 项目" style="padding: 48px 0;" />
|
|
98
68
|
|
|
99
69
|
<!-- Projects -->
|
|
100
70
|
<div v-else class="px-3 space-y-0.5">
|
|
101
71
|
<div
|
|
102
72
|
v-for="project in projects"
|
|
103
73
|
:key="project.path"
|
|
104
|
-
:class="[
|
|
105
|
-
'relative rounded-md cursor-pointer transition-all duration-150 overflow-hidden group',
|
|
106
|
-
]"
|
|
74
|
+
:class="['relative rounded-md cursor-pointer transition-all duration-150 overflow-hidden group']"
|
|
107
75
|
:style="{
|
|
108
|
-
background: isActive(project) ? 'rgba(
|
|
109
|
-
borderLeft: isActive(project) ? '2px solid #
|
|
76
|
+
background: isActive(project) ? 'rgba(217,119,6,0.06)' : 'transparent',
|
|
77
|
+
borderLeft: isActive(project) ? '2px solid #D97706' : '2px solid transparent',
|
|
110
78
|
}"
|
|
111
|
-
@mouseenter="$event.currentTarget.style.background = isActive(project) ? 'rgba(
|
|
112
|
-
@mouseleave="$event.currentTarget.style.background = isActive(project) ? 'rgba(
|
|
79
|
+
@mouseenter="$event.currentTarget.style.background = isActive(project) ? 'rgba(217,119,6,0.08)' : 'rgba(255,255,255,0.02)'"
|
|
80
|
+
@mouseleave="$event.currentTarget.style.background = isActive(project) ? 'rgba(217,119,6,0.06)' : 'transparent'"
|
|
113
81
|
@click="$emit('select', project)"
|
|
114
82
|
>
|
|
115
83
|
<div class="px-3 py-2.5">
|
|
116
84
|
<div class="flex items-center justify-between gap-2">
|
|
117
85
|
<div class="flex-1 min-w-0">
|
|
118
86
|
<h3
|
|
119
|
-
|
|
120
|
-
:style="{ color: isActive(project) ? '#
|
|
87
|
+
class="text-[12px] font-medium truncate transition-colors duration-150 font-[JetBrains_Mono,monospace]"
|
|
88
|
+
:style="{ color: isActive(project) ? '#D97706' : '#1C1C1E' }"
|
|
121
89
|
>
|
|
122
90
|
{{ project.name }}
|
|
123
91
|
</h3>
|
|
124
|
-
<p class="text-[10px] mt-0.5 truncate font-mono-log" style="color: #
|
|
92
|
+
<p class="text-[10px] mt-0.5 truncate font-mono-log" style="color: #6B7280;">
|
|
125
93
|
{{ project.path }}
|
|
126
94
|
</p>
|
|
127
95
|
</div>
|
|
128
|
-
<
|
|
96
|
+
<n-tag
|
|
129
97
|
v-if="project.state?.currentStage"
|
|
130
|
-
:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
98
|
+
:type="statusTagType(getProjectStatus(project))"
|
|
99
|
+
size="small"
|
|
100
|
+
:bordered="false"
|
|
101
|
+
round
|
|
102
|
+
>
|
|
103
|
+
{{ stageLabel(project) }}
|
|
104
|
+
</n-tag>
|
|
134
105
|
</div>
|
|
135
106
|
|
|
136
107
|
<!-- Progress -->
|
|
137
|
-
<div v-if="project.state?.progress" class="mt-2 h-[2px] rounded-full overflow-hidden" style="background: #
|
|
108
|
+
<div v-if="project.state?.progress" class="mt-2 h-[2px] rounded-full overflow-hidden" style="background: #FFFFFF;">
|
|
138
109
|
<div
|
|
139
110
|
class="h-full rounded-full transition-all duration-500 progress-gradient"
|
|
140
111
|
:style="{ width: getProjectProgress(project) + '%' }"
|
|
@@ -146,10 +117,10 @@
|
|
|
146
117
|
</div>
|
|
147
118
|
|
|
148
119
|
<!-- Footer -->
|
|
149
|
-
<div class="relative z-10 px-4 py-2.5" style="border-top: 1px solid #
|
|
120
|
+
<div class="relative z-10 px-4 py-2.5" style="border-top: 1px solid #F0F0F3;">
|
|
150
121
|
<div class="flex items-center justify-between">
|
|
151
|
-
<span class="text-[10px] font-[JetBrains_Mono,monospace]" style="color: #
|
|
152
|
-
<kbd class="text-[9px] px-1.5 py-0.5 rounded font-mono-log" style="color: #
|
|
122
|
+
<span class="text-[10px] font-[JetBrains_Mono,monospace]" style="color: #6B7280;">{{ projects.length }} 个项目</span>
|
|
123
|
+
<kbd class="text-[9px] px-1.5 py-0.5 rounded font-mono-log" style="color: #6B7280; background: #FFFFFF; border: 1px solid #E5E5EA;">⌘K</kbd>
|
|
153
124
|
</div>
|
|
154
125
|
</div>
|
|
155
126
|
</div>
|
|
@@ -157,7 +128,6 @@
|
|
|
157
128
|
|
|
158
129
|
<script setup>
|
|
159
130
|
import { ref, nextTick, watch } from 'vue'
|
|
160
|
-
import StageBadge from './StageBadge.vue'
|
|
161
131
|
|
|
162
132
|
const props = defineProps({
|
|
163
133
|
projects: { type: Array, default: () => [] },
|
|
@@ -181,6 +151,11 @@ function isActive(project) {
|
|
|
181
151
|
return props.activeProject?.path === project.path
|
|
182
152
|
}
|
|
183
153
|
|
|
154
|
+
function statusTagType(status) {
|
|
155
|
+
const map = { 'in-progress': 'warning', 'completed': 'success', 'failed': 'error', 'blocked': 'warning' }
|
|
156
|
+
return map[status] || 'default'
|
|
157
|
+
}
|
|
158
|
+
|
|
184
159
|
function getProjectStatus(project) {
|
|
185
160
|
const stage = project.state?.currentStage
|
|
186
161
|
if (!stage) return 'pending'
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="project" class="project-overview">
|
|
3
|
+
<!-- Name + Path -->
|
|
4
|
+
<div class="ov-section">
|
|
5
|
+
<span class="ov-name">{{ project.name }}</span>
|
|
6
|
+
<span class="ov-path">{{ shortPath }}</span>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="ov-divider" />
|
|
9
|
+
|
|
10
|
+
<!-- Tech Stack -->
|
|
11
|
+
<div class="ov-section ov-clickable" @click="$emit('show-detail', 'tech')">
|
|
12
|
+
<span class="ov-label">技术栈</span>
|
|
13
|
+
<span v-for="t in overview.techStack" :key="t" class="ov-tag">{{ t }}</span>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="ov-divider" />
|
|
16
|
+
|
|
17
|
+
<!-- Stage -->
|
|
18
|
+
<div v-if="project.state?.currentStage" class="ov-section">
|
|
19
|
+
<span class="ov-label">阶段</span>
|
|
20
|
+
<span class="ov-value">{{ project.state.currentStage }}</span>
|
|
21
|
+
</div>
|
|
22
|
+
<div v-else-if="project.state" class="ov-section">
|
|
23
|
+
<span class="ov-label">阶段</span>
|
|
24
|
+
<span class="ov-value" style="color:#9CA3AF;">未执行</span>
|
|
25
|
+
</div>
|
|
26
|
+
<div v-if="project.state?.currentStage" class="ov-divider" />
|
|
27
|
+
|
|
28
|
+
<!-- Last Active -->
|
|
29
|
+
<div v-if="overview.lastActive" class="ov-section">
|
|
30
|
+
<span class="ov-label">最近活跃</span>
|
|
31
|
+
<span class="ov-value">{{ formatTime(overview.lastActive) }}</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div v-if="overview.lastActive" class="ov-divider" />
|
|
34
|
+
|
|
35
|
+
<!-- Doc Stats -->
|
|
36
|
+
<div v-if="overview.docStats.total > 0" class="ov-section ov-clickable" @click="$emit('show-detail', 'docs')">
|
|
37
|
+
<span class="ov-label">文档</span>
|
|
38
|
+
<span class="ov-value">{{ docSummary }}</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div v-if="overview.docStats.total > 0" class="ov-divider" />
|
|
41
|
+
|
|
42
|
+
<!-- Git -->
|
|
43
|
+
<div class="ov-section ov-git ov-clickable" @click="$emit('show-detail', 'git')">
|
|
44
|
+
<span v-if="overview.git.branch" class="ov-branch">{{ overview.git.branch }}</span>
|
|
45
|
+
<span v-if="overview.git.lastCommit" class="ov-commit" :title="overview.git.lastCommit">{{ truncate(overview.git.lastCommit, 40) }}</span>
|
|
46
|
+
<span v-if="overview.git.dirtyCount > 0" class="ov-dirty">{{ overview.git.dirtyCount }} 未提交</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script setup>
|
|
52
|
+
import { computed } from 'vue'
|
|
53
|
+
|
|
54
|
+
const props = defineProps({
|
|
55
|
+
project: { type: Object, default: null }
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
defineEmits(['show-detail'])
|
|
59
|
+
|
|
60
|
+
const overview = computed(() => props.project?.overview || { techStack: [], lastActive: null, docStats: { design: 0, plan: 0, archive: 0, changes: 0, scan: 0, quicklog: 0, total: 0 }, git: { branch: '', lastCommit: '', dirtyCount: 0 } })
|
|
61
|
+
|
|
62
|
+
const shortPath = computed(() => {
|
|
63
|
+
const p = props.project?.path || ''
|
|
64
|
+
return p.replace(/^\/Users\/[^/]+/, '~')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const docSummary = computed(() => {
|
|
68
|
+
const s = overview.value.docStats
|
|
69
|
+
const parts = []
|
|
70
|
+
if (s.design) parts.push(`设计 ${s.design}`)
|
|
71
|
+
if (s.plan) parts.push(`计划 ${s.plan}`)
|
|
72
|
+
if (s.archive) parts.push(`归档 ${s.archive}`)
|
|
73
|
+
if (s.changes) parts.push(`变更 ${s.changes}`)
|
|
74
|
+
return parts.join(' / ') || `${s.total} 篇`
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
function formatTime(iso) {
|
|
78
|
+
if (!iso) return ''
|
|
79
|
+
const d = new Date(iso)
|
|
80
|
+
const now = new Date()
|
|
81
|
+
const diffMs = now - d
|
|
82
|
+
if (diffMs < 60000) return '刚刚'
|
|
83
|
+
if (diffMs < 3600000) return `${Math.floor(diffMs / 60000)} 分钟前`
|
|
84
|
+
if (diffMs < 86400000) return `${Math.floor(diffMs / 3600000)} 小时前`
|
|
85
|
+
if (diffMs < 604800000) return `${Math.floor(diffMs / 86400000)} 天前`
|
|
86
|
+
return `${d.getMonth() + 1}/${d.getDate()}`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function truncate(s, n) {
|
|
90
|
+
return s.length > n ? s.slice(0, n) + '…' : s
|
|
91
|
+
}
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<style scoped>
|
|
95
|
+
.project-overview {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
background: #FFFFFF;
|
|
99
|
+
border-bottom: 1px solid #E5E5EA;
|
|
100
|
+
padding: 8px 16px;
|
|
101
|
+
min-height: 48px;
|
|
102
|
+
gap: 8px;
|
|
103
|
+
flex-shrink: 0;
|
|
104
|
+
overflow-x: auto;
|
|
105
|
+
}
|
|
106
|
+
.ov-section {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
gap: 6px;
|
|
110
|
+
white-space: nowrap;
|
|
111
|
+
flex-shrink: 0;
|
|
112
|
+
}
|
|
113
|
+
.ov-clickable {
|
|
114
|
+
cursor: pointer;
|
|
115
|
+
border-radius: 4px;
|
|
116
|
+
padding: 2px 6px;
|
|
117
|
+
margin: -2px -6px;
|
|
118
|
+
transition: background 0.15s;
|
|
119
|
+
}
|
|
120
|
+
.ov-clickable:hover {
|
|
121
|
+
background: rgba(217,119,6,0.06);
|
|
122
|
+
}
|
|
123
|
+
.ov-name {
|
|
124
|
+
font-weight: 600;
|
|
125
|
+
font-size: 13px;
|
|
126
|
+
color: #1C1C1E;
|
|
127
|
+
}
|
|
128
|
+
.ov-path {
|
|
129
|
+
font-size: 11px;
|
|
130
|
+
color: #636366;
|
|
131
|
+
}
|
|
132
|
+
.ov-label {
|
|
133
|
+
font-size: 11px;
|
|
134
|
+
color: #636366;
|
|
135
|
+
}
|
|
136
|
+
.ov-value {
|
|
137
|
+
font-size: 12px;
|
|
138
|
+
color: #1C1C1E;
|
|
139
|
+
font-weight: 500;
|
|
140
|
+
}
|
|
141
|
+
.ov-tag {
|
|
142
|
+
font-size: 10px;
|
|
143
|
+
padding: 1px 8px;
|
|
144
|
+
border-radius: 10px;
|
|
145
|
+
background: rgba(217,119,6,0.08);
|
|
146
|
+
color: #D97706;
|
|
147
|
+
font-weight: 500;
|
|
148
|
+
}
|
|
149
|
+
.ov-divider {
|
|
150
|
+
width: 1px;
|
|
151
|
+
height: 20px;
|
|
152
|
+
background: #E5E5EA;
|
|
153
|
+
flex-shrink: 0;
|
|
154
|
+
}
|
|
155
|
+
.ov-git {
|
|
156
|
+
margin-left: auto;
|
|
157
|
+
gap: 10px;
|
|
158
|
+
}
|
|
159
|
+
.ov-branch {
|
|
160
|
+
font-size: 11px;
|
|
161
|
+
color: #636366;
|
|
162
|
+
background: #F5F5F7;
|
|
163
|
+
padding: 1px 8px;
|
|
164
|
+
border-radius: 4px;
|
|
165
|
+
}
|
|
166
|
+
.ov-commit {
|
|
167
|
+
font-size: 11px;
|
|
168
|
+
color: #636366;
|
|
169
|
+
max-width: 200px;
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
text-overflow: ellipsis;
|
|
172
|
+
}
|
|
173
|
+
.ov-dirty {
|
|
174
|
+
font-size: 11px;
|
|
175
|
+
color: #D97706;
|
|
176
|
+
font-weight: 500;
|
|
177
|
+
}
|
|
178
|
+
</style>
|
|
@@ -19,19 +19,19 @@ const props = defineProps({
|
|
|
19
19
|
|
|
20
20
|
const displayLabel = computed(() => {
|
|
21
21
|
if (props.label) return props.label
|
|
22
|
-
const labels = { 'completed': 'done', 'in-progress': 'running', 'blocked': 'blocked', 'failed': 'error'
|
|
23
|
-
return labels[props.status] || '
|
|
22
|
+
const labels = { 'completed': 'done', 'in-progress': 'running', 'blocked': 'blocked', 'failed': 'error' }
|
|
23
|
+
return labels[props.status] || ''
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
const sizeClass = computed(() => props.size === 'sm' ? 'px-1.5 py-0.5' : 'px-2 py-1')
|
|
27
27
|
|
|
28
28
|
const badgeStyle = computed(() => {
|
|
29
29
|
const styles = {
|
|
30
|
-
'completed': { background: 'rgba(
|
|
31
|
-
'in-progress': { background: 'rgba(251,191,36,0.1)', color: '#
|
|
32
|
-
'blocked': { background: 'rgba(251,146,60,0.1)', color: '#
|
|
33
|
-
'failed': { background: 'rgba(239,68,68,0.1)', color: '#
|
|
34
|
-
'pending': { background: 'rgba(82,82,82,0.15)', color: '#
|
|
30
|
+
'completed': { background: 'rgba(22,163,74,0.1)', color: '#16A34A' },
|
|
31
|
+
'in-progress': { background: 'rgba(251,191,36,0.1)', color: '#D97706' },
|
|
32
|
+
'blocked': { background: 'rgba(251,146,60,0.1)', color: '#EA580C' },
|
|
33
|
+
'failed': { background: 'rgba(239,68,68,0.1)', color: '#DC2626' },
|
|
34
|
+
'pending': { background: 'rgba(82,82,82,0.15)', color: '#6B7280' }
|
|
35
35
|
}
|
|
36
36
|
return styles[props.status] || styles.pending
|
|
37
37
|
})
|
|
@@ -42,12 +42,12 @@ const dotClass = computed(() => {
|
|
|
42
42
|
|
|
43
43
|
const dotStyle = computed(() => {
|
|
44
44
|
const colors = {
|
|
45
|
-
'completed': '#
|
|
46
|
-
'in-progress': '#
|
|
47
|
-
'blocked': '#
|
|
48
|
-
'failed': '#
|
|
49
|
-
'pending': '#
|
|
45
|
+
'completed': '#16A34A',
|
|
46
|
+
'in-progress': '#D97706',
|
|
47
|
+
'blocked': '#EA580C',
|
|
48
|
+
'failed': '#DC2626',
|
|
49
|
+
'pending': '#6B7280'
|
|
50
50
|
}
|
|
51
|
-
return { background: colors[props.status] || '#
|
|
51
|
+
return { background: colors[props.status] || '#6B7280' }
|
|
52
52
|
})
|
|
53
53
|
</script>
|