Compare commits

..

7 Commits

Author SHA1 Message Date
Claude Bot
04387d66a9 fix: sync tree node title on graph node rename (closes #10) 2026-03-31 07:26:09 +00:00
471bff1a8c feature: Modify readme' (#5) from claude/issue-4 into master
Reviewed-on: #5
2026-03-23 14:58:39 +00:00
Claude Bot
9aa28a9fa3 docs: rewrite README to describe ConceptSketch project (closes #4) 2026-03-23 14:55:43 +00:00
4733916523 Merge pull request 'Fix: Breadcrumb disappearing' (#3) from claude/issue-2 into master
Reviewed-on: #3
2026-03-23 14:41:42 +00:00
Claude Bot
eb3a23ab03 fix: resolve stale closure causing breadcrumbs to disappear on back-navigation (closes #2)
The onClick handler in createPathSegment closed over the graphsPath variable
from the render when the segment was created. By the time a breadcrumb was
clicked (after further navigation), that closure was stale, so findIndex
returned -1 and splice(0) wiped the entire breadcrumb array.

Fix: use the functional updater form of setGraphsPath so findIndex runs
against the current state rather than a stale snapshot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 22:57:48 +00:00
bba49e019e Update .gitea/workflows/build.yml 2026-03-22 22:47:12 +00:00
c3f651f421 Update .gitea/workflows/build.yml 2026-03-22 21:04:01 +00:00
4 changed files with 78 additions and 68 deletions

119
README.md
View File

@@ -1,73 +1,70 @@
# React + TypeScript + Vite # ConceptSketch
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. An interactive concept map editor for creating and exploring hierarchical graph diagrams. Build knowledge structures visually by connecting nodes into nested subgraphs.
Currently, two official plugins are available: ## Features
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh - **Interactive graph editing** — click nodes to create children, ctrl+click to multi-select, click edges to remove them
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - **Nested subgraphs** — convert any node into its own subgraph layer for infinite nesting
- **Breadcrumb navigation** — navigate through graph hierarchy with breadcrumb trail
- **Sidebar tree view** — see and navigate the full hierarchical structure at a glance
- **Rename nodes** — right-click any node to rename, create subgraphs, or remove
- **Orphaned nodes** — right-click empty canvas area to create standalone nodes
- **Save & load** — export/import your entire graph as a JSON file
## React Compiler ## Getting Started
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). ### Prerequisites
## Expanding the ESLint configuration - Node.js (v18+)
- npm
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: ### Installation
```js ```bash
export default defineConfig([ npm install
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
``` ```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: ### Development
```js ```bash
// eslint.config.js npm run dev
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
``` ```
Opens at `http://localhost:5173` with hot module replacement.
### Build
```bash
npm run build
```
### Tests
```bash
npx playwright test
```
## Usage
| Action | Result |
|---|---|
| Click a node | Create a new child node |
| Ctrl+click nodes, then click target | Link selected nodes as parents to target |
| Click an edge | Delete that edge |
| Right-click a node | Open context menu (Rename / Subgraph / Remove) |
| Right-click empty area | Create an orphaned (unconnected) node |
| Click sidebar tree item | Navigate to that subgraph |
## Tech Stack
- **React 19** + **TypeScript**
- **Vite** — build tool
- **Viz.js** (Graphviz) — graph rendering
- **D3** — SVG interactivity
- **Ant Design** — UI components
- **Zustand** — state management
## File Format
Graphs are saved as `concept-sketch.json`. The format supports nested subgraphs with recursive structure, preserving the full hierarchy.

View File

@@ -264,12 +264,7 @@ const Graph = forwardRef<GraphHandle, { setGraphPath: React.Dispatch<React.SetSt
title: selectedNodeName, title: selectedNodeName,
key: pathSegmentId, key: pathSegmentId,
onClick: () => { onClick: () => {
const index = graphsPath.findIndex(p => p.key === pathSegmentId); setGraphsPath(prev => prev.slice(0, prev.findIndex(p => p.key === pathSegmentId) + 1));
setGraphsPath(prev => {
prev.splice(index + 1);
return [...prev];
});
selectGraphId(pathSegmentId); selectGraphId(pathSegmentId);
} }
} as BreadcrumbItemType; } as BreadcrumbItemType;

View File

@@ -1,6 +1,7 @@
import { Input, Modal } from "antd"; import { Input, Modal } from "antd";
import { graphContext, type NodeContext } from "./Graph"; import { graphContext, type NodeContext } from "./Graph";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { useGraphLayersTreeStore } from "../stores/TreeStore";
export default function NodeRenameModal({ export default function NodeRenameModal({
nodeContext, nodeContext,
@@ -16,6 +17,7 @@ export default function NodeRenameModal({
} }
const [nodeName, setSelectedNodeName] = useState(nodeContext.nodeName); const [nodeName, setSelectedNodeName] = useState(nodeContext.nodeName);
const graphContextValue = useContext(graphContext)!; const graphContextValue = useContext(graphContext)!;
const renameTreeNode = useGraphLayersTreeStore(state => state.rename);
function renameNode() { function renameNode() {
const node = graphContextValue.graph.nodes.find(n => n.id === nodeContext.nodeId); const node = graphContextValue.graph.nodes.find(n => n.id === nodeContext.nodeId);
@@ -24,6 +26,7 @@ export default function NodeRenameModal({
} }
node.label = nodeName; node.label = nodeName;
graphContextValue.setGraph(prev => ({ ...prev, nodes: graphContextValue.graph.nodes })); graphContextValue.setGraph(prev => ({ ...prev, nodes: graphContextValue.graph.nodes }));
renameTreeNode(nodeContext.nodeId, nodeName);
openRenameModal(false); openRenameModal(false);
} }

View File

@@ -10,6 +10,7 @@ export interface TreeStore {
tree: TreeDataNode[]; tree: TreeDataNode[];
add: (childNode: NodeContext, parentNodeId: string | undefined) => void; add: (childNode: NodeContext, parentNodeId: string | undefined) => void;
remove: (nodeId: string) => void; remove: (nodeId: string) => void;
rename: (nodeId: string, newName: string) => void;
reset: () => void; reset: () => void;
} }
@@ -90,6 +91,20 @@ export const useGraphLayersTreeStore = create<TreeStore>()((set) => ({
tree: createTree([...state.rootNodes], nodesFlatById) tree: createTree([...state.rootNodes], nodesFlatById)
} }
}), }),
rename: (nodeId, newName) => set((state) => {
const node = state.nodesFlatById.get(nodeId);
if (!node) {
return state;
}
const nodesFlatById = new Map(state.nodesFlatById);
nodesFlatById.set(nodeId, { ...node, title: newName });
return {
...state,
nodesFlatById,
rootNodes: [...state.rootNodes],
tree: createTree([...state.rootNodes], nodesFlatById),
};
}),
reset: () => set({ reset: () => set({
nodesFlatById: new Map<React.Key, TreeDataNode>(), nodesFlatById: new Map<React.Key, TreeDataNode>(),
parentIdByChildId: new Map<React.Key, string>(), parentIdByChildId: new Map<React.Key, string>(),