+ {/* Tool buttons */}
+
+ {TOOLS.map((tool) => {
+ const label = t(tool.labelKey);
+ return (
+
+ );
+ })}
+
+
+
+
+ {/* Undo / Redo */}
+
+
+
+
+
+
+
+ {/* Zoom controls */}
+
+
+ {zoomPercent}%
+
+
+
+
+
+ {/* Grid + Snap toggles */}
+
+
+
+
+
+
+
+ {/* Layer visibility toggles */}
+
+
+
+
+
+
+
+ {/* Alignment tools — visible when 2+ items selected */}
+ {state.selectedIds.size >= 2 && (
+ <>
+
+
+ {ALIGNMENT_BUTTONS.map((btn) => (
+
+ ))}
+
+ >
+ )}
+
+ {/* Spacer */}
+
+
+ {/* Import + Export + Save buttons */}
+
+ {onImport && (
+
+ )}
+ {onExport && (
+
+ )}
+
+
+
+ );
+}
diff --git a/apps/client/src/components/editor/PropertiesPanel.tsx b/apps/client/src/components/editor/PropertiesPanel.tsx
new file mode 100644
index 0000000..53c2ff6
--- /dev/null
+++ b/apps/client/src/components/editor/PropertiesPanel.tsx
@@ -0,0 +1,664 @@
+import { useMemo, useState, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import type { Wall, WallOpening, ElectricalItem, FurnitureItem, DoorOpenDirection, FloorType } from '@house-plan-maker/shared';
+import { DOOR_OPEN_DIRECTIONS, FLOOR_TYPES } from '@house-plan-maker/shared';
+import { useEditor } from './context/EditorContext';
+import { useUndoRedo } from './context/UndoRedoContext';
+import { wallLength } from './utils/wallUtils';
+import { polygonArea, polygonPerimeter, generateLocalId } from './utils/geometry';
+import { getElectricalVariant, ELECTRICAL_SYMBOL_DEFS } from './symbols/electrical';
+import type { EditorCommand } from './types';
+import styles from './properties-panel.module.css';
+
+export function PropertiesPanel() {
+ const { t } = useTranslation();
+ const { state, dispatch, updateOpening, updateElectrical, updateFurniture, updateWall, addAnnotation } = useEditor();
+ const { execute } = useUndoRedo();
+ const { selectedIds, walls, openings, electricalItems, furnitureItems, room } = state;
+
+ const roomArea = useMemo(
+ () => room.shape.length >= 3 ? polygonArea(room.shape) : 0,
+ [room.shape],
+ );
+ const roomPerimeter = useMemo(
+ () => room.shape.length >= 2 ? polygonPerimeter(room.shape) : 0,
+ [room.shape],
+ );
+
+ // Find selected elements
+ const selected = useMemo(() => {
+ const items: {
+ type: 'wall' | 'opening' | 'electrical' | 'furniture';
+ data: Wall | WallOpening | ElectricalItem | FurnitureItem;
+ }[] = [];
+
+ for (const id of selectedIds) {
+ const wall = walls.find((w) => w.id === id);
+ if (wall) {
+ items.push({ type: 'wall', data: wall });
+ continue;
+ }
+ const opening = openings.find((o) => o.id === id);
+ if (opening) {
+ items.push({ type: 'opening', data: opening });
+ continue;
+ }
+ const elec = electricalItems.find((e) => e.id === id);
+ if (elec) {
+ items.push({ type: 'electrical', data: elec });
+ continue;
+ }
+ const furn = furnitureItems.find((f) => f.id === id);
+ if (furn) {
+ items.push({ type: 'furniture', data: furn });
+ }
+ }
+
+ return items;
+ }, [selectedIds, walls, openings, electricalItems, furnitureItems]);
+
+ if (selected.length === 0) {
+ return (
+
+
+ {opening.type === 'DOOR' ? t('properties.door') : t('properties.window')}
+
+
+
+
+ {opening.type === 'DOOR' && (
+
({
+ value: dir,
+ label: t(`properties.openDir.${dir}`),
+ }))}
+ onChange={handleOpenDirectionChange}
+ />
+ )}
+ {opening.type === 'WINDOW' && (
+
+ )}
+ {wall && (
+
+ )}
+
+ );
+}
+
+// ── Property Row Components ──
+
+interface PropertyRowProps {
+ readonly label: string;
+ readonly value: string;
+}
+
+function PropertyRow({ label, value }: PropertyRowProps) {
+ return (
+