Skip to content

Updated with fresco, tessera and etcher#537

Merged
ddon merged 12 commits into
BeamLabEU:devfrom
alexdont:dev
May 12, 2026
Merged

Updated with fresco, tessera and etcher#537
ddon merged 12 commits into
BeamLabEU:devfrom
alexdont:dev

Conversation

@alexdont
Copy link
Copy Markdown
Contributor

No description provided.

Alexander Don and others added 12 commits May 12, 2026 13:50
Tessera 0.2 split itself into a Fresco-based architecture: Fresco
(0.1.0, freshly published) owns the image viewer + nav overlay +
viewport clamping + smooth animations; Tessera (0.2.0) is now a layer
that attaches to a named Fresco viewer and contributes the DZI
source provider plus the multi-layer progressive-zoom logic.

  - mix.exs: replace the single tessera path/hex line with
    `{:fresco, "~> 0.1"}` + `{:tessera, "~> 0.2"}`
  - mix.lock: pinned to the two newly-published hex versions
  - media_browser.html.heex: image branch swaps the old
    `<Tessera.Viewer.viewer src=... sources=...>` for a
    `<Fresco.Viewer.viewer src=...>` + sibling `<Tessera.Layer.layer
    fresco_id=... sources=...>`. The user-visible behavior matches the
    previous state — medium loads first, multi-layer swaps in as the
    user zooms in or out — with Fresco now sitting underneath
    invisibly.
  - media_browser.ex: add `initial_tessera_src/1` helper that picks
    the first URL from the `tessera_sources/1` list (medium normally,
    falling back to original/medium for the rare image without a
    medium variant)

Server-side `Tessera.generate*` API is unchanged; no changes to the
tile-generation pipeline, the FileController routes, or the storage
adapter. The split is purely a client-side architecture refactor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires Etcher 0.1 into the MediaBrowser modal: users draw rectangle /
circle / polygon / freehand shapes on top of any image, the LiveView
saves them, and they re-render anchored to the image through pan/zoom
and Tessera source swaps.

* V114 migration creates `phoenix_kit_annotations`:
  - uuidv7 primary key, `file_uuid` FK → `phoenix_kit_files`
    `ON DELETE :delete_all`, `creator_uuid` FK → `phoenix_kit_users`
    `ON DELETE :nilify_all` so user removal preserves annotations
    as anonymous
  - JSONB `geometry` keyed by kind, JSONB `style` + `metadata`,
    `position` for ordering
  - DB-level CHECK on `kind` matching Etcher's v0.1 tool set
  - Indexes on `file_uuid` + partial on `creator_uuid` for the two
    common access patterns (per-file listing + author lookups)
  - Discussion threads attach lazily via the existing comments
    convention (`resource_type = "annotation"`,
    `resource_uuid = annotation.uuid`) — no `comment_uuid` column;
    the linkage is one-directional from the comment side.

* `PhoenixKit.Annotations` context + `Annotation` schema.

* `PhoenixKit.Modules.Storage.EtcherAdapter` implements
  `Etcher.Storage`. Maps Etcher's generic `target_type` / `target_uuid`
  API to `file_uuid` (only `"file"` targets are supported) and
  dispatches to the context. Normalises string-keyed LiveView event
  payloads to atom keys.

* `<Etcher.Layer.layer>` mounts beside `<Fresco.Viewer.viewer>` +
  `<Tessera.Layer.layer>` in the modal viewer. The MediaBrowser
  LiveComponent loads initial annotations on viewer-open / step,
  handles `etcher:created` / `:updated` / `:deleted` / `:selected`,
  and pushes `etcher:annotation-saved` back to the client so saved
  rows adopt their persisted uuid.

* mix.exs: adds `{:etcher, "~> 0.1"}`, bumps fresco to `~> 0.2`,
  @Version → 1.7.109 (CHANGELOG entry left for the maintainer per
  CLAUDE.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* `load_annotations_for/1` now includes `metadata` in the payload sent
  to Etcher's `<Etcher.Layer.layer>`. The JS overlay reads
  `metadata.label` for the hover tooltip — consumers stashing
  display-friendly labels there get them for free.

* `handle_event("etcher:updated", ...)` mirrors the persisted geometry
  back into the socket's in-memory `viewer_annotations` list. Without
  this, navigating with prev/next after editing a shape would re-render
  the pre-edit geometry until the file modal was closed and reopened.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end "solidify on Post" flow for newly-drawn annotations in the
MediaBrowser modal.

**Composer LiveComponent** (`AnnotationComposer`):
- Thin ~400-line component reusing `PhoenixKitComments.create_comment/4`,
  `search_giphy/2`, and `Storage.store_file/2` — no extraction of the
  monolithic CommentsComponent needed.
- Renders textarea (auto-focused via phx-mounted) + Giphy picker + file
  attachment menu + Post / Cancel.
- Posts the comment under the *file's* resource (`resource_type: "file"`,
  `resource_uuid: file_uuid`) with `metadata.annotation_uuid` pointing
  at the annotation. This way the comment shows up in the file's main
  comments thread alongside non-annotated discussion; the annotation
  linkage is recoverable via the metadata key.
