popilot 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +204 -2
- package/lib/doctor.mjs +38 -1
- package/lib/hydrate.mjs +15 -0
- package/lib/scaffold.mjs +5 -0
- package/lib/setup-wizard.mjs +35 -2
- package/package.json +1 -1
- package/scaffold/.context/project.yaml.example +19 -0
- package/scaffold/mcp-pm/package.json +19 -0
- package/scaffold/mcp-pm/src/api-client.ts +69 -0
- package/scaffold/mcp-pm/src/index.ts +660 -0
- package/scaffold/mcp-pm/tsconfig.json +14 -0
- package/scaffold/pm-api/package.json +21 -0
- package/scaffold/pm-api/sql/schema-core.sql +331 -0
- package/scaffold/pm-api/sql/schema-docs.sql +25 -0
- package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
- package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
- package/scaffold/pm-api/src/auth.ts +28 -0
- package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
- package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
- package/scaffold/pm-api/src/db/adapter.ts +36 -0
- package/scaffold/pm-api/src/db/turso.ts +147 -0
- package/scaffold/pm-api/src/index.ts +114 -0
- package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
- package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
- package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
- package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
- package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
- package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
- package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
- package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
- package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
- package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
- package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
- package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
- package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
- package/scaffold/pm-api/src/mcp.ts +871 -0
- package/scaffold/pm-api/src/nudge.ts +283 -0
- package/scaffold/pm-api/src/routes/auth.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
- package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
- package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
- package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
- package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
- package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
- package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
- package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
- package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
- package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
- package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
- package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
- package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
- package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
- package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
- package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
- package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
- package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
- package/scaffold/pm-api/src/types.ts +11 -0
- package/scaffold/pm-api/src/utils/activity.ts +22 -0
- package/scaffold/pm-api/src/utils/admin.ts +9 -0
- package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
- package/scaffold/pm-api/src/utils/assignee.ts +69 -0
- package/scaffold/pm-api/src/utils/db.ts +45 -0
- package/scaffold/pm-api/src/utils/initiative.ts +23 -0
- package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
- package/scaffold/pm-api/tsconfig.json +15 -0
- package/scaffold/pm-api/wrangler.toml.hbs +11 -0
- package/scaffold/spec-site/package-lock.json +40 -0
- package/scaffold/spec-site/package.json +4 -1
- package/scaffold/spec-site/src/api/types.ts +6 -0
- package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
- package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
- package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
- package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
- package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
- package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
- package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
- package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
- package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
- package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
- package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
- package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
- package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
- package/scaffold/spec-site/src/composables/useUser.ts +19 -1
- package/scaffold/spec-site/src/features.ts +108 -0
- package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
- package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
- package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
- package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
- package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
- package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
- package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
- package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
- package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
- package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
- package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
- package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
- package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
- package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
- package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
- package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
- package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
- package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
- package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
- package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
- package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
- package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
- package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
- package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
- package/scaffold/spec-site/src/router.ts +141 -0
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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 (
|
|
31
|
-
router.push(`/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(() =>
|
|
52
|
-
|
|
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
|
-
<!--
|
|
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 ▾
|
|
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="
|
|
69
|
-
:key="
|
|
70
|
-
:to="
|
|
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:
|
|
210
|
+
:class="{ active: route.path === item.path || route.path.startsWith(item.path + '/') }"
|
|
73
211
|
>
|
|
74
|
-
{{
|
|
212
|
+
{{ item.label }}
|
|
75
213
|
</router-link>
|
|
76
214
|
</nav>
|
|
77
215
|
</div>
|
|
78
216
|
|
|
79
217
|
<div class="header-right">
|
|
80
|
-
<!--
|
|
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
|
-
<!--
|
|
102
|
-
<div class="
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
class="
|
|
106
|
-
|
|
107
|
-
>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
:
|
|
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
|
-
|
|
315
|
+
{{ item.label }}
|
|
117
316
|
</router-link>
|
|
118
|
-
|
|
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
|
-
</
|
|
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:
|
|
127
|
-
|
|
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:
|
|
239
|
-
background:
|
|
240
|
-
|
|
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
|
-
/* ----
|
|
278
|
-
.
|
|
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:
|
|
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
|
-
.
|
|
285
|
-
|
|
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:
|
|
653
|
+
border-radius: 8px;
|
|
290
654
|
text-decoration: none;
|
|
291
655
|
transition: all 0.15s;
|
|
656
|
+
cursor: pointer;
|
|
292
657
|
}
|
|
293
|
-
.
|
|
294
|
-
.
|
|
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
|
-
.
|
|
301
|
-
|
|
302
|
-
font-size:
|
|
303
|
-
|
|
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>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted } from 'vue'
|
|
3
|
+
import { apiGet } from '@/api/client'
|
|
4
|
+
|
|
5
|
+
interface MemberItem { id: number; display_name: string }
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
modelValue: string[]
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<{
|
|
12
|
+
'update:modelValue': [value: string[]]
|
|
13
|
+
}>()
|
|
14
|
+
|
|
15
|
+
const members = ref<MemberItem[]>([])
|
|
16
|
+
|
|
17
|
+
function toggle(name: string) {
|
|
18
|
+
const current = new Set(props.modelValue)
|
|
19
|
+
if (current.has(name)) current.delete(name)
|
|
20
|
+
else current.add(name)
|
|
21
|
+
emit('update:modelValue', [...current])
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
onMounted(async () => {
|
|
25
|
+
const { data } = await apiGet<{ members: MemberItem[] }>('/api/v2/admin/members')
|
|
26
|
+
if (data?.members) members.value = data.members.filter(m => (m as any).is_active)
|
|
27
|
+
})
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div class="member-select">
|
|
32
|
+
<label v-for="m in members" :key="m.id"
|
|
33
|
+
class="member-tag" :class="{ selected: modelValue.includes(m.display_name) }"
|
|
34
|
+
@click="toggle(m.display_name)">
|
|
35
|
+
{{ m.display_name }}
|
|
36
|
+
</label>
|
|
37
|
+
</div>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<style scoped>
|
|
41
|
+
.member-select { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
42
|
+
.member-tag {
|
|
43
|
+
padding: 4px 10px; border-radius: 6px; font-size: 12px; cursor: pointer;
|
|
44
|
+
border: 1px solid rgba(0,0,0,0.08); background: rgba(0,0,0,0.02);
|
|
45
|
+
transition: all 0.15s; user-select: none;
|
|
46
|
+
}
|
|
47
|
+
.member-tag.selected { background: var(--primary); color: #fff; border-color: var(--primary); }
|
|
48
|
+
</style>
|