Project whiteboards (Step 11): freeform drawing boards on core's
Fresco/Etcher/annotations stack via the blank-background bridge —
core's annotation persistence anchors to a phoenix_kit_files row
(hard FK), so each board generates a solid-white PNG, registers it as
a Storage file in the project's folder, and lets core's
MediaCanvasViewer do everything else (drawing, annotation CRUD,
palettes). When core grows a file-less canvas the bridge shrinks to a
data migration; the board row (name/order/dimensions) is ours either
way.
The blank PNG is generated in pure Elixir (no ImageMagick for the
base file) and salted with a tEXt chunk carrying a fresh uuid:
Storage.store_file/2 dedups per-user by content checksum, so two
identical unsalted blanks would silently collide onto ONE file row —
and one shared annotation set.
Deleting a board deletes the ROW; the background file (with any drawings baked into its annotations) stays in the project folder — consistent with the hub's disable-hides-never-deletes philosophy.
Summary
Functions
A solid-white truecolor PNG, salted with a tEXt chunk so every call
produces DISTINCT bytes (Storage dedups per-user by content checksum —
unsalted, every same-size board for one user would collide onto a
single file row sharing one annotation set). Pure Elixir; no
ImageMagick for the base file.
Creates a whiteboard end-to-end: blank background PNG → Storage file
(owned by the creating user) → dimensions stamped → filed into the
project folder → board row. opts[:actor_uuid] is REQUIRED — the
files table needs an owning user for non-system files.
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. The 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.
Fetches a board scoped to its project (nil on cross-project uuids).
Boards for a project, in position/creation order.
Renames a board.
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 blank_png(pos_integer(), pos_integer(), String.t()) :: binary()
A solid-white truecolor PNG, salted with a tEXt chunk so every call
produces DISTINCT bytes (Storage dedups per-user by content checksum —
unsalted, every same-size board for one user would collide onto a
single file row sharing one annotation set). Pure Elixir; no
ImageMagick for the base file.
@spec create(map(), String.t(), keyword()) :: {:ok, PhoenixKitProjects.Schemas.Whiteboard.t()} | {:error, term()}
Creates a whiteboard end-to-end: blank background PNG → Storage file
(owned by the creating user) → dimensions stamped → filed into the
project folder → board row. opts[:actor_uuid] is REQUIRED — the
files table needs an owning user for non-system files.
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. The 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 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.
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.