Skip to content

Add square hover-flip CollectionCardSquare - #59

Merged
ebulgakov merged 6 commits into
mainfrom
fix/redesign-collectioncard-square-hover-flip
Aug 9, 2026
Merged

Add square hover-flip CollectionCardSquare#59
ebulgakov merged 6 commits into
mainfrom
fix/redesign-collectioncard-square-hover-flip

Conversation

@ebulgakov

@ebulgakov ebulgakov commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • New CollectionCardSquare shows a square preview with title overlaid, flipping on hover/focus to reveal the description mirrored on the same face (true 3D flip, no separate back panel).
  • Wired into / and /published-collections (4-per-row grid); existing CollectionCard untouched on /collections.
  • Animation polish: linear flip timing so the description/title swap lands exactly on the edge-on midpoint (no flicker), will-change: transform, :focus-within support for keyboard/touch, plus a blurred-background fill behind the preview image.

Test plan

  • pnpm test — colocated collection-card-square.test.ts passes
  • pnpm lint
  • Manual: hover/tab a card with a description on / and /published-collections, confirm smooth flip with no mid-animation pop

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added square collection cards with optional images, descriptions, navigation, flip behavior, and overlay actions.
    • Added Storybook examples for multiple square card variations.
  • Improvements
    • The homepage and published collections page now use square cards in four-column layouts.
    • Homepage displays four randomly selected published collections.
  • Tests
    • Added comprehensive coverage for square card behavior.
    • Increased code coverage requirements.

