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.
Files changed (165) hide show
  1. package/bin/cli.mjs +204 -2
  2. package/lib/doctor.mjs +38 -1
  3. package/lib/hydrate.mjs +15 -0
  4. package/lib/scaffold.mjs +5 -0
  5. package/lib/setup-wizard.mjs +35 -2
  6. package/package.json +1 -1
  7. package/scaffold/.context/project.yaml.example +19 -0
  8. package/scaffold/mcp-notification-server/package.json +18 -0
  9. package/scaffold/mcp-notification-server/src/index.ts +275 -0
  10. package/scaffold/mcp-notification-server/src/turso-client.ts +142 -0
  11. package/scaffold/mcp-notification-server/tsconfig.json +14 -0
  12. package/scaffold/mcp-pm/package.json +19 -0
  13. package/scaffold/mcp-pm/src/api-client.ts +69 -0
  14. package/scaffold/mcp-pm/src/index.ts +660 -0
  15. package/scaffold/mcp-pm/tsconfig.json +14 -0
  16. package/scaffold/pm-api/package.json +21 -0
  17. package/scaffold/pm-api/sql/001-memo-v2.sql +49 -0
  18. package/scaffold/pm-api/sql/002-notifications.sql +18 -0
  19. package/scaffold/pm-api/sql/003-content.sql +66 -0
  20. package/scaffold/pm-api/sql/004-agent-events.sql +21 -0
  21. package/scaffold/pm-api/sql/005-epic-sprint-decoupling.sql +6 -0
  22. package/scaffold/pm-api/sql/schema-core.sql +331 -0
  23. package/scaffold/pm-api/sql/schema-docs.sql +25 -0
  24. package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
  25. package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
  26. package/scaffold/pm-api/src/auth.ts +28 -0
  27. package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
  28. package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
  29. package/scaffold/pm-api/src/db/adapter.ts +36 -0
  30. package/scaffold/pm-api/src/db/turso.ts +147 -0
  31. package/scaffold/pm-api/src/index.ts +114 -0
  32. package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
  33. package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
  34. package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
  35. package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
  36. package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
  37. package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
  38. package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
  39. package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
  40. package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
  41. package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
  42. package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
  43. package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
  44. package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
  45. package/scaffold/pm-api/src/mcp.ts +871 -0
  46. package/scaffold/pm-api/src/nudge.ts +283 -0
  47. package/scaffold/pm-api/src/routes/auth.ts +32 -0
  48. package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
  49. package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
  50. package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
  51. package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
  52. package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
  53. package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
  54. package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
  55. package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
  56. package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
  57. package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
  58. package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
  59. package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
  60. package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
  61. package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
  62. package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
  63. package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
  64. package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
  65. package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
  66. package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
  67. package/scaffold/pm-api/src/types.ts +11 -0
  68. package/scaffold/pm-api/src/utils/activity.ts +22 -0
  69. package/scaffold/pm-api/src/utils/admin.ts +9 -0
  70. package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
  71. package/scaffold/pm-api/src/utils/assignee.ts +69 -0
  72. package/scaffold/pm-api/src/utils/db.ts +45 -0
  73. package/scaffold/pm-api/src/utils/initiative.ts +23 -0
  74. package/scaffold/pm-api/src/utils/retro-link.ts +32 -0
  75. package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
  76. package/scaffold/pm-api/tsconfig.json +15 -0
  77. package/scaffold/pm-api/wrangler.toml.hbs +11 -0
  78. package/scaffold/spec-site/package-lock.json +892 -0
  79. package/scaffold/spec-site/package.json +15 -1
  80. package/scaffold/spec-site/src/api/types.ts +6 -0
  81. package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
  82. package/scaffold/spec-site/src/components/AuthGate.vue +117 -0
  83. package/scaffold/spec-site/src/components/BurndownChart.vue +78 -0
  84. package/scaffold/spec-site/src/components/DocComments.vue +137 -0
  85. package/scaffold/spec-site/src/components/DocEditor.vue +118 -0
  86. package/scaffold/spec-site/src/components/DocExportBar.vue +110 -0
  87. package/scaffold/spec-site/src/components/DocsSidebar.vue +309 -0
  88. package/scaffold/spec-site/src/components/EmptyState.vue +30 -0
  89. package/scaffold/spec-site/src/components/ErrorBanner.vue +38 -0
  90. package/scaffold/spec-site/src/components/Icon.vue +58 -0
  91. package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
  92. package/scaffold/spec-site/src/components/MemoChecklist.vue +88 -0
  93. package/scaffold/spec-site/src/components/MemoGraph.vue +75 -0
  94. package/scaffold/spec-site/src/components/MemoItem.vue +353 -0
  95. package/scaffold/spec-site/src/components/MemoRelations.vue +101 -0
  96. package/scaffold/spec-site/src/components/MemoTimeline.vue +53 -0
  97. package/scaffold/spec-site/src/components/MentionInput.vue +174 -0
  98. package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
  99. package/scaffold/spec-site/src/components/PriorityBadge.vue +23 -0
  100. package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
  101. package/scaffold/spec-site/src/components/SlashCommand.ts +123 -0
  102. package/scaffold/spec-site/src/components/StateDisplay.vue +54 -0
  103. package/scaffold/spec-site/src/components/TreeNode.vue +82 -0
  104. package/scaffold/spec-site/src/components/UserAvatar.vue +24 -0
  105. package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
  106. package/scaffold/spec-site/src/composables/navTypes.ts +3 -0
  107. package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
  108. package/scaffold/spec-site/src/composables/useBottomSheet.ts +103 -0
  109. package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
  110. package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
  111. package/scaffold/spec-site/src/composables/useMemo.ts +39 -0
  112. package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
  113. package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
  114. package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
  115. package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
  116. package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
  117. package/scaffold/spec-site/src/composables/useTurso.ts +17 -0
  118. package/scaffold/spec-site/src/composables/useUser.ts +19 -1
  119. package/scaffold/spec-site/src/composables/useViewport.ts +26 -0
  120. package/scaffold/spec-site/src/features.ts +108 -0
  121. package/scaffold/spec-site/src/mockup/ComponentPalette.vue +61 -0
  122. package/scaffold/spec-site/src/mockup/MockupCanvas.vue +459 -0
  123. package/scaffold/spec-site/src/mockup/PropertyPanel.vue +217 -0
  124. package/scaffold/spec-site/src/mockup/componentCatalog.ts +68 -0
  125. package/scaffold/spec-site/src/mockup/useScenarios.ts +67 -0
  126. package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
  127. package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
  128. package/scaffold/spec-site/src/pages/DocsEditor.vue +119 -0
  129. package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
  130. package/scaffold/spec-site/src/pages/DocsPage.vue +444 -0
  131. package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
  132. package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
  133. package/scaffold/spec-site/src/pages/MemosPage.vue +857 -0
  134. package/scaffold/spec-site/src/pages/MockupEditorPage.vue +611 -0
  135. package/scaffold/spec-site/src/pages/MockupListPage.vue +121 -0
  136. package/scaffold/spec-site/src/pages/MockupViewerPage.vue +199 -0
  137. package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
  138. package/scaffold/spec-site/src/pages/NotificationSettingsPage.vue +59 -0
  139. package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
  140. package/scaffold/spec-site/src/pages/SprintAdmin.vue +521 -0
  141. package/scaffold/spec-site/src/pages/SprintTimeline.vue +159 -0
  142. package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
  143. package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
  144. package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
  145. package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
  146. package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
  147. package/scaffold/spec-site/src/pages/board/KanbanBoard.vue +93 -0
  148. package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
  149. package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
  150. package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
  151. package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
  152. package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
  153. package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
  154. package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
  155. package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
  156. package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
  157. package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
  158. package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
  159. package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
  160. package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
  161. package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
  162. package/scaffold/spec-site/src/router.ts +141 -0
  163. package/scaffold/spec-site/src/styles/buttons.css +124 -0
  164. package/scaffold/spec-site/src/utils/parseMentions.ts +56 -0
  165. package/scaffold/spec-site/src/utils/timezone.ts +18 -0
