Back to list
linsomniac

react-mobile

by linsomniac

Lightweight mail server for development use, including web and REST interfaces.

0🍴 0📅 Jan 17, 2026

SKILL.md


name: react-mobile description: Mobile-first React component development with Tailwind CSS, TanStack Query, and accessibility. Use when creating React components, pages, or hooks. allowed-tools: Read, Edit, Bash, Grep, Glob

Mobile-First React Development

This skill guides development of React components for the E-Signature Platform.

Component Implementation Checklist

  1. Mobile-first CSS: Use Tailwind's mobile-first breakpoints (sm:, md:, lg:)
  2. Touch targets: Minimum 44px for all interactive elements
  3. Loading states: Use Skeleton components, never spinners for content
  4. Empty states: Always handle empty data with friendly messages
  5. Error boundaries: Wrap complex components
  6. Accessibility: aria-labels, proper semantics, keyboard navigation

Touch Target Standards

// GOOD: Large touch target (44px minimum)
<button className="min-h-[44px] min-w-[44px] p-3 flex items-center justify-center">
  <PlusIcon className="h-6 w-6" />
</button>

// BAD: Small touch target
<button className="p-1">
  <PlusIcon className="h-4 w-4" />
</button>

// GOOD: List item with adequate touch area
<li className="py-3 px-4 min-h-[48px] flex items-center">
  {content}
</li>

Loading State Patterns

// GOOD: Skeleton loading
function EnvelopeList() {
  const { data: envelopes, isLoading } = useEnvelopes();

  if (isLoading) {
    return (
      <div className="space-y-4">
        {[1, 2, 3].map((i) => (
          <Skeleton key={i} className="h-24 rounded-lg" />
        ))}
      </div>
    );
  }

  return <>{envelopes.map(env => <EnvelopeCard key={env.id} envelope={env} />)}</>;
}

// BAD: Spinner for content loading
function EnvelopeList() {
  if (isLoading) return <Spinner />;  // Don't do this
}

Empty State Pattern

function EnvelopeList() {
  const { data: envelopes } = useEnvelopes();

  if (envelopes.length === 0) {
    return (
      <EmptyState
        icon={<FileTextIcon />}
        title="No envelopes yet"
        description="Create your first envelope to get started"
        action={
          <Button onClick={openCreateEnvelope}>
            Create Envelope
          </Button>
        }
      />
    );
  }

  return <>{/* envelope list */}</>;
}

TanStack Query Patterns

Basic Query

import { useQuery } from '@tanstack/react-query';
import { api } from '@/api/client';

function useEnvelopes(status?: string) {
  return useQuery({
    queryKey: ['envelopes', status],
    queryFn: () => api.envelopes.list({ status }),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

Mutation with Optimistic Updates

import { useMutation, useQueryClient } from '@tanstack/react-query';

function useCreateEnvelope() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: api.envelopes.create,

    // Optimistic update
    onMutate: async (newEnvelope) => {
      // Cancel outgoing queries
      await queryClient.cancelQueries({ queryKey: ['envelopes'] });

      // Snapshot previous value
      const previous = queryClient.getQueryData(['envelopes']);

      // Optimistically update
      queryClient.setQueryData(['envelopes'], (old: Envelope[]) => [
        { ...newEnvelope, id: 'temp-' + Date.now(), status: 'draft' },
        ...old,
      ]);

      return { previous };
    },

    // Rollback on error
    onError: (err, newEnvelope, context) => {
      queryClient.setQueryData(['envelopes'], context?.previous);
      toast.error('Failed to create envelope');
    },

    // Refetch on success
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['envelopes'] });
    },
  });
}

Infinite Query (for lists)

function useEnvelopesInfinite(status?: string) {
  return useInfiniteQuery({
    queryKey: ['envelopes', status],
    queryFn: ({ pageParam }) => api.envelopes.list({ status, cursor: pageParam }),
    getNextPageParam: (lastPage) => lastPage.next_cursor,
    initialPageParam: undefined,
  });
}

Component Structure

