@assistkick/create 1.5.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/assistkick-product-system/packages/shared/tools/add_node.ts +4 -11
- 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,93 @@
|
|
|
1
|
+
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { GraphView } from '../components/GraphView';
|
|
3
|
+
import type { GraphViewHandle } from '../components/GraphView';
|
|
4
|
+
import { GraphLegend } from '../components/GraphLegend';
|
|
5
|
+
import { GraphSettings } from '../components/GraphSettings';
|
|
6
|
+
import { useGraphStore } from '../stores/useGraphStore';
|
|
7
|
+
import { useGraphUIStore } from '../stores/useGraphUIStore';
|
|
8
|
+
import { useSidePanelStore } from '../stores/useSidePanelStore';
|
|
9
|
+
|
|
10
|
+
export function GraphRoute() {
|
|
11
|
+
const graphData = useGraphStore((s) => s.graphData);
|
|
12
|
+
const graphError = useGraphStore((s) => s.error);
|
|
13
|
+
const openSidePanel = useSidePanelStore((s) => s.open);
|
|
14
|
+
const settingsOpen = useGraphUIStore((s) => s.settingsOpen);
|
|
15
|
+
const setSettingsOpen = useGraphUIStore((s) => s.setSettingsOpen);
|
|
16
|
+
const setOnFit = useGraphUIStore((s) => s.setOnFit);
|
|
17
|
+
const setOnEdgeClick = useGraphUIStore((s) => s.setOnEdgeClick);
|
|
18
|
+
|
|
19
|
+
const graphRef = useRef<GraphViewHandle>(null);
|
|
20
|
+
const [legendVisible] = useState(true);
|
|
21
|
+
|
|
22
|
+
// Register graph callbacks with the UI store
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
setOnFit(() => graphRef.current?.fitToView());
|
|
25
|
+
setOnEdgeClick((neighborId: string) => {
|
|
26
|
+
const node = graphRef.current?.getNodeById(neighborId);
|
|
27
|
+
if (node) {
|
|
28
|
+
graphRef.current?.focusNode(neighborId);
|
|
29
|
+
openSidePanel(node);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return () => {
|
|
33
|
+
setOnFit(null);
|
|
34
|
+
setOnEdgeClick(null);
|
|
35
|
+
};
|
|
36
|
+
}, [setOnFit, setOnEdgeClick, openSidePanel]);
|
|
37
|
+
|
|
38
|
+
// Window resize
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const handler = () => graphRef.current?.fitToView();
|
|
41
|
+
window.addEventListener('resize', handler);
|
|
42
|
+
return () => window.removeEventListener('resize', handler);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
// Escape clears graph selection
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const handler = (e: KeyboardEvent) => {
|
|
48
|
+
if (e.key === 'Escape') {
|
|
49
|
+
graphRef.current?.clearSelection();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
document.addEventListener('keydown', handler);
|
|
53
|
+
return () => document.removeEventListener('keydown', handler);
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
const handleNodeClick = useCallback(async (node: any) => {
|
|
57
|
+
openSidePanel(node);
|
|
58
|
+
}, [openSidePanel]);
|
|
59
|
+
|
|
60
|
+
const handleTypeToggle = useCallback((newHiddenTypes: Set<string>) => {
|
|
61
|
+
graphRef.current?.setHiddenTypes(newHiddenTypes);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const handleSearchChange = useCallback((query: string) => {
|
|
65
|
+
graphRef.current?.highlightBySearch(query);
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
if (graphError) {
|
|
69
|
+
return (
|
|
70
|
+
<div id="graph-container">
|
|
71
|
+
<div className="error-msg">Failed to load graph data. Is the server running?</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!graphData) return null;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<>
|
|
80
|
+
<GraphView ref={graphRef} graphData={graphData} onNodeClick={handleNodeClick} visible={true} />
|
|
81
|
+
<GraphLegend
|
|
82
|
+
visible={legendVisible}
|
|
83
|
+
onTypeToggle={handleTypeToggle}
|
|
84
|
+
onSearchChange={handleSearchChange}
|
|
85
|
+
/>
|
|
86
|
+
<GraphSettings
|
|
87
|
+
isOpen={settingsOpen}
|
|
88
|
+
onClose={() => setSettingsOpen(false)}
|
|
89
|
+
graphRef={graphRef}
|
|
90
|
+
/>
|
|
91
|
+
</>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { KanbanView } from '../components/KanbanView';
|
|
3
|
+
import { useGraphStore } from '../stores/useGraphStore';
|
|
4
|
+
import { useProjectStore } from '../stores/useProjectStore';
|
|
5
|
+
import { useSidePanelStore } from '../stores/useSidePanelStore';
|
|
6
|
+
import { useQaSheetStore } from '../stores/useQaSheetStore';
|
|
7
|
+
|
|
8
|
+
export function KanbanRoute() {
|
|
9
|
+
const graphData = useGraphStore((s) => s.graphData);
|
|
10
|
+
const selectedProjectId = useProjectStore((s) => s.selectedProjectId);
|
|
11
|
+
const openSidePanel = useSidePanelStore((s) => s.open);
|
|
12
|
+
const openQaSheet = useQaSheetStore((s) => s.open);
|
|
13
|
+
|
|
14
|
+
const handleIssuesClick = useCallback((featureId: string, featureName: string, column: string, notes: any[], reviews: any[]) => {
|
|
15
|
+
openQaSheet(featureId, featureName, column, notes, reviews);
|
|
16
|
+
}, [openQaSheet]);
|
|
17
|
+
|
|
18
|
+
if (!graphData) return null;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex w-full h-full overflow-hidden p-3">
|
|
22
|
+
<KanbanView
|
|
23
|
+
graphData={graphData}
|
|
24
|
+
projectId={selectedProjectId}
|
|
25
|
+
onCardClick={openSidePanel}
|
|
26
|
+
onIssuesClick={handleIssuesClick}
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TerminalView } from '../components/TerminalView';
|
|
3
|
+
import { useProjectStore } from '../stores/useProjectStore';
|
|
4
|
+
|
|
5
|
+
export function TerminalRoute() {
|
|
6
|
+
const projects = useProjectStore((s) => s.projects);
|
|
7
|
+
|
|
8
|
+
return <TerminalView visible={true} projects={projects} />;
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type { Project } from './useProjectStore';
|
|
3
|
+
|
|
4
|
+
interface GitModalState {
|
|
5
|
+
project: Project | null;
|
|
6
|
+
open: (project: Project) => void;
|
|
7
|
+
close: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const useGitModalStore = create<GitModalState>((set) => ({
|
|
11
|
+
project: null,
|
|
12
|
+
open: (project) => set({ project }),
|
|
13
|
+
close: () => set({ project: null }),
|
|
14
|
+
}));
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { apiClient } from '../api/client';
|
|
3
|
+
import { useProjectStore } from './useProjectStore';
|
|
4
|
+
|
|
5
|
+
interface GraphState {
|
|
6
|
+
graphData: any | null;
|
|
7
|
+
error: string | null;
|
|
8
|
+
loading: boolean;
|
|
9
|
+
|
|
10
|
+
fetchGraph: (projectId?: string | null) => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const useGraphStore = create<GraphState>((set) => ({
|
|
14
|
+
graphData: null,
|
|
15
|
+
error: null,
|
|
16
|
+
loading: true,
|
|
17
|
+
|
|
18
|
+
fetchGraph: async (projectId?: string | null) => {
|
|
19
|
+
try {
|
|
20
|
+
set({ loading: true });
|
|
21
|
+
const data = await apiClient.fetchGraph(projectId ?? undefined);
|
|
22
|
+
set({ graphData: data, error: null, loading: false });
|
|
23
|
+
} catch (err: any) {
|
|
24
|
+
set({ error: err.message, loading: false });
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// Auto-fetch when selectedProjectId changes
|
|
30
|
+
let prevProjectId = useProjectStore.getState().selectedProjectId;
|
|
31
|
+
useProjectStore.subscribe((state) => {
|
|
32
|
+
if (state.selectedProjectId !== prevProjectId) {
|
|
33
|
+
prevProjectId = state.selectedProjectId;
|
|
34
|
+
useGraphStore.getState().fetchGraph(state.selectedProjectId);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
|
|
3
|
+
interface GraphUIState {
|
|
4
|
+
settingsOpen: boolean;
|
|
5
|
+
legendVisible: boolean;
|
|
6
|
+
|
|
7
|
+
onFit: (() => void) | null;
|
|
8
|
+
onEdgeClick: ((neighborId: string) => void) | null;
|
|
9
|
+
onSettingsToggle: () => void;
|
|
10
|
+
setOnFit: (fn: (() => void) | null) => void;
|
|
11
|
+
setOnEdgeClick: (fn: ((neighborId: string) => void) | null) => void;
|
|
12
|
+
setSettingsOpen: (open: boolean) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useGraphUIStore = create<GraphUIState>((set) => ({
|
|
16
|
+
settingsOpen: false,
|
|
17
|
+
legendVisible: true,
|
|
18
|
+
onFit: null,
|
|
19
|
+
onEdgeClick: null,
|
|
20
|
+
|
|
21
|
+
onSettingsToggle: () => set((s) => ({ settingsOpen: !s.settingsOpen })),
|
|
22
|
+
setOnFit: (fn) => set({ onFit: fn }),
|
|
23
|
+
setOnEdgeClick: (fn) => set({ onEdgeClick: fn }),
|
|
24
|
+
setSettingsOpen: (open) => set({ settingsOpen: open }),
|
|
25
|
+
}));
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { apiClient } from '../api/client';
|
|
3
|
+
|
|
4
|
+
const STORAGE_KEY = 'selectedProjectId';
|
|
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
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ProjectState {
|
|
20
|
+
projects: Project[];
|
|
21
|
+
selectedProjectId: string | null;
|
|
22
|
+
loading: boolean;
|
|
23
|
+
|
|
24
|
+
init: () => Promise<void>;
|
|
25
|
+
selectProject: (id: string) => void;
|
|
26
|
+
createProject: (name: string) => Promise<Project>;
|
|
27
|
+
renameProject: (id: string, name: string) => Promise<void>;
|
|
28
|
+
archiveProject: (id: string) => Promise<void>;
|
|
29
|
+
refetchProjects: () => Promise<Project[]>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const useProjectStore = create<ProjectState>((set, get) => ({
|
|
33
|
+
projects: [],
|
|
34
|
+
selectedProjectId: localStorage.getItem(STORAGE_KEY),
|
|
35
|
+
loading: true,
|
|
36
|
+
|
|
37
|
+
init: async () => {
|
|
38
|
+
const fetched = await get().refetchProjects();
|
|
39
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
40
|
+
if (stored && fetched.some((p) => p.id === stored)) {
|
|
41
|
+
set({ selectedProjectId: stored });
|
|
42
|
+
} else if (fetched.length > 0) {
|
|
43
|
+
const defaultProject = fetched.find((p) => p.isDefault === 1);
|
|
44
|
+
const fallback = defaultProject ? defaultProject.id : fetched[0].id;
|
|
45
|
+
set({ selectedProjectId: fallback });
|
|
46
|
+
localStorage.setItem(STORAGE_KEY, fallback);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
selectProject: (id) => {
|
|
51
|
+
set({ selectedProjectId: id });
|
|
52
|
+
localStorage.setItem(STORAGE_KEY, id);
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
refetchProjects: async () => {
|
|
56
|
+
try {
|
|
57
|
+
const data = await apiClient.fetchProjects();
|
|
58
|
+
set({ projects: data.projects, loading: false });
|
|
59
|
+
return data.projects as Project[];
|
|
60
|
+
} catch {
|
|
61
|
+
set({ projects: [], loading: false });
|
|
62
|
+
return [] as Project[];
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
createProject: async (name) => {
|
|
67
|
+
const data = await apiClient.createProject(name);
|
|
68
|
+
await get().refetchProjects();
|
|
69
|
+
get().selectProject(data.project.id);
|
|
70
|
+
return data.project;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
renameProject: async (id, name) => {
|
|
74
|
+
await apiClient.renameProject(id, name);
|
|
75
|
+
await get().refetchProjects();
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
archiveProject: async (id) => {
|
|
79
|
+
await apiClient.archiveProject(id);
|
|
80
|
+
const updated = await get().refetchProjects();
|
|
81
|
+
if (get().selectedProjectId === id && updated.length > 0) {
|
|
82
|
+
const defaultProject = updated.find((p) => p.isDefault === 1);
|
|
83
|
+
const fallback = defaultProject ? defaultProject.id : updated[0].id;
|
|
84
|
+
get().selectProject(fallback);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
}));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
|
|
3
|
+
interface QaSheetState {
|
|
4
|
+
isOpen: boolean;
|
|
5
|
+
featureId: string | null;
|
|
6
|
+
featureName: string;
|
|
7
|
+
column: string;
|
|
8
|
+
notes: any[];
|
|
9
|
+
reviews: any[];
|
|
10
|
+
|
|
11
|
+
open: (featureId: string, featureName: string, column: string, notes: any[], reviews: any[]) => void;
|
|
12
|
+
close: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useQaSheetStore = create<QaSheetState>((set) => ({
|
|
16
|
+
isOpen: false,
|
|
17
|
+
featureId: null,
|
|
18
|
+
featureName: '',
|
|
19
|
+
column: '',
|
|
20
|
+
notes: [],
|
|
21
|
+
reviews: [],
|
|
22
|
+
|
|
23
|
+
open: (featureId, featureName, column, notes, reviews) =>
|
|
24
|
+
set({ isOpen: true, featureId, featureName, column, notes, reviews }),
|
|
25
|
+
|
|
26
|
+
close: () => set({ isOpen: false }),
|
|
27
|
+
}));
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { apiClient } from '../api/client';
|
|
3
|
+
|
|
4
|
+
interface WorkSummary {
|
|
5
|
+
cycle: number;
|
|
6
|
+
filesCreated: string[];
|
|
7
|
+
filesUpdated: string[];
|
|
8
|
+
filesDeleted: string[];
|
|
9
|
+
approach: string;
|
|
10
|
+
decisions: string[];
|
|
11
|
+
timestamp: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Tasks {
|
|
15
|
+
total: number;
|
|
16
|
+
completed: number;
|
|
17
|
+
items: any[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SidePanelState {
|
|
21
|
+
isOpen: boolean;
|
|
22
|
+
node: any | null;
|
|
23
|
+
content: string;
|
|
24
|
+
workSummaries: WorkSummary[];
|
|
25
|
+
expandedSummaries: boolean;
|
|
26
|
+
tasks: Tasks | null;
|
|
27
|
+
expandedTasks: boolean;
|
|
28
|
+
|
|
29
|
+
open: (node: any) => Promise<void>;
|
|
30
|
+
close: () => void;
|
|
31
|
+
toggleSummaries: () => void;
|
|
32
|
+
toggleTasks: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const useSidePanelStore = create<SidePanelState>((set) => ({
|
|
36
|
+
isOpen: false,
|
|
37
|
+
node: null,
|
|
38
|
+
content: '',
|
|
39
|
+
workSummaries: [],
|
|
40
|
+
expandedSummaries: false,
|
|
41
|
+
tasks: null,
|
|
42
|
+
expandedTasks: false,
|
|
43
|
+
|
|
44
|
+
open: async (node: any) => {
|
|
45
|
+
set({
|
|
46
|
+
node,
|
|
47
|
+
workSummaries: [],
|
|
48
|
+
expandedSummaries: false,
|
|
49
|
+
tasks: null,
|
|
50
|
+
expandedTasks: false,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const detail = await apiClient.fetchNode(node.id);
|
|
55
|
+
set({ content: detail.content, isOpen: true });
|
|
56
|
+
|
|
57
|
+
if (node.type === 'feature' || node.id?.startsWith('feat_')) {
|
|
58
|
+
try {
|
|
59
|
+
const pStatus = await apiClient.getPipelineStatus(node.id);
|
|
60
|
+
set({
|
|
61
|
+
workSummaries: pStatus?.workSummaries?.length > 0 ? pStatus.workSummaries : [],
|
|
62
|
+
tasks: pStatus?.tasks ?? null,
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
// Pipeline status may not exist for all features
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error('Failed to fetch node:', err);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
close: () => set({ isOpen: false }),
|
|
74
|
+
toggleSummaries: () => set((s) => ({ expandedSummaries: !s.expandedSummaries })),
|
|
75
|
+
toggleTasks: () => set((s) => ({ expandedTasks: !s.expandedTasks })),
|
|
76
|
+
}));
|
|
@@ -1,47 +1,79 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/* Map existing CSS custom properties to Tailwind theme tokens.
|
|
4
|
+
Token names are chosen so generated utility classes read naturally:
|
|
5
|
+
bg-surface, text-content, border-edge, bg-panel, etc. */
|
|
6
|
+
@theme {
|
|
7
|
+
--color-surface: var(--bg-primary);
|
|
8
|
+
--color-surface-alt: var(--bg-secondary);
|
|
9
|
+
--color-surface-raised: var(--bg-tertiary);
|
|
10
|
+
--color-edge: var(--border-color);
|
|
11
|
+
--color-content: var(--text-primary);
|
|
12
|
+
--color-content-secondary: var(--text-secondary);
|
|
13
|
+
--color-content-muted: var(--text-muted);
|
|
14
|
+
--color-accent: var(--accent);
|
|
15
|
+
--color-accent-secondary: var(--accent-secondary);
|
|
16
|
+
--color-completeness-bg: var(--completeness-bg);
|
|
17
|
+
--color-completeness-fill: var(--completeness-fill);
|
|
18
|
+
--color-panel: var(--panel-bg);
|
|
19
|
+
--color-panel-shadow: var(--panel-shadow);
|
|
20
|
+
--color-tab-active: var(--tab-active-bg);
|
|
21
|
+
--color-tab-hover: var(--tab-hover-bg);
|
|
22
|
+
--color-error: var(--error-color);
|
|
23
|
+
|
|
24
|
+
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Fira Code", monospace;
|
|
25
|
+
--font-system: "Inter", system-ui, -apple-system, sans-serif;
|
|
26
|
+
}
|
|
27
|
+
|
|
1
28
|
/* Graph Visualization Web UI — Styles */
|
|
2
29
|
/* Minimal developer tool aesthetic, dark/light theme support */
|
|
3
30
|
|
|
4
31
|
:root {
|
|
5
32
|
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Fira Code", monospace;
|
|
6
|
-
--font-system: system-ui, -apple-system, sans-serif;
|
|
33
|
+
--font-system: "Inter", system-ui, -apple-system, sans-serif;
|
|
7
34
|
}
|
|
8
35
|
|
|
9
36
|
[data-theme="dark"] {
|
|
10
|
-
--bg-primary: #
|
|
11
|
-
--bg-secondary: #
|
|
12
|
-
--bg-tertiary: #
|
|
13
|
-
--border-color: #
|
|
14
|
-
--text-primary: #
|
|
15
|
-
--text-secondary: #
|
|
16
|
-
--text-muted: #
|
|
17
|
-
--accent: #
|
|
18
|
-
--
|
|
19
|
-
--completeness-
|
|
20
|
-
--
|
|
21
|
-
--panel-
|
|
22
|
-
--
|
|
23
|
-
--tab-
|
|
24
|
-
--
|
|
37
|
+
--bg-primary: #0c0c0e;
|
|
38
|
+
--bg-secondary: #18191c;
|
|
39
|
+
--bg-tertiary: #222326;
|
|
40
|
+
--border-color: #2e2f33;
|
|
41
|
+
--text-primary: #f0f0f0;
|
|
42
|
+
--text-secondary: #9a9ba0;
|
|
43
|
+
--text-muted: #56575c;
|
|
44
|
+
--accent: #b5e853;
|
|
45
|
+
--accent-secondary: #f0a050;
|
|
46
|
+
--completeness-bg: #2e2f33;
|
|
47
|
+
--completeness-fill: #b5e853;
|
|
48
|
+
--panel-bg: #18191c;
|
|
49
|
+
--panel-shadow: rgba(0, 0, 0, 0.5);
|
|
50
|
+
--tab-active-bg: #222326;
|
|
51
|
+
--tab-hover-bg: #2e2f33;
|
|
52
|
+
--error-color: #f06060;
|
|
25
53
|
}
|
|
26
54
|
|
|
27
55
|
[data-theme="light"] {
|
|
28
|
-
--bg-primary: #
|
|
56
|
+
--bg-primary: #fafbfc;
|
|
29
57
|
--bg-secondary: #ffffff;
|
|
30
|
-
--bg-tertiary: #
|
|
31
|
-
--border-color: #
|
|
32
|
-
--text-primary: #
|
|
33
|
-
--text-secondary: #
|
|
34
|
-
--text-muted: #
|
|
35
|
-
--accent: #
|
|
36
|
-
--
|
|
37
|
-
--completeness-
|
|
58
|
+
--bg-tertiary: #f4f5f7;
|
|
59
|
+
--border-color: #e8e9ec;
|
|
60
|
+
--text-primary: #1a1a1a;
|
|
61
|
+
--text-secondary: #5a5b60;
|
|
62
|
+
--text-muted: #a0a1a6;
|
|
63
|
+
--accent: #4a9e1e;
|
|
64
|
+
--accent-secondary: #d08530;
|
|
65
|
+
--completeness-bg: #eeeff1;
|
|
66
|
+
--completeness-fill: #4a9e1e;
|
|
38
67
|
--panel-bg: #ffffff;
|
|
39
|
-
--panel-shadow: rgba(0, 0, 0, 0.
|
|
68
|
+
--panel-shadow: rgba(0, 0, 0, 0.08);
|
|
40
69
|
--tab-active-bg: #ffffff;
|
|
41
|
-
--tab-hover-bg: #
|
|
42
|
-
--error-color: #
|
|
70
|
+
--tab-hover-bg: #f0f1f3;
|
|
71
|
+
--error-color: #d93025;
|
|
43
72
|
}
|
|
44
73
|
|
|
74
|
+
/* Legacy CSS wrapped in @layer so Tailwind utilities take precedence */
|
|
75
|
+
@layer base {
|
|
76
|
+
|
|
45
77
|
* {
|
|
46
78
|
margin: 0;
|
|
47
79
|
padding: 0;
|
|
@@ -400,82 +432,12 @@ html, body, #root {
|
|
|
400
432
|
|
|
401
433
|
/* Kanban container */
|
|
402
434
|
#kanban-container {
|
|
403
|
-
display: none;
|
|
404
|
-
width: 100%;
|
|
405
|
-
height: calc(100vh - 44px);
|
|
406
|
-
overflow: auto;
|
|
407
|
-
padding: 12px;
|
|
435
|
+
display: none; /* legacy — KanbanRoute uses Tailwind now */
|
|
408
436
|
color: var(--text-secondary);
|
|
409
437
|
font-size: 12px;
|
|
410
438
|
}
|
|
411
439
|
|
|
412
|
-
/* Kanban board */
|
|
413
|
-
.kanban-board {
|
|
414
|
-
display: flex;
|
|
415
|
-
gap: 12px;
|
|
416
|
-
height: 100%;
|
|
417
|
-
min-width: min-content;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
.kanban-column {
|
|
421
|
-
flex: 1;
|
|
422
|
-
min-width: 220px;
|
|
423
|
-
max-width: 320px;
|
|
424
|
-
display: flex;
|
|
425
|
-
flex-direction: column;
|
|
426
|
-
background: var(--bg-secondary);
|
|
427
|
-
border: 1px solid var(--border-color);
|
|
428
|
-
border-radius: 6px;
|
|
429
|
-
overflow: hidden;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
.kanban-column-header {
|
|
433
|
-
display: flex;
|
|
434
|
-
align-items: center;
|
|
435
|
-
justify-content: space-between;
|
|
436
|
-
padding: 10px 12px;
|
|
437
|
-
border-bottom: 1px solid var(--border-color);
|
|
438
|
-
background: var(--bg-tertiary);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
.kanban-column-title {
|
|
442
|
-
font-weight: 600;
|
|
443
|
-
font-size: 12px;
|
|
444
|
-
color: var(--text-primary);
|
|
445
|
-
text-transform: uppercase;
|
|
446
|
-
letter-spacing: 0.5px;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
.kanban-column-count {
|
|
450
|
-
font-size: 11px;
|
|
451
|
-
color: var(--text-muted);
|
|
452
|
-
background: var(--bg-primary);
|
|
453
|
-
padding: 1px 6px;
|
|
454
|
-
border-radius: 8px;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
.kanban-column-body {
|
|
458
|
-
flex: 1;
|
|
459
|
-
overflow-y: auto;
|
|
460
|
-
padding: 8px;
|
|
461
|
-
display: flex;
|
|
462
|
-
flex-direction: column;
|
|
463
|
-
gap: 8px;
|
|
464
|
-
min-height: 60px;
|
|
465
|
-
transition: background 0.15s;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
.kanban-column-body.drop-target {
|
|
469
|
-
background: rgba(77, 171, 247, 0.05);
|
|
470
|
-
border: 1px dashed var(--accent);
|
|
471
|
-
border-radius: 4px;
|
|
472
|
-
margin: 4px;
|
|
473
|
-
padding: 4px;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
.kanban-column-body.drag-over {
|
|
477
|
-
background: rgba(77, 171, 247, 0.12);
|
|
478
|
-
}
|
|
440
|
+
/* Kanban board — layout now handled by Tailwind in KanbanView.tsx */
|
|
479
441
|
|
|
480
442
|
/* Kanban cards */
|
|
481
443
|
.kanban-card {
|
|
@@ -3804,3 +3766,5 @@ html, body, #root {
|
|
|
3804
3766
|
.git-modal-btn-danger:hover {
|
|
3805
3767
|
background: rgba(255, 107, 107, 0.1);
|
|
3806
3768
|
}
|
|
3769
|
+
|
|
3770
|
+
} /* end @layer base */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineConfig, loadEnv } from 'vite';
|
|
2
2
|
import react from '@vitejs/plugin-react';
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { dirname, resolve } from 'node:path';
|
|
5
6
|
|
|
@@ -12,7 +13,7 @@ export default defineConfig(({ mode }) => {
|
|
|
12
13
|
const backendPort = parseInt(env.PORT || '3000', 10);
|
|
13
14
|
|
|
14
15
|
return {
|
|
15
|
-
plugins: [react()],
|
|
16
|
+
plugins: [tailwindcss(), react()],
|
|
16
17
|
build: {
|
|
17
18
|
outDir: 'dist',
|
|
18
19
|
emptyOutDir: true,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { eq, and } from 'drizzle-orm';
|
|
8
8
|
import { getDb, getProjectRoot as _getProjectRoot } from './db.js';
|
|
9
|
-
import { nodes, edges } from '../db/schema.js';
|
|
9
|
+
import { nodes, edges, projects } from '../db/schema.js';
|
|
10
10
|
|
|
11
11
|
export { getProjectRoot } from './db.js';
|
|
12
12
|
|
|
@@ -26,13 +26,19 @@ export const readGraph = async (projectId?: string) => {
|
|
|
26
26
|
? (() => { const ids = new Set(allNodes.map(n => n.id)); return allEdges.filter(e => ids.has(e.fromId) && ids.has(e.toId)); })()
|
|
27
27
|
: allEdges;
|
|
28
28
|
|
|
29
|
+
let projectRow;
|
|
30
|
+
if (projectId) {
|
|
31
|
+
const rows = await db.select().from(projects).where(eq(projects.id, projectId));
|
|
32
|
+
projectRow = rows[0];
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
return {
|
|
30
36
|
version: '1.0',
|
|
31
37
|
project: {
|
|
32
|
-
name: '',
|
|
38
|
+
name: projectRow?.name || '',
|
|
33
39
|
description: '',
|
|
34
|
-
created_at: '',
|
|
35
|
-
updated_at: '',
|
|
40
|
+
created_at: projectRow?.createdAt || '',
|
|
41
|
+
updated_at: projectRow?.updatedAt || '',
|
|
36
42
|
},
|
|
37
43
|
nodes: allNodes.map(n => ({
|
|
38
44
|
id: n.id,
|
|
@@ -107,7 +113,7 @@ export const patchNode = async (id, fields) => {
|
|
|
107
113
|
throw new Error(`Node not found in graph: ${id}`);
|
|
108
114
|
}
|
|
109
115
|
|
|
110
|
-
const updates = {};
|
|
116
|
+
const updates: Record<string, unknown> = {};
|
|
111
117
|
if (fields.status !== undefined) updates.status = fields.status;
|
|
112
118
|
if (fields.completeness !== undefined) updates.completeness = fields.completeness;
|
|
113
119
|
if (fields.open_questions_count !== undefined) updates.openQuestionsCount = fields.open_questions_count;
|