Updated with more bug revise. The login page and dashboard is also changedd
This commit is contained in:
Binary file not shown.
@@ -9,7 +9,6 @@ from typing import Dict, Optional
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
def create_layout_svg_from_gds(yaml_content: str, output_path: str, pdk_registry, project_dir: str = None) -> str:
|
def create_layout_svg_from_gds(yaml_content: str, output_path: str, pdk_registry, project_dir: str = None) -> str:
|
||||||
"""Create an SVG preview by placing real public _BB.gds cells from layout YAML."""
|
"""Create an SVG preview by placing real public _BB.gds cells from layout YAML."""
|
||||||
layout = yaml.safe_load(yaml_content) or {}
|
layout = yaml.safe_load(yaml_content) or {}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 439 KiB After Width: | Height: | Size: 149 KiB |
@@ -5,8 +5,8 @@ schema_version: "2.0.0"
|
|||||||
kind: cell
|
kind: cell
|
||||||
coordinate_system: gds_y_up
|
coordinate_system: gds_y_up
|
||||||
canvas_size:
|
canvas_size:
|
||||||
width: 1000
|
width: 5000
|
||||||
height: 1000
|
height: 500
|
||||||
project: mxpic_project_1
|
project: mxpic_project_1
|
||||||
name: canvas_1
|
name: canvas_1
|
||||||
type: composite
|
type: composite
|
||||||
@@ -16,35 +16,29 @@ version: "1.0.0"
|
|||||||
ports:
|
ports:
|
||||||
- name: port
|
- name: port
|
||||||
layer: WG_CORE
|
layer: WG_CORE
|
||||||
x: 200.0
|
x: 103.5
|
||||||
y: -370.0
|
y: -127.3
|
||||||
angle: 180.0
|
|
||||||
width: 0.5
|
|
||||||
- name: port_2
|
|
||||||
layer: WG_CORE
|
|
||||||
x: 200.0
|
|
||||||
y: -370.0
|
|
||||||
angle: 180.0
|
angle: 180.0
|
||||||
width: 0.5
|
width: 0.5
|
||||||
- name: port_1
|
- name: port_1
|
||||||
layer: WG_CORE
|
layer: WG_CORE
|
||||||
x: 200.0
|
x: 108.7
|
||||||
y: -420.0
|
y: -252.6
|
||||||
angle: 180.0
|
angle: 180.0
|
||||||
width: 0.5
|
width: 0.5
|
||||||
- name: port_3
|
- name: port_2
|
||||||
layer: WG_CORE
|
layer: WG_CORE
|
||||||
x: 691.9
|
x: 497.4
|
||||||
y: -267.5
|
y: -131.6
|
||||||
angle: 0.0
|
angle: 0.0
|
||||||
width: 0.5
|
width: 0.5
|
||||||
|
|
||||||
# 2. Instances (The sub-components dropped onto this canvas)
|
# 2. Instances (The sub-components dropped onto this canvas)
|
||||||
instances:
|
instances:
|
||||||
MMI_7:
|
MMI_1:
|
||||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
||||||
x: 310.0
|
x: 177.9
|
||||||
y: -370.0
|
y: -252.1
|
||||||
rotation: 0.0
|
rotation: 0.0
|
||||||
flip: 0
|
flip: 0
|
||||||
flop: 0
|
flop: 0
|
||||||
@@ -52,54 +46,10 @@ instances:
|
|||||||
settings:
|
settings:
|
||||||
length:
|
length:
|
||||||
|
|
||||||
MMI_8:
|
MMI_2:
|
||||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
||||||
x: 560.0
|
x: 356.7
|
||||||
y: -130.0
|
y: -142.9
|
||||||
rotation: 0.0
|
|
||||||
flip: 0
|
|
||||||
flop: 0
|
|
||||||
mirror: false
|
|
||||||
settings:
|
|
||||||
length:
|
|
||||||
|
|
||||||
MMI_9:
|
|
||||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
|
||||||
x: 560.0
|
|
||||||
y: -180.0
|
|
||||||
rotation: 0.0
|
|
||||||
flip: 0
|
|
||||||
flop: 0
|
|
||||||
mirror: false
|
|
||||||
settings:
|
|
||||||
length:
|
|
||||||
|
|
||||||
MMI_10:
|
|
||||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
|
||||||
x: 560.0
|
|
||||||
y: -230.0
|
|
||||||
rotation: 0.0
|
|
||||||
flip: 0
|
|
||||||
flop: 0
|
|
||||||
mirror: false
|
|
||||||
settings:
|
|
||||||
length:
|
|
||||||
|
|
||||||
MMI_11:
|
|
||||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
|
||||||
x: 560.0
|
|
||||||
y: -280.0
|
|
||||||
rotation: 0.0
|
|
||||||
flip: 0
|
|
||||||
flop: 0
|
|
||||||
mirror: false
|
|
||||||
settings:
|
|
||||||
length:
|
|
||||||
|
|
||||||
MMI_12:
|
|
||||||
component: Silterra/EMO1_2ML_CU_Al_RDL/primitives/multimode_interferometers/1x2MMI_1310nm_TE_Silterra_202603_ZKY_v2
|
|
||||||
x: 310.0
|
|
||||||
y: -420.0
|
|
||||||
rotation: 0.0
|
rotation: 0.0
|
||||||
flip: 0
|
flip: 0
|
||||||
flop: 0
|
flop: 0
|
||||||
@@ -110,28 +60,8 @@ instances:
|
|||||||
elements:
|
elements:
|
||||||
port:
|
port:
|
||||||
type: port
|
type: port
|
||||||
x: 200.0
|
x: 103.5
|
||||||
y: -370.0
|
y: -127.3
|
||||||
angle: 0.0
|
|
||||||
port_number: 1
|
|
||||||
pitch: 10
|
|
||||||
layer: WG_CORE
|
|
||||||
width: 0.5
|
|
||||||
description: ""
|
|
||||||
Anchor_1:
|
|
||||||
type: anchor
|
|
||||||
x: 460.0
|
|
||||||
y: -300.0
|
|
||||||
angle: 90.0
|
|
||||||
port_number: 4
|
|
||||||
pitch: 10
|
|
||||||
layer: WG_CORE
|
|
||||||
width: 0.5
|
|
||||||
description: ""
|
|
||||||
port_2:
|
|
||||||
type: port
|
|
||||||
x: 200.0
|
|
||||||
y: -370.0
|
|
||||||
angle: 0.0
|
angle: 0.0
|
||||||
port_number: 1
|
port_number: 1
|
||||||
pitch: 10
|
pitch: 10
|
||||||
@@ -140,18 +70,18 @@ elements:
|
|||||||
description: ""
|
description: ""
|
||||||
port_1:
|
port_1:
|
||||||
type: port
|
type: port
|
||||||
x: 200.0
|
x: 108.7
|
||||||
y: -420.0
|
y: -252.6
|
||||||
angle: 0.0
|
angle: 0.0
|
||||||
port_number: 1
|
port_number: 1
|
||||||
pitch: 10
|
pitch: 10
|
||||||
layer: WG_CORE
|
layer: WG_CORE
|
||||||
width: 0.5
|
width: 0.5
|
||||||
description: ""
|
description: ""
|
||||||
port_3:
|
port_2:
|
||||||
type: port
|
type: port
|
||||||
x: 691.9
|
x: 497.4
|
||||||
y: -267.5
|
y: -131.6
|
||||||
angle: 180.0
|
angle: 180.0
|
||||||
port_number: 1
|
port_number: 1
|
||||||
pitch: 10
|
pitch: 10
|
||||||
@@ -164,78 +94,22 @@ bundles:
|
|||||||
output_bus:
|
output_bus:
|
||||||
routing_type: euler_bend
|
routing_type: euler_bend
|
||||||
links:
|
links:
|
||||||
- from: MMI_7:a1
|
- from: MMI_1:a1
|
||||||
to: port_2:port
|
|
||||||
xsection: strip
|
|
||||||
family: optical
|
|
||||||
width: 0.45
|
|
||||||
radius: 10
|
|
||||||
routing_type: euler_bend
|
|
||||||
- from: MMI_12:a1
|
|
||||||
to: port_1:port
|
to: port_1:port
|
||||||
xsection: strip
|
xsection: strip
|
||||||
family: optical
|
family: optical
|
||||||
width: 0.45
|
width: 0.45
|
||||||
radius: 10
|
radius: 10
|
||||||
routing_type: euler_bend
|
routing_type: euler_bend
|
||||||
- from: MMI_7:b1
|
- from: MMI_1:b1
|
||||||
to: Anchor_1:a1
|
to: MMI_2:a1
|
||||||
xsection: strip
|
xsection: strip
|
||||||
family: optical
|
family: optical
|
||||||
width: 0.45
|
width: 0.45
|
||||||
radius: 10
|
radius: 10
|
||||||
routing_type: euler_bend
|
routing_type: euler_bend
|
||||||
- from: MMI_7:b2
|
- from: MMI_2:b1
|
||||||
to: Anchor_1:a2
|
to: port_2:port
|
||||||
xsection: strip
|
|
||||||
family: optical
|
|
||||||
width: 0.45
|
|
||||||
radius: 10
|
|
||||||
routing_type: euler_bend
|
|
||||||
- from: MMI_12:b1
|
|
||||||
to: Anchor_1:a3
|
|
||||||
xsection: strip
|
|
||||||
family: optical
|
|
||||||
width: 0.45
|
|
||||||
radius: 10
|
|
||||||
routing_type: euler_bend
|
|
||||||
- from: MMI_12:b2
|
|
||||||
to: Anchor_1:a4
|
|
||||||
xsection: strip
|
|
||||||
family: optical
|
|
||||||
width: 0.45
|
|
||||||
radius: 10
|
|
||||||
routing_type: euler_bend
|
|
||||||
- from: Anchor_1:b4
|
|
||||||
to: MMI_11:a1
|
|
||||||
xsection: strip
|
|
||||||
family: optical
|
|
||||||
width: 0.45
|
|
||||||
radius: 10
|
|
||||||
routing_type: euler_bend
|
|
||||||
- from: Anchor_1:b3
|
|
||||||
to: MMI_10:a1
|
|
||||||
xsection: strip
|
|
||||||
family: optical
|
|
||||||
width: 0.45
|
|
||||||
radius: 10
|
|
||||||
routing_type: euler_bend
|
|
||||||
- from: Anchor_1:b2
|
|
||||||
to: MMI_9:a1
|
|
||||||
xsection: strip
|
|
||||||
family: optical
|
|
||||||
width: 0.45
|
|
||||||
radius: 10
|
|
||||||
routing_type: euler_bend
|
|
||||||
- from: Anchor_1:b1
|
|
||||||
to: MMI_8:a1
|
|
||||||
xsection: strip
|
|
||||||
family: optical
|
|
||||||
width: 0.45
|
|
||||||
radius: 10
|
|
||||||
routing_type: euler_bend
|
|
||||||
- from: MMI_11:b1
|
|
||||||
to: port_3:port
|
|
||||||
xsection: strip
|
xsection: strip
|
||||||
family: optical
|
family: optical
|
||||||
width: 0.45
|
width: 0.45
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 579 KiB After Width: | Height: | Size: 195 KiB |
@@ -23,10 +23,10 @@ ports:
|
|||||||
|
|
||||||
# 2. Instances (The sub-components dropped onto this canvas)
|
# 2. Instances (The sub-components dropped onto this canvas)
|
||||||
instances:
|
instances:
|
||||||
MZM_1:
|
canvas_1:
|
||||||
component: Silterra/EMO1_2ML_CU_Al_RDL/composites/Mach_Zender_modulators/MZI_SiN400_Si220_PIN_mod_1310_L1300_QY_202603
|
component: canvas_1
|
||||||
x: 1740.0
|
x: 476.9
|
||||||
y: -2350.0
|
y: -1056.4
|
||||||
rotation: 0.0
|
rotation: 0.0
|
||||||
flip: 0
|
flip: 0
|
||||||
flop: 0
|
flop: 0
|
||||||
@@ -34,10 +34,10 @@ instances:
|
|||||||
settings:
|
settings:
|
||||||
length:
|
length:
|
||||||
|
|
||||||
canvas_1:
|
canvas_1_1:
|
||||||
component: canvas_1
|
component: canvas_1
|
||||||
x: 903.5
|
x: 1139.8
|
||||||
y: -2681.6
|
y: -958.5
|
||||||
rotation: 0.0
|
rotation: 0.0
|
||||||
flip: 0
|
flip: 0
|
||||||
flop: 0
|
flop: 0
|
||||||
@@ -62,15 +62,8 @@ bundles:
|
|||||||
output_bus:
|
output_bus:
|
||||||
routing_type: euler_bend
|
routing_type: euler_bend
|
||||||
links:
|
links:
|
||||||
- from: canvas_1:port_1
|
- from: canvas_1_1:port_1
|
||||||
to: MZM_1:a1
|
to: canvas_1:port_2
|
||||||
xsection: strip
|
|
||||||
family: optical
|
|
||||||
width: 0.45
|
|
||||||
radius: 10
|
|
||||||
routing_type: euler_bend
|
|
||||||
- from: canvas_1:port_3
|
|
||||||
to: MZM_1:a2
|
|
||||||
xsection: strip
|
xsection: strip
|
||||||
family: optical
|
family: optical
|
||||||
width: 0.45
|
width: 0.45
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -360,6 +360,53 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate the visual footprint of a user canvas when placed as a component.
|
||||||
|
const calculateCompositeBoxSize = (pageOrNodes, fallback) => {
|
||||||
|
const page = Array.isArray(pageOrNodes) ? { nodes: pageOrNodes } : (pageOrNodes || {});
|
||||||
|
const nodes = Array.isArray(page.nodes) ? page.nodes : [];
|
||||||
|
const fallbackSize = normalizeBoxSize({ box_size: fallback }, DEFAULT_COMPONENT_BOX_SIZE);
|
||||||
|
const points = [];
|
||||||
|
const addPoint = (x, y) => {
|
||||||
|
const px = Number(x);
|
||||||
|
const py = Number(y);
|
||||||
|
if (Number.isFinite(px) && Number.isFinite(py)) {
|
||||||
|
points.push({ x: px, y: py });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.values(buildPageComponentPorts(page.port, nodes)).forEach(port => {
|
||||||
|
addPoint(port.x, port.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (!node || node.type !== 'rotatableNode' || node.data?.elementType || !node.position) return;
|
||||||
|
const box = normalizeBoxSize({ box_size: node.data?.boxSize }, fallbackSize);
|
||||||
|
const origin = {
|
||||||
|
x: Number(node.position.x) || 0,
|
||||||
|
y: Number(node.position.y) || 0
|
||||||
|
};
|
||||||
|
[
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: box.width, y: 0 },
|
||||||
|
{ x: box.width, y: box.height },
|
||||||
|
{ x: 0, y: box.height }
|
||||||
|
].forEach(corner => {
|
||||||
|
const transformed = transformBoxCorner(corner, node.data);
|
||||||
|
addPoint(origin.x + transformed.x, origin.y + transformed.y);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (points.length < 2) return fallbackSize;
|
||||||
|
const minX = Math.min(...points.map(point => point.x));
|
||||||
|
const maxX = Math.max(...points.map(point => point.x));
|
||||||
|
const minY = Math.min(...points.map(point => point.y));
|
||||||
|
const maxY = Math.max(...points.map(point => point.y));
|
||||||
|
return {
|
||||||
|
width: Math.max(1, roundMeasureValue(maxX - minX)),
|
||||||
|
height: Math.max(1, roundMeasureValue(maxY - minY))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Round ruler measurements for compact display.
|
// Round ruler measurements for compact display.
|
||||||
const roundMeasureValue = (value) => Number(value.toFixed(3));
|
const roundMeasureValue = (value) => Number(value.toFixed(3));
|
||||||
|
|
||||||
@@ -1073,6 +1120,7 @@ ${linksYaml}`;
|
|||||||
normalizeCanvasSize,
|
normalizeCanvasSize,
|
||||||
clampPositionToCanvas,
|
clampPositionToCanvas,
|
||||||
calculateLayoutBounds,
|
calculateLayoutBounds,
|
||||||
|
calculateCompositeBoxSize,
|
||||||
createRulerMeasurement,
|
createRulerMeasurement,
|
||||||
createComponentSymbolMetrics,
|
createComponentSymbolMetrics,
|
||||||
transformPortInfo,
|
transformPortInfo,
|
||||||
|
|||||||
+173
-86
@@ -1456,6 +1456,7 @@ Organization : OptiHK Limited
|
|||||||
normalizeCanvasSize,
|
normalizeCanvasSize,
|
||||||
clampPositionToCanvas,
|
clampPositionToCanvas,
|
||||||
calculateLayoutBounds,
|
calculateLayoutBounds,
|
||||||
|
calculateCompositeBoxSize,
|
||||||
buildPortHandles,
|
buildPortHandles,
|
||||||
buildElementPorts,
|
buildElementPorts,
|
||||||
buildElementBoxSize,
|
buildElementBoxSize,
|
||||||
@@ -2245,7 +2246,7 @@ Organization : OptiHK Limited
|
|||||||
}
|
}
|
||||||
const dragData = JSON.stringify(
|
const dragData = JSON.stringify(
|
||||||
isUserCell
|
isUserCell
|
||||||
? { name: componentName, type: 'composite', ports: children.__ports__ || {} }
|
? { name: componentName, type: 'composite', ports: children.__ports__ || {}, boxSize: children.__boxSize__ }
|
||||||
: { name: componentName, category: componentCategory }
|
: { name: componentName, category: componentCategory }
|
||||||
);
|
);
|
||||||
console.log("DRAG START: Sending data ->", dragData);
|
console.log("DRAG START: Sending data ->", dragData);
|
||||||
@@ -2379,7 +2380,7 @@ Organization : OptiHK Limited
|
|||||||
const cellName = children.__cellName__ || compositeName;
|
const cellName = children.__cellName__ || compositeName;
|
||||||
const tree = children.tree || {};
|
const tree = children.tree || {};
|
||||||
const handleDragStart = (event) => {
|
const handleDragStart = (event) => {
|
||||||
const dragData = JSON.stringify({ name: cellName, type: 'composite', ports: children.__ports__ || {} });
|
const dragData = JSON.stringify({ name: cellName, type: 'composite', ports: children.__ports__ || {}, boxSize: children.__boxSize__ });
|
||||||
event.dataTransfer.setData('application/reactflow', dragData);
|
event.dataTransfer.setData('application/reactflow', dragData);
|
||||||
event.dataTransfer.effectAllowed = 'move';
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
};
|
};
|
||||||
@@ -2584,7 +2585,7 @@ Organization : OptiHK Limited
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<ProjectTreeNode key={item.name} name={item.name} children={{ __type__: 'composite', __name__: item.name, tree: item.tree || {}, pageId: item.pageId, __ports__: item.__ports__ || {} }} onOpenComposite={onOpenComposite} onOpenProject={onOpenProject} onSelectInstance={onSelectInstance} onRenameCanvas={onRenameCanvas} onDeleteCanvas={onDeleteCanvas} />
|
<ProjectTreeNode key={item.name} name={item.name} children={{ __type__: 'composite', __name__: item.name, tree: item.tree || {}, pageId: item.pageId, __ports__: item.__ports__ || {}, __boxSize__: item.__boxSize__ }} onOpenComposite={onOpenComposite} onOpenProject={onOpenProject} onSelectInstance={onSelectInstance} onRenameCanvas={onRenameCanvas} onDeleteCanvas={onDeleteCanvas} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -2620,7 +2621,7 @@ Organization : OptiHK Limited
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Renders editable properties for selected nodes, ports, anchors, and routes.
|
// Renders editable properties for selected nodes, ports, anchors, and routes.
|
||||||
const RightPanel = ({ selectedNode, selectedNodes = [], selectedEdge, selectedEdges = [], technologyManifest, projectName, width, onRenameComponent, onUpdateNode, onUpdateEdgeRoute }) => {
|
const RightPanel = ({ selectedNode, selectedNodes = [], selectedEdge, selectedEdges = [], technologyManifest, projectName, compositeNames = [], width, onRenameComponent, onUpdateNode, onUpdateEdgeRoute }) => {
|
||||||
const [componentData, setComponentData] = useState(null);
|
const [componentData, setComponentData] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [enlarged, setEnlarged] = useState(null);
|
const [enlarged, setEnlarged] = useState(null);
|
||||||
@@ -2629,16 +2630,54 @@ Organization : OptiHK Limited
|
|||||||
const [localX, setLocalX] = useState('');
|
const [localX, setLocalX] = useState('');
|
||||||
const [localY, setLocalY] = useState('');
|
const [localY, setLocalY] = useState('');
|
||||||
const [localRotation, setLocalRotation] = useState('');
|
const [localRotation, setLocalRotation] = useState('');
|
||||||
|
const [editingTransformField, setEditingTransformField] = useState(null);
|
||||||
|
const MIXED_VALUE = '--';
|
||||||
|
const selectedPositionNodes = useMemo(
|
||||||
|
() => (selectedNodes.length > 0 ? selectedNodes : (selectedNode ? [selectedNode] : [])).filter(node => node && node.position),
|
||||||
|
[selectedNodes, selectedNode]
|
||||||
|
);
|
||||||
|
const isMultiNodeSelection = selectedPositionNodes.length > 1;
|
||||||
|
const isPortRotationNode = useCallback((node) => (
|
||||||
|
node?.id === 'page-port' || node?.type === 'portNode' || node?.data?.elementType === 'port'
|
||||||
|
), []);
|
||||||
|
const getNodeRotationValue = useCallback((node) => (
|
||||||
|
isPortRotationNode(node) ? (node.data?.angle ?? 0) : (node.data?.rotation ?? 0)
|
||||||
|
), [isPortRotationNode]);
|
||||||
|
const getSharedNumericDisplay = useCallback((nodes, getValue) => {
|
||||||
|
if (!nodes.length) return '';
|
||||||
|
const firstValue = Number(getValue(nodes[0]));
|
||||||
|
if (!Number.isFinite(firstValue)) return MIXED_VALUE;
|
||||||
|
const sameValue = nodes.every(node => {
|
||||||
|
const nextValue = Number(getValue(node));
|
||||||
|
return Number.isFinite(nextValue) && Math.abs(nextValue - firstValue) < 0.0005;
|
||||||
|
});
|
||||||
|
return sameValue ? firstValue.toFixed(3) : MIXED_VALUE;
|
||||||
|
}, []);
|
||||||
|
const getSharedTextDisplay = useCallback((nodes, getValue) => {
|
||||||
|
if (!nodes.length) return '';
|
||||||
|
const firstValue = getValue(nodes[0]) || '';
|
||||||
|
return nodes.every(node => String(getValue(node) || '') === String(firstValue)) ? firstValue : MIXED_VALUE;
|
||||||
|
}, []);
|
||||||
|
const clearMixedInput = useCallback((event, setter) => {
|
||||||
|
if (event.currentTarget.value === MIXED_VALUE) {
|
||||||
|
setter('');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const beginTransformInput = useCallback((event, field, setter) => {
|
||||||
|
setEditingTransformField(field);
|
||||||
|
clearMixedInput(event, setter);
|
||||||
|
}, [clearMixedInput]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nodeId = selectedNode?.id;
|
const nodeId = selectedNode?.id;
|
||||||
if (!nodeId) {
|
if (!nodeId || isMultiNodeSelection) {
|
||||||
setComponentData(null);
|
setComponentData(null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const compName = selectedNode?.data?.componentName;
|
const compName = selectedNode?.data?.componentName;
|
||||||
if (selectedNode?.data?.elementType || isBasicComponent(compName)) {
|
const selectedIsComposite = selectedNode?.data?.type === 'composite' || compositeNames.includes(compName);
|
||||||
|
if (selectedNode?.data?.elementType || selectedIsComposite || isBasicComponent(compName)) {
|
||||||
setComponentData(null);
|
setComponentData(null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -2657,7 +2696,10 @@ Organization : OptiHK Limited
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetch(`/api/component/${encodeURIComponent(compName)}?project=${encodeURIComponent(projectName || '')}`)
|
fetch(`/api/component/${encodeURIComponent(compName)}?project=${encodeURIComponent(projectName || '')}`)
|
||||||
.then(r => r.json())
|
.then(r => {
|
||||||
|
if (!r.ok) throw new Error('Component metadata not found');
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
setComponentData({ ...data, nodeId: nodeId, componentDisplayName: selectedNode.data.componentDisplayName || data.name });
|
setComponentData({ ...data, nodeId: nodeId, componentDisplayName: selectedNode.data.componentDisplayName || data.name });
|
||||||
onUpdateNode(nodeId, {
|
onUpdateNode(nodeId, {
|
||||||
@@ -2671,33 +2713,27 @@ Organization : OptiHK Limited
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
})
|
||||||
.catch(() => setLoading(false));
|
.catch(() => setLoading(false));
|
||||||
}, [selectedNode?.id, selectedNode?.data?.componentName, selectedNode?.data?.componentDisplayName, projectName, onUpdateNode]);
|
}, [selectedNode?.id, selectedNode?.data?.componentName, selectedNode?.data?.componentDisplayName, isMultiNodeSelection, compositeNames, projectName, onUpdateNode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedNode) {
|
if (editingTransformField) return;
|
||||||
setLocalX(selectedNode.position.x.toFixed(3));
|
if (selectedPositionNodes.length > 0) {
|
||||||
setLocalY(selectedNode.position.y.toFixed(3));
|
setLocalX(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x));
|
||||||
const rot = selectedNode.id === 'page-port' || selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port'
|
setLocalY(getSharedNumericDisplay(selectedPositionNodes, node => node.position.y));
|
||||||
? (selectedNode.data?.angle ?? 0)
|
setLocalRotation(getSharedNumericDisplay(selectedPositionNodes, getNodeRotationValue));
|
||||||
: (selectedNode.data?.rotation ?? 0);
|
return;
|
||||||
setLocalRotation(rot.toFixed(3));
|
|
||||||
}
|
}
|
||||||
}, [selectedNode?.position.x, selectedNode?.position.y, selectedNode?.data?.rotation, selectedNode?.data?.angle, selectedNode?.id]);
|
setLocalX('');
|
||||||
|
setLocalY('');
|
||||||
const selectedPositionNodes = useMemo(
|
setLocalRotation('');
|
||||||
() => (selectedNodes.length > 0 ? selectedNodes : (selectedNode ? [selectedNode] : [])).filter(node => node && node.position),
|
}, [selectedPositionNodes, getSharedNumericDisplay, getNodeRotationValue, editingTransformField]);
|
||||||
[selectedNodes, selectedNode]
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatePosition = useCallback((id, axis, value) => {
|
const updatePosition = useCallback((id, axis, value) => {
|
||||||
const val = parseFloat(value);
|
const val = parseFloat(value);
|
||||||
if (isNaN(val)) return;
|
if (isNaN(val)) return;
|
||||||
if (selectedPositionNodes.length > 1 && selectedPositionNodes.some(node => node.id === id)) {
|
if (selectedPositionNodes.length > 1 && selectedPositionNodes.some(node => node.id === id)) {
|
||||||
const baseNode = selectedPositionNodes.find(node => node.id === id) || selectedPositionNodes[0];
|
|
||||||
const delta = val - Number((baseNode.position && baseNode.position[axis]) || 0);
|
|
||||||
selectedPositionNodes.forEach(node => {
|
selectedPositionNodes.forEach(node => {
|
||||||
const currentValue = Number((node.position && node.position[axis]) || 0);
|
onUpdateNode(node.id, { position: { [axis]: val } });
|
||||||
onUpdateNode(node.id, { position: { [axis]: currentValue + delta } });
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2708,9 +2744,38 @@ Organization : OptiHK Limited
|
|||||||
const val = parseFloat(value);
|
const val = parseFloat(value);
|
||||||
if (isNaN(val)) return;
|
if (isNaN(val)) return;
|
||||||
const clamped = Math.min(180, Math.max(-180, val));
|
const clamped = Math.min(180, Math.max(-180, val));
|
||||||
|
if (selectedPositionNodes.length > 1 && selectedPositionNodes.some(node => node.id === id)) {
|
||||||
|
selectedPositionNodes.forEach(node => {
|
||||||
|
const dataField = isPortRotationNode(node) ? { angle: clamped } : { rotation: clamped };
|
||||||
|
onUpdateNode(node.id, { data: dataField });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const dataField = isPortNode || id === 'page-port' ? { angle: clamped } : { rotation: clamped };
|
const dataField = isPortNode || id === 'page-port' ? { angle: clamped } : { rotation: clamped };
|
||||||
onUpdateNode(id, { data: dataField });
|
onUpdateNode(id, { data: dataField });
|
||||||
}, [onUpdateNode]);
|
}, [onUpdateNode, selectedPositionNodes, isPortRotationNode]);
|
||||||
|
|
||||||
|
const commitTransformInput = useCallback((event, field, setter) => {
|
||||||
|
const rawValue = event.currentTarget.value;
|
||||||
|
const val = parseFloat(rawValue);
|
||||||
|
if (!isNaN(val) && selectedNode) {
|
||||||
|
if (field === 'x') {
|
||||||
|
updatePosition(selectedNode.id, 'x', val);
|
||||||
|
} else if (field === 'y') {
|
||||||
|
updatePosition(selectedNode.id, 'y', val);
|
||||||
|
} else {
|
||||||
|
updateRotation(selectedNode.id, val, isPortRotationNode(selectedNode));
|
||||||
|
}
|
||||||
|
setter(val.toFixed(3));
|
||||||
|
} else if (field === 'x') {
|
||||||
|
setter(getSharedNumericDisplay(selectedPositionNodes, node => node.position.x));
|
||||||
|
} else if (field === 'y') {
|
||||||
|
setter(getSharedNumericDisplay(selectedPositionNodes, node => node.position.y));
|
||||||
|
} else {
|
||||||
|
setter(getSharedNumericDisplay(selectedPositionNodes, getNodeRotationValue));
|
||||||
|
}
|
||||||
|
setEditingTransformField(null);
|
||||||
|
}, [selectedNode, selectedPositionNodes, updatePosition, updateRotation, isPortRotationNode, getSharedNumericDisplay, getNodeRotationValue]);
|
||||||
|
|
||||||
const toggleComponentTransform = useCallback((key) => {
|
const toggleComponentTransform = useCallback((key) => {
|
||||||
if (!selectedNode) return;
|
if (!selectedNode) return;
|
||||||
@@ -2736,12 +2801,18 @@ Organization : OptiHK Limited
|
|||||||
const basicMetadata = basicSelected ? getBasicComponentMetadata(selectedComponentName, selectedNode?.data?.basicArguments) : null;
|
const basicMetadata = basicSelected ? getBasicComponentMetadata(selectedComponentName, selectedNode?.data?.basicArguments) : null;
|
||||||
const basicArguments = basicSelected ? createBasicSettings(selectedComponentName, selectedNode?.data?.basicArguments) : {};
|
const basicArguments = basicSelected ? createBasicSettings(selectedComponentName, selectedNode?.data?.basicArguments) : {};
|
||||||
const forgeArguments = createForgeArguments(selectedNode?.data?.forgeArguments);
|
const forgeArguments = createForgeArguments(selectedNode?.data?.forgeArguments);
|
||||||
const selectedIsPort = selectedNode && (selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port');
|
const selectedIsPort = !isMultiNodeSelection && selectedNode && (selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port');
|
||||||
const selectedIsAnchor = selectedNode?.data?.elementType === 'anchor';
|
const selectedIsAnchor = !isMultiNodeSelection && selectedNode?.data?.elementType === 'anchor';
|
||||||
const selectedNodeBoxSize = selectedNode?.data?.componentName && !selectedNode?.data?.elementType
|
const selectedNodeBoxSize = !isMultiNodeSelection && selectedNode?.data?.componentName && !selectedNode?.data?.elementType
|
||||||
? normalizeBoxSize({ box_size: selectedNode.data?.boxSize }, DEFAULT_COMPONENT_BOX_SIZE)
|
? normalizeBoxSize({ box_size: selectedNode.data?.boxSize }, DEFAULT_COMPONENT_BOX_SIZE)
|
||||||
: null;
|
: null;
|
||||||
const xsections = Object.keys((technologyManifest || FALLBACK_TECHNOLOGY_MANIFEST).xsections || {});
|
const xsections = Object.keys((technologyManifest || FALLBACK_TECHNOLOGY_MANIFEST).xsections || {});
|
||||||
|
const sharedComponentName = isMultiNodeSelection
|
||||||
|
? getSharedTextDisplay(selectedPositionNodes, node => node.data?.componentName || node.data?.elementType || node.type || '')
|
||||||
|
: '';
|
||||||
|
const sharedDisplayName = isMultiNodeSelection
|
||||||
|
? getSharedTextDisplay(selectedPositionNodes, node => node.data?.componentDisplayName || node.data?.label || node.id)
|
||||||
|
: '';
|
||||||
|
|
||||||
const selectedRouteEdges = selectedEdges.length > 0 ? selectedEdges : (selectedEdge ? [selectedEdge] : []);
|
const selectedRouteEdges = selectedEdges.length > 0 ? selectedEdges : (selectedEdge ? [selectedEdge] : []);
|
||||||
if (selectedRouteEdges.length > 0) {
|
if (selectedRouteEdges.length > 0) {
|
||||||
@@ -2908,19 +2979,12 @@ Organization : OptiHK Limited
|
|||||||
<label>
|
<label>
|
||||||
<span>X</span>
|
<span>X</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="text"
|
||||||
step="1"
|
step="1"
|
||||||
value={localX}
|
value={localX}
|
||||||
onChange={(e) => setLocalX(e.target.value)}
|
onChange={(e) => setLocalX(e.target.value)}
|
||||||
onBlur={() => {
|
onFocus={(event) => beginTransformInput(event, 'x', setLocalX)}
|
||||||
const val = parseFloat(localX);
|
onBlur={(event) => commitTransformInput(event, 'x', setLocalX)}
|
||||||
if (!isNaN(val) && selectedNode) {
|
|
||||||
updatePosition(selectedNode.id, 'x', val);
|
|
||||||
setLocalX(val.toFixed(3));
|
|
||||||
} else if (selectedNode) {
|
|
||||||
setLocalX(selectedNode.position.x.toFixed(3));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') e.currentTarget.blur();
|
if (e.key === 'Enter') e.currentTarget.blur();
|
||||||
}}
|
}}
|
||||||
@@ -2929,19 +2993,12 @@ Organization : OptiHK Limited
|
|||||||
<label>
|
<label>
|
||||||
<span>Y</span>
|
<span>Y</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="text"
|
||||||
step="1"
|
step="1"
|
||||||
value={localY}
|
value={localY}
|
||||||
onChange={(e) => setLocalY(e.target.value)}
|
onChange={(e) => setLocalY(e.target.value)}
|
||||||
onBlur={() => {
|
onFocus={(event) => beginTransformInput(event, 'y', setLocalY)}
|
||||||
const val = parseFloat(localY);
|
onBlur={(event) => commitTransformInput(event, 'y', setLocalY)}
|
||||||
if (!isNaN(val) && selectedNode) {
|
|
||||||
updatePosition(selectedNode.id, 'y', val);
|
|
||||||
setLocalY(val.toFixed(3));
|
|
||||||
} else if (selectedNode) {
|
|
||||||
setLocalY(selectedNode.position.y.toFixed(3));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') e.currentTarget.blur();
|
if (e.key === 'Enter') e.currentTarget.blur();
|
||||||
}}
|
}}
|
||||||
@@ -2950,22 +3007,12 @@ Organization : OptiHK Limited
|
|||||||
<label>
|
<label>
|
||||||
<span>Angle</span>
|
<span>Angle</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="text"
|
||||||
step="1"
|
step="1"
|
||||||
value={localRotation}
|
value={localRotation}
|
||||||
onChange={(e) => setLocalRotation(e.target.value)}
|
onChange={(e) => setLocalRotation(e.target.value)}
|
||||||
onBlur={() => {
|
onFocus={(event) => beginTransformInput(event, 'rotation', setLocalRotation)}
|
||||||
const val = parseFloat(localRotation);
|
onBlur={(event) => commitTransformInput(event, 'rotation', setLocalRotation)}
|
||||||
if (!isNaN(val) && selectedNode) {
|
|
||||||
updateRotation(selectedNode.id, val, selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port');
|
|
||||||
setLocalRotation(val.toFixed(3));
|
|
||||||
} else if (selectedNode) {
|
|
||||||
const rot = selectedNode.id === 'page-port' || selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port'
|
|
||||||
? (selectedNode.data?.angle ?? 0)
|
|
||||||
: (selectedNode.data?.rotation ?? 0);
|
|
||||||
setLocalRotation(rot.toFixed(3));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') e.currentTarget.blur();
|
if (e.key === 'Enter') e.currentTarget.blur();
|
||||||
}}
|
}}
|
||||||
@@ -2977,7 +3024,7 @@ Organization : OptiHK Limited
|
|||||||
{selectedPositionNodes.length} selected
|
{selectedPositionNodes.length} selected
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedNode?.data?.componentName && !selectedNode?.data?.elementType && (
|
{!isMultiNodeSelection && selectedNode?.data?.componentName && !selectedNode?.data?.elementType && (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginTop: 12 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginTop: 12 }}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -3002,6 +3049,19 @@ Organization : OptiHK Limited
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isMultiNodeSelection && (
|
||||||
|
<div className="right-block" style={{ flexShrink: 0 }}>
|
||||||
|
<div className="right-block-header">Component Information</div>
|
||||||
|
<div className="right-block-body">
|
||||||
|
<div style={{ display: 'grid', gap: 8, color: 'var(--text-muted)' }}>
|
||||||
|
<div><strong style={{ color: 'var(--text-main)' }}>Selection:</strong> {selectedPositionNodes.length} components</div>
|
||||||
|
<div><strong style={{ color: 'var(--text-main)' }}>Name:</strong> {sharedDisplayName || MIXED_VALUE}</div>
|
||||||
|
<div><strong style={{ color: 'var(--text-main)' }}>Component:</strong> {sharedComponentName || MIXED_VALUE}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedIsPort && (
|
{selectedIsPort && (
|
||||||
<div className="right-block" style={{ flexShrink: 0 }}>
|
<div className="right-block" style={{ flexShrink: 0 }}>
|
||||||
<div className="right-block-header">Port</div>
|
<div className="right-block-header">Port</div>
|
||||||
@@ -3095,7 +3155,7 @@ Organization : OptiHK Limited
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedNode?.data?.componentName && !selectedNode?.data?.elementType && (
|
{!isMultiNodeSelection && selectedNode?.data?.componentName && !selectedNode?.data?.elementType && (
|
||||||
<div className="right-block" style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
<div className="right-block" style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||||
<div className="right-block-header">Parameters</div>
|
<div className="right-block-header">Parameters</div>
|
||||||
<div className="right-block-body" style={{ flex: 1, overflowY: 'auto' }}>
|
<div className="right-block-body" style={{ flex: 1, overflowY: 'auto' }}>
|
||||||
@@ -3508,6 +3568,8 @@ Organization : OptiHK Limited
|
|||||||
const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []);
|
const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []);
|
||||||
|
|
||||||
const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]);
|
const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]);
|
||||||
|
const compositePageNames = useMemo(() => pages.filter(page => page.type === 'composite').map(page => page.name), [pages]);
|
||||||
|
const compositePageNameSet = useMemo(() => new Set(compositePageNames), [compositePageNames]);
|
||||||
const currentNodes = activePage && Array.isArray(activePage.nodes) ? activePage.nodes : [];
|
const currentNodes = activePage && Array.isArray(activePage.nodes) ? activePage.nodes : [];
|
||||||
const currentEdges = activePage && Array.isArray(activePage.edges) ? activePage.edges : [];
|
const currentEdges = activePage && Array.isArray(activePage.edges) ? activePage.edges : [];
|
||||||
const activeCanvasSize = useMemo(() => normalizeCanvasSize(activePage?.canvasSize), [activePage?.canvasSize]);
|
const activeCanvasSize = useMemo(() => normalizeCanvasSize(activePage?.canvasSize), [activePage?.canvasSize]);
|
||||||
@@ -3816,7 +3878,7 @@ Organization : OptiHK Limited
|
|||||||
|
|
||||||
// Fetch metadata for a component before creating a loaded or dropped node.
|
// Fetch metadata for a component before creating a loaded or dropped node.
|
||||||
const loadComponentMetadata = useCallback(async (componentName) => {
|
const loadComponentMetadata = useCallback(async (componentName) => {
|
||||||
if (!componentName || isForgeComponent(componentName)) return null;
|
if (!componentName || isForgeComponent(componentName) || compositePageNameSet.has(componentName)) return null;
|
||||||
if (componentDataCacheRef.current.has(componentName)) {
|
if (componentDataCacheRef.current.has(componentName)) {
|
||||||
return componentDataCacheRef.current.get(componentName);
|
return componentDataCacheRef.current.get(componentName);
|
||||||
}
|
}
|
||||||
@@ -3825,7 +3887,7 @@ Organization : OptiHK Limited
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
componentDataCacheRef.current.set(componentName, data);
|
componentDataCacheRef.current.set(componentName, data);
|
||||||
return data;
|
return data;
|
||||||
}, [currentProjectName]);
|
}, [currentProjectName, compositePageNameSet]);
|
||||||
|
|
||||||
// Send an auditable user action to the backend log endpoint.
|
// Send an auditable user action to the backend log endpoint.
|
||||||
const recordUserAction = useCallback((action, payload = {}) => {
|
const recordUserAction = useCallback((action, payload = {}) => {
|
||||||
@@ -4648,7 +4710,7 @@ Organization : OptiHK Limited
|
|||||||
return category;
|
return category;
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageFromYaml = (cellName, content, manifest) => {
|
const pageFromYaml = (cellName, content, manifest, knownCompositeNames = new Set()) => {
|
||||||
const doc = jsyaml.load(content) || {};
|
const doc = jsyaml.load(content) || {};
|
||||||
const usesGdsYUp = doc.coordinate_system === 'gds_y_up';
|
const usesGdsYUp = doc.coordinate_system === 'gds_y_up';
|
||||||
const firstPort = Array.isArray(doc.ports) ? doc.ports[0] : null;
|
const firstPort = Array.isArray(doc.ports) ? doc.ports[0] : null;
|
||||||
@@ -4676,6 +4738,7 @@ Organization : OptiHK Limited
|
|||||||
const instIsForge = isForgeComponent(compPath) || isForgeComponent(compName);
|
const instIsForge = isForgeComponent(compPath) || isForgeComponent(compName);
|
||||||
const instIsBasic = isBasicComponent(compPath) || isBasicComponent(compName);
|
const instIsBasic = isBasicComponent(compPath) || isBasicComponent(compName);
|
||||||
const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : (instIsBasic ? compPath : compName);
|
const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : (instIsBasic ? compPath : compName);
|
||||||
|
const instIsComposite = knownCompositeNames.has(compName);
|
||||||
const basicMetadata = instIsBasic ? getBasicComponentMetadata(displayCompName, inst.settings) : null;
|
const basicMetadata = instIsBasic ? getBasicComponentMetadata(displayCompName, inst.settings) : null;
|
||||||
const loadedAvailableComponents = getAvailableComponentsForLoadedComponent(displayCompName);
|
const loadedAvailableComponents = getAvailableComponentsForLoadedComponent(displayCompName);
|
||||||
const nodeId = `node-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
const nodeId = `node-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
||||||
@@ -4688,13 +4751,14 @@ Organization : OptiHK Limited
|
|||||||
y: usesGdsYUp ? layoutToCanvasY(inst.y) : (parseFloat(inst.y) || 0),
|
y: usesGdsYUp ? layoutToCanvasY(inst.y) : (parseFloat(inst.y) || 0),
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
label: displayCompName,
|
label: instIsComposite ? instName : displayCompName,
|
||||||
componentName: displayCompName,
|
componentName: instIsComposite ? compName : displayCompName,
|
||||||
category: instIsForge ? '' : findCategory(displayCompName),
|
category: instIsComposite || instIsForge ? '' : findCategory(displayCompName),
|
||||||
rotation: parseFloat(inst.rotation) || 0,
|
rotation: parseFloat(inst.rotation) || 0,
|
||||||
flip: toBooleanFlag(inst.flip ?? inst.mirror),
|
flip: toBooleanFlag(inst.flip ?? inst.mirror),
|
||||||
flop: toBooleanFlag(inst.flop),
|
flop: toBooleanFlag(inst.flop),
|
||||||
componentDisplayName: instName,
|
componentDisplayName: instName,
|
||||||
|
type: instIsComposite ? 'composite' : undefined,
|
||||||
availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents,
|
availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents,
|
||||||
ports: instIsBasic ? (basicMetadata?.ports || {}) : (instIsForge ? {} : undefined),
|
ports: instIsBasic ? (basicMetadata?.ports || {}) : (instIsForge ? {} : undefined),
|
||||||
boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined,
|
boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined,
|
||||||
@@ -4763,7 +4827,18 @@ Organization : OptiHK Limited
|
|||||||
const technology = data.technology || '';
|
const technology = data.technology || '';
|
||||||
setProjectTechnology(technology);
|
setProjectTechnology(technology);
|
||||||
const manifest = await loadTechnologyManifest(technology);
|
const manifest = await loadTechnologyManifest(technology);
|
||||||
const cellPages = (data.cells || []).map(cell => pageFromYaml(cell.name, cell.content, manifest));
|
const knownCompositeNames = new Set((data.cells || []).map(cell => cell.name).filter(name => name !== currentProjectName));
|
||||||
|
const parsedCellPages = (data.cells || []).map(cell => pageFromYaml(cell.name, cell.content, manifest, knownCompositeNames));
|
||||||
|
const compositeBoxSizes = new Map(parsedCellPages
|
||||||
|
.filter(page => page.type === 'composite')
|
||||||
|
.map(page => [page.name, calculateCompositeBoxSize(page)]));
|
||||||
|
const cellPages = parsedCellPages.map(page => ({
|
||||||
|
...page,
|
||||||
|
nodes: page.nodes.map(node => {
|
||||||
|
const boxSize = compositeBoxSizes.get(node.data?.componentName);
|
||||||
|
return boxSize ? { ...node, data: { ...node.data, boxSize } } : node;
|
||||||
|
})
|
||||||
|
}));
|
||||||
const loadedProjectPage = cellPages.find(page => page.type === 'project' && page.name === currentProjectName);
|
const loadedProjectPage = cellPages.find(page => page.type === 'project' && page.name === currentProjectName);
|
||||||
const nonProjectPages = cellPages.filter(page => page !== loadedProjectPage);
|
const nonProjectPages = cellPages.filter(page => page !== loadedProjectPage);
|
||||||
const resolvedProjectPage = loadedProjectPage || projectPage;
|
const resolvedProjectPage = loadedProjectPage || projectPage;
|
||||||
@@ -4810,23 +4885,26 @@ Organization : OptiHK Limited
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const compositePages = new Map(pages.filter(page => page.type === 'composite').map(page => [page.name, page]));
|
const compositePages = new Map(pages.filter(page => page.type === 'composite').map(page => [page.name, page]));
|
||||||
const portUpdates = new Map();
|
const compositeUpdates = new Map();
|
||||||
pages.forEach(page => {
|
pages.forEach(page => {
|
||||||
page.nodes.forEach(node => {
|
page.nodes.forEach(node => {
|
||||||
const compPage = compositePages.get(node.data?.componentName);
|
const compPage = compositePages.get(node.data?.componentName);
|
||||||
if (!compPage) return;
|
if (!compPage) return;
|
||||||
const nextPorts = buildPageComponentPorts(compPage.port, compPage.nodes);
|
const nextPorts = buildPageComponentPorts(compPage.port, compPage.nodes);
|
||||||
if (JSON.stringify(node.data?.ports || {}) !== JSON.stringify(nextPorts)) {
|
const nextBoxSize = calculateCompositeBoxSize(compPage);
|
||||||
portUpdates.set(node.id, nextPorts);
|
const portsChanged = JSON.stringify(node.data?.ports || {}) !== JSON.stringify(nextPorts);
|
||||||
|
const boxSizeChanged = JSON.stringify(node.data?.boxSize || {}) !== JSON.stringify(nextBoxSize);
|
||||||
|
if (portsChanged || boxSizeChanged) {
|
||||||
|
compositeUpdates.set(node.id, { ports: nextPorts, boxSize: nextBoxSize });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (portUpdates.size === 0) return;
|
if (compositeUpdates.size === 0) return;
|
||||||
setPages(prev => prev.map(page => ({
|
setPages(prev => prev.map(page => ({
|
||||||
...page,
|
...page,
|
||||||
nodes: page.nodes.map(node => (
|
nodes: page.nodes.map(node => (
|
||||||
portUpdates.has(node.id)
|
compositeUpdates.has(node.id)
|
||||||
? { ...node, data: { ...node.data, ports: portUpdates.get(node.id) } }
|
? { ...node, data: { ...node.data, ...compositeUpdates.get(node.id) } }
|
||||||
: node
|
: node
|
||||||
))
|
))
|
||||||
})));
|
})));
|
||||||
@@ -4837,7 +4915,7 @@ Organization : OptiHK Limited
|
|||||||
pages.forEach(page => {
|
pages.forEach(page => {
|
||||||
page.nodes.forEach(node => {
|
page.nodes.forEach(node => {
|
||||||
const componentName = node.data?.componentName;
|
const componentName = node.data?.componentName;
|
||||||
if (node.data?.elementType || !componentName || isForgeComponent(componentName) || node.data?.type === 'composite') return;
|
if (node.data?.elementType || !componentName || isForgeComponent(componentName) || node.data?.type === 'composite' || compositePageNameSet.has(componentName)) return;
|
||||||
if (isBasicComponent(componentName)) {
|
if (isBasicComponent(componentName)) {
|
||||||
if (node.data?.ports && node.data?.boxSize) return;
|
if (node.data?.ports && node.data?.boxSize) return;
|
||||||
const metadata = getBasicComponentMetadata(componentName, node.data?.basicArguments);
|
const metadata = getBasicComponentMetadata(componentName, node.data?.basicArguments);
|
||||||
@@ -5178,10 +5256,11 @@ Organization : OptiHK Limited
|
|||||||
parsedData = { name: rawData, category: 'default' };
|
parsedData = { name: rawData, category: 'default' };
|
||||||
}
|
}
|
||||||
if (parsedData.type === 'standaloneComposite') {
|
if (parsedData.type === 'standaloneComposite') {
|
||||||
|
const compositeBoxSize = normalizeBoxSize({ box_size: parsedData.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
|
||||||
const position = clampPositionToCanvas(
|
const position = clampPositionToCanvas(
|
||||||
reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }),
|
reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }),
|
||||||
activePage?.canvasSize || activeCanvasSize,
|
activePage?.canvasSize || activeCanvasSize,
|
||||||
DEFAULT_COMPONENT_BOX_SIZE
|
compositeBoxSize
|
||||||
);
|
);
|
||||||
const newNode = {
|
const newNode = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
@@ -5194,7 +5273,8 @@ Organization : OptiHK Limited
|
|||||||
type: 'composite',
|
type: 'composite',
|
||||||
category: null,
|
category: null,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
ports: parsedData.ports || {}
|
ports: parsedData.ports || {},
|
||||||
|
boxSize: compositeBoxSize
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
setPages(prev => prev.map(p => {
|
setPages(prev => prev.map(p => {
|
||||||
@@ -5227,10 +5307,11 @@ Organization : OptiHK Limited
|
|||||||
addLog(`Skipped self-reference: "${parsedData.name}" cannot be placed inside itself.`);
|
addLog(`Skipped self-reference: "${parsedData.name}" cannot be placed inside itself.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const compositeBoxSize = normalizeBoxSize({ box_size: parsedData.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
|
||||||
const position = clampPositionToCanvas(
|
const position = clampPositionToCanvas(
|
||||||
reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }),
|
reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }),
|
||||||
activePage?.canvasSize || activeCanvasSize,
|
activePage?.canvasSize || activeCanvasSize,
|
||||||
DEFAULT_COMPONENT_BOX_SIZE
|
compositeBoxSize
|
||||||
);
|
);
|
||||||
const newNode = {
|
const newNode = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
@@ -5243,7 +5324,8 @@ Organization : OptiHK Limited
|
|||||||
type: 'composite',
|
type: 'composite',
|
||||||
category: null,
|
category: null,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
ports: parsedData.ports || {}
|
ports: parsedData.ports || {},
|
||||||
|
boxSize: compositeBoxSize
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
setPages(prev => prev.map(p => {
|
setPages(prev => prev.map(p => {
|
||||||
@@ -5651,7 +5733,8 @@ Organization : OptiHK Limited
|
|||||||
__cellName__: componentName,
|
__cellName__: componentName,
|
||||||
tree: compositeTrees[componentName] || {},
|
tree: compositeTrees[componentName] || {},
|
||||||
pageId: compPage.id,
|
pageId: compPage.id,
|
||||||
__ports__: buildPageComponentPorts(compPage.port, compPage.nodes)
|
__ports__: buildPageComponentPorts(compPage.port, compPage.nodes),
|
||||||
|
__boxSize__: calculateCompositeBoxSize(compPage)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -5672,7 +5755,8 @@ Organization : OptiHK Limited
|
|||||||
__cellName__: name,
|
__cellName__: name,
|
||||||
tree: compositeTrees[name] || {},
|
tree: compositeTrees[name] || {},
|
||||||
pageId: compPage ? compPage.id : name,
|
pageId: compPage ? compPage.id : name,
|
||||||
__ports__: compPage ? buildPageComponentPorts(compPage.port, compPage.nodes) : {}
|
__ports__: compPage ? buildPageComponentPorts(compPage.port, compPage.nodes) : {},
|
||||||
|
__boxSize__: compPage ? calculateCompositeBoxSize(compPage) : DEFAULT_COMPONENT_BOX_SIZE
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
items.push({
|
items.push({
|
||||||
@@ -5688,7 +5772,8 @@ Organization : OptiHK Limited
|
|||||||
name: name,
|
name: name,
|
||||||
tree: compositeTrees[name] || {},
|
tree: compositeTrees[name] || {},
|
||||||
pageId: compPage?.id || name,
|
pageId: compPage?.id || name,
|
||||||
__ports__: buildPageComponentPorts(compPage?.port, compPage?.nodes)
|
__ports__: buildPageComponentPorts(compPage?.port, compPage?.nodes),
|
||||||
|
__boxSize__: compPage ? calculateCompositeBoxSize(compPage) : DEFAULT_COMPONENT_BOX_SIZE
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return items;
|
return items;
|
||||||
@@ -5705,7 +5790,8 @@ Organization : OptiHK Limited
|
|||||||
__name__: page.name,
|
__name__: page.name,
|
||||||
__category__: 'composite',
|
__category__: 'composite',
|
||||||
__cell__: true,
|
__cell__: true,
|
||||||
__ports__: buildPageComponentPorts(page.port, page.nodes)
|
__ports__: buildPageComponentPorts(page.port, page.nodes),
|
||||||
|
__boxSize__: calculateCompositeBoxSize(page)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const basicEntries = {
|
const basicEntries = {
|
||||||
@@ -6159,6 +6245,7 @@ ${bundlesBlock}`;
|
|||||||
selectedEdges={selectedEdges}
|
selectedEdges={selectedEdges}
|
||||||
technologyManifest={technologyManifest}
|
technologyManifest={technologyManifest}
|
||||||
projectName={currentProjectName}
|
projectName={currentProjectName}
|
||||||
|
compositeNames={compositePageNames}
|
||||||
width={rightWidth}
|
width={rightWidth}
|
||||||
onRenameComponent={renameComponent}
|
onRenameComponent={renameComponent}
|
||||||
onUpdateNode={handleUpdateNode}
|
onUpdateNode={handleUpdateNode}
|
||||||
|
|||||||
+970
-489
File diff suppressed because it is too large
Load Diff
+410
-177
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Description: Login page for authenticating users before entering the MXPIC EDA workspace.
|
Description: Login page for authenticating users before entering the MXPIC EDA workspace.
|
||||||
Inside functions: applyTheme
|
Inside functions: applyTheme, setSubmitState
|
||||||
Developer : Qin Yue @ 2026
|
Developer : Qin Yue @ 2026
|
||||||
Organization : OptiHK Limited
|
Organization : OptiHK Limited
|
||||||
-->
|
-->
|
||||||
@@ -15,68 +15,82 @@ Organization : OptiHK Limited
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg-main: #060b16;
|
--bg-main: #06101d;
|
||||||
--bg-panel: #0c1424;
|
--bg-panel: #0c1728;
|
||||||
--bg-card: #121b2d;
|
--bg-card: #111c2e;
|
||||||
--bg-soft: #182237;
|
--bg-soft: #17243a;
|
||||||
--text-main: #f6f8fb;
|
--text-main: #f6f8fb;
|
||||||
--text-muted: #91a0b5;
|
--text-muted: #97a6ba;
|
||||||
--accent: #6ee7ff;
|
--accent: #5dd8f3;
|
||||||
--accent-strong: #7c3aed;
|
--accent-strong: #2563eb;
|
||||||
--accent-warm: #f97316;
|
--accent-warm: #d97706;
|
||||||
|
--accent-green: #34d399;
|
||||||
--accent-red: #ef4444;
|
--accent-red: #ef4444;
|
||||||
--border: #28364c;
|
--border: #2a394f;
|
||||||
--border-strong: #42516a;
|
--border-strong: #496078;
|
||||||
--input-bg: #09111f;
|
--input-bg: #091320;
|
||||||
--shadow: rgba(0, 0, 0, 0.42);
|
--shadow: rgba(0, 0, 0, 0.38);
|
||||||
--error-text: #fecaca;
|
--error-text: #fecaca;
|
||||||
--error-bg: rgba(239, 68, 68, 0.14);
|
--error-bg: rgba(239, 68, 68, 0.14);
|
||||||
|
--focus-ring: rgba(93, 216, 243, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light-mode {
|
body.light-mode {
|
||||||
--bg-main: #edf3f8;
|
--bg-main: #f4f7fb;
|
||||||
--bg-panel: #f8fbff;
|
--bg-panel: #ffffff;
|
||||||
--bg-card: #ffffff;
|
--bg-card: #ffffff;
|
||||||
--bg-soft: #eef5fb;
|
--bg-soft: #eef4fa;
|
||||||
--text-main: #132032;
|
--text-main: #142235;
|
||||||
--text-muted: #64758a;
|
--text-muted: #5f7085;
|
||||||
--accent: #2563eb;
|
--accent: #1d4ed8;
|
||||||
--accent-strong: #0f9f7a;
|
--accent-strong: #0f766e;
|
||||||
--accent-warm: #38bdf8;
|
--accent-warm: #b45309;
|
||||||
|
--accent-green: #15803d;
|
||||||
--accent-red: #dc2626;
|
--accent-red: #dc2626;
|
||||||
--border: #d5e0eb;
|
--border: #d6e0eb;
|
||||||
--border-strong: #b8c7d8;
|
--border-strong: #b6c4d4;
|
||||||
--input-bg: #f5f8fb;
|
--input-bg: #f8fafc;
|
||||||
--shadow: rgba(37, 99, 235, 0.13);
|
--shadow: rgba(37, 99, 235, 0.12);
|
||||||
--error-text: #b91c1c;
|
--error-text: #991b1b;
|
||||||
--error-bg: rgba(220, 38, 38, 0.08);
|
--error-bg: rgba(220, 38, 38, 0.08);
|
||||||
|
--focus-ring: rgba(29, 78, 216, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
min-height: 100dvh;
|
||||||
font-family: 'IBM Plex Sans', "Segoe UI", sans-serif;
|
font-family: 'IBM Plex Sans', "Segoe UI", sans-serif;
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
background:
|
background:
|
||||||
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
linear-gradient(90deg, rgba(255, 255, 255, 0.026) 1px, transparent 1px),
|
||||||
linear-gradient(0deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
linear-gradient(0deg, rgba(255, 255, 255, 0.026) 1px, transparent 1px),
|
||||||
radial-gradient(circle at 16% 12%, rgba(124, 58, 237, 0.25), transparent 28%),
|
linear-gradient(135deg, var(--bg-main), #091526 62%, #0d1724);
|
||||||
radial-gradient(circle at 84% 82%, rgba(249, 115, 22, 0.16), transparent 26%),
|
background-size: 48px 48px, 48px 48px, auto;
|
||||||
linear-gradient(135deg, var(--bg-main), #0a1222 55%, #171923);
|
|
||||||
background-size: 42px 42px, 42px 42px, auto, auto, auto;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 28px;
|
padding: 28px;
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light-mode {
|
body.light-mode {
|
||||||
background:
|
background:
|
||||||
linear-gradient(90deg, rgba(37, 99, 235, 0.055) 1px, transparent 1px),
|
linear-gradient(90deg, rgba(29, 78, 216, 0.04) 1px, transparent 1px),
|
||||||
linear-gradient(0deg, rgba(37, 99, 235, 0.055) 1px, transparent 1px),
|
linear-gradient(0deg, rgba(29, 78, 216, 0.04) 1px, transparent 1px),
|
||||||
radial-gradient(circle at 16% 12%, rgba(56, 189, 248, 0.18), transparent 28%),
|
linear-gradient(135deg, #f8fbff, var(--bg-main));
|
||||||
radial-gradient(circle at 84% 82%, rgba(34, 197, 94, 0.13), transparent 24%),
|
}
|
||||||
linear-gradient(135deg, #f9fbfd, var(--bg-main));
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus-visible,
|
||||||
|
input:focus-visible {
|
||||||
|
outline: 3px solid var(--focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle {
|
.theme-toggle {
|
||||||
@@ -84,110 +98,90 @@ Organization : OptiHK Limited
|
|||||||
top: 20px;
|
top: 20px;
|
||||||
right: 22px;
|
right: 22px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: rgba(18, 27, 45, 0.88);
|
background: rgba(12, 23, 40, 0.86);
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
min-width: 92px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
padding: 0 14px;
|
padding: 0 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: inherit;
|
font-weight: 700;
|
||||||
font-weight: 600;
|
box-shadow: 0 12px 28px var(--shadow);
|
||||||
box-shadow: 0 14px 34px var(--shadow);
|
backdrop-filter: blur(14px);
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light-mode .theme-toggle {
|
body.light-mode .theme-toggle {
|
||||||
background: rgba(255, 255, 255, 0.86);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-shell {
|
|
||||||
width: min(980px, 100%);
|
|
||||||
min-height: 560px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 400px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: rgba(12, 20, 36, 0.84);
|
|
||||||
box-shadow: 0 32px 96px var(--shadow);
|
|
||||||
backdrop-filter: blur(18px);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.light-mode .login-shell {
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-shell {
|
||||||
|
width: min(1040px, 100%);
|
||||||
|
min-height: 590px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 410px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(12, 23, 40, 0.88);
|
||||||
|
box-shadow: 0 30px 80px var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .login-shell {
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
.login-visual {
|
.login-visual {
|
||||||
padding: 44px;
|
padding: 38px;
|
||||||
background:
|
background: var(--bg-panel);
|
||||||
linear-gradient(135deg, rgba(110, 231, 255, 0.1), transparent 35%),
|
|
||||||
linear-gradient(315deg, rgba(124, 58, 237, 0.16), transparent 42%),
|
|
||||||
var(--bg-panel);
|
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 28px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-visual::before {
|
.brand-row {
|
||||||
content: "";
|
display: flex;
|
||||||
position: absolute;
|
align-items: center;
|
||||||
inset: 92px 48px auto auto;
|
justify-content: space-between;
|
||||||
width: 190px;
|
gap: 18px;
|
||||||
height: 190px;
|
|
||||||
border: 1px solid rgba(110, 231, 255, 0.2);
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 22px rgba(110, 231, 255, 0.025),
|
|
||||||
inset 0 0 0 28px rgba(249, 115, 22, 0.045);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-visual::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: auto 44px 130px auto;
|
|
||||||
width: 260px;
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(90deg, transparent, var(--accent), transparent);
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-visual > * {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-logo {
|
.brand-logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
font-size: 1.25rem;
|
font-size: 1.02rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-logo span {
|
.brand-logo .company-hk {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.company-wordmark {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
.brand-mark {
|
.brand-mark {
|
||||||
width: 36px;
|
width: 34px;
|
||||||
height: 36px;
|
height: 34px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background:
|
background: linear-gradient(135deg, var(--accent), var(--accent-strong) 62%, var(--accent-warm));
|
||||||
linear-gradient(135deg, var(--accent), var(--accent-strong) 58%, var(--accent-warm));
|
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 0 0 5px rgba(110, 231, 255, 0.09);
|
box-shadow: 0 0 0 5px rgba(93, 216, 243, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-mark::before,
|
.brand-mark::before,
|
||||||
.brand-mark::after {
|
.brand-mark::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: white;
|
background: #ffffff;
|
||||||
opacity: 0.9;
|
opacity: 0.92;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-mark::before {
|
.brand-mark::before {
|
||||||
@@ -204,71 +198,241 @@ Organization : OptiHK Limited
|
|||||||
width: 2px;
|
width: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-kicker {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'IBM Plex Mono', Consolas, monospace;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
.visual-copy h1 {
|
.visual-copy h1 {
|
||||||
margin: 0 0 14px 0;
|
margin: 0 0 14px 0;
|
||||||
max-width: 430px;
|
max-width: 520px;
|
||||||
font-size: clamp(2.1rem, 4vw, 3.25rem);
|
font-size: clamp(2.25rem, 4.4vw, 3.7rem);
|
||||||
line-height: 1.02;
|
line-height: 1.02;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visual-copy p {
|
.visual-copy p {
|
||||||
max-width: 430px;
|
max-width: 520px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-strip {
|
.eda-preview {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-pill {
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: rgba(18, 27, 45, 0.72);
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px;
|
overflow: hidden;
|
||||||
color: var(--text-muted);
|
background: var(--bg-card);
|
||||||
font-size: 0.78rem;
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-pill strong {
|
.eda-preview img {
|
||||||
display: block;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-bar {
|
||||||
|
height: 36px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'IBM Plex Mono', Consolas, monospace;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-status {
|
||||||
|
color: var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-workspace {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 150px minmax(0, 1fr);
|
||||||
|
min-height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-sidebar {
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
background: var(--input-bg);
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'IBM Plex Mono', Consolas, monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-row {
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
font-size: 0.92rem;
|
border-radius: 6px;
|
||||||
margin-bottom: 2px;
|
padding: 0 8px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-row.muted {
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding-left: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-chip {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-chip span {
|
||||||
|
display: block;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 9px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'IBM Plex Mono', Consolas, monospace;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
background: var(--bg-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-canvas {
|
||||||
|
position: relative;
|
||||||
|
min-height: 260px;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
||||||
|
linear-gradient(0deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
||||||
|
var(--bg-card);
|
||||||
|
background-size: 28px 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .preview-canvas {
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgba(29, 78, 216, 0.05) 1px, transparent 1px),
|
||||||
|
linear-gradient(0deg, rgba(29, 78, 216, 0.05) 1px, transparent 1px),
|
||||||
|
var(--bg-card);
|
||||||
|
background-size: 28px 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node {
|
||||||
|
position: absolute;
|
||||||
|
width: 94px;
|
||||||
|
height: 44px;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(180deg, var(--bg-soft), var(--bg-card));
|
||||||
|
color: var(--text-main);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-family: 'IBM Plex Mono', Consolas, monospace;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 14px 28px var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node.a {
|
||||||
|
left: 42px;
|
||||||
|
top: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node.b {
|
||||||
|
right: 46px;
|
||||||
|
top: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node.c {
|
||||||
|
left: 50%;
|
||||||
|
top: 154px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.route {
|
||||||
|
position: absolute;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--accent);
|
||||||
|
box-shadow: 0 0 12px rgba(93, 216, 243, 0.45);
|
||||||
|
transform-origin: left center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route.one {
|
||||||
|
width: 124px;
|
||||||
|
left: 132px;
|
||||||
|
top: 74px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route.two {
|
||||||
|
width: 102px;
|
||||||
|
left: 122px;
|
||||||
|
top: 144px;
|
||||||
|
transform: rotate(34deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.route.three {
|
||||||
|
width: 102px;
|
||||||
|
right: 118px;
|
||||||
|
top: 144px;
|
||||||
|
transform: rotate(-34deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card {
|
.login-card {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.032), transparent),
|
||||||
var(--bg-card);
|
var(--bg-card);
|
||||||
padding: 44px;
|
padding: 42px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-product {
|
||||||
|
display: none;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
.system-title {
|
.system-title {
|
||||||
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.74rem;
|
font-size: 0.74rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.05em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card h2 {
|
.login-card h2 {
|
||||||
margin: 0 0 30px 0;
|
margin: 0 0 8px 0;
|
||||||
font-size: 1.48rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-card .form-note {
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0 0 28px;
|
||||||
|
font-size: 0.94rem;
|
||||||
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -284,149 +448,218 @@ Organization : OptiHK Limited
|
|||||||
label {
|
label {
|
||||||
font-size: 0.84rem;
|
font-size: 0.84rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-row {
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="password"] {
|
input[type="password"] {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 46px;
|
||||||
background-color: var(--input-bg);
|
background-color: var(--input-bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.98rem;
|
font-size: 0.98rem;
|
||||||
padding: 13px 14px;
|
padding: 12px 14px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.password-row input {
|
||||||
|
padding-right: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="text"]:focus,
|
input[type="text"]:focus,
|
||||||
input[type="password"]:focus {
|
input[type="password"]:focus {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
box-shadow: 0 0 0 3px rgba(110, 231, 255, 0.15);
|
box-shadow: 0 0 0 3px var(--focus-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
button[type="submit"] {
|
.password-toggle {
|
||||||
|
position: absolute;
|
||||||
|
right: 7px;
|
||||||
|
top: 7px;
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-soft);
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
min-height: 46px;
|
||||||
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
|
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
|
||||||
color: #04101f;
|
color: #04101f;
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.98rem;
|
font-size: 0.98rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 13px;
|
padding: 13px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.1s ease, box-shadow 0.2s ease;
|
transition: transform 0.1s ease, box-shadow 0.2s ease, opacity 0.2s ease;
|
||||||
margin-top: 8px;
|
margin-top: 4px;
|
||||||
box-shadow: 0 16px 34px rgba(37, 99, 235, 0.28);
|
box-shadow: 0 14px 28px rgba(37, 99, 235, 0.24);
|
||||||
}
|
}
|
||||||
|
|
||||||
button[type="submit"]:hover {
|
.submit-btn:hover:not(:disabled) {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
button[type="submit"]:active {
|
.submit-btn:active:not(:disabled) {
|
||||||
transform: scale(0.99);
|
transform: scale(0.99);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.submit-btn:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
background-color: var(--error-bg);
|
background-color: var(--error-bg);
|
||||||
color: var(--error-text);
|
color: var(--error-text);
|
||||||
border: 1px solid rgba(239, 68, 68, 0.38);
|
border: 1px solid rgba(239, 68, 68, 0.42);
|
||||||
padding: 12px;
|
padding: 11px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.86rem;
|
||||||
text-align: center;
|
line-height: 1.45;
|
||||||
margin-top: 20px;
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
position: static;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
|
||||||
.login-shell {
|
.login-shell {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
min-height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-visual {
|
.login-visual {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle {
|
.mobile-product {
|
||||||
position: static;
|
display: block;
|
||||||
justify-self: end;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
.login-card {
|
||||||
display: flex;
|
padding: 30px 22px;
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<button class="theme-toggle" id="theme-toggle" type="button">Bright Mode</button>
|
<button class="theme-toggle" id="theme-toggle" type="button" aria-label="Switch color theme">Light mode</button>
|
||||||
|
|
||||||
<main class="login-shell">
|
<main class="login-shell">
|
||||||
<section class="login-visual">
|
<section class="login-visual" aria-label="mxPIC EDA workspace preview">
|
||||||
|
<div class="brand-row">
|
||||||
<div class="brand-logo">
|
<div class="brand-logo">
|
||||||
<div class="brand-mark"></div>
|
<div class="brand-mark" aria-hidden="true"></div>
|
||||||
opti<span>hk</span>
|
MxPic - by <span class="company-wordmark">Opti<span class="company-hk">HK</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="product-kicker">EDA platform</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="visual-copy">
|
<div class="visual-copy">
|
||||||
<h1>Photonic EDA workspace for reusable PIC cells.</h1>
|
<h1>MxPIC</h1>
|
||||||
<p>Build hierarchical canvases from PDK components, inspect ports and parameters, and prepare structured layout definitions for mxPIC generation.</p>
|
<p>A end-to-end EDA solution for photonic integrated</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="status-strip">
|
<div class="eda-preview">
|
||||||
<div class="status-pill"><strong>PDK</strong>Silterra ready</div>
|
<img src="/frontend/assets/pic-mzm-directional-couplers.png" alt="2D render of a photonic integrated circuit with MZM arms and directional couplers">
|
||||||
<div class="status-pill"><strong>Cells</strong>Hierarchical</div>
|
|
||||||
<div class="status-pill"><strong>Flow</strong>Canvas to GDS</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="login-card">
|
<section class="login-card">
|
||||||
<div class="system-title">mxPIC Core Access</div>
|
<div class="mobile-product">
|
||||||
<h2>Sign in to your EDA workspace</h2>
|
<div class="brand-logo">
|
||||||
|
<div class="brand-mark" aria-hidden="true"></div>
|
||||||
|
MxPic - by <span class="company-wordmark">Opti<span class="company-hk">HK</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form action="/login" method="POST">
|
<div class="system-title">Access</div>
|
||||||
|
<h2>Sign in</h2>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="error-message" role="alert">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form action="/login" method="POST" id="login-form">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input type="text" id="username" name="username" required autocomplete="username">
|
<input type="text" id="username" name="username" required autocomplete="username" autofocus>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
|
<div class="password-row">
|
||||||
<input type="password" id="password" name="password" required autocomplete="current-password">
|
<input type="password" id="password" name="password" required autocomplete="current-password">
|
||||||
|
<button class="password-toggle" id="password-toggle" type="button" aria-label="Show password">Show</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit">Log In</button>
|
<button class="submit-btn" id="submit-btn" type="submit">Log in</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if error %}
|
|
||||||
<div class="error-message">
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
const passwordToggle = document.getElementById('password-toggle');
|
||||||
|
const loginForm = document.getElementById('login-form');
|
||||||
|
const submitButton = document.getElementById('submit-btn');
|
||||||
|
|
||||||
// Apply the selected login-page theme class and persist it for the next visit.
|
|
||||||
function applyTheme(mode) {
|
function applyTheme(mode) {
|
||||||
document.body.classList.toggle('light-mode', mode === 'light');
|
const isLight = mode === 'light';
|
||||||
themeToggle.textContent = mode === 'light' ? 'Dark Mode' : 'Bright Mode';
|
document.body.classList.toggle('light-mode', isLight);
|
||||||
|
themeToggle.textContent = isLight ? 'Dark mode' : 'Light mode';
|
||||||
localStorage.setItem('mxpic-theme', mode);
|
localStorage.setItem('mxpic-theme', mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSubmitState() {
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.textContent = 'Signing in...';
|
||||||
|
}
|
||||||
|
|
||||||
applyTheme(localStorage.getItem('mxpic-theme') || 'dark');
|
applyTheme(localStorage.getItem('mxpic-theme') || 'dark');
|
||||||
|
|
||||||
themeToggle.addEventListener('click', () => {
|
themeToggle.addEventListener('click', () => {
|
||||||
applyTheme(document.body.classList.contains('light-mode') ? 'dark' : 'light');
|
applyTheme(document.body.classList.contains('light-mode') ? 'dark' : 'light');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
passwordToggle.addEventListener('click', () => {
|
||||||
|
const showPassword = passwordInput.type === 'password';
|
||||||
|
passwordInput.type = showPassword ? 'text' : 'password';
|
||||||
|
passwordToggle.textContent = showPassword ? 'Hide' : 'Show';
|
||||||
|
passwordToggle.setAttribute('aria-label', showPassword ? 'Hide password' : 'Show password');
|
||||||
|
});
|
||||||
|
|
||||||
|
loginForm.addEventListener('submit', setSubmitState);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -119,6 +119,40 @@ assert.deepStrictEqual(
|
|||||||
{ width: 118.8, height: 55.76 },
|
{ width: 118.8, height: 55.76 },
|
||||||
'default symbols should still scale proportionally inside normal component boxes'
|
'default symbols should still scale proportionally inside normal component boxes'
|
||||||
);
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
helpers.calculateCompositeBoxSize({
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
type: 'portNode',
|
||||||
|
position: { x: 10, y: 5 },
|
||||||
|
data: { componentDisplayName: 'input', elementType: 'port', portNumber: 1, pitch: 10, width: 0.5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'rotatableNode',
|
||||||
|
position: { x: 50, y: 20 },
|
||||||
|
data: { componentName: 'MMI_1', boxSize: { width: 80, height: 30 } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'rotatableNode',
|
||||||
|
position: { x: 160, y: 70 },
|
||||||
|
data: { componentName: 'MMI_2', boxSize: { width: 20, height: 10 } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
{ width: 170, height: 75 },
|
||||||
|
'composite canvas symbols should use the bounds of exported ports and internal instance boxes'
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
helpers.calculateCompositeBoxSize({
|
||||||
|
nodes: [{
|
||||||
|
type: 'portNode',
|
||||||
|
position: { x: 10, y: 5 },
|
||||||
|
data: { componentDisplayName: 'input', elementType: 'port', portNumber: 1, pitch: 10 }
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
helpers.DEFAULT_COMPONENT_BOX_SIZE,
|
||||||
|
'single-port empty canvases should keep the default component footprint'
|
||||||
|
);
|
||||||
|
|
||||||
const rotatedHandles = helpers.buildPortHandles({
|
const rotatedHandles = helpers.buildPortHandles({
|
||||||
left_port: { x: -50, y: 0, a: 180 },
|
left_port: { x: -50, y: 0, a: 180 },
|
||||||
|
|||||||
@@ -379,9 +379,19 @@ assert(
|
|||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
canvasHtml.includes('selectedPositionNodes.length > 1') &&
|
canvasHtml.includes('selectedPositionNodes.length > 1') &&
|
||||||
canvasHtml.includes('const delta = val - Number') &&
|
canvasHtml.includes("const MIXED_VALUE = '--'") &&
|
||||||
|
canvasHtml.includes('getSharedNumericDisplay') &&
|
||||||
|
canvasHtml.includes('clearMixedInput') &&
|
||||||
|
canvasHtml.includes('event.currentTarget.value === MIXED_VALUE') &&
|
||||||
|
canvasHtml.includes('editingTransformField') &&
|
||||||
|
canvasHtml.includes('if (editingTransformField) return;') &&
|
||||||
|
canvasHtml.includes('commitTransformInput') &&
|
||||||
|
canvasHtml.includes('event.currentTarget.value') &&
|
||||||
|
canvasHtml.includes('updatePosition(selectedNode.id, \'x\', val)') &&
|
||||||
|
canvasHtml.includes('selectedPositionNodes.forEach(node => {') &&
|
||||||
|
canvasHtml.includes('position: { [axis]: val }') &&
|
||||||
canvasHtml.includes('selectedNodes={selectedNodes}'),
|
canvasHtml.includes('selectedNodes={selectedNodes}'),
|
||||||
'multi-selected components should move together when editing X or Y in the inspector'
|
'multi-selected components should show mixed values as -- and set all selected X/Y values absolutely from the inspector'
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
canvasHtml.includes('portInfo.description') || canvasHtml.includes('port.description'),
|
canvasHtml.includes('portInfo.description') || canvasHtml.includes('port.description'),
|
||||||
|
|||||||
Reference in New Issue
Block a user