// components/envelopes/EnvelopeCard.tsx
import { type Envelope } from '@/types';
import { formatDate } from '@/utils/formatters';

interface EnvelopeCardProps {
  envelope: Envelope;
  onEdit?: () => void;
}

export function EnvelopeCard({ envelope, onEdit }: EnvelopeCardProps) {
  return (
    <div
      className="bg-white rounded-lg shadow-sm p-4 min-h-[80px]"
      role="article"
      aria-label={`${envelope.name}, ${envelope.status}`}
    >
      <div className="flex items-center justify-between">
        <div>
          <h3 className="font-medium">{envelope.name}</h3>
          <p className="text-sm text-gray-500">{formatDate(envelope.created_at)}</p>
        </div>
        <StatusBadge status={envelope.status} />
      </div>

      {onEdit && (
        <button
          onClick={onEdit}
          className="mt-2 min-h-[44px] w-full text-center text-blue-600"
          aria-label="Edit envelope"
        >
          Edit
        </button>
      )}
    </div>
  );
}

Mobile Navigation Pattern

// components/layout/BottomNav.tsx
const NAV_ITEMS = [
  { path: '/dashboard', icon: HomeIcon, label: 'Home' },
  { path: '/envelopes', icon: FileTextIcon, label: 'Envelopes' },
  { path: '/templates', icon: LayersIcon, label: 'Templates' },
  { path: '/settings', icon: SettingsIcon, label: 'Settings' },
];

export function BottomNav() {
  const location = useLocation();

  return (
    <nav className="fixed bottom-0 left-0 right-0 bg-white border-t safe-area-pb">
      <div className="flex justify-around">
        {NAV_ITEMS.map(({ path, icon: Icon, label }) => (
          <Link
            key={path}
            to={path}
            className={cn(
              "flex flex-col items-center py-2 px-4 min-w-[64px] min-h-[56px]",
              location.pathname === path ? "text-blue-600" : "text-gray-500"
            )}
            aria-current={location.pathname === path ? 'page' : undefined}
          >
            <Icon className="h-6 w-6" />
            <span className="text-xs mt-1">{label}</span>
          </Link>
        ))}
      </div>
    </nav>
  );
}

Form Pattern (React Hook Form)

import { useForm } from 'react-hook-form';

interface EnvelopeFormData {
  name: string;
  message?: string;
  expires_at?: string;
}

function CreateEnvelopeForm({ onSubmit }: { onSubmit: (data: EnvelopeFormData) => void }) {
  const { register, handleSubmit, formState: { errors } } = useForm<EnvelopeFormData>();

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <label htmlFor="name" className="block text-sm font-medium">
          Envelope Name
        </label>
        <input
          id="name"
          {...register('name', { required: 'Name is required', maxLength: 255 })}
          className="mt-1 w-full rounded-md border p-3 min-h-[44px]"
          aria-invalid={errors.name ? 'true' : 'false'}
        />
        {errors.name && (
          <p className="mt-1 text-sm text-red-600" role="alert">
            {errors.name.message}
          </p>
        )}
      </div>

      <button
        type="submit"
        className="w-full bg-blue-600 text-white rounded-lg py-3 min-h-[44px] font-medium"
      >
        Create Envelope
      </button>
    </form>
  );
}

After Implementation

# Lint check
npm run lint

# Type check
npm run type-check

# Run component tests
npm run test -- --watch

File Organization

src/
├── components/
│   ├── ui/           # Base components (Button, Input, Modal)
│   ├── layout/       # App shell (BottomNav, Header)
│   ├── auth/         # Auth components
│   ├── envelopes/    # Envelope-specific components
│   ├── documents/    # Document viewer, upload
│   ├── fields/       # Field placement components
│   ├── signing/      # Signing flow components
│   └── templates/    # Template components
├── pages/            # Route components
├── hooks/            # Custom hooks (useEnvelopes, useAuth, useSigning)
├── api/              # API client and types
├── contexts/         # React contexts
└── utils/            # Formatters, validators

Score

Total Score

60/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon