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.
- 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-notification-server/package.json +18 -0
- package/scaffold/mcp-notification-server/src/index.ts +275 -0
- package/scaffold/mcp-notification-server/src/turso-client.ts +142 -0
- package/scaffold/mcp-notification-server/tsconfig.json +14 -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/001-memo-v2.sql +49 -0
- package/scaffold/pm-api/sql/002-notifications.sql +18 -0
- package/scaffold/pm-api/sql/003-content.sql +66 -0
- package/scaffold/pm-api/sql/004-agent-events.sql +21 -0
- package/scaffold/pm-api/sql/005-epic-sprint-decoupling.sql +6 -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/retro-link.ts +32 -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 +892 -0
- package/scaffold/spec-site/package.json +15 -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/AuthGate.vue +117 -0
- package/scaffold/spec-site/src/components/BurndownChart.vue +78 -0
- package/scaffold/spec-site/src/components/DocComments.vue +137 -0
- package/scaffold/spec-site/src/components/DocEditor.vue +118 -0
- package/scaffold/spec-site/src/components/DocExportBar.vue +110 -0
- package/scaffold/spec-site/src/components/DocsSidebar.vue +309 -0
- package/scaffold/spec-site/src/components/EmptyState.vue +30 -0
- package/scaffold/spec-site/src/components/ErrorBanner.vue +38 -0
- package/scaffold/spec-site/src/components/Icon.vue +58 -0
- package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
- package/scaffold/spec-site/src/components/MemoChecklist.vue +88 -0
- package/scaffold/spec-site/src/components/MemoGraph.vue +75 -0
- package/scaffold/spec-site/src/components/MemoItem.vue +353 -0
- package/scaffold/spec-site/src/components/MemoRelations.vue +101 -0
- package/scaffold/spec-site/src/components/MemoTimeline.vue +53 -0
- package/scaffold/spec-site/src/components/MentionInput.vue +174 -0
- package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
- package/scaffold/spec-site/src/components/PriorityBadge.vue +23 -0
- package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
- package/scaffold/spec-site/src/components/SlashCommand.ts +123 -0
- package/scaffold/spec-site/src/components/StateDisplay.vue +54 -0
- package/scaffold/spec-site/src/components/TreeNode.vue +82 -0
- package/scaffold/spec-site/src/components/UserAvatar.vue +24 -0
- package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
- package/scaffold/spec-site/src/composables/navTypes.ts +3 -0
- package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
- package/scaffold/spec-site/src/composables/useBottomSheet.ts +103 -0
- 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/useMemo.ts +39 -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/useTurso.ts +17 -0
- package/scaffold/spec-site/src/composables/useUser.ts +19 -1
- package/scaffold/spec-site/src/composables/useViewport.ts +26 -0
- package/scaffold/spec-site/src/features.ts +108 -0
- package/scaffold/spec-site/src/mockup/ComponentPalette.vue +61 -0
- package/scaffold/spec-site/src/mockup/MockupCanvas.vue +459 -0
- package/scaffold/spec-site/src/mockup/PropertyPanel.vue +217 -0
- package/scaffold/spec-site/src/mockup/componentCatalog.ts +68 -0
- package/scaffold/spec-site/src/mockup/useScenarios.ts +67 -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/DocsEditor.vue +119 -0
- package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
- package/scaffold/spec-site/src/pages/DocsPage.vue +444 -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/MemosPage.vue +857 -0
- package/scaffold/spec-site/src/pages/MockupEditorPage.vue +611 -0
- package/scaffold/spec-site/src/pages/MockupListPage.vue +121 -0
- package/scaffold/spec-site/src/pages/MockupViewerPage.vue +199 -0
- package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
- package/scaffold/spec-site/src/pages/NotificationSettingsPage.vue +59 -0
- package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
- package/scaffold/spec-site/src/pages/SprintAdmin.vue +521 -0
- package/scaffold/spec-site/src/pages/SprintTimeline.vue +159 -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/KanbanBoard.vue +93 -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
- package/scaffold/spec-site/src/styles/buttons.css +124 -0
- package/scaffold/spec-site/src/utils/parseMentions.ts +56 -0
- 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 {
|
|
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>
|