# Changelog

All notable changes to PhoenixKitComments will be documented in this file.

## 0.4.2 - 2026-08-17

Empty-content anchor comments for server-created threads (#37).

### Added

- **`create_comment/4` accepts `:allow_empty_content`** — a server-created
  anchor/topic comment whose visible text lives elsewhere (e.g. an
  annotation's own label rendered as the thread's decoration) can now skip
  the content-or-media requirement instead of duplicating that label into
  the comment body just to pass validation. Read directly off `attrs`
  before the changeset, the same pattern as `:inserted_at` — server-side
  callers only. Documented in `create_comment/4`'s doc and added integration
  coverage — the PR that introduced this shipped with neither.

## 0.4.1 - 2026-08-17

Comment backdating for server-created anchor comments, and a comment-card UI
rework (#36).

### Added

- **`create_comment/4` accepts `:inserted_at`** — a server-created anchor/topic
  comment (e.g. the thread a shape's discussion hangs under) can now carry the
  timestamp of the thing it anchors instead of the moment the thread was
  lazily instantiated by the first reply. Deliberately not part of the
  changeset `cast`, so no client form payload can drive it: only a real
  `DateTime` struct triggers the override, anything else (including a
  client-shaped JSON string) is silently ignored, and `updated_at` still
  records when the row was actually written. Added integration coverage —
  the PR that introduced this shipped with none.

### Changed

- **Comment card actions reworked.** A card at rest now shows author / body /
  date / reactions only. Edit and Delete move into a "…" dropdown at the top
  right, revealed on hover (`focus-within` keeps it visible for keyboard
  users). Reply moves to the bottom-right action row, hover-revealed ahead of
  the always-visible like/dislike buttons. The dropdown blurs itself on
  Edit/Delete click so a LiveView patch (which never moves focus) can't leave
  the menu hanging open over an edited or deleted card.

## 0.4.0 - 2026-08-11

Metadata-keyed reads and writes, a declared resource-handler contract, and the
translated sidebar labels.

### Added

- **Metadata-keyed reads** (#34). `list_comments/3` takes a `:metadata` map of
  keys that must match, and `:any` in place of a resource uuid to list across a
  whole resource type. `resource_uuid` is a UUID column and plenty of hosts key
  their comments on something that isn't one — a `(source, slug, chapter)`
  triple — so they mint a throwaway uuid and put the real key in `metadata`. The
  listing they actually want was not expressible, and they dropped to schemaless
  SQL against this package's own table to get it.

- **`update_metadata/2` and `merge_metadata/3`** (#34). Single atomic
  `metadata || patch` writes, so they cannot lose a concurrent writer's keys the
  way read-modify-write does. `merge_metadata/3` is the rename case — a host
  renames a slug and every comment carrying the old one has to follow. An empty
  match is refused rather than treated as "everything".

- **`count_replies/2`** (#34) — `parent_uuid => count` for a batch of parents in
  one grouped query, with a `0` entry for every uuid asked about, so a thread
  list renders uniformly without an N+1.

- **`PhoenixKitComments.ResourceHandler.callbacks/0` and `event_callbacks/0`**
  (#35), and the rationale for adopting the behaviour at all: dispatch is by
  `function_exported?/3`, so a misnamed or wrong-arity callback is
  indistinguishable from one you chose not to write — nothing fires and there is
  no error to find it by.

### Changed

- **Sidebar tab labels are translated** (#33). The admin and settings tabs
  declare `gettext_backend` and `gettext_domain`, which they need in order to
  resolve at all.

### Fixed

- **Estonian and Russian entries that carried another string's translation**
  (#33), and entries whose `fuzzy` flag meant Gettext ignored them at runtime
  and showed the English msgid instead. Two plural forms had also dropped their
  `%{action}` interpolation, so a bulk-action result read "5 comments" instead
  of "5 comments deleted".

- **`count_replies/2` reported zeros silently on a failed query.** "0 replies"
  on a thread that has replies is a plausible-looking wrong answer rather than an
  obviously broken one, and nothing anywhere distinguished it from a quiet
  thread. It now logs before degrading.

## 0.3.0 - 2026-08-10

### Changed

- **⚠️ Requires `phoenix_kit ~> 2.0`.** The core pin moved to `~> 2.0`, so this
  release no longer resolves against core 1.7.

  Core 2.0.0 squashes the migration chain into a single `V135` baseline and makes
  V135 the chain's floor: `mix ecto.migrate` now *refuses* on a database below it
  rather than migrating. Check `mix phoenix_kit.status` **before** upgrading. A
  host below V135 must install `phoenix_kit 1.7.236` — the migration bridge, the
  last release carrying the full pre-squash chain — migrate until the reported
  version is at least V135, and only then move to 2.0.

  This package does not call migration internals, so the change is the pin
  itself.

### Security

- **Stored XSS in every comment body (PR #31).** Bodies render with MDEx
  `unsafe: true`, which disables MDEx's escaping and makes whatever runs next
  the security boundary — that was six regexes over the rendered string, a
  blocklist over HTML. Two payloads went through untouched: `<script>alert(1)`
  with no closing tag (the pattern required a matching `</script>`) and
  `<a href=javascript:alert(1)>` (the pattern required the value to be quoted).
  Either executed for every reader of a thread **and again in the admin
  moderation list**, which renders the same component — so an unprivileged
  commenter ran script in an owner's authenticated session.

  Rendering now uses MDEx's `:sanitize` allow-list (ammonia), which drops
  unknown tags and every attribute outside the allowed set instead of matching
  against a pattern. MDEx's default list permits `style` on `div`, enough to
  lay an overlay over the moderation UI's own buttons, so that is stripped too.
  Ten payloads are pinned by tests.
- **`save_decoration` had no authorization check at all** — a logged-out
  visitor could rename any host record backed by a visible comment, and the
  `send_update` the host received was byte-identical to a legitimate one.
- **Admin reads were unguarded while every write was checked.** An admin
  *without* the `comments` permission could read every comment on the platform,
  commenter emails, and the Giphy API key as a form value.
- **`@enabled` gated only the template**, so writes landed on a disabled thread.
- **`save_edit` forwarded the decoration before checking permission**, so a
  refused request still committed half an edit.

### Fixed

- **Reaction counters were inflatable.** Dedup was a SELECT-then-INSERT with no
  unique index behind it — the index was dropped during the uuid-FK migration
  and never recreated, which also left the schemas' `unique_constraint` as dead
  code. Reactions now serialise on the parent comment row.
- **The counter drifted permanently once duplicates existed** — `delete_all`
  removes N rows while the decrement was hardcoded to 1.
- **A repeat click committed a write and reported nothing** — both paths remove
  the opposing reaction before deciding, and `after_reaction/3` skipped exactly
  those two atoms.

### Changed

- **⚠️ Breaking (small): `render_markdown/2` is now `render_markdown/1`,** and
  the `sanitize` attr is gone from `<.comment_markdown>`. There is no caller for
  whom skipping sanitisation of a user-authored body would be correct, and an
  opt-out is one `sanitize={false}` away from stored XSS on every reader. No
  sibling module in the umbrella calls this function.

## 0.2.15 — 2026-07-27

### Added

- **Comment editors open in the admin-configured default mode (PR #30).** Both
  the composer and the inline edit form pass the site-wide editor mode (set
  under Settings → Content Editor in core) to Leaf, instead of always opening in
  Leaf's `:hybrid` default.
- **Named-schema (`--prefix`) support at runtime (PR #29).** Every table-backed
  schema (`Comment`, `CommentLike`, `CommentDislike`, `CommentMedia`) compiles in
  `PhoenixKit.SchemaPrefix`, so on a prefixed install this module's queries
  target the named schema directly rather than relying on the DB role's
  `search_path`. No behaviour change for unprefixed installs. A conformance test
  scans `lib/` so future table-backed schemas can't silently skip the attribute.

### Changed

- **Moderation dashboard bulk-select moved client-side (PR #29).** Per-checkbox
  `phx-click` round-trips and the `selected_uuids` assign are replaced by core's
  `BulkSelect` components and the `BulkSelectScope` JS hook — selection lives in
  the browser and the server only learns the UUIDs when a toolbar button is
  clicked. Adds a header "select all" checkbox; `bulk_action` splits into
  `bulk_approve` / `bulk_hide` / `bulk_delete_comments`, all keeping the same
  authorization gate and the delete confirmation.

### Fixed

- **Comments component no longer crashes on the editor-mode lookup (post-merge
  review of PR #30).** The merged code called
  `PhoenixKit.Settings.get_editor_mode/0`, which does not exist in any
  phoenix_kit release the `~> 1.7.189` pin can resolve (latest is 1.7.213) —
  raising `UndefinedFunctionError` in `update/2` on **every** render of every
  comments embed. The lookup is now capability-probed and falls back to Leaf's
  `:hybrid` default on older cores, and starts honouring the setting
  automatically once core ships it.
- **Editor mode is normalized before it reaches Leaf.** Leaf's `:mode` clauses
  have no catch-all, so a string setting value (the shape settings round-trip
  as) or an unrecognised mode would raise `FunctionClauseError` inside Leaf.
  Values are now coerced to one of `:visual` / `:hybrid` / `:markdown` / `:html`,
  with test coverage pinning the contract.
- **Editor mode is read once per component lifetime** instead of on every
  `update/2` — Leaf only honours `:mode` on its first render, so the repeated
  settings lookup on each parent re-render / `send_update` was pure cost.

### Dependencies

- Bump the `phoenix_kit` floor to `~> 1.7.189` (provides `PhoenixKit.SchemaPrefix`).
- Drop the orphaned `:beamlab_ex_aws_sqs` entry from `mix.lock`, which made
  `mix deps.unlock --check-unused` (and therefore `mix precommit`) fail. No
  change to the resolved dependency set.

## 0.2.14 — 2026-07-10

### Changed

- **Settings page modernized (`Web.Settings`).** The five stacked cards collapse
  into a single card with lightweight in-card section headers (new
  `settings_section_header/1` component — a local copy of core's
  `FormSection.section_header/1` so the package renders identically without
  waiting on a core release). Toggles and inputs move to daisyUI label patterns,
  and the **Reset to Defaults** button gains a `data-confirm` prompt since it
  wipes moderation, limits, Giphy, and attachment settings.
- **Resource resolution delegated to core `PhoenixKit.ResourceLinks`.**
  `PhoenixKitComments` drops its own ~170-line handler registry and
  path-template engine and `defdelegate`s `get_resource_path_templates/0`,
  `update_resource_path_templates/1`, and `resolve_resource_context/1` (plus the
  notification-callback dispatch) to core. The comments moderation admin and the
  Activity feed now resolve deep-links through one shared resolver, off one set
  of handlers and the shared `comment_resource_paths` templates. Behaviour is
  preserved — same return shape, same default handlers (`post`/`file`/`user`),
  same template fallback — with `integration` and module-declared handlers added
  for free.

### Added

- **Full gettext coverage for the settings page.** Every user-facing string on
  `Web.Settings` (template + flash messages) is now wrapped in
  `gettext`/`ngettext` against the module's own `PhoenixKitComments.Gettext`
  backend, and the `default.pot` + `en`/`et`/`ru` catalogs are extended and
  fully translated to Russian and Estonian.

### Dependencies

- Bump `phoenix_kit` to `1.7.181` (provides `PhoenixKit.ResourceLinks`).

## 0.2.13 — 2026-06-25

### Added

- **Russian and Estonian translations for the comments UI.** The comment
  moderation dashboard (`Web.Index`) and the embeddable comment LiveComponent
  (`Web.CommentsComponent`) now ship a dedicated `PhoenixKitComments.Gettext`
  backend with its own `priv/gettext` catalogs, fully translated to `ru` and
  `et` (English remains the source). The strings follow the host's global locale
  automatically — no extra wiring — so a Russian- or Estonian-locale session
  sees those screens in that language; any other locale falls back to English.
  The catalogs now ship with the package (`priv` added to the Hex `files` list).

### Fixed

- **`mix dialyzer` no longer fails on the Russian plural catalog.** The new
  gettext backend's generated code passes an opaque `%Expo.PluralForms{}` (the
  `ru` `nplurals=3` plural form) to `Gettext.Plural.plural/2`, which Dialyzer
  reported as an opaqueness violation and broke the `quality.ci` / `precommit`
  gate. Added a targeted `.dialyzer_ignore.exs` filter (the value is correct at
  runtime; the suite exercises it).

## 0.2.12 — 2026-06-24

### Added

- **User comments link to the user's admin page.** Registers
  `PhoenixKit.Users.CommentResources` as the `"user"` resource handler (gated on
  the module being present in `phoenix_kit` core, alongside the existing
  `"post"`/`"file"` handlers). A comment attached to a user
  (`resource_type: "user"`) now resolves to that user's display name and
  `/admin/users/view/:uuid` detail page, with their avatar as the chip
  thumbnail, instead of rendering a bare uuid in the moderation dashboard.
  Gracefully no-ops on older core versions that don't ship the module.

### Changed

- **Friendlier no-handler fallback resource chip.** When a comment's
  `resource_type` has neither a registered handler nor a host-configured path
  template, the moderation-dashboard chip now renders a tag icon, a humanized
  type label (`"test_page"` → `"Test page"`), and a short uuid, with a
  `"<type>: <uuid>"` tooltip — instead of a raw type badge plus a bare uuid.

## 0.2.11 — 2026-06-17

### Changed

- **Dropped the direct `mdex` dependency.** `phoenix_kit` core now declares
  `mdex` directly, so comments no longer needs its own dep —
  `lib/phoenix_kit_comments/web/markdown.ex` calls `MDEx` and resolves it
  transitively through `phoenix_kit`, keeping a single shared version across all
  modules and eliminating version-mismatch risk. Mirrors the existing `leaf`
  arrangement.

## 0.2.10 — 2026-06-17

### Added

- **Comments admin moderation overhaul.** Title/subtitle moved into the top
  navbar (matching the media admin), so they collapse cleanly on mobile and
  free up vertical space; an Actions tile (Settings shortcut) joined the stats
  row; the toolbar gained a filtered count and a clear-X on the search field.
- **Resource chips with thumbnails.** Each comment shows a clickable chip for
  the resource it belongs to (with an image thumbnail where available), in both
  the table and the grid/card view. File comments link straight to their media,
  deep-linking to the corresponding Etcher shape when one exists.
- **Reply navigation.** A reply can jump to the original comment it answers by
  filtering the list to that comment's uuid (entered into the search box) — no
  modal.
- **Status-aware row actions** in a `…` dropdown that matches the users table:
  deleted comments offer Restore only; Status now sits next to Actions.
- **Long-comment handling.** Admin previews truncate to one line with a
  "Read more ›" cue and open the full, markdown-rendered comment in a modal. In
  the public `CommentsComponent`, long comments collapse behind a YouTube-style
  "Read more" / "Show less" toggle.

### Changed

- **Comment markdown now renders with MDEx (comrak) instead of Earmark** on
  display, in both the public component and the admin grid/card view. Comments
  are authored as markdown in the Leaf composer, which already renders with
  MDEx; using the same engine and `render` options (`hardbreaks`, `unsafe`) on
  display makes the rendered comment match what was typed. Output still passes
  through core's `HtmlSanitizer`, so XSS protection is unchanged; block spacing
  is restored via an explicit `.pk-comment-md` stylesheet (no typography plugin
  required). Adds `{:mdex, "~> 0.13"}` as a direct dependency (dropped again in
  0.2.11 once `phoenix_kit` core began providing it transitively).
- **i18n sweep** of the comments admin — all user-facing strings now go through
  `gettext`.

### Fixed

- **Reply indicator no longer crashes on media-only parents.** The
  `/admin/comments` "— Re: …" reply label sliced `parent.content` directly, but a
  comment can have blank content when it carries only a GIF or attachment — so a
  reply to such a comment raised `String.slice(nil, _)` and crashed the
  moderation page. Blank parents now show a `[no text]` placeholder.
- **Unified the comment-markdown styles.** The public `CommentsComponent` shipped
  its own inline `.pk-comment-md` `<style>` copy that had drifted from the shared
  `comment_markdown_styles/1` helper the admin uses (different paragraph/heading
  spacing). The component now renders the shared helper, so public and admin
  render identical markdown from a single source of truth.
- **Media-only comments are no longer invisible in the admin.** A GIF- or
  attachment-only comment (blank text) used to render an empty preview row and an
  empty full-comment modal. The list now shows a "GIF" / "Attachment" placeholder,
  and the modal renders the GIF and an attachment count.
- **Resource-chip thumbnails dropped the inline `onerror` handler** in favour of
  a CSS background image, so a missing thumbnail degrades gracefully without inline
  JS (consistent with the `window.PhoenixKitHooks` convention and strict CSPs).
- **Resource chips link correctly to host/external pages.** Non-prefixed resource
  paths (host controller pages or external URLs) now use a plain `href` instead of
  LiveView `navigate`, which only works between LiveViews in the session.
- **Keyboard access for the admin comment preview.** The clickable one-line
  preview is now a focusable `role="button"` that opens the full-comment modal on
  Enter.
- **Pagination links** no longer carry empty `search=`/`resource_type=`/`status=`
  query params; they reuse `build_url_params/2`, which strips blanks.
- **Editing a comment to empty text now works when it has a GIF or attachment.**
  The composer's `save_edit` rejected blank content unconditionally; it now allows
  it for GIF/attachment-only comments (which the changeset already considers valid),
  and only blocks a truly empty edit.
- **Admin grid/card view shows the status as a badge** (matching the table column)
  instead of a bare string.

## 0.2.9 — 2026-06-16

### Added

- **Reaction resource-handler callbacks** so hosts can surface "X liked your
  comment" notifications, symmetric with the existing
  `on_comment_created/3` / `on_comment_deleted/3` pair. `like_comment/2`,
  `unlike_comment/2`, `dislike_comment/2`, and `undislike_comment/2` now
  dispatch (best-effort, after the existing PubSub broadcast) to the registered
  resource handler's optional callback:
  - `on_comment_liked/3`, `on_comment_unliked/3`, `on_comment_disliked/3`,
    `on_comment_undisliked/3`
  - Third arg is a `%{comment: %Comment{}, liker_uuid: binary}` payload (the
    comment row carries the author, not the reacting user). The map shape keeps
    the existing 3-arity handler contract and stays extensible.
  - Each callback is optional (`function_exported?/3` guard) and fires only when
    the reaction state actually changed — never on `:already_liked` no-ops or
    `{:error, _}` results. Self-action skipping is left to the host. Purely
    additive: existing handlers are unaffected.

### Changed

- Reaction broadcasts and resource-handler callbacks now share a single comment
  lookup instead of two, restoring the original one-query cost per reaction
  toggle. No-op results (`:already_liked`, `:already_disliked`, errors) issue
  zero extra reads, and the post-commit side-effect is fully rescue-wrapped so a
  transient read failure cannot fail an already-committed reaction.
- Removed `leaf` as a direct dependency. PhoenixKit already requires `leaf`, so
  the package remains available transitively; the comments module still detects
  its presence at runtime and falls back to a plain `<textarea>` when it is
  absent or not wired by the host.

## 0.2.8 — 2026-06-09

### Added

- **Cross-session live updates over `Phoenix.PubSub`** (#20). Comment create,
  delete, and reaction changes now broadcast `{:comments_updated, %{resource_type,
  resource_uuid, action}}` (`action` is `:created | :deleted | :reaction`) on a
  per-resource topic. Hosts subscribe with `PhoenixKitComments.subscribe/2` (and
  `unsubscribe/2`) so other connected users — count badges, open threads — update
  without a reload. The payload mirrors the existing self-message the component
  sends its own host, so there's one message contract for local and remote
  updates. Broadcasts are best-effort and never break the write path; the PubSub
  server is resolved via `PhoenixKit.PubSubHelper`.
- **Batch comment counting** (#19). `count_comments/3` now accepts a **list** of
  `resource_uuid`s and returns a `uuid => count` map from a single grouped query
  (with a `0` entry for every requested uuid), avoiding the N+1 a host hit when
  rendering one count badge per row. Honors the same `:status` / `:include_deleted`
  options as the scalar form.

### Changed

- **Rich-text editor is now opt-out** (#18). The Leaf editor requires the host to
  register Leaf's JS hook; when it's missing the composer silently hangs on its
  loading text. Hosts can now fall back to the always-working plain `<textarea>`
  via the new `comments_rich_text` setting (default `true`) or a `rich_text`
  attr on `CommentsComponent`. Added an admin settings toggle and a "JavaScript
  wiring" + troubleshooting section to the README documenting which hooks the
  host must register.

## 0.2.7 — 2026-06-08

### Added

- Comment bodies now render their markdown (authored in the Leaf editor) to
  sanitized HTML on display via core's `<.markdown>` component (Earmark +
  `HtmlSanitizer`), so bold / italics / lists / quotes / links / code show
  formatted instead of as raw markdown. Previously the raw content string was
  shown verbatim.
- Scoped CSS (`.pk-comment-md`) restores list / blockquote / heading / code
  styling that Tailwind's preflight resets, without depending on the
  `@tailwindcss/typography` (`prose`) plugin being present in the host — so
  lists render with bullets/numbers even where prose isn't configured. Inline
  `` `code` `` gets a chip background, fenced `<pre>` blocks get a backdrop, and
  links render in the theme primary color so they're distinguishable from
  emphasized text.

### Changed

- `leaf` is now a required dependency at `~> 0.2.22` (was `optional: true`,
  `~> 0.2`). The comment composer is built on the Leaf editor, and phoenix_kit
  core already hard-depends on leaf, so it's always present wherever comments
  runs — the optional declaration described an unreachable leaf-free build. The
  `0.2.22` minimum pulls in Leaf's markdown-link round-trip fix (editing a
  comment with a link no longer doubles it into `[[label](url)](url)`). The
  `leaf_available?/0` textarea fallback stays as defensive code.

### Fixed

- `CommentsComponent` no longer flips to "Sign in to post a comment" for a
  logged-in user on a partial `send_update`. `can_post?` was derived from
  the incoming `assigns[:current_user]` (nil on any update that omits it,
  e.g. a parent poking `loaded?: false` to refresh the thread — as
  PhoenixKit's MediaCanvasViewer does when an annotation is drawn). It now
  reads the resolved socket value (kept across updates by `assign_new`),
  so the composer stays available.

## 0.2.6 — 2026-06-07

### Features

- New `PhoenixKitComments.Embed` macro. A host LiveView embedding
  `CommentsComponent` must forward the composer's rich-text (Leaf)
  `{:leaf_changed, …}` process message into
  `CommentsComponent.forward_leaf_event/2`, or the editor's content never
  reaches the component and "Post comment" silently no-ops. `use
  PhoenixKitComments.Embed` wires that forward as an `on_mount`
  `attach_hook(:handle_info)` lifecycle hook, so it composes with a host that
  already defines its own `handle_info` (no clause-grouping clash, no
  clobbering). For hosts that hard-depend on `phoenix_kit_comments`;
  soft-dependency hosts resolve `forward_leaf_event/2` at runtime instead (see
  the moduledoc).

### Changed

- Bumped dependencies (`mix.lock`).

## 0.2.5 — 2026-05-29

First Hex release since 0.2.1; bundles the unreleased 0.2.2–0.2.4 work
below plus the fixes and cleanup in this entry.

### Fixed

- Likes/dislikes no longer crash. `insert_reaction/2` had been switched to
  `insert_all` with `on_conflict: :nothing,
  conflict_target: [:comment_uuid, :user_uuid]`, but no composite unique
  index exists on those columns (the original `UNIQUE(comment_id,
  user_id)` was dropped with the integer `user_id` column during the
  uuid-FK migration and never recreated on `user_uuid`). That made every
  like/dislike raise a Postgrex "no unique or exclusion constraint
  matching the ON CONFLICT specification" error. Restored the
  `exists?`-precheck + changeset insert, which doesn't depend on the
  missing index.

### Changed

- Reaction highlight state (`liked_comment_uuids` /
  `disliked_comment_uuids`) is now reloaded only when comments reload or
  the viewer / `show_likes` change, instead of on every `update/2` —
  removing two redundant queries per parent re-render.
- The inline reply composer is no longer a feature-poor duplicate of the
  top/bottom composer. Both now share one `composer_form/1`, so replies
  gain the GIF picker, audio recorder, and full attach menu, and the form
  markup / translations live in a single place.
- Wrapped the remaining user-facing strings in the edit and reply forms
  in `gettext` (they had been missed in the gettext sweep).

## 0.2.4 — 2026-05-29

### Features

- Header is now configurable via three optional, backward-compatible
  assigns (defaults reproduce current behavior exactly):
  - `show_title` (default `true`) — when `false`, the
    "{title} ({count})" header line is not rendered.
  - `collapsible` (default `false`) — turns the header into a
    disclosure toggle (chevron + `aria-expanded`/`aria-controls`) that
    collapses/expands the whole body. Collapse state is ephemeral
    (in-memory, resets on remount).
  - `initial_collapsed` (default `false`) — starting collapse state when
    `collapsible`; host-customizable.
  Note: the collapse chevron lives in the header, so `collapsible` has
  no visible toggle when `show_title: false` (the thread stays per
  `initial_collapsed`).
- `composer_position` (default `:top`) — render the "Write comment"
  composer at `:top`, `:bottom`, or `:both`. Bottom is off by default.
  Internally the composer's open state is now position-aware
  (`composer_open_at`), so `:both` never mounts two Leaf editors or two
  upload inputs — only the opened position shows the form; the other
  stays a button.

## 0.2.3 — 2026-05-29

### Changed

- Comment card layout restructured to a strict vertical stack
  (avatar + email → decoration label → body → date → actions) so the
  card reads correctly in narrow embed containers (media sidebar,
  MediaDetail panel) instead of truncating the email under the action
  buttons.
- Action buttons (like / dislike / reply / edit / delete) are now
  right-aligned in their footer row.
- Decoration label (annotation title) now renders at the comment body's
  size, bold (`text-base font-bold`) — and gets top spacing so it reads
  as a peer title rather than a cramped sub-heading.
- Image attachments fill the comment width (`w-full max-h-96
  object-contain`) instead of being capped at `max-w-xs`.
- Bump leaf 0.2.13 → 0.2.21.

### Merged

- Integrated upstream `main` (gettext sweep + `precheck_create` upload
  refactor) with the local Leaf-editor, decoration-registry, inline-reply,
  reaction, and composer-toggle work.

## 0.2.2 — 2026-05-14

### Fixed

- Complete gettext/i18n coverage for `CommentsComponent` flash messages,
  error helpers, upload labels, video/audio fallback text, and accessibility
  attributes (`alt`, `aria-label`).
- Version sync between `mix.exs`, `version/0`, and test assertion.
- Precommit cleanliness: removed stale self-referential `phoenix_kit_comments`
  from `mix.lock`, formatted `mix.exs`.

## 0.2.1 — 2026-05-12

### Features

- Rendered comments now carry `data-comment-uuid` and (when present)
  `data-annotation-uuid` on the outer wrapper, letting sibling components
  on the host page correlate DOM nodes with comment + linked-resource
  uuids without reaching into render internals.
  `data-annotation-uuid` is sourced from `metadata["annotation_uuid"]`
  and omitted when nil.

## 0.2.0 — 2026-05-11

### Features

- **Comment attachments**: comments can now carry images, video, audio, and
  miscellaneous file uploads alongside text or a Giphy GIF. Attachments
  flow through the parent `PhoenixKit.Modules.Storage` stack (multi-bucket
  redundancy, variant generation) and link to comments via a new
  `phoenix_kit_comment_media` junction table.
- **In-browser voice recording**: a microphone button on the comment form
  records via `MediaRecorder` (webm/opus) and submits the result through
  the same upload queue as drag-and-drop attachments. Recordings are
  audio attachments — no separate code path.
- Three new admin settings under `/admin/settings/comments` →
  "Attachments":
  - `comments_attachments_enabled` (master toggle, default off)
  - `comments_max_attachments` (per-comment cap, 1–10, default 4)
  - `comments_attachment_max_size_mb` (per-file size cap, 1–500,
    clamped against the global `storage_max_upload_size_mb`)
- `create_comment/4` accepts a new `:attachment_file_uuids` key — a list
  of `PhoenixKit.Modules.Storage.File` UUIDs to attach in display order.
  Insert + attaches run in one transaction.
- New public context fns: `attach_media/3`, `detach_media/2`,
  `detach_media_by_uuid/1`, `list_comment_media/2`,
  `attachments_enabled?/0`, `get_max_attachments/0`,
  `get_max_attachment_size_mb/0`.
- Comment validity rule generalized to "content **OR** Giphy **OR**
  attachments". The Comment schema gains a virtual `:has_attachments?`
  field set by the orchestrator at insert time.

### Migration

- Requires the new `phoenix_kit_comment_media` table introduced in
  PhoenixKit migration V113. Run `mix ecto.migrate` after bumping the
  parent `phoenix_kit` dep.

## 0.1.5 — 2026-04-17

### Features

- Optional Giphy integration for the comment form. Users can post text-only,
  GIF-only, or text + GIF comments via a floating Giphy picker; the selected GIF
  is stored on `comment.metadata["giphy"]` and rendered inline with the comment.
- New admin settings under `/admin/settings/comments` → "Giphy Integration":
  enable toggle, API key (stored in DB), and content rating filter (G/PG/PG-13/R).
- `:form_extras` slot on `CommentsComponent` — parent projects can inject their
  own inputs into the new-comment form and any `name="metadata[<key>]"` values
  are merged into `comment.metadata` on submit (the `"giphy"` key is reserved).
- Character counter and Cancel button added to the top-level comment form.
- Responsive mobile overhaul of the entire comments component (down to ~320px):
  card `overflow-hidden`, wrapping header, `break-words` content,
  container-scaled GIFs, and a mobile bottom-sheet variant of the picker.

### Breaking

- `Comment.changeset/2` no longer requires `:content`; either `content` or a GIF
  attachment in `metadata["giphy"]` is accepted. Downstream consumers that built
  their own changeset relying on the `content can't be blank` error should check
  the new validation path.

### Dependencies

- Add `giphy_api ~> 0.1.1`. The API key is passed per-call via the library's
  `:api_key` option, so no global `Application.put_env/3` write happens on each
  search. Empty keys short-circuit before any HTTP call.

## 0.1.4 — 2026-04-12

### Fixed

- Add routing anti-pattern warning to AGENTS.md.

## 0.1.3 — 2026-04-02

### Improvements

- Migrate select elements to daisyUI 5 label wrapper pattern and remove deprecated
  `select-bordered` class.
- Mobile-responsive admin filter bar with separate mobile/desktop layouts.

## 0.1.2 — 2026-03-31

### Features

- Truncate long metadata values in display titles — individual values capped at 15 characters
  with `...` suffix to keep titles compact.
- Mobile-responsive settings page — table switches to stacked card view on small screens, toggles
  and buttons stack vertically, overflow prevention throughout.
- Client-side badge coloring — metadata field badges now highlight based on the currently focused
  input via the JS hook instead of server-side rendering.
- Display title shown in mobile card view on comments admin page when a display template is
  configured for the resource type.

## 0.1.1 — 2026-03-31

### Features

- Add `:prefix` placeholder for resource path templates — paths no longer auto-prefix with
  `Routes.path()`; include `:prefix` in your template to get the site URL prefix.
- Add configurable display title templates for resource types — show meaningful names instead of
  truncated UUIDs in the admin comment list.
- Add inline editing for resource link patterns in settings (edit button, save/cancel).
- Add inline comment content editing in CommentsComponent (edit button, save/cancel).
- Add clickable metadata field badges with live color updates — green when used in the template,
  gray when unused. Clicking inserts the placeholder at cursor position.
- Add `list_metadata_keys_by_type/0` — queries distinct JSONB metadata keys per resource type
  for display in settings.

### Bug Fixes

- Fix placeholder collision — `:metadata.prefix` and `:metadata.uuid` were corrupted by naive
  substring replacement. Metadata placeholders are now resolved first.
- Fix event listener accumulation in InsertAtCursor JS hook — listeners were re-added on every
  LiveView patch without cleanup. Now uses `AbortController` for proper teardown.
- Fix XSS vector in InsertAtCursor hook — replaced `querySelector` built from input name string
  with direct element reference.
- Fix missing server-side content validation on comment edits — now enforces empty check and
  configurable `comments_max_length` setting (previously only enforced on creation).
- Fix edit/reply state collision in CommentsComponent — entering edit mode now clears reply state
  and vice versa.
- Fix `editing_path_value` not cleared after saving resource path edit.
- Fix draft state (`draft_paths`/`draft_titles`) not cleaned up after adding unconfigured type.
- Add `Logger.warning` to `list_metadata_keys_by_type/0` rescue block instead of silently
  swallowing errors.
- Fix nested forms in settings page — Resource Link Patterns card was rendered inside the main
  settings `<form>`, producing invalid HTML. Moved outside the form.
- Fix stale assigns after adding/removing resource path templates — `unconfigured_types` now
  stays in sync without requiring a page refresh.
- Add path template input validation — templates must start with `/` or `:prefix` and cannot
  contain `://`, preventing XSS via `javascript:` URIs and open redirects.
- Add `Logger.warning` to rescue blocks in `count_comments_by_type/0` and
  `get_resource_path_templates/0` instead of silently swallowing errors.

### Improvements

- Deduplicate resource path add/save logic into shared `save_resource_config/5`.
- Make `extract_path/1` and `extract_title/1` private (only used within settings module).
- Resource path table uses `table-fixed` with `break-all` to handle long templates.

## 0.1.0 — 2026-03-27

### Features

- Initial release — polymorphic comments module extracted from PhoenixKit.
- Resource-agnostic design via `(resource_type, resource_uuid)` tuples with no FK constraints.
- Unlimited self-referencing comment threading with configurable max depth.
- Like/dislike system with denormalized counters and transactional safety.
- Moderation workflow: pending, published, hidden, deleted statuses with bulk operations.
- Admin UI: paginated comment list with search, status filters, and resource type grouping.
- Settings UI: toggles for enable/moderation, configurable max depth and max length.
- Resource handler callbacks for `on_comment_created/3` and `on_comment_deleted/3`.
- Resource resolution system with handler-based and path-template-based fallbacks.
- ILIKE search with proper wildcard escaping.
- Soft delete preserving comment tree structure.
