popilot 0.2.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 (136) hide show
  1. package/README.md +372 -0
  2. package/adapters/claude-code/.claude/commands/_domain.md.hbs +32 -0
  3. package/adapters/claude-code/.claude/commands/analytics.md.hbs +55 -0
  4. package/adapters/claude-code/.claude/commands/daily.md.hbs +301 -0
  5. package/adapters/claude-code/.claude/commands/dev.md.hbs +62 -0
  6. package/adapters/claude-code/.claude/commands/handoff.md +258 -0
  7. package/adapters/claude-code/.claude/commands/market.md +120 -0
  8. package/adapters/claude-code/.claude/commands/metrics.md +123 -0
  9. package/adapters/claude-code/.claude/commands/oscar-loop.md +436 -0
  10. package/adapters/claude-code/.claude/commands/party.md +85 -0
  11. package/adapters/claude-code/.claude/commands/plan.md +43 -0
  12. package/adapters/claude-code/.claude/commands/research.md +203 -0
  13. package/adapters/claude-code/.claude/commands/retro.md +68 -0
  14. package/adapters/claude-code/.claude/commands/save.md +440 -0
  15. package/adapters/claude-code/.claude/commands/sessions.md +139 -0
  16. package/adapters/claude-code/.claude/commands/sprint.md +106 -0
  17. package/adapters/claude-code/.claude/commands/start.md +368 -0
  18. package/adapters/claude-code/.claude/commands/strategy.md +41 -0
  19. package/adapters/claude-code/.claude/commands/task.md +220 -0
  20. package/adapters/claude-code/.claude/commands/tracking.md +116 -0
  21. package/adapters/claude-code/.claude/commands/validate.md +58 -0
  22. package/adapters/claude-code/CLAUDE.md.hbs +208 -0
  23. package/adapters/claude-code/manifest.yaml +36 -0
  24. package/bin/cli.mjs +218 -0
  25. package/lib/adapter.mjs +68 -0
  26. package/lib/doctor.mjs +161 -0
  27. package/lib/hydrate.mjs +421 -0
  28. package/lib/prompt.mjs +78 -0
  29. package/lib/scaffold.mjs +155 -0
  30. package/lib/setup-wizard.mjs +331 -0
  31. package/lib/template-engine.mjs +164 -0
  32. package/lib/yaml-lite.mjs +476 -0
  33. package/package.json +30 -0
  34. package/scaffold/.context/.secrets.yaml.example +20 -0
  35. package/scaffold/.context/WORKFLOW.md.hbs +332 -0
  36. package/scaffold/.context/agents/TEMPLATE.md +115 -0
  37. package/scaffold/.context/agents/analyst.md.hbs +362 -0
  38. package/scaffold/.context/agents/developer.md.hbs +390 -0
  39. package/scaffold/.context/agents/handoff-specialist.md.hbs +292 -0
  40. package/scaffold/.context/agents/market-researcher.md.hbs +288 -0
  41. package/scaffold/.context/agents/ollie.md +323 -0
  42. package/scaffold/.context/agents/operations.md.hbs +293 -0
  43. package/scaffold/.context/agents/orchestrator.md.hbs +434 -0
  44. package/scaffold/.context/agents/planner.md.hbs +405 -0
  45. package/scaffold/.context/agents/qa.md.hbs +409 -0
  46. package/scaffold/.context/agents/researcher.md.hbs +330 -0
  47. package/scaffold/.context/agents/sage.md +349 -0
  48. package/scaffold/.context/agents/strategist.md.hbs +339 -0
  49. package/scaffold/.context/agents/tracking-governor.md.hbs +291 -0
  50. package/scaffold/.context/agents/validator.md.hbs +365 -0
  51. package/scaffold/.context/integrations/_registry.yaml +38 -0
  52. package/scaffold/.context/integrations/providers/channel_io.yaml +38 -0
  53. package/scaffold/.context/integrations/providers/corti.yaml +203 -0
  54. package/scaffold/.context/integrations/providers/ga4.yaml +116 -0
  55. package/scaffold/.context/integrations/providers/intercom.yaml +47 -0
  56. package/scaffold/.context/integrations/providers/linear.yaml +46 -0
  57. package/scaffold/.context/integrations/providers/mixpanel.yaml +73 -0
  58. package/scaffold/.context/integrations/providers/notebooklm.yaml +74 -0
  59. package/scaffold/.context/integrations/providers/notion.yaml +129 -0
  60. package/scaffold/.context/integrations/providers/prod_db.yaml +183 -0
  61. package/scaffold/.context/oscar/workflows/multi-agent.md +82 -0
  62. package/scaffold/.context/oscar/workflows/ollie-sage.md +128 -0
  63. package/scaffold/.context/oscar/workflows/session-git.md +71 -0
  64. package/scaffold/.context/oscar/workflows/setup.md +663 -0
  65. package/scaffold/.context/oscar/workflows/tracking.md +118 -0
  66. package/scaffold/.context/project.yaml.example +102 -0
  67. package/scaffold/.context/templates/dev-guide.md +217 -0
  68. package/scaffold/.context/templates/epic-spec.md +225 -0
  69. package/scaffold/.context/templates/guardrail.md +94 -0
  70. package/scaffold/.context/templates/handoff-checklist.md +197 -0
  71. package/scaffold/.context/templates/prd.md +80 -0
  72. package/scaffold/.context/templates/retrospective.md +78 -0
  73. package/scaffold/.context/templates/screen-spec.md +714 -0
  74. package/scaffold/.context/templates/sprint-plan.md +72 -0
  75. package/scaffold/.context/templates/sprint-status.yaml +109 -0
  76. package/scaffold/.context/templates/story-v2.md +228 -0
  77. package/scaffold/.context/templates/validation-report.md +99 -0
  78. package/scaffold/.gitignore.append +7 -0
  79. package/scaffold/spec-site/env.d.ts +7 -0
  80. package/scaffold/spec-site/index.html +14 -0
  81. package/scaffold/spec-site/package.json +20 -0
  82. package/scaffold/spec-site/src/App.vue +27 -0
  83. package/scaffold/spec-site/src/assets/icons/menu/ic_ads.svg +10 -0
  84. package/scaffold/spec-site/src/assets/icons/menu/ic_ads_on.svg +10 -0
  85. package/scaffold/spec-site/src/assets/icons/menu/ic_board.svg +14 -0
  86. package/scaffold/spec-site/src/assets/icons/menu/ic_board_on.svg +14 -0
  87. package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard.svg +21 -0
  88. package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard_on.svg +21 -0
  89. package/scaffold/spec-site/src/assets/icons/menu/ic_pricing.svg +20 -0
  90. package/scaffold/spec-site/src/assets/icons/menu/ic_pricing_on.svg +20 -0
  91. package/scaffold/spec-site/src/assets/icons/menu/ic_store.svg +11 -0
  92. package/scaffold/spec-site/src/assets/icons/menu/ic_store_on.svg +11 -0
  93. package/scaffold/spec-site/src/components/Accordion.vue +108 -0
  94. package/scaffold/spec-site/src/components/AppHeader.vue +304 -0
  95. package/scaffold/spec-site/src/components/Badge.vue +25 -0
  96. package/scaffold/spec-site/src/components/CoachingCard.vue +112 -0
  97. package/scaffold/spec-site/src/components/MemoSidebar.vue +239 -0
  98. package/scaffold/spec-site/src/components/MockupShell.vue +100 -0
  99. package/scaffold/spec-site/src/components/RuleTable.vue +99 -0
  100. package/scaffold/spec-site/src/components/ScenarioSwitcher.vue +103 -0
  101. package/scaffold/spec-site/src/components/SpecNav.vue +26 -0
  102. package/scaffold/spec-site/src/components/SpecSection.vue +59 -0
  103. package/scaffold/spec-site/src/components/SummaryGrid.vue +39 -0
  104. package/scaffold/spec-site/src/components/VersionBadge.vue +38 -0
  105. package/scaffold/spec-site/src/composables/useActiveSection.ts +53 -0
  106. package/scaffold/spec-site/src/composables/useMemo.ts +138 -0
  107. package/scaffold/spec-site/src/composables/useRetro.ts +313 -0
  108. package/scaffold/spec-site/src/composables/useScenario.ts +43 -0
  109. package/scaffold/spec-site/src/composables/useScenarioStore.ts +102 -0
  110. package/scaffold/spec-site/src/composables/useTurso.ts +160 -0
  111. package/scaffold/spec-site/src/composables/useUser.ts +25 -0
  112. package/scaffold/spec-site/src/data/navigation.ts +59 -0
  113. package/scaffold/spec-site/src/data/types.ts +90 -0
  114. package/scaffold/spec-site/src/data/wireframeRegistry.ts +25 -0
  115. package/scaffold/spec-site/src/layouts/SplitPaneLayout.vue +79 -0
  116. package/scaffold/spec-site/src/main.ts +10 -0
  117. package/scaffold/spec-site/src/pages/IndexPage.vue +66 -0
  118. package/scaffold/spec-site/src/pages/PolicyDetail.vue +215 -0
  119. package/scaffold/spec-site/src/pages/PolicyIndex.vue +74 -0
  120. package/scaffold/spec-site/src/pages/retro/RetroActions.vue +191 -0
  121. package/scaffold/spec-site/src/pages/retro/RetroBoard.vue +192 -0
  122. package/scaffold/spec-site/src/pages/retro/RetroCard.vue +131 -0
  123. package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +287 -0
  124. package/scaffold/spec-site/src/pages/retro/RetroPage.vue +178 -0
  125. package/scaffold/spec-site/src/pages/shared/NoContentPlaceholder.vue +34 -0
  126. package/scaffold/spec-site/src/pages/shared/PlaceholderContent.vue +22 -0
  127. package/scaffold/spec-site/src/pages/shared/PlaceholderSpecPanel.vue +16 -0
  128. package/scaffold/spec-site/src/pages/shared/PolicyFallback.vue +145 -0
  129. package/scaffold/spec-site/src/pages/wireframe/WireframeShell.vue +151 -0
  130. package/scaffold/spec-site/src/router.ts +85 -0
  131. package/scaffold/spec-site/src/styles/base.css +21 -0
  132. package/scaffold/spec-site/src/styles/split-pane.css +143 -0
  133. package/scaffold/spec-site/src/styles/variables.css +47 -0
  134. package/scaffold/spec-site/src/utils/markdown.ts +197 -0
  135. package/scaffold/spec-site/tsconfig.json +20 -0
  136. package/scaffold/spec-site/vite.config.ts +18 -0