- Notifies the parent MediaBrowser via `Phoenix.LiveView.send_update/2`
  on Post (success → solidify) or Cancel (rollback).

**MediaBrowser changes** (`media_browser.ex` + `.html.heex`):
- `:composing_annotation_uuid` + `:composer_anchor` socket assigns set
  after a successful `etcher:created`; the JS supplies anchor coords
  (shape's bottom-left in container px) in the event payload.
- Floating composer popover absolute-positioned inside the viewer
  container at the anchor point — the sidebar's file comments thread
  stays visible alongside.
- Rollback path on Cancel / `close_viewer` / `step_viewer` / file change
  deletes the pending annotation via EtcherAdapter and pushes
  `etcher:annotation-removed` so the SVG element is stripped client-
  side.
- Two new `update/2` `:action` clauses (`:annotation_composer_posted`,
  `:annotation_composer_cancelled`) handle the composer notifications.
- `load_annotations_for/1` rewired to use the new preview loader and
  flow tooltip-friendly fields (`comment_text`, `comment_author`,
  `comment_thumbnail_url`, `comment_has_attachment`, `comment_count`)
  through each annotation's `metadata`.

**`PhoenixKit.Annotations.list_for_file_with_previews/1`**:
- Single-query loader: pulls every comment on the file in one shot,
  groups by `metadata.annotation_uuid`, and builds a preview map per
  annotation (first top-level comment + count).
- Soft-deps on PhoenixKitComments (`Code.ensure_loaded?/1` guard);
  degrades to `first_comment: nil, comment_count: 0` if the package
  isn't installed.
- Author display falls back through first+last name → first name →
  email prefix.
- Thumbnail extraction prefers image variants, exposes a
  `has_attachment` flag so the JS can render a paperclip for non-image
  attachments or broken-image fallbacks.

**EtcherAdapter** (`etcher_adapter.ex`):
- Whitelists the annotation schema fields (`kind`, `geometry`, `style`,
  `metadata`, `position`, `creator_uuid`) before atom conversion.
  Fixes the `String.to_existing_atom("anchor_x")` crash that surfaced
  when the JS payload grew client-only meta keys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three loose-ends so the post-Post and post-delete states stay
consistent without a page reload:

* **Comments cascade** — `Annotations.delete/1` now soft-deletes any
  comments tied to the annotation (file comments where
  `metadata.annotation_uuid` matches). Soft delete keeps reply chains
  attached as `[removed]` placeholders rather than orphaning them.
  Safely no-ops when PhoenixKitComments isn't installed; comment-side
  errors are swallowed so an annotation can always delete.

* **Sidebar comments refresh** — `etcher:deleted` handler and
  `finalize_annotation_compose/2` both call a shared
  `refresh_file_comments/1` helper that `send_update`s the file's
  `CommentsComponent` with `loaded?: false`. The component re-runs
  `load_comments/1` on the next render, so newly-posted or
  cascade-deleted comments appear/vanish in the sidebar immediately.

* **Tooltip live-sync + auto-exit drawing** — `finalize_annotation_compose/2`
  pushes two new hook events: `etcher:annotation-updated` (merges
  fresh tooltip metadata for the just-saved annotation, so its preview
  text/thumbnail/author appears without hovering away and back) and
  `etcher:exit-drawing` (switches the overlay to cursor mode so the
  user doesn't accidentally draw another shape after solidifying one).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* `list_for_file_with_previews/1` builds a parent_uuid → children map
  and recursively walks from each annotation-rooted comment, so the
  count surfaced in the tooltip reflects total thread activity
  (root + every reply descendant) instead of just direct
  metadata-tagged comments.
* `load_annotations_for/1` now ships `comment_created_at` (the
  annotation's `inserted_at`, formatted as "May 12, 2026" via a small
  `format_date/1` helper) so the tooltip subheader has something to
  render for both posted-comment annotations and the rare
  no-comment-yet edge case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
So persisted per-shape colors (set via Etcher's new picker) reload
with their original color on the next modal visit. `style` lives
alongside `geometry` in the initial_annotations payload Etcher's hook
parses on mount.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reproduces the previous Etcher tooltip out-of-the-box look as a
consumer of Etcher's new `window.Etcher.tooltipSlots` API rather than
relying on hardcoded `comment_*` metadata reads in etcher.js.

Three slot impls in phoenix_kit.js (registered unconditionally so the
load order with etcher.js doesn't matter — Etcher's bootstrap uses
`||` to preserve pre-existing slots):

- header → `metadata.comment_author` (else capitalized shape.kind)
- footer → `comment_created_at · N comments` (singular/plural)
- body   → optional thumbnail (image or paperclip fallback) +
  truncated quoted text, using Etcher's opt-in styling primitives
  (`.etcher-tooltip-body`, `.etcher-tooltip-thumb`, `.etcher-tooltip-text`,
  `.etcher-tooltip-quote`)

The `metadata.comment_*` key naming becomes PhoenixKit's internal
contract — Etcher itself knows nothing about comments anymore.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The server seeds the floating composer popover at the shape's
bottom-left in container px (anchor coords supplied by etcher.js in
the `etcher:created` event). That's the right anchor when the user
draws in the middle of the image, but a shape drawn near the right
edge or bottom of the viewer would place the popover off-screen.

New `AnnotationComposerPosition` hook in phoenix_kit.js measures the
popover and its container on mount, on update, and on window resize,
then clamps `left` / `top` to keep the popover fully inside the
container with an 8px margin. The wrapper in media_browser.html.heex
gains `phx-hook="AnnotationComposerPosition"` and a stable id keyed
by the composing annotation's uuid.

Falls back gracefully when the popover is taller than the container
(small viewports): pins to the top instead of overflowing the bottom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per maintainer judgment the new appendNavButton + animation events +
phx-update guard ship as fresco 0.1.1 (additive patch) rather than
0.2.0. Updating phoenix_kit's constraint to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…V114

Upstream's V114 (integration uuid-only keys) landed concurrently with
this branch's annotations migration. Renumbering this branch's
contribution to V115 so the two coexist post-merge:

- File rename: `v114.ex` → `v115.ex`
- Module rename: `V114` → `V115`
- Bumps `@current_version` 114 → 115 in `postgres.ex`
- Updates the release-notes section heading + the table-comment
  pattern (up sets '115', down restores '114')

No code change inside the migration body; the table shape stays
identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	lib/phoenix_kit/migrations/postgres.ex
#	lib/phoenix_kit_web/components/media_browser.html.heex
#	mix.exs
@ddon ddon merged commit 2ede7f3 into BeamLabEU:dev May 12, 2026
ddon pushed a commit that referenced this pull request May 12, 2026
23 findings: 2 BUG-MEDIUM (atomic delete, resource_type docstring drift),
3 BUG-LOW (race, partial upload, broad rescue), 5 IMPROVEMENT-MEDIUM
(authz, schema source-of-truth, gettext, geometry validation, hardcoded
component id), 6 IMPROVEMENT-LOW, 7 NITPICK.

Strengths called out: clean adapter↔context separation, payload-key
whitelisting, server-side creator_uuid override, LC↔LC lifecycle via
send_update, FK cascade semantics, DB-level kind CHECK, single-bulk
loader for tooltip previews.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ddon pushed a commit that referenced this pull request May 12, 2026
Code (PR #537 review items addressed):

- #1 BUG-MEDIUM: wrap `Annotations.delete/1` in `Repo.transaction/1` so
  comment-cascade + annotation-delete are atomic. Extracted into
  `delete_in_transaction/1` to keep the body flat (credo depth).
- #2 BUG-MEDIUM: sweep stale `resource_type = "annotation"` claims
  across 3 moduledocs (annotation.ex, v115.ex, etcher_adapter.ex). The
  implementation anchors comments to the file with
  `metadata.annotation_uuid` — docs now match reality.
- #5 BUG-LOW: narrow `delete_linked_comments` rescue to expected
  exception classes ([DBConnection.OwnershipError, Postgrex.Error,
  ArgumentError]) so logic bugs surface instead of being swallowed.
- #11 IMPROVEMENT-LOW: drop `normalize/1` — `Ecto.Changeset.cast/3`
  handles both atom- and string-keyed maps natively; the helper added
  silent failure-mode risk on typo'd keys.
- #12 IMPROVEMENT-LOW: drop in-repo `Code.ensure_loaded?(PhoenixKit.Annotations)`
  guard in MediaBrowser — Annotations is a core module, can't be
  missing.
- #19 NITPICK: drop `PhoenixKit.Modules.Storage` from
  AnnotationComposer's `@compile no_warn_undefined` (it's core, not
  optional — rename should fail loudly).
- #20 NITPICK: simplify `AnnotationComposerPosition.destroyed` guard.
- #21 NITPICK: fix misleading "Etcher's bootstrap uses `||` to preserve
  pre-existing slots" comment — PhoenixKit's JS owns the slots.
- #8 IMPROVEMENT-MEDIUM: gettext-wrap ~17 user-facing strings in
  AnnotationComposer (flash messages + heex literals + ARIA labels).

Credo / dialyzer:
- Alias `PhoenixKit.Annotations`, `PhoenixKit.Modules.Storage`,
  `PhoenixKit.Modules.Storage.EtcherAdapter`, `Storage.File` so the
  six "nested modules could be aliased" findings clear.
- Convert `first_attachment_thumbnail/1`'s single-clause `with` to
  `case` (credo readability).
- Add PhoenixKitComments-targeted entries to .dialyzer_ignore.exs for
  the annotations context + composer (optional sibling package,
  guarded at runtime).
- mix.lock picks up `etcher 0.1.0` via deps.get.

mix precommit: compile → format → credo --strict → dialyzer all clean.
Deferred to original author (Alex): #3 (race), #4 (upload rollback),
#6 (authz), #7 (schema-as-source), #9 (geometry validation), #10
(configurable component id), #13/#14 (locale-aware date + traverse_errors),
plus cosmetics #15-18, #22, #23. Disposition table in CLAUDE_REVIEW.md
updated separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ddon pushed a commit that referenced this pull request May 12, 2026
Disposition table in CLAUDE_REVIEW.md flags which review findings were
fixed in commit `b45a7a93` and which were left for the original PR
author (design judgment, larger scope, or product calls).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ddon pushed a commit that referenced this pull request May 12, 2026
Code (4 more PR #537 review items):

- #7 IMPROVEMENT-MEDIUM: `Annotation.adapter_writable_fields/0` exposes
  the schema's `@cast_fields` (minus `file_uuid`, which the adapter
  sets server-side). EtcherAdapter derives `@schema_keys` from it so a
  future schema field can't drift from the adapter whitelist silently.
- #13 IMPROVEMENT-LOW: gettext-wrap the strftime format string in
  `format_date` (`gettext("%b %d, %Y")`) so locales can reorder date
  components (e.g. "%d %b %Y" for en-GB / fr / de).
- #14 IMPROVEMENT-LOW: route `AnnotationComposer.first_error/1` through
  `PhoenixKitWeb.Components.Core.Input.translate_error/1` — gettext-aware
  helper used elsewhere in the codebase that interpolates `%{count}` and
  other opts properly.
- #23 NITPICK: docstring on `truncate/2` clarifying that `limit` is the
  output length (incl. ellipsis), not the source length.

Docs:
- CLAUDE_REVIEW.md disposition table updated: 13 items addressed across
  `b45a7a93` + this commit, 9 deferred to Alex (down from 10 — #7 took).

mix precommit: compile → format → credo --strict (0 findings) →
dialyzer (160 errors all skipped) clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ddon pushed a commit that referenced this pull request May 12, 2026
Replaces `<next-sha>` placeholders with `3e559fc3`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ddon pushed a commit that referenced this pull request May 12, 2026
CHANGELOG: covers PRs #536 (Integrations V114 + IntegrationPicker +
form fixes), #537 (V115 annotations + Etcher overlay + AnnotationComposer),
#538 (V116 entity_data parent_uuid + sortable_handle), plus all
post-merge review fixes folded into dev between merges. Organized
Added / Changed / Fixed / i18n to match the 1.7.108 entry's style.

AGENTS.md: the "entries written by the maintainer, not agents — flag
the gap and stop" line was filling context with a false rule. Replaced
with reality: agents draft the entry against the bumped @Version
heading.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants