popilot 0.7.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/package.json +1 -1
- 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/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/src/utils/retro-link.ts +32 -0
- package/scaffold/spec-site/package-lock.json +852 -0
- package/scaffold/spec-site/package.json +12 -1
- 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/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/PriorityBadge.vue +23 -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/composables/navTypes.ts +3 -0
- package/scaffold/spec-site/src/composables/useBottomSheet.ts +103 -0
- package/scaffold/spec-site/src/composables/useMemo.ts +39 -0
- package/scaffold/spec-site/src/composables/useTurso.ts +17 -0
- package/scaffold/spec-site/src/composables/useViewport.ts +26 -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/DocsEditor.vue +119 -0
- package/scaffold/spec-site/src/pages/DocsPage.vue +444 -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/NotificationSettingsPage.vue +59 -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/KanbanBoard.vue +93 -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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { apiPatch } from '@/composables/useTurso'
|
|
4
|
+
|
|
5
|
+
interface Story { id: number; title: string; status: string; priority: string; assignee: string; story_points: number; epic_id: number; epic_uid?: string }
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{ stories: Story[]; onUpdate?: () => void }>()
|
|
8
|
+
|
|
9
|
+
const COLUMNS = [
|
|
10
|
+
{ id: 'backlog', label: 'Backlog', color: '#9ca3af' },
|
|
11
|
+
{ id: 'ready-for-dev', label: 'Ready', color: '#3b82f6' },
|
|
12
|
+
{ id: 'in-progress', label: 'In Progress', color: '#f59e0b' },
|
|
13
|
+
{ id: 'review', label: 'Review', color: '#8b5cf6' },
|
|
14
|
+
{ id: 'done', label: 'Done', color: '#22c55e' },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
const grouped = computed(() => {
|
|
18
|
+
const groups: Record<string, Story[]> = {}
|
|
19
|
+
for (const col of COLUMNS) groups[col.id] = []
|
|
20
|
+
for (const s of props.stories) {
|
|
21
|
+
const col = COLUMNS.find(c => c.id === s.status) ? s.status : 'backlog'
|
|
22
|
+
groups[col].push(s)
|
|
23
|
+
}
|
|
24
|
+
return groups
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
let dragStoryId: number | null = null
|
|
28
|
+
|
|
29
|
+
function onDragStart(e: DragEvent, storyId: number) {
|
|
30
|
+
dragStoryId = storyId
|
|
31
|
+
e.dataTransfer?.setData('story-id', String(storyId))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function onDrop(e: DragEvent, newStatus: string) {
|
|
35
|
+
e.preventDefault()
|
|
36
|
+
const id = Number(e.dataTransfer?.getData('story-id') || dragStoryId)
|
|
37
|
+
if (!id) return
|
|
38
|
+
await apiPatch(`/api/v2/pm/stories/${id}`, { status: newStatus })
|
|
39
|
+
const story = props.stories.find(s => s.id === id)
|
|
40
|
+
if (story) story.status = newStatus
|
|
41
|
+
props.onUpdate?.()
|
|
42
|
+
dragStoryId = null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const priorityColors: Record<string, string> = { high: '#ef4444', medium: '#f59e0b', low: '#22c55e' }
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<div class="kanban-board">
|
|
50
|
+
<div v-for="col in COLUMNS" :key="col.id" class="kanban-column" @dragover.prevent @drop="onDrop($event, col.id)">
|
|
51
|
+
<div class="column-header" :style="{ borderTopColor: col.color }">
|
|
52
|
+
<span class="column-title">{{ col.label }}</span>
|
|
53
|
+
<span class="column-count">{{ grouped[col.id].length }}</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="column-body">
|
|
56
|
+
<div
|
|
57
|
+
v-for="story in grouped[col.id]" :key="story.id"
|
|
58
|
+
class="kanban-card"
|
|
59
|
+
draggable="true"
|
|
60
|
+
@dragstart="onDragStart($event, story.id)"
|
|
61
|
+
>
|
|
62
|
+
<div class="card-top">
|
|
63
|
+
<span class="card-id">SID:{{ story.id }}</span>
|
|
64
|
+
<span class="card-sp" v-if="story.story_points">{{ story.story_points }}SP</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="card-title">{{ story.title }}</div>
|
|
67
|
+
<div class="card-bottom">
|
|
68
|
+
<span class="card-priority" :style="{ color: priorityColors[story.priority] || '#9ca3af' }">{{ story.priority }}</span>
|
|
69
|
+
<span v-if="story.assignee" class="card-assignee">{{ story.assignee }}</span>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<style scoped>
|
|
78
|
+
.kanban-board { display: flex; gap: 12px; overflow-x: auto; padding: 8px 0; min-height: 400px; }
|
|
79
|
+
.kanban-column { flex: 1; min-width: 200px; background: var(--bg-card, #fff); border-radius: var(--radius-lg, 12px); display: flex; flex-direction: column; }
|
|
80
|
+
.column-header { padding: 12px 14px; border-top: 3px solid; display: flex; justify-content: space-between; align-items: center; border-radius: var(--radius-lg, 12px) var(--radius-lg, 12px) 0 0; }
|
|
81
|
+
.column-title { font-size: 13px; font-weight: 700; color: var(--text-primary); }
|
|
82
|
+
.column-count { font-size: 11px; background: var(--bg-hover); padding: 1px 6px; border-radius: 8px; color: var(--text-secondary); }
|
|
83
|
+
.column-body { padding: 8px; flex: 1; display: flex; flex-direction: column; gap: 6px; min-height: 100px; }
|
|
84
|
+
.kanban-card { background: var(--bg-main, #f5f6f8); border-radius: var(--radius-md, 8px); padding: 10px 12px; cursor: grab; transition: transform 0.1s; }
|
|
85
|
+
.kanban-card:active { cursor: grabbing; opacity: 0.7; }
|
|
86
|
+
.kanban-card:hover { transform: translateY(-1px); }
|
|
87
|
+
.card-top { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-muted); margin-bottom: 4px; }
|
|
88
|
+
.card-title { font-size: 13px; font-weight: 600; color: var(--text-primary); line-height: 1.4; }
|
|
89
|
+
.card-bottom { display: flex; justify-content: space-between; font-size: 11px; margin-top: 6px; }
|
|
90
|
+
.card-priority { font-weight: 600; }
|
|
91
|
+
.card-assignee { color: var(--text-secondary); }
|
|
92
|
+
.card-sp { font-weight: 600; }
|
|
93
|
+
</style>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/* Button design system */
|
|
2
|
+
|
|
3
|
+
.btn {
|
|
4
|
+
display: inline-flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
justify-content: center;
|
|
7
|
+
gap: 6px;
|
|
8
|
+
border: none;
|
|
9
|
+
border-radius: var(--radius-md, 8px);
|
|
10
|
+
font-size: 13px;
|
|
11
|
+
font-weight: 500;
|
|
12
|
+
cursor: pointer;
|
|
13
|
+
transition: all 0.15s ease;
|
|
14
|
+
padding: 8px 16px;
|
|
15
|
+
line-height: 1.4;
|
|
16
|
+
white-space: nowrap;
|
|
17
|
+
font-family: inherit;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.btn:disabled {
|
|
21
|
+
opacity: 0.5;
|
|
22
|
+
cursor: not-allowed;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Primary */
|
|
26
|
+
.btn--primary {
|
|
27
|
+
background: #3b82f6;
|
|
28
|
+
color: #fff;
|
|
29
|
+
}
|
|
30
|
+
.btn--primary:hover:not(:disabled) {
|
|
31
|
+
background: #2563eb;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Secondary */
|
|
35
|
+
.btn--secondary {
|
|
36
|
+
background: #f3f4f6;
|
|
37
|
+
color: #374151;
|
|
38
|
+
}
|
|
39
|
+
.btn--secondary:hover:not(:disabled) {
|
|
40
|
+
background: #e5e7eb;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Ghost */
|
|
44
|
+
.btn--ghost {
|
|
45
|
+
background: transparent;
|
|
46
|
+
color: #6b7280;
|
|
47
|
+
}
|
|
48
|
+
.btn--ghost:hover:not(:disabled) {
|
|
49
|
+
background: #f3f4f6;
|
|
50
|
+
color: #374151;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Danger */
|
|
54
|
+
.btn--danger {
|
|
55
|
+
background: #fef2f2;
|
|
56
|
+
color: #dc2626;
|
|
57
|
+
}
|
|
58
|
+
.btn--danger:hover:not(:disabled) {
|
|
59
|
+
background: #fee2e2;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Sizes */
|
|
63
|
+
.btn--xs {
|
|
64
|
+
font-size: 11px;
|
|
65
|
+
padding: 4px 8px;
|
|
66
|
+
border-radius: 6px;
|
|
67
|
+
}
|
|
68
|
+
.btn--sm {
|
|
69
|
+
font-size: 12px;
|
|
70
|
+
padding: 6px 12px;
|
|
71
|
+
}
|
|
72
|
+
.btn--lg {
|
|
73
|
+
font-size: 15px;
|
|
74
|
+
padding: 10px 20px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Icon button */
|
|
78
|
+
.btn--icon {
|
|
79
|
+
width: 32px;
|
|
80
|
+
height: 32px;
|
|
81
|
+
padding: 0;
|
|
82
|
+
border-radius: 8px;
|
|
83
|
+
}
|
|
84
|
+
.btn--icon.btn--xs {
|
|
85
|
+
width: 24px;
|
|
86
|
+
height: 24px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Input/Select globals */
|
|
90
|
+
.input, .select,
|
|
91
|
+
input[type="text"], input[type="search"], input[type="email"], input[type="number"], input[type="date"],
|
|
92
|
+
select, textarea {
|
|
93
|
+
font-family: inherit;
|
|
94
|
+
font-size: 13px;
|
|
95
|
+
border: 1px solid #e2e8f0;
|
|
96
|
+
border-radius: var(--radius-md, 8px);
|
|
97
|
+
padding: 8px 12px;
|
|
98
|
+
background: var(--bg-input, #f5f5f5);
|
|
99
|
+
color: var(--text-primary, #1a1a1a);
|
|
100
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
101
|
+
outline: none;
|
|
102
|
+
}
|
|
103
|
+
input:focus, select:focus, textarea:focus {
|
|
104
|
+
border-color: var(--primary);
|
|
105
|
+
box-shadow: 0 0 0 3px var(--primary-light, rgba(59,130,246,0.12));
|
|
106
|
+
background: var(--bg-card, #fff);
|
|
107
|
+
}
|
|
108
|
+
input::placeholder, textarea::placeholder {
|
|
109
|
+
color: var(--text-muted, #9ca3af);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Badge */
|
|
113
|
+
.badge {
|
|
114
|
+
display: inline-flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
padding: 2px 8px;
|
|
117
|
+
border-radius: 10px;
|
|
118
|
+
font-size: 11px;
|
|
119
|
+
font-weight: 600;
|
|
120
|
+
}
|
|
121
|
+
.badge--success { background: #dcfce7; color: #16a34a; }
|
|
122
|
+
.badge--warning { background: #fef3c7; color: #d97706; }
|
|
123
|
+
.badge--danger { background: #fee2e2; color: #dc2626; }
|
|
124
|
+
.badge--info { background: #dbeafe; color: #1d4ed8; }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pageName mentions → clickable links
|
|
3
|
+
* XSS-safe: text is escaped first, then only known page names are linked.
|
|
4
|
+
*
|
|
5
|
+
* Page mentions are empty by default — register your project's pages
|
|
6
|
+
* by editing the PAGE_MENTIONS array below.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const PAGE_MENTIONS: { label: string; path: string }[] = [
|
|
10
|
+
// TODO: Add your project's page mentions
|
|
11
|
+
// { label: 'Home', path: '/home' },
|
|
12
|
+
// { label: 'Board', path: '/board' },
|
|
13
|
+
// { label: 'Standup', path: '/standup' },
|
|
14
|
+
// { label: 'Retro', path: '/retro' },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
// Match longest labels first
|
|
18
|
+
const SORTED_LABELS = [...PAGE_MENTIONS].sort((a, b) => b.label.length - a.label.length)
|
|
19
|
+
|
|
20
|
+
function escapeHtml(text: string): string {
|
|
21
|
+
return text
|
|
22
|
+
.replace(/&/g, '&')
|
|
23
|
+
.replace(/</g, '<')
|
|
24
|
+
.replace(/>/g, '>')
|
|
25
|
+
.replace(/"/g, '"')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convert @pageName and @person mentions to clickable HTML.
|
|
30
|
+
* Returns an HTML string (use with v-html).
|
|
31
|
+
*/
|
|
32
|
+
export function parseMentions(text: string): string {
|
|
33
|
+
let html = escapeHtml(text)
|
|
34
|
+
|
|
35
|
+
for (const { label, path } of SORTED_LABELS) {
|
|
36
|
+
const escaped = escapeHtml(label)
|
|
37
|
+
const regex = new RegExp(`@${escaped}`, 'g')
|
|
38
|
+
html = html.replace(
|
|
39
|
+
regex,
|
|
40
|
+
`<a href="${path}" class="memo-mention" data-mention-page="${path}">@${escaped}</a>`,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Person mentions — unmatched @name rendered as blue chip
|
|
45
|
+
html = html.replace(/@([^@\s<][^@\n]*?)(?=\s|$|<|@)/g, (match, name) => {
|
|
46
|
+
if (match.includes('class="memo-mention"')) return match
|
|
47
|
+
return `<span class="mention-chip">@${name}</span>`
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return html
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Check if text contains any page mentions */
|
|
54
|
+
export function hasMentions(text: string): boolean {
|
|
55
|
+
return SORTED_LABELS.some(({ label }) => text.includes(`@${label}`))
|
|
56
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timezone-aware date/time helpers.
|
|
3
|
+
* Default timezone can be configured via VITE_TIMEZONE env var.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TZ = (import.meta.env.VITE_TIMEZONE as string) || Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
7
|
+
|
|
8
|
+
/** YYYY-MM-DD in the configured timezone */
|
|
9
|
+
export function toDateString(date: Date = new Date(), tz: string = DEFAULT_TZ): string {
|
|
10
|
+
return date.toLocaleDateString('en-CA', { timeZone: tz })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** YYYY-MM-DD HH:mm in the configured timezone */
|
|
14
|
+
export function toDateTimeString(date: Date = new Date(), tz: string = DEFAULT_TZ): string {
|
|
15
|
+
const d = date.toLocaleDateString('en-CA', { timeZone: tz })
|
|
16
|
+
const t = date.toLocaleTimeString('en-GB', { timeZone: tz, hour: '2-digit', minute: '2-digit', hour12: false })
|
|
17
|
+
return `${d} ${t}`
|
|
18
|
+
}
|