popilot 0.6.0 → 0.8.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/cli.mjs +204 -2
- package/lib/doctor.mjs +38 -1
- package/lib/hydrate.mjs +15 -0
- package/lib/scaffold.mjs +5 -0
- package/lib/setup-wizard.mjs +35 -2
- package/package.json +1 -1
- package/scaffold/.context/project.yaml.example +19 -0
- package/scaffold/mcp-notification-server/package.json +18 -0
- package/scaffold/mcp-notification-server/src/index.ts +275 -0
- package/scaffold/mcp-notification-server/src/turso-client.ts +142 -0
- package/scaffold/mcp-notification-server/tsconfig.json +14 -0
- package/scaffold/mcp-pm/package.json +19 -0
- package/scaffold/mcp-pm/src/api-client.ts +69 -0
- package/scaffold/mcp-pm/src/index.ts +660 -0
- package/scaffold/mcp-pm/tsconfig.json +14 -0
- package/scaffold/pm-api/package.json +21 -0
- package/scaffold/pm-api/sql/001-memo-v2.sql +49 -0
- package/scaffold/pm-api/sql/002-notifications.sql +18 -0
- package/scaffold/pm-api/sql/003-content.sql +66 -0
- package/scaffold/pm-api/sql/004-agent-events.sql +21 -0
- package/scaffold/pm-api/sql/005-epic-sprint-decoupling.sql +6 -0
- package/scaffold/pm-api/sql/schema-core.sql +331 -0
- package/scaffold/pm-api/sql/schema-docs.sql +25 -0
- package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
- package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
- package/scaffold/pm-api/src/auth.ts +28 -0
- package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
- package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
- package/scaffold/pm-api/src/db/adapter.ts +36 -0
- package/scaffold/pm-api/src/db/turso.ts +147 -0
- package/scaffold/pm-api/src/index.ts +114 -0
- package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
- package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
- package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
- package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
- package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
- package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
- package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
- package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
- package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
- package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
- package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
- package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
- package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
- package/scaffold/pm-api/src/mcp.ts +871 -0
- package/scaffold/pm-api/src/nudge.ts +283 -0
- package/scaffold/pm-api/src/routes/auth.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
- package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
- package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
- package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
- package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
- package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
- package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
- package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
- package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
- package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
- package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
- package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
- package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
- package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
- package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
- package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
- package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
- package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
- package/scaffold/pm-api/src/types.ts +11 -0
- package/scaffold/pm-api/src/utils/activity.ts +22 -0
- package/scaffold/pm-api/src/utils/admin.ts +9 -0
- package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
- package/scaffold/pm-api/src/utils/assignee.ts +69 -0
- package/scaffold/pm-api/src/utils/db.ts +45 -0
- package/scaffold/pm-api/src/utils/initiative.ts +23 -0
- package/scaffold/pm-api/src/utils/retro-link.ts +32 -0
- package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
- package/scaffold/pm-api/tsconfig.json +15 -0
- package/scaffold/pm-api/wrangler.toml.hbs +11 -0
- package/scaffold/spec-site/package-lock.json +892 -0
- package/scaffold/spec-site/package.json +15 -1
- package/scaffold/spec-site/src/api/types.ts +6 -0
- package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
- package/scaffold/spec-site/src/components/AuthGate.vue +117 -0
- package/scaffold/spec-site/src/components/BurndownChart.vue +78 -0
- package/scaffold/spec-site/src/components/DocComments.vue +137 -0
- package/scaffold/spec-site/src/components/DocEditor.vue +118 -0
- package/scaffold/spec-site/src/components/DocExportBar.vue +110 -0
- package/scaffold/spec-site/src/components/DocsSidebar.vue +309 -0
- package/scaffold/spec-site/src/components/EmptyState.vue +30 -0
- package/scaffold/spec-site/src/components/ErrorBanner.vue +38 -0
- package/scaffold/spec-site/src/components/Icon.vue +58 -0
- package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
- package/scaffold/spec-site/src/components/MemoChecklist.vue +88 -0
- package/scaffold/spec-site/src/components/MemoGraph.vue +75 -0
- package/scaffold/spec-site/src/components/MemoItem.vue +353 -0
- package/scaffold/spec-site/src/components/MemoRelations.vue +101 -0
- package/scaffold/spec-site/src/components/MemoTimeline.vue +53 -0
- package/scaffold/spec-site/src/components/MentionInput.vue +174 -0
- package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
- package/scaffold/spec-site/src/components/PriorityBadge.vue +23 -0
- package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
- package/scaffold/spec-site/src/components/SlashCommand.ts +123 -0
- package/scaffold/spec-site/src/components/StateDisplay.vue +54 -0
- package/scaffold/spec-site/src/components/TreeNode.vue +82 -0
- package/scaffold/spec-site/src/components/UserAvatar.vue +24 -0
- package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
- package/scaffold/spec-site/src/composables/navTypes.ts +3 -0
- package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
- package/scaffold/spec-site/src/composables/useBottomSheet.ts +103 -0
- package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
- package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
- package/scaffold/spec-site/src/composables/useMemo.ts +39 -0
- package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
- package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
- package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
- package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
- package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
- package/scaffold/spec-site/src/composables/useTurso.ts +17 -0
- package/scaffold/spec-site/src/composables/useUser.ts +19 -1
- package/scaffold/spec-site/src/composables/useViewport.ts +26 -0
- package/scaffold/spec-site/src/features.ts +108 -0
- package/scaffold/spec-site/src/mockup/ComponentPalette.vue +61 -0
- package/scaffold/spec-site/src/mockup/MockupCanvas.vue +459 -0
- package/scaffold/spec-site/src/mockup/PropertyPanel.vue +217 -0
- package/scaffold/spec-site/src/mockup/componentCatalog.ts +68 -0
- package/scaffold/spec-site/src/mockup/useScenarios.ts +67 -0
- package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
- package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
- package/scaffold/spec-site/src/pages/DocsEditor.vue +119 -0
- package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
- package/scaffold/spec-site/src/pages/DocsPage.vue +444 -0
- package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
- package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
- package/scaffold/spec-site/src/pages/MemosPage.vue +857 -0
- package/scaffold/spec-site/src/pages/MockupEditorPage.vue +611 -0
- package/scaffold/spec-site/src/pages/MockupListPage.vue +121 -0
- package/scaffold/spec-site/src/pages/MockupViewerPage.vue +199 -0
- package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
- package/scaffold/spec-site/src/pages/NotificationSettingsPage.vue +59 -0
- package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
- package/scaffold/spec-site/src/pages/SprintAdmin.vue +521 -0
- package/scaffold/spec-site/src/pages/SprintTimeline.vue +159 -0
- package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
- package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
- package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
- package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
- package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
- package/scaffold/spec-site/src/pages/board/KanbanBoard.vue +93 -0
- package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
- package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
- package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
- package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
- package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
- package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
- package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
- package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
- package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
- package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
- package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
- package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
- package/scaffold/spec-site/src/router.ts +141 -0
- package/scaffold/spec-site/src/styles/buttons.css +124 -0
- package/scaffold/spec-site/src/utils/parseMentions.ts +56 -0
- package/scaffold/spec-site/src/utils/timezone.ts +18 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import Icon from '@/components/Icon.vue'
|
|
3
|
+
import { ref, computed, onMounted } from 'vue'
|
|
4
|
+
import { useRouter } from 'vue-router'
|
|
5
|
+
import { apiGet, apiPost, apiDelete } from '@/composables/useTurso'
|
|
6
|
+
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
|
|
9
|
+
interface MockupPage {
|
|
10
|
+
id: number; slug: string; title: string; category: string
|
|
11
|
+
viewport: string; version: number; created_by: string; updated_at: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const mockups = ref<MockupPage[]>([])
|
|
15
|
+
const searchQuery = ref('')
|
|
16
|
+
const filteredMockups = computed(() => {
|
|
17
|
+
if (!searchQuery.value) return mockups.value
|
|
18
|
+
const q = searchQuery.value.toLowerCase()
|
|
19
|
+
return mockups.value.filter(m => m.title.toLowerCase().includes(q) || m.category.toLowerCase().includes(q))
|
|
20
|
+
})
|
|
21
|
+
const loading = ref(true)
|
|
22
|
+
|
|
23
|
+
async function deleteMockup(slug: string) {
|
|
24
|
+
if (!confirm('Are you sure you want to delete this?')) return
|
|
25
|
+
await apiDelete(`/api/v2/mockups/${slug}`)
|
|
26
|
+
await loadMockups()
|
|
27
|
+
}
|
|
28
|
+
const showCreate = ref(false)
|
|
29
|
+
const newSlug = ref('')
|
|
30
|
+
const newTitle = ref('')
|
|
31
|
+
const newViewport = ref('desktop')
|
|
32
|
+
|
|
33
|
+
async function loadMockups() {
|
|
34
|
+
loading.value = true
|
|
35
|
+
const { data } = await apiGet('/api/v2/mockups')
|
|
36
|
+
if (data?.mockups) mockups.value = data.mockups as MockupPage[]
|
|
37
|
+
loading.value = false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function createMockup() {
|
|
41
|
+
if (!newSlug.value || !newTitle.value) return
|
|
42
|
+
const targetSlug = newSlug.value
|
|
43
|
+
await apiPost('/api/v2/mockups', { slug: targetSlug, title: newTitle.value, viewport: newViewport.value })
|
|
44
|
+
newSlug.value = ''; newTitle.value = ''; showCreate.value = false
|
|
45
|
+
router.push(`/mockup-editor/${targetSlug}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatDate(d: string) {
|
|
49
|
+
if (!d) return ''
|
|
50
|
+
const date = new Date(d.endsWith('Z') ? d : d + 'Z')
|
|
51
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onMounted(loadMockups)
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<div class="mockup-list-page">
|
|
59
|
+
<div class="list-header">
|
|
60
|
+
<h1>Mockups</h1>
|
|
61
|
+
<button class="btn-new" @click="showCreate = !showCreate">{{ showCreate ? 'Cancel' : '+ New Mockup' }}</button>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Create form -->
|
|
65
|
+
<div v-if="showCreate" class="create-form">
|
|
66
|
+
<input v-model="newTitle" class="input" placeholder="Mockup title" />
|
|
67
|
+
<input v-model="newSlug" class="input" placeholder="Path (slug)" />
|
|
68
|
+
<select v-model="newViewport" class="input">
|
|
69
|
+
<option value="desktop">Desktop</option>
|
|
70
|
+
<option value="mobile">Mobile</option>
|
|
71
|
+
</select>
|
|
72
|
+
<button class="btn btn--primary" @click="createMockup" :disabled="!newSlug || !newTitle">Create</button>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<input v-if="mockups.length" v-model="searchQuery" class="search-input" placeholder="Search by title or category..." />
|
|
76
|
+
|
|
77
|
+
<div v-if="loading" class="loading">Loading...</div>
|
|
78
|
+
<div v-else-if="!mockups.length" class="empty">No mockups yet. Create a new mockup to get started.</div>
|
|
79
|
+
|
|
80
|
+
<div v-else class="mockup-grid">
|
|
81
|
+
<div v-for="m in filteredMockups" :key="m.id" class="mockup-card" @click="router.push(`/mockup-viewer/${m.slug}`)">
|
|
82
|
+
<div class="card-viewport">{{ m.viewport === 'mobile' ? '📱' : '<Icon name="monitor" :size="14" />' }}</div>
|
|
83
|
+
<div class="card-info">
|
|
84
|
+
<h3>{{ m.title }}</h3>
|
|
85
|
+
<div class="card-meta">
|
|
86
|
+
<span>{{ m.category }}</span>
|
|
87
|
+
<span>v{{ m.version }}</span>
|
|
88
|
+
<span>{{ formatDate(m.updated_at) }}</span>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<button class="card-edit" @click.stop="router.push(`/mockup-editor/${m.slug}`)">Edit</button>
|
|
92
|
+
<button class="card-delete" @click.stop="deleteMockup(m.slug)"><Icon name="trash" :size="14" /></button>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<style scoped>
|
|
99
|
+
.mockup-list-page { max-width: 900px; margin: 0 auto; padding: 24px 16px; }
|
|
100
|
+
.search-input { width: 100%; border: 1px solid #d1d5db; border-radius: 8px; padding: 8px 12px; font-size: 14px; margin-bottom: 12px; box-sizing: border-box; }
|
|
101
|
+
.list-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
|
102
|
+
.list-header h1 { font-size: 20px; font-weight: 700; }
|
|
103
|
+
.btn-new { background: #3b82f6; color: #fff; border: none; border-radius: 10px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
104
|
+
|
|
105
|
+
.create-form { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
106
|
+
.input { border: 1px solid rgba(0,0,0,0.15); border-radius: 8px; padding: 8px 12px; font-size: 13px; }
|
|
107
|
+
|
|
108
|
+
.mockup-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
|
|
109
|
+
.mockup-card { display: flex; align-items: center; gap: 12px; background: #fff; border-radius: 12px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); cursor: pointer; transition: all 0.2s; }
|
|
110
|
+
.mockup-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
|
111
|
+
.card-delete { border: none; background: none; font-size: 16px; cursor: pointer; opacity: 0.4; transition: opacity 0.15s; padding: 4px; }
|
|
112
|
+
.card-delete:hover { opacity: 1; }
|
|
113
|
+
.card-viewport { font-size: 28px; }
|
|
114
|
+
.card-info { flex: 1; }
|
|
115
|
+
.card-info h3 { font-size: 15px; font-weight: 600; }
|
|
116
|
+
.card-meta { display: flex; gap: 8px; font-size: 11px; color: #888; margin-top: 4px; }
|
|
117
|
+
.card-edit { background: #eff6ff; color: #2563eb; border: 1px solid #bfdbfe; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
|
|
118
|
+
|
|
119
|
+
.loading, .empty { text-align: center; color: #888; padding: 40px; }
|
|
120
|
+
@media (max-width: 640px) { .mockup-grid { grid-template-columns: 1fr; } .create-form { flex-direction: column; } }
|
|
121
|
+
</style>
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, onMounted, watch } from 'vue'
|
|
3
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
4
|
+
import { apiGet } from '@/composables/useTurso'
|
|
5
|
+
import SplitPaneLayout from '@/layouts/SplitPaneLayout.vue'
|
|
6
|
+
import MockupShell from '@/components/MockupShell.vue'
|
|
7
|
+
import MockupCanvas from '@/mockup/MockupCanvas.vue'
|
|
8
|
+
import { provideViewport } from '@/composables/useViewport'
|
|
9
|
+
import { provideActiveSection } from '@/composables/useActiveSection'
|
|
10
|
+
import { renderMarkdown } from '@/utils/markdown'
|
|
11
|
+
import { getComponentDef } from '@/mockup/componentCatalog'
|
|
12
|
+
import { useScenarios } from '@/mockup/useScenarios'
|
|
13
|
+
|
|
14
|
+
const { setMode } = provideViewport()
|
|
15
|
+
const { activeSection } = provideActiveSection()
|
|
16
|
+
|
|
17
|
+
const route = useRoute()
|
|
18
|
+
const router = useRouter()
|
|
19
|
+
const slug = computed(() => route.params.slug as string)
|
|
20
|
+
|
|
21
|
+
interface CanvasComponent {
|
|
22
|
+
id: string; componentType: string; props: Record<string, unknown>; children: CanvasComponent[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const components = ref<CanvasComponent[]>([])
|
|
26
|
+
const pageTitle = ref('')
|
|
27
|
+
const viewport = ref<'mobile' | 'desktop'>('desktop')
|
|
28
|
+
const loading = ref(true)
|
|
29
|
+
const selectedId = ref<string | null>(null)
|
|
30
|
+
|
|
31
|
+
// Scenarios
|
|
32
|
+
const token = localStorage.getItem('spec-auth-token') || ''
|
|
33
|
+
const { scenarios, activeScenarioId, overrides, fetchScenarios, selectScenario } = useScenarios(slug.value, token)
|
|
34
|
+
|
|
35
|
+
// Section tabs
|
|
36
|
+
const sections = computed(() => {
|
|
37
|
+
const cats = new Set<string>()
|
|
38
|
+
cats.add('All')
|
|
39
|
+
for (const c of components.value) {
|
|
40
|
+
const def = getComponentDef(c.componentType)
|
|
41
|
+
if (def?.category) cats.add(def.category)
|
|
42
|
+
}
|
|
43
|
+
return Array.from(cats)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const activeTab = ref('All')
|
|
47
|
+
|
|
48
|
+
const filteredComponents = computed(() => {
|
|
49
|
+
if (activeTab.value === 'All') return components.value
|
|
50
|
+
return components.value.filter(c => {
|
|
51
|
+
const def = getComponentDef(c.componentType)
|
|
52
|
+
return def?.category === activeTab.value
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Selected component spec
|
|
57
|
+
const selectedComp = computed(() => {
|
|
58
|
+
if (!selectedId.value) return null
|
|
59
|
+
return findComponent(components.value, selectedId.value)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const specTitle = computed(() => {
|
|
63
|
+
if (!selectedComp.value) return ''
|
|
64
|
+
const def = getComponentDef(selectedComp.value.componentType)
|
|
65
|
+
return `${def?.icon || ''} ${def?.name || selectedComp.value.componentType}`
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const specDescription = computed(() => {
|
|
69
|
+
if (!selectedComp.value) return ''
|
|
70
|
+
return (selectedComp.value.props.specDescription as string) || ''
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
function findComponent(list: CanvasComponent[], id: string): CanvasComponent | null {
|
|
74
|
+
for (const c of list) {
|
|
75
|
+
if (c.id === id) return c
|
|
76
|
+
const found = findComponent(c.children || [], id)
|
|
77
|
+
if (found) return found
|
|
78
|
+
}
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function onSelect(id: string) {
|
|
83
|
+
selectedId.value = id
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function copySpec() {
|
|
87
|
+
if (specDescription.value) {
|
|
88
|
+
navigator.clipboard.writeText(specDescription.value)
|
|
89
|
+
alert('Spec copied')
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onMounted(async () => {
|
|
94
|
+
fetchScenarios()
|
|
95
|
+
const { data } = await apiGet(`/api/v2/mockups/${slug.value}`)
|
|
96
|
+
if (data?.page) {
|
|
97
|
+
pageTitle.value = (data.page as any).title || slug.value
|
|
98
|
+
viewport.value = (data.page as any).viewport === 'mobile' ? 'mobile' : 'desktop'
|
|
99
|
+
}
|
|
100
|
+
if (data?.components) {
|
|
101
|
+
components.value = buildTree(data.components as any[])
|
|
102
|
+
}
|
|
103
|
+
loading.value = false
|
|
104
|
+
if (viewport.value === 'mobile') setMode('mobile')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
function buildTree(flat: any[]): CanvasComponent[] {
|
|
108
|
+
const map = new Map<number, CanvasComponent>()
|
|
109
|
+
const roots: CanvasComponent[] = []
|
|
110
|
+
for (const f of flat) {
|
|
111
|
+
map.set(f.id, { id: `c-${f.id}`, componentType: f.component_type, props: JSON.parse(f.props || '{}'), children: [] })
|
|
112
|
+
}
|
|
113
|
+
for (const f of flat) {
|
|
114
|
+
const comp = map.get(f.id)!
|
|
115
|
+
if (f.parent_id && map.has(f.parent_id)) map.get(f.parent_id)!.children.push(comp)
|
|
116
|
+
else roots.push(comp)
|
|
117
|
+
}
|
|
118
|
+
return roots
|
|
119
|
+
}
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<template>
|
|
123
|
+
<div v-if="loading" style="text-align:center; padding:60px; color:#888;">Loading...</div>
|
|
124
|
+
|
|
125
|
+
<SplitPaneLayout v-else :spec-areas="[]">
|
|
126
|
+
<template #mockup>
|
|
127
|
+
<div class="viewer-toolbar">
|
|
128
|
+
<h2>{{ pageTitle }}</h2>
|
|
129
|
+
<button class="styled-btn styled-btn--secondary" @click="router.push(`/mockup-editor/${slug}`)">Edit</button>
|
|
130
|
+
<select v-if="scenarios.length" class="scenario-select" :value="activeScenarioId || ''" @change="selectScenario(($event.target as HTMLSelectElement).value ? Number(($event.target as HTMLSelectElement).value) : null)">
|
|
131
|
+
<option value="">Default</option>
|
|
132
|
+
<option v-for="s in scenarios" :key="s.id" :value="s.id">{{ s.name }}</option>
|
|
133
|
+
</select>
|
|
134
|
+
<button class="styled-btn styled-btn--ghost" @click="router.push('/mockups')">List</button>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="section-tabs">
|
|
137
|
+
<button v-for="sec in sections" :key="sec" class="tab-btn" :class="{ active: activeTab === sec }" @click="activeTab = sec">{{ sec }}</button>
|
|
138
|
+
</div>
|
|
139
|
+
<MockupShell>
|
|
140
|
+
<MockupCanvas
|
|
141
|
+
:components="filteredComponents"
|
|
142
|
+
:selected-id="selectedId"
|
|
143
|
+
:viewport="viewport"
|
|
144
|
+
:view-mode="true"
|
|
145
|
+
@select="onSelect"
|
|
146
|
+
/>
|
|
147
|
+
</MockupShell>
|
|
148
|
+
</template>
|
|
149
|
+
|
|
150
|
+
<template #spec>
|
|
151
|
+
<div class="spec-panel-v2">
|
|
152
|
+
<template v-if="selectedComp">
|
|
153
|
+
<div class="spec-header">
|
|
154
|
+
<span class="spec-comp-name">{{ specTitle }}</span>
|
|
155
|
+
<span class="spec-comp-type">{{ selectedComp.componentType }}</span>
|
|
156
|
+
</div>
|
|
157
|
+
<div v-if="specDescription" class="spec-body" v-html="renderMarkdown(specDescription)"></div>
|
|
158
|
+
<div v-else class="spec-empty">No spec description. Add one in the editor.</div>
|
|
159
|
+
<div class="spec-actions">
|
|
160
|
+
<button v-if="specDescription" class="styled-btn styled-btn--ghost" @click="copySpec">Copy</button>
|
|
161
|
+
</div>
|
|
162
|
+
</template>
|
|
163
|
+
<div v-else class="spec-guide">
|
|
164
|
+
<p>Click a component to view its spec.</p>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</template>
|
|
168
|
+
</SplitPaneLayout>
|
|
169
|
+
</template>
|
|
170
|
+
|
|
171
|
+
<style scoped>
|
|
172
|
+
.viewer-toolbar { display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-bottom: 1px solid var(--border, #e5e7eb); }
|
|
173
|
+
.viewer-toolbar h2 { font-size: 16px; font-weight: 600; flex: 1; }
|
|
174
|
+
.styled-btn { border: none; border-radius: 8px; padding: 6px 14px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s; }
|
|
175
|
+
.styled-btn--secondary { background: #f3f4f6; color: #374151; }
|
|
176
|
+
.styled-btn--secondary:hover { background: #e5e7eb; }
|
|
177
|
+
.styled-btn--ghost { background: none; color: #6b7280; }
|
|
178
|
+
.styled-btn--ghost:hover { background: #f9fafb; }
|
|
179
|
+
|
|
180
|
+
.section-tabs { display: flex; gap: 4px; padding: 4px 16px; border-bottom: 1px solid var(--border, #e5e7eb); overflow-x: auto; }
|
|
181
|
+
.tab-btn { border: none; background: none; padding: 6px 12px; font-size: 13px; color: #6b7280; cursor: pointer; border-radius: 6px; white-space: nowrap; }
|
|
182
|
+
.tab-btn.active { background: #3b82f6; color: #fff; }
|
|
183
|
+
.tab-btn:hover:not(.active) { background: #f3f4f6; }
|
|
184
|
+
|
|
185
|
+
.scenario-select { border: 1px solid #d1d5db; border-radius: 6px; padding: 4px 8px; font-size: 12px; }
|
|
186
|
+
.search-input { width: 100%; border: 1px solid #d1d5db; border-radius: 8px; padding: 8px 12px; font-size: 14px; margin-bottom: 12px; box-sizing: border-box; }
|
|
187
|
+
.spec-panel-v2 { padding: 20px; height: 100%; overflow-y: auto; }
|
|
188
|
+
.spec-header { margin-bottom: 16px; }
|
|
189
|
+
.spec-comp-name { font-size: 16px; font-weight: 700; display: block; }
|
|
190
|
+
.spec-comp-type { font-size: 12px; color: #9ca3af; }
|
|
191
|
+
.spec-body { font-size: 14px; line-height: 1.7; color: var(--text-primary, #1c1c1e); }
|
|
192
|
+
.spec-body :deep(h1) { font-size: 18px; font-weight: 700; margin: 16px 0 8px; }
|
|
193
|
+
.spec-body :deep(h2) { font-size: 16px; font-weight: 600; margin: 12px 0 6px; }
|
|
194
|
+
.spec-body :deep(code) { background: #f1f3f5; padding: 1px 4px; border-radius: 3px; font-size: 12px; }
|
|
195
|
+
.spec-body :deep(ul) { padding-left: 16px; }
|
|
196
|
+
.spec-empty { color: #9ca3af; font-size: 13px; font-style: italic; }
|
|
197
|
+
.spec-actions { margin-top: 16px; }
|
|
198
|
+
.spec-guide { display: flex; align-items: center; justify-content: center; height: 100%; color: #9ca3af; font-size: 14px; }
|
|
199
|
+
</style>
|