popilot 0.6.0 → 0.7.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 (112) 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-pm/package.json +19 -0
  9. package/scaffold/mcp-pm/src/api-client.ts +69 -0
  10. package/scaffold/mcp-pm/src/index.ts +660 -0
  11. package/scaffold/mcp-pm/tsconfig.json +14 -0
  12. package/scaffold/pm-api/package.json +21 -0
  13. package/scaffold/pm-api/sql/schema-core.sql +331 -0
  14. package/scaffold/pm-api/sql/schema-docs.sql +25 -0
  15. package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
  16. package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
  17. package/scaffold/pm-api/src/auth.ts +28 -0
  18. package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
  19. package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
  20. package/scaffold/pm-api/src/db/adapter.ts +36 -0
  21. package/scaffold/pm-api/src/db/turso.ts +147 -0
  22. package/scaffold/pm-api/src/index.ts +114 -0
  23. package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
  24. package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
  25. package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
  26. package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
  27. package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
  28. package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
  29. package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
  30. package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
  31. package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
  32. package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
  33. package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
  34. package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
  35. package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
  36. package/scaffold/pm-api/src/mcp.ts +871 -0
  37. package/scaffold/pm-api/src/nudge.ts +283 -0
  38. package/scaffold/pm-api/src/routes/auth.ts +32 -0
  39. package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
  40. package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
  41. package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
  42. package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
  43. package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
  44. package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
  45. package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
  46. package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
  47. package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
  48. package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
  49. package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
  50. package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
  51. package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
  52. package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
  53. package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
  54. package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
  55. package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
  56. package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
  57. package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
  58. package/scaffold/pm-api/src/types.ts +11 -0
  59. package/scaffold/pm-api/src/utils/activity.ts +22 -0
  60. package/scaffold/pm-api/src/utils/admin.ts +9 -0
  61. package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
  62. package/scaffold/pm-api/src/utils/assignee.ts +69 -0
  63. package/scaffold/pm-api/src/utils/db.ts +45 -0
  64. package/scaffold/pm-api/src/utils/initiative.ts +23 -0
  65. package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
  66. package/scaffold/pm-api/tsconfig.json +15 -0
  67. package/scaffold/pm-api/wrangler.toml.hbs +11 -0
  68. package/scaffold/spec-site/package-lock.json +40 -0
  69. package/scaffold/spec-site/package.json +4 -1
  70. package/scaffold/spec-site/src/api/types.ts +6 -0
  71. package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
  72. package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
  73. package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
  74. package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
  75. package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
  76. package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
  77. package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
  78. package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
  79. package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
  80. package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
  81. package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
  82. package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
  83. package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
  84. package/scaffold/spec-site/src/composables/useUser.ts +19 -1
  85. package/scaffold/spec-site/src/features.ts +108 -0
  86. package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
  87. package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
  88. package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
  89. package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
  90. package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
  91. package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
  92. package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
  93. package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
  94. package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
  95. package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
  96. package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
  97. package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
  98. package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
  99. package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
  100. package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
  101. package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
  102. package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
  103. package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
  104. package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
  105. package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
  106. package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
  107. package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
  108. package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
  109. package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
  110. package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
  111. package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
  112. package/scaffold/spec-site/src/router.ts +141 -0
