Skip to content

refactor(components): component architecture — Shadcn Item as base primitive and layer boundary enforcement#289

Open
andrewck24 wants to merge 8 commits intodevfrom
refactor/component-architecture
Open

refactor(components): component architecture — Shadcn Item as base primitive and layer boundary enforcement#289
andrewck24 wants to merge 8 commits intodevfrom
refactor/component-architecture

Conversation

@andrewck24
Copy link
Copy Markdown
Owner

Summary

Implements the component-architecture change. Establishes Shadcn Item as the base presentation primitive, refactors PersonItem/TeamItem to compose it, enforces component layer boundaries, relocates misplaced components, and documents architecture conventions.

Changes

  • Shadcn Item installed at src/components/ui/item.tsx with full compound component set (Item, ItemMedia, ItemContent, ItemTitle, ItemActions, ItemFooter, ItemGroup, ItemSeparator)
  • PersonItem refactored to compose ItemMedia/ItemContent/ItemTitle; PersonItemSkeleton co-located; moved to custom/list-item/
  • TeamItem refactored similarly; TeamItemSkeleton co-located; moved to custom/list-item/
  • Consumers updated to two Item forms: navigable (Item asChild + Link) and static with actions (Item + ItemActions)
  • LoadingCourt relocated from custom/loading/court.tsx to custom/court/index.tsx as named export
  • Component relocations: ui/panel.tsxcustom/panel/, ui/flip-words.tsxlanding/flip-words.tsx, ui/sheet.tsx deleted (zero consumers)
  • Architecture documented at docs/architecture.md with layer boundary table, subdirectory grouping convention, and testing conventions (data-testid usage)

摘要

實作 component-architecture change。以 Shadcn Item 作為基礎展示 primitive,重構 PersonItem/TeamItem 使用 composition 模式,強制執行 component 層邊界,搬移位置不正確的元件,並記錄架構規範。

  • 安裝 Shadcn Item(ui/item.tsx)作為零 domain knowledge 的展示層 primitive
  • PersonItem/TeamItem 改為 compose Shadcn Item primitives,skeleton 共同定位,整體移至 custom/list-item/
  • 所有 consumer 採用兩種 Item form:navigable(asChild + Link)與 static with actions
  • 搬移 LoadingCourtpanelflip-words,刪除零消費者的 sheet.tsx
  • 新增 docs/architecture.md 記錄 layer 邊界、子目錄分組規範及測試規範

- Install Shadcn Item via npx shadcn@latest add item to src/components/ui/item.tsx
- Remove custom list-item compound component (superseded by Shadcn Item)
- Write Storybook stories for Shadcn Item covering two item forms,
  action footer pattern, variant/size combinations
…tives

- Rewrite PersonItem as thin wrapper over ItemMedia/ItemContent/ItemTitle
- Remove href/onClick/action props; consumers use Item asChild for navigation
- Add co-located PersonItemSkeleton export with data-testid markers
- Install shadcn Skeleton component
- Update players/list.tsx to navigable form (Item asChild + Link)
- Update roster-list.tsx to static with actions form (Item + ItemActions)
- Rewrite tests: data-testid for structure queries, Link instead of <a>
- Rewrite TeamItem as thin wrapper over ItemMedia/ItemContent/ItemTitle
- Remove href/onClick/action/className props; consumers use Item asChild
- Add co-located TeamItemSkeleton export with data-testid markers
- Update invitations to action footer pattern (Item asChild + accept/reject footer)
- Update menu to navigable form (Item asChild + button with className)
- Rewrite tests: data-testid for structure queries, Link/button via asChild
- Move LoadingCourt from custom/loading/court.tsx to custom/court/index.tsx
- Export as named export alongside other court primitives
- Update team/lineup and record/index imports accordingly
- Delete custom/loading/court.tsx
- Delete unused ui/sheet.tsx (zero consumers)
- Relocate ui/panel.tsx to custom/panel/index.tsx, update 9 import paths
- Relocate ui/flip-words.tsx to landing/flip-words.tsx, update hero.tsx import
- Fix hero.test.tsx mock path for flip-words relocation
- Create docs/architecture.md with layer boundary table (ui/custom/domain)
- Document testing conventions: data-testid usage, Link vs <a>, no container.querySelector
- Note iterative expansion policy for future changes
Move person-item and team-item to custom/list-item/ subdirectory,
mirror test structure under custom/__tests__/list-item/, and update
all consumer import paths.

