agentsclimarketplace

Frontend component style

Skill arndvs/ctrlshft/skills/frontend-component-style

Frontend component file structure, naming, and layer separation for new or refactored components.From its SKILL.md

Install
npx -y skills add arndvs/ctrlshft --skill frontend-component-style

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

13.2 KB, ~3.3k tokens by cl100k_base, as published. Nobody here has run it

Frontend Component Style

If running interactively (human present), output "Read Frontend Component Style skill." to acknowledge. If running with --dangerously-skip-permissions (AFK/unattended), skip acknowledgement and proceed directly.

When to use

Use when CREATING a new component, REFACTORING an existing one, SPLITTING a large file, EXTRACTING data or logic, or DECIDING where a piece of code should live. Triggers on phrases like 'build a component', 'scaffold this', 'sketch a layout', 'prototype this', 'extract this into', 'split this component', 'is this file too big', 'where should this live', 'promote to production'. Do NOT use for small edits, bug fixes, or style tweaks — path-gated rules cover those.

This skill answers two structural questions: where does each piece of code live, and what is each piece named (including the names of related types and interfaces). Styling and runtime concerns (Tailwind tokens, dark-mode variants, server/client split, animation) and accessibility requirements (ARIA semantics, keyboard navigation, focus management, reduced-motion handling, CLS-safe variants) are owned by the relevant path-gated rules listed at the bottom — trust them; don't duplicate, but do preserve those requirements when creating or refactoring components. TypeScript typing patterns themselves remain in rules/typescript-conventions.md.


Step 1 — Determine the mode

Single self-contained TSX files and four-layer split files are opposite structures. Always pick mode FIRST.

Detection priority

  1. Explicit word in user request

    • "prototype" / "sketch" / "draft" / "throwaway" / "experiment" / "mock up" → Prototype
    • "production" / "ship" / "real" / "extract" / "refactor" / "promote" → Production
  2. Context signals (only if no explicit word)

    • File is in prototypes/, sandbox/, experiments/, demos/ → Prototype
    • File is in app/, src/components/, src/features/, lib/ → Production
    • Repo already has separated *-content.ts / format-*.ts files → Production
    • Repo has only inline-JSON single-file components → Prototype
  3. Ask (if neither signal is decisive — DO NOT GUESS)

    "Is this a prototype/sketch (single file with inline data) or production code (separated into data, logic, primitives, composed)?"

Recording the choice

The first reply after invoking this skill MUST start with one line:

Mode: prototype

or

Mode: production

The choice persists for the rest of the conversation. The user overrides with one word ("make it production", "this is a prototype actually").


Mode: Prototype

When to use: sketching an idea, exploring a layout, throwaway experiments, components destined for CMS handoff where speed matters more than long-term maintainability.

File structure

  • One self-contained .tsx file
  • All content data in a single componentData JSON object at the top
  • Helper functions inline below the data
  • Subcomponents nested inside the same file
// user-dashboard-card.tsx

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

// ===== Component Data =====
const componentData = {
  title: "User Dashboard",
  metrics: [
    { id: "users", label: "Active Users", value: "5,234", trend: "+12%" },
    { id: "revenue", label: "Monthly Revenue", value: "$12,345", trend: "+8%" },
  ],
};

// ===== Helper Functions =====
const formatTrend = (trend: string) => {
  const isPositive = trend.startsWith("+");
  return {
    value: trend,
    className: isPositive ? "text-green-500" : "text-red-500",
    icon: isPositive ? "↑" : "↓",
  };
};

// ===== Component =====
const UserDashboardCard = () => (
  <Card>
    <CardHeader><CardTitle>{componentData.title}</CardTitle></CardHeader>
    <CardContent>
      {componentData.metrics.map((metric) => (
        <MetricRow key={metric.id} {...metric} />
      ))}
    </CardContent>
  </Card>
);

// ===== Subcomponents =====
interface MetricRowProps {
  label: string;
  value: string;
  trend: string;
}

const MetricRow = ({ label, value, trend }: MetricRowProps) => {
  const trendData = formatTrend(trend);
  return (
    <div>
      <span>{label}</span>
      <span>{value}</span>
      <span className={trendData.className}>{trendData.icon} {trendData.value}</span>
    </div>
  );
};

