popilot 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +372 -0
  2. package/adapters/claude-code/.claude/commands/_domain.md.hbs +32 -0
  3. package/adapters/claude-code/.claude/commands/analytics.md.hbs +55 -0
  4. package/adapters/claude-code/.claude/commands/daily.md.hbs +301 -0
  5. package/adapters/claude-code/.claude/commands/dev.md.hbs +62 -0
  6. package/adapters/claude-code/.claude/commands/handoff.md +258 -0
  7. package/adapters/claude-code/.claude/commands/market.md +120 -0
  8. package/adapters/claude-code/.claude/commands/metrics.md +123 -0
  9. package/adapters/claude-code/.claude/commands/oscar-loop.md +436 -0
  10. package/adapters/claude-code/.claude/commands/party.md +85 -0
  11. package/adapters/claude-code/.claude/commands/plan.md +43 -0
  12. package/adapters/claude-code/.claude/commands/research.md +203 -0
  13. package/adapters/claude-code/.claude/commands/retro.md +68 -0
  14. package/adapters/claude-code/.claude/commands/save.md +440 -0
  15. package/adapters/claude-code/.claude/commands/sessions.md +139 -0
  16. package/adapters/claude-code/.claude/commands/sprint.md +106 -0
  17. package/adapters/claude-code/.claude/commands/start.md +368 -0
  18. package/adapters/claude-code/.claude/commands/strategy.md +41 -0
  19. package/adapters/claude-code/.claude/commands/task.md +220 -0
  20. package/adapters/claude-code/.claude/commands/tracking.md +116 -0
  21. package/adapters/claude-code/.claude/commands/validate.md +58 -0
  22. package/adapters/claude-code/CLAUDE.md.hbs +208 -0
  23. package/adapters/claude-code/manifest.yaml +36 -0
  24. package/bin/cli.mjs +218 -0
  25. package/lib/adapter.mjs +68 -0
  26. package/lib/doctor.mjs +161 -0
  27. package/lib/hydrate.mjs +421 -0
  28. package/lib/prompt.mjs +78 -0
  29. package/lib/scaffold.mjs +155 -0
  30. package/lib/setup-wizard.mjs +331 -0
  31. package/lib/template-engine.mjs +164 -0
  32. package/lib/yaml-lite.mjs +476 -0
  33. package/package.json +30 -0
  34. package/scaffold/.context/.secrets.yaml.example +20 -0
  35. package/scaffold/.context/WORKFLOW.md.hbs +332 -0
  36. package/scaffold/.context/agents/TEMPLATE.md +115 -0
  37. package/scaffold/.context/agents/analyst.md.hbs +362 -0
  38. package/scaffold/.context/agents/developer.md.hbs +390 -0
  39. package/scaffold/.context/agents/handoff-specialist.md.hbs +292 -0
  40. package/scaffold/.context/agents/market-researcher.md.hbs +288 -0
  41. package/scaffold/.context/agents/ollie.md +323 -0
  42. package/scaffold/.context/agents/operations.md.hbs +293 -0
  43. package/scaffold/.context/agents/orchestrator.md.hbs +434 -0
  44. package/scaffold/.context/agents/planner.md.hbs +405 -0
  45. package/scaffold/.context/agents/qa.md.hbs +409 -0
  46. package/scaffold/.context/agents/researcher.md.hbs +330 -0
  47. package/scaffold/.context/agents/sage.md +349 -0
  48. package/scaffold/.context/agents/strategist.md.hbs +339 -0
  49. package/scaffold/.context/agents/tracking-governor.md.hbs +291 -0
  50. package/scaffold/.context/agents/validator.md.hbs +365 -0
  51. package/scaffold/.context/integrations/_registry.yaml +38 -0
  52. package/scaffold/.context/integrations/providers/channel_io.yaml +38 -0
  53. package/scaffold/.context/integrations/providers/corti.yaml +203 -0
  54. package/scaffold/.context/integrations/providers/ga4.yaml +116 -0
  55. package/scaffold/.context/integrations/providers/intercom.yaml +47 -0
  56. package/scaffold/.context/integrations/providers/linear.yaml +46 -0
  57. package/scaffold/.context/integrations/providers/mixpanel.yaml +73 -0
  58. package/scaffold/.context/integrations/providers/notebooklm.yaml +74 -0
  59. package/scaffold/.context/integrations/providers/notion.yaml +129 -0
  60. package/scaffold/.context/integrations/providers/prod_db.yaml +183 -0
  61. package/scaffold/.context/oscar/workflows/multi-agent.md +82 -0
  62. package/scaffold/.context/oscar/workflows/ollie-sage.md +128 -0
  63. package/scaffold/.context/oscar/workflows/session-git.md +71 -0
  64. package/scaffold/.context/oscar/workflows/setup.md +663 -0
  65. package/scaffold/.context/oscar/workflows/tracking.md +118 -0
  66. package/scaffold/.context/project.yaml.example +102 -0
  67. package/scaffold/.context/templates/dev-guide.md +217 -0
  68. package/scaffold/.context/templates/epic-spec.md +225 -0
  69. package/scaffold/.context/templates/guardrail.md +94 -0
  70. package/scaffold/.context/templates/handoff-checklist.md +197 -0
  71. package/scaffold/.context/templates/prd.md +80 -0
  72. package/scaffold/.context/templates/retrospective.md +78 -0
  73. package/scaffold/.context/templates/screen-spec.md +714 -0
  74. package/scaffold/.context/templates/sprint-plan.md +72 -0
  75. package/scaffold/.context/templates/sprint-status.yaml +109 -0
  76. package/scaffold/.context/templates/story-v2.md +228 -0
  77. package/scaffold/.context/templates/validation-report.md +99 -0
  78. package/scaffold/.gitignore.append +7 -0
  79. package/scaffold/spec-site/env.d.ts +7 -0
  80. package/scaffold/spec-site/index.html +14 -0
  81. package/scaffold/spec-site/package.json +20 -0
  82. package/scaffold/spec-site/src/App.vue +27 -0
  83. package/scaffold/spec-site/src/assets/icons/menu/ic_ads.svg +10 -0
  84. package/scaffold/spec-site/src/assets/icons/menu/ic_ads_on.svg +10 -0
  85. package/scaffold/spec-site/src/assets/icons/menu/ic_board.svg +14 -0
  86. package/scaffold/spec-site/src/assets/icons/menu/ic_board_on.svg +14 -0
  87. package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard.svg +21 -0
  88. package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard_on.svg +21 -0
  89. package/scaffold/spec-site/src/assets/icons/menu/ic_pricing.svg +20 -0
  90. package/scaffold/spec-site/src/assets/icons/menu/ic_pricing_on.svg +20 -0
  91. package/scaffold/spec-site/src/assets/icons/menu/ic_store.svg +11 -0
  92. package/scaffold/spec-site/src/assets/icons/menu/ic_store_on.svg +11 -0
  93. package/scaffold/spec-site/src/components/Accordion.vue +108 -0
  94. package/scaffold/spec-site/src/components/AppHeader.vue +304 -0
  95. package/scaffold/spec-site/src/components/Badge.vue +25 -0
  96. package/scaffold/spec-site/src/components/CoachingCard.vue +112 -0
  97. package/scaffold/spec-site/src/components/MemoSidebar.vue +239 -0
  98. package/scaffold/spec-site/src/components/MockupShell.vue +100 -0
  99. package/scaffold/spec-site/src/components/RuleTable.vue +99 -0
  100. package/scaffold/spec-site/src/components/ScenarioSwitcher.vue +103 -0
  101. package/scaffold/spec-site/src/components/SpecNav.vue +26 -0
  102. package/scaffold/spec-site/src/components/SpecSection.vue +59 -0
  103. package/scaffold/spec-site/src/components/SummaryGrid.vue +39 -0
  104. package/scaffold/spec-site/src/components/VersionBadge.vue +38 -0
  105. package/scaffold/spec-site/src/composables/useActiveSection.ts +53 -0
  106. package/scaffold/spec-site/src/composables/useMemo.ts +138 -0
  107. package/scaffold/spec-site/src/composables/useRetro.ts +313 -0
  108. package/scaffold/spec-site/src/composables/useScenario.ts +43 -0
  109. package/scaffold/spec-site/src/composables/useScenarioStore.ts +102 -0
  110. package/scaffold/spec-site/src/composables/useTurso.ts +160 -0
  111. package/scaffold/spec-site/src/composables/useUser.ts +25 -0
  112. package/scaffold/spec-site/src/data/navigation.ts +59 -0
  113. package/scaffold/spec-site/src/data/types.ts +90 -0
  114. package/scaffold/spec-site/src/data/wireframeRegistry.ts +25 -0
  115. package/scaffold/spec-site/src/layouts/SplitPaneLayout.vue +79 -0
  116. package/scaffold/spec-site/src/main.ts +10 -0
  117. package/scaffold/spec-site/src/pages/IndexPage.vue +66 -0
  118. package/scaffold/spec-site/src/pages/PolicyDetail.vue +215 -0
  119. package/scaffold/spec-site/src/pages/PolicyIndex.vue +74 -0
  120. package/scaffold/spec-site/src/pages/retro/RetroActions.vue +191 -0
  121. package/scaffold/spec-site/src/pages/retro/RetroBoard.vue +192 -0
  122. package/scaffold/spec-site/src/pages/retro/RetroCard.vue +131 -0
  123. package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +287 -0
  124. package/scaffold/spec-site/src/pages/retro/RetroPage.vue +178 -0
  125. package/scaffold/spec-site/src/pages/shared/NoContentPlaceholder.vue +34 -0
  126. package/scaffold/spec-site/src/pages/shared/PlaceholderContent.vue +22 -0
  127. package/scaffold/spec-site/src/pages/shared/PlaceholderSpecPanel.vue +16 -0
  128. package/scaffold/spec-site/src/pages/shared/PolicyFallback.vue +145 -0
  129. package/scaffold/spec-site/src/pages/wireframe/WireframeShell.vue +151 -0
  130. package/scaffold/spec-site/src/router.ts +85 -0
  131. package/scaffold/spec-site/src/styles/base.css +21 -0
  132. package/scaffold/spec-site/src/styles/split-pane.css +143 -0
  133. package/scaffold/spec-site/src/styles/variables.css +47 -0
  134. package/scaffold/spec-site/src/utils/markdown.ts +197 -0
  135. package/scaffold/spec-site/tsconfig.json +20 -0
  136. package/scaffold/spec-site/vite.config.ts +18 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Turso HTTP API v2 (/v2/pipeline) direct call
