feed-the-machine 1.1.0 → 1.3.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 (92) hide show
  1. package/bin/generate-manifest.mjs +253 -0
  2. package/bin/install.mjs +372 -26
  3. package/docs/INBOX.md +233 -0
  4. package/ftm/SKILL.md +34 -0
  5. package/ftm-audit/SKILL.md +69 -0
  6. package/ftm-brainstorm/SKILL.md +51 -0
  7. package/ftm-browse/SKILL.md +39 -0
  8. package/ftm-capture/SKILL.md +370 -0
  9. package/ftm-capture.yml +4 -0
  10. package/ftm-codex-gate/SKILL.md +59 -0
  11. package/ftm-config/SKILL.md +35 -0
  12. package/ftm-council/SKILL.md +56 -0
  13. package/ftm-dashboard/SKILL.md +34 -0
  14. package/ftm-debug/SKILL.md +84 -0
  15. package/ftm-diagram/SKILL.md +44 -0
  16. package/ftm-executor/SKILL.md +97 -0
  17. package/ftm-git/SKILL.md +60 -0
  18. package/ftm-inbox/backend/__init__.py +0 -0
  19. package/ftm-inbox/backend/adapters/__init__.py +0 -0
  20. package/ftm-inbox/backend/adapters/_retry.py +64 -0
  21. package/ftm-inbox/backend/adapters/base.py +230 -0
  22. package/ftm-inbox/backend/adapters/freshservice.py +104 -0
  23. package/ftm-inbox/backend/adapters/gmail.py +125 -0
  24. package/ftm-inbox/backend/adapters/jira.py +136 -0
  25. package/ftm-inbox/backend/adapters/registry.py +192 -0
  26. package/ftm-inbox/backend/adapters/slack.py +110 -0
  27. package/ftm-inbox/backend/db/__init__.py +0 -0
  28. package/ftm-inbox/backend/db/connection.py +54 -0
  29. package/ftm-inbox/backend/db/schema.py +78 -0
  30. package/ftm-inbox/backend/executor/__init__.py +7 -0
  31. package/ftm-inbox/backend/executor/engine.py +149 -0
  32. package/ftm-inbox/backend/executor/step_runner.py +98 -0
  33. package/ftm-inbox/backend/main.py +103 -0
  34. package/ftm-inbox/backend/models/__init__.py +1 -0
  35. package/ftm-inbox/backend/models/unified_task.py +36 -0
  36. package/ftm-inbox/backend/planner/__init__.py +6 -0
  37. package/ftm-inbox/backend/planner/generator.py +127 -0
  38. package/ftm-inbox/backend/planner/schema.py +34 -0
  39. package/ftm-inbox/backend/requirements.txt +5 -0
  40. package/ftm-inbox/backend/routes/__init__.py +0 -0
  41. package/ftm-inbox/backend/routes/execute.py +186 -0
  42. package/ftm-inbox/backend/routes/health.py +52 -0
  43. package/ftm-inbox/backend/routes/inbox.py +68 -0
  44. package/ftm-inbox/backend/routes/plan.py +271 -0
  45. package/ftm-inbox/bin/launchagent.mjs +91 -0
  46. package/ftm-inbox/bin/setup.mjs +188 -0
  47. package/ftm-inbox/bin/start.sh +10 -0
  48. package/ftm-inbox/bin/status.sh +17 -0
  49. package/ftm-inbox/bin/stop.sh +8 -0
  50. package/ftm-inbox/config.example.yml +55 -0
  51. package/ftm-inbox/package-lock.json +2898 -0
  52. package/ftm-inbox/package.json +26 -0
  53. package/ftm-inbox/postcss.config.js +6 -0
  54. package/ftm-inbox/src/app.css +199 -0
  55. package/ftm-inbox/src/app.html +18 -0
  56. package/ftm-inbox/src/lib/api.ts +166 -0
  57. package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -0
  58. package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -0
  59. package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -0
  60. package/ftm-inbox/src/lib/components/PlanView.svelte +206 -0
  61. package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -0
  62. package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -0
  63. package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -0
  64. package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -0
  65. package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -0
  66. package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -0
  67. package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -0
  68. package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -0
  69. package/ftm-inbox/src/lib/theme.ts +47 -0
  70. package/ftm-inbox/src/routes/+layout.svelte +76 -0
  71. package/ftm-inbox/src/routes/+page.svelte +401 -0
  72. package/ftm-inbox/static/favicon.png +0 -0
  73. package/ftm-inbox/svelte.config.js +12 -0
  74. package/ftm-inbox/tailwind.config.ts +63 -0
  75. package/ftm-inbox/tsconfig.json +13 -0
  76. package/ftm-inbox/vite.config.ts +6 -0
  77. package/ftm-intent/SKILL.md +44 -0
  78. package/ftm-manifest.json +3794 -0
  79. package/ftm-map/SKILL.md +50 -0
  80. package/ftm-mind/SKILL.md +173 -66
  81. package/ftm-pause/SKILL.md +43 -0
  82. package/ftm-researcher/SKILL.md +55 -0
  83. package/ftm-resume/SKILL.md +47 -0
  84. package/ftm-retro/SKILL.md +54 -0
  85. package/ftm-routine/SKILL.md +36 -0
  86. package/ftm-state/blackboard/capabilities.json +5 -0
  87. package/ftm-state/blackboard/capabilities.schema.json +27 -0
  88. package/ftm-upgrade/SKILL.md +41 -0
  89. package/hooks/ftm-blackboard-enforcer.sh +28 -27
  90. package/hooks/ftm-plan-gate.sh +21 -25
  91. package/install.sh +238 -111
  92. package/package.json +6 -2
