Init commit

This commit is contained in:
NikitaKalitov
2026-07-13 15:44:13 +03:00
commit 5b6bdc0a1f
22 changed files with 2193 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
+32
View File
@@ -0,0 +1,32 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
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).
## Expanding the Oxlint configuration
If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
```json
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"options": {
"typeAware": true
},
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
```
See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>portal-chart-builder</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+1605
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "portal-chart-builder",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "oxlint",
"preview": "vite preview"
},
"dependencies": {
"@xyflow/react": "^12.11.2",
"react": "^19.2.7",
"react-dom": "^19.2.7"
},
"devDependencies": {
"@types/node": "^24.13.2",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"oxlint": "^1.71.0",
"typescript": "~6.0.2",
"vite": "^8.1.1"
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

+31
View File
@@ -0,0 +1,31 @@
.app-shell {
display: flex;
height: 100%;
}
.app-canvas {
flex: 1;
min-width: 0;
}
.app-inspector {
width: 360px;
flex-shrink: 0;
border-left: 1px solid color-mix(in srgb, currentcolor 20%, transparent);
padding: 12px;
overflow: auto;
}
.app-inspector h2 {
margin: 0 0 8px;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.04em;
opacity: 0.7;
}
.app-inspector pre {
font-size: 12px;
white-space: pre-wrap;
word-break: break-word;
}
+54
View File
@@ -0,0 +1,54 @@
import { useCallback, useState } from 'react';
import { DiagramEditor } from './DiagramEditor';
import { createDevAdapter } from './host/devAdapter';
import type { DiagramDocument } from './types';
import './App.css';
const SAMPLE_DOCUMENT: DiagramDocument = {
nodes: [
{ id: 'start', type: 'event', position: { x: 0, y: 80 }, data: { label: 'Заявка получена' } },
{
id: 'review',
type: 'task',
position: { x: 220, y: 60 },
data: {
label: 'Приём заявок',
description: 'Приём заявок проводится ответственным лицом, занимает 1-2 дня.',
responsible: 'Иван Иванов',
duration: '1-2 дня',
linkType: 'description',
},
},
{ id: 'decision', type: 'gateway', position: { x: 480, y: 68 }, data: { label: 'Одобрено?' } },
],
edges: [
{ id: 'start-review', source: 'start', target: 'review' },
{ id: 'review-decision', source: 'review', target: 'decision' },
],
};
const adapter = createDevAdapter();
function App() {
const [initialDoc] = useState<DiagramDocument>(() => adapter.getInitialValue() ?? SAMPLE_DOCUMENT);
const [doc, setDoc] = useState<DiagramDocument>(initialDoc);
const handleChange = useCallback((next: DiagramDocument) => {
adapter.onChange(next);
setDoc(next);
}, []);
return (
<div className="app-shell">
<div className="app-canvas">
<DiagramEditor initialValue={initialDoc} onChange={handleChange} />
</div>
<aside className="app-inspector">
<h2>Текущий doc (для демо)</h2>
<pre>{JSON.stringify(doc, null, 2)}</pre>
</aside>
</div>
);
}
export default App;
+114
View File
@@ -0,0 +1,114 @@
import { useCallback, useMemo, useState } from 'react';
import {
ReactFlow,
ReactFlowProvider,
Background,
Controls,
MiniMap,
addEdge,
applyNodeChanges,
applyEdgeChanges,
type Node,
type Edge,
type OnConnect,
type NodeChange,
type EdgeChange,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { nodeTypes } from './nodes';
import { debounce, serializeDocument } from './serialize';
import type { DiagramDocument } from './types';
import './diagram.css';
interface DiagramEditorProps {
initialValue: DiagramDocument | null;
onChange: (doc: DiagramDocument) => void;
}
function toRfNodes(doc: DiagramDocument | null): Node[] {
return (doc?.nodes ?? []).map((node) => ({
id: node.id,
type: node.type,
position: node.position,
data: node.data,
}));
}
function toRfEdges(doc: DiagramDocument | null): Edge[] {
return (doc?.edges ?? []).map((edge) => ({
id: edge.id,
source: edge.source,
target: edge.target,
type: edge.type,
data: edge.data,
}));
}
function DiagramCanvas({ initialValue, onChange }: DiagramEditorProps) {
const [nodes, setNodes] = useState<Node[]>(() => toRfNodes(initialValue));
const [edges, setEdges] = useState<Edge[]>(() => toRfEdges(initialValue));
const emitChange = useMemo(
() => debounce((n: Node[], e: Edge[]) => onChange(serializeDocument(n, e)), 400),
[onChange],
);
const onNodesChange = useCallback(
(changes: NodeChange[]) => {
setNodes((current) => {
const next = applyNodeChanges(changes, current);
emitChange(next, edges);
return next;
});
},
[edges, emitChange],
);
const onEdgesChange = useCallback(
(changes: EdgeChange[]) => {
setEdges((current) => {
const next = applyEdgeChanges(changes, current);
emitChange(nodes, next);
return next;
});
},
[nodes, emitChange],
);
const onConnect: OnConnect = useCallback(
(connection) => {
setEdges((current) => {
const next = addEdge(connection, current);
emitChange(nodes, next);
return next;
});
},
[nodes, emitChange],
);
return (
<div className="diagram-canvas">
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
>
<Background />
<Controls />
<MiniMap />
</ReactFlow>
</div>
);
}
export function DiagramEditor(props: DiagramEditorProps) {
return (
<ReactFlowProvider>
<DiagramCanvas {...props} />
</ReactFlowProvider>
);
}
+44
View File
@@ -0,0 +1,44 @@
.diagram-canvas {
width: 100%;
height: 100%;
}
.diagram-node {
min-width: 120px;
padding: 10px 16px;
border: 1px solid #555;
border-radius: 6px;
background: canvas;
color: canvastext;
font-size: 12px;
text-align: center;
}
.diagram-node--gateway {
width: 64px;
height: 64px;
min-width: 0;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transform: rotate(45deg);
}
.diagram-node--gateway span {
display: inline-block;
transform: rotate(-45deg);
font-size: 11px;
}
.diagram-node--event {
width: 64px;
height: 64px;
min-width: 0;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
+12
View File
@@ -0,0 +1,12 @@
import type { DiagramDocument } from '../types';
/**
* Seam between the diagram app and whatever embeds it.
* Dev builds read/write localStorage; the Bitrix build will read the
* initial value from the bridged hidden input and push changes back
* to it (see bitrix-diagram-constructor-summary.md).
*/
export interface HostAdapter {
getInitialValue: () => DiagramDocument | null;
onChange: (doc: DiagramDocument) => void;
}
+21
View File
@@ -0,0 +1,21 @@
import type { DiagramDocument } from '../types';
import type { HostAdapter } from './HostAdapter';
const STORAGE_KEY = 'diagram-builder:dev-doc';
export function createDevAdapter(): HostAdapter {
return {
getInitialValue() {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
try {
return JSON.parse(raw) as DiagramDocument;
} catch {
return null;
}
},
onChange(doc) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(doc));
},
};
}
+15
View File
@@ -0,0 +1,15 @@
:root {
color-scheme: light dark;
font: 14px/1.4 system-ui, 'Segoe UI', Roboto, sans-serif;
}
* {
box-sizing: border-box;
}
html,
body,
#root {
height: 100%;
margin: 0;
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
+46
View File
@@ -0,0 +1,46 @@
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
import type { DiagramNodeData, NodeKind } from './types';
type ShapeNode = Node<DiagramNodeData, NodeKind>;
function ShapeHandles() {
return (
<>
<Handle type="target" position={Position.Left} />
<Handle type="source" position={Position.Right} />
</>
);
}
function TaskNode({ data }: NodeProps<ShapeNode>) {
return (
<div className="diagram-node diagram-node--task">
<ShapeHandles />
<span>{data.label}</span>
</div>
);
}
function GatewayNode({ data }: NodeProps<ShapeNode>) {
return (
<div className="diagram-node diagram-node--gateway">
<ShapeHandles />
<span>{data.label}</span>
</div>
);
}
function EventNode({ data }: NodeProps<ShapeNode>) {
return (
<div className="diagram-node diagram-node--event">
<ShapeHandles />
<span>{data.label}</span>
</div>
);
}
export const nodeTypes = {
task: TaskNode,
gateway: GatewayNode,
event: EventNode,
};
+36
View File
@@ -0,0 +1,36 @@
import type { Node, Edge } from '@xyflow/react';
import type { DiagramDocument, DiagramEdge, DiagramNode, NodeKind } from './types';
/**
* React Flow's runtime Node/Edge carry UI-only fields (measured, selected,
* dragging, zIndex, ...). Only this subset gets persisted.
*/
export function serializeDocument(nodes: Node[], edges: Edge[]): DiagramDocument {
return {
nodes: nodes.map(
(node): DiagramNode => ({
id: node.id,
type: (node.type ?? 'task') as NodeKind,
position: { x: node.position.x, y: node.position.y },
data: node.data as DiagramNode['data'],
}),
),
edges: edges.map(
(edge): DiagramEdge => ({
id: edge.id,
source: edge.source,
target: edge.target,
type: edge.type,
data: edge.data as DiagramEdge['data'],
}),
),
};
}
export function debounce<Args extends unknown[]>(fn: (...args: Args) => void, delayMs: number) {
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: Args) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn(...args), delayMs);
};
}
+38
View File
@@ -0,0 +1,38 @@
export type NodeKind = 'task' | 'gateway' | 'event';
export type NodeLinkType = 'description' | 'subDiagram';
export interface DiagramNodeData extends Record<string, unknown> {
label: string;
description?: string;
responsible?: string;
duration?: string;
linkType?: NodeLinkType;
subDiagramId?: string;
}
export interface DiagramNode {
id: string;
type: NodeKind;
position: { x: number; y: number };
data: DiagramNodeData;
}
export interface DiagramEdgeData extends Record<string, unknown> {
label?: string;
}
export interface DiagramEdge {
id: string;
source: string;
target: string;
type?: string;
data?: DiagramEdgeData;
}
export interface DiagramDocument {
nodes: DiagramNode[];
edges: DiagramEdge[];
}
export const EMPTY_DOCUMENT: DiagramDocument = { nodes: [], edges: [] };
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"allowArbitraryExtensions": true,
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"module": "nodenext",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})