Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ jobs:
- run: npm ci
- run: npm run type-check
- run: npm run check
- run: npm test
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"$schema": "https://biomejs.dev/schemas/2.2.2/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
Expand Down
1,703 changes: 1,044 additions & 659 deletions package-lock.json

Large diffs are not rendered by default.

38 changes: 21 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,38 @@
"type-check": "tsc",
"type-check:watch": "tsc --watch",
"check": "biome check",
"check:fix": "biome check --fix"
"check:fix": "biome check --fix",
"test": "vitest"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.3.0",
"@mui/material": "^6.3.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.1",
"@mui/material": "^7.3.1",
"debounce": "^2.2.0",
"es-toolkit": "^1.39.10",
"eventemitter3": "^5.0.1",
"lodash-es": "^4.17.21",
"memoize-one": "^6.0.0",
"mnemonist": "^0.39.8",
"mnemonist": "^0.40.3",
"nullthrows": "^1.1.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-use": "^17.6.0",
"tiny-invariant": "^1.3.3",
"transformation-matrix": "^2.16.1",
"zustand": "^5.0.2"
"transformation-matrix": "^3.1.0",
"zod": "^4.1.12",
"zustand": "^5.0.8"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@biomejs/biome": "^2.2.2",
"@tsconfig/strictest": "^2.0.5",
"@types/lodash-es": "^4.17.12",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@vitejs/plugin-react": "^4.3.4",
"type-fest": "^4.31.0",
"typescript": "^5.7.2",
"vite": "^6.0.6"
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.2",
"type-fest": "^4.41.0",
"typescript": "^5.9.2",
"vite": "^7.1.3",
"vitest": "^3.2.4"
}
}
133 changes: 117 additions & 16 deletions src/components/ComponentPropertyDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,157 @@
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormLabel,
Stack,
TextField,
} from "@mui/material";
import nullthrows from "nullthrows";
import { useState } from "react";
import { z } from "zod";
import type CCStore from "../store";
import type { CCComponentId } from "../store/component";
import type { CCComponentPinId } from "../store/componentPin";
import { useStore } from "../store/react";

export type ComponentPropertyDialogProps = {
componentId: CCComponentId;
defaultName: string;
onAccept(newName: string): void;
onClose(): void;
onCancel(): void;
};

const stateSchema = z.object({
name: z.string().nonempty(),
pinNameById: z.map(z.custom<CCComponentPinId>(), z.string().nonempty()),
});
function extractStateFromStore(
store: CCStore,
componentId: CCComponentId,
): z.input<typeof stateSchema> {
const component = nullthrows(store.components.get(componentId));
return {
name: component.name,
pinNameById: new Map(
store.componentPins
.getManyByComponentId(componentId)
.map((pin) => [pin.id, pin.name]),
),
};
}
function applyStateToStore(
store: CCStore,
componentId: CCComponentId,
state: z.output<typeof stateSchema>,
) {
store.components.update(componentId, { name: state.name });
for (const [pinId, pinName] of state.pinNameById) {
store.componentPins.update(pinId, { name: pinName });
}
}