Also use Array.from loop for LoadingCourt placeholder cards for
consistency with RecordCourt pattern, and update architecture docs
to reflect subdirectory grouping convention.
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 27, 2026

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

Project Deployment Actions Updated (UTC)
volleybro Ready Ready Preview, Comment Mar 27, 2026 9:18pm

@claude
Copy link
Copy Markdown

claude bot commented Mar 27, 2026

Claude finished @andrewck24's task in 3m 12s —— View job


PR Review: component architecture refactor

  • Gather context & understand request
  • Review ui/item.tsx — new Shadcn Item primitive
  • Review custom/list-item/person-item.tsx & team-item.tsx
  • Review consumer updates (user/invitations, user/menu, team/players/list, record/new/roster-list)
  • Review test files
  • Review Storybook stories
  • Review architecture docs
  • Post final review

Overall this is a well-structured refactor. The compound component pattern for Item, the asChild polymorphism, and the two-form consumer convention (navigable vs. static-with-actions) are clean and consistent. Test coverage including axe a11y checks is excellent. A few issues worth addressing before merge:


Issues

1. Invalid Tailwind arbitrary-variant syntax — ui/item.tsx:34 (likely a bug)

"... [a]:transition-colors [a]:hover:bg-accent/50"

Tailwind arbitrary variants require & to reference the element itself, and _ for descendant selectors. [a]: is not valid Tailwind syntax and will generate no CSS. The intent appears to be styling anchor descendants of Item on hover.

Should be:

"... [&_a]:transition-colors [&_a]:hover:bg-accent/50"

Fix this →


2. Missing React import in ui/skeleton.tsx — TypeScript error

skeleton.tsx uses React.HTMLAttributes<HTMLDivElement> (line 3) without importing React. All other ui/ components use import * as React from "react". With the project's tsconfig.json types array not including a global React reference, npx tsc --noEmit should flag this.

// skeleton.tsx — add at the top
import * as React from "react"

Fix this →


3. TeamItem loading state uses raw Tailwind instead of <Skeleton>custom/list-item/team-item.tsx:27–30

// team-item.tsx — inconsistent loading state
<span
 
  className="inline-block h-4 w-24 animate-pulse rounded bg-muted"
/>

PersonItemSkeleton and TeamItemSkeleton both use the <Skeleton> component (which is already imported in this file). The inline loading state inside TeamItem bypasses that abstraction, producing a slightly different pulse color (bg-muted vs. bg-primary/10 in Skeleton).


4. handleReject silently drops per-item errors — user/invitations/index.tsx:54–67

handleAccept stores error messages in errorMap and renders them inline per invitation row. handleReject only calls showErrorToast on failure — if the rejection API call fails, the user has no per-item feedback and the invitation row stays on-screen without indication of what went wrong.

const handleReject = async (playerId: string): Promise<void> => {
  try { ... }
  catch (err) {
    // Missing: setErrorMap((prev) => ({ ...prev, [playerId]: getErrorMessage(err) }));
    showErrorToast(err, toast);
  }
};

Nits

ItemHeader and ItemFooter have identical class strings (flex basis-full items-center justify-between gap-2). If they're intentionally the same right now but semantically distinct, a comment explaining the distinction would help future contributors avoid collapsing them.

TeamItem test gap: No test for team?.name being undefined after loading completes (i.e., team fetch resolves but name is missing). The component would render an empty ItemTitle, which is silent. Worth a test case.


What's good

  • Compound component pattern and asChild polymorphism are correctly applied
  • Two-form convention (navigable asChild + Link vs. static Item + ItemActions) is well-documented and consistently followed across all 4 updated consumers
  • Axe accessibility tests on both forms — good practice
  • data-testid markers on structural skeleton elements
  • ActionFooterPattern story documents the invitations UX clearly
  • docs/architecture.md layer boundary table is a useful reference
  • LoadingCourt relocation into custom/court/index.tsx as a named export alongside Court is a clean colocation

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.

1 participant