Updated with fresco, tessera and etcher#537
Merged
Merged
Conversation
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.