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,145 @@
1
+ <script setup lang="ts">
2
+ import { ref, watchEffect, computed } from 'vue'
3
+ import { sprints, getPagesByCategory, getEpicSpecFileName } from '@/data/navigation'
4
+ import { renderMarkdown } from '@/utils/markdown'
5
+
6
+ const props = defineProps<{
7
+ sprint: string
8
+ epicId: string
9
+ pageLabel: string
10
+ }>()
11
+
12
+ const sprintLabel = computed(() => sprints.find(s => s.id === props.sprint)?.label ?? props.sprint)
13
+ const epicConfig = computed(() => {
14
+ const epics = getPagesByCategory(props.sprint, 'policy')
15
+ return epics.find(e => e.id === props.epicId)
16
+ })
17
+
18
+ const markdownHtml = ref('')
19
+ const loading = ref(true)
20
+ const error = ref(false)
21
+
22
+ const mdModules = import.meta.glob(
23
+ '../../../../.context/sprints/*/epic-specs/*.md',
24
+ { query: '?raw', import: 'default' }
25
+ )
26
+
27
+ watchEffect(async () => {
28
+ loading.value = true
29
+ error.value = false
30
+ markdownHtml.value = ''
31
+
32
+ const fileName = getEpicSpecFileName(props.sprint, props.epicId)
33
+ if (!fileName) {
34
+ error.value = true
35
+ loading.value = false
36
+ return
37
+ }
38
+
39
+ const key = `../../../../.context/sprints/${props.sprint}/epic-specs/${fileName}`
40
+ const loader = mdModules[key]
41
+
42
+ if (!loader) {
43
+ error.value = true
44
+ loading.value = false
45
+ return
46
+ }
47
+
48
+ try {
49
+ const raw = (await loader()) as string
50
+ markdownHtml.value = renderMarkdown(raw)
51
+ } catch {
52
+ error.value = true
53
+ }
54
+ loading.value = false
55
+ })
56
+ </script>
57
+
58
+ <template>
59
+ <div class="policy-fallback">
60
+ <div class="fallback-banner">
61
+ <span class="banner-icon">📄</span>
62
+ <span>No wireframe spec for <strong>{{ pageLabel }}</strong> in {{ sprintLabel }}. Showing policy document ({{ epicId }}) instead.</span>
63
+ </div>
64
+
65
+ <div class="fallback-content">
66
+ <div v-if="loading" class="fallback-loading">Loading...</div>
67
+ <div v-else-if="error" class="fallback-error">
68
+ <p>Policy document not found.</p>
69
+ </div>
70
+ <article v-else class="markdown-body" v-html="markdownHtml"></article>
71
+ </div>
72
+ </div>
73
+ </template>
74
+
75
+ <style scoped>
76
+ .policy-fallback {
77
+ height: calc(100vh - var(--header-height));
78
+ overflow-y: auto;
79
+ background: var(--bg);
80
+ }
81
+
82
+ .fallback-banner {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 8px;
86
+ padding: 10px 24px;
87
+ background: var(--blue-bg);
88
+ border-bottom: 1px solid var(--border-light);
89
+ font-size: 13px;
90
+ color: var(--text-secondary);
91
+ }
92
+ .banner-icon { font-size: 16px; }
93
+
94
+ .fallback-content {
95
+ max-width: 800px;
96
+ margin: 0 auto;
97
+ padding: 32px 40px;
98
+ }
99
+
100
+ .fallback-loading,
101
+ .fallback-error {
102
+ display: flex;
103
+ justify-content: center;
104
+ align-items: center;
105
+ height: 200px;
106
+ color: var(--text-muted);
107
+ font-size: 14px;
108
+ }
109
+
110
+ /* ---- Markdown styles ---- */
111
+ .markdown-body {
112
+ font-size: 14px;
113
+ line-height: 1.7;
114
+ color: var(--text-primary);
115
+ }
116
+ .markdown-body :deep(h1) { font-size: 24px; font-weight: 700; margin: 0 0 16px; padding-bottom: 8px; border-bottom: 2px solid var(--border); }
117
+ .markdown-body :deep(h2) { font-size: 19px; font-weight: 700; margin: 32px 0 12px; }
118
+ .markdown-body :deep(h3) { font-size: 15px; font-weight: 700; margin: 24px 0 8px; }
119
+ .markdown-body :deep(h4) { font-size: 14px; font-weight: 700; margin: 20px 0 6px; color: var(--text-secondary); }
120
+ .markdown-body :deep(p) { margin: 0 0 10px; }
121
+ .markdown-body :deep(strong) { font-weight: 700; }
122
+ .markdown-body :deep(code) {
123
+ font-size: 12px; background: var(--border-light); padding: 2px 5px;
124
+ border-radius: 3px; font-family: 'SF Mono', 'Menlo', monospace;
125
+ }
126
+ .markdown-body :deep(pre) {
127
+ background: var(--text-primary); color: #e5e7eb; padding: 16px;
128
+ border-radius: var(--radius-sm); overflow-x: auto; margin: 12px 0;
129
+ font-size: 12px; line-height: 1.6;
130
+ }
131
+ .markdown-body :deep(pre code) { background: none; padding: 0; color: inherit; }
132
+ .markdown-body :deep(ul), .markdown-body :deep(ol) { margin: 8px 0; padding-left: 24px; }
133
+ .markdown-body :deep(li) { margin: 4px 0; }
134
+ .markdown-body :deep(blockquote) {
135
+ border-left: 3px solid var(--primary); padding: 8px 16px; margin: 12px 0;
136
+ background: var(--primary-light); color: var(--text-secondary);
137
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
138
+ }
139
+ .markdown-body :deep(hr) { border: none; border-top: 1px solid var(--border); margin: 24px 0; }
140
+ .markdown-body :deep(table) { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; }
141
+ .markdown-body :deep(th) { text-align: left; padding: 8px 12px; background: var(--border-light); font-weight: 700; border: 1px solid var(--border); }
142
+ .markdown-body :deep(td) { padding: 8px 12px; border: 1px solid var(--border); }
143
+ .markdown-body :deep(a) { color: var(--primary); text-decoration: none; }
144
+ .markdown-body :deep(a:hover) { text-decoration: underline; }
145
+ </style>
@@ -0,0 +1,151 @@
1
+ <script setup lang="ts">
2
+ import { computed, watch, onMounted, ref } from 'vue'
3
+ import { useRoute } from 'vue-router'
4
+ import SplitPaneLayout from '@/layouts/SplitPaneLayout.vue'
5
+ import ScenarioSwitcher from '@/components/ScenarioSwitcher.vue'
6
+ import VersionBadge from '@/components/VersionBadge.vue'
7
+ import MockupShell from '@/components/MockupShell.vue'
8
+ import PolicyFallback from '@/pages/shared/PolicyFallback.vue'
9
+ import NoContentPlaceholder from '@/pages/shared/NoContentPlaceholder.vue'
10
+ import { provideScenario } from '@/composables/useScenario'
11
+ import { useScenarioStore } from '@/composables/useScenarioStore'
12
+ import { useUser } from '@/composables/useUser'
13
+ import { getWireframe } from '@/data/wireframeRegistry'
14
+ import { featurePages } from '@/data/navigation'
15
+ import type { Scenario } from '@/data/types'
16
+
17
+ const route = useRoute()
18
+ const { currentUser } = useUser()
19
+
20
+ const pageId = computed(() => (route.params.pageId as string) || '')
21
+ const sprint = computed(() => (route.params.sprint as string) || '')
22
+
23
+ const wireframeConfig = computed(() => getWireframe(pageId.value, sprint.value))
24
+ const hasWireframe = computed(() => !!wireframeConfig.value)
25
+
26
+ const featurePage = computed(() => featurePages.find(p => p.id === pageId.value))
27
+
28
+ const fallbackEpicId = computed(() => {
29
+ if (hasWireframe.value) return null
30
+ return featurePage.value?.epicMap[sprint.value] ?? null
31
+ })
32
+
33
+ const renderMode = computed<'wireframe' | 'policy' | 'empty'>(() => {
34
+ if (hasWireframe.value) return 'wireframe'
35
+ if (fallbackEpicId.value) return 'policy'
36
+ return 'empty'
37
+ })
38
+
39
+ // Turso custom scenario store
40
+ const store = useScenarioStore(pageId.value, sprint.value)
41
+ const storeRef = ref(store)
42
+
43
+ const initialConfig = wireframeConfig.value
44
+ const { scenarios, activeScenarioId, setScenario, updateScenarios } = provideScenario(
45
+ initialConfig?.scenarios ?? [],
46
+ initialConfig?.defaultScenarioId ?? '',
47
+ )
48
+
49
+ async function mergeCustomScenarios() {
50
+ const config = getWireframe(pageId.value, sprint.value)
51
+ const baseScenarios: Scenario<any>[] = config?.scenarios ?? []
52
+ const custom = await storeRef.value.loadCustomScenarios()
53
+ if (storeRef.value.error) {
54
+ console.warn('[WireframeShell] custom scenario sync failed:', storeRef.value.error)
55
+ }
56
+ updateScenarios([...baseScenarios, ...custom], config?.defaultScenarioId ?? '')
57
+ }
58
+
59
+ async function handleDuplicate(sourceId: string) {
60
+ const source = scenarios.value.find(s => s.id === sourceId)
61
+ if (!source) return
62
+ const label = `${source.label} (copy)`
63
+ const newId = await storeRef.value.duplicateScenario(source, label, currentUser.value ?? 'unknown')
64
+ if (!newId) return
65
+ await mergeCustomScenarios()
66
+ }
67
+
68
+ async function handleDeleteCustom(scenarioId: string) {
69
+ const ok = await storeRef.value.deleteScenario(scenarioId)
70
+ if (!ok) return
71
+ await mergeCustomScenarios()
72
+ }
73
+
74
+ watch([pageId, sprint], () => {
75
+ const newStore = useScenarioStore(pageId.value, sprint.value)
76
+ storeRef.value = newStore
77
+ mergeCustomScenarios()
78
+ })
79
+
80
+ onMounted(() => {
81
+ mergeCustomScenarios()
82
+ })
83
+
84
+ const showScenarioSwitcher = computed(() =>
85
+ hasWireframe.value && (wireframeConfig.value?.scenarios.length ?? 0) > 0
86
+ )
87
+ </script>
88
+
89
+ <template>
90
+ <div class="wireframe-shell">
91
+ <!-- Wireframe mode -->
92
+ <SplitPaneLayout
93
+ v-if="renderMode === 'wireframe'"
94
+ :spec-areas="wireframeConfig!.specAreas"
95
+ :title="wireframeConfig!.specTitle"
96
+ >
97
+ <template #mockup>
98
+ <div v-if="showScenarioSwitcher" class="mockup-toolbar">
99
+ <ScenarioSwitcher
100
+ :scenarios="scenarios"
101
+ :active-id="activeScenarioId"
102
+ @change="setScenario"
103
+ @duplicate="handleDuplicate"
104
+ @delete-custom="handleDeleteCustom"
105
+ />
106
+ <div class="mockup-version">
107
+ <VersionBadge :version="wireframeConfig!.version" />
108
+ </div>
109
+ </div>
110
+ <MockupShell>
111
+ <component :is="wireframeConfig!.mockup" :key="`${pageId}-${sprint}`" />
112
+ </MockupShell>
113
+ </template>
114
+
115
+ <template #spec>
116
+ <component :is="wireframeConfig!.specPanel" :key="`${pageId}-${sprint}`" />
117
+ </template>
118
+ </SplitPaneLayout>
119
+
120
+ <!-- Policy fallback -->
121
+ <PolicyFallback
122
+ v-else-if="renderMode === 'policy'"
123
+ :sprint="sprint"
124
+ :epic-id="fallbackEpicId!"
125
+ :page-label="featurePage?.label ?? pageId"
126
+ />
127
+
128
+ <!-- No content -->
129
+ <NoContentPlaceholder
130
+ v-else
131
+ :page-id="pageId"
132
+ :sprint="sprint"
133
+ />
134
+ </div>
135
+ </template>
136
+
137
+ <style scoped>
138
+ .wireframe-shell { height: 100%; }
139
+ .mockup-toolbar {
140
+ position: sticky;
141
+ top: 0;
142
+ z-index: 100;
143
+ }
144
+ .mockup-version {
145
+ background: #fff;
146
+ padding: 6px 16px;
147
+ border-bottom: 1px solid var(--border);
148
+ display: flex;
149
+ justify-content: flex-end;
150
+ }
151
+ </style>
@@ -0,0 +1,85 @@
1
+ import { createRouter, createWebHistory } from 'vue-router'
2
+ import { isValidFeaturePage, getActiveSprint, featurePages } from './data/navigation'
3
+ import { getWireframe, getAvailableSprints } from './data/wireframeRegistry'
4
+
5
+ const activeSprint = getActiveSprint().id
6
+
7
+ const routes = [
8
+ { path: '/', component: () => import('./pages/IndexPage.vue') },
9
+
10
+ // -- Policy documents --
11
+ {
12
+ path: '/policy',
13
+ redirect: `/policy/${activeSprint}`,
14
+ },
15
+ {
16
+ path: '/policy/:sprint',
17
+ component: () => import('./pages/PolicyIndex.vue'),
18
+ meta: { title: 'Policy' },
19
+ },
20
+ {
21
+ path: '/policy/:sprint/:epicId',
22
+ component: () => import('./pages/PolicyDetail.vue'),
23
+ meta: { title: 'Policy' },
24
+ },
25
+
26
+ // -- Retro (Turso-based team collaboration) --
27
+ {
28
+ path: '/retro',
29
+ redirect: `/retro/${activeSprint}`,
30
+ },
31
+ {
32
+ path: '/retro/:sprint',
33
+ component: () => import('./pages/retro/RetroPage.vue'),
34
+ meta: { title: 'Retro' },
35
+ },
36
+
37
+ // -- Feature pages (wireframe shell) --
38
+ {
39
+ path: '/:pageId',
40
+ redirect: (to: any) => {
41
+ const id = to.params.pageId as string
42
+ if (!isValidFeaturePage(id)) return '/'
43
+ const sprints = getAvailableSprints(id)
44
+ const best = sprints.includes(activeSprint) ? activeSprint : sprints[0] ?? activeSprint
45
+ return `/${id}/${best}`
46
+ },
47
+ },
48
+ {
49
+ path: '/:pageId/:sprint',
50
+ component: () => import('./pages/wireframe/WireframeShell.vue'),
51
+ beforeEnter: (to: any) => {
52
+ if (!isValidFeaturePage(to.params.pageId as string)) return '/'
53
+ },
54
+ },
55
+
56
+ // Catch-all
57
+ {
58
+ path: '/:pathMatch(.*)*',
59
+ redirect: '/',
60
+ },
61
+ ]
62
+
63
+ const router = createRouter({
64
+ history: createWebHistory(),
65
+ routes,
66
+ })
67
+
68
+ router.afterEach((to) => {
69
+ const pageId = to.params.pageId as string
70
+ const sprint = to.params.sprint as string
71
+
72
+ if (pageId && sprint) {
73
+ const config = getWireframe(pageId, sprint)
74
+ if (config) {
75
+ document.title = config.routeTitle ?? 'Spec Site'
76
+ } else {
77
+ const fp = featurePages.find(p => p.id === pageId)
78
+ document.title = fp ? `${fp.label} — Spec Site` : 'Spec Site'
79
+ }
80
+ } else {
81
+ document.title = (to.meta.title as string) ?? 'Spec Site'
82
+ }
83
+ })
84
+
85
+ export default router
@@ -0,0 +1,21 @@
1
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2
+
3
+ body {
4
+ font-family: var(--font-kr);
5
+ background: var(--bg);
6
+ color: var(--text-primary);
7
+ -webkit-font-smoothing: antialiased;
8
+ }
9
+
10
+ a { color: inherit; text-decoration: none; }
11
+
12
+ button {
13
+ font-family: var(--font-kr);
14
+ cursor: pointer;
15
+ }
16
+
17
+ /* Scrollbar */
18
+ ::-webkit-scrollbar { width: 6px; }
19
+ ::-webkit-scrollbar-track { background: transparent; }
20
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
21
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
@@ -0,0 +1,143 @@
1
+ /* ========== Split-Pane Layout ========== */
2
+ .split-pane {
3
+ display: flex;
4
+ height: calc(100vh - var(--header-height));
5
+ overflow: hidden;
6
+ width: 100%;
7
+ }
8
+
9
+ .pane-left {
10
+ width: 55%;
11
+ min-width: 400px;
12
+ overflow-y: auto;
13
+ overflow-x: hidden;
14
+ position: relative;
15
+ flex-shrink: 0;
16
+ }
17
+
18
+ .pane-divider {
19
+ width: 6px;
20
+ background: var(--border);
21
+ cursor: col-resize;
22
+ flex-shrink: 0;
23
+ position: relative;
24
+ z-index: 200;
25
+ transition: background 0.15s;
26
+ }
27
+ .pane-divider:hover,
28
+ .pane-divider.dragging { background: var(--primary); }
29
+ .pane-divider::after {
30
+ content: '\22EE';
31
+ position: absolute;
32
+ top: 50%;
33
+ left: 50%;
34
+ transform: translate(-50%, -50%);
35
+ color: var(--text-muted);
36
+ font-size: 14px;
37
+ pointer-events: none;
38
+ }
39
+
40
+ .pane-right {
41
+ flex: 1;
42
+ min-width: 360px;
43
+ overflow-y: auto;
44
+ background: #fff;
45
+ display: flex;
46
+ flex-direction: column;
47
+ }
48
+
49
+ /* ========== Area Highlight ========== */
50
+ [data-area] {
51
+ position: relative;
52
+ cursor: pointer;
53
+ transition: outline 0.2s, box-shadow 0.2s;
54
+ border-radius: 2px;
55
+ }
56
+ [data-area]:hover {
57
+ outline: 2px dashed var(--primary);
58
+ outline-offset: -2px;
59
+ }
60
+ [data-area].area-active {
61
+ outline: 3px solid var(--primary);
62
+ outline-offset: -3px;
63
+ box-shadow: inset 0 0 0 9999px rgba(0, 85, 212, 0.04);
64
+ }
65
+ [data-area]::before {
66
+ content: attr(data-area-label);
67
+ position: absolute;
68
+ top: 4px;
69
+ right: 4px;
70
+ background: var(--primary);
71
+ color: #fff;
72
+ font-size: 10px;
73
+ font-weight: 700;
74
+ padding: 2px 8px;
75
+ border-radius: 4px;
76
+ opacity: 0;
77
+ transition: opacity 0.15s;
78
+ pointer-events: none;
79
+ z-index: 10;
80
+ letter-spacing: 0.3px;
81
+ }
82
+ [data-area]:hover::before,
83
+ [data-area].area-active::before { opacity: 1; }
84
+
85
+ /* ========== Spec Panel ========== */
86
+ .spec-panel-header {
87
+ position: sticky;
88
+ top: 0;
89
+ z-index: 50;
90
+ background: #fff;
91
+ border-bottom: 2px solid var(--border);
92
+ padding: 12px 16px;
93
+ }
94
+ .spec-panel-title {
95
+ font-size: 15px;
96
+ font-weight: 700;
97
+ margin-bottom: 10px;
98
+ color: var(--text-primary);
99
+ }
100
+ .spec-nav {
101
+ display: flex;
102
+ flex-wrap: wrap;
103
+ gap: 4px;
104
+ }
105
+ .spec-nav-btn {
106
+ padding: 4px 10px;
107
+ border-radius: 6px;
108
+ border: 1px solid var(--border);
109
+ background: #fff;
110
+ font-size: 11px;
111
+ font-family: var(--font-kr);
112
+ cursor: pointer;
113
+ transition: all 0.15s;
114
+ font-weight: 600;
115
+ color: var(--text-secondary);
116
+ }
117
+ .spec-nav-btn:hover { background: var(--bg); }
118
+ .spec-nav-btn.active {
119
+ background: var(--primary);
120
+ color: #fff;
121
+ border-color: var(--primary);
122
+ }
123
+ .spec-panel-body {
124
+ flex: 1;
125
+ overflow-y: auto;
126
+ }
127
+ .spec-placeholder {
128
+ display: flex;
129
+ flex-direction: column;
130
+ align-items: center;
131
+ justify-content: center;
132
+ height: 400px;
133
+ color: var(--text-muted);
134
+ font-size: 14px;
135
+ text-align: center;
136
+ line-height: 1.8;
137
+ padding: 40px;
138
+ }
139
+ .spec-placeholder .placeholder-icon {
140
+ font-size: 48px;
141
+ margin-bottom: 16px;
142
+ opacity: 0.3;
143
+ }
@@ -0,0 +1,47 @@
1
+ :root {
2
+ /* Brand */
3
+ --primary: #0055d4;
4
+ --primary-light: #e8f0fe;
5
+ --primary-dark: #003d9e;
6
+
7
+ /* Background */
8
+ --bg: #F8F8FB;
9
+ --card-bg: #ffffff;
10
+
11
+ /* Text */
12
+ --text-primary: #1f2123;
13
+ --text-secondary: #6b7280;
14
+ --text-muted: #9ca3af;
15
+
16
+ /* Border */
17
+ --border: #e5e7eb;
18
+ --border-light: #f3f4f6;
19
+
20
+ /* Severity colors */
21
+ --red: #E53E3E;
22
+ --red-bg: #FEF2F2;
23
+ --red-border: #FECACA;
24
+ --yellow: #D69E2E;
25
+ --yellow-bg: #FFFBEB;
26
+ --yellow-border: #FDE68A;
27
+ --green: #38A169;
28
+ --green-bg: #F0FFF4;
29
+ --green-border: #C6F6D5;
30
+ --blue: #3B82F6;
31
+ --blue-bg: #EFF6FF;
32
+ --blue-border: #BFDBFE;
33
+ --negative: #fc4747;
34
+
35
+ /* Layout */
36
+ --header-height: 48px;
37
+ --radius: 12px;
38
+ --radius-sm: 8px;
39
+
40
+ /* Shadow */
41
+ --shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
42
+ --shadow-md: 0 4px 6px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.04);
43
+
44
+ /* Font */
45
+ --font-kr: 'Noto Sans KR', sans-serif;
46
+ --font-num: 'Roboto', 'Noto Sans KR', sans-serif;
47
+ }