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
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { getComponentDef, type ComponentDef } from './componentCatalog'
|
|
4
|
+
|
|
5
|
+
export interface CanvasComponent {
|
|
6
|
+
id: string
|
|
7
|
+
componentType: string
|
|
8
|
+
props: Record<string, unknown>
|
|
9
|
+
children: CanvasComponent[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
components: CanvasComponent[]
|
|
14
|
+
selectedId: string | null
|
|
15
|
+
viewport: 'mobile' | 'desktop'
|
|
16
|
+
viewMode?: boolean
|
|
17
|
+
}>()
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
select: [id: string, event: MouseEvent]
|
|
21
|
+
drop: [componentId: string, parentId?: string, x?: number, y?: number]
|
|
22
|
+
delete: [id: string]
|
|
23
|
+
reorder: [id: string, direction: 'up' | 'down']
|
|
24
|
+
}>()
|
|
25
|
+
|
|
26
|
+
// Auto-expand canvas based on component positions
|
|
27
|
+
const canvasMinWidth = computed(() => {
|
|
28
|
+
let maxX = 0
|
|
29
|
+
for (const c of props.components) {
|
|
30
|
+
const x = (c.props.x as number) || 0
|
|
31
|
+
const w = (c.props.w as number) || 200
|
|
32
|
+
maxX = Math.max(maxX, x + w + 40)
|
|
33
|
+
}
|
|
34
|
+
return maxX > 0 ? `${maxX}px` : 'auto'
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const canvasMinHeight = computed(() => {
|
|
38
|
+
let maxY = 0
|
|
39
|
+
for (const c of props.components) {
|
|
40
|
+
const y = (c.props.y as number) || 0
|
|
41
|
+
const h = (c.props.h as number) || 40
|
|
42
|
+
maxY = Math.max(maxY, y + h + 40)
|
|
43
|
+
}
|
|
44
|
+
return maxY > 400 ? `${maxY}px` : '400px'
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const canvasWidth = computed(() => props.viewport === 'mobile' ? '375px' : '100%')
|
|
48
|
+
|
|
49
|
+
function onDragOver(e: DragEvent) {
|
|
50
|
+
e.preventDefault()
|
|
51
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function onDrop(e: DragEvent, parentId?: string) {
|
|
55
|
+
e.preventDefault()
|
|
56
|
+
e.stopPropagation()
|
|
57
|
+
const compId = e.dataTransfer?.getData('component-id')
|
|
58
|
+
if (compId) {
|
|
59
|
+
// Drop position X/Y
|
|
60
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
|
61
|
+
const x = Math.round(e.clientX - rect.left)
|
|
62
|
+
const y = Math.round(e.clientY - rect.top)
|
|
63
|
+
emit('drop', compId, parentId, x, y)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Internal drag (reorder)
|
|
68
|
+
const dragInsert = ref('')
|
|
69
|
+
let internalDragId = ''
|
|
70
|
+
|
|
71
|
+
function onInternalDragStart(e: DragEvent, id: string) {
|
|
72
|
+
internalDragId = id
|
|
73
|
+
if (e.dataTransfer) {
|
|
74
|
+
e.dataTransfer.effectAllowed = 'move'
|
|
75
|
+
e.dataTransfer.setData('internal-comp-id', id)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function onInternalDragOver(e: DragEvent, targetId: string) {
|
|
80
|
+
e.preventDefault()
|
|
81
|
+
if (!internalDragId || internalDragId === targetId) return
|
|
82
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
|
83
|
+
const midY = rect.top + rect.height / 2
|
|
84
|
+
dragInsert.value = e.clientY < midY ? targetId + '-top' : targetId + '-bottom'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function onInternalDrop(e: DragEvent, targetId: string) {
|
|
88
|
+
e.preventDefault()
|
|
89
|
+
const sourceId = e.dataTransfer?.getData('internal-comp-id') || internalDragId
|
|
90
|
+
if (!sourceId || sourceId === targetId) { dragInsert.value = ''; return }
|
|
91
|
+
|
|
92
|
+
// Drop from palette
|
|
93
|
+
const paletteCompId = e.dataTransfer?.getData('component-id')
|
|
94
|
+
if (paletteCompId) {
|
|
95
|
+
emit('drop', paletteCompId, getComponentDef(targetId)?.allowChildren ? targetId : undefined)
|
|
96
|
+
dragInsert.value = ''
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Internal reorder
|
|
101
|
+
const isTop = dragInsert.value.endsWith('-top')
|
|
102
|
+
emit('reorder', sourceId, isTop ? 'up' : 'down')
|
|
103
|
+
dragInsert.value = ''
|
|
104
|
+
internalDragId = ''
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Snap guidelines
|
|
108
|
+
const snapLines = ref<{ type: 'h' | 'v'; pos: number }[]>([])
|
|
109
|
+
const SNAP_THRESHOLD = 5
|
|
110
|
+
|
|
111
|
+
function getSnapTargets(excludeId: string): { x: number; y: number; cx: number; cy: number; r: number; b: number }[] {
|
|
112
|
+
return props.components
|
|
113
|
+
.filter(c => c.id !== excludeId && typeof c.props.x === 'number')
|
|
114
|
+
.map(c => {
|
|
115
|
+
const x = c.props.x as number
|
|
116
|
+
const y = c.props.y as number
|
|
117
|
+
const w = (c.props.w as number) || 200
|
|
118
|
+
const h = (c.props.h as number) || 40
|
|
119
|
+
return { x, y, cx: x + w / 2, cy: y + h / 2, r: x + w, b: y + h }
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function snapPosition(x: number, y: number, w: number, h: number, excludeId: string): { x: number; y: number } {
|
|
124
|
+
const targets = getSnapTargets(excludeId)
|
|
125
|
+
const lines: { type: 'h' | 'v'; pos: number }[] = []
|
|
126
|
+
let sx = x, sy = y
|
|
127
|
+
const cx = x + w / 2, r = x + w
|
|
128
|
+
const cy = y + h / 2, b = y + h
|
|
129
|
+
|
|
130
|
+
for (const t of targets) {
|
|
131
|
+
// Vertical snap (X axis)
|
|
132
|
+
if (Math.abs(x - t.x) < SNAP_THRESHOLD) { sx = t.x; lines.push({ type: 'v', pos: t.x }) }
|
|
133
|
+
else if (Math.abs(r - t.r) < SNAP_THRESHOLD) { sx = t.r - w; lines.push({ type: 'v', pos: t.r }) }
|
|
134
|
+
else if (Math.abs(cx - t.cx) < SNAP_THRESHOLD) { sx = t.cx - w / 2; lines.push({ type: 'v', pos: t.cx }) }
|
|
135
|
+
else if (Math.abs(x - t.r) < SNAP_THRESHOLD) { sx = t.r; lines.push({ type: 'v', pos: t.r }) }
|
|
136
|
+
else if (Math.abs(r - t.x) < SNAP_THRESHOLD) { sx = t.x - w; lines.push({ type: 'v', pos: t.x }) }
|
|
137
|
+
|
|
138
|
+
// Horizontal snap (Y axis)
|
|
139
|
+
if (Math.abs(y - t.y) < SNAP_THRESHOLD) { sy = t.y; lines.push({ type: 'h', pos: t.y }) }
|
|
140
|
+
else if (Math.abs(b - t.b) < SNAP_THRESHOLD) { sy = t.b - h; lines.push({ type: 'h', pos: t.b }) }
|
|
141
|
+
else if (Math.abs(cy - t.cy) < SNAP_THRESHOLD) { sy = t.cy - h / 2; lines.push({ type: 'h', pos: t.cy }) }
|
|
142
|
+
else if (Math.abs(y - t.b) < SNAP_THRESHOLD) { sy = t.b; lines.push({ type: 'h', pos: t.b }) }
|
|
143
|
+
else if (Math.abs(b - t.y) < SNAP_THRESHOLD) { sy = t.y - h; lines.push({ type: 'h', pos: t.y }) }
|
|
144
|
+
}
|
|
145
|
+
snapLines.value = lines
|
|
146
|
+
return { x: sx, y: sy }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Mouse drag movement
|
|
150
|
+
let dragMoveComp: any = null
|
|
151
|
+
let dragStartX = 0
|
|
152
|
+
let dragStartY = 0
|
|
153
|
+
let origX = 0
|
|
154
|
+
let origY = 0
|
|
155
|
+
|
|
156
|
+
function startDragMove(e: MouseEvent, comp: any) {
|
|
157
|
+
if (props.viewMode) return
|
|
158
|
+
// Initialize default values if x/y/w/h are missing
|
|
159
|
+
if (typeof comp.props.x !== 'number') comp.props.x = 0
|
|
160
|
+
if (typeof comp.props.y !== 'number') comp.props.y = 0
|
|
161
|
+
if (typeof comp.props.w !== 'number') comp.props.w = 200
|
|
162
|
+
if (typeof comp.props.h !== 'number') comp.props.h = 40
|
|
163
|
+
dragMoveComp = comp
|
|
164
|
+
dragStartX = e.clientX
|
|
165
|
+
dragStartY = e.clientY
|
|
166
|
+
origX = comp.props.x as number
|
|
167
|
+
origY = comp.props.y as number
|
|
168
|
+
document.addEventListener('mousemove', onDragMove)
|
|
169
|
+
document.addEventListener('mouseup', onDragMoveEnd)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function onDragMove(e: MouseEvent) {
|
|
173
|
+
if (!dragMoveComp) return
|
|
174
|
+
const rawX = origX + (e.clientX - dragStartX)
|
|
175
|
+
const rawY = origY + (e.clientY - dragStartY)
|
|
176
|
+
const w = (dragMoveComp.props.w as number) || 200
|
|
177
|
+
const h = (dragMoveComp.props.h as number) || 40
|
|
178
|
+
const snapped = snapPosition(rawX, rawY, w, h, dragMoveComp.id)
|
|
179
|
+
dragMoveComp.props.x = snapped.x
|
|
180
|
+
dragMoveComp.props.y = snapped.y
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function onDragMoveEnd() {
|
|
184
|
+
dragMoveComp = null
|
|
185
|
+
snapLines.value = []
|
|
186
|
+
document.removeEventListener('mousemove', onDragMove)
|
|
187
|
+
document.removeEventListener('mouseup', onDragMoveEnd)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Resize
|
|
191
|
+
let resizeComp: any = null
|
|
192
|
+
let resizeDir = ''
|
|
193
|
+
let resizeStartX = 0
|
|
194
|
+
let resizeStartY = 0
|
|
195
|
+
let resizeOrigW = 0
|
|
196
|
+
let resizeOrigH = 0
|
|
197
|
+
let resizeOrigX = 0
|
|
198
|
+
let resizeOrigY = 0
|
|
199
|
+
|
|
200
|
+
function startResize(e: MouseEvent, comp: any, dir: string) {
|
|
201
|
+
resizeComp = comp
|
|
202
|
+
resizeDir = dir
|
|
203
|
+
resizeStartX = e.clientX
|
|
204
|
+
resizeStartY = e.clientY
|
|
205
|
+
resizeOrigW = (comp.props.w as number) || 200
|
|
206
|
+
resizeOrigH = (comp.props.h as number) || 40
|
|
207
|
+
resizeOrigX = (comp.props.x as number) || 0
|
|
208
|
+
resizeOrigY = (comp.props.y as number) || 0
|
|
209
|
+
document.addEventListener('mousemove', onResize)
|
|
210
|
+
document.addEventListener('mouseup', onResizeEnd)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function onResize(e: MouseEvent) {
|
|
214
|
+
if (!resizeComp) return
|
|
215
|
+
const dx = e.clientX - resizeStartX
|
|
216
|
+
const dy = e.clientY - resizeStartY
|
|
217
|
+
|
|
218
|
+
if (resizeDir.includes('e')) resizeComp.props.w = Math.max(20, resizeOrigW + dx)
|
|
219
|
+
if (resizeDir.includes('w')) { resizeComp.props.w = Math.max(20, resizeOrigW - dx); resizeComp.props.x = resizeOrigX + dx }
|
|
220
|
+
if (resizeDir.includes('s')) resizeComp.props.h = Math.max(20, resizeOrigH + dy)
|
|
221
|
+
if (resizeDir.includes('n')) { resizeComp.props.h = Math.max(20, resizeOrigH - dy); resizeComp.props.y = resizeOrigY + dy }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function onResizeEnd() {
|
|
225
|
+
resizeComp = null
|
|
226
|
+
document.removeEventListener('mousemove', onResize)
|
|
227
|
+
document.removeEventListener('mouseup', onResizeEnd)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function onInlineEdit(comp: any, key: string, e: Event) {
|
|
231
|
+
const text = (e.target as HTMLElement).textContent || ''
|
|
232
|
+
if (comp.props[key] !== text) comp.props[key] = text
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function onKeydown(e: KeyboardEvent) {
|
|
236
|
+
if (e.key === 'Delete' && props.selectedId) {
|
|
237
|
+
emit('delete', props.selectedId)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function renderStyle(comp: CanvasComponent): Record<string, string> {
|
|
242
|
+
const p = comp.props
|
|
243
|
+
const style: Record<string, string> = {}
|
|
244
|
+
|
|
245
|
+
// Absolute positioning (when x/y are set)
|
|
246
|
+
if (typeof p.x === 'number' && typeof p.y === 'number') {
|
|
247
|
+
style.position = 'absolute'
|
|
248
|
+
style.left = `${p.x}px`
|
|
249
|
+
style.top = `${p.y}px`
|
|
250
|
+
}
|
|
251
|
+
if (typeof p.w === 'number' && p.w > 0) style.width = `${p.w}px`
|
|
252
|
+
if (typeof p.h === 'number' && p.h > 0) style.height = `${p.h}px`
|
|
253
|
+
|
|
254
|
+
if (p.padding) style.padding = `${p.padding}px`
|
|
255
|
+
if (p.gap) style.gap = `${p.gap}px`
|
|
256
|
+
if (p.direction === 'row') style.flexDirection = 'row'
|
|
257
|
+
if (p.direction === 'column') style.flexDirection = 'column'
|
|
258
|
+
if (p.height && typeof p.x !== 'number') style.height = `${p.height}px`
|
|
259
|
+
if (p.width && typeof p.width === 'number' && typeof p.x !== 'number') style.width = `${p.width}px`
|
|
260
|
+
if (p.maxWidth) style.maxWidth = `${p.maxWidth}px`
|
|
261
|
+
if (p.borderRadius) style.borderRadius = `${p.borderRadius}px`
|
|
262
|
+
if (p.margin) style.margin = `${p.margin}px 0`
|
|
263
|
+
if (typeof p.zIndex === 'number') style.zIndex = String(p.zIndex)
|
|
264
|
+
|
|
265
|
+
// CSS whitelist application
|
|
266
|
+
if (p.css && typeof p.css === 'object') {
|
|
267
|
+
const css = p.css as Record<string, string>
|
|
268
|
+
for (const [k, v] of Object.entries(css)) {
|
|
269
|
+
if (v) style[k] = v
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return style
|
|
274
|
+
}
|
|
275
|
+
</script>
|
|
276
|
+
|
|
277
|
+
<template>
|
|
278
|
+
<div class="canvas-wrap" tabindex="0" @keydown="onKeydown">
|
|
279
|
+
<div class="canvas-scroll-wrapper">
|
|
280
|
+
<div class="canvas" :style="{ maxWidth: canvasWidth, minWidth: canvasMinWidth, minHeight: canvasMinHeight }" @dragover="onDragOver" @drop="onDrop($event)">
|
|
281
|
+
<div v-if="!components.length && !viewMode" class="canvas-empty">Drag components from the palette on the left<br/><small>Shift/Cmd+click for multi-select | Right-click for menu</small></div>
|
|
282
|
+
<div v-if="!components.length" class="canvas-empty" @dragover="onDragOver" @drop="onDrop($event)">
|
|
283
|
+
Drag and drop components to place them
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<template v-for="comp in components" :key="comp.id">
|
|
287
|
+
<div
|
|
288
|
+
class="canvas-comp"
|
|
289
|
+
:class="{ selected: selectedId === comp.id && !viewMode, 'has-children': getComponentDef(comp.componentType)?.allowChildren, 'view-mode': viewMode, 'drag-over-top': dragInsert === comp.id + '-top', 'drag-over-bottom': dragInsert === comp.id + '-bottom' }"
|
|
290
|
+
:style="renderStyle(comp)"
|
|
291
|
+
:draggable="!viewMode"
|
|
292
|
+
@click.stop="emit('select', comp.id, $event)"
|
|
293
|
+
@mousedown.stop="!comp.props.locked && !$event.shiftKey && !$event.metaKey && !$event.ctrlKey && startDragMove($event, comp)"
|
|
294
|
+
@dragstart.stop="onInternalDragStart($event, comp.id)"
|
|
295
|
+
@dragover.stop="onInternalDragOver($event, comp.id)"
|
|
296
|
+
@dragleave.stop="dragInsert = ''"
|
|
297
|
+
@drop.stop="onInternalDrop($event, comp.id)"
|
|
298
|
+
>
|
|
299
|
+
<div v-if="!viewMode" class="comp-label">{{ getComponentDef(comp.componentType)?.icon }} {{ getComponentDef(comp.componentType)?.name }}</div>
|
|
300
|
+
|
|
301
|
+
<!-- Component rendering -->
|
|
302
|
+
<div v-if="comp.componentType === 'page-title'" class="render-title" :contenteditable="!viewMode && selectedId === comp.id" @blur="onInlineEdit(comp, 'text', $event)">{{ comp.props.text }}</div>
|
|
303
|
+
<div v-else-if="comp.componentType === 'text'" class="render-text" :style="{ fontSize: comp.props.size + 'px', color: comp.props.color as string }" :contenteditable="!viewMode && selectedId === comp.id" @blur="onInlineEdit(comp, 'text', $event)">{{ comp.props.text }}</div>
|
|
304
|
+
<div v-else-if="comp.componentType === 'button'" class="render-button" :class="'btn--' + comp.props.variant" :style="{ cursor: comp.props.onClick ? 'pointer' : 'default' }" @click.stop="comp.props.onClick && viewMode && $router?.push(comp.props.onClick as string)">{{ comp.props.text }}</div>
|
|
305
|
+
<input v-else-if="comp.componentType === 'text-field'" class="render-input" :placeholder="comp.props.placeholder as string" disabled />
|
|
306
|
+
<div v-else-if="comp.componentType === 'data-grid'" class="render-grid">
|
|
307
|
+
<div class="grid-header"><span v-for="col in (comp.props.columns as string[])" :key="col">{{ col }}</span></div>
|
|
308
|
+
<div v-for="i in (comp.props.rows as number)" :key="i" class="grid-row"><span v-for="col in (comp.props.columns as string[])" :key="col">—</span></div>
|
|
309
|
+
</div>
|
|
310
|
+
<div v-else-if="comp.componentType === 'chart'" class="render-chart">📊 {{ comp.props.title }}</div>
|
|
311
|
+
<div v-else-if="comp.componentType === 'alert'" class="render-alert" :class="'alert--' + comp.props.type">{{ comp.props.message }}</div>
|
|
312
|
+
<div v-else-if="comp.componentType === 'divider'" class="render-divider" />
|
|
313
|
+
<div v-else-if="comp.componentType === 'spacer'" :style="{ height: comp.props.height + 'px' }" />
|
|
314
|
+
<div v-else-if="comp.componentType === 'label'" class="render-label">{{ comp.props.text }}</div>
|
|
315
|
+
<label v-else-if="comp.componentType === 'checkbox'" class="render-check">
|
|
316
|
+
<input type="checkbox" :checked="comp.props.checked as boolean" disabled /> {{ comp.props.label }}
|
|
317
|
+
</label>
|
|
318
|
+
<div v-else-if="comp.componentType === 'date-picker'" class="render-datepicker">
|
|
319
|
+
<span class="dp-label">{{ comp.props.label }}</span>
|
|
320
|
+
<input type="date" class="dp-input" disabled />
|
|
321
|
+
</div>
|
|
322
|
+
<div v-else-if="comp.componentType === 'pagination'" class="render-pagination">
|
|
323
|
+
<button class="pg-btn" disabled><</button>
|
|
324
|
+
<button v-for="p in Math.min(comp.props.totalPages as number || 5, 7)" :key="p" class="pg-btn" :class="{ 'pg-active': p === (comp.props.current as number || 1) }" disabled>{{ p }}</button>
|
|
325
|
+
<button class="pg-btn" disabled>></button>
|
|
326
|
+
</div>
|
|
327
|
+
<div v-else-if="comp.componentType === 'switch'" class="render-switch">
|
|
328
|
+
<span>{{ comp.props.label }}</span>
|
|
329
|
+
<span class="switch-track" :class="{ 'switch-on': comp.props.checked }"><span class="switch-thumb" /></span>
|
|
330
|
+
</div>
|
|
331
|
+
<div v-else-if="comp.componentType === 'card'" class="render-card">
|
|
332
|
+
<img v-if="comp.props.imageUrl" :src="comp.props.imageUrl as string" class="card-image" />
|
|
333
|
+
<div class="card-title">{{ comp.props.title }}</div>
|
|
334
|
+
<div class="card-content">{{ comp.props.content }}</div>
|
|
335
|
+
</div>
|
|
336
|
+
<div v-else-if="comp.componentType === 'sidebar'" class="render-sidebar">
|
|
337
|
+
<div v-for="(item, idx) in (comp.props.menuItems as any[] || [])" :key="idx" class="sidebar-item" @click.stop="item.link && $router?.push(item.link)">
|
|
338
|
+
<span class="sidebar-icon">{{ item.icon }}</span>
|
|
339
|
+
<span class="sidebar-text">{{ item.text }}</span>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
<div v-else class="render-placeholder">{{ comp.props.text || comp.props.title || comp.componentType }}</div>
|
|
343
|
+
|
|
344
|
+
<!-- Children -->
|
|
345
|
+
<template v-if="comp.children?.length">
|
|
346
|
+
<MockupCanvas
|
|
347
|
+
:components="comp.children"
|
|
348
|
+
:selected-id="selectedId"
|
|
349
|
+
:viewport="viewport"
|
|
350
|
+
:view-mode="viewMode"
|
|
351
|
+
@select="(id: string, ev: MouseEvent) => emit('select', id, ev)"
|
|
352
|
+
@drop="emit('drop', $event, comp.id)"
|
|
353
|
+
@delete="emit('delete', $event)"
|
|
354
|
+
/>
|
|
355
|
+
</template>
|
|
356
|
+
|
|
357
|
+
<!-- Lock indicator -->
|
|
358
|
+
<div v-if="comp.props.locked && !viewMode" class="lock-indicator">🔒</div>
|
|
359
|
+
<!-- Resize handles + delete -->
|
|
360
|
+
<template v-if="selectedId === comp.id && !viewMode && !comp.props.locked">
|
|
361
|
+
<div class="resize-handle rh-n" @mousedown.stop.prevent="startResize($event, comp, 'n')" />
|
|
362
|
+
<div class="resize-handle rh-s" @mousedown.stop.prevent="startResize($event, comp, 's')" />
|
|
363
|
+
<div class="resize-handle rh-e" @mousedown.stop.prevent="startResize($event, comp, 'e')" />
|
|
364
|
+
<div class="resize-handle rh-w" @mousedown.stop.prevent="startResize($event, comp, 'w')" />
|
|
365
|
+
<div class="resize-handle rh-ne" @mousedown.stop.prevent="startResize($event, comp, 'ne')" />
|
|
366
|
+
<div class="resize-handle rh-nw" @mousedown.stop.prevent="startResize($event, comp, 'nw')" />
|
|
367
|
+
<div class="resize-handle rh-se" @mousedown.stop.prevent="startResize($event, comp, 'se')" />
|
|
368
|
+
<div class="resize-handle rh-sw" @mousedown.stop.prevent="startResize($event, comp, 'sw')" />
|
|
369
|
+
</template>
|
|
370
|
+
<div v-if="selectedId === comp.id && !viewMode" class="comp-controls">
|
|
371
|
+
<button class="comp-delete" @click.stop="emit('delete', comp.id)">✕</button>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
</template>
|
|
375
|
+
<!-- Snap lines -->
|
|
376
|
+
<div v-for="(line, idx) in snapLines" :key="idx" class="snap-line" :class="line.type === 'h' ? 'snap-h' : 'snap-v'" :style="line.type === 'h' ? { top: line.pos + 'px' } : { left: line.pos + 'px' }" />
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
</template>
|
|
381
|
+
|
|
382
|
+
<style scoped>
|
|
383
|
+
.canvas-wrap { flex: 1; overflow: auto; padding: 16px; background: #f8f9fa; min-height: 300px; }
|
|
384
|
+
.canvas-scroll-wrapper { overflow: auto; flex: 1; }
|
|
385
|
+
.canvas-empty { display: flex; align-items: center; justify-content: center; height: 200px; color: #9ca3af; font-size: 14px; }
|
|
386
|
+
.snap-line { position: absolute; z-index: 9999; pointer-events: none; }
|
|
387
|
+
.snap-h { left: 0; right: 0; height: 1px; background: #f43f5e; }
|
|
388
|
+
.snap-v { top: 0; bottom: 0; width: 1px; background: #f43f5e; }
|
|
389
|
+
.canvas { margin: 0 auto; min-height: 400px; background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); padding: 16px; display: flex; flex-direction: column; gap: 4px; position: relative; }
|
|
390
|
+
.canvas-empty { display: flex; align-items: center; justify-content: center; height: 200px; color: #9ca3af; font-size: 14px; border: 2px dashed #d1d5db; border-radius: 8px; }
|
|
391
|
+
.canvas-comp { position: relative; border: 1px solid transparent; border-radius: 4px; padding: 0; transition: all 0.1s; display: flex; flex-direction: column; box-sizing: border-box; }
|
|
392
|
+
.canvas-comp > * { flex: 1; }
|
|
393
|
+
.lock-indicator { position: absolute; top: 2px; left: 2px; font-size: 12px; opacity: 0.5; z-index: 5; pointer-events: none; }
|
|
394
|
+
.render-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
|
|
395
|
+
.card-image { width: 100%; height: 120px; object-fit: cover; }
|
|
396
|
+
.card-title { font-size: 14px; font-weight: 700; padding: 8px 12px 4px; }
|
|
397
|
+
.card-content { font-size: 12px; color: #6b7280; padding: 0 12px 12px; line-height: 1.5; }
|
|
398
|
+
.render-sidebar { background: #1e293b; border-radius: 8px; padding: 8px 0; min-height: 200px; width: 100%; height: 100%; box-sizing: border-box; }
|
|
399
|
+
.sidebar-item { display: flex; align-items: center; gap: 8px; padding: 8px 16px; color: #cbd5e1; font-size: 13px; cursor: pointer; transition: background 0.15s; }
|
|
400
|
+
.sidebar-item:hover { background: rgba(255,255,255,0.1); color: #fff; }
|
|
401
|
+
.sidebar-icon { font-size: 16px; }
|
|
402
|
+
.sidebar-text { font-weight: 500; }
|
|
403
|
+
.canvas-comp:hover { border-color: #93c5fd; }
|
|
404
|
+
.canvas-comp.view-mode { border: none !important; padding: 2px 0; outline: none !important; }
|
|
405
|
+
.canvas-comp.view-mode:hover { border-color: transparent !important; box-shadow: none; }
|
|
406
|
+
.canvas-comp.view-mode { cursor: pointer; }
|
|
407
|
+
.canvas-comp.view-mode.selected { outline: 2px solid rgba(59,130,246,0.3) !important; border-radius: 4px; background: rgba(59,130,246,0.04); }
|
|
408
|
+
.canvas-comp.selected { border-color: #3b82f6; outline: 2px solid rgba(59,130,246,0.3); }
|
|
409
|
+
.canvas-comp.has-children { min-height: 40px; }
|
|
410
|
+
.comp-label { font-size: 10px; color: #9ca3af; margin-bottom: 2px; }
|
|
411
|
+
.canvas-comp.drag-over-top { border-top: 2px solid #3b82f6 !important; }
|
|
412
|
+
.canvas-comp.drag-over-bottom { border-bottom: 2px solid #3b82f6 !important; }
|
|
413
|
+
.resize-handle { position: absolute; width: 8px; height: 8px; background: #3b82f6; border-radius: 50%; z-index: 10; }
|
|
414
|
+
.rh-n { top: -4px; left: 50%; transform: translateX(-50%); cursor: n-resize; }
|
|
415
|
+
.rh-s { bottom: -4px; left: 50%; transform: translateX(-50%); cursor: s-resize; }
|
|
416
|
+
.rh-e { right: -4px; top: 50%; transform: translateY(-50%); cursor: e-resize; }
|
|
417
|
+
.rh-w { left: -4px; top: 50%; transform: translateY(-50%); cursor: w-resize; }
|
|
418
|
+
.rh-ne { top: -4px; right: -4px; cursor: ne-resize; }
|
|
419
|
+
.rh-nw { top: -4px; left: -4px; cursor: nw-resize; }
|
|
420
|
+
.rh-se { bottom: -4px; right: -4px; cursor: se-resize; }
|
|
421
|
+
.rh-sw { bottom: -4px; left: -4px; cursor: sw-resize; }
|
|
422
|
+
.comp-controls { position: absolute; top: -10px; right: -10px; display: flex; gap: 2px; }
|
|
423
|
+
.comp-delete { width: 20px; height: 20px; border-radius: 50%; background: #ef4444; color: #fff; border: none; font-size: 12px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
|
|
424
|
+
|
|
425
|
+
/* Rendering */
|
|
426
|
+
.render-title { font-size: 24px; font-weight: 700; }
|
|
427
|
+
.render-text { line-height: 1.5; }
|
|
428
|
+
.render-button { display: inline-block; padding: 8px 16px; border-radius: 6px; font-size: 13px; font-weight: 500; }
|
|
429
|
+
.btn--primary { background: #3b82f6; color: #fff; }
|
|
430
|
+
.btn--secondary { background: #e5e7eb; color: #333; }
|
|
431
|
+
.btn--danger { background: #ef4444; color: #fff; }
|
|
432
|
+
.btn--ghost { background: transparent; color: #3b82f6; border: 1px solid #3b82f6; }
|
|
433
|
+
.render-input { border: 1px solid #d1d5db; border-radius: 6px; padding: 8px; font-size: 13px; width: 100%; box-sizing: border-box; }
|
|
434
|
+
.render-grid { border: 1px solid #e5e7eb; border-radius: 4px; overflow: hidden; font-size: 12px; }
|
|
435
|
+
.grid-header { display: flex; background: #f8f9fa; font-weight: 600; }
|
|
436
|
+
.grid-header span, .grid-row span { flex: 1; padding: 6px 8px; border-right: 1px solid #e5e7eb; }
|
|
437
|
+
.grid-row { display: flex; border-top: 1px solid #e5e7eb; }
|
|
438
|
+
.render-chart { background: #f0f4ff; border-radius: 6px; padding: 24px; text-align: center; color: #6b7280; }
|
|
439
|
+
.render-alert { padding: 8px 12px; border-radius: 6px; font-size: 13px; }
|
|
440
|
+
.alert--info { background: #dbeafe; color: #1d4ed8; }
|
|
441
|
+
.alert--warning { background: #fef3c7; color: #d97706; }
|
|
442
|
+
.alert--error { background: #fef2f2; color: #dc2626; }
|
|
443
|
+
.alert--success { background: #dcfce7; color: #16a34a; }
|
|
444
|
+
.render-divider { height: 1px; background: #e5e7eb; margin: 8px 0; }
|
|
445
|
+
.render-label { font-size: 12px; color: #6b7280; font-weight: 500; }
|
|
446
|
+
.render-check { font-size: 13px; display: flex; align-items: center; gap: 6px; }
|
|
447
|
+
.render-datepicker { display: flex; flex-direction: column; gap: 4px; }
|
|
448
|
+
.dp-label { font-size: 12px; color: #6b7280; }
|
|
449
|
+
.dp-input { border: 1px solid #d1d5db; border-radius: 6px; padding: 6px 8px; font-size: 13px; }
|
|
450
|
+
.render-pagination { display: flex; gap: 4px; }
|
|
451
|
+
.pg-btn { width: 28px; height: 28px; border: 1px solid #d1d5db; border-radius: 4px; background: #fff; font-size: 12px; display: flex; align-items: center; justify-content: center; }
|
|
452
|
+
.pg-active { background: #3b82f6; color: #fff; border-color: #3b82f6; }
|
|
453
|
+
.render-switch { display: flex; align-items: center; gap: 8px; font-size: 13px; }
|
|
454
|
+
.switch-track { width: 36px; height: 20px; background: #d1d5db; border-radius: 10px; position: relative; display: inline-block; }
|
|
455
|
+
.switch-on { background: #3b82f6; }
|
|
456
|
+
.switch-thumb { width: 16px; height: 16px; background: #fff; border-radius: 50%; position: absolute; top: 2px; left: 2px; transition: left 0.2s; }
|
|
457
|
+
.switch-on .switch-thumb { left: 18px; }
|
|
458
|
+
.render-placeholder { color: #9ca3af; font-size: 12px; padding: 8px; background: #f9fafb; border-radius: 4px; }
|
|
459
|
+
</style>
|