CtrlK
BlogDocsLog inGet started
Tessl Logo

tessleng/ui

Implement frontend designs from figma using Chakra UI v3 and Storybook

92

1.64x
Quality

92%

Does it follow best practices?

Impact

92%

1.64x

Average score across 6 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

SKILL.mdskills/building-page-components/

name:
building-page-components
description:
Build page-level components that assemble composite and low-level components, handle data loading and mutations, and define page layout. Use when building a new page or route, wiring up data fetching, connecting mutations, or laying out a full page view. Use when user mentions "build a page", "create a route", "page component", "page layout", "wire up data loading", or asks to assemble existing components into a full page. Requires Figma MCP server connection if building from a design.
metadata:
{"mcp-server":"figma, figma-desktop"}

Building Page Components

Page components are the top-level assembly layer. They compose composite and low-level components, own data loading and mutations, and define layout — but contain no UI of their own. Any visual element (buttons, cards, forms, tables) should be a composite or low-level component, not inline JSX in the page.

Step 1: Check What Exists

  1. Check src/routes/ for existing pages with similar patterns (data loading, layout, mutations).
  2. Check composite components in src/components/ — the page should assemble these, not rebuild them.
  3. Check src/components/layout/PageLayout/ — all pages wrap content in <PageLayout>.

Step 2: Define the Page Structure

Before writing code, identify:

  • Route path — where does this page live in the URL hierarchy?
  • Data dependencies — what queries does this page need? What data must load before render?
  • Mutations — what actions can the user take that modify data?
  • Child components — which composite/low-level components does this page assemble?
  • Layout — how are child components arranged on the page?

What Belongs in a Page Component

Belongs hereDoes NOT belong here
Route definition and loaderStyled UI elements (buttons, cards, badges)
Data fetching via query optionsBusiness logic for rendering states
Mutations and cache invalidationReusable form fields
Layout with VStack/HStack/SimpleGridComplex conditional rendering trees
Feature flag checks in beforeLoadHardcoded visual styles
Breadcrumb configurationComponent-level state management

Step 3: Implement the Page

File Location

Pages live in src/routes/ following TanStack Router's file-based routing:

src/routes/_app/_authed/workspaces/$workspace/my-page/
├── index.tsx        # The page component
└── route.lazy.tsx   # Lazy-loaded route (if needed)

Data Loading Pattern

Use TanStack Router's loader with queryClient.ensureQueryData() to pre-fetch data before navigation. This prevents loading spinners on page transitions.

import { createFileRoute } from '@tanstack/react-router';

export const Route = createFileRoute('/_app/_authed/workspaces/$workspace/my-page/')({
  loader: async ({ context: { queryClient }, params: { workspace } }) => {
    const { data } = await queryClient.ensureQueryData(
      getMyDataQueryOptions(workspace)
    );
    return { items: data };
  },
  component: MyPage,
});

Access loaded data in the component:

function MyPage() {
  const { items } = Route.useLoaderData();
  // ...
}

Mutation Pattern

Use useMutation() with $api.mutationOptions(). Always invalidate related queries after a successful mutation.

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { $api } from '~/lib/api/client';

function MyPage() {
  const queryClient = useQueryClient();
  const { mutateAsync } = useMutation(
    $api.mutationOptions('post', '/api/path/{id}')
  );

  async function handleAction(id: string) {
    await mutateAsync({ params: { path: { id } }, body: { /* ... */ } });
    await queryClient.invalidateQueries({ queryKey: ['relevant-key'] });
  }
}

For form submissions, use useTesslForm which wraps mutations with automatic error handling:

import { useTesslForm } from '~/lib/form/use-tessl-form';
import { Form } from '~/lib/form/components/form';

const form = useTesslForm({
  defaultValues: { name: '' },
  onSubmit: async ({ value }) => {
    await mutateAsync({ body: value });
    await queryClient.invalidateQueries(/* ... */);
  },
});

