System updated with many bug revised
This commit is contained in:
Binary file not shown.
+12
-1
@@ -340,9 +340,20 @@ def delete_project(project_name):
|
|||||||
return jsonify({"message": "deleted", "project": safe_name(project_name, 'project_1')})
|
return jsonify({"message": "deleted", "project": safe_name(project_name, 'project_1')})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/projects/<project_name>/cells/<cell_name>', methods=['PATCH'])
|
@app.route('/api/projects/<project_name>/cells/<cell_name>', methods=['PATCH', 'DELETE'])
|
||||||
@login_required_json
|
@login_required_json
|
||||||
def rename_cell(project_name, cell_name):
|
def rename_cell(project_name, cell_name):
|
||||||
|
if request.method == 'DELETE':
|
||||||
|
cell = safe_name(cell_name, 'canvas_1')
|
||||||
|
target = os.path.abspath(cell_file_path(project_name, cell))
|
||||||
|
project_dir = os.path.abspath(project_root(project_name))
|
||||||
|
if not target.startswith(project_dir + os.sep):
|
||||||
|
return jsonify({"error": "Invalid cell path"}), 400
|
||||||
|
if not os.path.exists(target):
|
||||||
|
return jsonify({"message": "already deleted", "cell": cell})
|
||||||
|
os.remove(target)
|
||||||
|
return jsonify({"message": "deleted", "cell": cell})
|
||||||
|
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
old_cell = safe_name(cell_name, 'canvas_1')
|
old_cell = safe_name(cell_name, 'canvas_1')
|
||||||
new_cell = safe_name(data.get('name'), old_cell)
|
new_cell = safe_name(data.get('name'), old_cell)
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
# =============================================
|
|
||||||
# mxPIC Cell/Project Definition File
|
|
||||||
# =============================================
|
|
||||||
schema_version: "2.0.0"
|
|
||||||
kind: cell
|
|
||||||
project: mxpic_project_1
|
|
||||||
name: mxpic_project_1
|
|
||||||
type: project
|
|
||||||
version: "1.0.0"
|
|
||||||
|
|
||||||
# 1. External Ports (How this cell connects to the outside world)
|
|
||||||
ports:
|
|
||||||
- name: in0
|
|
||||||
layer: WG_CORE
|
|
||||||
x: 0.0
|
|
||||||
y: 0.0
|
|
||||||
angle: 180.0
|
|
||||||
width: 0.5
|
|
||||||
- name: out0
|
|
||||||
layer: WG_CORE
|
|
||||||
x: 100.0
|
|
||||||
y: 10.0
|
|
||||||
angle: 0.0
|
|
||||||
width: 0.5
|
|
||||||
- name: out1
|
|
||||||
layer: WG_CORE
|
|
||||||
x: 100.0
|
|
||||||
y: -10.0
|
|
||||||
angle: 0.0
|
|
||||||
width: 0.5
|
|
||||||
|
|
||||||
# 2. Instances (The sub-components dropped onto this canvas)
|
|
||||||
instances:
|
|
||||||
canvas_1:
|
|
||||||
component: canvas_1
|
|
||||||
x: 250.0
|
|
||||||
y: 200.0
|
|
||||||
rotation: 0.0
|
|
||||||
mirror: false
|
|
||||||
settings:
|
|
||||||
length:
|
|
||||||
|
|
||||||
component_2:
|
|
||||||
component: canvas_1
|
|
||||||
x: 250.0
|
|
||||||
y: 280.0
|
|
||||||
rotation: 0.0
|
|
||||||
mirror: false
|
|
||||||
settings:
|
|
||||||
length:
|
|
||||||
|
|
||||||
# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
|
||||||
bundles:
|
|
||||||
output_bus:
|
|
||||||
routing_type: euler_bend
|
|
||||||
links:
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "mxpic_project_1_2",
|
|
||||||
"technology": "Silterra/EMO1_2ML_CU_Al_RDL"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "mxpic_project_1_3",
|
|
||||||
"technology": "Silterra/EMO1_2ML_CU_Al_RDL"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "mxpic_project_1_4",
|
|
||||||
"technology": "Silterra/EMO1_2ML_CU_Al_RDL"
|
|
||||||
}
|
|
||||||
+118
-19
@@ -144,9 +144,16 @@
|
|||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-folder summary {
|
summary.tree-folder {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.tree-folder::marker {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-leaf {
|
.component-leaf {
|
||||||
@@ -484,10 +491,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tree-summary-row {
|
.tree-summary-row {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
width: calc(100% - 18px);
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.folder-icon {
|
.folder-icon {
|
||||||
@@ -563,6 +571,33 @@
|
|||||||
transition: transform 0.15s ease;
|
transition: transform 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tree-delete-btn {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.45);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(239, 68, 68, 0.08);
|
||||||
|
color: #fca5a5;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-delete-btn:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.18);
|
||||||
|
border-color: var(--danger);
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .tree-delete-btn {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
details[open] > summary .tree-expander {
|
details[open] > summary .tree-expander {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
@@ -1076,7 +1111,7 @@
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProjectTreeNode = ({ name, children, onOpenComposite, onOpenProject, onSelectInstance, onRenameCanvas }) => {
|
const ProjectTreeNode = ({ name, children, onOpenComposite, onOpenProject, onSelectInstance, onRenameCanvas, onDeleteCanvas }) => {
|
||||||
if (children && children.__type__ === 'project') {
|
if (children && children.__type__ === 'project') {
|
||||||
const projectName = children.__name__ || name;
|
const projectName = children.__name__ || name;
|
||||||
const composites = children.composites || [];
|
const composites = children.composites || [];
|
||||||
@@ -1088,12 +1123,12 @@
|
|||||||
<summary className="tree-folder" onDoubleClick={handleDoubleClick} style={{ cursor: 'pointer' }}>
|
<summary className="tree-folder" onDoubleClick={handleDoubleClick} style={{ cursor: 'pointer' }}>
|
||||||
<span className="tree-summary-row">
|
<span className="tree-summary-row">
|
||||||
<span className="folder-icon project-folder"></span>
|
<span className="folder-icon project-folder"></span>
|
||||||
<span className="tree-summary-name">Project - {name}</span>
|
<span className="tree-summary-name">{name}</span>
|
||||||
<span className="tree-expander">></span>
|
<span className="tree-expander">></span>
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
{composites.map(comp => (
|
{composites.map(comp => (
|
||||||
<ProjectTreeNode key={comp.pageId || comp.__name__} name={comp.__name__} children={comp} onOpenComposite={onOpenComposite} onOpenProject={onOpenProject} onSelectInstance={onSelectInstance} onRenameCanvas={onRenameCanvas} />
|
<ProjectTreeNode key={comp.pageId || comp.__name__} name={comp.__name__} children={comp} onOpenComposite={onOpenComposite} onOpenProject={onOpenProject} onSelectInstance={onSelectInstance} onRenameCanvas={onRenameCanvas} onDeleteCanvas={onDeleteCanvas} />
|
||||||
))}
|
))}
|
||||||
</details>
|
</details>
|
||||||
);
|
);
|
||||||
@@ -1125,8 +1160,14 @@
|
|||||||
};
|
};
|
||||||
const handleOpen = (event) => {
|
const handleOpen = (event) => {
|
||||||
if (event.target.closest('.tree-expander')) return;
|
if (event.target.closest('.tree-expander')) return;
|
||||||
|
if (event.target.closest('.tree-delete-btn')) return;
|
||||||
if (onOpenComposite) onOpenComposite(cellName);
|
if (onOpenComposite) onOpenComposite(cellName);
|
||||||
};
|
};
|
||||||
|
const handleDeleteCanvas = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (onDeleteCanvas) onDeleteCanvas(cellName);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<details>
|
<details>
|
||||||
<summary className="tree-folder" draggable onDragStart={handleDragStart} onClick={handleOpen}>
|
<summary className="tree-folder" draggable onDragStart={handleDragStart} onClick={handleOpen}>
|
||||||
@@ -1139,6 +1180,7 @@
|
|||||||
onRename={onRenameCanvas}
|
onRename={onRenameCanvas}
|
||||||
onOpen={() => onOpenComposite && onOpenComposite(cellName)}
|
onOpen={() => onOpenComposite && onOpenComposite(cellName)}
|
||||||
/>
|
/>
|
||||||
|
<button className="tree-delete-btn" type="button" title={`Delete ${cellName}`} onClick={handleDeleteCanvas}>x</button>
|
||||||
<span className="tree-expander">></span>
|
<span className="tree-expander">></span>
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
@@ -1186,7 +1228,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
{Object.entries(children).map(([childName, childData]) => (
|
{Object.entries(children).map(([childName, childData]) => (
|
||||||
<ProjectTreeNode key={childName} name={childName} children={childData} onOpenComposite={onOpenComposite} onOpenProject={onOpenProject} onSelectInstance={onSelectInstance} onRenameCanvas={onRenameCanvas} />
|
<ProjectTreeNode key={childName} name={childName} children={childData} onOpenComposite={onOpenComposite} onOpenProject={onOpenProject} onSelectInstance={onSelectInstance} onRenameCanvas={onRenameCanvas} onDeleteCanvas={onDeleteCanvas} />
|
||||||
))}
|
))}
|
||||||
</details>
|
</details>
|
||||||
);
|
);
|
||||||
@@ -1229,7 +1271,7 @@
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LeftPanel = ({ projectTreeItems, library, treeKey, expanded, onToggle, treeRef, width, onOpenComposite, onOpenProject, onSelectInstance, onRenameCanvas, projectExpanded, onProjectToggle, projectTreeRef, projectTreeKey }) => {
|
const LeftPanel = ({ projectTreeItems, library, treeKey, expanded, onToggle, treeRef, width, onOpenComposite, onOpenProject, onSelectInstance, onRenameCanvas, onDeleteCanvas, projectExpanded, onProjectToggle, projectTreeRef, projectTreeKey }) => {
|
||||||
const [projectPanelHeight, setProjectPanelHeight] = useState(270);
|
const [projectPanelHeight, setProjectPanelHeight] = useState(270);
|
||||||
const [resizingProjectPanel, setResizingProjectPanel] = useState(false);
|
const [resizingProjectPanel, setResizingProjectPanel] = useState(false);
|
||||||
const leftPanelRef = useRef(null);
|
const leftPanelRef = useRef(null);
|
||||||
@@ -1278,11 +1320,11 @@
|
|||||||
projectTreeItems.map(item => {
|
projectTreeItems.map(item => {
|
||||||
if (item.type === 'project') {
|
if (item.type === 'project') {
|
||||||
return (
|
return (
|
||||||
<ProjectTreeNode key={item.name} name={item.name} children={{ __type__: 'project', __name__: item.name, composites: item.composites }} onOpenComposite={onOpenComposite} onOpenProject={onOpenProject} onSelectInstance={onSelectInstance} onRenameCanvas={onRenameCanvas} />
|
<ProjectTreeNode key={item.name} name={item.name} children={{ __type__: 'project', __name__: item.name, composites: item.composites }} onOpenComposite={onOpenComposite} onOpenProject={onOpenProject} onSelectInstance={onSelectInstance} onRenameCanvas={onRenameCanvas} onDeleteCanvas={onDeleteCanvas} />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<ProjectTreeNode key={item.name} name={item.name} children={{ __type__: 'composite', __name__: item.name, tree: item.tree || {}, pageId: item.pageId }} onOpenComposite={onOpenComposite} onOpenProject={onOpenProject} onSelectInstance={onSelectInstance} onRenameCanvas={onRenameCanvas} />
|
<ProjectTreeNode key={item.name} name={item.name} children={{ __type__: 'composite', __name__: item.name, tree: item.tree || {}, pageId: item.pageId }} onOpenComposite={onOpenComposite} onOpenProject={onOpenProject} onSelectInstance={onSelectInstance} onRenameCanvas={onRenameCanvas} onDeleteCanvas={onDeleteCanvas} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -2266,6 +2308,7 @@
|
|||||||
}, [pages, library, syncAllCompositeTrees]);
|
}, [pages, library, syncAllCompositeTrees]);
|
||||||
|
|
||||||
const selectedNode = useMemo(() => currentNodes.find(n => n.selected), [currentNodes]);
|
const selectedNode = useMemo(() => currentNodes.find(n => n.selected), [currentNodes]);
|
||||||
|
const openTabs = useMemo(() => pages.filter(page => !page.isClosed), [pages]);
|
||||||
|
|
||||||
const selectInstanceInPage = useCallback((pageName, instanceName) => {
|
const selectInstanceInPage = useCallback((pageName, instanceName) => {
|
||||||
if (!pageName || !instanceName) return;
|
if (!pageName || !instanceName) return;
|
||||||
@@ -2294,7 +2337,7 @@
|
|||||||
const existing = prev.find(p => p.name === name && p.type === 'project');
|
const existing = prev.find(p => p.name === name && p.type === 'project');
|
||||||
if (existing) {
|
if (existing) {
|
||||||
setActivePageId(existing.id);
|
setActivePageId(existing.id);
|
||||||
return prev;
|
return prev.map(p => p.id === existing.id ? { ...p, isClosed: false } : p);
|
||||||
}
|
}
|
||||||
const newProjectPage = {
|
const newProjectPage = {
|
||||||
id: Date.now().toString() + Math.random().toString(36).substr(2, 5),
|
id: Date.now().toString() + Math.random().toString(36).substr(2, 5),
|
||||||
@@ -2319,12 +2362,13 @@
|
|||||||
const existing = prev.find(p => p.name === name && p.type === 'composite');
|
const existing = prev.find(p => p.name === name && p.type === 'composite');
|
||||||
if (existing) {
|
if (existing) {
|
||||||
setActivePageId(existing.id);
|
setActivePageId(existing.id);
|
||||||
return prev;
|
return prev.map(p => p.id === existing.id ? { ...p, isClosed: false } : p);
|
||||||
}
|
}
|
||||||
const newComposite = {
|
const newComposite = {
|
||||||
id: Date.now().toString() + Math.random().toString(36).substr(2, 5),
|
id: Date.now().toString() + Math.random().toString(36).substr(2, 5),
|
||||||
name: name,
|
name: name,
|
||||||
type: 'composite',
|
type: 'composite',
|
||||||
|
isClosed: false,
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
id: 'page-port',
|
id: 'page-port',
|
||||||
@@ -2413,6 +2457,7 @@
|
|||||||
id: Date.now().toString() + Math.random().toString(36).substr(2, 5),
|
id: Date.now().toString() + Math.random().toString(36).substr(2, 5),
|
||||||
name: cellName,
|
name: cellName,
|
||||||
type: 'composite',
|
type: 'composite',
|
||||||
|
isClosed: false,
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
id: 'page-port',
|
id: 'page-port',
|
||||||
@@ -2438,17 +2483,62 @@
|
|||||||
|
|
||||||
const closePage = useCallback((pageId) => {
|
const closePage = useCallback((pageId) => {
|
||||||
setPages(prev => {
|
setPages(prev => {
|
||||||
const pageToClose = prev.find(p => p.id === pageId);
|
const closed = prev.map(p => p.id === pageId ? { ...p, isClosed: true } : p);
|
||||||
const filtered = prev.filter(p => p.id !== pageId);
|
|
||||||
if (activePageId === pageId) {
|
if (activePageId === pageId) {
|
||||||
const idx = prev.findIndex(p => p.id === pageId);
|
const idx = prev.findIndex(p => p.id === pageId);
|
||||||
const nextActive = filtered[Math.min(idx, filtered.length - 1)] || null;
|
const openPages = closed.filter(p => !p.isClosed);
|
||||||
|
const nextActive = openPages[Math.min(idx, openPages.length - 1)] || openPages[openPages.length - 1] || null;
|
||||||
setActivePageId(nextActive ? nextActive.id : null);
|
setActivePageId(nextActive ? nextActive.id : null);
|
||||||
}
|
}
|
||||||
return filtered;
|
return closed;
|
||||||
});
|
});
|
||||||
}, [activePageId]);
|
}, [activePageId]);
|
||||||
|
|
||||||
|
const deleteCanvas = useCallback((cellName) => {
|
||||||
|
if (!cellName) return;
|
||||||
|
if (!window.confirm(`Delete canvas "${cellName}" from this project?`)) return;
|
||||||
|
const pageToDelete = pages.find(p => p.type === 'composite' && p.name === cellName);
|
||||||
|
setPages(prev => {
|
||||||
|
const withoutCell = prev
|
||||||
|
.filter(p => !(p.type === 'composite' && p.name === cellName))
|
||||||
|
.map(p => {
|
||||||
|
if (p.type !== 'project') return p;
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
nodes: p.nodes.filter(node => node.data?.componentName !== cellName),
|
||||||
|
edges: p.edges.filter(edge => {
|
||||||
|
const removedNodeIds = new Set(p.nodes.filter(node => node.data?.componentName === cellName).map(node => node.id));
|
||||||
|
return !removedNodeIds.has(edge.source) && !removedNodeIds.has(edge.target);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (activePageId === pageToDelete?.id) {
|
||||||
|
const nextActive = withoutCell.find(p => !p.isClosed) || withoutCell[0] || null;
|
||||||
|
setActivePageId(nextActive ? nextActive.id : null);
|
||||||
|
}
|
||||||
|
return withoutCell;
|
||||||
|
});
|
||||||
|
setProjectCompositeMap(prev => {
|
||||||
|
const next = {};
|
||||||
|
Object.entries(prev).forEach(([project, cells]) => {
|
||||||
|
next[project] = cells.filter(name => name !== cellName);
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setStandaloneComposites(prev => prev.filter(name => name !== cellName));
|
||||||
|
setCompositeTrees(prev => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[cellName];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
fetch(`/api/projects/${encodeURIComponent(currentProjectName)}/cells/${encodeURIComponent(cellName)}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
}).then(response => {
|
||||||
|
if (response.ok) addLog(`Deleted canvas "${cellName}".`);
|
||||||
|
else addLog(`Canvas "${cellName}" was removed locally, but file delete failed.`);
|
||||||
|
}).catch(() => addLog(`Canvas "${cellName}" was removed locally, but file delete failed.`));
|
||||||
|
}, [pages, activePageId, currentProjectName, addLog]);
|
||||||
|
|
||||||
const switchPage = useCallback((pageId) => {
|
const switchPage = useCallback((pageId) => {
|
||||||
setActivePageId(pageId);
|
setActivePageId(pageId);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -2680,7 +2770,15 @@
|
|||||||
|
|
||||||
const projectTreeItems = useMemo(() => {
|
const projectTreeItems = useMemo(() => {
|
||||||
const items = [];
|
const items = [];
|
||||||
const projectPages = pages.filter(p => p.type === 'project');
|
const projectPagesByName = new Map();
|
||||||
|
pages
|
||||||
|
.filter(p => p.type === 'project')
|
||||||
|
.forEach(project => {
|
||||||
|
if (!projectPagesByName.has(project.name) || project.id === activePageId) {
|
||||||
|
projectPagesByName.set(project.name, project);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const projectPages = Array.from(projectPagesByName.values());
|
||||||
projectPages.forEach(project => {
|
projectPages.forEach(project => {
|
||||||
const projectNodeItems = project.nodes
|
const projectNodeItems = project.nodes
|
||||||
.filter(node => node.id !== 'page-port' && node.data?.componentName)
|
.filter(node => node.id !== 'page-port' && node.data?.componentName)
|
||||||
@@ -2732,7 +2830,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
return items;
|
return items;
|
||||||
}, [pages, library, projectCompositeMap, standaloneComposites, compositeTrees]);
|
}, [pages, library, projectCompositeMap, standaloneComposites, compositeTrees, activePageId]);
|
||||||
|
|
||||||
const libraryWithCells = useMemo(() => {
|
const libraryWithCells = useMemo(() => {
|
||||||
const cellEntries = {};
|
const cellEntries = {};
|
||||||
@@ -2904,6 +3002,7 @@ ${bundlesBlock}`;
|
|||||||
onOpenProject={openProject}
|
onOpenProject={openProject}
|
||||||
onSelectInstance={selectInstanceInPage}
|
onSelectInstance={selectInstanceInPage}
|
||||||
onRenameCanvas={renameCanvas}
|
onRenameCanvas={renameCanvas}
|
||||||
|
onDeleteCanvas={deleteCanvas}
|
||||||
projectExpanded={projectExpanded}
|
projectExpanded={projectExpanded}
|
||||||
onProjectToggle={handleProjectToggle}
|
onProjectToggle={handleProjectToggle}
|
||||||
projectTreeRef={projectTreeContainerRef}
|
projectTreeRef={projectTreeContainerRef}
|
||||||
@@ -2919,7 +3018,7 @@ ${bundlesBlock}`;
|
|||||||
<div className="canvas-tab" style={{ cursor: 'pointer' }} onClick={createCell}>
|
<div className="canvas-tab" style={{ cursor: 'pointer' }} onClick={createCell}>
|
||||||
+ Cell
|
+ Cell
|
||||||
</div>
|
</div>
|
||||||
{pages.map(page => (
|
{openTabs.map(page => (
|
||||||
<div key={page.id} className={`canvas-tab ${page.id === activePageId ? 'active' : ''}`} onClick={() => switchPage(page.id)}>
|
<div key={page.id} className={`canvas-tab ${page.id === activePageId ? 'active' : ''}`} onClick={() => switchPage(page.id)}>
|
||||||
<EditableCanvasTabName page={page} active={page.id === activePageId} onRename={renameCanvas} />
|
<EditableCanvasTabName page={page} active={page.id === activePageId} onRename={renameCanvas} />
|
||||||
<button onClick={(e) => { e.stopPropagation(); closePage(page.id); }}>x</button>
|
<button onClick={(e) => { e.stopPropagation(); closePage(page.id); }}>x</button>
|
||||||
|
|||||||
+69
-11
@@ -183,7 +183,9 @@
|
|||||||
min-height: 76px;
|
min-height: 76px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 30px minmax(0, 1fr) minmax(64px, auto) auto;
|
||||||
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -211,15 +213,37 @@
|
|||||||
box-shadow: 0 20px 44px rgba(37, 99, 235, 0.18);
|
box-shadow: 0 20px 44px rgba(37, 99, 235, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-card.empty-project-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
cursor: default;
|
||||||
|
color: var(--text-muted);
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card.empty-project-card:hover {
|
||||||
|
transform: none;
|
||||||
|
border-color: var(--border);
|
||||||
|
box-shadow: 0 16px 34px var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
.project-meta {
|
.project-meta {
|
||||||
margin-left: auto;
|
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: 'IBM Plex Mono', Consolas, monospace;
|
font-family: 'IBM Plex Mono', Consolas, monospace;
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-details {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.project-name {
|
.project-name {
|
||||||
|
display: block;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -229,7 +253,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.delete-project-btn {
|
.delete-project-btn {
|
||||||
margin-left: 14px;
|
|
||||||
border: 1px solid rgba(239, 68, 68, 0.45);
|
border: 1px solid rgba(239, 68, 68, 0.45);
|
||||||
background: rgba(239, 68, 68, 0.08);
|
background: rgba(239, 68, 68, 0.08);
|
||||||
color: #fecaca;
|
color: #fecaca;
|
||||||
@@ -345,6 +368,21 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-field {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-field label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-panel input,
|
||||||
.modal-panel select {
|
.modal-panel select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--panel-soft);
|
background: var(--panel-soft);
|
||||||
@@ -354,8 +392,10 @@
|
|||||||
padding: 11px;
|
padding: 11px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-panel input:focus,
|
||||||
.modal-panel select:focus {
|
.modal-panel select:focus {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 0 0 3px rgba(110, 231, 255, 0.12);
|
box-shadow: 0 0 0 3px rgba(110, 231, 255, 0.12);
|
||||||
@@ -418,13 +458,19 @@
|
|||||||
|
|
||||||
.project-card {
|
.project-card {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: 30px minmax(0, 1fr) auto;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-meta {
|
.project-meta {
|
||||||
margin-left: 45px;
|
grid-column: 2 / 4;
|
||||||
width: calc(100% - 45px);
|
margin-top: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-project-btn {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -462,8 +508,15 @@
|
|||||||
|
|
||||||
<div class="modal-backdrop" id="technology-modal">
|
<div class="modal-backdrop" id="technology-modal">
|
||||||
<div class="modal-panel">
|
<div class="modal-panel">
|
||||||
<h3>Select Technology</h3>
|
<h3>Create Project</h3>
|
||||||
<select id="technology-select"></select>
|
<div class="modal-field">
|
||||||
|
<label for="project-name-input">Project Name</label>
|
||||||
|
<input id="project-name-input" type="text" value="mxpic_project_1" autocomplete="off" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label for="technology-select">Technology</label>
|
||||||
|
<select id="technology-select"></select>
|
||||||
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="secondary-btn" type="button" id="cancel-project">Cancel</button>
|
<button class="secondary-btn" type="button" id="cancel-project">Cancel</button>
|
||||||
<button class="new-project-btn" type="button" id="create-project">Create Project</button>
|
<button class="new-project-btn" type="button" id="create-project">Create Project</button>
|
||||||
@@ -476,6 +529,7 @@
|
|||||||
const newProjectForm = document.getElementById('new-project-form');
|
const newProjectForm = document.getElementById('new-project-form');
|
||||||
const technologyModal = document.getElementById('technology-modal');
|
const technologyModal = document.getElementById('technology-modal');
|
||||||
const technologySelect = document.getElementById('technology-select');
|
const technologySelect = document.getElementById('technology-select');
|
||||||
|
const projectNameInput = document.getElementById('project-name-input');
|
||||||
const createProjectButton = document.getElementById('create-project');
|
const createProjectButton = document.getElementById('create-project');
|
||||||
const cancelProjectButton = document.getElementById('cancel-project');
|
const cancelProjectButton = document.getElementById('cancel-project');
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
@@ -512,7 +566,7 @@
|
|||||||
|
|
||||||
if (!data.projects || data.projects.length === 0) {
|
if (!data.projects || data.projects.length === 0) {
|
||||||
const empty = document.createElement('li');
|
const empty = document.createElement('li');
|
||||||
empty.className = 'project-card';
|
empty.className = 'project-card empty-project-card';
|
||||||
empty.textContent = 'No projects yet. Create a new PIC layout to begin.';
|
empty.textContent = 'No projects yet. Create a new PIC layout to begin.';
|
||||||
projectList.appendChild(empty);
|
projectList.appendChild(empty);
|
||||||
return;
|
return;
|
||||||
@@ -555,13 +609,16 @@
|
|||||||
projectList.appendChild(item);
|
projectList.appendChild(item);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
projectList.innerHTML = '<li class="project-card">Failed to load projects.</li>';
|
projectList.innerHTML = '<li class="project-card empty-project-card">Failed to load projects.</li>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newProjectForm.addEventListener('submit', async (event) => {
|
newProjectForm.addEventListener('submit', async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
projectNameInput.value = 'mxpic_project_1';
|
||||||
technologyModal.classList.add('open');
|
technologyModal.classList.add('open');
|
||||||
|
projectNameInput.focus();
|
||||||
|
projectNameInput.select();
|
||||||
});
|
});
|
||||||
|
|
||||||
cancelProjectButton.addEventListener('click', () => {
|
cancelProjectButton.addEventListener('click', () => {
|
||||||
@@ -570,10 +627,11 @@
|
|||||||
|
|
||||||
createProjectButton.addEventListener('click', async () => {
|
createProjectButton.addEventListener('click', async () => {
|
||||||
const selectedTechnology = technologySelect.value;
|
const selectedTechnology = technologySelect.value;
|
||||||
|
const requestedName = projectNameInput.value.trim() || 'mxpic_project_1';
|
||||||
const response = await fetch('/api/projects', {
|
const response = await fetch('/api/projects', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name: 'mxpic_project_1', technology: selectedTechnology })
|
body: JSON.stringify({ name: requestedName, technology: selectedTechnology })
|
||||||
});
|
});
|
||||||
const project = await response.json();
|
const project = await response.json();
|
||||||
addLog(`Created project "${project.name}" with technology "${selectedTechnology}".`);
|
addLog(`Created project "${project.name}" with technology "${selectedTechnology}".`);
|
||||||
|
|||||||
Reference in New Issue
Block a user