conductor/frontend/src/store/useProjectStore.ts
2026-07-18 10:23:13 -04:00

189 lines
5.5 KiB
TypeScript

import { useCallback } from 'react';
import type {
ProjectDocument,
CanvasComponent,
ComponentType,
Page,
} from '../types/project';
// ── Default sizes per component type ─────────────────────────────────────────
export const DEFAULT_SIZES: Record<ComponentType, { width: number; height: number }> = {
Label: { width: 200, height: 32 },
Button: { width: 120, height: 40 },
TextInput: { width: 240, height: 40 },
JsonViewer: { width: 400, height: 200 },
Dropdown: { width: 260, height: 72 },
Table: { width: 520, height: 260 },
};
// ── Default label text per component type ─────────────────────────────────────
const DEFAULT_LABELS: Record<ComponentType, string> = {
Label: 'Label',
Button: 'Button',
TextInput: '',
JsonViewer: '',
Dropdown: 'Dropdown',
Table: 'Table',
};
// ── ID counter (session-stable, not persistent) ───────────────────────────────
let _counter = 1;
export function nextComponentId(type: ComponentType): string {
return `${type.toLowerCase()}_${_counter++}`;
}
// ── Grid snap helper ──────────────────────────────────────────────────────────
const GRID = 8;
export function snap(v: number): number {
return Math.round(v / GRID) * GRID;
}
// ── Hook ──────────────────────────────────────────────────────────────────────
// This hook owns canvas mutation logic only.
// The doc state is owned by ProjectContext and passed in via doc/setDoc.
export type CanvasActions = {
activePage: Page;
addComponent: (type: ComponentType, x: number, y: number) => void;
moveComponent: (id: string, dx: number, dy: number) => void;
removeComponent: (id: string) => void;
updateComponentProperty: (id: string, key: string, value: unknown) => void;
updateComponentName: (id: string, name: string) => void;
updateComponentSize: (id: string, width: number, height: number) => void;
};
export function useCanvasActions(
doc: ProjectDocument,
setDoc: React.Dispatch<React.SetStateAction<ProjectDocument>>,
): CanvasActions {
// Always operate on the first page for this MVP step
const activePage = doc.project.pages[0];
const updatePage = useCallback(
(updater: (p: Page) => Page) => {
setDoc((prev) => ({
...prev,
project: {
...prev.project,
pages: prev.project.pages.map((p, i) => (i === 0 ? updater(p) : p)),
},
}));
},
[setDoc],
);
const addComponent = useCallback(
(type: ComponentType, x: number, y: number) => {
const id = nextComponentId(type);
const size = DEFAULT_SIZES[type];
const label = DEFAULT_LABELS[type];
const properties: CanvasComponent['properties'] = { label, visible: true, disabled: false };
if (type === 'TextInput') properties.placeholder = 'Enter text…';
if (type === 'Dropdown') {
properties.placeholder = 'Select an option';
properties.options = [];
properties.value = '';
}
if (type === 'Table') {
properties.columns = [];
properties.rows = [];
}
const component: CanvasComponent = {
id,
type,
name: id,
position: { x: snap(x), y: snap(y) },
size,
properties,
};
updatePage((p) => ({ ...p, components: [...p.components, component] }));
},
[updatePage],
);
const moveComponent = useCallback(
(id: string, dx: number, dy: number) => {
updatePage((p) => ({
...p,
components: p.components.map((c) =>
c.id === id
? {
...c,
position: {
x: snap(Math.max(0, c.position.x + dx)),
y: snap(Math.max(0, c.position.y + dy)),
},
}
: c,
),
}));
},
[updatePage],
);
const removeComponent = useCallback(
(id: string) => {
updatePage((p) => ({
...p,
components: p.components.filter((c) => c.id !== id),
}));
},
[updatePage],
);
const updateComponentProperty = useCallback(
(id: string, key: string, value: unknown) => {
updatePage((p) => ({
...p,
components: p.components.map((c) =>
c.id === id
? { ...c, properties: { ...c.properties, [key]: value } }
: c,
),
}));
},
[updatePage],
);
const updateComponentName = useCallback(
(id: string, name: string) => {
updatePage((p) => ({
...p,
components: p.components.map((c) =>
c.id === id ? { ...c, name } : c,
),
}));
},
[updatePage],
);
const updateComponentSize = useCallback(
(id: string, width: number, height: number) => {
updatePage((p) => ({
...p,
components: p.components.map((c) =>
c.id === id ? { ...c, size: { width, height } } : c,
),
}));
},
[updatePage],
);
return {
activePage,
addComponent,
moveComponent,
removeComponent,
updateComponentProperty,
updateComponentName,
updateComponentSize,
};
}