本文へ移動
cccskills
無料GitHub で公開

efficient-tap-button-migration

Migrating custom icon buttons to TapButtons. Use when replacing a raw <button>, IconButton, or className-styled icon; migrating header, toolbar, or footer icons; lazy-splitting a button's highlight decoration; or on 'tap targets', 'efficiency update', 'lazy split', or making a small component production-ready.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md14.2 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Efficient TapButton Migration Pattern

The single source of truth for migrating any small icon button to the project's tap-target system — and what an "efficiency update", "efficiency refactor", or "lazy split" of a small UI component means here: the production-ready pattern below. Reference implementation: features/feedback/FeedbackButton.tsx + features/feedback/FeedbackHighlight.tsx. Read both before applying this skill to a new component.

The pattern has four pillars. All four must hold for the migration to be considered complete.


Pillar 1 — Tap-target hygiene

Rules

  • No surrounding spacing. Never wrap a TapButton in p-*, m-*, or gap-*. The invisible 44×44 outer ring already reserves space; adding more produces double-spacing.
  • No className for visuals. Variation goes through documented props: variant, tooltip, ariaLabel, bgColor, iconColor, hoverBgColor, activeBgColor. A className passthrough on a TapButton consumer is a code smell.
  • Pre-composed > primitive. Use BugTapButton, PlusTapButton, SearchTapButton, etc. from @ai-matrx/tap-target/buttons. Don't reach for the raw TapTargetButton + manual icon.
  • Tooltip auto-derives from ariaLabel. Set ariaLabel="Submit Feedback" and the tooltip mirrors. Pass tooltip="..." only to override. Pass tooltip={false} to opt out.
  • Anchor decorations to the visible 32×32 inner pill. Badges/dots/pings use top-1.5 right-1.5 (offset 6px from the 44×44 outer), not top-0 right-0.

