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,495 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, nextTick, watch, onMounted, onUnmounted } from 'vue'
3
+ import type { PmStory, StoryStatus } from '@/composables/usePmStore'
4
+ import { getActiveSprint } from '@/composables/useNavStore'
5
+ import {
6
+ getTasksForStory, getEpicById, updateStoryStatus, updateStory, addTask,
7
+ STORY_STATUSES, STORY_STATUS_LABELS, PRIORITY_LABELS,
8
+ } from '@/composables/usePmStore'
9
+ import { useAuth } from '@/composables/useAuth'
10
+ import StatusBadge from './StatusBadge.vue'
11
+ import BoardTaskItem from './BoardTaskItem.vue'
12
+
13
+ const props = defineProps<{ story: PmStory }>()
14
+ const emit = defineEmits<{ close: []; updated: [] }>()
15
+
16
+ const { authUser } = useAuth()
17
+
18
+ // Inline task add
19
+ const showAddTask = ref(false)
20
+ const newTaskTitle = ref('')
21
+ const addingTask = ref(false)
22
+ const taskInput = ref<HTMLInputElement | null>(null)
23
+
24
+ async function openAddTask() {
25
+ showAddTask.value = true
26
+ await nextTick()
27
+ taskInput.value?.focus()
28
+ }
29
+
30
+ async function handleAddTask() {
31
+ const title = newTaskTitle.value.trim()
32
+ if (!title || addingTask.value) return
33
+ addingTask.value = true
34
+ await addTask({ storyId: props.story.id, title })
35
+ newTaskTitle.value = ''
36
+ addingTask.value = false
37
+ emit('updated')
38
+ await nextTick()
39
+ taskInput.value?.focus()
40
+ }
41
+
42
+ function cancelAddTask() {
43
+ showAddTask.value = false
44
+ newTaskTitle.value = ''
45
+ }
46
+
47
+ const storyTasks = computed(() => getTasksForStory(props.story.id))
48
+ const doneCount = computed(() => storyTasks.value.filter(t => t.status === 'done').length)
49
+ const epicTitle = computed(() => props.story.epicId ? getEpicById(props.story.epicId)?.title ?? null : null)
50
+
51
+ // AC check-off
52
+ interface AcItem { text: string; checked: boolean }
53
+
54
+ function parseAcItems(raw: string | null): AcItem[] {
55
+ if (!raw) return []
56
+ const lines = raw.includes('\n')
57
+ ? raw.split('\n').map(l => l.trim()).filter(Boolean)
58
+ : [raw.trim()]
59
+ return lines.map(line => {
60
+ if (line.startsWith('[x] ') || line.startsWith('[X] ')) return { text: line.slice(4), checked: true }
61
+ if (line.startsWith('[ ] ')) return { text: line.slice(4), checked: false }
62
+ return { text: line, checked: false }
63
+ })
64
+ }
65
+
66
+ function serializeAc(items: AcItem[]): string {
67
+ return items.map(i => `${i.checked ? '[x]' : '[ ]'} ${i.text}`).join('\n')
68
+ }
69
+
70
+ const acItems = computed(() => parseAcItems(props.story.acceptanceCriteria))
71
+ const acDoneCount = computed(() => acItems.value.filter(i => i.checked).length)
72
+
73
+ async function handleMergeOk() {
74
+ if (!confirm('Mark as Merge OK? Story status will change to done.')) return
75
+ await updateStory(props.story.id, { status: 'done' } as any)
76
+ emit('updated')
77
+ }
78
+
79
+ async function toggleAc(idx: number) {
80
+ const items = [...acItems.value]
81
+ items[idx] = { ...items[idx], checked: !items[idx].checked }
82
+ await updateStory(props.story.id, { acceptanceCriteria: serializeAc(items) })
83
+ emit('updated')
84
+ }
85
+
86
+ // DoR checklist (gate for ready-for-dev)
87
+ const showDorModal = ref(false)
88
+ const dorChecks = ref({ specDone: false, acExists: false, mockupReady: false })
89
+ const pendingStatus = ref<StoryStatus | null>(null)
90
+
91
+ function resetDor() { dorChecks.value = { specDone: false, acExists: false, mockupReady: false } }
92
+
93
+ const dorAllChecked = computed(() =>
94
+ dorChecks.value.specDone && dorChecks.value.acExists && dorChecks.value.mockupReady
95
+ )
96
+
97
+ async function confirmDor() {
98
+ if (!dorAllChecked.value || !pendingStatus.value) return
99
+ await updateStoryStatus(props.story.id, pendingStatus.value)
100
+ showDorModal.value = false
101
+ pendingStatus.value = null
102
+ resetDor()
103
+ emit('updated')
104
+ }
105
+
106
+ // Status dropdown
107
+ const statusDropOpen = ref(false)
108
+ const statusTrigger = ref<HTMLElement | null>(null)
109
+ const STATUS_COLORS: Record<string, string> = {
110
+ draft: '#94a3b8', backlog: '#a78bfa', ready: '#3b82f6',
111
+ 'in-progress': '#f59e0b', review: '#8b5cf6', done: '#22c55e',
112
+ }
113
+
114
+ async function selectStatus(status: StoryStatus) {
115
+ statusDropOpen.value = false
116
+ if (status === props.story.status) return
117
+ if (status === 'ready' && props.story.status !== 'ready') {
118
+ pendingStatus.value = status
119
+ resetDor()
120
+ showDorModal.value = true
121
+ return
122
+ }
123
+ await updateStoryStatus(props.story.id, status)
124
+ emit('updated')
125
+ }
126
+
127
+ // Assignee multi-select
128
+ const assigneeDropOpen = ref(false)
129
+ const assigneeTrigger = ref<HTMLElement | null>(null)
130
+
131
+ const assigneeList = computed(() =>
132
+ props.story.assignee ? props.story.assignee.split(',').map(s => s.trim()).filter(Boolean) : []
133
+ )
134
+
135
+ const assigneeDisplay = computed(() =>
136
+ assigneeList.value.length === 0 ? 'Unassigned' : assigneeList.value.join(', ')
137
+ )
138
+
139
+ async function toggleAssignee(name: string) {
140
+ const current = new Set(assigneeList.value)
141
+ if (current.has(name)) current.delete(name)
142
+ else current.add(name)
143
+ const value = current.size > 0 ? [...current].join(',') : null
144
+ await updateStory(props.story.id, { assignee: value })
145
+ emit('updated')
146
+ }
147
+
148
+ // Dropdown position
149
+ function menuStyle(trigger: HTMLElement | null): Record<string, string> {
150
+ if (!trigger) return {}
151
+ const rect = trigger.getBoundingClientRect()
152
+ return { position: 'fixed', top: `${rect.bottom + 4}px`, left: `${rect.left}px`, width: `${rect.width}px`, zIndex: '9999' }
153
+ }
154
+
155
+ // Figma link editing
156
+ const editingFigma = ref(false)
157
+ const figmaUrlDraft = ref('')
158
+ const figmaInput = ref<HTMLInputElement | null>(null)
159
+
160
+ const figmaLabel = computed(() => {
161
+ if (!props.story.figmaUrl) return ''
162
+ try {
163
+ const url = new URL(props.story.figmaUrl)
164
+ const parts = url.pathname.split('/')
165
+ return parts[parts.length - 1]?.replace(/-/g, ' ') || 'Figma'
166
+ } catch { return 'Figma' }
167
+ })
168
+
169
+ async function startEditFigma() {
170
+ figmaUrlDraft.value = props.story.figmaUrl ?? ''
171
+ editingFigma.value = true
172
+ await nextTick()
173
+ figmaInput.value?.focus()
174
+ }
175
+
176
+ async function saveFigma() {
177
+ const url = figmaUrlDraft.value.trim() || null
178
+ editingFigma.value = false
179
+ if (url === props.story.figmaUrl) return
180
+ await updateStory(props.story.id, { figmaUrl: url })
181
+ emit('updated')
182
+ }
183
+
184
+ // Close dropdown on outside click
185
+ function onDocClick(e: MouseEvent) {
186
+ const target = e.target as HTMLElement
187
+ if (!target.closest('.field-dropdown') && !target.closest('.field-menu-portal')) {
188
+ statusDropOpen.value = false
189
+ assigneeDropOpen.value = false
190
+ }
191
+ }
192
+ const panelBodyRef = ref<HTMLElement | null>(null)
193
+
194
+ async function updateDate(field: 'startDate' | 'dueDate', value: string) {
195
+ await updateStory(props.story.id, { [field]: value || null } as any)
196
+ emit('updated')
197
+ }
198
+
199
+ async function assignToSprint() {
200
+ const activeSprint = getActiveSprint().id
201
+ await updateStory(props.story.id, { sprint: activeSprint })
202
+ emit('updated')
203
+ }
204
+
205
+ async function unassignFromSprint() {
206
+ await updateStory(props.story.id, { sprint: null } as any)
207
+ emit('updated')
208
+ }
209
+
210
+ onMounted(async () => {
211
+ document.addEventListener('click', onDocClick)
212
+ const { pmEpics, loadEpics } = await import('@/composables/usePmStore')
213
+ if (!pmEpics.value.length) await loadEpics()
214
+ nextTick(() => { if (panelBodyRef.value) panelBodyRef.value.scrollTop = 0 })
215
+ })
216
+ onUnmounted(() => document.removeEventListener('click', onDocClick))
217
+ </script>
218
+
219
+ <template>
220
+ <div class="panel-overlay" @click.self="emit('close')">
221
+ <div class="panel">
222
+ <div class="panel-header">
223
+ <div class="panel-header-top">
224
+ <div class="panel-badges">
225
+ <StatusBadge :label="PRIORITY_LABELS[story.priority]" type="priority" :value="story.priority" />
226
+ <span class="panel-area">{{ story.area }}</span>
227
+ <span v-if="story.storyPoints" class="panel-points">{{ story.storyPoints }} SP</span>
228
+ </div>
229
+ <button class="close-btn" @click="emit('close')">&times;</button>
230
+ </div>
231
+ <h2 class="panel-title">{{ story.title }}</h2>
232
+ </div>
233
+
234
+ <div ref="panelBodyRef" class="panel-body">
235
+ <!-- Property fields -->
236
+ <div class="field-grid">
237
+ <!-- Status dropdown -->
238
+ <div class="field-row">
239
+ <span class="field-label">Status</span>
240
+ <div ref="statusTrigger" class="field-dropdown" @click.stop="statusDropOpen = !statusDropOpen; assigneeDropOpen = false">
241
+ <button class="field-value field-value--clickable">
242
+ <span class="status-dot" :style="{ background: STATUS_COLORS[story.status] }"></span>
243
+ {{ STORY_STATUS_LABELS[story.status] }}
244
+ <span class="field-chevron">&#9662;</span>
245
+ </button>
246
+ </div>
247
+ </div>
248
+ <!-- Assignee -->
249
+ <div class="field-row">
250
+ <span class="field-label">Assign</span>
251
+ <div ref="assigneeTrigger" class="field-dropdown" @click.stop="assigneeDropOpen = !assigneeDropOpen; statusDropOpen = false">
252
+ <button class="field-value field-value--clickable">
253
+ {{ assigneeDisplay }}
254
+ <span class="field-chevron">&#9662;</span>
255
+ </button>
256
+ </div>
257
+ </div>
258
+ <!-- Epic -->
259
+ <div v-if="epicTitle" class="field-row">
260
+ <span class="field-label">Epic</span>
261
+ <span class="field-value">{{ epicTitle }}</span>
262
+ </div>
263
+ <!-- Dates -->
264
+ <div class="field-row">
265
+ <span class="field-label">Start</span>
266
+ <input type="date" class="field-date-input" :value="story.startDate ?? ''" @change="updateDate('startDate', ($event.target as HTMLInputElement).value)" />
267
+ </div>
268
+ <div class="field-row">
269
+ <span class="field-label">Due</span>
270
+ <input type="date" class="field-date-input" :value="story.dueDate ?? ''" @change="updateDate('dueDate', ($event.target as HTMLInputElement).value)" />
271
+ </div>
272
+ <!-- Sprint assignment -->
273
+ <div class="field-row">
274
+ <span class="field-label">Sprint</span>
275
+ <div class="field-value sprint-assign">
276
+ <span v-if="story.sprint">{{ story.sprint }}</span>
277
+ <span v-else class="field-placeholder">Backlog</span>
278
+ <button v-if="story.sprint" class="sprint-btn sprint-btn--remove" @click.stop="unassignFromSprint" title="Remove from sprint">Remove</button>
279
+ <button v-else class="sprint-btn sprint-btn--assign" @click.stop="assignToSprint" title="Assign to current sprint">Assign</button>
280
+ </div>
281
+ </div>
282
+ <!-- Figma link -->
283
+ <div class="field-row">
284
+ <span class="field-label">Figma</span>
285
+ <div v-if="!editingFigma" class="field-value field-value--clickable" @click="startEditFigma">
286
+ <a v-if="story.figmaUrl" :href="story.figmaUrl" target="_blank" rel="noopener" class="figma-link" @click.stop>{{ figmaLabel }}</a>
287
+ <span v-else class="field-placeholder">+ Add link</span>
288
+ </div>
289
+ <div v-else class="figma-edit">
290
+ <input ref="figmaInput" v-model="figmaUrlDraft" class="figma-input" placeholder="https://www.figma.com/..." @keydown.enter="saveFigma" @keydown.escape="editingFigma = false" />
291
+ <button class="figma-save" @click="saveFigma">Save</button>
292
+ <button class="figma-cancel" @click="editingFigma = false">&times;</button>
293
+ </div>
294
+ </div>
295
+ </div>
296
+
297
+ <!-- Dropdown menus (Teleport to body) -->
298
+ <Teleport to="body">
299
+ <div v-if="statusDropOpen" class="field-menu-portal" :style="menuStyle(statusTrigger)" @click.stop>
300
+ <div class="field-menu">
301
+ <div v-for="s in STORY_STATUSES" :key="s" class="field-menu-item" :class="{ active: s === story.status }" @click="selectStatus(s)">
302
+ <span class="status-dot" :style="{ background: STATUS_COLORS[s] }"></span>
303
+ {{ STORY_STATUS_LABELS[s] }}
304
+ </div>
305
+ </div>
306
+ </div>
307
+ <div v-if="assigneeDropOpen" class="field-menu-portal" :style="menuStyle(assigneeTrigger)" @click.stop>
308
+ <div class="field-menu">
309
+ <div class="field-menu-item" style="color:#94a3b8;" @click="toggleAssignee('')">
310
+ (Use admin member list for options)
311
+ </div>
312
+ </div>
313
+ </div>
314
+ </Teleport>
315
+
316
+ <!-- Description -->
317
+ <section v-if="story.description" class="panel-section">
318
+ <h3>Description</h3>
319
+ <div class="desc-text">{{ story.description }}</div>
320
+ </section>
321
+
322
+ <!-- Acceptance Criteria -->
323
+ <section v-if="acItems.length > 0" class="panel-section">
324
+ <h3>Acceptance Criteria <span class="task-counter">{{ acDoneCount }}/{{ acItems.length }}</span></h3>
325
+ <div class="ac-list">
326
+ <label v-for="(ac, i) in acItems" :key="i" class="ac-item" :class="{ 'ac-item--done': ac.checked }">
327
+ <input type="checkbox" :checked="ac.checked" class="ac-check" @change="toggleAc(i)" />
328
+ <span class="ac-text">{{ ac.text }}</span>
329
+ </label>
330
+ </div>
331
+ <button
332
+ v-if="(story.status === 'review' || story.status === 'qa') && acItems.length > 0"
333
+ class="btn merge-ok-btn"
334
+ :class="{ 'merge-ok-active': acDoneCount === acItems.length }"
335
+ :disabled="acDoneCount !== acItems.length"
336
+ @click="handleMergeOk"
337
+ >
338
+ {{ acDoneCount === acItems.length ? 'Merge OK -- All AC passed' : `AC ${acDoneCount}/${acItems.length} passed` }}
339
+ </button>
340
+ </section>
341
+
342
+ <!-- No AC free-form QA -->
343
+ <section v-if="acItems.length === 0 && (story.status === 'review' || story.status === 'qa')" class="panel-section">
344
+ <h3>QA Checklist</h3>
345
+ <p class="qa-note">No acceptance criteria defined. Verify manually before merging.</p>
346
+ <button class="btn merge-ok-btn merge-ok-active" @click="handleMergeOk">Merge OK</button>
347
+ </section>
348
+
349
+ <!-- Tasks -->
350
+ <section class="panel-section">
351
+ <div class="task-header">
352
+ <h3>Tasks <span v-if="storyTasks.length > 0" class="task-counter">{{ doneCount }}/{{ storyTasks.length }}</span></h3>
353
+ <button v-if="!showAddTask" class="add-task-btn" @click="openAddTask">+ Add</button>
354
+ </div>
355
+ <!-- Related PRs -->
356
+ <div v-if="story.relatedPrs?.length" class="panel-prs">
357
+ <div class="panel-section-title">Linked PRs</div>
358
+ <div v-for="pr in story.relatedPrs" :key="pr.prNumber" class="pr-item">
359
+ <a :href="pr.prUrl" target="_blank" class="pr-link">#{{ pr.prNumber }}</a>
360
+ <span class="pr-title-text">{{ pr.prTitle }}</span>
361
+ <span class="pr-status" :class="'pr--' + pr.status">{{ pr.status }}</span>
362
+ </div>
363
+ </div>
364
+ <div v-if="storyTasks.length === 0 && !showAddTask" class="panel-empty">No tasks</div>
365
+ <div v-if="storyTasks.length > 0" class="panel-tasks">
366
+ <BoardTaskItem v-for="t in storyTasks" :key="t.id" :task="t" @updated="emit('updated')" />
367
+ </div>
368
+ <!-- Inline task add -->
369
+ <div v-if="showAddTask" class="add-task-form">
370
+ <input ref="taskInput" v-model="newTaskTitle" class="add-task-input" placeholder="Task title (Enter to add)" @keydown.enter.prevent="handleAddTask" @keydown.escape="cancelAddTask" :disabled="addingTask" />
371
+ <div class="add-task-actions">
372
+ <button class="add-task-submit" @click="handleAddTask" :disabled="!newTaskTitle.trim() || addingTask">Add</button>
373
+ <button class="add-task-cancel" @click="cancelAddTask">Cancel</button>
374
+ </div>
375
+ </div>
376
+ </section>
377
+
378
+ <!-- Meta -->
379
+ <div class="panel-meta">
380
+ <span>Created: {{ story.createdAt?.split('T')[0] ?? '-' }}</span>
381
+ <span>Updated: {{ story.updatedAt?.split('T')[0] ?? '-' }}</span>
382
+ </div>
383
+ </div>
384
+ </div>
385
+
386
+ <!-- DoR Checklist Modal -->
387
+ <div v-if="showDorModal" class="dor-overlay" @click.self="showDorModal = false">
388
+ <div class="dor-modal">
389
+ <h3 class="dor-title">Definition of Ready</h3>
390
+ <p class="dor-desc">Confirm the following before marking as ready.</p>
391
+ <div class="dor-checks">
392
+ <label class="dor-check"><input type="checkbox" v-model="dorChecks.specDone" /><span>Epic spec is finalized?</span></label>
393
+ <label class="dor-check"><input type="checkbox" v-model="dorChecks.acExists" /><span>Acceptance criteria defined?</span></label>
394
+ <label class="dor-check"><input type="checkbox" v-model="dorChecks.mockupReady" /><span>Mockup / screen spec ready?</span></label>
395
+ </div>
396
+ <div class="dor-actions">
397
+ <button class="dor-confirm" :disabled="!dorAllChecked" @click="confirmDor">Mark as Ready</button>
398
+ <button class="dor-cancel" @click="showDorModal = false">Cancel</button>
399
+ </div>
400
+ </div>
401
+ </div>
402
+ </div>
403
+ </template>
404
+
405
+ <style scoped>
406
+ .panel-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.3); z-index: 900; display: flex; justify-content: flex-end; }
407
+ .panel { width: 480px; max-width: 100vw; height: 100vh; max-height: 100vh; background: #f8f8fb; box-shadow: -4px 0 24px rgba(0,0,0,0.08); overflow: hidden; padding: 0; display: flex; flex-direction: column; animation: slide-in 0.2s ease-out; }
408
+ .panel-header { background: #f8f8fb; padding: 20px 24px 16px; border-bottom: 1px solid rgba(0,0,0,0.06); flex-shrink: 0; position: relative; }
409
+ .panel-header-top { display: flex; justify-content: space-between; align-items: flex-start; }
410
+ .close-btn { position: absolute; top: 16px; right: 16px; width: 32px; height: 32px; border: none; background: none; font-size: 22px; color: #94a3b8; cursor: pointer; border-radius: 6px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
411
+ .close-btn:hover { background: rgba(0,0,0,0.04); color: var(--text-primary); }
412
+ .panel-header .panel-title { margin: 8px 32px 0 0; font-size: 16px; font-weight: 700; line-height: 1.4; word-break: break-word; }
413
+ .panel-body { flex: 1 1 0; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 12px 20px 32px; display: flex; flex-direction: column; gap: 4px; }
414
+ @keyframes slide-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
415
+ .panel-badges { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
416
+ .panel-area { font-size: 11px; font-weight: 600; color: var(--text-secondary); background: rgba(0,0,0,0.04); padding: 2px 8px; border-radius: 4px; }
417
+ .panel-points { font-size: 12px; font-weight: 700; color: #3b82f6; }
418
+ .field-grid { display: flex; flex-direction: column; gap: 1px; background: #fff; border-radius: 10px; padding: 4px 0; overflow: hidden; flex-shrink: 0; }
419
+ .field-row { display: flex; align-items: center; background: rgba(255,255,255,0.25); padding: 0; min-height: 38px; }
420
+ .field-label { width: 64px; flex-shrink: 0; padding: 8px 12px; font-size: 11px; font-weight: 600; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.3px; }
421
+ .field-value { flex: 1; padding: 8px 12px; font-size: 13px; color: var(--text-primary); font-weight: 500; border: none; background: none; text-align: left; font-family: inherit; }
422
+ .field-date-input { flex: 1; padding: 6px 10px; border: 1px solid rgba(0,0,0,0.08); border-radius: 6px; font-size: 13px; background: transparent; font-family: inherit; color: var(--text-primary); }
423
+ .sprint-assign { display: flex; align-items: center; gap: 8px; }
424
+ .sprint-btn { border: none; border-radius: 6px; padding: 2px 8px; font-size: 11px; cursor: pointer; font-weight: 500; }
425
+ .sprint-btn--assign { background: rgba(59,130,246,0.12); color: #2563EB; }
426
+ .sprint-btn--remove { background: rgba(239,68,68,0.12); color: #dc2626; }
427
+ .field-value--clickable { cursor: pointer; display: flex; align-items: center; gap: 6px; border-radius: 4px; transition: background 0.1s; }
428
+ .field-value--clickable:hover { background: rgba(0,0,0,0.02); }
429
+ .field-chevron { font-size: 10px; color: #94a3b8; margin-left: auto; }
430
+ .field-dropdown { position: relative; flex: 1; }
431
+ .field-menu { background: #fff; border: 1px solid rgba(0,0,0,0.1); border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.15); padding: 4px; max-height: 240px; overflow-y: auto; }
432
+ .field-menu-item { display: flex; align-items: center; gap: 8px; padding: 7px 10px; font-size: 13px; color: #475569; border-radius: 6px; cursor: pointer; transition: all 0.1s; }
433
+ .field-menu-item:hover { background: rgba(0,0,0,0.04); color: var(--text-primary); }
434
+ .field-menu-item.active { background: #eff6ff; color: #3b82f6; font-weight: 600; }
435
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
436
+ .field-placeholder { color: #94a3b8; font-size: 12px; }
437
+ .figma-link { color: #a259ff; font-size: 13px; font-weight: 500; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
438
+ .figma-link:hover { text-decoration: underline; }
439
+ .figma-edit { flex: 1; display: flex; align-items: center; gap: 4px; padding: 4px 8px; }
440
+ .figma-input { flex: 1; padding: 4px 8px; border: 1px solid rgba(0,0,0,0.06); border-radius: 4px; font-size: 12px; font-family: inherit; color: var(--text-primary); min-width: 0; }
441
+ .figma-input:focus { outline: none; border-color: #a259ff; }
442
+ .figma-save { padding: 3px 8px; background: var(--text-primary); color: #fff; border: none; border-radius: 4px; font-size: 11px; font-weight: 600; cursor: pointer; white-space: nowrap; }
443
+ .figma-cancel { width: 24px; height: 24px; border: none; background: none; color: #94a3b8; font-size: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
444
+ .panel-section { background: #fff; border-radius: 10px; padding: 14px 16px; flex-shrink: 0; }
445
+ .panel-section h3 { font-size: 12px; font-weight: 700; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
446
+ .task-counter { font-weight: 400; color: #94a3b8; margin-left: 4px; }
447
+ .desc-text { font-size: 13px; color: #334155; line-height: 1.7; padding: 10px 12px; background: rgba(0,0,0,0.02); border-radius: 6px; border-left: 3px solid rgba(0,0,0,0.06); white-space: pre-wrap; }
448
+ .task-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
449
+ .task-header h3 { margin-bottom: 0; }
450
+ .add-task-btn { padding: 3px 10px; border: 1px dashed #cbd5e1; border-radius: 4px; background: none; font-size: 11px; font-weight: 600; color: var(--text-secondary); cursor: pointer; transition: all 0.15s; }
451
+ .add-task-btn:hover { border-color: #3b82f6; color: #3b82f6; }
452
+ .add-task-form { margin-top: 6px; display: flex; flex-direction: column; gap: 6px; }
453
+ .add-task-input { width: 100%; padding: 6px 10px; border: 1px solid rgba(0,0,0,0.06); border-radius: 6px; font-size: 12px; font-family: inherit; color: var(--text-primary); box-sizing: border-box; }
454
+ .add-task-input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59,130,246,0.1); }
455
+ .add-task-actions { display: flex; gap: 6px; }
456
+ .add-task-submit { padding: 4px 12px; border: none; border-radius: 4px; background: var(--text-primary); color: #fff; font-size: 11px; font-weight: 600; cursor: pointer; }
457
+ .add-task-submit:disabled { background: #cbd5e1; cursor: not-allowed; }
458
+ .add-task-cancel { padding: 4px 12px; border: 1px solid rgba(0,0,0,0.06); border-radius: 4px; background: rgba(255,255,255,0.25); color: var(--text-secondary); font-size: 11px; cursor: pointer; }
459
+ .ac-list { display: flex; flex-direction: column; gap: 4px; }
460
+ .ac-item { display: flex; align-items: flex-start; gap: 8px; padding: 6px 10px; background: rgba(0,0,0,0.02); border-radius: 6px; border-left: 3px solid rgba(0,0,0,0.06); cursor: pointer; transition: all 0.15s; }
461
+ .ac-item:hover { background: rgba(0,0,0,0.04); }
462
+ .ac-item--done { border-left-color: #22c55e; }
463
+ .ac-item--done .ac-text { text-decoration: line-through; color: #94a3b8; }
464
+ .ac-check { margin-top: 2px; accent-color: #22c55e; cursor: pointer; flex-shrink: 0; }
465
+ .ac-text { font-size: 13px; color: #334155; line-height: 1.5; }
466
+ .panel-empty { font-size: 13px; color: #cbd5e1; padding: 8px 0; }
467
+ .panel-tasks { display: flex; flex-direction: column; gap: 2px; }
468
+ .panel-meta { margin-top: auto; padding-top: 12px; border-top: 1px solid rgba(0,0,0,0.04); display: flex; gap: 16px; font-size: 11px; color: #94a3b8; }
469
+ .panel-prs { margin-bottom: 16px; }
470
+ .panel-section-title { font-size: 12px; font-weight: 600; color: var(--text-muted, #888); margin-bottom: 8px; }
471
+ .pr-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 13px; }
472
+ .pr-link { color: #3b82f6; font-weight: 600; text-decoration: none; }
473
+ .pr-link:hover { text-decoration: underline; }
474
+ .pr-title-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
475
+ .pr-status { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; }
476
+ .pr--open { background: #dcfce7; color: #16a34a; }
477
+ .pr--merged { background: #e0e7ff; color: #4338ca; }
478
+ .pr--closed { background: #f3f4f6; color: #6b7280; }
479
+ .merge-ok-btn { width: 100%; margin-top: 12px; padding: 8px; border-radius: 8px; font-size: 13px; font-weight: 600; background: #e5e7eb; color: #6b7280; border: none; cursor: not-allowed; }
480
+ .merge-ok-active { background: #22c55e; color: #fff; cursor: pointer; }
481
+ .merge-ok-active:hover { background: #16a34a; }
482
+ .qa-note { font-size: 12px; color: var(--text-muted, #888); margin-bottom: 12px; }
483
+ .dor-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 1000; display: flex; align-items: center; justify-content: center; }
484
+ .dor-modal { background: #fff; border-radius: 12px; padding: 24px; width: 400px; max-width: 92vw; box-shadow: 0 8px 32px rgba(0,0,0,0.2); }
485
+ .dor-title { font-size: 16px; font-weight: 700; color: var(--text-primary); margin-bottom: 4px; }
486
+ .dor-desc { font-size: 12px; color: var(--text-secondary); margin-bottom: 16px; }
487
+ .dor-checks { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }
488
+ .dor-check { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #334155; cursor: pointer; }
489
+ .dor-check input { accent-color: #22c55e; cursor: pointer; }
490
+ .dor-actions { display: flex; gap: 8px; justify-content: flex-end; }
491
+ .dor-confirm { padding: 8px 16px; background: var(--text-primary); color: #fff; border: none; border-radius: 6px; font-size: 13px; font-weight: 600; cursor: pointer; }
492
+ .dor-confirm:disabled { background: #cbd5e1; cursor: not-allowed; }
493
+ .dor-cancel { padding: 8px 16px; background: rgba(255,255,255,0.25); color: var(--text-secondary); border: 1px solid rgba(0,0,0,0.06); border-radius: 6px; font-size: 13px; cursor: pointer; }
494
+ @media (max-width: 767px) { .panel { width: 100vw; } }
495
+ </style>
@@ -0,0 +1,42 @@
1
+ <script setup lang="ts">
2
+ import type { PmStory } from '@/composables/usePmStore'
3
+
4
+ defineProps<{ story: PmStory }>()
5
+ defineEmits<{ (e: 'click'): void }>()
6
+
7
+ function statusColor(status: string): string {
8
+ const map: Record<string, string> = { done: '#22c55e', 'in-progress': '#f59e0b', review: '#8b5cf6', backlog: '#94a3b8', draft: '#94a3b8', blocked: '#ef4444' }
9
+ return map[status] || '#6b7280'
10
+ }
11
+ </script>
12
+
13
+ <template>
14
+ <div class="task-card" @click="$emit('click')" draggable="true">
15
+ <div class="task-header">
16
+ <span class="task-id">S{{ story.id }}</span>
17
+ <span class="task-status-dot" :style="{ background: statusColor(story.status) }" />
18
+ </div>
19
+ <div class="task-title">{{ story.title }}</div>
20
+ <div class="task-footer">
21
+ <span v-if="story.assignee" class="task-assignee">{{ story.assignee }}</span>
22
+ <span v-if="story.storyPoints" class="task-sp">{{ story.storyPoints }} SP</span>
23
+ <span class="task-priority" :class="'priority--' + story.priority">{{ story.priority }}</span>
24
+ </div>
25
+ </div>
26
+ </template>
27
+
28
+ <style scoped>
29
+ .task-card { padding: 12px 14px; background: var(--card-bg, #fff); border: 1px solid var(--border-light, #e2e8f0); border-radius: 10px; cursor: pointer; transition: all 0.15s; }
30
+ .task-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); transform: translateY(-1px); }
31
+ .task-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
32
+ .task-id { font-size: 11px; font-weight: 600; color: var(--text-muted); }
33
+ .task-status-dot { width: 8px; height: 8px; border-radius: 50%; }
34
+ .task-title { font-size: 13px; font-weight: 500; color: var(--text-primary); line-height: 1.4; margin-bottom: 8px; }
35
+ .task-footer { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--text-secondary); }
36
+ .task-assignee { font-weight: 500; }
37
+ .task-sp { background: rgba(59,130,246,0.1); color: #3b82f6; padding: 1px 6px; border-radius: 4px; font-weight: 600; }
38
+ .task-priority { padding: 1px 6px; border-radius: 4px; font-weight: 600; font-size: 10px; }
39
+ .priority--high { background: rgba(239,68,68,0.1); color: #ef4444; }
40
+ .priority--medium { background: rgba(245,158,11,0.1); color: #f59e0b; }
41
+ .priority--low { background: rgba(34,197,94,0.1); color: #22c55e; }
42
+ </style>
@@ -1,6 +1,14 @@
1
1
  <script setup lang="ts">
2
2
  import type { RetroItem, RetroPhase } from '@/composables/useRetro'
3
3
 
4
+ const AUTHOR_COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316']
5
+
6
+ function authorColor(name: string): string {
7
+ let hash = 0
8
+ for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash)
9
+ return AUTHOR_COLORS[Math.abs(hash) % AUTHOR_COLORS.length]
10
+ }
11
+
4
12
  const props = defineProps<{
5
13
  item: RetroItem
6
14
  phase: RetroPhase
@@ -15,10 +23,13 @@ const emit = defineEmits<{
15
23
  </script>
16
24
 
17
25
  <template>
18
- <div class="retro-card" :class="{ voted: item.hasVoted }">
26
+ <div class="retro-card" :class="{ voted: item.hasVoted, mine: item.author === currentUser }">
19
27
  <div class="card-content">{{ item.content }}</div>
20
28
  <div class="card-footer">
21
- <span class="card-author">{{ item.author }}</span>
29
+ <div class="card-author-wrap">
30
+ <span class="card-author-dot" :style="{ background: authorColor(item.author) }">{{ item.author.charAt(0) }}</span>
31
+ <span class="card-author">{{ item.author }}</span>
32
+ </div>
22
33
  <div class="card-actions">
23
34
  <button
24
35
  v-if="phase === 'vote' || phase === 'discuss'"
@@ -71,12 +82,35 @@ const emit = defineEmits<{
71
82
  gap: 8px;
72
83
  }
73
84
 
85
+ .card-author-wrap {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 5px;
89
+ }
90
+
91
+ .card-author-dot {
92
+ width: 18px;
93
+ height: 18px;
94
+ border-radius: 50%;
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ color: #fff;
99
+ font-size: 9px;
100
+ font-weight: 700;
101
+ flex-shrink: 0;
102
+ }
103
+
74
104
  .card-author {
75
105
  font-size: 12px;
76
106
  color: var(--text-muted);
77
107
  font-weight: 500;
78
108
  }
79
109
 
110
+ .retro-card.mine {
111
+ border-left: 3px solid var(--primary);
112
+ }
113
+
80
114
  .card-actions {
81
115
  display: flex;
82
116
  align-items: center;