189 lines
5.5 KiB
TypeScript
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,
|
|
};
|
|
}
|