latitude-dev/latitude-llm · Archived

web-frontend

apps/web UI — routes, @repo/ui, TanStack Start server functions and collections, forms (useForm + createFormSubmitHandler + fieldErrorsAsStrings for Zod field errors), Tailwind layout rules, design-system updates, and useEffect / useMountEffect policy.

First seen May 10, 2026

Installation

$ npx skills add latitude-dev/latitude-llm --skill web-frontend

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from latitude-dev/latitude-llm · top by installs.

npx skills add latitude-dev/latitude-llm

Browse all from latitude-dev/latitude-llm

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 4.6K
License LICENSE
Default branch development
Open issues 26
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 13,552 B
  • docs SUMMARY.md 274 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 5 installs

SKILL.md

Web app frontend (apps/web)

When to use: apps/web UI — routes, @repo/ui, TanStack Start server functions and collections, forms (useForm with createFormSubmitHandler + fieldErrorsAsStrings when Zod validation errors should appear on fields), Tailwind layout rules, design-system updates, and useEffect / useMountEffect policy.

Legacy UI reference

  • Before building new UI, inspect the old v1 UI/components and product patterns as a reference when relevant.
  • Reuse as much as possible when the old implementation is still solid.
  • Do not copy v1 UI blindly; review it critically and improve it to match v2 conventions, architecture, and quality expectations when needed.

React 19

The project uses React 19. Follow modern patterns and avoid deprecated APIs:

  • No forwardRefref is a regular prop in React 19. Declare it in the props type and destructure it directly.
  • No ElementRef — use React.ComponentRef<typeof SomeComponent> instead (the ElementRef alias is deprecated).
  • No gratuitous useMemo / useCallback / React.memo — the React Compiler (enabled in the build) auto-memoizes. Only add manual memoization when profiling shows a concrete bottleneck; remove existing wrappers when they have no measured benefit.
  • Prefer use() for consuming promises and context where appropriate.
// ❌ Deprecated React 18 pattern
const Input = forwardRef<ElementRef<typeof Primitive>, InputProps>(({ className, ...props }, ref) => (
  <Primitive ref={ref} {...props} />
))
Input.displayName = "Input"

// ✅ React 19 — ref is a regular prop
function Input({ className, ref, ...props }: InputProps & { ref?: React.Ref<React.ComponentRef<typeof Primitive>> }) {
  return <Primitive ref={ref} {...props} />
}

Components

  • Always use Text from @repo/ui for text content
  • Always use Button from @repo/ui for buttons
  • Do not nest Text inside Button. Button already sets font size, weight, and color; use plain text (and optional icons) as direct children. Wrapping the label in Text duplicates styles (e.g. avoid <Button><Text.H5>Save</Text.H5></Button>).
  • Lucide icons: import from lucide-react and pass the component to @repo/ui’s Icon via the icon prop (e.g. <Icon icon={Pencil} size="sm" />). Prefer that over raw <Pencil /> so shared sizing and color tokens apply. Buttons and other primitives that accept an icon prop follow the same pattern; otherwise wrap with Icon.
  • Always use GoogleIcon and GitHubIcon from @repo/ui for OAuth provider icons

Modal: short form vs composition

Default to the short form<Modal title=… description=… footer=… open dismissible onOpenChange={…}>{children}</Modal>. It auto-wraps children in Modal.Body with the standard px-6 padding and the same scroll behavior every other modal uses, so content inside it can't drift out of the design system. This is the shape RenameProjectModal (the form reference cited above) uses — treat it as the canonical pattern.

<Modal
  open
  dismissible
  onOpenChange={(next) => (!next ? onClose() : undefined)}
  title="Change sampling rate"
  description="Percentage of incoming traces this evaluation runs against."
  footer={
    <>
      <CloseTrigger />
      <Button onClick={() => void form.handleSubmit()}>Save</Button>
    </>
  }
>
  <form onSubmit={…}>{/* fields */}</form>
</Modal>

Reach for the composition form (Modal.Root / Modal.Content / Modal.Header / Modal.Body / Modal.Footer) only when the short form can't express what you need:

  • Custom header JSX beyond a string title + description — render <Modal.Header>{custom JSX}</Modal.Header> directly.
  • Multiple body regions, or content that must opt out of the standard body padding / scroll.
  • Conditional rendering of header or footer that can't be driven by passing undefined.

