Files
ConceptSketch/src/App.tsx
2026-03-16 14:48:01 +01:00

135 lines
4.4 KiB
TypeScript

import React, { useEffectEvent, useRef, useState } from 'react';
import { Layout, theme, Breadcrumb, Button, Space, Tree } from 'antd';
import Graph, { type GraphHandle } from './components/Graph';
import type { BreadcrumbItemType } from 'antd/es/breadcrumb/Breadcrumb';
import { useKeysdownStore } from './stores/ArrayStore';
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
SaveOutlined,
FolderOpenOutlined,
} from '@ant-design/icons';
import { saveConceptSketch } from './utils/saveGraph';
import { loadConceptSketch } from './utils/loadGraph';
import { useGraphLayersTreeStore } from './stores/TreeStore';
const { Header, Content, Sider } = Layout;
const App: React.FC = () => {
const {
token: { colorBgContainer },
} = theme.useToken();
const [graphLevel, setGraphLevel] = useState<BreadcrumbItemType[]>([])
const [collapsed, setCollapsed] = useState(true);
const addKey = useKeysdownStore(state => state.add);
const removeKey = useKeysdownStore(state => state.remove);
const onKeydown = useEffectEvent((key: string) => {
addKey(key);
});
const onKeyUp = useEffectEvent((key: string) => {
removeKey(key);
})
document.addEventListener('keydown', (ev) => {
onKeydown(ev.key)
});
document.addEventListener('keyup', (ev) => {
onKeyUp(ev.key);
});
const treeData = useGraphLayersTreeStore(store => store.tree);
const nodesFlatById = useGraphLayersTreeStore(store => store.nodesFlatById);
const parentIdByChildId = useGraphLayersTreeStore(store => store.parentIdByChildId);
const graphRef = useRef<GraphHandle>(null);
function buildPathToNode(nodeId: string) {
const path: Array<{ id: string; name: string | undefined }> = [];
let current: string | undefined = nodeId;
while (current && nodesFlatById.has(current)) {
const node = nodesFlatById.get(current)!;
path.unshift({ id: current, name: node.title as string | undefined });
current = parentIdByChildId.get(current);
}
return [{ id: 'main', name: 'Main' }, ...path];
}
return (
<Layout>
<Sider trigger={null} collapsible collapsed={collapsed} collapsedWidth={0} style={{
background: colorBgContainer,
}}>
<Header style={{ padding: 0 }}></Header>
<Tree
checkable
treeData={treeData}
defaultExpandAll={true}
style={{
borderRadius: 0
}}
onSelect={(keys) => {
const nodeId = keys[0] as string;
if (!nodeId) return;
graphRef.current?.navigateTo(nodeId, buildPathToNode(nodeId));
}}
/>
</Sider>
<Layout>
<Header style={{ padding: 0, background: colorBgContainer, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Space>
<div style={{ background: '#001529' }}>
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed(!collapsed)}
style={{
fontSize: '16px',
width: 32,
height: 32,
color: colorBgContainer,
}}
/>
</div>
<Breadcrumb items={graphLevel} />
</Space>
<div style={{ background: '#001529', display: 'flex' }}>
<Button
type="text"
icon={<FolderOpenOutlined />}
onClick={loadConceptSketch}
title="Load JSON"
style={{
fontSize: '16px',
width: 32,
height: 32,
color: colorBgContainer,
}}
/>
<Button
type="text"
icon={<SaveOutlined />}
onClick={saveConceptSketch}
title="Save as JSON"
style={{
fontSize: '16px',
width: 32,
height: 32,
color: colorBgContainer,
}}
/>
</div>
</Header>
<Content
style={{
margin: '8px 16px',
padding: 24,
minHeight: 280,
background: colorBgContainer,
borderRadius: '6px'
}}
>
<Graph ref={graphRef} setGraphPath={setGraphLevel} />
</Content>
</Layout>
</Layout>
);
};
export default App;