diff --git a/Cargo.toml b/Cargo.toml index 297e9c11..7513e66f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "0.8.48" +version = "0.8.49" edition = "2024" publish = false diff --git a/apps/gui/frontend/src/features/design/Canvas.tsx b/apps/gui/frontend/src/features/design/Canvas.tsx new file mode 100644 index 00000000..6b252753 --- /dev/null +++ b/apps/gui/frontend/src/features/design/Canvas.tsx @@ -0,0 +1,82 @@ +import type { JSX } from "@solidjs/web"; +import { For, Show } from "solid-js"; +import { tx } from "~/stores/i18n"; +import { ROOT_ID } from "./document"; +import { beginPointerDrag, hitAt } from "./gesture"; +import { DesignedNode } from "./render"; +import { design } from "./store"; + +/** + * The artboard. + * + * Renders in the app's own document, which is the H6 decision and the reason + * everything here is simple: hit testing is `closest("[data-design-id]")`, + * the control socket can see every node, and ps-qa addresses them by id. + * + * Pointer events are taken in the capture phase and stopped there. A designed + * Button is a real Button, so without that a click would press it instead of + * selecting it. Capture is the exact tool for "the canvas sees this first and + * the component never does", and it needs no `pointer-events: none` layer, + * which would have broken the hit test it was meant to serve. + */ +export function Canvas(props: { + canvas: () => HTMLElement | undefined; + setCanvas: (element: HTMLElement) => void; +}): JSX.Element { + const doc = () => design.document(); + const empty = () => doc().root.children.length === 0; + + const onPointerDown = (event: PointerEvent): void => { + event.stopPropagation(); + event.preventDefault(); + const hit = hitAt(event.clientX, event.clientY); + design.select(hit?.id ?? ROOT_ID); + if (!hit) return; + design.beginDrag({ kind: "node", nodeId: hit.id }); + beginPointerDrag({ x: event.clientX, y: event.clientY }, props.canvas, () => { + /* An unmoved press on a node is a selection, already applied above. */ + }); + }; + + return ( +
+ {tx("Compose @pathscale/ui components and read the source they emit")} +
+{props.children}
; +} + +/** Where the selection sits, and a way back up to any ancestor. */ +function Breadcrumb(): JSX.Element { + const trail = () => pathTo(design.document(), design.selectedId()); + return ( +{tx("Drag onto the canvas, or click")}
+`, not on the `` + * itself, which has no role to carry it. The name is how ps-qa + * addresses the emitted text. + */ ++ + )} ++ {file().source} ++