Notes:

  • dismissible defaults to false — pass it to show the close X.
  • scrollable defaults to true. For short bodies that shouldn't grow to fill height, pass scrollable={false}.
  • Do not wrap your children in your own <Modal.Body> while using the short form — children are already wrapped, double-wrapping nests padding.

Route-level component organization

Place React components close to the routes that use them, inside a -components/ subfolder within the route directory. This keeps route files (which TanStack Router auto-discovers) clearly separated from supporting components.

routes/_authenticated/projects/$projectId/datasets/
├── index.tsx                       # route file
├── $datasetId.tsx                  # route file
└── -components/                    # supporting components for these routes
    ├── dataset-table.tsx
    ├── row-detail-panel.tsx
    └── version-badge.tsx
  • Route files live directly in the route directory — TanStack Router discovers them
  • Supporting UI for those routes lives in the adjacent -components/ folder
  • domains/ directories (apps/web/src/domains/) are for state management only: server functions (writes) and collections/queries (reads) — not UI components

Design system showcase

  • When adding a new implemented UI component in packages/ui (or replacing a placeholder export with a real implementation), update apps/web/src/routes/design-system.tsx to include a usage example for that component in both light and dark mode previews.
  • Treat apps/web/src/routes/design-system.tsx as the canonical visual inventory for @repo/ui components.

State management (TanStack)

The web app uses a server-centric, query-driven architecture built on the TanStack ecosystem. No Zustand, Redux, or global stores.

Server functions — All data fetching and mutations use createServerFn from @tanstack/react-start:

import { Effect } from "effect"
import { ProjectRepository, createProjectUseCase } from "@domain/projects"
import { ProjectRepositoryLive, SqlClientLive } from "@platform/db-postgres"
import { getPostgresClient } from "../../server/clients.ts"

// Query (GET)
export const listProjects = createServerFn({ method: "GET" }).handler(async () => {
  const { organizationId } = await requireSession()
  const client = getPostgresClient()

  return await Effect.runPromise(
    Effect.gen(function* () {
      const repo = yield* ProjectRepository
      return yield* repo.findAll()
    }).pipe(
      Effect.provide(ProjectRepositoryLive),
      Effect.provide(SqlClientLive(client, organizationId)),
    ),
  )
})

// Mutation (POST) with Zod validation
export const createProject = createServerFn({ method: "POST" })
  .inputValidator(createProjectSchema)
  .handler(async ({ data }) => {
    const { userId, organizationId } = await requireSession()
    const client = getPostgresClient()

    return await Effect.runPromise(
      createProjectUseCase({...}).pipe(
        Effect.provide(ProjectRepositoryLive),
        Effect.provide(SqlClientLive(client, organizationId)),
      ),
    )
  })

Server functions live in apps/web/src/domains/*/functions.ts.

Collections — Client-side reactive state uses TanStack React DB + Query via queryCollectionOptions:

const projectsCollection = createCollection(
  queryCollectionOptions({
    queryClient,
    queryKey: ["projects"],
    queryFn: () => listProjects(),
    getKey: (item) => item.id,
    onInsert: async ({ transaction }) => { /* optimistic insert */ },
    onUpdate: async ({ transaction }) => { /* optimistic update */ },
    onDelete: async ({ transaction }) => { /* optimistic delete */ },
  }),
)

export const useProjectsCollection = (...) => useLiveQuery(...)