export default UserDashboardCard;

Prototype guardrails

  • If a prototype crosses ~250 lines or has 4+ subcomponents, mention it. Suggest promoting to production. Do not auto-promote.
  • If an external dataset is already imported in the file, use it — don't refactor the data shape just to match the inline-JSON convention.

Mode: Production — The Four Layers

When to use: code going into the main app, components that will be tested, reused, or maintained by others.

The four layers

LayerJobFile suffixExample
DataStatic content, CMS text, config*-content.tsdashboard-metrics-content.ts
LogicPure functions, formatters, transformersverb-noun .tsformat-metric-trend.ts
PrimitiveSingle-element UI, no internal statedescriptive .tsxtrend-badge.tsx
ComposedAssembles primitives into a sectiondescriptive .tsxmetric-card.tsx

Dependency arrow (one direction only)

Page-level view → Composed → Primitive → Logic + Data
  • Data files import nothing local; export typed content
  • Logic files import nothing local; export pure functions
  • Primitives may import Logic + types and shared UI primitives (e.g. components/ui/*, framework helpers); never import other feature Primitives or Composed components
  • Composed imports feature Primitives + types (and shared UI primitives if needed); owns no formatting
  • Page-level views import Composed + Data; no logic, no formatting

What it looks like

// dashboard-metrics-content.ts
export type DashboardMetric = {
  id: string;
  label: string;
  value: string;
  trend: number;
};

export const dashboardMetrics: DashboardMetric[] = [
  { id: "active-users", label: "Active Users", value: "5,234", trend: 12 },
  { id: "monthly-revenue", label: "Monthly Revenue", value: "$12,345", trend: 8 },
];
// format-metric-trend.ts
export type TrendDirection = "up" | "down" | "flat";

export const getTrendDirection = (trend: number): TrendDirection => {
  if (trend > 0) return "up";
  if (trend < 0) return "down";
  return "flat";
};

export const formatTrendLabel = (trend: number): string =>
  trend === 0 ? "No change" : `${trend > 0 ? "+" : ""}${trend}%`;
// trend-badge.tsx
import { getTrendDirection, formatTrendLabel } from "@/lib/format-metric-trend";

interface TrendBadgeProps {
  trend: number;
}

const TrendBadge = ({ trend }: TrendBadgeProps) => {
  const direction = getTrendDirection(trend);
  return <span data-direction={direction}>{formatTrendLabel(trend)}</span>;
};

export default TrendBadge;
// metric-card.tsx
import { Card, CardContent } from "@/components/ui/card";
import TrendBadge from "@/components/trend-badge";
import type { DashboardMetric } from "@/content/dashboard-metrics-content";

interface MetricCardProps {
  metric: DashboardMetric;
}

const MetricCard = ({ metric }: MetricCardProps) => (
  <Card>
    <CardContent>
      <span>{metric.label}</span>
      <span>{metric.value}</span>
      <TrendBadge trend={metric.trend} />
    </CardContent>
  </Card>
);

export default MetricCard;
// dashboard-metrics-section.tsx (page-level view)
import { dashboardMetrics } from "@/content/dashboard-metrics-content";
import MetricCard from "@/components/metric-card";

const DashboardMetricsSection = () => (
  <section>
    {dashboardMetrics.map((metric) => (
      <MetricCard key={metric.id} metric={metric} />
    ))}
  </section>
);

export default DashboardMetricsSection;

Production guardrails — when NOT to split

The four-layer rule is a constraint on dependencies, not a minimum file count. A 30-line component that passes the SRP test stays in one file. Don't extract:

  • A formatter used in exactly one place that's three lines long
  • A primitive used in exactly one place that doesn't need its own tests
  • An EmptyState that appears exactly once and has no logic

Extract when there is a second consumer, non-trivial logic, or a real testing need.


Naming (both modes)

Files

  • kebab-case always
  • .tsx for components, .ts for logic and data
  • Named after what they render or do — never after where they live or how they're used
  • No generic names: card.tsx, utils.ts, helpers.ts, mgr.ts, widget.tsx
  • No abbreviations unless universally understood (url, id, api)

Components

  • Specific noun phrase: MetricTrendBadge, InvoiceLineItemRow, PlanUpgradeCallout
  • A reviewer scanning a file tree should know what each component renders without opening the file
  • Forbidden: Card, Item, Widget, Section, DisplayComponent

Functions

  • Returns, derives, formats, or builds a value → verb-led value name: formatCurrency, getTrendDirection, buildInvoiceRows
  • Performs a side effect or user/system action → verb phrase: handlePlanUpgrade, submitBillingForm, downloadInvoicePdf
  • Event handlers name the action, not the event: handlePlanUpgrade not handleClick, handleInvoiceDownload not handleSubmit

Types and interfaces

  • Props interface = <ComponentName>Props
  • Types named after the thing: TrendDirection, PlanTier, InvoiceStatus
  • Forbidden: Props (collides), ICard (Hungarian), T (meaningless)

File layout (within every file, both modes)

1. Imports
2. Types and interfaces
3. Content data (only if this file owns data)
4. Helper functions / hooks (Prototype mode only)
5. Component or function body
6. Subcomponents (Prototype mode only)
7. Default export

A reviewer should always know where to look for each kind of thing.


SRP test (both modes)

Describe what this does in one sentence without using "and".

  • ✅ "Displays a metric value with a trend indicator"
  • ✅ "Renders a list of invoice line items"
  • ❌ "Shows the metric, handles the click, and formats the trend" → split into three

If you can't, split.


Cross-cutting guardrails (both modes)

  • Don't change website copy unless told to. This applies in Prototype and Production alike, including refactors and file splits.

Anti-patterns (both modes)

PatternProblemFix
const data = { ... } inline in production codeHides content inside presentationMove to <feature>-content.ts
utils.ts with many unrelated functionsImpossible to navigateOne function (or one tightly related group) per file, named after what it does
<Card /> as a component nameNo hint of what it renders<UserBillingCard />, <MetricSummaryCard />
handleClick / handleSubmit on a specific componentNo hint of what is being acted onhandlePlanUpgrade, handleInvoiceDownload
Formatting logic inside JSXMixes concerns, hard to testExtract to a named function in a logic file
Mixed isLoading / isError / isEmpty in one render blockTangled conditional logicEach state gets its own named branch or component
Production component with inline JSON dataShould have been promotedRun the Promote workflow below

Promote: prototype → production

Triggered by phrases like "promote to production", "clean this up", "extract this properly", "this is going live".

  1. Extract content — move the inline componentData object to <feature>-content.ts with a typed export.
  2. Extract logic — move formatting/transformation functions to <verb>-<noun>.ts files (e.g. format-metric-trend.ts).
  3. Extract subcomponents — each nested subcomponent becomes its own file, named after what it renders.
  4. Re-aim the original file — it becomes a Composed or page-level view that imports content + primitives only. No formatting, no inline data.
  5. Apply path-gated rules — server/client split, dark-mode tokens, Tailwind grouping etc. (these auto-load when you edit the new files).
  6. Update mode — record Mode: production in your next reply so the rest of the conversation uses production rules.

Path-gated rules already in effect

These auto-load when you edit matching files. Do not duplicate their content here. Trust them.

RuleScope
rules/dark-mode.md**/*.{tsx,jsx,css,scss} — DMDS tokens, dark variants
rules/tailwind-shadcn.md**/*.{tsx,jsx} — Tailwind grouping, shadcn imports, responsive
rules/server-vs-client-components.md**/app/**/*.{tsx,jsx} — server-first, error handling
rules/framer-motion.md**/*.{tsx,jsx} — animation philosophy, reduced motion
rules/typescript-conventions.md**/*.{ts,tsx} — props/types, parameter style
rules/frontend-conventions.md**/*.{ts,tsx,js,jsx,mjs,cjs,css,scss,html,svelte,vue} — browser baseline

If a question is covered by a path-gated rule (Tailwind syntax, dark-mode tokens, when to use 'use client'), defer to the rule. This skill answers structure and naming only.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,512. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.