Uh oh!
There was an error while loading. Please reload this page.
MediaGallery: add max_count attr + disable Add button at limit - #556
Conversation
The MediaGallery LiveComponent now accepts an optional :max_count integer attribute. When set, the gallery caps the selection and disables the Add (open picker) button once the limit is reached. - :single mode implicitly enforces max_count = 1 regardless of the attribute, matching existing behaviour. - :multiple mode with a positive max_count clamps any selection via apply_selection/4 (defence-in-depth — the disabled button is the primary UI guard) and disables the Add trigger when length(selected) >= max_count. - nil / unset = unlimited (no behavioural change for existing callers). - selection_at_limit?/3 mirrors apply_selection so the button state stays in sync with the selection rule. Used by consumers (e.g. Andi's document picker) that need to honour a template-author-defined cap on the number of images per slot. Test suite extended with 7 focused cases covering below-limit, at-limit, unlimited, single-mode, and apply_selection clamping.
…to table_default
- New `card_media` slot renders an image/thumbnail region above the card body
in card view. Receives item via `:let` and owns its own padding/background
so consumers can wrap a clickable link or set a backdrop. Enables the
Document Creator "preview image at top of card" pattern through the
shared core component.
- New `view_mode` (`"card"` | `"table"`) + `view_event` attrs for
controlled view selection. When set, the JS hook is disabled and only
the chosen view renders; toolbar buttons emit `phx-click={view_event}`
with `phx-value-mode` so consumers can drive view through `push_patch`
(URL-backed). `view_mode=nil` (default) keeps the existing JS hook +
localStorage behavior — fully backwards compatible.
- `table_default_header` now accepts a `class` attr (default keeps the
current `bg-primary text-primary-content`). Pass `class=""` for a
neutral header that blends with `<tbody>` — Document Creator and other
modules can opt out of the loud primary backdrop.
timujinne
left a comment
There was a problem hiding this comment.
PR #556 Review — table_default: card_media slot, controlled view_mode, header class override
Scope: Only commit 15ddac1e (the table_default changes). Commit 0805bb59 (MediaGallery max_count) is NOT reviewed here.
File:lib/phoenix_kit_web/components/core/table_default.ex
Reviewer: Claude Opus 4.6
Backwards Compatibility
Verified all 15 existing usage sites of <.table_default> and <.table_default_header> across lib/. None pass view_mode, view_event, card_media, or class to table_default_header. All defaults preserve existing behavior:
view_mode=nil→ JS hook + localStorage (unchanged)table_default_headerclass defaults to"bg-primary text-primary-content"(unchanged)card_mediaslot empty → no wrapper div rendered (:ifguard)- Toggle buttons:
data-view-actionstill emitted in JS mode,phx-clickisfalse(suppressed by Phoenix)
Verdict: No regressions for existing consumers.
Findings
NITPICK: above_cards slot doc mentions only JS hook hiding
File:table_default.ex:171
The above_cards doc says "Hidden automatically when the JS hook switches to table mode". In controlled mode (view_mode="table"), hiding is driven by the CSS class hidden on the parent data-card-view div, not by the JS hook. The behavior is correct either way (slot is hidden in table mode), but the doc is JS-mode-specific. Consider adding a note: "In controlled mode, hidden when view_mode="table" via the parent container."
NITPICK: card_media wrapper div has no class — intentional but worth a doc note
File:table_default.ex:354
The <div :if={@card_media != []}> wrapper is classless. The moduledoc says "slot owns its own padding/background", which is correct — the consumer controls styling. However, since this div sits outside card-body (above it), consumers using daisyUI's figure pattern might expect an auto-applied <figure> tag. Current approach is fine — a plain <div> is more flexible — but confirming this is a deliberate design choice, not an oversight.
NITPICK: Duplicate "grid" class in card-view controlled mode
File:table_default.ex:323-327
class={["grid gap-4 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4",# always has "grid"is_nil(@view_mode)&&"md:hidden",@view_mode=="card"&&"grid",# ← redundant "grid"@view_mode=="table"&&"hidden"]}The base class list already includes "grid". The @view_mode == "card" && "grid" line is redundant — it adds "grid" again. Harmless (CSS deduplicates), but noisy. Could be removed. Similarly, "block" in the table-view controlled branch (line 300) is redundant since <div> is block by default. These are symmetry/readability choices more than bugs.
Technical Verification
phx-click={@view_mode && @view_event}whenview_mode=nil:nil && "switch_view"→nil. Phoenix suppressesnil-valued attributes. Nophx-clickin DOM. Correct.data-view-action={if is_nil(@view_mode), do: "card"}whenview_modeis set: returnsnil, attribute suppressed. JS hook also suppressed. Consistent.values: [nil, "card", "table"]: Phoenix.Component acceptsnilinvalueslist — documented pattern for optional enum attrs. Correct.- No class conflicts in controlled mode: JS hook not attached → no JS toggling
md:hidden/md:block. Each div gets exactly one visibility class. Nomd:-prefixed classes leak into controlled mode. Verified for all three states (nil,"card","table"). - Compilation: Clean compile, no warnings.
Verdict: Approve
Clean, well-scoped extension. All three features (card_media, controlled view_mode, header class override) are backwards-compatible and correctly implemented. No bugs found. Three nitpicks, all optional.
The previous `bg-primary text-primary-content` default rendered as a loud band on top of every table — visually heavy and at odds with the calmer look modules like Document Creator already use. Flip the default to `bg-base-300` so existing usages quietly pick up the calmer style without having to thread a class through every call site. Consumers who want the old loud look can pass `class="bg-primary text-primary-content"` explicitly; `class=""` still works for a fully bare header.
timujinne
left a comment
There was a problem hiding this comment.
Review 2: table_default_header default class change (20132de6)
Verdict: Approve — no issues found.
Call-site audit
14 call sites use <.table_default_header> without an explicit class= — all will silently adopt bg-base-300. Checked every one:
activity/index.html.heex:131,users.html.heex:179,sessions.html.heex:215,live_sessions.html.heex:196,roles.html.heex:161,settings/users.html.heex:414,integrations.html.heex:77,jobs/index.html.heex:188,292(PhoenixKit core)referrals/list.html.heex:126,storage/dimensions.html.heex:143,339,storage/settings.html.heex:184,storage/health.html.heex:217(modules)
None rely on the primary band for intentional UX emphasis — all are standard admin data tables where a calmer header is appropriate.
Contrast
bg-base-300 + inherited base-content is the idiomatic daisyUI pairing and is contrast-safe across all themes (including dark). The <th> cells carry no explicit text color, so they correctly inherit from the page. Actually more robust than the old bg-primary text-primary-content since it stays in the neutral scale.
Documentation
@doc at table_default.ex:440-449 is correctly updated: documents the new default, explains how to restore the legacy look, no stale references to bg-primary as default remain.
Tests
No tests assert on header CSS classes — zero breakage risk.
Clean change, well-documented escape hatch for anyone who wants the old look back.
Follow-ups from the post-merge review of #556 and #557: - media_gallery: replace `length(selected) >= 1` emptiness check with `selected != []` — clears the credo --strict warning that #556 left on dev (credo exited 16 / CI-blocking). - table_default: card-view container sets its `display` utility per-branch instead of carrying a permanent `grid` plus a layered `hidden`, so controlled table mode no longer relies on Tailwind's hidden-beats-grid source order. - AI.Translation: add the `i` flag to the marker regex so parsing is actually case-insensitive, matching the documented contract. - language_switcher: drop the documented-but-unimplemented `completed` key from the ai_translate shape, and correct the bulk-handler doc to enqueue actionable (missing minus in_flight) languages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The headline fix: #557's PhoenixKitAI.ask_with_prompt/4 call only had a `@compile {:no_warn_undefined, ...}` (compiler-only) — dialyzer still flagged it as unknown_function, leaving `mix quality.ci` / precommit red on dev since the merge. Added the matching `.dialyzer_ignore.exs` entry, mirroring how publishing/integration guard their optional plugins. Code-review follow-ups (#556/#557): - AI.Translation: drop the dead `variable_key/1` fallback clause and the reduce that just rebuilt `fields`; field keys are strings per the contract, so they Map.merge directly with the language slots. - language_switcher: `event_name/1` now delegates to PhoenixKit.Utils.Values.presence/1 instead of hand-rolling trim-or-nil. mix precommit (compile, deps.unlock, format, credo --strict, dialyzer) passes: REAL_PRECOMMIT_EXIT=0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add author-addressed 'Follow-up changes applied' sections so @timujinne and @mdon see what landed on dev on top of their PRs: - #556: credo --strict regression (length/1 → != []) that was red on dev, plus the table_default card-view display cleanup. - #557: the dialyzer ignore fix (was red on dev), variable_key removal, marker-regex i flag, and the language_switcher doc/contract fixes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers PR #556 (MediaGallery max_count; TableDefault card_media slot, controlled view_mode, table_default_header class override + bg-base-300 default) and PR #557 (PhoenixKit.Modules.AI + AI.Translation; language switcher ai_translate affordance). Dependency bumps omitted by request. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
PhoenixKitWeb.Components.MediaGalleryLiveComponent now accepts an optional:max_countinteger attribute. When set, the gallery caps the selection and disables the Add (open picker) button once the limit is reached.:singlemode implicitly enforcesmax_count = 1regardless of the attribute, matching existing behaviour.:multiplemode with a positivemax_countclamps any selection viaapply_selection/4(defence-in-depth — the disabled button is the primary UI guard) and disables the Add trigger whenlength(selected) >= max_count.nil/ unset = unlimited (no behavioural change for existing callers).selection_at_limit?/3mirrorsapply_selectionso the button state stays in sync with the selection rule.Used by consumers (e.g. Andi's document picker) that need to honour a template-author-defined cap on the number of images per slot.
Tests
7 focused cases added covering below-limit, at-limit, unlimited, single-mode, and
apply_selectionclamping. Full suite green locally.Test plan
max_count={3}: the Add button disables at the third selection and re-enables after a removal:singlecallers (nomax_countset) behave unchanged