THE PLACEMENT RULES (owner, 2026-10-02) — the guard enforces them

  • Glass only floats. Glass is see-through. It belongs only on a bar floating over moving content (sticky or fixed with the page scrolling behind it, like iOS Messages' header) or on a data-matrx-glass-plane. Everywhere else (a page, card, toast, dialog or table), pass variant="transparent" (or outline) and use TapTargetButtonGroup surface="solid". Never take the glass default without choosing it.
  • All glass or none. A glass button never sits beside a non-glass element in the same row.
  • The 3px half-gap. The box adds 3px of unseen space per side. Whatever sits beside the button (breadcrumb text, a field, a container's edge) adds its own 3px, so the visible gap is always 6px.

Mapping consumer className → variant

Old className intentNew variant
shell-glass … background, on a bar floating over scrolling contentvariant="glass" (only there — see the placement rules)
hover:bg-accent / hover-only backgroundvariant="transparent"
Solid filled button (e.g. primary CTA)variant="solid" + bgColor="bg-…"
Inside a TapTargetButtonGroupvariant="group"

Migration before/after

// ❌ Before — custom <button>, className overrides, p-2 padding
<button
  className="p-2 rounded-full hover:bg-accent transition-colors"
  aria-label="Submit Feedback"
  onClick={handleClick}
>
  <Bug className="w-4 h-4" />
</button>

// ✅ After — pre-composed TapButton, no className, props only
<BugTapButton
  variant="transparent"
  ariaLabel="Submit Feedback"
  onClick={handleClick}
/>

Suspense fallbacks must match the 44×44 outer

A TapButton's outer ring is 44×44 (h-11 w-11). If a parent lazy-loads the button via Suspense, its fallback must reserve the same dimensions or the row will shift on hydration.

// ❌ Before — sized to the old custom button (~32×32)
<Suspense fallback={<button className="p-2 opacity-30" disabled><Bug className="w-4 h-4" /></button>}>
  <FeedbackButton />
</Suspense>

// ✅ After — sized to the 44×44 tap target
<Suspense
  fallback={
    <span className="flex h-11 w-11 items-center justify-center opacity-30" aria-hidden="true">
      <Bug className="w-4 h-4" />
    </span>
  }
>
  <FeedbackButton />
</Suspense>

Pillar 2 — Lazy-bundle discipline

The default for a small button used across many routes: only the icon and click-dispatch ship in main. Heavy decoration (extra icons, animation classes, dismiss UI, persistence logic, redux actions used only by the decoration) goes in a sibling component loaded via next/dynamic.

Rules

  • Split when the lazy chunk is meaningfully heavier than the main chunk. A 2-line decoration with no extra deps shouldn't be split — the network round-trip and chunk bookkeeping cost more than they save. Split when the lazy code pulls extra lucide-react icons, animation logic, persistence calls, or substantial JSX.
  • Use next/dynamic({ ssr: false, loading: () => null }). No SSR (interactive-only), no fallback flicker.
  • Always include "use client" on the lazy file. Required even though it's only imported via next/dynamic from a client file.
  • Gate the dynamic render with cheap selectors. Wrap <Lazy /> in a redux/state boolean check so the chunk isn't fetched for users who'll never see it. The gate uses cheap selectors that already live in main; only the decoration's side effects (timers, dispatches) move to the lazy file.
  • Coordinate parent ↔ lazy child via a one-shot tick: number prop. When the parent click should trigger something in the lazy child (e.g. dismiss the highlight), pass dismissTick: number and increment on click; the lazy child watches it via useEffect. No callback refs, no event bus, no imperative handles, no context.
  • Co-locate redux actions with the chunk that dispatches them. Actions used by the always-rendered icon (e.g. the typed opener) stay in main. Actions used only by the decoration (e.g. setModulePreferences) live in the lazy file.

File layout

features/<feature>/
├── <Feature>Button.tsx        # main chunk: icon + click + gate + dynamic ref
└── <Feature>Highlight.tsx     # lazy chunk: decoration + persistence + extra icons

Reference: main file

// features/feedback/FeedbackButton.tsx
"use client";

import dynamic from "next/dynamic";
import { useCallback, useState } from "react";
import {
  BugTapButton,
  type TapButtonProps,
} from "@ai-matrx/tap-target/buttons";
import { useAppSelector } from "@/lib/redux/hooks";
import { useOpenFeedbackWindow } from "@/features/overlays/openers/feedbackDialog";

const FeedbackHighlight = dynamic(() => import("./FeedbackHighlight"), {
  ssr: false,
  loading: () => null,
});

type FeedbackButtonProps = Pick<TapButtonProps, "variant" | "tooltip">;

export default function FeedbackButton({
  variant = "glass",
  tooltip,
}: FeedbackButtonProps) {
  const openFeedback = useOpenFeedbackWindow();
  const userId = useAppSelector((s) => s.userAuth.id);
  const viewCount = useAppSelector(
    (s) => s.userPreferences.system.feedbackFeatureViewCount,
  );
  const prefsLoaded = useAppSelector(
    (s) => s.userPreferences._meta.loadedPreferences !== null,
  );
  const [dismissTick, setDismissTick] = useState(0);

  const shouldShowHighlight = !!userId && prefsLoaded && viewCount < 5;

  const handleClick = useCallback(() => {
    if (shouldShowHighlight) setDismissTick((n) => n + 1);
    openFeedback();
  }, [openFeedback, shouldShowHighlight]);

  return (
    <div className="relative">
      <BugTapButton
        variant={variant}
        ariaLabel="Submit Feedback"
        tooltip={tooltip}
        onClick={handleClick}
      />
      {shouldShowHighlight && <FeedbackHighlight dismissTick={dismissTick} />}
    </div>
  );
}

The lazy file (FeedbackHighlight.tsx) is a regular client component — see the reference file for the full structure.


Pillar 3 — Type ownership

A type is defined once, by its OWNER, and imported everywhere else. This is non-negotiable.

Rules

  • Never duplicate a type that already exists. Even if it's a 4-member union you "happen to know."
  • If the owner doesn't export the type, export it from the owner. Don't fork a private copy. Add the export keyword to the owner's file in the same change.
  • Use Pick<OwnerType, ...> to derive narrow subsets when a wrapper forwards a few of many props.
  • Use OwnerType["fieldName"] to extract a single field's type when you don't need a separate alias.

Forbidden

// ❌ BAD — local fake type duplicating the owner's union
type TapVariant = "glass" | "transparent" | "solid" | "group";

interface MyButtonProps {
  variant?: TapVariant;
  tooltip?: string | false;
}

Correct

// ✅ GOOD — derived from the canonical source
import type { TapButtonProps } from "@ai-matrx/tap-target/buttons";

type MyButtonProps = Pick<TapButtonProps, "variant" | "tooltip">;

If the owner doesn't yet export the type, add the export to the owner file in the same change. Example:

// @ai-matrx/tap-target/buttons — owner
export interface TapButtonProps {
  variant?: Variant;
  // ...
}

Pillar 4 — Opening a dialog from the button

Use the overlay's typed opener — useOpenX() from features/overlays/openers/<overlayId>.tsx (~210 openers, ~520 call sites). Never dispatch(openOverlay(...)) in new or migrated code; the ~115 raw dispatch sites left are legacy and get migrated when you touch them. The one owner of this rule is the overlay-system skill — read it for callbacks, declarative controllers, and adding a new overlay.

// ❌ Raw dispatch — untyped data, callers must know the id string
dispatch(openOverlay({ overlayId: "feedbackDialog" }));

// ✅ Typed opener — options are type-checked, close() handle returned
import { useOpenFeedbackWindow } from "@/features/overlays/openers/feedbackDialog";
const openFeedback = useOpenFeedbackWindow();
openFeedback();

Migration checklist

Copy this checklist when applying the pattern to a new button. Tick each item before declaring the migration complete:

- [ ] 1. Identify the canonical TapButton owner type (TapButtonProps from @ai-matrx/tap-target/buttons)
- [ ] 2. Replace the legacy <button> / <IconButton> / className-styled element with a pre-composed TapButton (BugTapButton, PlusTapButton, etc.)
- [ ] 3. Drop ALL className props on the TapButton itself. Use variant= instead.
- [ ] 4. Set ariaLabel; let tooltip auto-derive (or pass explicitly).
- [ ] 5. Resize any matching Suspense fallbacks to flex h-11 w-11.
- [ ] 6. Anchor any badges/dots/pings to top-1.5 right-1.5 (visible inner pill).
- [ ] 7. Identify heavy decoration (extra lucide icons, animations, persistence logic, dedicated useEffects) and move it to a sibling file.
- [ ] 8. Wrap the lazy file in next/dynamic({ ssr: false, loading: () => null }).
- [ ] 9. Add "use client" to the lazy file.
- [ ] 10. Gate the lazy render with cheap redux/state selectors so the chunk doesn't fetch for users who won't see it.
- [ ] 11. Coordinate parent → lazy child via a tick: number prop, never callbacks/refs/context.
- [ ] 12. Move redux actions used only by the decoration into the lazy file.
- [ ] 13. Replace any local type aliases with Pick<OwnerType, ...> or OwnerType["field"] imports. If the owner doesn't export the type, export it from the owner in the same change.
- [ ] 14. Open any dialog through its typed opener (`useOpenX()` — see `overlay-system`); replace any `dispatch(openOverlay(...))` or legacy `openSomethingDialog()` wrapper you touch.
- [ ] 15. Audit ALL consumers (grep for the component name) and update each call site in the same change. Update Suspense fallbacks in those consumers too.

Reference implementation

Read these before applying the pattern to a new component:

  • features/feedback/FeedbackButton.tsx — main chunk: icon + typed opener + gate + dynamic ref + Pick<>-derived type.
  • features/feedback/FeedbackHighlight.tsx — lazy chunk: PartyPopper + X icons, dismiss button, view-count auto-increment timer, setModulePreferences dispatch.
  • @ai-matrx/tap-target/buttons (package) — owner of TapButtonProps. Pre-composed buttons (BugTapButton, PlusTapButton, etc.) and the Wrap variant resolver.
  • @ai-matrx/tap-target (package) — primitive (TapTargetButton, TapTargetButtonTransparent, TapTargetButtonSolid, TapTargetButtonForGroup, TapTargetButtonGroup). Don't import directly unless a pre-composed version doesn't exist; add a new pre-composed export instead.
  • components/icons/README.md — definitive doc on the spacing rule and the Wrap helper for adding new pre-composed buttons.
  • app/(dev)/demos/button-demo/page.dev.tsx — live demo of every variant, group, and AI brand button.
  • features/overlays/openers/ — the typed openers (owner: overlay-system).

The migration of FeedbackButton's consumers (components/layout/new-layout/DesktopLayout.tsx, features/public-chat/components/ChatMobileHeader.tsx, components/matrx/PublicHeaderFeedback.tsx) is the canonical example of consumer-side rules in action — read those diffs to see how className=... becomes variant=... and how Suspense fallbacks are resized.


Anti-patterns

  • ❌ Wrapping a TapButton in a <div className="p-2"> to add space.
  • ❌ Passing className="hover:bg-accent rounded-full transition-colors" to a TapButton "for theming."
  • ❌ Importing TapTargetButton directly when a BugTapButton / PlusTapButton already exists.
  • ❌ Re-declaring type TapVariant = "glass" | "transparent" | ... instead of importing TapButtonProps from the owner.
  • ❌ Splitting a 2-line decoration into a lazy chunk just because lazy is "good practice."
  • ❌ Forgetting "use client" on the lazy file.
  • ❌ Using a callback ref, custom event, or context for parent ↔ lazy-child coordination.
  • ❌ Opening a dialog with dispatch(openOverlay(...)) or a legacy openXDialog() wrapper in new code — use the typed opener (useOpenX()).
  • ❌ Inventing a new typed wrapper for an overlay that takes zero or trivial params.
  • ❌ Leaving Suspense fallbacks at the old button's dimensions after the underlying button grows to 44×44.
  • ❌ Updating only the focused file's call site and forgetting the other consumers.
  • ❌ Forking a private type from the owner instead of export-ing it from the owner.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Admin debug system wiring for the floating AdminIndicator's live debug data and Copy Full Context. Use when adding debug visibility to a route or feature, wiring useDebugContext or the debug panel, capturing console errors, or enabling the copy-context workflow for a page.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

Compact two-icon Copy / Copy-for-AI controls (components/agent-copy). Use when adding copy buttons to a row, card, list, or record; merging duplicate Copy/JSON/AI controls; continuing the copy rollout; or writing a Copy-for-AI payload. NOT for markdown content actions (use rich-document-actions).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

Disclosing a surface's existing fixed AI jobs in the shell's top Agents menu. Use when a page, panel, overlay, or window already runs a mandate behind a button, assist, automatic action, or mode; when the agent-disclosure guard names a file; or during a surface check.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

The watch-fix-rerun method for making an existing platform agent or automated process efficient and correct: baseline its ledger, run one unit yourself, fix the class behind every wasted call, rerun, record. Use when asked to improve, optimize, watch, or 'make efficient' an agent, a sandbox session, a sync job, or any recurring automated process.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

Redux state for agent execution and firing agent shortcuts. Use when editing features/agents/redux/, building agent UI, creating a conversation, touching assembleRequest or NDJSON stream state, wiring agent-state selectors, adding a per-conversation capability, or triggering a shortcut from a button, menu, or code (useShortcutTrigger, launchAgentExecution).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

The provision for a call site: the exhaustive menu of values that place in the code can realistically produce. Use before creating or fixing a mandate or agent that reasons about more than its own input, when deciding whether an agent can answer at all with what it is sent, or when its output 'looks right' but nobody checked it could be. NOT for building the agent (use create-agent).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

armanisadeghi のスキルをすべて見る

このスキルの問題を報告する