// Auto-generated from src/ai-service/skills/system/superblocks-frontend/SKILL.md
// Do not edit directly - edit the .md file instead
export const content = `---
name: superblocks-frontend
description: |
  Build frontend UI using React, Tailwind CSS, and Superblocks components. Essential for connecting Superblocks UIs with APIs.
  Use when creating pages, components, handling user interactions, or working with the design system.
readOnly: true
metadata:
  author: superblocks
  version: "1.0"
---

# Superblocks Frontend Development

This skill covers building frontend UI for Superblocks applications using React, Tailwind CSS v4, and the Superblocks component library.

## Platform Overview

This is a React-based web application platform. Use standard React patterns:

- \`useState\`, \`useEffect\`, \`useCallback\`, \`useMemo\`
- Event handlers and controlled components
- JSX with Tailwind CSS classes

## Using APIs from Frontend

Three primitives are available from \`@superblocksteam/library\`:

- **\`useApiData\`** — declarative reads with SWR caching (preferred for data loading)
- **\`useApi\`** — imperative mutations (for event-driven actions)
- **\`executeApi\`** — plain Promise outside React (utility functions, event handlers without hooks)

**Data loading** — auto-fetches on mount and when inputs change (preferred for reads):

\`\`\`typescript
import { useApiData } from "@superblocksteam/library";

// Auto-fetches when email or name changes. No useEffect or HMR guards needed.
// IMPORTANT: Always use \`fetching\` to show a loading indicator during refetches.
const { data, loading, fetching, isError, error } = useApiData("GetUsers", {
  email,
  name,
});
\`\`\`

**Mutations** — call \`run()\` manually (for event-driven actions like form submissions):

\`\`\`typescript
import { useApi } from "@superblocksteam/library";
import { toast } from "sonner";

const { run: createOrder, loading } = useApi("CreateOrder");

const handleSubmit = useCallback(async () => {
  try {
    await createOrder({ item, qty });
  } catch (error) {
    const message =
      error && typeof error === "object" && "message" in error
        ? String((error as { message: unknown }).message)
        : String(error);
    toast.error("Error creating order: " + message);
  }
}, [item, qty, createOrder]);
\`\`\`

**Outside React** — call APIs as a plain Promise (utility functions, event handlers):

\`\`\`typescript
import { executeApi } from "@superblocksteam/library";

const result = await executeApi("GetUsers", { email });
\`\`\`

\`useApiData\` returns \`{ data, loading, fetching, isError, isSuccess, isStale, error, status, fetchStatus, refetch, cancel }\`. \`data\` persists across background refetches (stale-while-revalidate). \`loading\` is true on first fetch with no cached data. \`fetching\` is true during any fetch including background refetch. \`status\` is \`"pending"\` | \`"success"\` | \`"error"\`. \`fetchStatus\` is \`"idle"\` | \`"fetching"\`. \`cancel()\` is synchronous (returns \`void\`).

\`useApi\` returns \`{ run, cancel, reset, loading, data, error, status, variables }\`. \`data\` persists across re-runs (shows previous result while loading). \`error\` holds the most recent failure, or \`undefined\` on success. \`status\` is \`"idle"\` | \`"pending"\` | \`"success"\` | \`"error"\`. \`variables\` holds the inputs from the most recent \`run()\`. \`reset()\` clears all state back to idle. HMR double-fetch prevention is built in — no \`useRef\` guard needed.

### useApiData Options

\`\`\`typescript
const { data } = useApiData("GetUsers", { email }, {
  enabled: true,           // Skip fetching when false (conditional fetching)
  staleTime: 0,            // Ms before cached data is considered stale (0 = always refetch)
  retry: 3,                // Retry attempts on failure (false to disable)
  retryDelay: (n) => ...,  // Custom delay strategy (default: exponential backoff, max 30s)
  refetchOnWindowFocus: false, // Refetch when tab regains focus
  refetchOnReconnect: true,    // Refetch when network reconnects
  refetchInterval: false,      // Polling interval in ms (false = disabled)
  structuralSharing: false,    // Deep-compare to preserve object identity
  placeholderData: undefined,  // Shown while the first fetch is in progress
});
\`\`\`

### Cache invalidation and optimistic updates

**Same scope as the read**: After a mutation, prefer \`refetch()\` from the \`useApiData\` that loads the data (same component or wherever you can pass \`refetch\`).

\`\`\`typescript
import { useApi, useApiData } from "@superblocksteam/library";
import { toast } from "sonner";

const { data: orders, refetch } = useApiData("GetOrders", { status });
const { run: createOrder } = useApi("CreateOrder");

const handleSubmit = useCallback(async () => {
  try {
    await createOrder({ item, qty });
    await refetch();
  } catch (error) {
    const message =
      error && typeof error === "object" && "message" in error
        ? String((error as { message: unknown }).message)
        : String(error);
    toast.error("Error creating order: " + message);
  }
}, [item, qty, createOrder, refetch]);
\`\`\`

**Cross-scope or broad invalidation**: Use \`queryClient.invalidateQueries\` when you cannot call that \`refetch\` (mutation in a different place than the \`useApiData\`, or you need every cached variant of an API to refetch). For **\`queryClient.invalidateQueries\`, \`buildCacheKey\`, \`setQueryData\`, and any other \`queryClient\` usage, always import \`queryClient\` from \`@superblocksteam/library\`**.

\`\`\`typescript
import { queryClient, useApi } from "@superblocksteam/library";
import { toast } from "sonner";

const { run: createOrder } = useApi("CreateOrder");

const handleSubmit = useCallback(async () => {
  try {
    await createOrder({ item, qty });
    await queryClient.invalidateQueries("GetOrders");
  } catch (error) {
    const message =
      error && typeof error === "object" && "message" in error
        ? String((error as { message: unknown }).message)
        : String(error);
    toast.error("Error creating order: " + message);
  }
}, [item, qty, createOrder]);
\`\`\`

\`\`\`typescript
// Optimistic update: set cached data without refetching
import { queryClient } from "@superblocksteam/library";

const cacheKey = queryClient.buildCacheKey("GetOrders", { status: "active" });
queryClient.setQueryData(cacheKey, (old) => ({
  ...old,
  orders: [...old.orders, newOrder],
}));
\`\`\`

### Critical API Rules

1. **MUST call API by exact name**: Format is \`<ApiName>\` matching folder \`apis/<ApiName>/api.yaml\`
2. **Use \`useApiData\` for data loading, \`useApi\` for mutations**: For data fetching, use \`useApiData("GetUsers", { email })\`. Only use \`useApi\` for event-driven actions (button clicks, form submissions). Use \`executeApi\` for API calls outside React components.
3. **Use the hook's state fields**: Do NOT create separate \`useState\` for loading or response data — the hooks manage this for you
4. **ALWAYS check API interfaces**: NEVER guess the response structure
5. **Include all parameters**: Even optional ones should be passed as \`null\`
6. **ALWAYS use try/catch**: In imperative mode, APIs throw errors when they fail - wrap ALL \`run()\` calls in try/catch blocks. In declarative mode, check the \`error\` field for failure details.

### File Handling

**CRITICAL: Files must be wrapped in \`{ files: [...] }\` format:**

\`\`\`tsx
// ✅ CORRECT - Frontend: wrap files in { files: [...] }
const response = await runUploadApi({ userFile: { files: selectedFiles } });

// ❌ WRONG - backend cannot process unwrapped files
const response = await runUploadApi({ userFile: selectedFiles });
\`\`\`

## Platform Hooks and Functions

Available hooks and functions from \`@superblocksteam/library\`:

### User and Group Context

\`\`\`typescript
import {
  useSuperblocksUser,
  useSuperblocksGroups,
} from "@superblocksteam/library";

// Get current user info
const user = useSuperblocksUser();
// user.name, user.email, user.id, user.groups, user.username, user.metadata

// Get organization groups
const groups = useSuperblocksGroups();
\`\`\`

### Environments and Data Tags

Concepts & terminology:

- \`Data tags\` are labels for different data segments, such as \`Staging\`, \`Production\`, or \`us-east\`.
- \`Profiles\` is the legacy term for the same concept. If a user asks about profiles, treat that as data tags.
- \`Environments\` are \`Edit\`, \`Preview\`, and \`Production\`, and each data tag is allowed in one or more of those environments.
- Clark operates in \`Edit\` mode, so the data tags visible in app context represent the tags available in \`Edit\`; do not assume this is the same set that will be available in \`Preview\` or \`Production\`.
- When implementing app code, prefer \`useSuperblocksDataTags()\`, \`dataTags\`, and \`setDataTag()\`.

The \`DataTag\` type has these fields (it is an alias for \`Profile\` from \`@superblocksteam/shared\`):

\`\`\`typescript
type DataTag = {
  id: string;
  key: string;
  displayName: string; // human-readable label — use this for display, NOT "name"
  description: string;
  type: "RESERVED" | "CUSTOM";
};
\`\`\`

Relevant hooks and functions:

\`\`\`typescript
import { useSuperblocksDataTags, getAppMode } from "@superblocksteam/library";
import type { DataTag, DataTags } from "@superblocksteam/library";

// Manage data tags
const { dataTags, setDataTag } = useSuperblocksDataTags();
// dataTags.available: DataTag[]   — all tags in the current environment
// dataTags.selected:  DataTag | undefined — currently active tag (may be undefined)
// dataTags.default:   DataTag     — the default tag
// setDataTag(dataTag.key)         — switch the active tag by key

// Read the current app mode when reasoning about environment-specific behavior
const appMode = getAppMode();
\`\`\`

Example — data tag switcher using a Select:

\`\`\`tsx
const { dataTags, setDataTag } = useSuperblocksDataTags();

<Select value={dataTags?.selected?.key} onValueChange={setDataTag}>
  {dataTags?.available.map((tag) => (
    <SelectItem key={tag.key} value={tag.key}>
      {tag.displayName}
    </SelectItem>
  ))}
</Select>;
\`\`\`

### Embedded Applications

**For embedded applications**, see the \`references/embedding.md\` file for \`useEmbedProperties\`, \`useEmbedEvent\`, and \`useEmitEmbedEvent\` hooks.

## Logging Out of Integrations

When building a "Log out" or "Sign out" button for apps that use OAuth-like integrations, use \`logoutIntegrations\` from the library to clear OAuth tokens:

\`\`\`typescript
import { logoutIntegrations } from "@superblocksteam/library";

const handleLogout = async () => {
  await logoutIntegrations();
  // Optionally redirect or show confirmation
};
\`\`\`

## Application Architecture

### App.tsx Layout Structure

**For single-page applications:**

- Put all content in \`pages/<pageName>/index.tsx\`
- Use components to compose and build the page
- Keep \`App.tsx\` minimal with just \`<AppProvider>\` and \`<Outlet />\`

**For multi-page applications:**

- Put shared navigation/layout in \`App.tsx\` (sidebars, headers, footers)
- Always include \`<Outlet />\` for page content
- Always include \`<AppProvider />\` wrapper
- Individual pages focus on content, not layout

\`\`\`tsx
// ✅ CORRECT - Multi-page App.tsx layout
<AppProvider>
  <div className="flex flex-row size-screen">
    {/* Sidebar - persistent across all pages */}
    <div className="flex bg-sidebar border-r w-[250px]">
      <Navigation />
    </div>

    {/* Main content area */}
    <div className="flex flex-1 h-full flex-col">
      {/* Header - persistent across all pages */}
      <div className="flex bg-header border-b h-[60px]">
        <Header />
      </div>

      {/* Page content area */}
      <div className="flex p-4 flex-1 overflow-auto">
        <Outlet />
      </div>
    </div>
  </div>
</AppProvider>
\`\`\`

**PROTECTED: NEVER remove \`<AppProvider>\` or \`<Outlet />\` from App.tsx!**

### Page Structure

\`\`\`tsx
// ✅ CORRECT - Page focuses on content only
<div className="flex flex-col gap-4 size-screen overflow-auto">
  <h1 className="text-3xl font-bold">Dashboard Content</h1>
  <Card>{/* Page-specific content */}</Card>
</div>
\`\`\`

**IMPORTANT**: The first div on the page must have \`overflow-auto\` so the user's page can scroll.

## Routing

Use \`react-router@7\` in data mode. Standard patterns apply:

- \`useNavigate()\` for programmatic navigation
- \`useParams()\` for route parameters
- \`useSearchParams()\` for query strings

**If you add new pages or rename files, you MUST update the router.**

## Design System (Tailwind CSS v4)

**All design tokens are defined in \`index.css\`**. Apps come with a professional black-and-white theme by default.

### Semantic Tokens Rule

**ALWAYS use semantic tokens** — NEVER raw Tailwind utilities:

\`\`\`tsx
// ✅ CORRECT
<Card className="bg-background text-foreground border border-border" />

// ❌ WRONG
<Card className="bg-white text-black border-gray-200" />
\`\`\`

### When to Modify index.css

Only modify when:

- User explicitly requests branding/theme changes
- Replicating a specific brand look (e.g., Yelp, Instacart)
- Migrating an attached third-party app — see \`skills/system/third-party-migration/SKILL.md\` (its theme-port rules override the Modification Rules below — copy source tokens as-is, do not convert to OKLCH or trim the palette)
- Feature requests (lists, filters, CRUD) do NOT require changes

**Modification Rules:**

- All colors must be in OKLCH format
- Use semantic names (\`--color-warning\`, \`--shadow-elevated\`)
- Do not remove or rename existing tokens
- Limit color palette to 5 colors max
- Avoid gradients unless explicitly requested
- Minimal font sizes (3 max: body, section heading, main heading)

### Component Variants

Use or create variants instead of one-off styles:

\`\`\`tsx
// ❌ WRONG - Hacky inline overrides
<Button className="text-white border-white hover:bg-white" />

// ✅ CORRECT - Use a variant
<Button variant="secondary" />
\`\`\`

## Icons

Use icons from Lucide React library:

\`\`\`tsx
import { Icon } from "@/components/ui/icon";
<Icon icon="heart" />;

import { Button } from "@/components/ui/button";
<Button>
  <Icon icon="plus" /> Add Item
</Button>;
\`\`\`

**Always use icons rather than emojis unless explicitly requested.**

## Custom Components

**CRITICAL: Component composition is MANDATORY. DO NOT create monolithic pages.**

### The Composition Rule

When building ANY feature:

1. **First**, identify reusable parts (list items, cards, forms, filters, headers)
2. **Then**, create separate component files
3. **Finally**, compose them together in the page

### When to Create Components

- **Rendering any list** - Extract to a component
- **Building cards/complex UI** - Each card type is a component
- **Creating forms** - Form sections are components
- **Adding filters/headers** - These are components
- **Page has >50 lines JSX** - Break it down

### Component Boundary Rules

**Inside a custom component:**

- ✅ Use React hooks (useState, useReducer, useEffect, etc.)
- ✅ Use local React state
- ✅ Use internal helper components
- ✅ Use other registered components
- ❌ CANNOT call APIs - must pass data into the component

### Example: Proper Component Structure

\`\`\`tsx
// components/ProductCard/index.tsx - Extract to component
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";

type ProductCardProps = {
  product: {
    id: string;
    name: string;
    image: string;
  };
};

export default function ProductCard(props: ProductCardProps) {
  return (
    <Card>
      <img src={props.product.image} />
      <h3 className="text-lg font-semibold">{props.product.name}</h3>
      <Button>Add</Button>
    </Card>
  );
}

// pages/Products/index.tsx - Clean composition
import ProductCard from "@/components/ProductCard";

const ProductsPage = () => {
  const [products, setProducts] = useState([]);

  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map((p) => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  );
};
\`\`\`

## Visual Excellence

**Prioritize visual excellence from the start:**

1. **Design System Enhancement**: Start by enhancing \`index.css\` with app-specific colors that match the target aesthetic
2. **Professional Layout Architecture**: Use sophisticated layouts with proper spacing, responsive design
3. **Rich Interactive Components**: Leverage advanced components with proper variants and states
4. **Visual Polish**: Add shadows, smooth transitions, skeleton loaders, hover effects, typography hierarchy
5. **Real-World UI Patterns**: Create layouts that feel like professional applications

**Make applications that look and feel like real, polished products - not basic wireframes.**

### Loading States

Loading behavior must differ based on whether data has already been fetched:

**Initial load (no data yet):** Show skeleton shimmers that mirror the shape of the final content. Never show a blank screen or a generic spinner.

**Refetch / background refresh (data already exists):** Keep showing the existing data. Use \`fetching\` from \`useApiData\` to apply a subtle visual indicator: light opacity (e.g. \`opacity-70\`) to signal a refresh is in progress, plus a non-blocking label such as a small inline spinner, a thin progress bar, or an "Updating…" label. **Do not** use \`pointer-events-none\` or otherwise disable the content — it must remain fully interactive during refetch.

Use \`loading\` and \`fetching\` from \`useApiData\` to distinguish these states:

\`\`\`tsx
const { data, loading, fetching, isError, error } = useApiData("GetOrders", {
  status: statusFilter,
  search,
});

// Initial load — no data yet → show skeleton placeholder
if (loading) {
  return <OrderTableSkeleton />;
}

if (isError) return <ErrorBanner error={error} />;

// Refetch — data exists → subtle opacity + indicator, table stays interactive
return (
  <div>
    {fetching && <div className="text-xs text-muted-foreground">Updating…</div>}
    <div className={fetching ? "opacity-70" : ""}>
      <OrderTable orders={data.orders} />
    </div>
  </div>
);
\`\`\`

\`\`\`tsx
// ❌ WRONG — pointer-events-none disables the table, making it feel broken
return (
  <div className={fetching ? "pointer-events-none" : ""}>
    <OrderTable orders={data.orders} />
  </div>
);

// ❌ WRONG — no loading feedback when filters change
if (loading) return <Skeleton />;
return <OrderTable orders={data.orders} />;
\`\`\`

#### Table Loading Rules

- **Do not** use \`pointer-events-none\` on a table during loading — this disables interaction and makes the UI feel broken. During refetch, apply a subtle opacity (e.g. \`opacity-70\`) plus a non-blocking indicator (e.g., an "Updating…" label or a thin progress bar). The table must remain clickable, sortable, and scrollable.
- **Do not** replace a populated table with a full skeleton on refetch — this causes disorienting content flashes.
- **Skeleton tables are only for initial load** (\`loading\` is true) when there is no data to display yet. Build them to match the real table's column structure (header + a few placeholder rows).

### Efficient Loading Patterns

1. **Always show loading indicators on refetch**: When inputs change (e.g. filters, search), show a non-blocking visual indicator while new data loads. Use \`fetching\` from \`useApiData\`.
2. **Loading State Hierarchy**:
   - No data yet (\`loading\`) → Full skeleton placeholder
   - Has data, refetching (\`fetching\` && !\`loading\`) → Keep showing current data with a subtle visual indicator: light opacity (e.g. \`opacity-70\`) plus a non-blocking label (e.g. "Updating…" text, thin progress bar, inline spinner). Do not use \`pointer-events-none\` — the content must remain fully interactive.
   - Error state (\`isError\`) → Show error with retry option, optionally keep stale data visible
3. **Debounce Rapid Requests**: Prevent multiple API calls in short succession
4. **Use useApiData for automatic refetching**: \`useApiData\` auto-refetches when inputs change and supports \`staleTime\`, \`refetchOnWindowFocus\`, \`refetchOnReconnect\`, \`refetchInterval\`, and \`retry\` options.

## Performance Rules

### 1. ALWAYS Paginate Tables and Lists

**NEVER render more than 50 rows without pagination.** Always add client-side pagination:

\`\`\`tsx
function PaginatedTable({ data }: { data: any[] }) {
  const PAGE_SIZE = 20;
  const [page, setPage] = useState(0);
  const totalPages = Math.ceil(data.length / PAGE_SIZE);
  const pageData = useMemo(
    () => data.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE),
    [data, page],
  );

  useEffect(() => {
    setPage(0);
  }, [data.length]);

  return (
    <>
      <Table>{/* render pageData rows */}</Table>
      <span>
        Page {page + 1} of {totalPages} ({data.length} total rows)
      </span>
      <Button
        onClick={() => setPage((p) => Math.max(0, p - 1))}
        disabled={page === 0}
      >
        Previous
      </Button>
      <Button
        onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
        disabled={page >= totalPages - 1}
      >
        Next
      </Button>
    </>
  );
}
\`\`\`

For 200+ rows, prefer **server-side pagination**; use cursor/keyset pagination for high offsets instead of \`LIMIT\`/\`OFFSET\`.

For 500+ item lists where pagination doesn't fit the UX (chat messages, infinite scroll feeds, large dropdowns), use **virtualization** (\`react-virtuoso\` or \`@tanstack/react-virtual\`) to render only the items visible in the viewport.

### 2. ALWAYS Debounce Input-Driven API Calls

**NEVER call an API directly from onChange.** Debounce search/filter inputs with a 300ms timer:

\`\`\`tsx
function DebouncedSearch({ onSearch }: { onSearch: (query: string) => void }) {
  const [localValue, setLocalValue] = useState("");
  const timerRef = useRef<ReturnType<typeof setTimeout>>();
  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      setLocalValue(e.target.value);
      clearTimeout(timerRef.current);
      timerRef.current = setTimeout(() => onSearch(e.target.value), 300);
    },
    [onSearch],
  );
  useEffect(() => () => clearTimeout(timerRef.current), []);

  return (
    <Input value={localValue} onChange={handleChange} placeholder="Search..." />
  );
}
\`\`\`

### 3. Memoize Expensive Renders

Use memoization (\`memo()\`, \`useMemo\`, \`useCallback\`) when it prevents measurable re-renders or expensive recomputation:

\`\`\`tsx
const OrderRow = memo(function OrderRow({
  order,
  onSelect,
}: {
  order: Order;
  onSelect: (id: string) => void;
}) {
  return (
    <TableRow onClick={() => onSelect(order.id)}>
      <TableCell>{order.id}</TableCell>...
    </TableRow>
  );
});

const handleSelect = useCallback((id: string) => {
  setSelectedId(id);
}, []);

const filtered = useMemo(
  () =>
    orders.filter((o) => o.status === status).sort((a, b) => b.total - a.total),
  [orders, status],
);
\`\`\`

### 4. ALWAYS Clean Up Side Effects

Clean up timers, event listeners, and subscriptions in \`useEffect\` return functions:

\`\`\`tsx
useEffect(() => {
  const handler = (e: KeyboardEvent) => {
    /* ... */
  };
  window.addEventListener("keydown", handler);
  return () => window.removeEventListener("keydown", handler);
}, []);
\`\`\`

### 5. Cancel In-Flight API Requests on Unmount

For search/filter patterns, prefer \`useApiData\` — it handles cancellation and cleanup automatically:

\`\`\`tsx
// useApiData: auto-fetches and cancels on unmount/input change
const { data, fetching } = useApiData("SearchProducts", { query });
\`\`\`

For imperative usage with manual cleanup, use the \`cancel()\` function:

\`\`\`tsx
const { run, cancel } = useApi("SearchProducts");

useEffect(() => {
  if (!query) return;
  run({ query }).catch(console.error);
  return () => {
    cancel().catch(() => {});
  };
}, [query, run, cancel]);
\`\`\`
`;
//# sourceMappingURL=skill.generated.js.map