3
+ * Uses raw fetch + /v2/pipeline for browser CORS compatibility
4
+ */
5
+
6
+ const url = import.meta.env.VITE_TURSO_URL as string
7
+ const authToken = import.meta.env.VITE_TURSO_AUTH_TOKEN as string
8
+
9
+ let _reachable: boolean | null = null
10
+ let _lastFailureAt = 0
11
+ const RETRY_COOLDOWN_MS = 15_000
12
+
13
+ export interface QueryResult<T = Record<string, unknown>> {
14
+ rows: T[]
15
+ error?: string
16
+ }
17
+
18
+ type ArgValue = string | number | null
19
+
20
+ interface HranaValue {
21
+ type: 'text' | 'integer' | 'float' | 'null'
22
+ value?: string
23
+ }
24
+
25
+ function toHranaArg(v: ArgValue): HranaValue {
26
+ if (v === null) return { type: 'null' }
27
+ if (typeof v === 'number') {
28
+ return Number.isInteger(v)
29
+ ? { type: 'integer', value: String(v) }
30
+ : { type: 'float', value: String(v) }
31
+ }
32
+ return { type: 'text', value: String(v) }
33
+ }
34
+
35
+ interface PipelineResult {
36
+ results: Array<{
37
+ type: 'ok' | 'error'
38
+ response?: {
39
+ type: string
40
+ result?: {
41
+ cols: Array<{ name: string }>
42
+ rows: Array<Array<HranaValue>>
43
+ affected_row_count: number
44
+ last_insert_rowid: string | null
45
+ }
46
+ }
47
+ error?: { message: string }
48
+ }>
49
+ }
50
+
51
+ async function pipeline(
52
+ sql: string,
53
+ args: ArgValue[],
54
+ ): Promise<{
55
+ cols: string[]
56
+ rows: Array<Record<string, unknown>>
57
+ affectedRowCount: number
58
+ lastInsertRowid: number | null
59
+ error?: string
60
+ }> {
61
+ if (!url || !authToken) {
62
+ return { cols: [], rows: [], affectedRowCount: 0, lastInsertRowid: null, error: 'Missing credentials' }
63
+ }
64
+
65
+ if (_reachable === false) {
66
+ if (Date.now() - _lastFailureAt >= RETRY_COOLDOWN_MS) {
67
+ _reachable = null
68
+ } else {
69
+ return { cols: [], rows: [], affectedRowCount: 0, lastInsertRowid: null, error: 'Turso unreachable' }
70
+ }
71
+ }
72
+
73
+ try {
74
+ const resp = await fetch(url + '/v2/pipeline', {
75
+ method: 'POST',
76
+ headers: {
77
+ Authorization: 'Bearer ' + authToken,
78
+ 'Content-Type': 'application/json',
79
+ },
80
+ body: JSON.stringify({
81
+ requests: [
82
+ { type: 'execute', stmt: { sql, args: args.map(toHranaArg) } },
83
+ { type: 'close' },
84
+ ],
85
+ }),
86
+ signal: AbortSignal.timeout(5000),
87
+ })
88
+
89
+ if (!resp.ok) {
90
+ const text = await resp.text().catch(() => '')
91
+ _reachable = false
92
+ _lastFailureAt = Date.now()
93
+ return { cols: [], rows: [], affectedRowCount: 0, lastInsertRowid: null, error: `HTTP ${resp.status}: ${text}` }
94
+ }
95
+
96
+ _reachable = true
97
+ const data: PipelineResult = await resp.json()
98
+ const first = data.results[0]
99
+
100
+ if (first.type === 'error') {
101
+ return { cols: [], rows: [], affectedRowCount: 0, lastInsertRowid: null, error: first.error?.message ?? 'Unknown error' }
102
+ }
103
+
104
+ const result = first.response?.result
105
+ if (!result) {
106
+ return { cols: [], rows: [], affectedRowCount: 0, lastInsertRowid: null }
107
+ }
108
+
109
+ const colNames = result.cols.map((c) => c.name)
110
+ const rows = result.rows.map((row) => {
111
+ const obj: Record<string, unknown> = {}
112
+ colNames.forEach((col, i) => {
113
+ const cell = row[i]
114
+ if (cell.type === 'null') obj[col] = null
115
+ else if (cell.type === 'integer') obj[col] = Number(cell.value)
116
+ else if (cell.type === 'float') obj[col] = Number(cell.value)
117
+ else obj[col] = cell.value
118
+ })
119
+ return obj
120
+ })
121
+
122
+ return {
123
+ cols: colNames,
124
+ rows,
125
+ affectedRowCount: result.affected_row_count,
126
+ lastInsertRowid: result.last_insert_rowid ? Number(result.last_insert_rowid) : null,
127
+ }
128
+ } catch (err: unknown) {
129
+ if (_reachable !== false) {
130
+ console.warn('[useTurso] Turso unreachable, using offline mode')
131
+ }
132
+ _reachable = false
133
+ _lastFailureAt = Date.now()
134
+ const message = err instanceof Error ? err.message : 'Unknown error'
135
+ return { cols: [], rows: [], affectedRowCount: 0, lastInsertRowid: null, error: message }
136
+ }
137
+ }
138
+
139
+ // -- Public API --
140
+
141
+ export async function query<T = Record<string, unknown>>(
142
+ sql: string,
143
+ args: ArgValue[] = [],
144
+ ): Promise<QueryResult<T>> {
145
+ const result = await pipeline(sql, args)
146
+ if (result.error) return { rows: [], error: result.error }
147
+ return { rows: result.rows as T[] }
148
+ }
149
+
150
+ export async function execute(
151
+ sql: string,
152
+ args: ArgValue[] = [],
153
+ ): Promise<{ lastInsertRowid?: number | bigint; rowsAffected: number; error?: string }> {
154
+ const result = await pipeline(sql, args)
155
+ if (result.error) return { rowsAffected: 0, error: result.error }
156
+ return {
157
+ lastInsertRowid: result.lastInsertRowid ?? undefined,
158
+ rowsAffected: result.affectedRowCount,
159
+ }
160
+ }
@@ -0,0 +1,25 @@
1
+ import { ref } from 'vue'
2
+
3
+ // TODO: Replace with your team members
4
+ export const TEAM_MEMBERS = ['Member1', 'Member2', 'Member3'] as const
5
+ export type TeamMember = (typeof TEAM_MEMBERS)[number]
6
+
7
+ const STORAGE_KEY = 'retro-user-name'
8
+
9
+ const currentUser = ref<TeamMember | null>(
10
+ (localStorage.getItem(STORAGE_KEY) as TeamMember | null) ?? null,
11
+ )
12
+
13
+ export function useUser() {
14
+ function setUser(name: TeamMember) {
15
+ currentUser.value = name
16
+ localStorage.setItem(STORAGE_KEY, name)
17
+ }
18
+
19
+ function clearUser() {
20
+ currentUser.value = null
21
+ localStorage.removeItem(STORAGE_KEY)
22
+ }
23
+
24
+ return { currentUser, setUser, clearUser, TEAM_MEMBERS }
25
+ }
@@ -0,0 +1,59 @@
1
+ import type { PageVersion } from './types'
2
+
3
+ export interface SprintConfig {
4
+ id: string
5
+ label: string
6
+ theme: string
7
+ active: boolean
8
+ }
9
+
10
+ export interface FeaturePage {
11
+ id: string
12
+ label: string
13
+ icon: string
14
+ epicMap: Record<string, string> // sprint -> epicId (policy fallback)
15
+ }
16
+
17
+ export interface PageConfig {
18
+ id: string
19
+ label: string
20
+ badge?: string
21
+ category: string
22
+ sprint: string
23
+ description?: string
24
+ /** Relative path from spec-site/src/ to the markdown file */
25
+ mdPath?: string
26
+ }
27
+
28
+ /** Sprint definitions -- TODO: Replace with your sprints */
29
+ export const sprints: SprintConfig[] = [
30
+ { id: 's1', label: 'S1', theme: '', active: true },
31
+ ]
32
+
33
+ /** Feature pages for the sidebar navigation -- TODO: Add your feature pages */
34
+ export const featurePages: FeaturePage[] = []
35
+
36
+ export function isValidFeaturePage(id: string): boolean {
37
+ return featurePages.some(p => p.id === id)
38
+ }
39
+
40
+ // -- Policy pages (epic specs per sprint) -- TODO: Add your policy pages
41
+ export const pages: PageConfig[] = []
42
+
43
+ export function getPagesByCategory(sprint: string, category: string): PageConfig[] {
44
+ return pages.filter(p => p.sprint === sprint && p.category === category)
45
+ }
46
+
47
+ export function getActiveSprint(): SprintConfig {
48
+ return sprints.find(s => s.active) ?? sprints[0]
49
+ }
50
+
51
+ /** All navigable pages (features + extras like retro) */
52
+ export const allPages = [...featurePages]
53
+
54
+ /** Epic spec file mapping: sprint -> epic files -- TODO: Add your epic spec files */
55
+ const epicSpecFiles: Record<string, Record<string, string>> = {}
56
+
57
+ export function getEpicSpecFileName(sprint: string, epicId: string): string | null {
58
+ return epicSpecFiles[sprint]?.[epicId] ?? null
59
+ }
@@ -0,0 +1,90 @@
1
+ export type Severity = 'danger' | 'warning' | 'info' | 'good' | 'opportunity'
2
+
3
+ export type ImplStatus = 'done' | 'data-ready' | 'logic-needed' | 'new-data'
4
+
5
+ export type RuleCategory =
6
+ | 'AD' | 'PROD' | 'REV' | 'SET' | 'MALL'
7
+ | 'TAC' | 'SETUP' | 'OPP' | 'KW' | 'ACT'
8
+
9
+ export interface Rule {
10
+ id: string
11
+ category: RuleCategory
12
+ name: string
13
+ condition: string
14
+ severity: Severity
15
+ homeMessage: string
16
+ action: string
17
+ dataSource: string
18
+ implStatus: ImplStatus
19
+ implNote?: string
20
+ }
21
+
22
+ export interface RuleGroup {
23
+ category: RuleCategory
24
+ label: string
25
+ icon: string
26
+ ruleCount: number
27
+ dataSource: string
28
+ rules: Rule[]
29
+ }
30
+
31
+ export interface ScenarioMockData {
32
+ greeting: string
33
+ healthStatus: 'good' | 'caution' | 'critical'
34
+ healthLabel: string
35
+ summary: {
36
+ revenue: string
37
+ revenueDelta: string
38
+ revenueDeltaDir: 'up' | 'down' | 'flat'
39
+ netProfit: string
40
+ netProfitDelta: string
41
+ netProfitDeltaDir: 'up' | 'down' | 'flat'
42
+ marginRate: string
43
+ marginRateDelta: string
44
+ marginRateDeltaDir: 'up' | 'down' | 'flat'
45
+ adCost: string
46
+ adCostDelta: string
47
+ adCostDeltaDir: 'up' | 'down' | 'flat'
48
+ }
49
+ coachingCards: CoachingCardData[]
50
+ showMallSection: boolean
51
+ showAdsSection: boolean
52
+ showGrowthCard: boolean
53
+ showExternalCta: boolean
54
+ }
55
+
56
+ export interface CoachingCardData {
57
+ severity: 'red' | 'yellow' | 'green'
58
+ severityLabel: string
59
+ title: string
60
+ action: string
61
+ effect: string
62
+ buttons: { label: string; variant: 'primary' | 'outline' }[]
63
+ }
64
+
65
+ export interface Scenario<T = ScenarioMockData> {
66
+ id: string
67
+ label: string
68
+ data: T
69
+ }
70
+
71
+ export interface SpecArea {
72
+ id: string
73
+ label: string
74
+ shortLabel: string
75
+ ruleCount: number
76
+ }
77
+
78
+ export interface VersionChange {
79
+ date: string
80
+ description: string
81
+ }
82
+
83
+ export interface PageVersion {
84
+ page: string
85
+ version: string
86
+ lastUpdated: string
87
+ sprint: string
88
+ status: 'draft' | 'review' | 'approved' | 'dev'
89
+ changelog: VersionChange[]
90
+ }
@@ -0,0 +1,25 @@
1
+ import type { Component } from 'vue'
2
+ import type { Scenario, SpecArea, PageVersion } from './types'
3
+
4
+ export interface WireframePageConfig {
5
+ id: string
6
+ mockup: Component
7
+ specPanel: Component
8
+ scenarios: Scenario<any>[]
9
+ defaultScenarioId: string
10
+ specAreas: SpecArea[]
11
+ version: PageVersion
12
+ specTitle: string
13
+ routeTitle: string
14
+ }
15
+
16
+ /** Wireframe page registry: pageId -> sprint -> config */
17
+ export const wireframePages: Record<string, Record<string, WireframePageConfig>> = {}
18
+
19
+ export function getWireframe(pageId: string, sprint: string): WireframePageConfig | null {
20
+ return wireframePages[pageId]?.[sprint] ?? null
21
+ }
22
+
23
+ export function getAvailableSprints(pageId: string): string[] {
24
+ return Object.keys(wireframePages[pageId] ?? {})
25
+ }
@@ -0,0 +1,79 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted, onUnmounted } from 'vue'
3
+ import { provideActiveSection } from '@/composables/useActiveSection'
4
+ import SpecNav from '@/components/SpecNav.vue'
5
+ import type { SpecArea } from '@/data/types'
6
+
7
+ defineProps<{
8
+ specAreas: SpecArea[]
9
+ title?: string
10
+ }>()
11
+
12
+ const { activeSection, setActiveSection } = provideActiveSection()
13
+
14
+ // Resizable divider
15
+ const leftPane = ref<HTMLElement | null>(null)
16
+ const divider = ref<HTMLElement | null>(null)
17
+ let isDragging = false
18
+ let startX = 0
19
+ let startWidth = 0
20
+
21
+ function onMouseDown(e: MouseEvent) {
22
+ if (!leftPane.value) return
23
+ isDragging = true
24
+ startX = e.clientX
25
+ startWidth = leftPane.value.offsetWidth
26
+ divider.value?.classList.add('dragging')
27
+ document.body.style.cursor = 'col-resize'
28
+ document.body.style.userSelect = 'none'
29
+ }
30
+
31
+ function onMouseMove(e: MouseEvent) {
32
+ if (!isDragging || !leftPane.value) return
33
+ const newWidth = Math.max(400, Math.min(startWidth + e.clientX - startX, window.innerWidth - 560))
34
+ leftPane.value.style.width = newWidth + 'px'
35
+ leftPane.value.style.flex = 'none'
36
+ }
37
+
38
+ function onMouseUp() {
39
+ if (!isDragging) return
40
+ isDragging = false
41
+ divider.value?.classList.remove('dragging')
42
+ document.body.style.cursor = ''
43
+ document.body.style.userSelect = ''
44
+ }
45
+
46
+ onMounted(() => {
47
+ window.addEventListener('mousemove', onMouseMove)
48
+ window.addEventListener('mouseup', onMouseUp)
49
+ })
50
+
51
+ onUnmounted(() => {
52
+ window.removeEventListener('mousemove', onMouseMove)
53
+ window.removeEventListener('mouseup', onMouseUp)
54
+ })
55
+ </script>
56
+
57
+ <template>
58
+ <div class="split-pane">
59
+ <div class="pane-left" ref="leftPane">
60
+ <slot name="mockup" />
61
+ </div>
62
+
63
+ <div class="pane-divider" ref="divider" @mousedown="onMouseDown" />
64
+
65
+ <div class="pane-right">
66
+ <div class="spec-panel-header">
67
+ <div class="spec-panel-title">{{ title ?? 'Storyboard Spec' }}</div>
68
+ <SpecNav
69
+ :areas="specAreas"
70
+ :active-id="activeSection"
71
+ @select="setActiveSection"
72
+ />
73
+ </div>
74
+ <div class="spec-panel-body" id="spec-body">
75
+ <slot name="spec" />
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </template>
@@ -0,0 +1,10 @@
1
+ import { createApp } from 'vue'
2
+ import App from './App.vue'
3
+ import router from './router'
4
+ import './styles/variables.css'
5
+ import './styles/base.css'
6
+ import './styles/split-pane.css'
7
+
8
+ const app = createApp(App)
9
+ app.use(router)
10
+ app.mount('#app')
@@ -0,0 +1,66 @@
1
+ <script setup lang="ts">
2
+ import { useRouter } from 'vue-router'
3
+ import { featurePages } from '@/data/navigation'
4
+
5
+ const router = useRouter()
6
+ </script>
7
+
8
+ <template>
9
+ <div class="index-page">
10
+ <div class="index-header">
11
+ <!-- TODO: Replace with your project name and description -->
12
+ <h1>Spec Site</h1>
13
+ <p class="index-subtitle">Interactive mockups + detailed specs</p>
14
+ </div>
15
+
16
+ <div class="feature-grid" v-if="featurePages.length > 0">
17
+ <div
18
+ v-for="feat in featurePages"
19
+ :key="feat.id"
20
+ class="feature-card"
21
+ @click="router.push(`/${feat.id}`)"
22
+ >
23
+ <div class="feature-card-header">
24
+ <span class="feature-icon">{{ feat.icon }}</span>
25
+ </div>
26
+ <div class="feature-title">{{ feat.label }}</div>
27
+ </div>
28
+ </div>
29
+
30
+ <div v-else class="empty-state">
31
+ <div class="empty-icon">📋</div>
32
+ <h2>No feature pages yet</h2>
33
+ <p>Add feature pages in <code>src/data/navigation.ts</code> and register wireframes in <code>src/data/wireframeRegistry.ts</code>.</p>
34
+ </div>
35
+ </div>
36
+ </template>
37
+
38
+ <style scoped>
39
+ .index-page { padding: 48px 40px; max-width: 900px; margin: 0 auto; }
40
+ .index-header { margin-bottom: 40px; }
41
+ h1 { font-size: 28px; font-weight: 700; margin-bottom: 8px; }
42
+ .index-subtitle { font-size: 14px; color: var(--text-secondary); }
43
+ .feature-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
44
+ .feature-card {
45
+ padding: 24px;
46
+ background: var(--card-bg);
47
+ border-radius: var(--radius);
48
+ border: 1px solid var(--border-light);
49
+ box-shadow: var(--shadow);
50
+ cursor: pointer;
51
+ transition: all 0.15s;
52
+ }
53
+ .feature-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
54
+ .feature-card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
55
+ .feature-icon { font-size: 28px; }
56
+ .feature-title { font-size: 18px; font-weight: 700; margin-bottom: 8px; }
57
+
58
+ .empty-state {
59
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
60
+ padding: 80px 40px; text-align: center; color: var(--text-muted);
61
+ }
62
+ .empty-icon { font-size: 64px; margin-bottom: 16px; opacity: 0.3; }
63
+ .empty-state h2 { font-size: 24px; color: var(--text-primary); margin-bottom: 8px; }
64
+ .empty-state p { font-size: 14px; line-height: 1.6; max-width: 480px; }
65
+ .empty-state code { background: var(--border-light); padding: 2px 6px; border-radius: 4px; font-size: 12px; }
66
+ </style>