export function ComponentPropertyDialog({
defaultName,
onAccept,
onCancel,
componentId,
onClose,
}: ComponentPropertyDialogProps) {
const [newName, setNewName] = useState(defaultName);
const { store } = useStore();
const component = nullthrows(store.components.get(componentId));
const [state, setState] = useState(() =>
extractStateFromStore(store, componentId),
);
const result = stateSchema.safeParse(state);

return (
<Dialog maxWidth="xs" fullWidth open onClose={onCancel}>
<Dialog maxWidth="sm" fullWidth open onClose={onClose}>
<form
onSubmit={(e) => {
e.preventDefault();
if (!newName) return;
onAccept(newName);
if (!result.success) return;
applyStateToStore(store, componentId, result.data);
onClose();
}}
>
<DialogTitle>Component property</DialogTitle>
<DialogTitle>{component.name} Properties</DialogTitle>
<DialogContent>
<FormLabel component="div">Name</FormLabel>
<TextField
label="Name"
value={newName}
size="small"
value={state.name}
fullWidth
onChange={(e) => setNewName(e.target.value)}
onChange={(e) => setState({ ...state, name: e.target.value })}
placeholder="Name"
sx={{ mt: 0.5 }}
/>
<Box
sx={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: 2,
mt: 2,
}}
>
<Stack sx={{ gap: 0.5 }}>
<FormLabel component="div">Input Pins</FormLabel>
{store.componentPins
.getManyByComponentId(componentId)
.filter((pin) => pin.type === "input")
.map((pin) => (
<TextField
key={pin.id}
size="small"
value={state.pinNameById.get(pin.id)}
fullWidth
onChange={(e) =>
setState({
...state,
pinNameById: new Map(state.pinNameById).set(
pin.id,
e.target.value,
),
})
}
/>
))}
</Stack>
<Stack sx={{ gap: 0.5 }}>
<FormLabel component="div">Output Pins</FormLabel>
{store.componentPins
.getManyByComponentId(componentId)
.filter((pin) => pin.type === "output")
.map((pin) => (
<TextField
key={pin.id}
size="small"
value={state.pinNameById.get(pin.id)}
fullWidth
onChange={(e) =>
setState({
...state,
pinNameById: new Map(state.pinNameById).set(
pin.id,
e.target.value,
),
})
}
/>
))}
</Stack>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onCancel} color="inherit">
<Button onClick={onClose} color="inherit">
Cancel
</Button>
<Button
variant="outlined"
color="inherit"
color="primary"
type="submit"
disabled={!newName}
disabled={!result.success}
>
Create
Apply
</Button>
</DialogActions>
</form>
Expand Down
6 changes: 3 additions & 3 deletions src/pages/edit/Editor/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ export default function CCComponentEditorContextMenu({
width: "200px",
}}
>
<MenuItem onClick={componentEditorState.closeContextMenu}>
{/* <MenuItem onClick={componentEditorState.closeContextMenu}>
Create a node
</MenuItem>
</MenuItem> */}
{componentEditorState.selectedNodeIds.size > 0 && (
<MenuItem
onClick={() => {
Expand Down Expand Up @@ -135,7 +135,7 @@ export default function CCComponentEditorContextMenu({
store.connections.unregister([
...componentEditorState.selectedConnectionIds,
]);
componentEditorState.selectNode([], true);
// componentEditorState.selectNode([], true);
componentEditorState.selectConnection([], false);
componentEditorState.closeContextMenu();
}}
Expand Down
27 changes: 21 additions & 6 deletions src/pages/edit/Editor/components/NodePinPropertyEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button, Popover, Stack, TextField, Typography } from "@mui/material";
import { zip } from "lodash-es";
import { zip } from "es-toolkit";
import nullthrows from "nullthrows";
import { useState } from "react";
import invariant from "tiny-invariant";
Expand Down Expand Up @@ -28,7 +28,7 @@ export function CCComponentEditorNodePinPropertyEditor() {
.getManyByNodeIdAndComponentPinId(target.nodeId, target.componentPinId)
.toSorted((a, b) => a.order - b.order);
invariant(
nodePins.every((p) => p.userSpecifiedBitWidth !== null),
nodePins.every((p) => p.manualBitWidth !== null),
"NodePinPropertyEditor can only be used for node pins with user specified bit width",
);
const componentPinAttributes = nullthrows(
Expand Down Expand Up @@ -64,7 +64,7 @@ export function CCComponentEditorNodePinPropertyEditor() {

const bitWidthList =
newBitWidthList ??
nodePins.map((nodePin) => nullthrows(nodePin.userSpecifiedBitWidth));
nodePins.map((nodePin) => nullthrows(nodePin.manualBitWidth));

const isTouched = Boolean(newBitWidthList);
const isValid = bitWidthList.every((bitWidth) => bitWidth > 0);
Expand Down Expand Up @@ -103,7 +103,7 @@ export function CCComponentEditorNodePinPropertyEditor() {
componentPinId: target.componentPinId,
nodeId: target.nodeId,
order: ++maxOrder,
userSpecifiedBitWidth: bitWidth,
manualBitWidth: bitWidth,
}),
);
continue;
Expand All @@ -116,10 +116,25 @@ export function CCComponentEditorNodePinPropertyEditor() {
// Update NodePin
if (nodePin && bitWidth) {
maxOrder = nodePin.order; // nodePins are sorted by order
if (nodePin.userSpecifiedBitWidth !== bitWidth)
if (nodePin.manualBitWidth !== bitWidth) {
store.nodePins.update(nodePin.id, {
userSpecifiedBitWidth: bitWidth,
manualBitWidth: bitWidth,
});
const connections = store.connections.getConnectionsByNodePinId(
nodePin.id,
);
for (const connection of connections) {
const anotherNodePinId =
connection.from === nodePin.id
? connection.to
: connection.from;
if (
!store.nodePins.isConnectable(nodePin.id, anotherNodePinId)
) {
store.connections.unregister([connection.id]);
}
}
}
continue;
}
throw new Error("Unreachable");
Expand Down
4 changes: 2 additions & 2 deletions src/pages/edit/Editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ function CCComponentEditorContent({
<CCComponentEditorNodePinPropertyEditor />
{isComponentPropertyDialogOpen && (
<ComponentPropertyDialog
componentId={componentId}
defaultName={component.name}
onAccept={(newName) => {
store.components.update(componentId, { name: newName });
onClose={() => {
setIsComponentPropertyDialogOpen(false);
}}
onCancel={() => {
Expand Down
1 change: 1 addition & 0 deletions src/pages/edit/Editor/renderer/Background.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default function CCComponentEditorRendererBackground() {
const viewBox = componentEditorState.getViewBox();

return (
// biome-ignore lint/a11y/noStaticElementInteractions: SVG
<rect
{...viewBox}
onClick={() => {
Expand Down
Loading