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,239 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch } from 'vue'
3
+ import { useRoute } from 'vue-router'
4
+ import { useMemo } from '@/composables/useMemo'
5
+ import { useUser } from '@/composables/useUser'
6
+
7
+ const route = useRoute()
8
+ const pageId = computed(() => (route.params.pageId as string) || 'global')
9
+ const { currentUser } = useUser()
10
+
11
+ const memoStore = ref(useMemo(pageId.value))
12
+
13
+ watch(pageId, (newId) => {
14
+ memoStore.value = useMemo(newId)
15
+ })
16
+
17
+ const memoOpen = ref(false)
18
+ const newMemo = ref('')
19
+ const isComposing = ref(false)
20
+
21
+ function openMemo() {
22
+ memoOpen.value = true
23
+ memoStore.value.loadMemos()
24
+ }
25
+
26
+ async function handleAdd() {
27
+ if (isComposing.value) return
28
+ const text = newMemo.value.trim()
29
+ if (!text) return
30
+ await memoStore.value.addMemo(text, currentUser.value ?? '')
31
+ newMemo.value = ''
32
+ }
33
+
34
+ function handleClearAll() {
35
+ if (confirm('Delete all memos?')) {
36
+ memoStore.value.clearAll()
37
+ }
38
+ }
39
+ </script>
40
+
41
+ <template>
42
+ <!-- Tab -->
43
+ <div class="memo-tab" :class="{ hidden: memoOpen }" @click="openMemo">
44
+ <span class="memo-tab-icon">&#9998;</span>
45
+ <span class="memo-tab-label">Memo</span>
46
+ <span v-if="memoStore.memoCount > 0" class="memo-tab-count">
47
+ {{ memoStore.memoCount }}
48
+ </span>
49
+ </div>
50
+
51
+ <!-- Sidebar -->
52
+ <Teleport to="body">
53
+ <Transition name="memo-slide">
54
+ <div v-if="memoOpen" class="memo-sidebar">
55
+ <div class="memo-header">
56
+ <span class="memo-title">{{ pageId }} memo</span>
57
+ <button class="memo-close" @click="memoOpen = false">&times;</button>
58
+ </div>
59
+ <div class="memo-body">
60
+ <div class="memo-input-area">
61
+ <textarea
62
+ v-model="newMemo"
63
+ class="memo-textarea"
64
+ placeholder="Write a memo... (Enter to save, Shift+Enter for newline)"
65
+ rows="3"
66
+ @compositionstart="isComposing = true"
67
+ @compositionend="isComposing = false"
68
+ @keydown.enter.exact.prevent="handleAdd"
69
+ ></textarea>
70
+ <button class="memo-add-btn" @click="handleAdd" :disabled="!newMemo.trim()">Save</button>
71
+ </div>
72
+ <div class="memo-list">
73
+ <div v-if="memoStore.error" class="memo-error">
74
+ Sync issue: {{ memoStore.error }}
75
+ </div>
76
+ <div v-for="m in memoStore.memos" :key="m.id" class="memo-item">
77
+ <div class="memo-item-header">
78
+ <span class="memo-item-meta">
79
+ <span v-if="m.author" class="memo-item-author">{{ m.author }}</span>
80
+ <span class="memo-item-time">{{ memoStore.formatTime(m.ts) }}</span>
81
+ </span>
82
+ <button class="memo-item-del" @click="memoStore.deleteMemo(m.id)" title="Delete">&times;</button>
83
+ </div>
84
+ <div class="memo-item-text">{{ m.text }}</div>
85
+ </div>
86
+ <div v-if="memoStore.memos.length === 0" class="memo-empty">
87
+ No memos yet.<br>Record thoughts while reviewing specs.
88
+ </div>
89
+ </div>
90
+ </div>
91
+ <div class="memo-footer">
92
+ <span class="memo-footer-info">Team shared memos</span>
93
+ <button v-if="memoStore.memos.length > 0" class="memo-clear-btn" @click="handleClearAll">Clear all</button>
94
+ </div>
95
+ </div>
96
+ </Transition>
97
+ </Teleport>
98
+ </template>
99
+
100
+ <style scoped>
101
+ .memo-tab {
102
+ position: fixed;
103
+ right: 0;
104
+ top: 50%;
105
+ transform: translateY(-50%);
106
+ z-index: 999;
107
+ display: flex;
108
+ flex-direction: column;
109
+ align-items: center;
110
+ gap: 2px;
111
+ padding: 10px 6px;
112
+ background: #1e293b;
113
+ color: #fff;
114
+ border-radius: 8px 0 0 8px;
115
+ cursor: pointer;
116
+ transition: all 0.2s;
117
+ box-shadow: -2px 0 8px rgba(0,0,0,0.15);
118
+ writing-mode: vertical-rl;
119
+ }
120
+ .memo-tab:hover { background: #334155; padding-right: 10px; }
121
+ .memo-tab.hidden { display: none; }
122
+ .memo-tab-icon { font-size: 16px; writing-mode: horizontal-tb; }
123
+ .memo-tab-label { font-size: 12px; font-weight: 600; letter-spacing: 1px; }
124
+ .memo-tab-count {
125
+ writing-mode: horizontal-tb;
126
+ background: #ef4444;
127
+ color: #fff;
128
+ font-size: 10px;
129
+ font-weight: 700;
130
+ min-width: 18px;
131
+ height: 18px;
132
+ border-radius: 9px;
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ }
137
+ </style>
138
+
139
+ <style>
140
+ /* Teleported -- unscoped */
141
+ .memo-sidebar {
142
+ position: fixed;
143
+ right: 0;
144
+ top: 0;
145
+ bottom: 0;
146
+ width: 360px;
147
+ max-width: 90vw;
148
+ background: #fff;
149
+ z-index: 9999;
150
+ display: flex;
151
+ flex-direction: column;
152
+ box-shadow: -4px 0 24px rgba(0,0,0,0.15);
153
+ pointer-events: auto;
154
+ }
155
+ .memo-header {
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: space-between;
159
+ padding: 16px 20px;
160
+ border-bottom: 1px solid #e2e8f0;
161
+ background: #f8fafc;
162
+ flex-shrink: 0;
163
+ }
164
+ .memo-title { font-size: 15px; font-weight: 700; color: #1e293b; }
165
+ .memo-close {
166
+ background: none; border: none; font-size: 22px; color: #94a3b8;
167
+ cursor: pointer; padding: 0 4px; line-height: 1;
168
+ }
169
+ .memo-close:hover { color: #1e293b; }
170
+
171
+ .memo-body { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
172
+
173
+ .memo-input-area { padding: 16px 20px; border-bottom: 1px solid #e2e8f0; flex-shrink: 0; }
174
+ .memo-textarea {
175
+ width: 100%; border: 1px solid #e2e8f0; border-radius: 8px;
176
+ padding: 10px 12px; font-size: 13px; line-height: 1.5;
177
+ resize: vertical; font-family: inherit; color: #1e293b; box-sizing: border-box;
178
+ }
179
+ .memo-textarea:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,0.1); }
180
+ .memo-textarea::placeholder { color: #94a3b8; }
181
+
182
+ .memo-add-btn {
183
+ margin-top: 8px; width: 100%; padding: 8px; background: #1e293b;
184
+ color: #fff; border: none; border-radius: 6px; font-size: 13px;
185
+ font-weight: 600; cursor: pointer;
186
+ }
187
+ .memo-add-btn:hover { background: #334155; }
188
+ .memo-add-btn:disabled { background: #cbd5e1; cursor: not-allowed; }
189
+
190
+ .memo-list { flex: 1; overflow-y: auto; padding: 12px 20px; }
191
+ .memo-item {
192
+ padding: 12px 14px; background: #f8fafc; border: 1px solid #e2e8f0;
193
+ border-radius: 8px; margin-bottom: 8px;
194
+ }
195
+ .memo-item-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
196
+ .memo-item-meta { display: flex; align-items: center; gap: 6px; }
197
+ .memo-item-author { font-size: 11px; color: #3b82f6; font-weight: 600; }
198
+ .memo-item-time { font-size: 11px; color: #94a3b8; font-weight: 500; }
199
+ .memo-item-del {
200
+ background: none; border: none; font-size: 16px; color: #cbd5e1;
201
+ cursor: pointer; padding: 0 2px; line-height: 1;
202
+ }
203
+ .memo-item-del:hover { color: #ef4444; }
204
+ .memo-item-text {
205
+ font-size: 13px; color: #1e293b; line-height: 1.6;
206
+ white-space: pre-wrap; word-break: break-word;
207
+ }
208
+
209
+ .memo-empty {
210
+ text-align: center; color: #94a3b8; font-size: 13px;
211
+ padding: 40px 20px; line-height: 1.6;
212
+ }
213
+ .memo-error {
214
+ margin-bottom: 10px;
215
+ padding: 8px 10px;
216
+ border: 1px solid #fecaca;
217
+ background: #fef2f2;
218
+ color: #b91c1c;
219
+ border-radius: 6px;
220
+ font-size: 12px;
221
+ }
222
+
223
+ .memo-footer {
224
+ display: flex; align-items: center; justify-content: space-between;
225
+ padding: 10px 20px; border-top: 1px solid #e2e8f0;
226
+ background: #f8fafc; flex-shrink: 0;
227
+ }
228
+ .memo-footer-info { font-size: 11px; color: #94a3b8; }
229
+ .memo-clear-btn {
230
+ background: none; border: 1px solid #fca5a5; color: #ef4444;
231
+ font-size: 11px; padding: 4px 10px; border-radius: 4px;
232
+ cursor: pointer; font-weight: 500;
233
+ }
234
+ .memo-clear-btn:hover { background: #fef2f2; }
235
+
236
+ /* Transitions */
237
+ .memo-slide-enter-active, .memo-slide-leave-active { transition: transform 0.25s ease; }
238
+ .memo-slide-enter-from, .memo-slide-leave-to { transform: translateX(100%); }
239
+ </style>
@@ -0,0 +1,100 @@
1
+ <script setup lang="ts">
2
+ import { computed, watch } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+ import { useActiveSection } from '@/composables/useActiveSection'
5
+
6
+ const route = useRoute()
7
+ const router = useRouter()
8
+ const { clearActiveSection } = useActiveSection()
9
+
10
+ const currentSprint = computed(() => (route.params.sprint as string) || '')
11
+ const currentPageId = computed(() => (route.params.pageId as string) || '')
12
+
13
+ interface SidebarItem {
14
+ id: string
15
+ icon: string
16
+ label: string
17
+ pageId: string | null
18
+ }
19
+
20
+ // TODO: Replace with your project's sidebar items
21
+ const sidebarItems: SidebarItem[] = [
22
+ { id: 'home', icon: '🏠', label: 'Home', pageId: 'home' },
23
+ ]
24
+
25
+ function navigate(item: SidebarItem) {
26
+ if (!item.pageId || item.pageId === currentPageId.value) return
27
+ router.push(`/${item.pageId}/${currentSprint.value}`)
28
+ }
29
+
30
+ watch(currentPageId, () => {
31
+ clearActiveSection()
32
+ })
33
+ </script>
34
+
35
+ <template>
36
+ <div class="mockup-shell">
37
+ <nav class="app-sidebar">
38
+ <div class="sidebar-logo">APP</div>
39
+ <div class="sidebar-menu">
40
+ <a
41
+ v-for="item in sidebarItems"
42
+ :key="item.id"
43
+ class="sidebar-item"
44
+ :class="{
45
+ active: item.pageId === currentPageId,
46
+ disabled: !item.pageId,
47
+ }"
48
+ @click="navigate(item)"
49
+ >
50
+ <span class="sidebar-icon">{{ item.icon }}</span>{{ item.label }}
51
+ </a>
52
+ </div>
53
+ </nav>
54
+ <div class="mockup-main">
55
+ <slot />
56
+ </div>
57
+ </div>
58
+ </template>
59
+
60
+ <style scoped>
61
+ .mockup-shell {
62
+ display: flex;
63
+ min-height: 100%;
64
+ height: 100%;
65
+ background: var(--bg);
66
+ }
67
+
68
+ .app-sidebar {
69
+ width: var(--sidebar-width);
70
+ background: var(--text-primary);
71
+ color: #fff;
72
+ display: flex;
73
+ flex-direction: column;
74
+ align-items: center;
75
+ padding: 16px 0;
76
+ flex-shrink: 0;
77
+ }
78
+ .sidebar-logo {
79
+ width: 44px; height: 44px; background: var(--primary); border-radius: 10px;
80
+ display: flex; align-items: center; justify-content: center;
81
+ font-weight: 700; font-size: 11px; margin-bottom: 24px; letter-spacing: -0.5px;
82
+ }
83
+ .sidebar-menu { display: flex; flex-direction: column; gap: 4px; width: 100%; padding: 0 8px; }
84
+ .sidebar-item {
85
+ display: flex; flex-direction: column; align-items: center; gap: 2px;
86
+ padding: 10px 4px; border-radius: 8px; font-size: 10px; color: #9ca3af;
87
+ cursor: pointer; transition: all 0.15s; text-decoration: none;
88
+ }
89
+ .sidebar-item:hover:not(.disabled) { background: rgba(255,255,255,0.08); color: #fff; }
90
+ .sidebar-item.active { background: rgba(255,255,255,0.12); color: #fff; }
91
+ .sidebar-item.disabled { opacity: 0.35; cursor: default; }
92
+ .sidebar-icon { font-size: 18px; height: 20px; }
93
+
94
+ .mockup-main {
95
+ flex: 1;
96
+ min-width: 0;
97
+ background: var(--bg);
98
+ overflow-y: auto;
99
+ }
100
+ </style>
@@ -0,0 +1,99 @@
1
+ <script setup lang="ts">
2
+ import type { Rule } from '@/data/types'
3
+
4
+ defineProps<{
5
+ rules: Rule[]
6
+ }>()
7
+
8
+ function severityClass(sev: string) {
9
+ const map: Record<string, string> = {
10
+ danger: 'sev-r',
11
+ warning: 'sev-y',
12
+ good: 'sev-g',
13
+ info: 'sev-b',
14
+ opportunity: 'sev-b',
15
+ }
16
+ return map[sev] ?? 'sev-b'
17
+ }
18
+
19
+ function implClass(status: string) {
20
+ const map: Record<string, string> = {
21
+ done: 'impl-done',
22
+ 'data-ready': 'impl-ready',
23
+ 'logic-needed': 'impl-logic',
24
+ 'new-data': 'impl-new',
25
+ }
26
+ return map[status] ?? ''
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <table class="sb-rules">
32
+ <thead>
33
+ <tr>
34
+ <th>ID</th>
35
+ <th>Condition</th>
36
+ <th>Sev</th>
37
+ <th>Message</th>
38
+ <th>Impl</th>
39
+ </tr>
40
+ </thead>
41
+ <tbody>
42
+ <tr v-for="rule in rules" :key="rule.id">
43
+ <td class="rid">{{ rule.id }}</td>
44
+ <td>{{ rule.condition }}</td>
45
+ <td><span class="sev" :class="severityClass(rule.severity)" /></td>
46
+ <td class="msg">{{ rule.homeMessage }}</td>
47
+ <td><span class="impl" :class="implClass(rule.implStatus)">{{ rule.implStatus }}</span></td>
48
+ </tr>
49
+ </tbody>
50
+ </table>
51
+ </template>
52
+
53
+ <style scoped>
54
+ .sb-rules {
55
+ width: 100%;
56
+ border-collapse: collapse;
57
+ font-size: 12px;
58
+ line-height: 1.5;
59
+ }
60
+ .sb-rules th {
61
+ text-align: left;
62
+ padding: 6px 8px;
63
+ background: #f8fafc;
64
+ border-bottom: 2px solid var(--border);
65
+ font-weight: 700;
66
+ color: var(--text-secondary);
67
+ font-size: 11px;
68
+ text-transform: uppercase;
69
+ letter-spacing: 0.5px;
70
+ }
71
+ .sb-rules td {
72
+ padding: 6px 8px;
73
+ border-bottom: 1px solid var(--border-light);
74
+ vertical-align: top;
75
+ }
76
+ .rid { font-family: var(--font-num); font-weight: 600; white-space: nowrap; color: var(--primary); }
77
+ .msg { color: var(--text-secondary); max-width: 200px; }
78
+ .sev {
79
+ display: inline-block;
80
+ width: 10px;
81
+ height: 10px;
82
+ border-radius: 50%;
83
+ }
84
+ .sev-r { background: var(--red); }
85
+ .sev-y { background: var(--yellow); }
86
+ .sev-g { background: var(--green); }
87
+ .sev-b { background: var(--blue); }
88
+ .impl {
89
+ font-size: 10px;
90
+ padding: 1px 6px;
91
+ border-radius: 4px;
92
+ font-weight: 600;
93
+ white-space: nowrap;
94
+ }
95
+ .impl-done { background: var(--green-bg); color: var(--green); }
96
+ .impl-ready { background: var(--blue-bg); color: var(--blue); }
97
+ .impl-logic { background: var(--yellow-bg); color: var(--yellow); }
98
+ .impl-new { background: var(--red-bg); color: var(--red); }
99
+ </style>
@@ -0,0 +1,103 @@
1
+ <script setup lang="ts">
2
+ import type { Scenario } from '@/data/types'
3
+
4
+ const props = defineProps<{
5
+ scenarios: Scenario<any>[]
6
+ activeId: string
7
+ }>()
8
+
9
+ const emit = defineEmits<{
10
+ change: [id: string]
11
+ duplicate: [id: string]
12
+ deleteCustom: [id: string]
13
+ }>()
14
+
15
+ function isCustom(id: string) {
16
+ return id.startsWith('custom-')
17
+ }
18
+ </script>
19
+
20
+ <template>
21
+ <div class="variant-bar">
22
+ <span>Scenario:</span>
23
+ <div
24
+ v-for="s in scenarios"
25
+ :key="s.id"
26
+ class="variant-item"
27
+ >
28
+ <button
29
+ class="variant-btn"
30
+ :class="{ active: activeId === s.id }"
31
+ @click="emit('change', s.id)"
32
+ >
33
+ {{ s.label }}
34
+ </button>
35
+ <button
36
+ v-if="activeId === s.id && !isCustom(s.id)"
37
+ class="action-btn duplicate-btn"
38
+ title="Duplicate scenario"
39
+ @click.stop="emit('duplicate', s.id)"
40
+ >+</button>
41
+ <button
42
+ v-if="isCustom(s.id)"
43
+ class="action-btn delete-btn"
44
+ title="Delete custom scenario"
45
+ @click.stop="emit('deleteCustom', s.id)"
46
+ >&times;</button>
47
+ </div>
48
+ </div>
49
+ </template>
50
+
51
+ <style scoped>
52
+ .variant-bar {
53
+ position: sticky;
54
+ top: 0;
55
+ z-index: 100;
56
+ background: #1f2123;
57
+ color: #fff;
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ gap: 8px;
62
+ padding: 10px 20px;
63
+ font-size: 13px;
64
+ flex-wrap: wrap;
65
+ }
66
+ .variant-bar > span { opacity: 0.6; }
67
+ .variant-item {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 2px;
71
+ }
72
+ .variant-btn {
73
+ padding: 5px 14px;
74
+ border-radius: 6px;
75
+ border: 1px solid rgba(255,255,255,0.2);
76
+ background: transparent;
77
+ color: #fff;
78
+ font-size: 12px;
79
+ font-family: var(--font-kr);
80
+ cursor: pointer;
81
+ transition: all 0.15s;
82
+ }
83
+ .variant-btn:hover { background: rgba(255,255,255,0.1); }
84
+ .variant-btn.active { background: var(--primary); border-color: var(--primary); }
85
+ .action-btn {
86
+ width: 20px;
87
+ height: 20px;
88
+ border-radius: 50%;
89
+ border: 1px solid rgba(255,255,255,0.3);
90
+ background: transparent;
91
+ color: #fff;
92
+ font-size: 12px;
93
+ line-height: 1;
94
+ cursor: pointer;
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ transition: all 0.15s;
99
+ }
100
+ .action-btn:hover { background: rgba(255,255,255,0.15); }
101
+ .delete-btn { border-color: rgba(255,100,100,0.5); color: #ff8a8a; }
102
+ .delete-btn:hover { background: rgba(255,100,100,0.2); }
103
+ </style>
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ import type { SpecArea } from '@/data/types'
3
+
4
+ defineProps<{
5
+ areas: SpecArea[]
6
+ activeId: string | null
7
+ }>()
8
+
9
+ const emit = defineEmits<{
10
+ select: [id: string]
11
+ }>()
12
+ </script>
13
+
14
+ <template>
15
+ <div class="spec-nav">
16
+ <button
17
+ v-for="area in areas"
18
+ :key="area.id"
19
+ class="spec-nav-btn"
20
+ :class="{ active: activeId === area.id }"
21
+ @click="emit('select', area.id)"
22
+ >
23
+ {{ area.shortLabel }}
24
+ </button>
25
+ </div>
26
+ </template>
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ areaId: string
4
+ title: string
5
+ ruleCount?: number
6
+ }>()
7
+ </script>
8
+
9
+ <template>
10
+ <div class="spec-section" :id="`spec-section-${areaId}`">
11
+ <div class="sb-area">
12
+ <div class="sb-area-head">
13
+ <div class="a-id">{{ areaId }}</div>
14
+ <div class="a-title">{{ title }}</div>
15
+ <div v-if="ruleCount" class="a-badge">{{ ruleCount }} rules</div>
16
+ </div>
17
+ <div class="sb-body">
18
+ <slot />
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </template>
23
+
24
+ <style scoped>
25
+ .spec-section {
26
+ border-bottom: 1px solid var(--border);
27
+ }
28
+ .sb-area-head {
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 10px;
32
+ padding: 12px 16px;
33
+ background: #f8fafc;
34
+ border-bottom: 1px solid var(--border-light);
35
+ }
36
+ .a-id {
37
+ width: 28px;
38
+ height: 28px;
39
+ border-radius: 6px;
40
+ background: var(--primary);
41
+ color: #fff;
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ font-size: 12px;
46
+ font-weight: 700;
47
+ flex-shrink: 0;
48
+ }
49
+ .a-title { font-size: 14px; font-weight: 700; flex: 1; }
50
+ .a-badge {
51
+ font-size: 11px;
52
+ padding: 2px 8px;
53
+ border-radius: 10px;
54
+ background: var(--blue-bg);
55
+ color: var(--blue);
56
+ border: 1px solid var(--blue-border);
57
+ }
58
+ .sb-body { padding: 16px; }
59
+ </style>
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ items: {
4
+ label: string
5
+ value: string
6
+ delta: string
7
+ deltaDir: 'up' | 'down' | 'flat'
8
+ }[]
9
+ }>()
10
+ </script>
11
+
12
+ <template>
13
+ <div class="summary-grid">
14
+ <div v-for="(item, i) in items" :key="i" class="summary-item">
15
+ <div class="summary-label">{{ item.label }}</div>
16
+ <div class="summary-value">{{ item.value }}</div>
17
+ <div class="summary-delta" :class="`delta-${item.deltaDir}`">{{ item.delta }}</div>
18
+ </div>
19
+ </div>
20
+ </template>
21
+
22
+ <style scoped>
23
+ .summary-grid {
24
+ display: grid;
25
+ grid-template-columns: repeat(4, 1fr);
26
+ gap: 16px;
27
+ }
28
+ .summary-label { font-size: 12px; color: var(--text-muted); margin-bottom: 4px; }
29
+ .summary-value {
30
+ font-family: var(--font-num);
31
+ font-size: 22px;
32
+ font-weight: 700;
33
+ letter-spacing: -0.5px;
34
+ }
35
+ .summary-delta { font-size: 12px; margin-top: 2px; font-family: var(--font-num); }
36
+ .delta-up { color: var(--green); }
37
+ .delta-down { color: var(--red); }
38
+ .delta-flat { color: var(--text-muted); }
39
+ </style>
@@ -0,0 +1,38 @@
1
+ <script setup lang="ts">
2
+ import type { PageVersion } from '@/data/types'
3
+
4
+ defineProps<{
5
+ version: PageVersion
6
+ }>()
7
+
8
+ function statusColor(s: string) {
9
+ const m: Record<string, string> = { draft: 'yellow', review: 'blue', approved: 'green', dev: 'green' }
10
+ return m[s] ?? 'blue'
11
+ }
12
+ </script>
13
+
14
+ <template>
15
+ <div class="version-badge">
16
+ <span class="ver-tag">v{{ version.version }}</span>
17
+ <span class="ver-status" :class="`status-${statusColor(version.status)}`">
18
+ {{ version.status }}
19
+ </span>
20
+ <span class="ver-date">{{ version.lastUpdated }}</span>
21
+ </div>
22
+ </template>
23
+
24
+ <style scoped>
25
+ .version-badge { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; }
26
+ .ver-tag { font-family: var(--font-num); font-weight: 700; color: var(--text-secondary); }
27
+ .ver-status {
28
+ padding: 1px 6px;
29
+ border-radius: 4px;
30
+ font-weight: 600;
31
+ text-transform: uppercase;
32
+ letter-spacing: 0.3px;
33
+ }
34
+ .status-yellow { background: var(--yellow-bg); color: var(--yellow); }
35
+ .status-blue { background: var(--blue-bg); color: var(--blue); }
36
+ .status-green { background: var(--green-bg); color: var(--green); }
37
+ .ver-date { color: var(--text-muted); }
38
+ </style>