@@ -0,0 +1,80 @@
1
+ <script lang="ts">
2
+ import { theme } from '$lib/theme';
3
+
4
+ let rotating = false;
5
+
6
+ function handleToggle() {
7
+ rotating = true;
8
+ theme.toggle();
9
+ setTimeout(() => (rotating = false), 400);
10
+ }
11
+ </script>
12
+
13
+ <button
14
+ class="theme-toggle"
15
+ class:rotating
16
+ on:click={handleToggle}
17
+ aria-label={$theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
18
+ title={$theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
19
+ >
20
+ {#if $theme === 'light'}
21
+ <!-- Moon icon -->
22
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
23
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
24
+ </svg>
25
+ {:else}
26
+ <!-- Sun icon -->
27
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
28
+ <circle cx="12" cy="12" r="5" />
29
+ <line x1="12" y1="1" x2="12" y2="3" />
30
+ <line x1="12" y1="21" x2="12" y2="23" />
31
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
32
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
33
+ <line x1="1" y1="12" x2="3" y2="12" />
34
+ <line x1="21" y1="12" x2="23" y2="12" />
35
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
36
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
37
+ </svg>
38
+ {/if}
39
+ </button>
40
+
41
+ <style>
42
+ .theme-toggle {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ width: 40px;
47
+ height: 40px;
48
+ border-radius: 9999px;
49
+ border: 2px solid var(--border-card);
50
+ background: var(--bg-card);
51
+ color: var(--text-secondary);
52
+ cursor: pointer;
53
+ transition:
54
+ background 0.2s ease,
55
+ border-color 0.2s ease,
56
+ color 0.2s ease,
57
+ transform 0.18s cubic-bezier(0.68, -0.55, 0.265, 1.55),
58
+ box-shadow 0.18s ease;
59
+ }
60
+
61
+ .theme-toggle:hover {
62
+ border-color: var(--accent-primary);
63
+ color: var(--accent-primary);
64
+ transform: scale(1.08);
65
+ box-shadow: 0 2px 12px rgba(76, 175, 80, 0.2);
66
+ }
67
+
68
+ .theme-toggle:active {
69
+ transform: scale(0.95);
70
+ }
71
+
72
+ .theme-toggle.rotating svg {
73
+ animation: spin-once 0.4s ease-in-out;
74
+ }
75
+
76
+ @keyframes spin-once {
77
+ from { transform: rotate(0deg); }
78
+ to { transform: rotate(360deg); }
79
+ }
80
+ </style>
@@ -0,0 +1,47 @@
1
+ import { writable } from 'svelte/store';
2
+ import { browser } from '$app/environment';
3
+
4
+ export type Theme = 'light' | 'dark';
5
+
6
+ const STORAGE_KEY = 'ftm-inbox-theme';
7
+
8
+ function getInitialTheme(): Theme {
9
+ if (!browser) return 'light';
10
+ const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
11
+ if (stored === 'light' || stored === 'dark') return stored;
12
+ // Respect system preference on first visit
13
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
14
+ }
15
+
16
+ function createThemeStore() {
17
+ const { subscribe, set, update } = writable<Theme>(getInitialTheme());
18
+
19
+ function applyTheme(theme: Theme) {
20
+ if (!browser) return;
21
+ document.body.classList.remove('theme-light', 'theme-dark');
22
+ document.body.classList.add(`theme-${theme}`);
23
+ localStorage.setItem(STORAGE_KEY, theme);
24
+ }
25
+
26
+ return {
27
+ subscribe,
28
+ set(theme: Theme) {
29
+ applyTheme(theme);
30
+ set(theme);
31
+ },
32
+ toggle() {
33
+ update((current) => {
34
+ const next: Theme = current === 'light' ? 'dark' : 'light';
35
+ applyTheme(next);
36
+ return next;
37
+ });
38
+ },
39
+ init() {
40
+ const theme = getInitialTheme();
41
+ applyTheme(theme);
42
+ set(theme);
43
+ }
44
+ };
45
+ }
46
+
47
+ export const theme = createThemeStore();
@@ -0,0 +1,76 @@
1
+ <script lang="ts">
2
+ import '../app.css';
3
+ import { onMount } from 'svelte';
4
+ import { theme } from '$lib/theme';
5
+ import ThemeToggle from '$lib/components/ui/ThemeToggle.svelte';
6
+
7
+ onMount(() => {
8
+ theme.init();
9
+ });
10
+ </script>
11
+
12
+ <div class="app-shell">
13
+ <!-- Top nav bar -->
14
+ <header class="topnav">
15
+ <div class="topnav-brand">
16
+ <span class="brand-dot" aria-hidden="true">◉</span>
17
+ <span class="brand-name">ftm inbox</span>
18
+ </div>
19
+ <div class="topnav-actions">
20
+ <ThemeToggle />
21
+ </div>
22
+ </header>
23
+
24
+ <!-- Page content (three-column layout lives in +page.svelte) -->
25
+ <slot />
26
+ </div>
27
+
28
+ <style>
29
+ .app-shell {
30
+ display: flex;
31
+ flex-direction: column;
32
+ min-height: 100vh;
33
+ }
34
+
35
+ /* ─── Top nav ─── */
36
+ .topnav {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: space-between;
40
+ padding: 0.6rem 1.25rem;
41
+ background: var(--bg-card);
42
+ border-bottom: 2px solid var(--border-card);
43
+ position: sticky;
44
+ top: 0;
45
+ z-index: 50;
46
+ box-shadow: 0 2px 8px rgba(76, 175, 80, 0.07);
47
+ }
48
+
49
+ .topnav-brand {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: 0.5rem;
53
+ font-family: 'Nunito', sans-serif;
54
+ font-weight: 800;
55
+ font-size: 1.1rem;
56
+ color: var(--text-primary);
57
+ user-select: none;
58
+ }
59
+
60
+ .brand-dot {
61
+ color: var(--accent-primary);
62
+ font-size: 1rem;
63
+ animation: pulse-dot 2.5s ease-in-out infinite;
64
+ }
65
+
66
+ @keyframes pulse-dot {
67
+ 0%, 100% { opacity: 1; }
68
+ 50% { opacity: 0.5; }
69
+ }
70
+
71
+ .topnav-actions {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 0.5rem;
75
+ }
76
+ </style>
@@ -0,0 +1,401 @@
1
+ <script lang="ts">
2
+ import KawaiiCard from '$lib/components/ui/KawaiiCard.svelte';
3
+ import StatusBadge from '$lib/components/ui/StatusBadge.svelte';
4
+ import PillButton from '$lib/components/ui/PillButton.svelte';
5
+ import EmptyState from '$lib/components/ui/EmptyState.svelte';
6
+ import StreamDrawer from '$lib/components/ui/StreamDrawer.svelte';
7
+ import InboxFeed from '$lib/components/InboxFeed.svelte';
8
+ import PlanView from '$lib/components/PlanView.svelte';
9
+ import type { UnifiedTask, Plan } from '$lib/api';
10
+ import { generatePlan, getPlan } from '$lib/api';
11
+ type StatusBadgeStatus = 'pending' | 'planning' | 'approved' | 'executing' | 'complete' | 'failed';
12
+
13
+ // Map arbitrary task status string to a valid StatusBadge value
14
+ function taskStatusBadge(s: string): StatusBadgeStatus {
15
+ const valid: StatusBadgeStatus[] = ['pending', 'planning', 'approved', 'executing', 'complete', 'failed'];
16
+ return (valid.includes(s as StatusBadgeStatus) ? s : 'pending') as StatusBadgeStatus;
17
+ }
18
+
19
+ const API_BASE = 'http://localhost:8042';
20
+
21
+ const sourceAccent: Record<string, 'blue' | 'green' | 'yellow' | 'coral'> = {
22
+ jira: 'blue',
23
+ freshservice: 'green',
24
+ slack: 'yellow',
25
+ gmail: 'coral'
26
+ };
27
+
28
+ let selectedTask: UnifiedTask | null = null;
29
+ let currentPlan: Plan | null = null;
30
+ let planLoading = false;
31
+ let drawerOpen = false;
32
+ let drawerLines: string[] = [];
33
+ let auditEntries: { time: string; level: string; msg: string }[] = [];
34
+
35
+ function addAudit(level: 'info' | 'warn' | 'success' | 'error', msg: string) {
36
+ auditEntries = [
37
+ ...auditEntries,
38
+ { time: new Date().toLocaleTimeString('en-GB', { hour12: false }), level, msg }
39
+ ];
40
+ }
41
+
42
+ async function handleSelectTask(e: CustomEvent<UnifiedTask>) {
43
+ selectedTask = e.detail;
44
+ currentPlan = null;
45
+ // Load existing plan for this task if one exists
46
+ try {
47
+ currentPlan = await getPlan(e.detail.id);
48
+ } catch {
49
+ // No plan yet — that's fine
50
+ }
51
+ }
52
+
53
+ async function handleGeneratePlan(e: CustomEvent<UnifiedTask>) {
54
+ selectedTask = e.detail;
55
+ currentPlan = null;
56
+ planLoading = true;
57
+ drawerLines = [];
58
+ drawerOpen = true;
59
+
60
+ const task = e.detail;
61
+ addAudit('info', `Plan generation started for: ${task.title}`);
62
+ drawerLines = [...drawerLines, `[${new Date().toLocaleTimeString()}] Generate plan: ${task.title}`];
63
+
64
+ // Use SSE stream for live output in the drawer
65
+ try {
66
+ const evtSource = new EventSource(`${API_BASE}/api/tasks/${task.id}/plan-stream`);
67
+
68
+ evtSource.onmessage = (event) => {
69
+ try {
70
+ const msg = JSON.parse(event.data);
71
+ if (msg.type === 'chunk') {
72
+ drawerLines = [...drawerLines, msg.text.trimEnd()];
73
+ } else if (msg.type === 'done') {
74
+ currentPlan = msg.plan as Plan;
75
+ planLoading = false;
76
+ addAudit('success', `Plan ready: ${currentPlan.steps.length} steps`);
77
+ drawerLines = [...drawerLines, `Plan ready — ${currentPlan.steps.length} steps generated.`];
78
+ evtSource.close();
79
+ } else if (msg.type === 'error') {
80
+ addAudit('error', `Plan failed: ${msg.message}`);
81
+ drawerLines = [...drawerLines, `Error: ${msg.message}`];
82
+ planLoading = false;
83
+ evtSource.close();
84
+ }
85
+ } catch {
86
+ // Ignore malformed SSE frames
87
+ }
88
+ };
89
+
90
+ evtSource.onerror = () => {
91
+ planLoading = false;
92
+ addAudit('warn', 'SSE stream closed unexpectedly');
93
+ evtSource.close();
94
+ };
95
+ } catch (err) {
96
+ // Fallback: direct POST without streaming
97
+ try {
98
+ const plan = await generatePlan(task.id);
99
+ currentPlan = plan;
100
+ addAudit('success', `Plan ready: ${plan.steps.length} steps`);
101
+ } catch (genErr) {
102
+ addAudit('error', `Plan generation failed: ${genErr}`);
103
+ } finally {
104
+ planLoading = false;
105
+ }
106
+ }
107
+ }
108
+
109
+ function handlePlanUpdated(e: CustomEvent<Plan>) {
110
+ currentPlan = e.detail;
111
+ addAudit('info', `Plan updated — status: ${e.detail.status}`);
112
+ }
113
+ </script>
114
+
115
+ <!-- Three-column layout + bottom drawer -->
116
+ <div class="layout-grid">
117
+ <!-- Left: Task Inbox -->
118
+ <aside class="sidebar sidebar-left" aria-label="Task inbox">
119
+ <div class="sidebar-header">
120
+ <h2 class="sidebar-title">Inbox</h2>
121
+ </div>
122
+ <div class="sidebar-body">
123
+ <InboxFeed
124
+ selectedTaskId={selectedTask?.id ?? null}
125
+ on:selectTask={handleSelectTask}
126
+ on:generatePlan={handleGeneratePlan}
127
+ />
128
+ </div>
129
+ </aside>
130
+
131
+ <!-- Center: Plan Viewer -->
132
+ <section class="center-panel" aria-label="Plan viewer">
133
+ {#if selectedTask}
134
+ <div class="plan-viewer">
135
+ <div class="plan-header">
136
+ <div class="plan-header-top">
137
+ <span class="plan-id">{selectedTask.source}:{selectedTask.source_id}</span>
138
+ <StatusBadge status={taskStatusBadge(selectedTask.status)} />
139
+ </div>
140
+ <h1 class="plan-title">{selectedTask.title}</h1>
141
+ {#if selectedTask.body}
142
+ <p class="plan-body">{selectedTask.body}</p>
143
+ {/if}
144
+ </div>
145
+
146
+ <KawaiiCard accent={sourceAccent[selectedTask.source] ?? 'green'}>
147
+ <span slot="header" class="card-label">Execution Plan</span>
148
+
149
+ <PlanView
150
+ plan={currentPlan}
151
+ loading={planLoading}
152
+ on:planUpdated={handlePlanUpdated}
153
+ />
154
+
155
+ <div slot="footer" class="plan-actions">
156
+ <PillButton
157
+ variant="primary"
158
+ size="sm"
159
+ disabled={planLoading}
160
+ on:click={() => { if (selectedTask) handleGeneratePlan(new CustomEvent('generatePlan', { detail: selectedTask })); }}
161
+ >
162
+ {planLoading ? 'Generating…' : currentPlan ? 'Regenerate Plan' : 'Generate Plan'}
163
+ </PillButton>
164
+ {#if selectedTask.source_url}
165
+ <PillButton variant="ghost" size="sm" on:click={() => window.open(selectedTask?.source_url ?? '', '_blank')}>
166
+ Open Source
167
+ </PillButton>
168
+ {/if}
169
+ </div>
170
+ </KawaiiCard>
171
+ </div>
172
+ {:else}
173
+ <EmptyState
174
+ emoji="🗂️"
175
+ title="Select a task"
176
+ message="Choose a task from the inbox to view its plan."
177
+ />
178
+ {/if}
179
+ </section>
180
+
181
+ <!-- Right: Audit Log -->
182
+ <aside class="sidebar sidebar-right" aria-label="Audit log">
183
+ <div class="sidebar-header">
184
+ <h2 class="sidebar-title">Audit Log</h2>
185
+ {#if auditEntries.length > 0}
186
+ <span class="sidebar-count">{auditEntries.length}</span>
187
+ {/if}
188
+ </div>
189
+ <div class="sidebar-body">
190
+ {#if auditEntries.length === 0}
191
+ <EmptyState
192
+ emoji="📋"
193
+ title="No events yet"
194
+ message="Audit events will appear here."
195
+ />
196
+ {:else}
197
+ <div class="audit-list">
198
+ {#each auditEntries as entry, i (i)}
199
+ <div class="audit-entry audit-{entry.level}">
200
+ <span class="audit-time">{entry.time}</span>
201
+ <span class="audit-msg">{entry.msg}</span>
202
+ </div>
203
+ {/each}
204
+ </div>
205
+ {/if}
206
+ </div>
207
+ </aside>
208
+ </div>
209
+
210
+ <!-- Bottom drawer for streaming agent output -->
211
+ <StreamDrawer bind:open={drawerOpen} lines={drawerLines} />
212
+
213
+ <style>
214
+ /* ─── Three-column layout ─── */
215
+ .layout-grid {
216
+ display: flex;
217
+ flex: 1;
218
+ height: calc(100vh - 57px - 36px); /* minus nav height minus drawer handle */
219
+ overflow: hidden;
220
+ }
221
+
222
+ .sidebar {
223
+ display: flex;
224
+ flex-direction: column;
225
+ background: var(--bg-sidebar);
226
+ overflow: hidden;
227
+ flex-shrink: 0;
228
+ }
229
+
230
+ .sidebar-left {
231
+ width: 280px;
232
+ border-right: 1px solid var(--border-card);
233
+ }
234
+
235
+ .sidebar-right {
236
+ width: 320px;
237
+ border-left: 1px solid var(--border-card);
238
+ }
239
+
240
+ .sidebar-header {
241
+ display: flex;
242
+ align-items: center;
243
+ justify-content: space-between;
244
+ padding: 0.75rem 1rem 0.5rem;
245
+ border-bottom: 1px solid var(--border-card);
246
+ background: var(--bg-card);
247
+ flex-shrink: 0;
248
+ }
249
+
250
+ .sidebar-title {
251
+ font-size: 0.72rem;
252
+ font-weight: 800;
253
+ letter-spacing: 0.08em;
254
+ text-transform: uppercase;
255
+ color: var(--text-muted);
256
+ margin: 0;
257
+ }
258
+
259
+ .sidebar-count {
260
+ font-size: 0.68rem;
261
+ font-weight: 800;
262
+ background: var(--border-card);
263
+ color: var(--text-muted);
264
+ padding: 2px 7px;
265
+ border-radius: 9999px;
266
+ }
267
+
268
+ .sidebar-body {
269
+ flex: 1;
270
+ overflow-y: auto;
271
+ padding: 0.75rem;
272
+ }
273
+
274
+ .center-panel {
275
+ flex: 1;
276
+ overflow-y: auto;
277
+ padding: 1.25rem;
278
+ min-width: 0;
279
+ }
280
+
281
+ /* ─── Plan viewer ─── */
282
+ .plan-viewer {
283
+ display: flex;
284
+ flex-direction: column;
285
+ gap: 1rem;
286
+ max-width: 680px;
287
+ margin: 0 auto;
288
+ animation: fadeUp 0.3s ease-out both;
289
+ }
290
+
291
+ @keyframes fadeUp {
292
+ from { opacity: 0; transform: translateY(8px); }
293
+ to { opacity: 1; transform: translateY(0); }
294
+ }
295
+
296
+ .plan-header {
297
+ display: flex;
298
+ flex-direction: column;
299
+ gap: 0.4rem;
300
+ }
301
+
302
+ .plan-header-top {
303
+ display: flex;
304
+ align-items: center;
305
+ gap: 0.75rem;
306
+ }
307
+
308
+ .plan-id {
309
+ font-size: 0.75rem;
310
+ font-weight: 800;
311
+ letter-spacing: 0.06em;
312
+ color: var(--text-muted);
313
+ font-family: 'Menlo', monospace;
314
+ }
315
+
316
+ .plan-title {
317
+ font-size: 1.25rem;
318
+ font-weight: 800;
319
+ color: var(--text-primary);
320
+ line-height: 1.3;
321
+ }
322
+
323
+ .plan-body {
324
+ font-size: 0.85rem;
325
+ color: var(--text-secondary);
326
+ line-height: 1.5;
327
+ margin: 0;
328
+ max-height: 100px;
329
+ overflow-y: auto;
330
+ }
331
+
332
+ .card-label {
333
+ font-size: 0.72rem;
334
+ font-weight: 800;
335
+ text-transform: uppercase;
336
+ letter-spacing: 0.08em;
337
+ color: var(--text-muted);
338
+ }
339
+
340
+
341
+ .plan-actions {
342
+ display: flex;
343
+ gap: 0.5rem;
344
+ flex-wrap: wrap;
345
+ }
346
+
347
+ /* ─── Audit log ─── */
348
+ .audit-list {
349
+ display: flex;
350
+ flex-direction: column;
351
+ gap: 0.25rem;
352
+ }
353
+
354
+ .audit-entry {
355
+ display: flex;
356
+ flex-direction: column;
357
+ gap: 0.15rem;
358
+ padding: 0.4rem 0.5rem;
359
+ border-radius: 8px;
360
+ font-size: 0.72rem;
361
+ border-left: 3px solid transparent;
362
+ transition: background 0.1s;
363
+ }
364
+
365
+ .audit-entry:hover { background: rgba(76, 175, 80, 0.04); }
366
+
367
+ .audit-info { border-left-color: #66bb6a; }
368
+ .audit-warn { border-left-color: #ffd54f; background: rgba(255, 213, 79, 0.06); }
369
+ .audit-success { border-left-color: #4caf50; background: rgba(76, 175, 80, 0.06); }
370
+ .audit-error { border-left-color: #ef5350; background: rgba(239, 83, 80, 0.06); }
371
+
372
+ .audit-time {
373
+ font-family: 'Menlo', monospace;
374
+ font-size: 0.65rem;
375
+ color: var(--text-muted);
376
+ }
377
+
378
+ .audit-msg {
379
+ color: var(--text-secondary);
380
+ font-weight: 600;
381
+ line-height: 1.4;
382
+ }
383
+
384
+ /* ─── Mobile responsive ─── */
385
+ @media (max-width: 768px) {
386
+ .layout-grid {
387
+ flex-direction: column;
388
+ height: auto;
389
+ overflow: visible;
390
+ }
391
+
392
+ .sidebar-left,
393
+ .sidebar-right {
394
+ width: 100%;
395
+ border-right: none;
396
+ border-left: none;
397
+ border-bottom: 1px solid var(--border-card);
398
+ max-height: 40vh;
399
+ }
400
+ }
401
+ </style>
File without changes
@@ -0,0 +1,12 @@
1
+ import adapter from '@sveltejs/adapter-auto';
2
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3
+
4
+ /** @type {import('@sveltejs/kit').Config} */
5
+ const config = {
6
+ preprocess: vitePreprocess(),
7
+ kit: {
8
+ adapter: adapter()
9
+ }
10
+ };
11
+
12
+ export default config;
@@ -0,0 +1,63 @@
1
+ import type { Config } from 'tailwindcss';
2
+
3
+ export default {
4
+ content: ['./src/**/*.{html,js,svelte,ts}'],
5
+ darkMode: 'class',
6
+ theme: {
7
+ extend: {
8
+ colors: {
9
+ kawaii: {
10
+ mint: {
11
+ 50: '#e8f5e9',
12
+ 100: '#c8e6c9',
13
+ 200: '#a5d6a7',
14
+ 300: '#81c784',
15
+ 400: '#66bb6a',
16
+ 500: '#4caf50',
17
+ 600: '#43a047',
18
+ 700: '#388e3c',
19
+ 800: '#2e7d32',
20
+ 900: '#1b5e20',
21
+ 950: '#1b2e1b'
22
+ },
23
+ cream: '#fefefe',
24
+ coral: '#ffccbc',
25
+ yellow: '#fff9c4',
26
+ blue: '#bbdefb',
27
+ orange: '#ffe0b2',
28
+ teal: '#b2dfdb',
29
+ red: '#ffcdd2',
30
+ neon: '#69f0ae'
31
+ }
32
+ },
33
+ borderRadius: {
34
+ kawaii: '16px',
35
+ pill: '9999px'
36
+ },
37
+ fontFamily: {
38
+ kawaii: ['Nunito', 'Quicksand', 'sans-serif']
39
+ },
40
+ boxShadow: {
41
+ kawaii: '0 4px 24px 0 rgba(76, 175, 80, 0.10)',
42
+ 'kawaii-md': '0 6px 32px 0 rgba(76, 175, 80, 0.15)',
43
+ 'kawaii-glow': '0 0 0 2px #69f0ae, 0 4px 24px 0 rgba(105, 240, 174, 0.25)'
44
+ },
45
+ animation: {
46
+ 'bounce-in': 'bounceIn 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55) both',
47
+ 'fade-up': 'fadeUp 0.3s ease-out both',
48
+ 'spin-once': 'spin 0.4s ease-in-out'
49
+ },
50
+ keyframes: {
51
+ bounceIn: {
52
+ '0%': { opacity: '0', transform: 'scale(0.8)' },
53
+ '100%': { opacity: '1', transform: 'scale(1)' }
54
+ },
55
+ fadeUp: {
56
+ '0%': { opacity: '0', transform: 'translateY(8px)' },
57
+ '100%': { opacity: '1', transform: 'translateY(0)' }
58
+ }
59
+ }
60
+ }
61
+ },
62
+ plugins: []
63
+ } satisfies Config;
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "./.svelte-kit/tsconfig.json",
3
+ "compilerOptions": {
4
+ "allowJs": true,
5
+ "checkJs": true,
6
+ "esModuleInterop": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "resolveJsonModule": true,
9
+ "skipLibCheck": true,
10
+ "sourceMap": true,
11
+ "strict": true
12
+ }
13
+ }
@@ -0,0 +1,6 @@
1
+ import { sveltekit } from '@sveltejs/kit/vite';
2
+ import { defineConfig } from 'vite';
3
+
4
+ export default defineConfig({
5
+ plugins: [sveltekit()]
6
+ });