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,225 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted, watch } from 'vue'
3
+ import { useRoute } from 'vue-router'
4
+ import { useStandup, type StandupFeedback } from '@/composables/useStandup'
5
+ import { useUser, TEAM_MEMBERS } from '@/composables/useUser'
6
+ import { loadPmData, loadEpics, stories, tasks, addTask } from '@/composables/usePmStore'
7
+ import StandupEntryCard from './StandupEntryCard.vue'
8
+
9
+ const route = useRoute()
10
+ const { currentUser, dynamicMembers, loadMembers } = useUser()
11
+
12
+ const sprint = computed(() => route.params.sprint as string)
13
+ const { entries, loading, error, feedback, loadEntries, saveEntry, getEntryForUser, loadFeedback, submitFeedback, startPolling, stopPolling } = useStandup()
14
+
15
+ // Track feedback per entry
16
+ const feedbackByEntry = ref<Record<number, StandupFeedback[]>>({})
17
+
18
+ async function loadAllFeedback() {
19
+ const { apiGet } = await import('@/api/client')
20
+ const results = await Promise.all(entries.value.map(async (entry) => {
21
+ const { data } = await apiGet<{ feedback: Array<{
22
+ id: number; standup_entry_id: number; sprint: string; target_user: string
23
+ feedback_by: string; feedback_text: string; review_type: string; created_at: string
24
+ }> }>('/api/v2/standup/feedback', { standup_entry_id: String(entry.id) })
25
+ const items: StandupFeedback[] = (data?.feedback ?? []).map(r => ({
26
+ id: r.id, standupEntryId: r.standup_entry_id, sprint: r.sprint,
27
+ targetUser: r.target_user, feedbackBy: r.feedback_by,
28
+ feedbackText: r.feedback_text,
29
+ reviewType: (r.review_type as StandupFeedback['reviewType']) || 'comment',
30
+ createdAt: r.created_at,
31
+ }))
32
+ return { id: entry.id, items }
33
+ }))
34
+ const map: Record<number, StandupFeedback[]> = {}
35
+ for (const r of results) map[r.id] = r.items
36
+ feedbackByEntry.value = map
37
+ }
38
+
39
+ function getFeedbackForEntry(entryId: number | undefined): StandupFeedback[] {
40
+ if (!entryId) return []
41
+ return feedbackByEntry.value[entryId] ?? []
42
+ }
43
+
44
+ function todayStr(): string {
45
+ return new Date().toISOString().split('T')[0]
46
+ }
47
+
48
+ const selectedDate = ref(todayStr())
49
+
50
+ const members = computed(() => {
51
+ return dynamicMembers.value.length > 0 ? dynamicMembers.value : [...TEAM_MEMBERS]
52
+ })
53
+
54
+ // PM data — sprint scoped (for story picker in edit mode)
55
+ const sprintStories = computed(() => stories.value.filter(s => s.sprint === sprint.value))
56
+ const sprintTasks = computed(() => {
57
+ const storyIds = new Set(sprintStories.value.map(s => s.id))
58
+ return tasks.value.filter(t => storyIds.has(t.storyId))
59
+ })
60
+
61
+ async function onDateChange() {
62
+ await loadEntries(selectedDate.value)
63
+ await loadAllFeedback()
64
+ stopPolling()
65
+ startPolling(selectedDate.value)
66
+ }
67
+
68
+ async function handleSave(userName: string, data: { doneText: string | null; planText: string | null; planStoryIds: number[]; blockers: string | null }) {
69
+ await saveEntry(userName, selectedDate.value, sprint.value, data)
70
+ }
71
+
72
+ async function handleCreateTask(userName: string, data: { storyId: number; title: string }) {
73
+ await addTask({ storyId: data.storyId, title: data.title, assignee: userName })
74
+ }
75
+
76
+ async function handleSubmitFeedback(userName: string, data: { feedbackText: string; reviewType: string }) {
77
+ const entry = getEntryForUser(userName, selectedDate.value)
78
+ if (!entry) return
79
+ if (!currentUser.value) return
80
+ await submitFeedback(entry.id, sprint.value, userName, currentUser.value, data.feedbackText, data.reviewType)
81
+ await loadAllFeedback()
82
+ }
83
+
84
+ function changeDate(delta: number) {
85
+ const d = new Date(selectedDate.value)
86
+ d.setDate(d.getDate() + delta)
87
+ selectedDate.value = d.toISOString().split('T')[0]
88
+ }
89
+
90
+ watch(selectedDate, () => onDateChange())
91
+
92
+ onMounted(async () => {
93
+ await loadMembers()
94
+ await Promise.all([
95
+ loadEntries(selectedDate.value),
96
+ loadEpics(),
97
+ loadPmData(sprint.value),
98
+ ])
99
+ await loadAllFeedback()
100
+ startPolling(selectedDate.value)
101
+ })
102
+ </script>
103
+
104
+ <template>
105
+ <div class="standup-page">
106
+ <div class="standup-header">
107
+ <h1>Daily Standup</h1>
108
+ <div class="date-nav">
109
+ <button class="date-btn" @click="changeDate(-1)">&larr;</button>
110
+ <input
111
+ type="date"
112
+ v-model="selectedDate"
113
+ class="date-input"
114
+ />
115
+ <button class="date-btn" @click="changeDate(1)">&rarr;</button>
116
+ <button class="date-btn date-btn--today" @click="selectedDate = todayStr()">Today</button>
117
+ </div>
118
+ </div>
119
+
120
+ <div v-if="error" class="error-msg">{{ error }}</div>
121
+ <div v-if="loading" class="loading">Loading...</div>
122
+
123
+ <div v-else class="entries-grid">
124
+ <StandupEntryCard
125
+ v-for="member in members"
126
+ :key="member"
127
+ :entry="getEntryForUser(member, selectedDate)"
128
+ :user-name="member"
129
+ :editable="currentUser === member"
130
+ :current-user="currentUser ?? ''"
131
+ :sprint-stories="sprintStories"
132
+ :sprint-tasks="sprintTasks"
133
+ :feedback="getFeedbackForEntry(getEntryForUser(member, selectedDate)?.id)"
134
+ @save="(data) => handleSave(member, data)"
135
+ @create-task="(data) => handleCreateTask(member, data)"
136
+ @submit-feedback="(data) => handleSubmitFeedback(member, data)"
137
+ />
138
+ </div>
139
+ </div>
140
+ </template>
141
+
142
+ <style scoped>
143
+ .standup-page {
144
+ max-width: 1100px;
145
+ margin: 0 auto;
146
+ padding: 24px;
147
+ height: 100%;
148
+ overflow-y: auto;
149
+ }
150
+
151
+ .standup-header {
152
+ display: flex;
153
+ align-items: center;
154
+ justify-content: space-between;
155
+ margin-bottom: 20px;
156
+ flex-wrap: wrap;
157
+ gap: 12px;
158
+ }
159
+
160
+ h1 {
161
+ font-size: 22px;
162
+ font-weight: 700;
163
+ color: var(--text-primary);
164
+ }
165
+
166
+ .date-nav {
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 4px;
170
+ }
171
+
172
+ .date-btn {
173
+ padding: 6px 10px;
174
+ border: 1px solid rgba(0,0,0,0.06);
175
+ border-radius: 6px;
176
+ background: rgba(255,255,255,0.25);
177
+ backdrop-filter: blur(20px);
178
+ -webkit-backdrop-filter: blur(20px);
179
+ color: var(--text-secondary);
180
+ cursor: pointer;
181
+ font-size: 13px;
182
+ transition: all 0.1s;
183
+ }
184
+ .date-btn:hover { background: rgba(255,255,255,0.4); }
185
+ .date-btn--today {
186
+ color: #3b82f6;
187
+ border-color: #bfdbfe;
188
+ }
189
+
190
+ .date-input {
191
+ padding: 6px 10px;
192
+ border: 1px solid rgba(0,0,0,0.06);
193
+ background: rgba(255,255,255,0.25);
194
+ backdrop-filter: blur(20px);
195
+ -webkit-backdrop-filter: blur(20px);
196
+ border-radius: 6px;
197
+ font-size: 13px;
198
+ color: var(--text-primary);
199
+ font-weight: 600;
200
+ }
201
+ .date-input:focus { outline: none; border-color: #3b82f6; }
202
+
203
+ .entries-grid {
204
+ display: grid;
205
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
206
+ gap: 12px;
207
+ }
208
+
209
+ .loading, .error-msg {
210
+ text-align: center;
211
+ padding: 40px;
212
+ font-size: 14px;
213
+ }
214
+ .loading { color: var(--text-muted); }
215
+ .error-msg { color: var(--red); }
216
+
217
+ @media (max-width: 767px) {
218
+ .standup-page { padding: 12px; }
219
+ .standup-header { flex-direction: column; align-items: flex-start; gap: 8px; }
220
+ .standup-header h1 { font-size: 18px; }
221
+ .entries-grid { grid-template-columns: 1fr; gap: 8px; }
222
+ .date-nav { flex-wrap: wrap; }
223
+ .date-input { font-size: 12px; }
224
+ }
225
+ </style>
@@ -2,14 +2,154 @@ import { createRouter, createWebHistory } from 'vue-router'
2
2
  import { isValidFeaturePage, featurePages } from './data/navigation'
3
3
  import { getWireframe, getAvailableSprints } from './data/wireframeRegistry'
4
4
  import { getActiveSprint, sprints } from './composables/useNavStore'
5
+ import { isFeatureEnabled } from './features'
5
6
 
6
7
  function currentActiveSprint(): string {
7
8
  return getActiveSprint().id
8
9
  }
9
10
 
11
+ /** Guard: redirect to '/' if feature is disabled */
12
+ function featureGuard(featureId: string) {
13
+ return () => {
14
+ if (!isFeatureEnabled(featureId as any)) return '/'
15
+ }
16
+ }
17
+
10
18
  const routes = [
11
19
  { path: '/', component: () => import('./pages/IndexPage.vue') },
12
20
 
21
+ // -- Tier 2: Dashboard --
22
+ {
23
+ path: '/dashboard',
24
+ component: () => import('./pages/DashboardPage.vue'),
25
+ meta: { title: 'Dashboard' },
26
+ beforeEnter: featureGuard('dashboard'),
27
+ },
28
+
29
+ // -- Tier 2: Board --
30
+ {
31
+ path: '/board',
32
+ redirect: () => `/board/${currentActiveSprint()}`,
33
+ },
34
+ {
35
+ path: '/board/backlog',
36
+ component: () => import('./pages/board/BoardPage.vue'),
37
+ meta: { title: 'Backlog' },
38
+ beforeEnter: featureGuard('board'),
39
+ },
40
+ {
41
+ path: '/board/:sprint',
42
+ component: () => import('./pages/board/BoardPage.vue'),
43
+ meta: { title: 'Board' },
44
+ beforeEnter: featureGuard('board'),
45
+ },
46
+
47
+ // -- Board Admin --
48
+ {
49
+ path: '/admin/board',
50
+ component: () => import('./pages/board/BoardAdmin.vue'),
51
+ meta: { title: 'Board Admin' },
52
+ beforeEnter: featureGuard('admin'),
53
+ },
54
+
55
+ // -- My Tasks --
56
+ {
57
+ path: '/my-tasks',
58
+ redirect: () => `/my-tasks/${currentActiveSprint()}`,
59
+ },
60
+ {
61
+ path: '/my-tasks/:sprint',
62
+ component: () => import('./pages/board/MyTasksPage.vue'),
63
+ meta: { title: 'My Tasks' },
64
+ beforeEnter: featureGuard('board'),
65
+ },
66
+
67
+ // -- Sprint Kickoff --
68
+ {
69
+ path: '/kickoff/new',
70
+ component: () => import('./pages/board/SprintKickoff.vue'),
71
+ meta: { title: 'Sprint Kickoff' },
72
+ beforeEnter: featureGuard('board'),
73
+ },
74
+ {
75
+ path: '/kickoff/:sprintId',
76
+ component: () => import('./pages/board/SprintKickoff.vue'),
77
+ meta: { title: 'Sprint Kickoff' },
78
+ beforeEnter: featureGuard('board'),
79
+ },
80
+
81
+ // -- Sprint Close --
82
+ {
83
+ path: '/close/:sprintId',
84
+ component: () => import('./pages/board/SprintClose.vue'),
85
+ meta: { title: 'Sprint Close' },
86
+ beforeEnter: featureGuard('board'),
87
+ },
88
+
89
+ // -- Tier 2: Standup --
90
+ {
91
+ path: '/standup',
92
+ redirect: () => `/standup/${currentActiveSprint()}`,
93
+ },
94
+ {
95
+ path: '/standup/:sprint',
96
+ component: () => import('./pages/standup/StandupPage.vue'),
97
+ meta: { title: 'Standup' },
98
+ beforeEnter: featureGuard('standup'),
99
+ },
100
+
101
+ // -- Tier 2: Inbox --
102
+ {
103
+ path: '/inbox',
104
+ component: () => import('./pages/InboxPage.vue'),
105
+ meta: { title: 'Inbox' },
106
+ beforeEnter: featureGuard('inbox'),
107
+ },
108
+
109
+ // -- Tier 2: My Page --
110
+ {
111
+ path: '/my',
112
+ component: () => import('./pages/MyPage.vue'),
113
+ meta: { title: 'My Page' },
114
+ beforeEnter: featureGuard('my-page'),
115
+ },
116
+ {
117
+ path: '/me',
118
+ redirect: '/my',
119
+ },
120
+ {
121
+ path: '/my-page',
122
+ redirect: '/my',
123
+ },
124
+
125
+ // -- Tier 2: Admin --
126
+ {
127
+ path: '/admin',
128
+ component: () => import('./pages/AdminPage.vue'),
129
+ meta: { title: 'Admin' },
130
+ beforeEnter: featureGuard('admin'),
131
+ },
132
+
133
+ // -- Tier 2: Optional modules --
134
+ {
135
+ path: '/rewards',
136
+ component: () => import('./pages/RewardsPage.vue'),
137
+ meta: { title: 'Rewards' },
138
+ beforeEnter: featureGuard('rewards'),
139
+ },
140
+ {
141
+ path: '/meetings',
142
+ component: () => import('./pages/MeetingsPage.vue'),
143
+ meta: { title: 'Meetings' },
144
+ beforeEnter: featureGuard('meetings'),
145
+ },
146
+ {
147
+ path: '/docs',
148
+ component: () => import('./pages/DocsHub.vue'),
149
+ meta: { title: 'Docs' },
150
+ beforeEnter: featureGuard('docs'),
151
+ },
152
+
13
153
  // -- Policy documents --
14
154
  {
15
155
  path: '/policy',
@@ -35,6 +175,7 @@ const routes = [
35
175
  path: '/retro/:sprint',
36
176
  component: () => import('./pages/retro/RetroPage.vue'),
37
177
  meta: { title: 'Retro' },
178
+ beforeEnter: featureGuard('retro'),
38
179
  },
39
180
 
40
181
  // -- Feature pages (wireframe shell) --
@@ -0,0 +1,124 @@
1
+ /* Button design system */
2
+
3
+ .btn {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ gap: 6px;
8
+ border: none;
9
+ border-radius: var(--radius-md, 8px);
10
+ font-size: 13px;
11
+ font-weight: 500;
12
+ cursor: pointer;
13
+ transition: all 0.15s ease;
14
+ padding: 8px 16px;
15
+ line-height: 1.4;
16
+ white-space: nowrap;
17
+ font-family: inherit;
18
+ }
19
+
20
+ .btn:disabled {
21
+ opacity: 0.5;
22
+ cursor: not-allowed;
23
+ }
24
+
25
+ /* Primary */
26
+ .btn--primary {
27
+ background: #3b82f6;
28
+ color: #fff;
29
+ }
30
+ .btn--primary:hover:not(:disabled) {
31
+ background: #2563eb;
32
+ }
33
+
34
+ /* Secondary */
35
+ .btn--secondary {
36
+ background: #f3f4f6;
37
+ color: #374151;
38
+ }
39
+ .btn--secondary:hover:not(:disabled) {
40
+ background: #e5e7eb;
41
+ }
42
+
43
+ /* Ghost */
44
+ .btn--ghost {
45
+ background: transparent;
46
+ color: #6b7280;
47
+ }
48
+ .btn--ghost:hover:not(:disabled) {
49
+ background: #f3f4f6;
50
+ color: #374151;
51
+ }
52
+
53
+ /* Danger */
54
+ .btn--danger {
55
+ background: #fef2f2;
56
+ color: #dc2626;
57
+ }
58
+ .btn--danger:hover:not(:disabled) {
59
+ background: #fee2e2;
60
+ }
61
+
62
+ /* Sizes */
63
+ .btn--xs {
64
+ font-size: 11px;
65
+ padding: 4px 8px;
66
+ border-radius: 6px;
67
+ }
68
+ .btn--sm {
69
+ font-size: 12px;
70
+ padding: 6px 12px;
71
+ }
72
+ .btn--lg {
73
+ font-size: 15px;
74
+ padding: 10px 20px;
75
+ }
76
+
77
+ /* Icon button */
78
+ .btn--icon {
79
+ width: 32px;
80
+ height: 32px;
81
+ padding: 0;
82
+ border-radius: 8px;
83
+ }
84
+ .btn--icon.btn--xs {
85
+ width: 24px;
86
+ height: 24px;
87
+ }
88
+
89
+ /* Input/Select globals */
90
+ .input, .select,
91
+ input[type="text"], input[type="search"], input[type="email"], input[type="number"], input[type="date"],
92
+ select, textarea {
93
+ font-family: inherit;
94
+ font-size: 13px;
95
+ border: 1px solid #e2e8f0;
96
+ border-radius: var(--radius-md, 8px);
97
+ padding: 8px 12px;
98
+ background: var(--bg-input, #f5f5f5);
99
+ color: var(--text-primary, #1a1a1a);
100
+ transition: border-color 0.15s, box-shadow 0.15s;
101
+ outline: none;
102
+ }
103
+ input:focus, select:focus, textarea:focus {
104
+ border-color: var(--primary);
105
+ box-shadow: 0 0 0 3px var(--primary-light, rgba(59,130,246,0.12));
106
+ background: var(--bg-card, #fff);
107
+ }
108
+ input::placeholder, textarea::placeholder {
109
+ color: var(--text-muted, #9ca3af);
110
+ }
111
+
112
+ /* Badge */
113
+ .badge {
114
+ display: inline-flex;
115
+ align-items: center;
116
+ padding: 2px 8px;
117
+ border-radius: 10px;
118
+ font-size: 11px;
119
+ font-weight: 600;
120
+ }
121
+ .badge--success { background: #dcfce7; color: #16a34a; }
122
+ .badge--warning { background: #fef3c7; color: #d97706; }
123
+ .badge--danger { background: #fee2e2; color: #dc2626; }
124
+ .badge--info { background: #dbeafe; color: #1d4ed8; }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @pageName mentions → clickable links
3
+ * XSS-safe: text is escaped first, then only known page names are linked.
4
+ *
5
+ * Page mentions are empty by default — register your project's pages
6
+ * by editing the PAGE_MENTIONS array below.
7
+ */
8
+
9
+ const PAGE_MENTIONS: { label: string; path: string }[] = [
10
+ // TODO: Add your project's page mentions
11
+ // { label: 'Home', path: '/home' },
12
+ // { label: 'Board', path: '/board' },
13
+ // { label: 'Standup', path: '/standup' },
14
+ // { label: 'Retro', path: '/retro' },
15
+ ]
16
+
17
+ // Match longest labels first
18
+ const SORTED_LABELS = [...PAGE_MENTIONS].sort((a, b) => b.label.length - a.label.length)
19
+
20
+ function escapeHtml(text: string): string {
21
+ return text
22
+ .replace(/&/g, '&amp;')
23
+ .replace(/</g, '&lt;')
24
+ .replace(/>/g, '&gt;')
25
+ .replace(/"/g, '&quot;')
26
+ }
27
+
28
+ /**
29
+ * Convert @pageName and @person mentions to clickable HTML.
30
+ * Returns an HTML string (use with v-html).
31
+ */
32
+ export function parseMentions(text: string): string {
33
+ let html = escapeHtml(text)
34
+
35
+ for (const { label, path } of SORTED_LABELS) {
36
+ const escaped = escapeHtml(label)
37
+ const regex = new RegExp(`@${escaped}`, 'g')
38
+ html = html.replace(
39
+ regex,
40
+ `<a href="${path}" class="memo-mention" data-mention-page="${path}">@${escaped}</a>`,
41
+ )
42
+ }
43
+
44
+ // Person mentions — unmatched @name rendered as blue chip
45
+ html = html.replace(/@([^@\s&lt;][^@\n]*?)(?=\s|$|&lt;|@)/g, (match, name) => {
46
+ if (match.includes('class="memo-mention"')) return match
47
+ return `<span class="mention-chip">@${name}</span>`
48
+ })
49
+
50
+ return html
51
+ }
52
+
53
+ /** Check if text contains any page mentions */
54
+ export function hasMentions(text: string): boolean {
55
+ return SORTED_LABELS.some(({ label }) => text.includes(`@${label}`))
56
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Timezone-aware date/time helpers.
3
+ * Default timezone can be configured via VITE_TIMEZONE env var.
4
+ */
5
+
6
+ const DEFAULT_TZ = (import.meta.env.VITE_TIMEZONE as string) || Intl.DateTimeFormat().resolvedOptions().timeZone
7
+
8
+ /** YYYY-MM-DD in the configured timezone */
9
+ export function toDateString(date: Date = new Date(), tz: string = DEFAULT_TZ): string {
10
+ return date.toLocaleDateString('en-CA', { timeZone: tz })
11
+ }
12
+
13
+ /** YYYY-MM-DD HH:mm in the configured timezone */
14
+ export function toDateTimeString(date: Date = new Date(), tz: string = DEFAULT_TZ): string {
15
+ const d = date.toLocaleDateString('en-CA', { timeZone: tz })
16
+ const t = date.toLocaleTimeString('en-GB', { timeZone: tz, hour: '2-digit', minute: '2-digit', hour12: false })
17
+ return `${d} ${t}`
18
+ }