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.
- package/README.md +372 -0
- package/adapters/claude-code/.claude/commands/_domain.md.hbs +32 -0
- package/adapters/claude-code/.claude/commands/analytics.md.hbs +55 -0
- package/adapters/claude-code/.claude/commands/daily.md.hbs +301 -0
- package/adapters/claude-code/.claude/commands/dev.md.hbs +62 -0
- package/adapters/claude-code/.claude/commands/handoff.md +258 -0
- package/adapters/claude-code/.claude/commands/market.md +120 -0
- package/adapters/claude-code/.claude/commands/metrics.md +123 -0
- package/adapters/claude-code/.claude/commands/oscar-loop.md +436 -0
- package/adapters/claude-code/.claude/commands/party.md +85 -0
- package/adapters/claude-code/.claude/commands/plan.md +43 -0
- package/adapters/claude-code/.claude/commands/research.md +203 -0
- package/adapters/claude-code/.claude/commands/retro.md +68 -0
- package/adapters/claude-code/.claude/commands/save.md +440 -0
- package/adapters/claude-code/.claude/commands/sessions.md +139 -0
- package/adapters/claude-code/.claude/commands/sprint.md +106 -0
- package/adapters/claude-code/.claude/commands/start.md +368 -0
- package/adapters/claude-code/.claude/commands/strategy.md +41 -0
- package/adapters/claude-code/.claude/commands/task.md +220 -0
- package/adapters/claude-code/.claude/commands/tracking.md +116 -0
- package/adapters/claude-code/.claude/commands/validate.md +58 -0
- package/adapters/claude-code/CLAUDE.md.hbs +208 -0
- package/adapters/claude-code/manifest.yaml +36 -0
- package/bin/cli.mjs +218 -0
- package/lib/adapter.mjs +68 -0
- package/lib/doctor.mjs +161 -0
- package/lib/hydrate.mjs +421 -0
- package/lib/prompt.mjs +78 -0
- package/lib/scaffold.mjs +155 -0
- package/lib/setup-wizard.mjs +331 -0
- package/lib/template-engine.mjs +164 -0
- package/lib/yaml-lite.mjs +476 -0
- package/package.json +30 -0
- package/scaffold/.context/.secrets.yaml.example +20 -0
- package/scaffold/.context/WORKFLOW.md.hbs +332 -0
- package/scaffold/.context/agents/TEMPLATE.md +115 -0
- package/scaffold/.context/agents/analyst.md.hbs +362 -0
- package/scaffold/.context/agents/developer.md.hbs +390 -0
- package/scaffold/.context/agents/handoff-specialist.md.hbs +292 -0
- package/scaffold/.context/agents/market-researcher.md.hbs +288 -0
- package/scaffold/.context/agents/ollie.md +323 -0
- package/scaffold/.context/agents/operations.md.hbs +293 -0
- package/scaffold/.context/agents/orchestrator.md.hbs +434 -0
- package/scaffold/.context/agents/planner.md.hbs +405 -0
- package/scaffold/.context/agents/qa.md.hbs +409 -0
- package/scaffold/.context/agents/researcher.md.hbs +330 -0
- package/scaffold/.context/agents/sage.md +349 -0
- package/scaffold/.context/agents/strategist.md.hbs +339 -0
- package/scaffold/.context/agents/tracking-governor.md.hbs +291 -0
- package/scaffold/.context/agents/validator.md.hbs +365 -0
- package/scaffold/.context/integrations/_registry.yaml +38 -0
- package/scaffold/.context/integrations/providers/channel_io.yaml +38 -0
- package/scaffold/.context/integrations/providers/corti.yaml +203 -0
- package/scaffold/.context/integrations/providers/ga4.yaml +116 -0
- package/scaffold/.context/integrations/providers/intercom.yaml +47 -0
- package/scaffold/.context/integrations/providers/linear.yaml +46 -0
- package/scaffold/.context/integrations/providers/mixpanel.yaml +73 -0
- package/scaffold/.context/integrations/providers/notebooklm.yaml +74 -0
- package/scaffold/.context/integrations/providers/notion.yaml +129 -0
- package/scaffold/.context/integrations/providers/prod_db.yaml +183 -0
- package/scaffold/.context/oscar/workflows/multi-agent.md +82 -0
- package/scaffold/.context/oscar/workflows/ollie-sage.md +128 -0
- package/scaffold/.context/oscar/workflows/session-git.md +71 -0
- package/scaffold/.context/oscar/workflows/setup.md +663 -0
- package/scaffold/.context/oscar/workflows/tracking.md +118 -0
- package/scaffold/.context/project.yaml.example +102 -0
- package/scaffold/.context/templates/dev-guide.md +217 -0
- package/scaffold/.context/templates/epic-spec.md +225 -0
- package/scaffold/.context/templates/guardrail.md +94 -0
- package/scaffold/.context/templates/handoff-checklist.md +197 -0
- package/scaffold/.context/templates/prd.md +80 -0
- package/scaffold/.context/templates/retrospective.md +78 -0
- package/scaffold/.context/templates/screen-spec.md +714 -0
- package/scaffold/.context/templates/sprint-plan.md +72 -0
- package/scaffold/.context/templates/sprint-status.yaml +109 -0
- package/scaffold/.context/templates/story-v2.md +228 -0
- package/scaffold/.context/templates/validation-report.md +99 -0
- package/scaffold/.gitignore.append +7 -0
- package/scaffold/spec-site/env.d.ts +7 -0
- package/scaffold/spec-site/index.html +14 -0
- package/scaffold/spec-site/package.json +20 -0
- package/scaffold/spec-site/src/App.vue +27 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_ads.svg +10 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_ads_on.svg +10 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_board.svg +14 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_board_on.svg +14 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard.svg +21 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard_on.svg +21 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_pricing.svg +20 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_pricing_on.svg +20 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_store.svg +11 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_store_on.svg +11 -0
- package/scaffold/spec-site/src/components/Accordion.vue +108 -0
- package/scaffold/spec-site/src/components/AppHeader.vue +304 -0
- package/scaffold/spec-site/src/components/Badge.vue +25 -0
- package/scaffold/spec-site/src/components/CoachingCard.vue +112 -0
- package/scaffold/spec-site/src/components/MemoSidebar.vue +239 -0
- package/scaffold/spec-site/src/components/MockupShell.vue +100 -0
- package/scaffold/spec-site/src/components/RuleTable.vue +99 -0
- package/scaffold/spec-site/src/components/ScenarioSwitcher.vue +103 -0
- package/scaffold/spec-site/src/components/SpecNav.vue +26 -0
- package/scaffold/spec-site/src/components/SpecSection.vue +59 -0
- package/scaffold/spec-site/src/components/SummaryGrid.vue +39 -0
- package/scaffold/spec-site/src/components/VersionBadge.vue +38 -0
- package/scaffold/spec-site/src/composables/useActiveSection.ts +53 -0
- package/scaffold/spec-site/src/composables/useMemo.ts +138 -0
- package/scaffold/spec-site/src/composables/useRetro.ts +313 -0
- package/scaffold/spec-site/src/composables/useScenario.ts +43 -0
- package/scaffold/spec-site/src/composables/useScenarioStore.ts +102 -0
- package/scaffold/spec-site/src/composables/useTurso.ts +160 -0
- package/scaffold/spec-site/src/composables/useUser.ts +25 -0
- package/scaffold/spec-site/src/data/navigation.ts +59 -0
- package/scaffold/spec-site/src/data/types.ts +90 -0
- package/scaffold/spec-site/src/data/wireframeRegistry.ts +25 -0
- package/scaffold/spec-site/src/layouts/SplitPaneLayout.vue +79 -0
- package/scaffold/spec-site/src/main.ts +10 -0
- package/scaffold/spec-site/src/pages/IndexPage.vue +66 -0
- package/scaffold/spec-site/src/pages/PolicyDetail.vue +215 -0
- package/scaffold/spec-site/src/pages/PolicyIndex.vue +74 -0
- package/scaffold/spec-site/src/pages/retro/RetroActions.vue +191 -0
- package/scaffold/spec-site/src/pages/retro/RetroBoard.vue +192 -0
- package/scaffold/spec-site/src/pages/retro/RetroCard.vue +131 -0
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +287 -0
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +178 -0
- package/scaffold/spec-site/src/pages/shared/NoContentPlaceholder.vue +34 -0
- package/scaffold/spec-site/src/pages/shared/PlaceholderContent.vue +22 -0
- package/scaffold/spec-site/src/pages/shared/PlaceholderSpecPanel.vue +16 -0
- package/scaffold/spec-site/src/pages/shared/PolicyFallback.vue +145 -0
- package/scaffold/spec-site/src/pages/wireframe/WireframeShell.vue +151 -0
- package/scaffold/spec-site/src/router.ts +85 -0
- package/scaffold/spec-site/src/styles/base.css +21 -0
- package/scaffold/spec-site/src/styles/split-pane.css +143 -0
- package/scaffold/spec-site/src/styles/variables.css +47 -0
- package/scaffold/spec-site/src/utils/markdown.ts +197 -0
- package/scaffold/spec-site/tsconfig.json +20 -0
- 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>
|