Layout Pattern

Pages wrap all content in <PageLayout> and use Chakra layout primitives to arrange child components. The page itself should read like a table of contents — a list of composed components with layout between them.

import { VStack, HStack, SimpleGrid } from '@chakra-ui/react';
import { PageLayout } from '~/components/layout/PageLayout/PageLayout';

function MyPage() {
  const { items } = Route.useLoaderData();

  return (
    <PageLayout>
      <VStack gap="6" align="stretch">
        <MyPageHeader />
        <SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} gap="4">
          {items.map((item) => (
            <MyItemCard key={item.id} item={item} />
          ))}
        </SimpleGrid>
      </VStack>
    </PageLayout>
  );
}

Feature Flag Checks

Gate pages behind feature flags in beforeLoad:

beforeLoad: ({ context }) => {
  assertFeatureFlagEnabled(context, 'my-feature');
},

Breadcrumbs

Configure breadcrumbs in the loader return value — <PageLayout> renders them automatically from the route hierarchy.

Step 4: Validate Against Figma

When building from a Figma design, follow this comparison process:

Capture the Reference

  1. Get a Figma screenshot — use get_screenshot(fileKey, nodeId) from the Figma MCP server. This is the visual source of truth.
  2. Note the layout specifics — column counts, spacing between sections, responsive breakpoints, max-widths.

Build and Compare

  1. Start Storybook or the dev server: cd apps/frontend && bun run dev:server --port XXXX (random port 3100–3999).
  2. Navigate to the page in the browser.
  3. Take a Playwright screenshotplaywright-cli screenshot <url> — and compare side-by-side with the Figma screenshot.
  4. Check:
    • Layout matches — column arrangement, spacing, alignment
    • Component placement — correct composite components in the right positions
    • Responsive behaviour — resize viewport and verify breakpoint transitions
    • Empty states — what does the page look like with no data?
    • Loading states — verify no unexpected spinners (data should be pre-loaded via the route loader)

What to Verify at the Page Level

Page-level Figma comparison focuses on layout and composition, not individual component styling (that's verified at the composite/low-level tier):

  • Overall page width and padding match the design
  • Section spacing and gaps match
  • Grid column counts match at each breakpoint
  • Component ordering matches the design
  • Empty and error states are handled

Step 5: Testing Plan

Route-Level Tests

Test data loading and navigation behaviour:

// my-page.test.tsx
import { render, screen } from '~/test/utils';

test('loads and displays items', async () => {
  // Mock the API response
  // Render the route
  // Assert that composite components receive the correct data
});

What to Test at the Page Level

  • Data loading — the loader fetches the right data and passes it to child components
  • Mutations — user actions trigger the correct API calls and invalidate the right queries
  • Error handling — API errors display appropriately (via useTesslForm error handling or error boundaries)
  • Feature flags — gated pages redirect or show fallback when the flag is off
  • Empty states — the page handles zero items gracefully

What NOT to Test at the Page Level

  • Individual component rendering (tested in component stories/tests)
  • Design token compliance (tested at composite/low-level tier)
  • Form field validation (tested in form field stories)

Step 6: Final Checklist

Composition:

  • Page contains only layout primitives and composed components — no inline UI
  • All visual elements are composite or low-level components
  • Components receive data via props, not by fetching their own data

Data:

  • Route loader uses ensureQueryData for pre-fetching
  • Mutations invalidate the correct query keys
  • Form submissions use useTesslForm for automatic error handling
  • <form.SubmissionError /> included for form pages

Layout:

  • Page wraps content in <PageLayout>
  • Layout uses VStack/HStack/SimpleGrid — no custom CSS
  • Responsive breakpoints match the Figma design

Figma Comparison (if applicable):

  • Playwright screenshot matches Figma screenshot at desktop width
  • Responsive layout matches at tablet and mobile breakpoints
  • Empty and loading states match the design

skills

building-page-components

README.md

tile.json