@@ -0,0 +1,215 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watchEffect } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+ import { sprints, getPagesByCategory, getEpicSpecFileName } from '../data/navigation'
5
+ import { renderMarkdown } from '../utils/markdown'
6
+
7
+ const route = useRoute()
8
+ const router = useRouter()
9
+
10
+ const sprintId = computed(() => (route.params.sprint as string) || sprints[0]?.id || '')
11
+ const epicId = computed(() => (route.params.epicId as string) || '')
12
+ const sprintConfig = computed(() => sprints.find(s => s.id === sprintId.value))
13
+ const epics = computed(() => getPagesByCategory(sprintId.value, 'policy'))
14
+ const currentEpic = computed(() => epics.value.find(e => e.id === epicId.value))
15
+
16
+ const markdownHtml = ref('')
17
+ const loading = ref(true)
18
+ const error = ref(false)
19
+
20
+ // Glob import all epic spec markdown files
21
+ const mdModules = import.meta.glob(
22
+ '../../../.context/sprints/*/epic-specs/*.md',
23
+ { query: '?raw', import: 'default' }
24
+ )
25
+
26
+ watchEffect(async () => {
27
+ loading.value = true
28
+ error.value = false
29
+ markdownHtml.value = ''
30
+
31
+ const fileName = getEpicSpecFileName(sprintId.value, epicId.value)
32
+ if (!fileName) {
33
+ error.value = true
34
+ loading.value = false
35
+ return
36
+ }
37
+
38
+ const key = `../../../.context/sprints/${sprintId.value}/epic-specs/${fileName}`
39
+ const loader = mdModules[key]
40
+
41
+ if (!loader) {
42
+ error.value = true
43
+ loading.value = false
44
+ return
45
+ }
46
+
47
+ try {
48
+ const raw = (await loader()) as string
49
+ markdownHtml.value = renderMarkdown(raw)
50
+ } catch {
51
+ error.value = true
52
+ }
53
+ loading.value = false
54
+ })
55
+
56
+ function goBack() {
57
+ router.push(`/policy/${sprintId.value}`)
58
+ }
59
+ </script>
60
+
61
+ <template>
62
+ <div class="policy-detail">
63
+ <!-- Sidebar: epic list -->
64
+ <aside class="policy-sidebar">
65
+ <div class="sidebar-header">
66
+ <button class="back-btn" @click="goBack">&larr; {{ sprintConfig?.label }}</button>
67
+ </div>
68
+ <router-link
69
+ v-for="epic in epics"
70
+ :key="epic.id"
71
+ :to="`/policy/${sprintId}/${epic.id}`"
72
+ class="sidebar-item"
73
+ :class="{ active: epic.id === epicId }"
74
+ >
75
+ <span class="sidebar-id">{{ epic.id }}</span>
76
+ <span class="sidebar-label">{{ epic.label }}</span>
77
+ </router-link>
78
+ </aside>
79
+
80
+ <!-- Content -->
81
+ <main class="policy-content">
82
+ <div v-if="loading" class="policy-loading">Loading...</div>
83
+ <div v-else-if="error" class="policy-error">
84
+ <p>Document not found.</p>
85
+ </div>
86
+ <article v-else class="markdown-body" v-html="markdownHtml"></article>
87
+ </main>
88
+ </div>
89
+ </template>
90
+
91
+ <style scoped>
92
+ .policy-detail {
93
+ display: flex;
94
+ height: calc(100vh - var(--header-height));
95
+ overflow: hidden;
96
+ }
97
+
98
+ /* ---- Sidebar ---- */
99
+ .policy-sidebar {
100
+ width: 220px;
101
+ flex-shrink: 0;
102
+ border-right: 1px solid var(--border);
103
+ background: #fff;
104
+ overflow-y: auto;
105
+ padding: 12px 0;
106
+ }
107
+
108
+ .sidebar-header { padding: 0 12px 8px; }
109
+ .back-btn {
110
+ font-size: 12px;
111
+ color: var(--text-muted);
112
+ background: none;
113
+ border: none;
114
+ cursor: pointer;
115
+ font-family: var(--font-kr);
116
+ padding: 4px 8px;
117
+ border-radius: 4px;
118
+ transition: all 0.15s;
119
+ }
120
+ .back-btn:hover { background: var(--bg); color: var(--text-primary); }
121
+
122
+ .sidebar-item {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 8px;
126
+ padding: 8px 16px;
127
+ font-size: 12px;
128
+ color: var(--text-secondary);
129
+ text-decoration: none;
130
+ transition: all 0.1s;
131
+ border-left: 3px solid transparent;
132
+ }
133
+ .sidebar-item:hover { background: var(--bg); color: var(--text-primary); }
134
+ .sidebar-item.active {
135
+ background: var(--primary-light);
136
+ color: var(--primary);
137
+ border-left-color: var(--primary);
138
+ font-weight: 600;
139
+ }
140
+
141
+ .sidebar-id {
142
+ font-size: 10px;
143
+ font-weight: 700;
144
+ color: var(--text-muted);
145
+ min-width: 32px;
146
+ }
147
+ .sidebar-item.active .sidebar-id { color: var(--primary); }
148
+ .sidebar-label { flex: 1; }
149
+
150
+ /* ---- Content ---- */
151
+ .policy-content {
152
+ flex: 1;
153
+ overflow-y: auto;
154
+ padding: 32px 40px;
155
+ background: var(--bg);
156
+ }
157
+
158
+ .policy-loading,
159
+ .policy-error {
160
+ display: flex;
161
+ justify-content: center;
162
+ align-items: center;
163
+ height: 200px;
164
+ color: var(--text-muted);
165
+ font-size: 14px;
166
+ }
167
+
168
+ /* ---- Markdown styles ---- */
169
+ .markdown-body {
170
+ max-width: 800px;
171
+ font-size: 14px;
172
+ line-height: 1.7;
173
+ color: var(--text-primary);
174
+ }
175
+ .markdown-body :deep(h1) { font-size: 24px; font-weight: 700; margin: 0 0 16px; padding-bottom: 8px; border-bottom: 2px solid var(--border); }
176
+ .markdown-body :deep(h2) { font-size: 19px; font-weight: 700; margin: 32px 0 12px; }
177
+ .markdown-body :deep(h3) { font-size: 15px; font-weight: 700; margin: 24px 0 8px; }
178
+ .markdown-body :deep(h4) { font-size: 14px; font-weight: 700; margin: 20px 0 6px; color: var(--text-secondary); }
179
+ .markdown-body :deep(p) { margin: 0 0 10px; }
180
+ .markdown-body :deep(strong) { font-weight: 700; }
181
+ .markdown-body :deep(code) {
182
+ font-size: 12px;
183
+ background: var(--border-light);
184
+ padding: 2px 5px;
185
+ border-radius: 3px;
186
+ font-family: 'SF Mono', 'Menlo', monospace;
187
+ }
188
+ .markdown-body :deep(pre) {
189
+ background: var(--text-primary);
190
+ color: #e5e7eb;
191
+ padding: 16px;
192
+ border-radius: var(--radius-sm);
193
+ overflow-x: auto;
194
+ margin: 12px 0;
195
+ font-size: 12px;
196
+ line-height: 1.6;
197
+ }
198
+ .markdown-body :deep(pre code) { background: none; padding: 0; color: inherit; }
199
+ .markdown-body :deep(ul), .markdown-body :deep(ol) { margin: 8px 0; padding-left: 24px; }
200
+ .markdown-body :deep(li) { margin: 4px 0; }
201
+ .markdown-body :deep(blockquote) {
202
+ border-left: 3px solid var(--primary);
203
+ padding: 8px 16px;
204
+ margin: 12px 0;
205
+ background: var(--primary-light);
206
+ color: var(--text-secondary);
207
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
208
+ }
209
+ .markdown-body :deep(hr) { border: none; border-top: 1px solid var(--border); margin: 24px 0; }
210
+ .markdown-body :deep(table) { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; }
211
+ .markdown-body :deep(th) { text-align: left; padding: 8px 12px; background: var(--border-light); font-weight: 700; border: 1px solid var(--border); }
212
+ .markdown-body :deep(td) { padding: 8px 12px; border: 1px solid var(--border); }
213
+ .markdown-body :deep(a) { color: var(--primary); text-decoration: none; }
214
+ .markdown-body :deep(a:hover) { text-decoration: underline; }
215
+ </style>
@@ -0,0 +1,74 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+ import { sprints, getPagesByCategory } from '../data/navigation'
5
+
6
+ const route = useRoute()
7
+ const router = useRouter()
8
+
9
+ const sprintId = computed(() => (route.params.sprint as string) || sprints[0]?.id || '')
10
+ const sprintConfig = computed(() => sprints.find(s => s.id === sprintId.value))
11
+ const epics = computed(() => getPagesByCategory(sprintId.value, 'policy'))
12
+ </script>
13
+
14
+ <template>
15
+ <div class="policy-index">
16
+ <div class="policy-header">
17
+ <h1>{{ sprintConfig?.label }} Policy</h1>
18
+ <p class="policy-subtitle">{{ sprintConfig?.theme }} · {{ epics.length }} epic specs</p>
19
+ </div>
20
+
21
+ <div class="epic-grid">
22
+ <div
23
+ v-for="epic in epics"
24
+ :key="epic.id"
25
+ class="epic-card"
26
+ @click="router.push(`/policy/${sprintId}/${epic.id}`)"
27
+ >
28
+ <div class="epic-card-header">
29
+ <span class="epic-id">{{ epic.id }}</span>
30
+ <span v-if="epic.badge" class="epic-badge" :class="badgeClass(epic.badge)">{{ epic.badge }}</span>
31
+ </div>
32
+ <div class="epic-title">{{ epic.label }}</div>
33
+ <div v-if="epic.description" class="epic-desc">{{ epic.description }}</div>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </template>
38
+
39
+ <script lang="ts">
40
+ function badgeClass(badge: string): string {
41
+ if (badge === 'P0') return 'badge-red'
42
+ if (badge === 'P1') return 'badge-yellow'
43
+ if (badge === 'out') return 'badge-muted'
44
+ return 'badge-blue'
45
+ }
46
+ </script>
47
+
48
+ <style scoped>
49
+ .policy-index { padding: 48px 40px; max-width: 960px; margin: 0 auto; }
50
+ .policy-header { margin-bottom: 32px; }
51
+ h1 { font-size: 24px; font-weight: 700; margin-bottom: 6px; }
52
+ .policy-subtitle { font-size: 13px; color: var(--text-secondary); }
53
+
54
+ .epic-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
55
+ .epic-card {
56
+ padding: 20px;
57
+ background: var(--card-bg);
58
+ border-radius: var(--radius-sm);
59
+ border: 1px solid var(--border-light);
60
+ box-shadow: var(--shadow);
61
+ cursor: pointer;
62
+ transition: all 0.15s;
63
+ }
64
+ .epic-card:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }
65
+ .epic-card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
66
+ .epic-id { font-size: 11px; font-weight: 700; color: var(--primary); padding: 2px 6px; background: var(--primary-light); border-radius: 4px; }
67
+ .epic-badge { font-size: 9px; font-weight: 700; padding: 2px 5px; border-radius: 3px; text-transform: uppercase; margin-left: auto; }
68
+ .badge-red { background: var(--red-bg); color: var(--red); }
69
+ .badge-yellow { background: var(--yellow-bg); color: var(--yellow); }
70
+ .badge-blue { background: var(--blue-bg); color: var(--blue); }
71
+ .badge-muted { background: var(--border-light); color: var(--text-muted); }
72
+ .epic-title { font-size: 15px; font-weight: 700; margin-bottom: 4px; }
73
+ .epic-desc { font-size: 12px; color: var(--text-secondary); line-height: 1.5; }
74
+ </style>
@@ -0,0 +1,191 @@
1
+ <script setup lang="ts">
2
+ import type { RetroAction } from '@/composables/useRetro'
3
+ import { ref } from 'vue'
4
+
5
+ defineProps<{
6
+ actions: RetroAction[]
7
+ teamMembers: string[]
8
+ readonly?: boolean
9
+ }>()
10
+
11
+ const emit = defineEmits<{
12
+ (e: 'add-action', content: string, assignee: string | null): void
13
+ (e: 'toggle-status', id: number, status: 'pending' | 'done'): void
14
+ }>()
15
+
16
+ const newContent = ref('')
17
+ const newAssignee = ref('')
18
+
19
+ function handleAdd() {
20
+ const text = newContent.value.trim()
21
+ if (!text) return
22
+ emit('add-action', text, newAssignee.value || null)
23
+ newContent.value = ''
24
+ newAssignee.value = ''
25
+ }
26
+ </script>
27
+
28
+ <template>
29
+ <div class="retro-actions">
30
+ <div class="actions-title">Action Items</div>
31
+
32
+ <div v-if="!readonly" class="actions-add">
33
+ <input
34
+ v-model="newContent"
35
+ class="actions-input"
36
+ placeholder="Add action item..."
37
+ @keydown.enter="handleAdd"
38
+ />
39
+ <select v-model="newAssignee" class="actions-select">
40
+ <option value="">Assignee</option>
41
+ <option v-for="m in teamMembers" :key="m" :value="m">{{ m }}</option>
42
+ </select>
43
+ <button class="actions-add-btn" :disabled="!newContent.trim()" @click="handleAdd">
44
+ Add
45
+ </button>
46
+ </div>
47
+
48
+ <div class="actions-list">
49
+ <div
50
+ v-for="action in actions"
51
+ :key="action.id"
52
+ class="action-row"
53
+ :class="{ done: action.status === 'done' }"
54
+ >
55
+ <button
56
+ class="action-check"
57
+ :disabled="readonly"
58
+ @click="emit('toggle-status', action.id, action.status)"
59
+ >
60
+ {{ action.status === 'done' ? '&#9989;' : '&#11036;' }}
61
+ </button>
62
+ <span class="action-content">{{ action.content }}</span>
63
+ <span v-if="action.assignee" class="action-assignee">@{{ action.assignee }}</span>
64
+ </div>
65
+ <div v-if="actions.length === 0" class="actions-empty">No action items yet</div>
66
+ </div>
67
+ </div>
68
+ </template>
69
+
70
+ <style scoped>
71
+ .retro-actions {
72
+ background: var(--card-bg);
73
+ border: 1px solid var(--border);
74
+ border-radius: var(--radius);
75
+ padding: 16px;
76
+ margin: 0 16px 16px;
77
+ }
78
+
79
+ .actions-title {
80
+ font-size: 15px;
81
+ font-weight: 700;
82
+ color: var(--text-primary);
83
+ margin-bottom: 12px;
84
+ }
85
+
86
+ .actions-add {
87
+ display: flex;
88
+ gap: 8px;
89
+ margin-bottom: 12px;
90
+ }
91
+
92
+ .actions-input {
93
+ flex: 1;
94
+ padding: 8px 12px;
95
+ border: 1px solid var(--border);
96
+ border-radius: var(--radius-sm);
97
+ font-size: 13px;
98
+ font-family: var(--font-kr);
99
+ outline: none;
100
+ transition: border-color 0.15s;
101
+ }
102
+ .actions-input:focus {
103
+ border-color: var(--primary);
104
+ }
105
+
106
+ .actions-select {
107
+ padding: 8px 10px;
108
+ border: 1px solid var(--border);
109
+ border-radius: var(--radius-sm);
110
+ font-size: 13px;
111
+ font-family: var(--font-kr);
112
+ background: #fff;
113
+ min-width: 90px;
114
+ }
115
+
116
+ .actions-add-btn {
117
+ padding: 8px 16px;
118
+ background: var(--primary);
119
+ color: #fff;
120
+ border: none;
121
+ border-radius: var(--radius-sm);
122
+ font-size: 13px;
123
+ font-weight: 600;
124
+ font-family: var(--font-kr);
125
+ cursor: pointer;
126
+ transition: opacity 0.15s;
127
+ white-space: nowrap;
128
+ }
129
+ .actions-add-btn:disabled {
130
+ opacity: 0.4;
131
+ cursor: not-allowed;
132
+ }
133
+
134
+ .actions-list {
135
+ display: flex;
136
+ flex-direction: column;
137
+ gap: 4px;
138
+ }
139
+
140
+ .action-row {
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 8px;
144
+ padding: 8px;
145
+ border-radius: 6px;
146
+ transition: background 0.1s;
147
+ }
148
+ .action-row:hover {
149
+ background: var(--bg);
150
+ }
151
+ .action-row.done .action-content {
152
+ text-decoration: line-through;
153
+ color: var(--text-muted);
154
+ }
155
+
156
+ .action-check {
157
+ border: none;
158
+ background: none;
159
+ font-size: 16px;
160
+ cursor: pointer;
161
+ padding: 0;
162
+ line-height: 1;
163
+ flex-shrink: 0;
164
+ }
165
+ .action-check:disabled {
166
+ cursor: default;
167
+ }
168
+
169
+ .action-content {
170
+ flex: 1;
171
+ font-size: 14px;
172
+ color: var(--text-primary);
173
+ }
174
+
175
+ .action-assignee {
176
+ font-size: 12px;
177
+ color: var(--primary);
178
+ font-weight: 500;
179
+ background: var(--primary-light);
180
+ padding: 2px 8px;
181
+ border-radius: 10px;
182
+ white-space: nowrap;
183
+ }
184
+
185
+ .actions-empty {
186
+ text-align: center;
187
+ padding: 20px;
188
+ color: var(--text-muted);
189
+ font-size: 13px;
190
+ }
191
+ </style>
@@ -0,0 +1,192 @@
1
+ <script setup lang="ts">
2
+ import type { RetroItem, RetroCategory, RetroPhase } from '@/composables/useRetro'
3
+ import RetroCard from './RetroCard.vue'
4
+ import { ref } from 'vue'
5
+
6
+ const props = defineProps<{
7
+ keepItems: RetroItem[]
8
+ problemItems: RetroItem[]
9
+ tryItems: RetroItem[]
10
+ phase: RetroPhase
11
+ currentUser: string
12
+ votesRemaining: number
13
+ }>()
14
+
15
+ const emit = defineEmits<{
16
+ (e: 'add-item', category: RetroCategory, content: string, author: string): void
17
+ (e: 'delete-item', id: number): void
18
+ (e: 'toggle-vote', id: number, hasVoted: boolean): void
19
+ }>()
20
+
21
+ const newContent = ref<Record<RetroCategory, string>>({
22
+ keep: '',
23
+ problem: '',
24
+ try: '',
25
+ })
26
+
27
+ function handleAdd(cat: RetroCategory, e?: KeyboardEvent) {
28
+ if (e?.isComposing) return
29
+ const text = newContent.value[cat].trim()
30
+ if (!text) return
31
+ emit('add-item', cat, text, props.currentUser)
32
+ newContent.value[cat] = ''
33
+ }
34
+
35
+ function getItems(cat: RetroCategory): RetroItem[] {
36
+ if (cat === 'keep') return props.keepItems
37
+ if (cat === 'problem') return props.problemItems
38
+ return props.tryItems
39
+ }
40
+
41
+ function getSortedItems(cat: RetroCategory): RetroItem[] {
42
+ const list = getItems(cat)
43
+ if (props.phase === 'discuss') {
44
+ return [...list].sort((a, b) => b.voteCount - a.voteCount)
45
+ }
46
+ return list
47
+ }
48
+
49
+ const COLUMNS: { id: RetroCategory; label: string; emoji: string; bg: string }[] = [
50
+ { id: 'keep', label: 'Keep', emoji: '&#9989;', bg: '#E8F5E9' },
51
+ { id: 'problem', label: 'Problem', emoji: '&#128308;', bg: '#FFF4F4' },
52
+ { id: 'try', label: 'Try', emoji: '&#128161;', bg: '#EFF6FF' },
53
+ ]
54
+ </script>
55
+
56
+ <template>
57
+ <div class="retro-board">
58
+ <div
59
+ v-for="col in COLUMNS"
60
+ :key="col.id"
61
+ class="retro-col"
62
+ :style="{ '--col-bg': col.bg }"
63
+ >
64
+ <div class="col-header">
65
+ <span class="col-emoji" v-html="col.emoji" />
66
+ <span class="col-label">{{ col.label }}</span>
67
+ <span class="col-count">{{ getItems(col.id).length }}</span>
68
+ </div>
69
+
70
+ <!-- Input area: write phase only -->
71
+ <div v-if="phase === 'write'" class="col-input">
72
+ <textarea
73
+ v-model="newContent[col.id]"
74
+ class="col-textarea"
75
+ placeholder="Add card... (Enter to submit)"
76
+ rows="2"
77
+ @keydown.enter.exact.prevent="handleAdd(col.id, $event)"
78
+ />
79
+ <button
80
+ class="col-add-btn"
81
+ :disabled="!newContent[col.id].trim()"
82
+ @click="handleAdd(col.id)"
83
+ >
84
+ Add
85
+ </button>
86
+ </div>
87
+
88
+ <!-- Cards -->
89
+ <div class="col-cards">
90
+ <RetroCard
91
+ v-for="item in getSortedItems(col.id)"
92
+ :key="item.id"
93
+ :item="item"
94
+ :phase="phase"
95
+ :current-user="currentUser"
96
+ :can-vote="votesRemaining > 0 || item.hasVoted"
97
+ @delete="emit('delete-item', item.id)"
98
+ @toggle-vote="emit('toggle-vote', item.id, item.hasVoted)"
99
+ />
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </template>
104
+
105
+ <style scoped>
106
+ .retro-board {
107
+ display: grid;
108
+ grid-template-columns: repeat(3, 1fr);
109
+ gap: 12px;
110
+ padding: 16px;
111
+ flex: 1;
112
+ overflow-y: auto;
113
+ min-height: 0;
114
+ }
115
+
116
+ .retro-col {
117
+ background: var(--col-bg);
118
+ border-radius: var(--radius);
119
+ padding: 12px;
120
+ display: flex;
121
+ flex-direction: column;
122
+ gap: 8px;
123
+ min-height: 200px;
124
+ }
125
+
126
+ .col-header {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 6px;
130
+ font-weight: 700;
131
+ font-size: 14px;
132
+ padding-bottom: 8px;
133
+ border-bottom: 1px solid rgba(0, 0, 0, 0.06);
134
+ }
135
+
136
+ .col-count {
137
+ margin-left: auto;
138
+ font-size: 11px;
139
+ color: var(--text-muted);
140
+ background: rgba(0, 0, 0, 0.06);
141
+ border-radius: 10px;
142
+ padding: 1px 7px;
143
+ font-weight: 600;
144
+ }
145
+
146
+ .col-input {
147
+ display: flex;
148
+ flex-direction: column;
149
+ gap: 6px;
150
+ }
151
+
152
+ .col-textarea {
153
+ width: 100%;
154
+ padding: 8px 10px;
155
+ border: 1px solid rgba(0, 0, 0, 0.1);
156
+ border-radius: 6px;
157
+ font-size: 13px;
158
+ font-family: var(--font-kr);
159
+ resize: none;
160
+ outline: none;
161
+ transition: border-color 0.15s;
162
+ background: #fff;
163
+ box-sizing: border-box;
164
+ }
165
+ .col-textarea:focus {
166
+ border-color: var(--primary);
167
+ }
168
+
169
+ .col-add-btn {
170
+ align-self: flex-end;
171
+ padding: 5px 14px;
172
+ background: var(--primary);
173
+ color: #fff;
174
+ border: none;
175
+ border-radius: 6px;
176
+ font-size: 12px;
177
+ font-weight: 600;
178
+ font-family: var(--font-kr);
179
+ cursor: pointer;
180
+ transition: opacity 0.15s;
181
+ }
182
+ .col-add-btn:disabled {
183
+ opacity: 0.3;
184
+ cursor: not-allowed;
185
+ }
186
+
187
+ .col-cards {
188
+ display: flex;
189
+ flex-direction: column;
190
+ gap: 8px;
191
+ }
192
+ </style>