@@ -0,0 +1,551 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch } from 'vue'
3
+ import type { StandupEntry, StandupFeedback } from '@/composables/useStandup'
4
+ import type { PmStory, PmTask } from '@/composables/usePmStore'
5
+
6
+ const props = defineProps<{
7
+ entry?: StandupEntry
8
+ userName: string
9
+ editable: boolean
10
+ sprintStories: PmStory[]
11
+ sprintTasks: PmTask[]
12
+ feedback: StandupFeedback[]
13
+ currentUser: string
14
+ }>()
15
+
16
+ const emit = defineEmits<{
17
+ save: [data: { doneText: string | null; planText: string | null; planStoryIds: number[]; blockers: string | null }]
18
+ createTask: [data: { storyId: number; title: string }]
19
+ submitFeedback: [data: { feedbackText: string; reviewType: string }]
20
+ }>()
21
+
22
+ const doneText = ref(props.entry?.doneText ?? '')
23
+ const planText = ref(props.entry?.planText ?? '')
24
+ const blockers = ref(props.entry?.blockers ?? '')
25
+ const selectedStoryIds = ref<number[]>([...(props.entry?.planStoryIds ?? [])])
26
+ const editing = ref(false)
27
+
28
+ // Task creation state
29
+ const creatingTask = ref(false)
30
+ const newTaskTitle = ref('')
31
+ const newTaskStoryId = ref<number | null>(null)
32
+
33
+ watch(() => props.entry, (e) => {
34
+ if (e && !editing.value) {
35
+ doneText.value = e.doneText ?? ''
36
+ planText.value = e.planText ?? ''
37
+ blockers.value = e.blockers ?? ''
38
+ selectedStoryIds.value = [...(e.planStoryIds ?? [])]
39
+ }
40
+ })
41
+
42
+ const myStories = computed(() =>
43
+ props.sprintStories.filter(s => s.assignee === props.userName && s.status !== 'done'),
44
+ )
45
+
46
+ interface LinkedStoryInfo {
47
+ story: PmStory
48
+ totalTasks: number
49
+ doneTasks: number
50
+ progress: number
51
+ }
52
+
53
+ const viewLinkedStories = computed<LinkedStoryInfo[]>(() => {
54
+ const ids = props.entry?.planStoryIds ?? []
55
+ return ids.map(id => {
56
+ const story = props.sprintStories.find(s => s.id === id)
57
+ if (!story) return null
58
+ const storyTasks = props.sprintTasks.filter(t => t.storyId === id)
59
+ const doneTasks = storyTasks.filter(t => t.status === 'done').length
60
+ return {
61
+ story,
62
+ totalTasks: storyTasks.length,
63
+ doneTasks,
64
+ progress: storyTasks.length > 0 ? doneTasks / storyTasks.length : 0,
65
+ }
66
+ }).filter((x): x is LinkedStoryInfo => x !== null)
67
+ })
68
+
69
+ function getStoryProgress(storyId: number): string {
70
+ const storyTasks = props.sprintTasks.filter(t => t.storyId === storyId)
71
+ const done = storyTasks.filter(t => t.status === 'done').length
72
+ return storyTasks.length > 0 ? `${done}/${storyTasks.length}` : '-'
73
+ }
74
+
75
+ function startEdit() {
76
+ selectedStoryIds.value = [...(props.entry?.planStoryIds ?? [])]
77
+ editing.value = true
78
+ }
79
+
80
+ function save() {
81
+ emit('save', {
82
+ doneText: doneText.value.trim() || null,
83
+ planText: planText.value.trim() || null,
84
+ planStoryIds: [...selectedStoryIds.value],
85
+ blockers: blockers.value.trim() || null,
86
+ })
87
+ editing.value = false
88
+ }
89
+
90
+ function cancel() {
91
+ doneText.value = props.entry?.doneText ?? ''
92
+ planText.value = props.entry?.planText ?? ''
93
+ blockers.value = props.entry?.blockers ?? ''
94
+ selectedStoryIds.value = [...(props.entry?.planStoryIds ?? [])]
95
+ editing.value = false
96
+ }
97
+
98
+ function startCreateTask() {
99
+ newTaskTitle.value = props.entry?.planText ?? ''
100
+ newTaskStoryId.value = myStories.value.length > 0 ? myStories.value[0].id : null
101
+ creatingTask.value = true
102
+ }
103
+
104
+ function onCreateTask() {
105
+ if (!newTaskStoryId.value || !newTaskTitle.value.trim()) return
106
+ emit('createTask', { storyId: newTaskStoryId.value, title: newTaskTitle.value.trim() })
107
+ creatingTask.value = false
108
+ newTaskTitle.value = ''
109
+ newTaskStoryId.value = null
110
+ }
111
+
112
+ // Feedback
113
+ const showFeedbackForm = ref(false)
114
+ const feedbackText = ref('')
115
+ const feedbackType = ref<'comment' | 'approve' | 'request_changes'>('comment')
116
+
117
+ function onSubmitFeedback() {
118
+ if (!feedbackText.value.trim()) return
119
+ emit('submitFeedback', { feedbackText: feedbackText.value.trim(), reviewType: feedbackType.value })
120
+ feedbackText.value = ''
121
+ feedbackType.value = 'comment'
122
+ showFeedbackForm.value = false
123
+ }
124
+
125
+ function feedbackIcon(type: string): string {
126
+ if (type === 'approve') return '&#10003;'
127
+ if (type === 'request_changes') return '&#8634;'
128
+ return '&#128172;'
129
+ }
130
+ </script>
131
+
132
+ <template>
133
+ <div class="standup-card" :class="{ 'has-blockers': blockers.trim() }">
134
+ <div class="card-header">
135
+ <div class="card-header-left">
136
+ <span class="card-user">{{ userName }}</span>
137
+ <span v-if="entry?.sprint" class="sprint-badge">{{ entry.sprint }}</span>
138
+ </div>
139
+ <button v-if="editable && !editing" class="btn btn--sm" @click="startEdit">Edit</button>
140
+ </div>
141
+
142
+ <template v-if="editing">
143
+ <!-- Done -->
144
+ <div class="section">
145
+ <label class="section-label done-label">Done (completed yesterday)</label>
146
+ <textarea v-model="doneText" class="textarea" rows="3" placeholder="Work completed yesterday..." />
147
+ </div>
148
+
149
+ <!-- Plan: story linking + free text -->
150
+ <div class="section">
151
+ <label class="section-label plan-label">Plan (today's work)</label>
152
+
153
+ <div v-if="myStories.length > 0" class="story-picker">
154
+ <div class="picker-header">My Stories</div>
155
+ <label v-for="s in myStories" :key="s.id" class="story-check">
156
+ <input type="checkbox" :value="s.id" v-model="selectedStoryIds" />
157
+ <span class="story-check-title">{{ s.title }}</span>
158
+ <span class="story-check-progress">{{ getStoryProgress(s.id) }}</span>
159
+ </label>
160
+ </div>
161
+
162
+ <textarea v-model="planText" class="textarea" rows="3" placeholder="Other plans..." />
163
+ </div>
164
+
165
+ <!-- Blockers -->
166
+ <div class="section">
167
+ <label class="section-label blocker-label">Blockers</label>
168
+ <textarea v-model="blockers" class="textarea" rows="2" placeholder="Blocking issues (leave empty if none)" />
169
+ </div>
170
+
171
+ <div class="card-actions">
172
+ <button class="btn btn--sm btn--primary" @click="save">Save</button>
173
+ <button class="btn btn--sm" @click="cancel">Cancel</button>
174
+ </div>
175
+ </template>
176
+
177
+ <template v-else>
178
+ <!-- Done view -->
179
+ <div class="section" v-if="doneText">
180
+ <div class="section-label done-label">Done</div>
181
+ <div class="section-text">{{ doneText }}</div>
182
+ </div>
183
+
184
+ <!-- Plan view: linked stories + free text -->
185
+ <div class="section" v-if="viewLinkedStories.length > 0 || planText">
186
+ <div class="section-label plan-label">Plan</div>
187
+
188
+ <div v-for="ls in viewLinkedStories" :key="ls.story.id" class="linked-story">
189
+ <div class="linked-story-title">{{ ls.story.title }}</div>
190
+ <div class="progress-row">
191
+ <div class="progress-bar">
192
+ <div class="progress-fill" :style="{ width: (ls.progress * 100) + '%' }" />
193
+ </div>
194
+ <span class="progress-text">{{ ls.doneTasks }}/{{ ls.totalTasks }}</span>
195
+ </div>
196
+ </div>
197
+
198
+ <div v-if="planText" class="section-text" :class="{ 'has-linked': viewLinkedStories.length > 0 }">{{ planText }}</div>
199
+
200
+ <!-- Task creation prompt -->
201
+ <div v-if="editable && planText && !creatingTask" class="create-task-cta">
202
+ <button class="btn btn--xs" @click="startCreateTask">+ Create as task</button>
203
+ </div>
204
+
205
+ <div v-if="creatingTask" class="create-task-form">
206
+ <input v-model="newTaskTitle" class="task-input" placeholder="Task title" />
207
+ <select v-model="newTaskStoryId" class="story-select">
208
+ <option :value="null" disabled>Select story...</option>
209
+ <option v-for="s in myStories" :key="s.id" :value="s.id">{{ s.title }}</option>
210
+ </select>
211
+ <div class="create-task-actions">
212
+ <button class="btn btn--xs btn--primary" @click="onCreateTask" :disabled="!newTaskStoryId || !newTaskTitle.trim()">Create</button>
213
+ <button class="btn btn--xs" @click="creatingTask = false">Cancel</button>
214
+ </div>
215
+ </div>
216
+ </div>
217
+
218
+ <!-- Blockers view -->
219
+ <div class="section" v-if="blockers">
220
+ <div class="section-label blocker-label">Blockers</div>
221
+ <div class="section-text blocker-text">{{ blockers }}</div>
222
+ </div>
223
+
224
+ <!-- Empty -->
225
+ <div v-if="!doneText && !planText && viewLinkedStories.length === 0 && !blockers" class="empty-entry">
226
+ <span v-if="editable" @click="startEdit" class="empty-cta">Click to write</span>
227
+ <span v-else>Not submitted yet</span>
228
+ </div>
229
+
230
+ <!-- Feedback Section -->
231
+ <div v-if="entry && (feedback.length > 0 || !editable)" class="feedback-section">
232
+ <div class="section-label feedback-label">Feedback ({{ feedback.length }})</div>
233
+
234
+ <div v-for="fb in feedback" :key="fb.id" class="feedback-item" :class="'feedback--' + fb.reviewType.replace('_', '-')">
235
+ <div class="feedback-header">
236
+ <span class="feedback-icon" v-html="feedbackIcon(fb.reviewType)"></span>
237
+ <span class="feedback-author">{{ fb.feedbackBy }}</span>
238
+ <span class="feedback-type-badge">{{ fb.reviewType }}</span>
239
+ </div>
240
+ <div class="feedback-text">{{ fb.feedbackText }}</div>
241
+ <div class="feedback-time">{{ fb.createdAt }}</div>
242
+ </div>
243
+
244
+ <!-- Feedback Form (visible to non-owners) -->
245
+ <div v-if="entry && !editable">
246
+ <button v-if="!showFeedbackForm" class="btn btn--xs feedback-btn" @click="showFeedbackForm = true">+ Write feedback</button>
247
+
248
+ <div v-if="showFeedbackForm" class="feedback-form">
249
+ <div class="feedback-type-select">
250
+ <label class="type-option" :class="{ active: feedbackType === 'comment' }">
251
+ <input type="radio" v-model="feedbackType" value="comment" /> Comment
252
+ </label>
253
+ <label class="type-option" :class="{ active: feedbackType === 'approve' }">
254
+ <input type="radio" v-model="feedbackType" value="approve" /> Approve
255
+ </label>
256
+ <label class="type-option" :class="{ active: feedbackType === 'request_changes' }">
257
+ <input type="radio" v-model="feedbackType" value="request_changes" /> Request Changes
258
+ </label>
259
+ </div>
260
+ <textarea v-model="feedbackText" class="textarea" rows="2" placeholder="Enter your feedback..." />
261
+ <div class="card-actions">
262
+ <button class="btn btn--xs btn--primary" @click="onSubmitFeedback" :disabled="!feedbackText.trim()">Submit</button>
263
+ <button class="btn btn--xs" @click="showFeedbackForm = false">Cancel</button>
264
+ </div>
265
+ </div>
266
+ </div>
267
+ </div>
268
+ </template>
269
+ </div>
270
+ </template>
271
+
272
+ <style scoped>
273
+ .standup-card {
274
+ background: #fff;
275
+ border: 1px solid #e2e8f0;
276
+ border-radius: 10px;
277
+ padding: 14px 16px;
278
+ transition: box-shadow 0.15s;
279
+ }
280
+ .standup-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
281
+ .standup-card.has-blockers { border-left: 3px solid #ef4444; }
282
+
283
+ .card-header {
284
+ display: flex;
285
+ align-items: center;
286
+ justify-content: space-between;
287
+ margin-bottom: 10px;
288
+ }
289
+
290
+ .card-header-left {
291
+ display: flex;
292
+ align-items: center;
293
+ gap: 6px;
294
+ }
295
+
296
+ .card-user {
297
+ font-size: 14px;
298
+ font-weight: 700;
299
+ color: #1e293b;
300
+ }
301
+
302
+ .sprint-badge {
303
+ font-size: 10px;
304
+ font-weight: 600;
305
+ color: #64748b;
306
+ background: #f1f5f9;
307
+ padding: 1px 6px;
308
+ border-radius: 4px;
309
+ text-transform: uppercase;
310
+ }
311
+
312
+ .section { margin-bottom: 8px; }
313
+
314
+ .section-label {
315
+ font-size: 10px;
316
+ font-weight: 700;
317
+ text-transform: uppercase;
318
+ letter-spacing: 0.5px;
319
+ margin-bottom: 4px;
320
+ }
321
+ .done-label { color: #22c55e; }
322
+ .plan-label { color: #3b82f6; }
323
+ .blocker-label { color: #ef4444; }
324
+
325
+ .section-text {
326
+ font-size: 13px;
327
+ color: #334155;
328
+ line-height: 1.5;
329
+ white-space: pre-wrap;
330
+ }
331
+ .section-text.has-linked { margin-top: 6px; padding-top: 6px; border-top: 1px dashed #e2e8f0; }
332
+ .blocker-text { color: #dc2626; }
333
+
334
+ .textarea {
335
+ width: 100%;
336
+ padding: 8px 10px;
337
+ border: 1px solid #e2e8f0;
338
+ border-radius: 6px;
339
+ font-size: 13px;
340
+ resize: vertical;
341
+ min-height: 40px;
342
+ }
343
+ .textarea:focus { outline: none; border-color: #3b82f6; }
344
+
345
+ .card-actions { display: flex; gap: 6px; margin-top: 8px; }
346
+
347
+ /* Story Picker (edit mode) */
348
+ .story-picker {
349
+ border: 1px solid #e2e8f0;
350
+ border-radius: 6px;
351
+ padding: 8px;
352
+ margin-bottom: 6px;
353
+ background: #f8fafc;
354
+ }
355
+ .picker-header {
356
+ font-size: 11px;
357
+ font-weight: 600;
358
+ color: #64748b;
359
+ margin-bottom: 6px;
360
+ }
361
+ .story-check {
362
+ display: flex;
363
+ align-items: center;
364
+ gap: 6px;
365
+ padding: 4px 0;
366
+ font-size: 12px;
367
+ color: #334155;
368
+ cursor: pointer;
369
+ }
370
+ .story-check input[type="checkbox"] {
371
+ flex-shrink: 0;
372
+ accent-color: #3b82f6;
373
+ }
374
+ .story-check-title {
375
+ flex: 1;
376
+ overflow: hidden;
377
+ text-overflow: ellipsis;
378
+ white-space: nowrap;
379
+ }
380
+ .story-check-progress {
381
+ flex-shrink: 0;
382
+ font-size: 11px;
383
+ color: #94a3b8;
384
+ font-weight: 600;
385
+ }
386
+
387
+ /* Linked stories (view mode) */
388
+ .linked-story {
389
+ padding: 6px 0;
390
+ }
391
+ .linked-story + .linked-story {
392
+ border-top: 1px solid #f1f5f9;
393
+ }
394
+ .linked-story-title {
395
+ font-size: 12px;
396
+ color: #334155;
397
+ font-weight: 500;
398
+ margin-bottom: 4px;
399
+ }
400
+ .progress-row {
401
+ display: flex;
402
+ align-items: center;
403
+ gap: 8px;
404
+ }
405
+ .progress-bar {
406
+ flex: 1;
407
+ height: 4px;
408
+ background: #e2e8f0;
409
+ border-radius: 2px;
410
+ overflow: hidden;
411
+ }
412
+ .progress-fill {
413
+ height: 100%;
414
+ background: #22c55e;
415
+ border-radius: 2px;
416
+ transition: width 0.3s;
417
+ }
418
+ .progress-text {
419
+ font-size: 11px;
420
+ color: #94a3b8;
421
+ font-weight: 600;
422
+ flex-shrink: 0;
423
+ }
424
+
425
+ /* Task creation */
426
+ .create-task-cta {
427
+ margin-top: 6px;
428
+ }
429
+ .create-task-form {
430
+ margin-top: 8px;
431
+ display: flex;
432
+ flex-direction: column;
433
+ gap: 6px;
434
+ }
435
+ .task-input {
436
+ width: 100%;
437
+ padding: 6px 8px;
438
+ border: 1px solid #e2e8f0;
439
+ border-radius: 4px;
440
+ font-size: 12px;
441
+ }
442
+ .task-input:focus { outline: none; border-color: #3b82f6; }
443
+ .story-select {
444
+ width: 100%;
445
+ padding: 6px 8px;
446
+ border: 1px solid #e2e8f0;
447
+ border-radius: 4px;
448
+ font-size: 12px;
449
+ background: #fff;
450
+ }
451
+ .create-task-actions { display: flex; gap: 4px; }
452
+
453
+ /* Empty state */
454
+ .empty-entry {
455
+ text-align: center;
456
+ padding: 12px;
457
+ color: #cbd5e1;
458
+ font-size: 13px;
459
+ }
460
+ .empty-cta {
461
+ cursor: pointer;
462
+ color: #3b82f6;
463
+ text-decoration: underline;
464
+ }
465
+
466
+ /* Buttons */
467
+ .btn {
468
+ padding: 6px 14px; border: 1px solid #e2e8f0; border-radius: 6px;
469
+ font-size: 13px; font-weight: 500; cursor: pointer; background: #fff; color: #475569;
470
+ transition: all 0.15s;
471
+ }
472
+ .btn:hover { background: #f1f5f9; }
473
+ .btn--sm { padding: 4px 10px; font-size: 11px; }
474
+ .btn--xs { padding: 3px 8px; font-size: 10px; }
475
+ .btn--primary { background: #1e293b; color: #fff; border-color: #1e293b; }
476
+ .btn--primary:hover { background: #334155; }
477
+ .btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
478
+
479
+ /* Feedback */
480
+ .feedback-section {
481
+ margin-top: 10px;
482
+ padding-top: 10px;
483
+ border-top: 1px solid #e2e8f0;
484
+ }
485
+ .feedback-label { color: #8b5cf6; }
486
+
487
+ .feedback-item {
488
+ padding: 8px;
489
+ margin-bottom: 6px;
490
+ border-radius: 6px;
491
+ background: #f8fafc;
492
+ border-left: 3px solid #cbd5e1;
493
+ }
494
+ .feedback--approve { border-left-color: #22c55e; background: #f0fdf4; }
495
+ .feedback--request-changes { border-left-color: #f59e0b; background: #fffbeb; }
496
+ .feedback--comment { border-left-color: #3b82f6; background: #eff6ff; }
497
+
498
+ .feedback-header {
499
+ display: flex;
500
+ align-items: center;
501
+ gap: 6px;
502
+ margin-bottom: 4px;
503
+ }
504
+ .feedback-icon { font-size: 14px; }
505
+ .feedback-author { font-size: 12px; font-weight: 600; color: #334155; }
506
+ .feedback-type-badge {
507
+ font-size: 9px;
508
+ font-weight: 600;
509
+ padding: 1px 5px;
510
+ border-radius: 3px;
511
+ background: #e2e8f0;
512
+ color: #64748b;
513
+ text-transform: uppercase;
514
+ }
515
+ .feedback-text { font-size: 12px; color: #475569; line-height: 1.5; }
516
+ .feedback-time { font-size: 10px; color: #94a3b8; margin-top: 4px; }
517
+
518
+ .feedback-btn { margin-top: 6px; color: #8b5cf6; border-color: #c4b5fd; }
519
+ .feedback-btn:hover { background: #f5f3ff; }
520
+
521
+ .feedback-form { margin-top: 8px; }
522
+ .feedback-type-select {
523
+ display: flex;
524
+ gap: 8px;
525
+ margin-bottom: 6px;
526
+ flex-wrap: wrap;
527
+ }
528
+ .type-option {
529
+ font-size: 11px;
530
+ padding: 3px 8px;
531
+ border: 1px solid #e2e8f0;
532
+ border-radius: 4px;
533
+ cursor: pointer;
534
+ display: flex;
535
+ align-items: center;
536
+ gap: 4px;
537
+ transition: all 0.1s;
538
+ }
539
+ .type-option input[type="radio"] {
540
+ position: absolute;
541
+ width: 1px;
542
+ height: 1px;
543
+ padding: 0;
544
+ margin: -1px;
545
+ overflow: hidden;
546
+ clip: rect(0, 0, 0, 0);
547
+ white-space: nowrap;
548
+ border: 0;
549
+ }
550
+ .type-option.active { border-color: #8b5cf6; background: #f5f3ff; color: #7c3aed; font-weight: 600; }
551
+ </style>
@@ -0,0 +1,68 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted } from 'vue'
3
+
4
+ const props = defineProps<{
5
+ existing?: { doneText: string | null; planText: string | null; blockers: string | null } | null
6
+ }>()
7
+
8
+ const emit = defineEmits<{
9
+ (e: 'save', data: { doneText: string | null; planText: string | null; blockers: string | null }): void
10
+ }>()
11
+
12
+ const doneText = ref('')
13
+ const planText = ref('')
14
+ const blockers = ref('')
15
+ const isEditing = ref(false)
16
+
17
+ onMounted(() => {
18
+ if (props.existing) {
19
+ doneText.value = props.existing.doneText ?? ''
20
+ planText.value = props.existing.planText ?? ''
21
+ blockers.value = props.existing.blockers ?? ''
22
+ }
23
+ })
24
+
25
+ function save() {
26
+ emit('save', { doneText: doneText.value || null, planText: planText.value || null, blockers: blockers.value || null })
27
+ isEditing.value = false
28
+ }
29
+ </script>
30
+
31
+ <template>
32
+ <div class="standup-form">
33
+ <h3 v-if="existing && !isEditing">Your standup is submitted</h3>
34
+ <div v-if="existing && !isEditing" class="submitted-preview">
35
+ <div v-if="existing.doneText" class="preview-section"><span class="preview-label">Done:</span> {{ existing.doneText }}</div>
36
+ <div v-if="existing.planText" class="preview-section"><span class="preview-label">Plan:</span> {{ existing.planText }}</div>
37
+ <div v-if="existing.blockers" class="preview-section"><span class="preview-label">Blockers:</span> {{ existing.blockers }}</div>
38
+ <button class="btn btn--sm" @click="isEditing = true">Edit</button>
39
+ </div>
40
+ <div v-if="!existing || isEditing" class="form-fields">
41
+ <h3>Write Your Standup</h3>
42
+ <div class="field"><label>What did you do yesterday?</label><textarea v-model="doneText" rows="3" placeholder="Completed tasks..." /></div>
43
+ <div class="field"><label>What will you do today?</label><textarea v-model="planText" rows="3" placeholder="Planned tasks..." /></div>
44
+ <div class="field"><label>Any blockers?</label><textarea v-model="blockers" rows="2" placeholder="Blockers (if any)..." /></div>
45
+ <div class="form-actions">
46
+ <button class="btn btn--primary" @click="save">Save</button>
47
+ <button v-if="isEditing" class="btn" @click="isEditing = false">Cancel</button>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </template>
52
+
53
+ <style scoped>
54
+ .standup-form { background: var(--card-bg, #fff); border: 1px solid var(--border-light, #e2e8f0); border-radius: 12px; padding: 20px; margin-bottom: 20px; }
55
+ h3 { font-size: 15px; font-weight: 600; margin-bottom: 12px; }
56
+ .submitted-preview { display: flex; flex-direction: column; gap: 6px; }
57
+ .preview-section { font-size: 13px; color: var(--text-secondary); }
58
+ .preview-label { font-weight: 600; color: var(--text-primary); margin-right: 4px; }
59
+ .form-fields { display: flex; flex-direction: column; gap: 12px; }
60
+ .field label { font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 4px; display: block; }
61
+ .field textarea { width: 100%; padding: 8px 12px; border: 1px solid var(--border-light, #e2e8f0); border-radius: 8px; font-size: 13px; font-family: inherit; resize: vertical; box-sizing: border-box; }
62
+ .field textarea:focus { outline: none; border-color: #3b82f6; }
63
+ .form-actions { display: flex; gap: 8px; }
64
+ .btn { padding: 8px 16px; border: 1px solid var(--border-light, #e2e8f0); border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; background: var(--card-bg, #fff); color: var(--text-secondary); transition: all 0.15s; }
65
+ .btn:hover { background: #f1f5f9; }
66
+ .btn--primary { background: #1e293b; color: #fff; border-color: #1e293b; }
67
+ .btn--sm { padding: 4px 10px; font-size: 11px; margin-top: 8px; }
68
+ </style>
@@ -0,0 +1,71 @@
1
+ <script setup lang="ts">
2
+ export interface StandupEntry {
3
+ id: number
4
+ user: string
5
+ done_text: string
6
+ plan_text: string
7
+ blockers: string
8
+ created_at: string
9
+ }
10
+
11
+ defineProps<{
12
+ entries: StandupEntry[]
13
+ }>()
14
+ </script>
15
+
16
+ <template>
17
+ <div class="standup-list">
18
+ <h3>Team Standups</h3>
19
+
20
+ <div v-if="!entries.length" class="empty-msg">No standups submitted yet today.</div>
21
+
22
+ <div v-for="entry in entries" :key="entry.id" class="standup-entry">
23
+ <div class="entry-header">
24
+ <span class="entry-avatar">{{ entry.user.charAt(0).toUpperCase() }}</span>
25
+ <span class="entry-user">{{ entry.user }}</span>
26
+ <span class="entry-time">{{ entry.created_at }}</span>
27
+ </div>
28
+
29
+ <div class="entry-section" v-if="entry.done_text">
30
+ <div class="section-label">Done</div>
31
+ <p>{{ entry.done_text }}</p>
32
+ </div>
33
+
34
+ <div class="entry-section" v-if="entry.plan_text">
35
+ <div class="section-label">Plan</div>
36
+ <p>{{ entry.plan_text }}</p>
37
+ </div>
38
+
39
+ <div class="entry-section entry-blockers" v-if="entry.blockers">
40
+ <div class="section-label">Blockers</div>
41
+ <p>{{ entry.blockers }}</p>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </template>
46
+
47
+ <style scoped>
48
+ .standup-list { margin-top: 24px; }
49
+ h3 { font-size: 16px; font-weight: 700; margin: 0 0 16px; }
50
+
51
+ .empty-msg { font-size: 13px; color: var(--text-muted); padding: 20px 0; }
52
+
53
+ .standup-entry {
54
+ padding: 16px; background: var(--card-bg); border: 1px solid var(--border-light);
55
+ border-radius: var(--radius); margin-bottom: 12px;
56
+ }
57
+ .entry-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
58
+ .entry-avatar {
59
+ width: 28px; height: 28px; border-radius: 50%; background: var(--primary);
60
+ color: #fff; font-size: 12px; font-weight: 700;
61
+ display: flex; align-items: center; justify-content: center;
62
+ }
63
+ .entry-user { font-size: 14px; font-weight: 600; }
64
+ .entry-time { margin-left: auto; font-size: 12px; color: var(--text-muted); }
65
+
66
+ .entry-section { margin-bottom: 8px; }
67
+ .entry-section:last-child { margin-bottom: 0; }
68
+ .section-label { font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 2px; }
69
+ .entry-section p { font-size: 13px; line-height: 1.5; margin: 0; }
70
+ .entry-blockers .section-label { color: var(--red, #ef4444); }
71
+ </style>