Collection files live in apps/web/src/domains/*/collection.ts.

Route middleware vs route data

  • Use beforeLoad for middleware-style checks that should block the route tree early: auth redirects, authorization gates, and other preconditions.
  • Use loader for data the route or layout actually renders. This keeps rendered data in TanStack Router's loader lifecycle, so it can use staleTime, useLoaderData({ select }), and avoid unnecessary refetching on same-route search-param navigations.
  • If the same lookup is both your guard and your rendered data source, prefer doing that work in loader once instead of duplicating it across beforeLoad and loader.
  • When multiple descendant routes need parent loader data, prefer a small route-scoped wrapper around getRouteApi("...") instead of repeating the route id string in every file.
export const Route = createFileRoute("/admin")({
  beforeLoad: async () => {
    const session = await getSession()
    if (!session?.user.isAdmin) throw redirect({ to: "/" })
  },
})
export const Route = createFileRoute("/_authenticated")({
  staleTime: Infinity,
  loader: async () => {
    const session = await getSession()
    if (!session) throw redirect({ to: "/login" })

    const sessionData = session.session as Record<string, unknown>
    const organizationId =
      typeof sessionData.activeOrganizationId === "string" ? sessionData.activeOrganizationId : null
    if (!organizationId) throw redirect({ to: "/welcome" })

    return {
      user: session.user,
      organizationId,
    }
  },
})
const authenticatedRoute = getRouteApi("/_authenticated")

export function useAuthenticatedUser() {
  return authenticatedRoute.useLoaderData({ select: (data) => data.user })
}

Key rules:

  • Server functions are the only data-fetching mechanism — no direct REST API calls from the client
  • Use collections for reactive, queryable client state with automatic server sync
  • Use useState for local UI state (modals, form visibility); no global stores
  • Invalidate query cache after mutations: getQueryClient().invalidateQueries({ queryKey: [...] })
  • Forms use TanStack React Form (useForm + form.Field)

TanStack Form + Zod field errors (createFormSubmitHandler + fieldErrorsAsStrings)

When: useForm submits work that can fail with Zod validation (for example server functions using inputValidator), and you want inline errors on @repo/ui fields (not only a toast).

Module: apps/web/src/lib/form-server-action.ts

Helper Use
createFormSubmitHandler Pass as useForm({ onSubmit: createFormSubmitHandler(async (value) => { ... }, { onSuccess, onError }) }). On validation failure it maps serialized Zod issues onto TanStack Form field meta via extractFieldErrors in apps/web/src/lib/errors.ts. Non-field errors go to onError. On success it resets the form and runs onSuccess.
fieldErrorsAsStrings On every Input, Textarea, or other control with an errors prop inside form.Field, set errors={fieldErrorsAsStrings(field.state.meta.errors)} so those meta errors display.

Always use both when you want Zod-driven field errors: the submit handler wires errors into form state; the helper wires form state into @repo/ui.

Do not duplicate the inline pattern field.state.meta.errors.length > 0 ? field.state.meta.errors.map(String) : undefined — use fieldErrorsAsStrings instead.

Reference: apps/web/src/routes/_authenticated/index.tsxRenameProjectModal: createFormSubmitHandler in useForm (~line 225), fieldErrorsAsStrings on the name field Input (~line 287).

Layout and spacing

  • Always use flexbox for layout (flex, flex-col, flex-row)
  • Never use margin utilities (no m-, mx-, my-, mt-, etc.)
  • Always use gap utilities for spacing between elements (gap-, gap-x-, gap-y-*)
  • Always use p-* (padding) for internal spacing within containers

Conditional classes (cn)

With cn(), use object syntax { "class-name": condition } — not short-circuit condition && "class-name".

// ❌ Bad
<div className={cn("base-class", isActive && "bg-accent")} />

// ✅ Good
<div className={cn("base-class", { "bg-accent": isActive })} />

Example

// ❌ Bad - using margins and space-y
<div className="space-y-4 mt-4">
  <div className="mb-2">Item 1</div>
  <div className="mb-2">Item 2</div>
</div>

// ✅ Good - using flexbox with gap
<div className="flex flex-col gap-4 pt-4">
  <div>Item 1</div>
  <div>Item 2</div>
</div>

React effects (useEffect policy)

  • Do not call useEffect directly in components; use useMountEffect from @repo/ui for mount/unmount-only sync (listeners, imperative widgets, one-time setup).
  • If raw useEffect is unavoidable, add TODO(frontend-use-effect-policy) with a short reason.

Prefer: derive values during render; run work in event handlers; controlled vs uncontrolled via value !== undefined; reset by key when an entity id changes.

Avoid: deriving state from props in an effect; fetching in effects to set state; mirroring props into local state; effects as command dispatchers.

import { useMountEffect } from "@repo/ui"

useMountEffect(() => {
  const cleanup = subscribeToExternalSystem()
  return () => cleanup()
})