@assistkick/create 1.6.0 → 1.7.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/dist/bin/create.js +0 -0
- package/package.json +7 -9
- package/templates/assistkick-product-system/packages/frontend/index.html +3 -0
- package/templates/assistkick-product-system/packages/frontend/package.json +5 -1
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +16 -7
- package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +363 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +6 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +92 -188
- package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +11 -20
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +15 -70
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +149 -77
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +254 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +216 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +163 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useGraph.ts +6 -21
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +15 -80
- package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +19 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +54 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +93 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +9 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useGitModalStore.ts +14 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphStore.ts +36 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useGraphUIStore.ts +25 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useQaSheetStore.ts +27 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +76 -0
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +64 -100
- package/templates/assistkick-product-system/packages/frontend/vite.config.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/lib/graph.ts +11 -5
- package/templates/skills/assistkick-bootstrap/SKILL.md +3 -3
- package/templates/skills/assistkick-code-reviewer/SKILL.md +2 -2
- package/templates/skills/assistkick-debugger/SKILL.md +2 -2
- package/templates/skills/assistkick-developer/SKILL.md +3 -3
- package/templates/skills/assistkick-interview/SKILL.md +2 -2
- package/templates/assistkick-product-system/packages/frontend/package-lock.json +0 -2666
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Play, AlertTriangle, Lock, CircleDot, Copy,
|
|
4
|
+
} from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
/* ── Demo data ── */
|
|
7
|
+
|
|
8
|
+
interface DemoCard {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
pct: number;
|
|
12
|
+
kind: 'new' | 'improvement' | 'bugfix';
|
|
13
|
+
rejections: number;
|
|
14
|
+
pipeline: 'idle' | 'active' | 'completed' | 'failed' | 'interrupted' | 'blocked';
|
|
15
|
+
blocked: boolean;
|
|
16
|
+
issues: number;
|
|
17
|
+
stage: 'in_progress' | 'in_review' | 'qa';
|
|
18
|
+
tools: { read: number; tools: number; write: number; edit: number };
|
|
19
|
+
ctx: string;
|
|
20
|
+
turns: number;
|
|
21
|
+
cost: string;
|
|
22
|
+
model: string;
|
|
23
|
+
stop: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEMO_CARDS: DemoCard[] = [
|
|
27
|
+
{
|
|
28
|
+
id: 'feat_0f2d6616', name: 'TailwindCSS Integration', pct: 100, kind: 'new',
|
|
29
|
+
rejections: 1, pipeline: 'interrupted', blocked: false, issues: 1,
|
|
30
|
+
stage: 'in_review', tools: { read: 6, tools: 11, write: 0, edit: 0 },
|
|
31
|
+
ctx: '17%', turns: 19, cost: '$0.3948', model: 'opus-4', stop: 'end_turn',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'feat_a3c71b02', name: 'Auth Flow Redesign', pct: 72, kind: 'improvement',
|
|
35
|
+
rejections: 0, pipeline: 'active', blocked: false, issues: 0,
|
|
36
|
+
stage: 'in_progress', tools: { read: 12, tools: 4, write: 3, edit: 7 },
|
|
37
|
+
ctx: '34%', turns: 26, cost: '$0.5120', model: 'sonnet-4', stop: '',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'feat_9e45f1d8', name: 'WebSocket Events', pct: 45, kind: 'new',
|
|
41
|
+
rejections: 3, pipeline: 'failed', blocked: false, issues: 3,
|
|
42
|
+
stage: 'qa', tools: { read: 2, tools: 8, write: 1, edit: 0 },
|
|
43
|
+
ctx: '9%', turns: 11, cost: '$0.1840', model: 'opus-4', stop: 'max_turns',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'feat_b7d20cc4', name: 'Rate Limiter Middleware', pct: 88, kind: 'bugfix',
|
|
47
|
+
rejections: 0, pipeline: 'idle', blocked: true, issues: 0,
|
|
48
|
+
stage: 'in_progress', tools: { read: 0, tools: 0, write: 0, edit: 0 },
|
|
49
|
+
ctx: '', turns: 0, cost: '', model: '', stop: '',
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/* ── Pipeline helpers ── */
|
|
54
|
+
|
|
55
|
+
function pipelineStyle(status: DemoCard['pipeline']): string {
|
|
56
|
+
switch (status) {
|
|
57
|
+
case 'active': return 'bg-accent/10 text-accent animate-pulse';
|
|
58
|
+
case 'completed': return 'bg-emerald-500/10 text-emerald-400';
|
|
59
|
+
case 'failed': return 'bg-error/10 text-error';
|
|
60
|
+
case 'blocked': return 'bg-error/10 text-error';
|
|
61
|
+
case 'interrupted': return 'bg-amber-500/10 text-amber-400';
|
|
62
|
+
default: return '';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pipelineLabel(status: DemoCard['pipeline'], rejections: number): string {
|
|
67
|
+
if (rejections >= 1 && status !== 'idle') return `Retry ${rejections + 1}`;
|
|
68
|
+
switch (status) {
|
|
69
|
+
case 'active': return 'Running...';
|
|
70
|
+
case 'completed': return 'Completed';
|
|
71
|
+
case 'failed': return 'Failed';
|
|
72
|
+
case 'blocked': return 'Blocked';
|
|
73
|
+
case 'interrupted': return 'Interrupted';
|
|
74
|
+
default: return '';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ── Kind badge ── */
|
|
79
|
+
|
|
80
|
+
function KindBadge({ kind }: { kind: DemoCard['kind'] }) {
|
|
81
|
+
if (kind === 'new') return null;
|
|
82
|
+
const cls = kind === 'improvement'
|
|
83
|
+
? 'text-blue-400 bg-blue-400/15'
|
|
84
|
+
: 'text-amber-400 bg-amber-400/15';
|
|
85
|
+
return (
|
|
86
|
+
<span className={`rounded px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide ${cls}`}>
|
|
87
|
+
{kind}
|
|
88
|
+
</span>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ── Glass Card ── */
|
|
93
|
+
|
|
94
|
+
function CardGlass({ card }: { card: DemoCard }) {
|
|
95
|
+
const hasTools = Object.values(card.tools).some(v => v > 0);
|
|
96
|
+
const hasPipeline = card.pipeline !== 'idle';
|
|
97
|
+
const hasStatusRow = card.rejections > 0 || hasPipeline || card.stop;
|
|
98
|
+
const stripColor = card.blocked
|
|
99
|
+
? 'from-error/60 to-error/20'
|
|
100
|
+
: card.rejections >= 3
|
|
101
|
+
? 'from-error/40 to-amber-500/20'
|
|
102
|
+
: card.pipeline === 'active'
|
|
103
|
+
? 'from-accent/60 to-accent/20'
|
|
104
|
+
: card.pipeline === 'completed'
|
|
105
|
+
? 'from-emerald-400/60 to-emerald-400/20'
|
|
106
|
+
: 'from-edge to-transparent';
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="group relative overflow-hidden rounded-2xl bg-surface border border-edge shadow-lg shadow-black/10 backdrop-blur-sm transition-all duration-200 hover:border-content/15 hover:shadow-xl hover:shadow-black/15">
|
|
110
|
+
<div className={`absolute left-0 top-0 h-full w-1 bg-gradient-to-b ${stripColor}`} />
|
|
111
|
+
<div className="p-5 pl-6">
|
|
112
|
+
{/* Header */}
|
|
113
|
+
<div className="flex items-center gap-2.5">
|
|
114
|
+
<span className="font-mono text-[12px] text-content-secondary">{card.id}</span>
|
|
115
|
+
<KindBadge kind={card.kind} />
|
|
116
|
+
<button className="flex h-6 w-6 items-center justify-center rounded-md bg-white/[0.08] text-content-secondary transition-colors hover:bg-white/15 hover:text-content cursor-pointer">
|
|
117
|
+
<Copy size={12} strokeWidth={2} />
|
|
118
|
+
</button>
|
|
119
|
+
<div className="flex-1" />
|
|
120
|
+
{card.blocked ? (
|
|
121
|
+
<span className="flex items-center gap-1.5 rounded-full bg-error/15 px-2.5 py-1 text-[11px] font-bold text-error backdrop-blur">
|
|
122
|
+
<Lock size={11} strokeWidth={2.5} /> Blocked
|
|
123
|
+
</span>
|
|
124
|
+
) : card.pipeline === 'idle' ? (
|
|
125
|
+
<button className="flex h-7 w-7 items-center justify-center rounded-full bg-accent/10 text-accent backdrop-blur transition-all hover:bg-accent hover:text-surface hover:shadow-[0_0_12px_-2px_var(--accent)] cursor-pointer">
|
|
126
|
+
<Play size={12} strokeWidth={2.5} fill="currentColor" />
|
|
127
|
+
</button>
|
|
128
|
+
) : null}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Name */}
|
|
132
|
+
<div className="mt-3 text-[15px] font-semibold leading-snug text-content">{card.name}</div>
|
|
133
|
+
|
|
134
|
+
{/* Spec disc */}
|
|
135
|
+
<div className="mt-3.5 flex items-center gap-2.5">
|
|
136
|
+
<svg width="24" height="24" viewBox="0 0 24 24" className="shrink-0 -rotate-90">
|
|
137
|
+
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth="2.5" className="text-white/5" />
|
|
138
|
+
<circle
|
|
139
|
+
cx="12" cy="12" r="10" fill="none" strokeWidth="2.5"
|
|
140
|
+
stroke="url(#specGrad)" strokeLinecap="round"
|
|
141
|
+
strokeDasharray={`${(card.pct / 100) * 2 * Math.PI * 10} ${2 * Math.PI * 10}`}
|
|
142
|
+
/>
|
|
143
|
+
<defs>
|
|
144
|
+
<linearGradient id="specGrad" x1="0" y1="0" x2="1" y2="1">
|
|
145
|
+
<stop offset="0%" stopColor="var(--completeness-fill)" />
|
|
146
|
+
<stop offset="100%" stopColor="var(--accent)" stopOpacity="0.6" />
|
|
147
|
+
</linearGradient>
|
|
148
|
+
</defs>
|
|
149
|
+
</svg>
|
|
150
|
+
<span className="font-mono text-[12px] text-content-secondary">{card.pct}%</span>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{/* Stats — evenly spaced pills */}
|
|
154
|
+
{(hasTools || hasStatusRow) && (
|
|
155
|
+
<div className="mt-3.5 space-y-2">
|
|
156
|
+
{hasTools && (
|
|
157
|
+
<div className="flex gap-1.5">
|
|
158
|
+
{Object.entries(card.tools).filter(([, v]) => v > 0).map(([k, v]) => (
|
|
159
|
+
<span key={k} className="flex-1 rounded-full bg-accent/10 py-1 text-center font-mono text-[11px] font-medium text-accent backdrop-blur">
|
|
160
|
+
{k.charAt(0).toUpperCase() + k.slice(1)}: {v}
|
|
161
|
+
</span>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
{hasTools && (
|
|
166
|
+
<div className="flex gap-1.5">
|
|
167
|
+
{[card.ctx && `Ctx ${card.ctx}`, card.turns > 0 && `${card.turns}t`, card.cost, card.model].filter(Boolean).map(t => (
|
|
168
|
+
<span key={t as string} className="flex-1 rounded-full bg-white/[0.08] py-1 text-center font-mono text-[11px] text-content backdrop-blur">{t}</span>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
{hasStatusRow && (
|
|
173
|
+
<div className="flex gap-1.5">
|
|
174
|
+
{card.rejections > 0 && (
|
|
175
|
+
<span className="flex-1 rounded-full bg-amber-400/15 py-1 text-center text-[11px] font-semibold text-amber-400 backdrop-blur">
|
|
176
|
+
{card.rejections}x rejected
|
|
177
|
+
</span>
|
|
178
|
+
)}
|
|
179
|
+
{hasPipeline && (
|
|
180
|
+
<span className={`flex-1 inline-flex items-center justify-center gap-1.5 rounded-full py-1 text-[11px] font-semibold backdrop-blur ${pipelineStyle(card.pipeline)}`}>
|
|
181
|
+
{card.pipeline === 'active' && <CircleDot size={12} strokeWidth={2.5} />}
|
|
182
|
+
{card.pipeline === 'failed' && <AlertTriangle size={12} strokeWidth={2.5} />}
|
|
183
|
+
{pipelineLabel(card.pipeline, card.rejections)}
|
|
184
|
+
</span>
|
|
185
|
+
)}
|
|
186
|
+
{card.stop && (
|
|
187
|
+
<span className="flex-1 rounded-full bg-accent-secondary/10 py-1 text-center font-mono text-[11px] text-accent-secondary backdrop-blur">{card.stop}</span>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{/* Issues */}
|
|
195
|
+
<button className={[
|
|
196
|
+
'mt-3.5 w-full rounded-full border py-2 text-center text-[12px] font-mono backdrop-blur transition-all cursor-pointer',
|
|
197
|
+
card.issues > 0
|
|
198
|
+
? 'border-white/10 text-content-secondary hover:border-accent/30 hover:text-accent'
|
|
199
|
+
: 'border-white/5 text-content-muted/50 hover:border-white/10 hover:text-content-muted',
|
|
200
|
+
].join(' ')}>
|
|
201
|
+
{card.issues > 0 ? `${card.issues} issue${card.issues !== 1 ? 's' : ''} reported` : 'No issues'}
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ── Showcase export ── */
|
|
209
|
+
|
|
210
|
+
export function KanbanCardShowcase() {
|
|
211
|
+
return (
|
|
212
|
+
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
|
213
|
+
{DEMO_CARDS.map(c => <CardGlass key={c.id} card={c} />)}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Search } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export interface NavItem {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: React.ReactNode;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface NavBarSidekickProps {
|
|
12
|
+
/** Navigation items shown in the segmented control */
|
|
13
|
+
items: NavItem[];
|
|
14
|
+
/** Currently active item id — controlled externally */
|
|
15
|
+
activeId?: string;
|
|
16
|
+
/** Called when a nav item is selected */
|
|
17
|
+
onNavigate?: (id: string) => void;
|
|
18
|
+
/** Left-side brand / project slot */
|
|
19
|
+
brand?: React.ReactNode;
|
|
20
|
+
/** Content between the segmented control and the right-side actions */
|
|
21
|
+
center?: React.ReactNode;
|
|
22
|
+
/** Right-side actions rendered before the search trigger */
|
|
23
|
+
actions?: React.ReactNode;
|
|
24
|
+
/** Far-right trailing element (avatar, etc.) */
|
|
25
|
+
trailing?: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function NavBarSidekick({
|
|
29
|
+
items, activeId, onNavigate, brand, center, actions, trailing,
|
|
30
|
+
}: NavBarSidekickProps) {
|
|
31
|
+
// Fallback to internal state when no external activeId is provided (design system demo mode)
|
|
32
|
+
const [internalActive, setInternalActive] = useState(items[0]?.id ?? '');
|
|
33
|
+
const active = activeId ?? internalActive;
|
|
34
|
+
|
|
35
|
+
const [cmdOpen, setCmdOpen] = useState(false);
|
|
36
|
+
const [cmdQuery, setCmdQuery] = useState('');
|
|
37
|
+
|
|
38
|
+
const filteredItems = cmdQuery
|
|
39
|
+
? items.filter((i) =>
|
|
40
|
+
i.label.toLowerCase().includes(cmdQuery.toLowerCase()) ||
|
|
41
|
+
(i.description?.toLowerCase().includes(cmdQuery.toLowerCase()) ?? false))
|
|
42
|
+
: items;
|
|
43
|
+
|
|
44
|
+
const handleSelect = (id: string) => {
|
|
45
|
+
if (onNavigate) {
|
|
46
|
+
onNavigate(id);
|
|
47
|
+
} else {
|
|
48
|
+
setInternalActive(id);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<nav className="bg-surface-alt">
|
|
54
|
+
{/* Top bar */}
|
|
55
|
+
<div className="flex items-center gap-3 px-5 py-2">
|
|
56
|
+
{brand && <div className="mr-1 shrink-0">{brand}</div>}
|
|
57
|
+
|
|
58
|
+
{/* Segmented control */}
|
|
59
|
+
<div className="flex items-center rounded-xl bg-surface p-1 border border-edge">
|
|
60
|
+
{items.map((item) => {
|
|
61
|
+
const isActive = active === item.id;
|
|
62
|
+
return (
|
|
63
|
+
<button
|
|
64
|
+
key={item.id}
|
|
65
|
+
onClick={() => handleSelect(item.id)}
|
|
66
|
+
className={[
|
|
67
|
+
'flex items-center gap-1.5 rounded-lg px-3.5 py-1.5',
|
|
68
|
+
'text-[12px] font-semibold uppercase tracking-wider',
|
|
69
|
+
'transition-all duration-150 cursor-pointer outline-none',
|
|
70
|
+
isActive
|
|
71
|
+
? 'bg-accent text-surface shadow-sm'
|
|
72
|
+
: 'text-content-muted hover:text-content hover:bg-surface-raised',
|
|
73
|
+
].join(' ')}
|
|
74
|
+
style={isActive ? { boxShadow: '0 2px 8px -2px var(--accent)' } : {}}
|
|
75
|
+
>
|
|
76
|
+
{item.icon && <span className="flex items-center">{item.icon}</span>}
|
|
77
|
+
{item.label}
|
|
78
|
+
</button>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div className="flex-1">
|
|
84
|
+
{center}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Actions slot */}
|
|
88
|
+
{actions}
|
|
89
|
+
|
|
90
|
+
{/* Command palette trigger */}
|
|
91
|
+
<button
|
|
92
|
+
data-cmd-trigger
|
|
93
|
+
onClick={() => { setCmdOpen(!cmdOpen); setCmdQuery(''); }}
|
|
94
|
+
className={[
|
|
95
|
+
'flex items-center gap-2 rounded-lg border px-3 py-1.5',
|
|
96
|
+
'text-[12px] transition-all duration-150 cursor-pointer outline-none',
|
|
97
|
+
cmdOpen
|
|
98
|
+
? 'border-accent/40 bg-surface text-content'
|
|
99
|
+
: 'border-edge text-content-muted hover:border-content/20 hover:text-content-secondary',
|
|
100
|
+
].join(' ')}
|
|
101
|
+
>
|
|
102
|
+
<Search size={13} strokeWidth={2} />
|
|
103
|
+
<span className="hidden sm:inline">Search</span>
|
|
104
|
+
<kbd className="hidden sm:inline-flex items-center rounded border border-edge bg-surface-raised px-1.5 py-0.5 text-[10px] font-mono text-content-muted">
|
|
105
|
+
/
|
|
106
|
+
</kbd>
|
|
107
|
+
</button>
|
|
108
|
+
|
|
109
|
+
{trailing && <div className="ml-1 shrink-0">{trailing}</div>}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Command palette dropdown */}
|
|
113
|
+
{cmdOpen && (
|
|
114
|
+
<div className="border-t border-edge bg-surface px-5 py-3">
|
|
115
|
+
<input
|
|
116
|
+
autoFocus
|
|
117
|
+
value={cmdQuery}
|
|
118
|
+
onChange={(e) => setCmdQuery(e.target.value)}
|
|
119
|
+
placeholder="Type to search pages..."
|
|
120
|
+
className="w-full bg-transparent text-[13px] text-content outline-none placeholder:text-content-muted"
|
|
121
|
+
onKeyDown={(e) => {
|
|
122
|
+
if (e.key === 'Escape') { setCmdOpen(false); setCmdQuery(''); }
|
|
123
|
+
if (e.key === 'Enter' && filteredItems.length > 0) {
|
|
124
|
+
handleSelect(filteredItems[0].id);
|
|
125
|
+
setCmdOpen(false);
|
|
126
|
+
setCmdQuery('');
|
|
127
|
+
}
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
{cmdQuery && (
|
|
131
|
+
<div className="mt-2 flex flex-col gap-1">
|
|
132
|
+
{filteredItems.length === 0 ? (
|
|
133
|
+
<div className="py-2 text-[12px] text-content-muted">No results</div>
|
|
134
|
+
) : (
|
|
135
|
+
filteredItems.map((item) => (
|
|
136
|
+
<button
|
|
137
|
+
key={item.id}
|
|
138
|
+
onClick={() => { handleSelect(item.id); setCmdOpen(false); setCmdQuery(''); }}
|
|
139
|
+
className={[
|
|
140
|
+
'flex items-center gap-3 rounded-lg px-3 py-2 text-left',
|
|
141
|
+
'text-[13px] transition-colors duration-100 cursor-pointer outline-none',
|
|
142
|
+
active === item.id
|
|
143
|
+
? 'bg-accent/10 text-accent'
|
|
144
|
+
: 'text-content-secondary hover:bg-surface-raised hover:text-content',
|
|
145
|
+
].join(' ')}
|
|
146
|
+
>
|
|
147
|
+
{item.icon && <span className="flex items-center">{item.icon}</span>}
|
|
148
|
+
<div>
|
|
149
|
+
<div className="font-medium">{item.label}</div>
|
|
150
|
+
{item.description && (
|
|
151
|
+
<div className="text-[11px] text-content-muted">{item.description}</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
</button>
|
|
155
|
+
))
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</nav>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -1,27 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useGraphStore } from '../stores/useGraphStore';
|
|
3
3
|
|
|
4
4
|
export function useGraph(projectId?: string | null) {
|
|
5
|
-
const
|
|
6
|
-
const [error, setError] = useState<string | null>(null);
|
|
7
|
-
const [loading, setLoading] = useState(true);
|
|
8
|
-
|
|
9
|
-
const fetchGraph = useCallback(async () => {
|
|
10
|
-
try {
|
|
11
|
-
setLoading(true);
|
|
12
|
-
const data = await apiClient.fetchGraph(projectId ?? undefined);
|
|
13
|
-
setGraphData(data);
|
|
14
|
-
setError(null);
|
|
15
|
-
} catch (err: any) {
|
|
16
|
-
setError(err.message);
|
|
17
|
-
} finally {
|
|
18
|
-
setLoading(false);
|
|
19
|
-
}
|
|
20
|
-
}, [projectId]);
|
|
5
|
+
const { graphData, error, loading, fetchGraph } = useGraphStore();
|
|
21
6
|
|
|
22
7
|
useEffect(() => {
|
|
23
|
-
fetchGraph();
|
|
24
|
-
}, [fetchGraph]);
|
|
8
|
+
fetchGraph(projectId);
|
|
9
|
+
}, [projectId, fetchGraph]);
|
|
25
10
|
|
|
26
|
-
return { graphData, error, loading, refetch: fetchGraph };
|
|
11
|
+
return { graphData, error, loading, refetch: () => fetchGraph(projectId) };
|
|
27
12
|
}
|
|
@@ -1,90 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useProjectStore } from '../stores/useProjectStore';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export interface Project {
|
|
7
|
-
id: string;
|
|
8
|
-
name: string;
|
|
9
|
-
isDefault: number;
|
|
10
|
-
archivedAt: string | null;
|
|
11
|
-
repoUrl: string | null;
|
|
12
|
-
githubInstallationId: string | null;
|
|
13
|
-
githubRepoFullName: string | null;
|
|
14
|
-
baseBranch: string | null;
|
|
15
|
-
createdAt: string;
|
|
16
|
-
updatedAt: string;
|
|
17
|
-
}
|
|
4
|
+
export type { Project } from '../stores/useProjectStore';
|
|
18
5
|
|
|
19
6
|
export function useProjects() {
|
|
20
|
-
const
|
|
21
|
-
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
|
|
22
|
-
() => localStorage.getItem(STORAGE_KEY),
|
|
23
|
-
);
|
|
24
|
-
const [loading, setLoading] = useState(true);
|
|
25
|
-
|
|
26
|
-
const fetchProjects = useCallback(async () => {
|
|
27
|
-
try {
|
|
28
|
-
const data = await apiClient.fetchProjects();
|
|
29
|
-
setProjects(data.projects);
|
|
30
|
-
return data.projects as Project[];
|
|
31
|
-
} catch {
|
|
32
|
-
setProjects([]);
|
|
33
|
-
return [] as Project[];
|
|
34
|
-
} finally {
|
|
35
|
-
setLoading(false);
|
|
36
|
-
}
|
|
37
|
-
}, []);
|
|
7
|
+
const store = useProjectStore();
|
|
38
8
|
|
|
39
9
|
useEffect(() => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (stored && fetched.some((p: Project) => p.id === stored)) {
|
|
43
|
-
setSelectedProjectId(stored);
|
|
44
|
-
} else if (fetched.length > 0) {
|
|
45
|
-
const defaultProject = fetched.find((p: Project) => p.isDefault === 1);
|
|
46
|
-
const fallback = defaultProject ? defaultProject.id : fetched[0].id;
|
|
47
|
-
setSelectedProjectId(fallback);
|
|
48
|
-
localStorage.setItem(STORAGE_KEY, fallback);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}, [fetchProjects]);
|
|
52
|
-
|
|
53
|
-
const selectProject = useCallback((id: string) => {
|
|
54
|
-
setSelectedProjectId(id);
|
|
55
|
-
localStorage.setItem(STORAGE_KEY, id);
|
|
56
|
-
}, []);
|
|
57
|
-
|
|
58
|
-
const createProject = useCallback(async (name: string) => {
|
|
59
|
-
const data = await apiClient.createProject(name);
|
|
60
|
-
await fetchProjects();
|
|
61
|
-
selectProject(data.project.id);
|
|
62
|
-
return data.project;
|
|
63
|
-
}, [fetchProjects, selectProject]);
|
|
64
|
-
|
|
65
|
-
const renameProject = useCallback(async (id: string, name: string) => {
|
|
66
|
-
await apiClient.renameProject(id, name);
|
|
67
|
-
await fetchProjects();
|
|
68
|
-
}, [fetchProjects]);
|
|
69
|
-
|
|
70
|
-
const archiveProject = useCallback(async (id: string) => {
|
|
71
|
-
await apiClient.archiveProject(id);
|
|
72
|
-
const updated = await fetchProjects();
|
|
73
|
-
if (selectedProjectId === id && updated.length > 0) {
|
|
74
|
-
const defaultProject = updated.find((p: Project) => p.isDefault === 1);
|
|
75
|
-
const fallback = defaultProject ? defaultProject.id : updated[0].id;
|
|
76
|
-
selectProject(fallback);
|
|
10
|
+
if (store.loading) {
|
|
11
|
+
store.init();
|
|
77
12
|
}
|
|
78
|
-
}, [
|
|
13
|
+
}, []);
|
|
79
14
|
|
|
80
15
|
return {
|
|
81
|
-
projects,
|
|
82
|
-
selectedProjectId,
|
|
83
|
-
loading,
|
|
84
|
-
selectProject,
|
|
85
|
-
createProject,
|
|
86
|
-
renameProject,
|
|
87
|
-
archiveProject,
|
|
88
|
-
refetchProjects:
|
|
16
|
+
projects: store.projects,
|
|
17
|
+
selectedProjectId: store.selectedProjectId,
|
|
18
|
+
loading: store.loading,
|
|
19
|
+
selectProject: store.selectProject,
|
|
20
|
+
createProject: store.createProject,
|
|
21
|
+
renameProject: store.renameProject,
|
|
22
|
+
archiveProject: store.archiveProject,
|
|
23
|
+
refetchProjects: store.refetchProjects,
|
|
89
24
|
};
|
|
90
25
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { CoherenceView } from '../components/CoherenceView';
|
|
3
|
+
import { useGraphStore } from '../stores/useGraphStore';
|
|
4
|
+
import { useProjectStore } from '../stores/useProjectStore';
|
|
5
|
+
import { useSidePanelStore } from '../stores/useSidePanelStore';
|
|
6
|
+
|
|
7
|
+
export function CoherenceRoute() {
|
|
8
|
+
const graphData = useGraphStore((s) => s.graphData);
|
|
9
|
+
const selectedProjectId = useProjectStore((s) => s.selectedProjectId);
|
|
10
|
+
const openSidePanel = useSidePanelStore((s) => s.open);
|
|
11
|
+
|
|
12
|
+
if (!graphData) return null;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div id="coherence-container" style={{ display: 'block' }}>
|
|
16
|
+
<CoherenceView graphData={graphData} onNodeClick={openSidePanel} projectId={selectedProjectId} />
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { Outlet } from 'react-router-dom';
|
|
3
|
+
import { Toolbar } from '../components/Toolbar';
|
|
4
|
+
import { SidePanel } from '../components/SidePanel';
|
|
5
|
+
import { QaIssueSheet } from '../components/QaIssueSheet';
|
|
6
|
+
import { GitRepoModal } from '../components/GitRepoModal';
|
|
7
|
+
import { useSidePanelStore } from '../stores/useSidePanelStore';
|
|
8
|
+
import { useQaSheetStore } from '../stores/useQaSheetStore';
|
|
9
|
+
import { useGitModalStore } from '../stores/useGitModalStore';
|
|
10
|
+
import { useGraphUIStore } from '../stores/useGraphUIStore';
|
|
11
|
+
import { useProjects } from '../hooks/useProjects';
|
|
12
|
+
import { useGraph } from '../hooks/useGraph';
|
|
13
|
+
import { ToastProvider } from '../hooks/useToast';
|
|
14
|
+
|
|
15
|
+
export function DashboardLayout() {
|
|
16
|
+
const { selectedProjectId } = useProjects();
|
|
17
|
+
// Keep graph data fetching at layout level so it's available for all views
|
|
18
|
+
useGraph(selectedProjectId);
|
|
19
|
+
|
|
20
|
+
const closeSidePanel = useSidePanelStore((s) => s.close);
|
|
21
|
+
const closeQaSheet = useQaSheetStore((s) => s.close);
|
|
22
|
+
const gitModalProject = useGitModalStore((s) => s.project);
|
|
23
|
+
const setSettingsOpen = useGraphUIStore((s) => s.setSettingsOpen);
|
|
24
|
+
|
|
25
|
+
// Global keyboard handler
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const handler = (e: KeyboardEvent) => {
|
|
28
|
+
if (e.key === 'Escape') {
|
|
29
|
+
closeSidePanel();
|
|
30
|
+
setSettingsOpen(false);
|
|
31
|
+
closeQaSheet();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
document.addEventListener('keydown', handler);
|
|
35
|
+
return () => document.removeEventListener('keydown', handler);
|
|
36
|
+
}, [closeSidePanel, closeQaSheet, setSettingsOpen]);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ToastProvider>
|
|
40
|
+
<div className="app-container">
|
|
41
|
+
<Toolbar />
|
|
42
|
+
|
|
43
|
+
<div className="main-content">
|
|
44
|
+
<Outlet />
|
|
45
|
+
|
|
46
|
+
<SidePanel />
|
|
47
|
+
<QaIssueSheet />
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{gitModalProject && <GitRepoModal />}
|
|
51
|
+
</div>
|
|
52
|
+
</ToastProvider>
|
|
53
|
+
);
|
|
54
|
+
}
|