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
@@ -9,7 +9,21 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "vue": "^3.5.27",
12
- "vue-router": "^4.5.0"
12
+ "vue-router": "^4.5.0",
13
+ "dompurify": "^3.2.0",
14
+ "marked": "^15.0.0",
15
+ "lucide-vue-next": "^0.470.0",
16
+ "@tiptap/vue-3": "^2.11.0",
17
+ "@tiptap/starter-kit": "^2.11.0",
18
+ "@tiptap/extension-link": "^2.11.0",
19
+ "@tiptap/extension-image": "^2.11.0",
20
+ "@tiptap/extension-table": "^2.11.0",
21
+ "@tiptap/extension-table-row": "^2.11.0",
22
+ "@tiptap/extension-table-cell": "^2.11.0",
23
+ "@tiptap/extension-table-header": "^2.11.0",
24
+ "@tiptap/extension-placeholder": "^2.11.0",
25
+ "@tiptap/core": "^2.11.0",
26
+ "@tiptap/suggestion": "^2.11.0"
13
27
  },
14
28
  "devDependencies": {
15
29
  "@vitejs/plugin-vue": "^5.2.3",
@@ -122,6 +122,10 @@ export interface PmStoryRow {
122
122
  priority: string
123
123
  area: string
124
124
  story_points: number | null
125
+ start_date: string | null
126
+ due_date: string | null
127
+ figma_url: string | null
128
+ related_prs: string | null
125
129
  sort_order: number
126
130
  created_at: string
127
131
  updated_at: string
@@ -134,6 +138,8 @@ export interface PmTaskRow {
134
138
  assignee: string | null
135
139
  status: string
136
140
  description: string | null
141
+ story_points: number | null
142
+ due_date: string | null
137
143
  sort_order: number
138
144
  created_at: string
139
145
  updated_at: string
@@ -1,13 +1,28 @@
1
1
  <script setup lang="ts">
2
- import { ref, computed, onMounted, onUnmounted } from 'vue'
2
+ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
3
3
  import { useRoute, useRouter } from 'vue-router'
4
4
  import { sprints, getActiveSprint, type SprintConfig } from '../composables/useNavStore'
5
- import { featurePages } from '../data/navigation'
5
+ import { getNavItems, isFeatureEnabled } from '@/features'
6
+ import { useAuth } from '@/composables/useAuth'
7
+ import { useTheme } from '@/composables/useTheme'
8
+ import { useMediaQuery } from '@/composables/useMediaQuery'
9
+ import { useNotification, type NotificationItem, shouldOpenMemoSidebar, pendingNotificationPageId } from '@/composables/useNotification'
10
+ import NotificationDropdown from './NotificationDropdown.vue'
11
+ import SearchModal from './SearchModal.vue'
6
12
 
7
13
  const route = useRoute()
8
14
  const router = useRouter()
15
+ const { isAuthenticated, authUser, logout } = useAuth()
16
+ const { theme, toggle: toggleTheme } = useTheme()
17
+ const isMobile = useMediaQuery('(max-width: 767px)')
9
18
 
10
- const currentPageId = computed(() => (route.params.pageId as string) || '')
19
+ const {
20
+ notifications, unreadCount,
21
+ markAsRead, markAllAsRead,
22
+ startPolling, stopPolling,
23
+ } = useNotification()
24
+
25
+ const navItems = computed(() => getNavItems())
11
26
  const currentSprint = computed(() => (route.params.sprint as string) || getActiveSprint().id)
12
27
 
13
28
  const activeSprintLabel = computed(() => {
@@ -15,11 +30,20 @@ const activeSprintLabel = computed(() => {
15
30
  return s?.label ?? currentSprint.value.toUpperCase()
16
31
  })
17
32
 
18
- const isPolicyPage = computed(() => route.path.startsWith('/policy'))
33
+ // Sprint-level page detection
34
+ const isBoardPage = computed(() => route.path.startsWith('/board') && route.path !== '/board/backlog')
35
+ const isBacklogPage = computed(() => route.path === '/board/backlog')
36
+ const isStandupPage = computed(() => route.path.startsWith('/standup'))
19
37
  const isRetroPage = computed(() => route.path.startsWith('/retro'))
38
+ const isMyTasksPage = computed(() => route.path.startsWith('/my-tasks'))
20
39
 
21
40
  // Dropdown state
22
41
  const sprintOpen = ref(false)
42
+ const sprintMenuOpen = ref(false)
43
+ const mobileMenuOpen = ref(false)
44
+ const userMenuOpen = ref(false)
45
+ const notifOpen = ref(false)
46
+ const searchVisible = ref(false)
23
47
 
24
48
  function toggleSprint() {
25
49
  sprintOpen.value = !sprintOpen.value
@@ -27,57 +51,208 @@ function toggleSprint() {
27
51
 
28
52
  function selectSprint(s: SprintConfig) {
29
53
  sprintOpen.value = false
30
- if (isPolicyPage.value) {
31
- router.push(`/policy/${s.id}`)
54
+ if (isBoardPage.value) {
55
+ router.push(`/board/${s.id}`)
56
+ } else if (isStandupPage.value) {
57
+ router.push(`/standup/${s.id}`)
32
58
  } else if (isRetroPage.value) {
33
59
  router.push(`/retro/${s.id}`)
60
+ } else if (isMyTasksPage.value) {
61
+ router.push(`/my-tasks/${s.id}`)
34
62
  } else {
35
- router.push(`/${currentPageId.value}/${s.id}`)
63
+ const basePath = route.path.replace(/\/[^/]+$/, '')
64
+ router.push(`${basePath}/${s.id}`)
36
65
  }
37
66
  }
38
67
 
39
68
  function goHome() {
40
69
  router.push('/')
70
+ mobileMenuOpen.value = false
71
+ }
72
+
73
+ function toggleMobileMenu() {
74
+ mobileMenuOpen.value = !mobileMenuOpen.value
75
+ }
76
+
77
+ function toggleUserMenu() {
78
+ userMenuOpen.value = !userMenuOpen.value
79
+ }
80
+
81
+ function openSearch() {
82
+ searchVisible.value = true
83
+ }
84
+
85
+ function handleLogout() {
86
+ userMenuOpen.value = false
87
+ logout()
88
+ router.push('/')
89
+ }
90
+
91
+ function navigateTo(path: string) {
92
+ mobileMenuOpen.value = false
93
+ sprintMenuOpen.value = false
94
+ router.push(path)
95
+ }
96
+
97
+ function handleNotifToggle() {
98
+ notifOpen.value = !notifOpen.value
99
+ }
100
+
101
+ function handleNotifClick(n: NotificationItem) {
102
+ markAsRead(n.id)
103
+ notifOpen.value = false
104
+ if (n.sourceType === 'memo' && n.pageId) {
105
+ pendingNotificationPageId.value = n.pageId
106
+ shouldOpenMemoSidebar.value = true
107
+ router.push(`/${n.pageId}`)
108
+ } else {
109
+ router.push('/inbox')
110
+ }
41
111
  }
42
112
 
43
- // Close dropdown on outside click
113
+ function handleMarkAllRead() {
114
+ markAllAsRead()
115
+ }
116
+
117
+ const themeIcon = computed(() => {
118
+ if (theme.value === 'dark') return 'Dark'
119
+ if (theme.value === 'system') return 'Auto'
120
+ return 'Light'
121
+ })
122
+
123
+ // Ctrl+K shortcut
124
+ function onGlobalKeydown(e: KeyboardEvent) {
125
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
126
+ e.preventDefault()
127
+ searchVisible.value = !searchVisible.value
128
+ }
129
+ }
130
+
131
+ // Close dropdowns on outside click
44
132
  function onDocClick(e: MouseEvent) {
45
133
  const target = e.target as HTMLElement
46
- if (!target.closest('.dropdown')) {
47
- sprintOpen.value = false
48
- }
134
+ if (!target.closest('.dropdown')) sprintOpen.value = false
135
+ if (!target.closest('.user-menu')) userMenuOpen.value = false
136
+ if (!target.closest('.notification-bell')) notifOpen.value = false
137
+ if (!target.closest('.sprint-dropdown')) sprintMenuOpen.value = false
49
138
  }
50
139
 
51
- onMounted(() => document.addEventListener('click', onDocClick))
52
- onUnmounted(() => document.removeEventListener('click', onDocClick))
140
+ onMounted(() => {
141
+ document.addEventListener('click', onDocClick)
142
+ document.addEventListener('keydown', onGlobalKeydown)
143
+ startPolling()
144
+ })
145
+ onUnmounted(() => {
146
+ document.removeEventListener('click', onDocClick)
147
+ document.removeEventListener('keydown', onGlobalKeydown)
148
+ stopPolling()
149
+ })
53
150
  </script>
54
151
 
55
152
  <template>
56
153
  <header class="app-header">
57
154
  <div class="header-left">
155
+ <!-- Mobile hamburger -->
156
+ <button v-if="isMobile" class="hamburger-btn" @click="toggleMobileMenu">
157
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
158
+ <template v-if="!mobileMenuOpen">
159
+ <line x1="3" y1="6" x2="21" y2="6" />
160
+ <line x1="3" y1="12" x2="21" y2="12" />
161
+ <line x1="3" y1="18" x2="21" y2="18" />
162
+ </template>
163
+ <template v-else>
164
+ <line x1="18" y1="6" x2="6" y2="18" />
165
+ <line x1="6" y1="6" x2="18" y2="18" />
166
+ </template>
167
+ </svg>
168
+ </button>
169
+
58
170
  <!-- Logo -->
59
171
  <div class="header-logo" @click="goHome">
60
- <!-- TODO: Change logo text to your project name -->
61
172
  <span class="logo-mark">SPEC</span>
62
173
  <span class="logo-sub">SITE</span>
63
174
  </div>
64
175
 
65
- <!-- Feature page tabs -->
66
- <nav class="page-tabs">
176
+ <!-- Desktop navigation -->
177
+ <nav v-if="!isMobile" class="page-tabs">
178
+ <!-- Sprint dropdown -->
179
+ <div v-if="isFeatureEnabled('board')" class="sprint-dropdown">
180
+ <button
181
+ class="page-tab"
182
+ :class="{ active: route.path === '/' || isBoardPage || isBacklogPage || isStandupPage || isRetroPage || isMyTasksPage }"
183
+ @click.stop="sprintMenuOpen = !sprintMenuOpen"
184
+ >
185
+ Sprint &#9662;
186
+ </button>
187
+ <div v-if="sprintMenuOpen" class="sprint-dropdown-menu">
188
+ <div class="dropdown-item" @click="navigateTo('/')">Dashboard</div>
189
+ <div class="dropdown-item" @click="navigateTo(`/board/${currentSprint}`)">Board</div>
190
+ <div class="dropdown-item" @click="navigateTo(`/standup/${currentSprint}`)">Standup</div>
191
+ <div class="dropdown-item" @click="navigateTo(`/retro/${currentSprint}`)">Retro</div>
192
+ <div class="menu-divider" />
193
+ <div class="dropdown-item" @click="navigateTo(`/board/${currentSprint}?view=timeline`)">Timeline</div>
194
+ <div class="dropdown-item" @click="navigateTo(`/board/${currentSprint}?view=roadmap`)">Roadmap</div>
195
+ <div class="dropdown-item" @click="navigateTo('/board/backlog')">Backlog</div>
196
+ <div class="dropdown-item" @click="navigateTo(`/my-tasks/${currentSprint}`)">My Tasks</div>
197
+ <div class="menu-divider" />
198
+ <div class="dropdown-item" @click="navigateTo('/kickoff/new')">New Sprint Kickoff</div>
199
+ <div class="dropdown-item" @click="navigateTo(`/close/${currentSprint}`)">Close Sprint</div>
200
+ <div class="dropdown-item" @click="navigateTo('/admin/board')">Board Admin</div>
201
+ </div>
202
+ </div>
203
+
204
+ <!-- Other nav items (excluding board/standup/retro which are in sprint dropdown) -->
67
205
  <router-link
68
- v-for="fp in featurePages"
69
- :key="fp.id"
70
- :to="`/${fp.id}/${currentSprint}`"
206
+ v-for="item in navItems.filter(n => !['board', 'standup', 'retro', 'dashboard'].includes(n.id))"
207
+ :key="item.id"
208
+ :to="item.path"
71
209
  class="page-tab"
72
- :class="{ active: !isPolicyPage && !isRetroPage && currentPageId === fp.id }"
210
+ :class="{ active: route.path === item.path || route.path.startsWith(item.path + '/') }"
73
211
  >
74
- {{ fp.label }}
212
+ {{ item.label }}
75
213
  </router-link>
76
214
  </nav>
77
215
  </div>
78
216
 
79
217
  <div class="header-right">
80
- <!-- Sprint version selector -->
218
+ <!-- Search trigger -->
219
+ <button class="icon-btn" @click="openSearch" title="Search (Ctrl+K)">
220
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
221
+ <circle cx="11" cy="11" r="8" />
222
+ <path d="m21 21-4.35-4.35" />
223
+ </svg>
224
+ <kbd v-if="!isMobile" class="kbd-hint">Ctrl+K</kbd>
225
+ </button>
226
+
227
+ <!-- Theme toggle -->
228
+ <button class="icon-btn" @click="toggleTheme" :title="`Theme: ${themeIcon}`">
229
+ <svg v-if="theme === 'light'" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
230
+ <circle cx="12" cy="12" r="5" />
231
+ <line x1="12" y1="1" x2="12" y2="3" /><line x1="12" y1="21" x2="12" y2="23" />
232
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" /><line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
233
+ <line x1="1" y1="12" x2="3" y2="12" /><line x1="21" y1="12" x2="23" y2="12" />
234
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" /><line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
235
+ </svg>
236
+ <svg v-else-if="theme === 'dark'" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
237
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
238
+ </svg>
239
+ <svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
240
+ <rect x="2" y="3" width="20" height="14" rx="2" ry="2" /><line x1="8" y1="21" x2="16" y2="21" /><line x1="12" y1="17" x2="12" y2="21" />
241
+ </svg>
242
+ </button>
243
+
244
+ <!-- Notifications -->
245
+ <div class="notif-wrapper" :class="{ open: notifOpen }">
246
+ <NotificationDropdown
247
+ :notifications="notifications"
248
+ :unread-count="unreadCount"
249
+ @toggle="handleNotifToggle"
250
+ @click="handleNotifClick"
251
+ @mark-all-read="handleMarkAllRead"
252
+ />
253
+ </div>
254
+
255
+ <!-- Sprint selector -->
81
256
  <div class="dropdown" :class="{ open: sprintOpen }">
82
257
  <button class="dropdown-trigger" @click.stop="toggleSprint">
83
258
  {{ activeSprintLabel }}
@@ -98,33 +273,67 @@ onUnmounted(() => document.removeEventListener('click', onDocClick))
98
273
  </div>
99
274
  </div>
100
275
 
101
- <!-- Sprint-level links -->
102
- <div class="sprint-links">
103
- <router-link
104
- :to="`/policy/${currentSprint}`"
105
- class="sprint-link"
106
- :class="{ active: isPolicyPage }"
107
- >
108
- Policy
109
- </router-link>
110
- <span class="sprint-link-sep">|</span>
276
+ <!-- User menu -->
277
+ <div v-if="isAuthenticated" class="user-menu" :class="{ open: userMenuOpen }">
278
+ <button class="user-btn" @click.stop="toggleUserMenu">
279
+ <span class="user-avatar">{{ (authUser || '?').charAt(0).toUpperCase() }}</span>
280
+ <span v-if="!isMobile" class="user-name">{{ authUser }}</span>
281
+ </button>
282
+ <div v-if="userMenuOpen" class="dropdown-menu user-dropdown">
283
+ <div class="dropdown-item" @click="navigateTo('/my')">My Page</div>
284
+ <div class="dropdown-item" @click="handleLogout">Log out</div>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ </header>
289
+
290
+ <!-- Mobile navigation drawer -->
291
+ <Teleport to="body">
292
+ <div v-if="isMobile && mobileMenuOpen" class="mobile-overlay" @click="mobileMenuOpen = false">
293
+ <nav class="mobile-drawer" @click.stop>
294
+ <!-- Sprint section -->
295
+ <div class="mobile-section-label">Sprint</div>
296
+ <div class="mobile-nav-item" @click="navigateTo('/')">Dashboard</div>
297
+ <div class="mobile-nav-item" @click="navigateTo(`/board/${currentSprint}`)">Board</div>
298
+ <div class="mobile-nav-item" @click="navigateTo(`/standup/${currentSprint}`)">Standup</div>
299
+ <div class="mobile-nav-item" @click="navigateTo(`/retro/${currentSprint}`)">Retro</div>
300
+ <div class="mobile-nav-item" @click="navigateTo(`/my-tasks/${currentSprint}`)">My Tasks</div>
301
+ <div class="mobile-nav-item" @click="navigateTo('/board/backlog')">Backlog</div>
302
+ <div class="mobile-nav-item" @click="navigateTo('/kickoff/new')">Sprint Kickoff</div>
303
+
304
+ <div class="mobile-divider" />
305
+
306
+ <!-- Other nav items -->
111
307
  <router-link
112
- :to="`/retro/${currentSprint}`"
113
- class="sprint-link"
114
- :class="{ active: isRetroPage }"
308
+ v-for="item in navItems.filter(n => !['board', 'standup', 'retro', 'dashboard'].includes(n.id))"
309
+ :key="item.id"
310
+ :to="item.path"
311
+ class="mobile-nav-item"
312
+ :class="{ active: route.path === item.path || route.path.startsWith(item.path + '/') }"
313
+ @click="mobileMenuOpen = false"
115
314
  >
116
- Retro
315
+ {{ item.label }}
117
316
  </router-link>
118
- </div>
317
+
318
+ <template v-if="isAuthenticated">
319
+ <div class="mobile-divider" />
320
+ <div class="mobile-nav-item" @click="navigateTo('/my')">My Page</div>
321
+ </template>
322
+ </nav>
119
323
  </div>
120
- </header>
324
+ </Teleport>
325
+
326
+ <!-- Search modal -->
327
+ <SearchModal :visible="searchVisible" @close="searchVisible = false" />
121
328
  </template>
122
329
 
123
330
  <style scoped>
124
331
  .app-header {
125
332
  height: var(--header-height);
126
- background: #fff;
127
- border-bottom: 1px solid var(--border);
333
+ background: rgba(255, 255, 255, 0.55);
334
+ backdrop-filter: blur(24px);
335
+ -webkit-backdrop-filter: blur(24px);
336
+ border-bottom: 1px solid rgba(255, 255, 255, 0.60);
128
337
  display: flex;
129
338
  align-items: center;
130
339
  justify-content: space-between;
@@ -132,6 +341,8 @@ onUnmounted(() => document.removeEventListener('click', onDocClick))
132
341
  flex-shrink: 0;
133
342
  gap: 8px;
134
343
  z-index: 500;
344
+ position: sticky;
345
+ top: 0;
135
346
  }
136
347
 
137
348
  /* ---- Left section ---- */
@@ -141,6 +352,20 @@ onUnmounted(() => document.removeEventListener('click', onDocClick))
141
352
  gap: 4px;
142
353
  }
143
354
 
355
+ .hamburger-btn {
356
+ display: flex;
357
+ align-items: center;
358
+ justify-content: center;
359
+ width: 36px;
360
+ height: 36px;
361
+ border: none;
362
+ background: none;
363
+ border-radius: 8px;
364
+ color: var(--text-secondary);
365
+ cursor: pointer;
366
+ }
367
+ .hamburger-btn:hover { background: var(--bg); }
368
+
144
369
  .header-logo {
145
370
  display: flex;
146
371
  align-items: baseline;
@@ -180,11 +405,14 @@ onUnmounted(() => document.removeEventListener('click', onDocClick))
180
405
  font-size: 13px;
181
406
  font-weight: 500;
182
407
  color: var(--text-secondary);
408
+ border: none;
183
409
  border-radius: 6px;
410
+ background: none;
184
411
  cursor: pointer;
185
412
  transition: all 0.15s;
186
413
  text-decoration: none;
187
414
  white-space: nowrap;
415
+ font-family: inherit;
188
416
  }
189
417
  .page-tab:hover { background: var(--bg); color: var(--text-primary); }
190
418
  .page-tab.active {
@@ -193,6 +421,42 @@ onUnmounted(() => document.removeEventListener('click', onDocClick))
193
421
  font-weight: 600;
194
422
  }
195
423
 
424
+ /* ---- Sprint dropdown ---- */
425
+ .sprint-dropdown { position: relative; }
426
+ .sprint-dropdown-menu {
427
+ position: absolute;
428
+ top: 100%;
429
+ left: 0;
430
+ z-index: 100;
431
+ background: rgba(255,255,255,0.75);
432
+ backdrop-filter: blur(40px) saturate(1.8);
433
+ -webkit-backdrop-filter: blur(40px) saturate(1.8);
434
+ border: 1px solid rgba(255,255,255,0.45);
435
+ border-radius: 8px;
436
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
437
+ min-width: 180px;
438
+ padding: 4px;
439
+ margin-top: 4px;
440
+ }
441
+ .sprint-dropdown-menu .dropdown-item {
442
+ display: block;
443
+ padding: 6px 12px;
444
+ font-size: 13px;
445
+ color: var(--text-secondary);
446
+ text-decoration: none;
447
+ border-radius: 4px;
448
+ cursor: pointer;
449
+ }
450
+ .sprint-dropdown-menu .dropdown-item:hover {
451
+ background: var(--bg);
452
+ color: var(--text-primary);
453
+ }
454
+ .menu-divider {
455
+ height: 1px;
456
+ background: rgba(0,0,0,0.06);
457
+ margin: 4px 8px;
458
+ }
459
+
196
460
  /* ---- Right section ---- */
197
461
  .header-right {
198
462
  display: flex;
@@ -200,6 +464,37 @@ onUnmounted(() => document.removeEventListener('click', onDocClick))
200
464
  gap: 4px;
201
465
  }
202
466
 
467
+ /* ---- Icon buttons ---- */
468
+ .icon-btn {
469
+ display: flex;
470
+ align-items: center;
471
+ gap: 4px;
472
+ padding: 6px 8px;
473
+ border: none;
474
+ background: none;
475
+ border-radius: 6px;
476
+ color: var(--text-secondary);
477
+ cursor: pointer;
478
+ font-family: var(--font-sans);
479
+ transition: all 0.15s;
480
+ }
481
+ .icon-btn:hover { background: var(--bg); color: var(--text-primary); }
482
+
483
+ .kbd-hint {
484
+ font-size: 10px;
485
+ padding: 1px 4px;
486
+ border-radius: 3px;
487
+ background: var(--bg);
488
+ border: 1px solid var(--border);
489
+ color: var(--text-muted);
490
+ font-family: var(--font-sans);
491
+ }
492
+
493
+ /* ---- Notification wrapper ---- */
494
+ .notif-wrapper { position: relative; }
495
+ .notif-wrapper :deep(.notif-dropdown) { display: none; }
496
+ .notif-wrapper.open :deep(.notif-dropdown) { display: flex; }
497
+
203
498
  /* ---- Dropdown ---- */
204
499
  .dropdown { position: relative; }
205
500
 
@@ -235,9 +530,11 @@ onUnmounted(() => document.removeEventListener('click', onDocClick))
235
530
  position: absolute;
236
531
  top: calc(100% + 4px);
237
532
  right: 0;
238
- min-width: 240px;
239
- background: #fff;
240
- border: 1px solid var(--border);
533
+ min-width: 200px;
534
+ background: rgba(255,255,255,0.75);
535
+ backdrop-filter: blur(40px) saturate(1.8);
536
+ -webkit-backdrop-filter: blur(40px) saturate(1.8);
537
+ border: 1px solid rgba(255,255,255,0.45);
241
538
  border-radius: 8px;
242
539
  box-shadow: var(--shadow-md);
243
540
  padding: 4px;
@@ -274,32 +571,109 @@ onUnmounted(() => document.removeEventListener('click', onDocClick))
274
571
  flex-shrink: 0;
275
572
  }
276
573
 
277
- /* ---- Sprint-level links ---- */
278
- .sprint-links {
574
+ /* ---- User menu ---- */
575
+ .user-menu {
576
+ position: relative;
577
+ }
578
+
579
+ .user-btn {
279
580
  display: flex;
280
581
  align-items: center;
281
- gap: 2px;
582
+ gap: 6px;
583
+ padding: 4px 8px;
584
+ border: none;
585
+ background: none;
586
+ border-radius: 6px;
587
+ cursor: pointer;
588
+ transition: background 0.15s;
589
+ font-family: var(--font-sans);
282
590
  }
591
+ .user-btn:hover { background: var(--bg); }
283
592
 
284
- .sprint-link {
285
- padding: 6px 10px;
593
+ .user-avatar {
594
+ width: 28px;
595
+ height: 28px;
596
+ border-radius: 50%;
597
+ background: var(--primary-light, #e0e7ff);
598
+ color: var(--primary);
599
+ font-size: 12px;
600
+ font-weight: 700;
601
+ display: flex;
602
+ align-items: center;
603
+ justify-content: center;
604
+ flex-shrink: 0;
605
+ }
606
+
607
+ .user-name {
286
608
  font-size: 13px;
287
609
  font-weight: 500;
610
+ color: var(--text-primary);
611
+ max-width: 100px;
612
+ white-space: nowrap;
613
+ overflow: hidden;
614
+ text-overflow: ellipsis;
615
+ }
616
+
617
+ .user-dropdown {
618
+ min-width: 140px;
619
+ }
620
+
621
+ /* ---- Mobile drawer ---- */
622
+ .mobile-overlay {
623
+ position: fixed;
624
+ inset: 0;
625
+ top: var(--header-height);
626
+ background: rgba(0, 0, 0, 0.3);
627
+ z-index: 999;
628
+ }
629
+
630
+ .mobile-drawer {
631
+ position: absolute;
632
+ top: 0;
633
+ left: 0;
634
+ width: 260px;
635
+ height: 100%;
636
+ background: rgba(255,255,255,0.85);
637
+ backdrop-filter: blur(40px) saturate(1.8);
638
+ -webkit-backdrop-filter: blur(40px) saturate(1.8);
639
+ box-shadow: 4px 0 16px rgba(0, 0, 0, 0.1);
640
+ padding: 8px;
641
+ display: flex;
642
+ flex-direction: column;
643
+ gap: 2px;
644
+ }
645
+
646
+ .mobile-nav-item {
647
+ display: flex;
648
+ align-items: center;
649
+ padding: 10px 14px;
650
+ font-size: 14px;
651
+ font-weight: 500;
288
652
  color: var(--text-secondary);
289
- border-radius: 6px;
653
+ border-radius: 8px;
290
654
  text-decoration: none;
291
655
  transition: all 0.15s;
656
+ cursor: pointer;
292
657
  }
293
- .sprint-link:hover { background: var(--bg); color: var(--text-primary); }
294
- .sprint-link.active {
658
+ .mobile-nav-item:hover { background: var(--bg); color: var(--text-primary); }
659
+ .mobile-nav-item.active {
295
660
  background: var(--primary-light);
296
661
  color: var(--primary);
297
662
  font-weight: 600;
298
663
  }
299
664
 
300
- .sprint-link-sep {
301
- color: var(--border);
302
- font-size: 12px;
303
- user-select: none;
665
+ .mobile-section-label {
666
+ padding: 8px 16px 4px;
667
+ font-size: 11px;
668
+ font-weight: 700;
669
+ color: var(--text-secondary);
670
+ text-transform: uppercase;
671
+ letter-spacing: 0.5px;
672
+ }
673
+
674
+ .mobile-divider {
675
+ height: 1px;
676
+ background: var(--border);
677
+ margin: 4px 12px;
304
678
  }
305
679
  </style>