v1.0

AccountCell

Preview

Basic

Loading…

Preview

Code

ts
import AccountCell from '@/components/table/AccountCell';

src/components/table/AccountCell.tsx

AI prompt

text
Build a two-line account identity table cell component in React + TypeScript + Tailwind CSS.

## Look
- It renders a fragment that goes INSIDE the caller's `<td>`. It has no padding of its own: row height belongs to the table (one table may use `py-3`, another `py-2`), not to the cell.
- Line 1, the username: `font-mono leading-tight text-slate-800 dark:text-slate-100`.
- Line 2, `mt-1 text-[10px] leading-tight text-slate-400`: the group (`cluster`) in `font-semibold text-slate-600 dark:text-slate-300`, then " · ", then the category (`brand`) in plain slate-400.
- Group first, category second. The group is what rows are sorted and followed up by, and the category only narrows things down inside it.

## Behaviour
- Either part of line 2 may be null or missing. Show only the parts that exist, with no separator left hanging. When both are missing, leave line 2 out.
- No state and no handlers, so no `'use client'`. It then renders in both server-rendered and client tables, and two tables can show the same column with exactly the same markup.

## API
- `username: string`, `cluster?: string | null`, `brand?: string | null`.

## Demo
A one-column "Person" table with three rows. Username is the email, cluster is the team, brand is the role: `ada@example.com` / **Engineering** · Administrator, `grace@example.com` / **Engineering** · Maintainer, `alan@example.com` / **Research** · Maintainer.

## House style (applies to everything above)
- Stack: React 19 + TypeScript + Tailwind CSS v4, icons from lucide-react. One self-contained file; default-export the component and named-export its types. `'use client'` if it has state, refs or handlers.
- Font Inter; palette indigo on slate. Primary accent indigo-600 (hover indigo-700, dark mode indigo-400). Body text slate-700 / dark slate-200; secondary slate-500 / dark slate-400.
- Dark mode is a `.dark` class on <html> (not prefers-color-scheme). Every colour needs its `dark:` pair.
- Compact admin scale: text-xs (12px) for controls and body, 10–11px for meta, rounded-lg (8px) controls, rounded-2xl (16px) cards.
- Card surface ("panel"): `bg-white/60 dark:bg-slate-800/60 backdrop-blur-xl border border-white/60 dark:border-slate-700/60 rounded-2xl shadow-lg`, on a soft slate gradient page background.
- Floating surfaces (dropdowns, popovers, menus) are OPAQUE: `bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-2xl shadow-lg`, no backdrop blur (it creates a stacking context that traps the popover's z-index). In-flow popovers are z-50; portalled overlays z-200.
- Text inputs and select triggers: `w-full px-3 py-2 text-xs rounded-lg border border-slate-300 dark:border-slate-700 bg-white/80 dark:bg-slate-900/60 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/40 focus:border-indigo-500`.
- Field labels: 11px semibold slate-600. Section titles: 10px semibold uppercase wide-tracking slate-500.
- Primary button: indigo-600 fill, white 12px semibold text, rounded-lg, px-3 py-2, disabled at 50% opacity. Ghost button: slate-600 text, hover slate-100.
- Popovers close on outside click AND on Escape (listen to both; include the portalled panel's element in the outside-click check).
- Don't nest scroll containers around popovers: an ancestor with overflow hidden/auto clips an absolutely-positioned dropdown. Portal the panel to <body> when it must escape a scroller, and reposition it on scroll and resize.
- Accessible by default: visible focus rings, keyboard support that matches the WAI-ARIA pattern for the widget, `aria-label` on icon-only buttons, `min-w-0` so text truncates instead of overflowing.

Source

tsx
/* Origin: bonus-adjustment (96S2), verbatim. */
/**
 * The player account a row is for, as one table cell: the username, with the
 * back office and brand it resolves to underneath.
 *
 * Extracted from the Approvals list so the Individual drafts list can show the
 * SAME thing rather than an imitation of it. Two copies of this markup drifted
 * apart once already — the drafts list showed a bare cluster code where the
 * approvals list showed the account — and one component is the only way the two
 * tables can be read as the same column.
 *
 * BO first, brand second: the back office is what a release is grouped and
 * chased by, and the brand only narrows within it.
 *
 * No `'use client'`. Both callers need it — Approvals is a server component and
 * DraftList is a client one — and a component with no state or handlers renders
 * in either tree.
 *
 * The padding stays on the caller's `<td>`: the two tables are on different row
 * heights (py-3 against py-2) and that is a property of the table, not of this.
 */
export default function AccountCell({
  username,
  cluster,
  brand,
}: {
  username: string;
  /**
   * Null only where the target is not known yet. On an `AdjustmentItem` both are
   * NOT NULL — a released row always has a resolved brand — but a draft row with
   * an unrecognised `gamePrefix` has neither, and the sub-line then says nothing
   * instead of printing a lone separator.
   */
  cluster?: string | null;
  brand?: string | null;
}) {
  return (
    <>
      <div className="font-mono leading-tight text-slate-800 dark:text-slate-100">{username}</div>
      {(cluster || brand) && (
        <div className="mt-1 text-[10px] leading-tight text-slate-400">
          {cluster && (
            <span className="font-semibold text-slate-600 dark:text-slate-300">{cluster}</span>
          )}
          {cluster && brand && ' · '}
          {brand}
        </div>
      )}
    </>
  );
}

Props

PropTypeDefaultDescription
username*string—
clusterstring | null—Null only where the target is not known yet. On an `AdjustmentItem` both are NOT NULL — a released row always has a resolved brand — but a draft row with an unrecognised `gamePrefix` has neither, and the sub-line then says nothing instead of printing a lone separator.
brandstring | null—