Project whiteboards (Step 11): freeform drawing boards on core's Fresco/Etcher/annotations stack.
Since core V183 an annotation can anchor to any target_type +
target_uuid, and Fresco renders a scene with zero images, so a board
is just its row: create/3 inserts it and core's MediaCanvasViewer
draws it in board mode (target_type: "projects_whiteboard",
target_uuid: board.uuid) on an empty, infinite canvas — no file, no
Storage, no folder. The blank-background bridge this module used
to run (a salted solid-white PNG per board, registered as a Storage
file, drawn over as if it were a photo — Max, 2026-09-05: "the dev
added support to the package so that hack wouldn't be needed") is
gone from the create path. Boards made by it keep their file and keep
working: a board WITH a file_uuid renders through the file viewer as
before, a board without one through board mode. create_board_for_file/3
(a board over a real image) still exists for the former shape.
Deleting a board deletes the ROW; a board's shapes go with it (the annotations are keyed by its uuid), and a file-backed board's background file stays in the project folder — consistent with the hub's disable-hides-never-deletes philosophy.
Summary
Functions
Creates a whiteboard: one row, no file (chain V16 / core V183 — the
canvas is drawn from the board's dimensions and its shapes anchor to
target_type/0 + the board's uuid). opts[:actor_uuid] is REQUIRED —
it is the board's creator and the actor on the activity row.
The DB-side composition: board row for an EXISTING file + project-folder
filing + activity/broadcast. Split from create/3 so the row logic is
exercisable without configured storage buckets (tests, and any future
"board from an existing image" flow).
Deletes the board row. A file-less board's shapes go with it (nothing else holds them). A file-backed board's background file — and the drawings living in its annotation rows — stays in the project folder, still reachable from the Files page as an annotated image.
Drops the shapes of every FILE-LESS board of a project — for the project
delete, whose ON DELETE CASCADE removes the board rows but cannot reach
core's annotations table (a file-less board's shapes have no FK to
anything; the sweep, 2026-09-05). Returns the number of shapes removed.
Fetches a board scoped to its project (nil on cross-project uuids).
Boards for a project, in position/creation order.
Renames a board.
The target_type this module's boards anchor annotations to.
The :board assign core's MediaCanvasViewer takes for a file-less
board: the target pair and the canvas extent. nil for a board that
still has a file — render that one through the file path.
The curated file map MediaCanvasViewer expects (same shape core's
MediaViewer.curate_file/1 builds — that one is private). Nil when the
file is gone or Storage errors: the LV renders a fallback card instead
of a canvas.
Functions
@spec create(map(), String.t(), keyword()) :: {:ok, PhoenixKitProjects.Schemas.Whiteboard.t()} | {:error, term()}
Creates a whiteboard: one row, no file (chain V16 / core V183 — the
canvas is drawn from the board's dimensions and its shapes anchor to
target_type/0 + the board's uuid). opts[:actor_uuid] is REQUIRED —
it is the board's creator and the actor on the activity row.
Options: :width/:height (default 1920×1080, capped 8000).
@spec create_board_for_file(map(), binary(), map()) :: {:ok, PhoenixKitProjects.Schemas.Whiteboard.t()} | {:error, term()}
The DB-side composition: board row for an EXISTING file + project-folder
filing + activity/broadcast. Split from create/3 so the row logic is
exercisable without configured storage buckets (tests, and any future
"board from an existing image" flow).
@spec delete(PhoenixKitProjects.Schemas.Whiteboard.t(), keyword()) :: :ok | {:error, term()}
Deletes the board row. A file-less board's shapes go with it (nothing else holds them). A file-backed board's background file — and the drawings living in its annotation rows — stays in the project folder, still reachable from the Files page as an annotated image.
@spec delete_shapes_for_project(String.t()) :: non_neg_integer()
Drops the shapes of every FILE-LESS board of a project — for the project
delete, whose ON DELETE CASCADE removes the board rows but cannot reach
core's annotations table (a file-less board's shapes have no FK to
anything; the sweep, 2026-09-05). Returns the number of shapes removed.
@spec get(binary(), binary()) :: PhoenixKitProjects.Schemas.Whiteboard.t() | nil
Fetches a board scoped to its project (nil on cross-project uuids).
@spec list_for_project(binary()) :: [PhoenixKitProjects.Schemas.Whiteboard.t()]
Boards for a project, in position/creation order.
@spec rename(PhoenixKitProjects.Schemas.Whiteboard.t(), String.t(), keyword()) :: {:ok, PhoenixKitProjects.Schemas.Whiteboard.t()} | {:error, term()}
Renames a board.
@spec target_type() :: String.t()
The target_type this module's boards anchor annotations to.
@spec viewer_board(PhoenixKitProjects.Schemas.Whiteboard.t()) :: map() | nil
The :board assign core's MediaCanvasViewer takes for a file-less
board: the target pair and the canvas extent. nil for a board that
still has a file — render that one through the file path.
The curated file map MediaCanvasViewer expects (same shape core's
MediaViewer.curate_file/1 builds — that one is private). Nil when the
file is gone or Storage errors: the LV renders a fallback card instead
of a canvas.