@@ -0,0 +1,389 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+ import { apiGet, apiPost } from '@/api/client'
5
+ import { useUser } from '@/composables/useUser'
6
+
7
+ const route = useRoute()
8
+ const router = useRouter()
9
+ const { dynamicMembers, loadMembers } = useUser()
10
+
11
+ const sprintId = computed(() => route.params.sprintId as string)
12
+
13
+ // ── State ──
14
+ const step = ref<'create' | 'checkin' | 'plan' | 'done'>('create')
15
+ const loading = ref(false)
16
+ const error = ref('')
17
+ const retroActions = ref<Array<{ id: number; content: string; assignee: string | null }>>([])
18
+
19
+ // Step 1: Create
20
+ const newSprint = ref({ id: '', label: '', theme: '', startDate: '', endDate: '' })
21
+ const totalWorkingDays = ref(0)
22
+
23
+ // Step 2: Checkin
24
+ interface MemberInfo { id: number; display_name: string; checked: boolean }
25
+ const allMembers = ref<MemberInfo[]>([])
26
+ const absences = ref<Record<number, string[]>>({})
27
+ const absenceInput = ref<Record<number, string>>({})
28
+
29
+ // Step 3: Plan
30
+ const planData = ref<{
31
+ sprint: { velocity: number | null; start_date: string; end_date: string; status: string }
32
+ members: Array<{ member_id: number; display_name: string; working_days: number }>
33
+ absences: Array<{ member_id: number; absence_date: string; reason: string | null }>
34
+ stories: Array<{ id: number; title: string; story_points: number | null; assignee: string | null }>
35
+ totalSP: number
36
+ velocity: number
37
+ totalWorkingDays: number
38
+ } | null>(null)
39
+
40
+ // Backlog stories for selection
41
+ const backlogStories = ref<Array<{ id: number; title: string; story_points: number | null }>>([])
42
+ const selectedStoryIds = ref<Set<number>>(new Set())
43
+
44
+ const selectedSP = computed(() => {
45
+ return backlogStories.value
46
+ .filter(s => selectedStoryIds.value.has(s.id))
47
+ .reduce((sum, s) => sum + (s.story_points ?? 0), 0)
48
+ })
49
+
50
+ // ── Actions ──
51
+ async function createSprint() {
52
+ if (!newSprint.value.id || !newSprint.value.startDate || !newSprint.value.endDate) {
53
+ error.value = 'ID, start date, and end date are required'; return
54
+ }
55
+ loading.value = true; error.value = ''
56
+ const { data, error: e } = await apiPost('/api/v2/kickoff/create', newSprint.value)
57
+ loading.value = false
58
+ if (e) { error.value = e; return }
59
+ totalWorkingDays.value = (data as { totalWorkingDays: number }).totalWorkingDays
60
+ step.value = 'checkin'
61
+ }
62
+
63
+ async function loadMemberList() {
64
+ const { data } = await apiGet<{ members: Array<{ id: number; display_name: string }> }>('/api/v2/admin/members')
65
+ if (data?.members) {
66
+ allMembers.value = data.members.map(m => ({ ...m, checked: false }))
67
+ }
68
+ }
69
+
70
+ async function addAbsence(memberId: number) {
71
+ const date = absenceInput.value[memberId]
72
+ if (!date) return
73
+ loading.value = true
74
+ await apiPost(`/api/v2/kickoff/${newSprint.value.id || sprintId.value}/absence`, {
75
+ memberId, dates: [date]
76
+ })
77
+ if (!absences.value[memberId]) absences.value[memberId] = []
78
+ absences.value[memberId].push(date)
79
+ absenceInput.value[memberId] = ''
80
+ loading.value = false
81
+ }
82
+
83
+ async function submitCheckin() {
84
+ const checkedIds = allMembers.value.filter(m => m.checked).map(m => m.id)
85
+ if (!checkedIds.length) { error.value = 'Please select participants'; return }
86
+ loading.value = true; error.value = ''
87
+ const sid = newSprint.value.id || sprintId.value
88
+ const { error: e } = await apiPost(`/api/v2/kickoff/${sid}/checkin`, { memberIds: checkedIds })
89
+ loading.value = false
90
+ if (e) { error.value = e; return }
91
+ await loadPlan()
92
+ step.value = 'plan'
93
+ }
94
+
95
+ async function loadPlan() {
96
+ const sid = newSprint.value.id || sprintId.value
97
+ const { data } = await apiGet<typeof planData.value>(`/api/v2/kickoff/${sid}/plan`)
98
+ if (data) planData.value = data
99
+
100
+ // Load backlog stories
101
+ const { data: blData } = await apiGet<{ stories: Array<{ id: number; title: string; story_points: number | null }> }>('/api/v2/pm/data?sprint=backlog')
102
+ if (blData?.stories) backlogStories.value = blData.stories
103
+ }
104
+
105
+ function toggleStory(id: number) {
106
+ if (selectedStoryIds.value.has(id)) selectedStoryIds.value.delete(id)
107
+ else selectedStoryIds.value.add(id)
108
+ }
109
+
110
+ async function doKickoff() {
111
+ const sid = newSprint.value.id || sprintId.value
112
+ loading.value = true; error.value = ''
113
+ const { error: e } = await apiPost(`/api/v2/nav/sprints/${sid}/kickoff`, {
114
+ storyIds: [...selectedStoryIds.value],
115
+ velocity: planData.value?.velocity,
116
+ })
117
+ loading.value = false
118
+ if (e) { error.value = e; return }
119
+ step.value = 'done'
120
+ }
121
+
122
+ onMounted(async () => {
123
+ await loadMembers()
124
+ await loadMemberList()
125
+
126
+ // Load retro action items from the most recently closed sprint
127
+ try {
128
+ const { data: navData } = await apiGet<{ sprints: Array<{ id: string; status: string }> }>('/api/v2/nav')
129
+ const lastClosed = navData?.sprints?.filter((s: { status: string }) => s.status === 'closed')?.[0]
130
+ if (lastClosed) {
131
+ const { data: actData } = await apiGet<{ actions: Array<{ id: number; content: string; assignee: string | null }> }>(`/api/v2/retro/${lastClosed.id}/actions`)
132
+ if (actData?.actions) retroActions.value = actData.actions
133
+ }
134
+ } catch (_) { /* ignore if no retro data */ }
135
+
136
+ // If sprintId in URL, check existing sprint status
137
+ if (sprintId.value && sprintId.value !== 'new') {
138
+ newSprint.value.id = sprintId.value
139
+ const { data } = await apiGet<{ sprint: { status: string } }>(`/api/v2/kickoff/${sprintId.value}/plan`)
140
+ if (data?.sprint) {
141
+ if (data.sprint.status === 'planning') {
142
+ await loadPlan()
143
+ step.value = 'plan'
144
+ } else if (data.sprint.status === 'active') {
145
+ error.value = `${sprintId.value} is already an active sprint. Navigate to /kickoff/new to create a new sprint.`
146
+ } else {
147
+ error.value = `${sprintId.value} is already a closed sprint.`
148
+ }
149
+ }
150
+ }
151
+ })
152
+ </script>
153
+
154
+ <template>
155
+ <div class="kickoff">
156
+ <!-- Step indicator -->
157
+ <div class="steps">
158
+ <div class="step-item" :class="{ active: step === 'create', done: step !== 'create' }">
159
+ <span class="step-num">1</span> Create Sprint
160
+ </div>
161
+ <div class="step-line" />
162
+ <div class="step-item" :class="{ active: step === 'checkin', done: step === 'plan' || step === 'done' }">
163
+ <span class="step-num">2</span> Team Check-in
164
+ </div>
165
+ <div class="step-line" />
166
+ <div class="step-item" :class="{ active: step === 'plan', done: step === 'done' }">
167
+ <span class="step-num">3</span> Story Selection + Kickoff
168
+ </div>
169
+ </div>
170
+
171
+ <p v-if="error" class="error-msg">{{ error }}</p>
172
+
173
+ <!-- Previous retro action items -->
174
+ <div v-if="retroActions.length && step === 'create'" class="retro-actions-info">
175
+ <h3>Previous Retro Action Items (added to backlog)</h3>
176
+ <div v-for="a in retroActions" :key="a.id" class="retro-action-item">
177
+ {{ a.content }}
178
+ <span v-if="a.assignee" class="retro-assignee">{{ a.assignee }}</span>
179
+ </div>
180
+ </div>
181
+
182
+ <!-- Step 1: Create Sprint -->
183
+ <section v-if="step === 'create' && !error" class="kickoff-section">
184
+ <h2>Create Sprint</h2>
185
+ <div class="form-grid">
186
+ <div class="form-field">
187
+ <label>Sprint ID</label>
188
+ <input v-model="newSprint.id" class="input" placeholder="s56" />
189
+ </div>
190
+ <div class="form-field">
191
+ <label>Label</label>
192
+ <input v-model="newSprint.label" class="input" placeholder="S56" />
193
+ </div>
194
+ <div class="form-field full">
195
+ <label>Theme</label>
196
+ <input v-model="newSprint.theme" class="input" placeholder="Sprint goal / theme" />
197
+ </div>
198
+ <div class="form-field">
199
+ <label>Start Date</label>
200
+ <input v-model="newSprint.startDate" type="date" class="input" />
201
+ </div>
202
+ <div class="form-field">
203
+ <label>End Date</label>
204
+ <input v-model="newSprint.endDate" type="date" class="input" />
205
+ </div>
206
+ </div>
207
+ <button class="btn btn--primary" :disabled="loading" @click="createSprint">
208
+ Next &rarr;
209
+ </button>
210
+ </section>
211
+
212
+ <!-- Step 2: Team Check-in -->
213
+ <section v-if="step === 'checkin'" class="kickoff-section">
214
+ <h2>Team Check-in</h2>
215
+ <p class="section-desc">Select team members participating in this sprint and enter any planned absences.</p>
216
+ <p class="info-badge">Total working days: <strong>{{ totalWorkingDays }}</strong></p>
217
+
218
+ <div class="member-list">
219
+ <div v-for="m in allMembers" :key="m.id" class="member-card" :class="{ checked: m.checked }">
220
+ <div class="member-header">
221
+ <label class="member-check">
222
+ <input type="checkbox" v-model="m.checked" />
223
+ <span class="member-name">{{ m.display_name }}</span>
224
+ </label>
225
+ </div>
226
+ <!-- Absence input (checked members only) -->
227
+ <div v-if="m.checked" class="absence-section">
228
+ <div class="absence-tags">
229
+ <span v-for="(d, i) in (absences[m.id] || [])" :key="i" class="absence-tag">
230
+ {{ d }} &times;
231
+ </span>
232
+ </div>
233
+ <div class="absence-input-row">
234
+ <input v-model="absenceInput[m.id]" type="date" class="input input--sm" placeholder="Absence date" />
235
+ <button class="btn btn--sm" :disabled="!absenceInput[m.id]" @click="addAbsence(m.id)">Add</button>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ </div>
240
+
241
+ <button class="btn btn--primary" :disabled="loading || !allMembers.some(m => m.checked)" @click="submitCheckin">
242
+ Next &rarr;
243
+ </button>
244
+ </section>
245
+
246
+ <!-- Step 3: Plan + Kickoff -->
247
+ <section v-if="step === 'plan' && planData" class="kickoff-section">
248
+ <h2>Story Selection + Kickoff</h2>
249
+
250
+ <!-- Velocity summary -->
251
+ <div class="velocity-summary">
252
+ <div class="velocity-card">
253
+ <div class="velocity-label">Team Velocity</div>
254
+ <div class="velocity-value">{{ planData.velocity }} WD</div>
255
+ <div class="velocity-sub">{{ planData.members.length }} members &times; {{ planData.totalWorkingDays }} working days</div>
256
+ </div>
257
+ <div class="velocity-card">
258
+ <div class="velocity-label">Selected SP</div>
259
+ <div class="velocity-value" :class="{ over: selectedSP > planData.velocity }">{{ selectedSP }}</div>
260
+ <div v-if="selectedSP > planData.velocity" class="velocity-warn">Exceeds velocity!</div>
261
+ </div>
262
+ </div>
263
+
264
+ <!-- Per-member working days -->
265
+ <div class="wd-grid">
266
+ <div v-for="m in planData.members" :key="m.member_id" class="wd-chip">
267
+ {{ m.display_name }}: <strong>{{ m.working_days }} days</strong>
268
+ </div>
269
+ </div>
270
+
271
+ <!-- Backlog story selection -->
272
+ <h3>Select from Backlog</h3>
273
+ <div v-if="!backlogStories.length" class="empty">Backlog is empty</div>
274
+ <div v-else class="story-list">
275
+ <div v-for="s in backlogStories" :key="s.id"
276
+ class="story-card" :class="{ selected: selectedStoryIds.has(s.id) }"
277
+ @click="toggleStory(s.id)">
278
+ <input type="checkbox" :checked="selectedStoryIds.has(s.id)" @click.stop="toggleStory(s.id)" />
279
+ <span class="story-title">{{ s.title }}</span>
280
+ <span class="story-sp">{{ s.story_points ?? '-' }} SP</span>
281
+ </div>
282
+ </div>
283
+
284
+ <!-- Already assigned stories -->
285
+ <div v-if="planData.stories.length" class="assigned-stories">
286
+ <h3>Already Assigned ({{ planData.stories.length }} stories, {{ planData.totalSP }} SP)</h3>
287
+ <div v-for="s in planData.stories" :key="s.id" class="story-card assigned">
288
+ <span class="story-title">{{ s.title }}</span>
289
+ <span class="story-sp">{{ s.story_points ?? '-' }} SP</span>
290
+ </div>
291
+ </div>
292
+
293
+ <button class="btn btn--primary btn--lg" :disabled="loading" @click="doKickoff">
294
+ Kickoff! ({{ selectedSP + planData.totalSP }} SP)
295
+ </button>
296
+ </section>
297
+
298
+ <!-- Step 4: Done -->
299
+ <section v-if="step === 'done'" class="kickoff-section done-section">
300
+ <h2>Kickoff Complete!</h2>
301
+ <p>The sprint is now active.</p>
302
+ <button class="btn btn--primary" @click="router.push('/board/' + (newSprint.id || sprintId))">Go to Board &rarr;</button>
303
+ </section>
304
+ </div>
305
+ </template>
306
+
307
+ <style scoped>
308
+ .kickoff { max-width: 720px; margin: 0 auto; padding: 24px; }
309
+
310
+ /* Steps */
311
+ .steps { display: flex; align-items: center; gap: 0; margin-bottom: 32px; }
312
+ .step-item { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-muted); font-weight: 500; }
313
+ .step-item.active { color: #3b82f6; font-weight: 700; }
314
+ .step-item.done { color: #16a34a; }
315
+ .step-num { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; background: rgba(0,0,0,0.06); color: var(--text-secondary); }
316
+ .step-item.active .step-num { background: #3b82f6; color: white; }
317
+ .step-item.done .step-num { background: #16a34a; color: white; }
318
+ .step-line { flex: 1; height: 2px; background: rgba(0,0,0,0.06); margin: 0 8px; }
319
+
320
+ .kickoff-section { margin-bottom: 32px; }
321
+ .kickoff-section h2 { margin-bottom: 12px; font-size: 18px; }
322
+ .section-desc { color: var(--text-secondary); font-size: 14px; margin-bottom: 16px; }
323
+ .info-badge { background: #eff6ff; color: #2563eb; padding: 8px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 16px; display: inline-block; }
324
+
325
+ /* Form */
326
+ .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 20px; }
327
+ .form-field { display: flex; flex-direction: column; gap: 4px; }
328
+ .form-field.full { grid-column: 1 / -1; }
329
+ .form-field label { font-size: 12px; font-weight: 600; color: var(--text-secondary); }
330
+ .input { padding: 8px 12px; border: 1px solid rgba(0,0,0,0.06); border-radius: 6px; font-size: 14px; background: rgba(255,255,255,0.25); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); }
331
+ .input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59,130,246,0.15); }
332
+ .input--sm { padding: 4px 8px; font-size: 12px; }
333
+
334
+ /* Members */
335
+ .member-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 20px; }
336
+ .member-card { border: 1px solid rgba(0,0,0,0.06); border-radius: 8px; padding: 12px; background: rgba(255,255,255,0.25); backdrop-filter: blur(40px) saturate(1.8); -webkit-backdrop-filter: blur(40px) saturate(1.8); }
337
+ .member-card.checked { border-color: #3b82f6; background: rgba(59,130,246,0.06); }
338
+ .member-header { display: flex; align-items: center; }
339
+ .member-check { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; font-weight: 500; }
340
+ .absence-section { margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(0,0,0,0.04); }
341
+ .absence-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }
342
+ .absence-tag { background: #fef3c7; color: #92400e; padding: 2px 8px; border-radius: 4px; font-size: 11px; }
343
+ .absence-input-row { display: flex; gap: 6px; align-items: center; }
344
+
345
+ /* Velocity */
346
+ .velocity-summary { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 20px; }
347
+ .velocity-card { background: rgba(255,255,255,0.25); backdrop-filter: blur(40px) saturate(1.8); -webkit-backdrop-filter: blur(40px) saturate(1.8); border: 1px solid rgba(255,255,255,0.45); box-shadow: 0 2px 12px rgba(0,0,0,0.03), inset 0 1px 0 rgba(255,255,255,0.7); border-radius: 8px; padding: 16px; text-align: center; }
348
+ .velocity-label { font-size: 12px; color: var(--text-secondary); font-weight: 600; }
349
+ .velocity-value { font-size: 28px; font-weight: 800; color: var(--text-primary); margin: 4px 0; }
350
+ .velocity-value.over { color: #dc2626; }
351
+ .velocity-sub { font-size: 11px; color: var(--text-muted); }
352
+ .velocity-warn { font-size: 12px; color: #dc2626; font-weight: 600; }
353
+
354
+ .wd-grid { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 20px; }
355
+ .wd-chip { background: rgba(0,0,0,0.04); padding: 4px 10px; border-radius: 4px; font-size: 12px; }
356
+
357
+ /* Stories */
358
+ .story-list { display: flex; flex-direction: column; gap: 4px; margin-bottom: 20px; }
359
+ .story-card { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border: 1px solid rgba(0,0,0,0.06); border-radius: 6px; cursor: pointer; font-size: 13px; background: rgba(255,255,255,0.25); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); }
360
+ .story-card.selected { background: rgba(59,130,246,0.08); border-color: #3b82f6; }
361
+ .story-card.assigned { background: rgba(34,197,94,0.06); border-color: rgba(34,197,94,0.2); cursor: default; }
362
+ .story-title { flex: 1; }
363
+ .story-sp { font-weight: 600; min-width: 4rem; text-align: right; }
364
+
365
+ .assigned-stories { margin-bottom: 20px; }
366
+ .assigned-stories h3 { font-size: 14px; color: var(--text-secondary); margin-bottom: 8px; }
367
+
368
+ /* Buttons */
369
+ .btn { padding: 8px 16px; border-radius: 6px; border: none; font-size: 14px; font-weight: 600; cursor: pointer; }
370
+ .btn--sm { padding: 4px 8px; font-size: 12px; }
371
+ .btn--lg { padding: 12px 24px; font-size: 16px; width: 100%; }
372
+ .btn--primary { background: #3b82f6; color: white; }
373
+ .btn--primary:hover { background: #2563eb; }
374
+ .btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
375
+
376
+ .done-section { text-align: center; padding: 48px 0; }
377
+ .error-msg { color: #dc2626; background: #fef2f2; padding: 8px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 16px; }
378
+ .empty { color: var(--text-muted); padding: 16px; text-align: center; }
379
+ .retro-actions-info { background: rgba(59,130,246,0.06); border: 1px solid rgba(59,130,246,0.15); border-radius: 12px; padding: 16px; margin-bottom: 16px; }
380
+ .retro-actions-info h3 { font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 8px; }
381
+ .retro-action-item { font-size: 13px; padding: 4px 0; display: flex; justify-content: space-between; }
382
+ .retro-assignee { font-size: 11px; color: var(--text-muted); }
383
+
384
+ @media (max-width: 640px) {
385
+ .form-grid { grid-template-columns: 1fr; }
386
+ .velocity-summary { grid-template-columns: 1fr; }
387
+ .steps { flex-wrap: wrap; gap: 4px; }
388
+ }
389
+ </style>
@@ -0,0 +1,52 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ label: string
4
+ type?: 'status' | 'priority' | 'area'
5
+ value: string
6
+ }>()
7
+
8
+ const statusColors: Record<string, string> = {
9
+ 'draft': '#94a3b8',
10
+ 'backlog': '#a78bfa',
11
+ 'ready': '#3b82f6',
12
+ 'in-progress': '#f59e0b',
13
+ 'review': '#8b5cf6',
14
+ 'done': '#22c55e',
15
+ 'todo': '#94a3b8',
16
+ }
17
+
18
+ const priorityColors: Record<string, string> = {
19
+ 'low': '#94a3b8',
20
+ 'medium': '#3b82f6',
21
+ 'high': '#f59e0b',
22
+ 'critical': '#ef4444',
23
+ }
24
+
25
+ function getColor(type: string, value: string): string {
26
+ if (type === 'priority') return priorityColors[value] ?? '#94a3b8'
27
+ return statusColors[value] ?? '#94a3b8'
28
+ }
29
+ </script>
30
+
31
+ <template>
32
+ <span
33
+ class="status-badge"
34
+ :style="{
35
+ '--badge-color': getColor(type ?? 'status', value),
36
+ }"
37
+ >{{ label }}</span>
38
+ </template>
39
+
40
+ <style scoped>
41
+ .status-badge {
42
+ display: inline-block;
43
+ padding: 2px 8px;
44
+ border-radius: 10px;
45
+ font-size: 10px;
46
+ font-weight: 600;
47
+ background: color-mix(in srgb, var(--badge-color) 15%, transparent);
48
+ color: var(--badge-color);
49
+ white-space: nowrap;
50
+ line-height: 1.6;
51
+ }
52
+ </style>