ebulgakov and others added 5 commits August 7, 2026 11:23
New CollectionCardSquare shows preview image as a proportional square
with title overlaid, flipping on hover to reveal the description on
its back (skipped when there's no description). Wired into / and
/published-collections; existing CollectionCard is untouched and
still used on /collections and /collections/[id].

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Squares were too big at 3-per-row; now 4-per-row on / and
/published-collections, homepage picks 4 random collections instead
of 6 to fill exactly one row. Flip no longer swaps to a solid
description panel - it's a true 3D flip so the same preview image
shows mirrored, with the description fading in on top as a
non-rotating overlay.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
linkfolio Ready Ready Preview Aug 9, 2026 12:50pm

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ebulgakov, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ab8829b5-f88e-41c6-9cd3-e3251ca5ba18

📥 Commits

Reviewing files that changed from the base of the PR and between 4e3adc4 and 40bba04.

📒 Files selected for processing (2)
  • app/shared/ui/__tests__/collection-card-square.test.ts
  • app/shared/ui/collection-card-square.vue
📝 Walkthrough

Walkthrough

Changes

Published collection card experience

Layer / File(s) Summary
Square card component and validation
app/shared/ui/collection-card-square.vue, app/shared/ui/index.ts, app/shared/ui/collection-card-square.stories.ts, app/shared/ui/__tests__/collection-card-square.test.ts, vitest.config.ts
Adds CollectionCardSquare with optional navigation, images, placeholders, descriptions, flipping, overlays, and reduced-motion handling. Adds Storybook stories, Vitest coverage, and higher coverage thresholds.
Four-item published collection selection
app/features/published-collections/model/use-random-published-collections.ts, app/features/published-collections/model/use-homepage-published-collections.ts, app/features/published-collections/model/__tests__/use-random-published-collections.test.ts
Limits random published collection selection to four items and updates related documentation and tests.
Published collection grid integration
app/pages/index.vue, app/pages/published-collections.vue
Replaces CollectionCard with CollectionCardSquare and changes medium-breakpoint grids to four columns.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Homepage
  participant PublishedCollectionsModel
  participant CollectionCardSquare
  Homepage->>PublishedCollectionsModel: request published collections
  PublishedCollectionsModel->>PublishedCollectionsModel: select up to four random collections
  PublishedCollectionsModel-->>Homepage: return selected collections
  Homepage->>CollectionCardSquare: render each collection with square card props
  CollectionCardSquare->>CollectionCardSquare: render image, placeholder, description, and overlays
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding the square hover-flip CollectionCardSquare component.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/redesign-collectioncard-square-hover-flip

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ebulgakov ebulgakov added the review Call @claude review & @coderabbit review label Aug 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/shared/ui/__tests__/collection-card-square.test.ts`:
- Around line 15-30: Update mountCard to use the shared testing utilities to
stub Vuetify’s VImg and VIcon components instead of creating and registering a
real createVuetify() plugin. Remove the local Vuetify instance and configure the
shared boundary stubs in the mount options, preserving the existing route,
slots, and prop behavior while asserting CollectionCardSquare’s contract through
those stubs.

In `@app/shared/ui/collection-card-square.vue`:
- Line 14: The description is rendered with opacity 0 when canFlip is false,
causing it to disappear in noFlip mode. Update the description layout and
associated selectors in the collection card template/styles so noFlip displays
the description visibly on the front face, while preserving the existing flip
behavior when canFlip is true.
- Around line 19-23: Move the hover and focus-within flip-state styling from the
outer card to the collection-card-square__flipper component so overlay controls
remain operable. When to is absent, make the flippable div keyboard-focusable
and ensure keyboard focus reveals the description without immediately hiding
controls. Add coverage for overlay pointer activation, overlay keyboard
interaction, and keyboard access to unlinked cards.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a9beaea-1ae5-4a6f-906f-421d6af59112

📥 Commits

Reviewing files that changed from the base of the PR and between 06381ad and 4e3adc4.

📒 Files selected for processing (10)
  • app/features/published-collections/model/__tests__/use-random-published-collections.test.ts
  • app/features/published-collections/model/use-homepage-published-collections.ts
  • app/features/published-collections/model/use-random-published-collections.ts
  • app/pages/index.vue
  • app/pages/published-collections.vue
  • app/shared/ui/__tests__/collection-card-square.test.ts
  • app/shared/ui/collection-card-square.stories.ts
  • app/shared/ui/collection-card-square.vue
  • app/shared/ui/index.ts
  • vitest.config.ts

Comment on lines +15 to +30
// Vuetify's components need a real Vuetify instance to render (they inject
// their defaults/theme via provide/inject) - mountSuspended gives Nuxt
// context but doesn't run the app's vuetify-nuxt-module plugin, so it's
// supplied explicitly here, same as Vuetify's own unit-testing docs.
const vuetify = createVuetify();

function mountCard(
props: CollectionCardSquareProps,
extra: { route?: string; slots?: Record<string, () => string> } = {}
) {
return mountSuspended(CollectionCardSquare, {
props,
route: extra.route,
slots: extra.slots,
global: { plugins: [vuetify] }
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg '(^|/)collection-card-square\.(test\.ts|vue)$|testing|vuetify|ui' | head -200 || true

echo
echo "== target test =="
if [ -f app/shared/ui/__tests__/collection-card-square.test.ts ]; then
  wc -l app/shared/ui/__tests__/collection-card-square.test.ts
  cat -n app/shared/ui/__tests__/collection-card-square.test.ts
fi

echo
echo "== candidate shared test utilities =="
python3 - <<'PY'
import os
base='app/shared/testing'
for root, dirs, files in os.walk(base):
    dirs[:] = sorted(d for d in dirs if not d.startswith('.'))
    for f in sorted(files):
        if f.endswith(('.ts','.vue')):
            p=os.path.join(root,f)
            print(f)
            lines=open(p,encoding='utf-8',errors='replace').readlines()
            hits=[i+1 for i,l in enumerate(lines) if 'Vuetify' in l or 'VImg' in l or 'VIcon' in l or 'Mock' in l or 'Stub' in l or 'mountSuspended' in l]
            print('  hits:', hits[:20] if hits else [])
PY

echo
echo "== search for vuetify mocking/stubbing in tests =="
rg -n "createVuetify|VImg|VIcon|useVuetify|localVue|vuetify|setupVuetify|global:|plugins:|resolve\(\"|Mock|Stub|component" app/shared app -g '*.test.ts' -g '*.ts' -g '*.vue' | head -250 || true

Repository: ebulgakov/linkfolio

Length of output: 32779


Mock the Vuetify boundary in this unit test.

The test uses createVuetify() instead of shared boundary mocking. Replace the real Vuetify plugin with stubs for VImg and VIcon through the shared testing utilities, then check the component contract against those stubs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/shared/ui/__tests__/collection-card-square.test.ts` around lines 15 - 30,
Update mountCard to use the shared testing utilities to stub Vuetify’s VImg and
VIcon components instead of creating and registering a real createVuetify()
plugin. Remove the local Vuetify instance and configure the shared boundary
stubs in the mount options, preserving the existing route, slots, and prop
behavior while asserting CollectionCardSquare’s contract through those stubs.

Source: Coding guidelines

Comment thread app/shared/ui/collection-card-square.vue
Comment thread app/shared/ui/collection-card-square.vue
…s in CollectionCardSquare

Addresses CodeRabbit review on PR #59:
- noFlip cards never showed the description: the hover/focus rule that
  fades scrim->description was scoped to the --flip class, which noFlip
  disables. Split into a --reveal class (crossfade, gated on description
  presence alone) separate from --flip (rotation, still gated on
  !noFlip). The description's counter-rotation transform is now scoped to
  --flip only, since --reveal-only cards never rotate the parent and
  would otherwise render the text mirrored.
- Hover/focus-within on the outer card made overlay slot controls
  (e.g. an "Add link" button) unclickable/unfocusable the instant the
  pointer or focus entered the card, and unlinked-but-flippable cards had
  no focusable element at all. Triggers now bind to the flipper itself
  (:hover/:focus), and the overlay uses a sibling-hover selector so it
  stays interactive once the pointer/focus actually reaches it; unlinked
  revealable cards get tabindex=0.

Left the Vuetify-mocking suggestion (createVuetify() vs. a shared
VImg/VIcon stub) unaddressed: no such shared stub utility exists in
app/shared/testing, and the real-instance pattern here is the one
documented in Vuetify's own testing docs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ebulgakov

Copy link
Copy Markdown
Owner Author

Addressed in 40bba04:

  • noFlip hides description (Minor) — fixed. Split --flip (rotation, needs !noFlip) from a new --reveal class (scrim↔description crossfade, gated on description alone). noFlip cards now crossfade on hover/focus without rotating. Also had to scope the description's counter-rotation transform: rotateY(180deg) to --flip only — it was unconditional, so once made visible for --reveal-only cards it would've rendered mirrored/backwards with no rotated parent to cancel it out.
  • Overlay controls unusable / unlinked cards unfocusable (Major) — fixed. Hover/focus triggers now bind to .collection-card-square__flipper itself instead of the outer container, and the overlay uses a sibling-hover selector (.flipper:hover ~ .overlay) so it stays interactive once the pointer/focus actually reaches it, rather than hiding itself the instant it receives focus. Unlinked-but-revealable cards (to absent) get tabindex="0" on the flipper.
  • Vuetify mocking in the test (Major) — skipped. No shared VImg/VIcon stub utility exists in app/shared/testing (checked — that directory only has API/composable boundary mocks), and this is the only component test needing real Vuetify rendering; the createVuetify() instance is the pattern documented in Vuetify's own unit-testing docs, already explained inline in the test's comment. Building a new shared stub abstraction for a single caller felt out of proportion here.

Added test coverage for the tabindex/reveal-class contract (pnpm test — 10/10 passing), pnpm type-check and pnpm lint clean.

@ebulgakov
ebulgakov merged commit 9b4f64e into main Aug 9, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review Call @claude review & @coderabbit review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant