agentleFS
lightly-ailightly-ai/lightly-studio/.agents/skills/frontend-guide/SKILL.md

frontend-guide

Skill896 starsChanged 31 days agoApache-2.0View on GitHub

Read before writing or reviewing any frontend code in lightly_studio_view - Svelte, TypeScript, or SvelteKit files. Covers component structure and naming, stores vs runes, absolute vs relative imports, Shadcn and Tailwind usage, Svelte 5 syntax, TanStack Query hooks, bundle size limits, Storybook stories, and vitest conventions.

We split this skill into the rules it gives an agent

31 rules. About 0 are common, 16 are distinctive, and 15 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈16

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 15

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Write concise, technical TypeScript code.

    line 12
  2. Distinctive:

    Keep components under 100 lines.

    line 13
  3. Distinctive:

    Follow Svelte and SvelteKit official documentation.

    line 15
  4. Distinctive:

    Prefer writable/readable/derived stores over runes for state management - stores have explicit imports and clearer dependencies.

    line 21
  5. Distinctive:

    Use runes only when their specific features are necessary for component-level reactivity.

    line 22
  6. Distinctive:

    Every component and hook lives in its own folder scoping related files together.

    line 29
  7. Very specific:

    Use .svelte.ts files for component logic, state machines, and hooks that use TanStack Query (since v6 uses runes internally).

    line 58
  8. Very specific:

    Use barrel exports (index.ts) to define a module's public API.

    line 59
  9. Very specific:

    Relative imports: only within the same module folder: from "../UserDashboard.helpers".

    line 69
  10. Distinctive:

    Use TypeScript for all code.

    line 73
  11. Distinctive:

    Avoid exporting/importing types.

    line 92
  12. Very specific:

    Use cn() from $lib/utils for conditional Tailwind class composition.

    line 107
  13. Very specific:

    Prefer explicit props over object spreading - components should receive only the specific props they need.

    line 108
  14. Distinctive:

    Keep JS chunk sizes below 500KB.

    line 112
  15. Distinctive:

    When reviewing bundle size:

    line 131
  16. Distinctive:

    Check build output for chunk-size warnings.

    line 133
  17. Distinctive:

    Prefer lazy loading, vendor splitting, or dependency deduplication when a chunk exceeds the limit.

    line 135
  18. Distinctive:

    Use Svelte 5 patterns in all new code:

    line 139
  19. Very specific:

    Props: - use $props() with a typed interface:

    line 141
  20. Very specific:

    Event handlers: - use onclick, onchange, etc.

    line 161
  21. Very specific:

    Reactive declarations: - use $derived or derived() stores, not $:.

    line 163
  22. Very specific:

    Page state: - use $app/state, not $app/stores:

    line 165
  23. Very specific:

    Create small, reusable hooks in src/lib/hooks - avoid monolithic stores.

    line 204
  24. Very specific:

    Use TanStack Query for all data fetching.

    line 211
  25. Distinctive:

    Prefer these solutions in order:

    line 246
  26. Very specific:

    Use SvelteKit's file-based routing in src/routes/.

    line 255
  27. Very specific:

    Use dynamic routes with slug syntax.

    line 256
  28. Very specific:

    Use +layout.svelte for shared layouts.

    line 257
  29. Very specific:

    Use simplified story syntax - no explicit {#snippet children()} for text content:

    line 261
  30. Distinctive:

    Test behavior, not implementation: - don't assert on CSS classes, internal structure, or how hooks are called.

    line 292
  31. Distinctive:

    All checks must pass before committing.

    line 307

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Frontend coding guidelines

Coding standards for frontend development in LightlyStudio using SvelteKit and TypeScript.

Key Principles

  • Write concise, technical TypeScript code. Use Svelte-specific features (like runes) only when necessary within Svelte-compiled components.
  • Keep components under 100 lines. Split code into logical, testable parts.
  • Embrace TDD. Use vitest for unit and integration tests.
  • Follow Svelte and SvelteKit official documentation.

Framework-agnostic approach

Minimize framework-specific syntax to reduce coupling and improve testability:

  • Prefer writable/readable/derived stores over runes for state management - stores have explicit imports and clearer dependencies.
  • Use runes only when their specific features are necessary for component-level reactivity.
  • Less framework coupling means easier maintenance, onboarding, and migration.

Project structure & naming

  • PascalCase for component names and their folders (e.g., AuthForm/AuthForm.svelte).
  • camelCase for non-component files, variables, functions, and props (e.g., useAuth.ts, const myVar).
  • Every component and hook lives in its own folder scoping related files together.

Canonical layout:

src/
  components/
    AuthForm/
      AuthForm.svelte
      AuthForm.test.ts
      AuthForm.stories.svelte       # if needed
    UserDashboard/
      UserDashboard.svelte
      UserDashboard.test.ts
      UserDashboard.helpers.ts       # if needed
      UserProfile/                   # subcomponent
        UserProfile.svelte
        UserProfile.test.ts
  lib/
    hooks/
      index.ts                       # barrel exports
      useAuth/
        useAuth.ts
        useAuth.test.ts
      useData/
        useData.ts
        useData.test.ts
  • Use .svelte.ts files for component logic, state machines, and hooks that use TanStack Query (since v6 uses runes internally).
  • Use barrel exports (index.ts) to define a module's public API. Import from module level, not deep paths:
// ✅ from "$lib/hooks"  - not "$lib/hooks/useData/useData"
import { useData, useAuth } from "$lib/hooks";

Imports

  • Absolute imports for shared modules: from "$lib/hooks", from "$lib/components/ui/button".
  • Relative imports only within the same module folder: from "../UserDashboard.helpers".

TypeScript

  • Use TypeScript for all code.
  • Define interface for component props, function parameters, and return types:
interface UseDataParams {
  title: string;
  onClick: () => void;
}

interface UseDataReturn {
  data: string;
  isLoading: boolean;
}

export function useData(params: UseDataParams): UseDataReturn {
  return { data: params.title, isLoading: false };
}
  • Avoid exporting/importing types. Derive types from source code using utility types to keep things DRY:
type UseDataParams = Parameters<typeof useData>[0];
type UseDataReturn = ReturnType<typeof useData>;
type TitleOnly = Pick<UseDataParams, "title">;
type WithoutTitle = Omit<UseDataParams, "title">;

UI and Styling

  • Shadcn components from $lib/components/ui for standard UI elements (buttons, inputs, cards, tabs, alerts, tables).
  • Project-specific components from $lib/components when combining multiple Shadcn components, adding business logic, or needing custom behavior.
  • Bits-UI as the base component library underlying Shadcn.
  • Lucide Icons for all icons - import from @lucide/svelte.
  • Use cn() from $lib/utils for conditional Tailwind class composition.
  • Prefer explicit props over object spreading - components should receive only the specific props they need. Exception: forwarding HTML attributes via ...rest, or intentional wrapper/proxy components.

Performance

Keep JS chunk sizes below 500KB. Use dynamic imports for heavy components:

<script lang="ts">
  import { onMount } from 'svelte';

  let HeavyChart;

  onMount(async () => {
    const module = await import('$lib/components/HeavyChart.svelte');
    HeavyChart = module.default;
  });
</script>

{#if HeavyChart}
  <svelte:component this={HeavyChart} />
{/if}

When reviewing bundle size:

  • Check build output for chunk-size warnings.
  • Identify heavy dependencies before adding them to eagerly loaded routes.
  • Prefer lazy loading, vendor splitting, or dependency deduplication when a chunk exceeds the limit.

Svelte 5 syntax

Use Svelte 5 patterns in all new code:

Props - use $props() with a typed interface:

<script lang="ts">
  interface Props {
    value: string;
    placeholder?: string;
    onSearch: (query: string) => void;
    disabled?: boolean;
  }

  let {
    value,
    placeholder = 'Enter text',
    onSearch,
    disabled = false
  }: Props = $props();
</script>

Event handlers - use onclick, onchange, etc. (not on:click, on:change).

Reactive declarations - use $derived or derived() stores, not $:. Do not mix the two approaches - pick one per file/module.

Page state - use $app/state, not $app/stores:

import { page } from "$app/state";
page.params.sampleId; // no $ prefix needed

Hooks and reactivity - for hooks that wrap TanStack Query, accept a getter function (thunk) for reactive parameters. TanStack Query v6 uses thunks for reactivity — do not pass Svelte stores or $derived values:

// Hook definition (useFrames.svelte.ts — must be .svelte.ts for TanStack v6)
export const useFrames = (
  getParams: () => { video_frame_collection_id: string; filter: VideoFrameFilter }
) => {
  const query = createInfiniteQuery(() => {
    const { video_frame_collection_id, filter } = getParams();
    return {
      ...getAllFramesInfiniteOptions({
        path: { video_frame_collection_id },
        body: { filter }
      }),
      getNextPageParam: (lastPage) => lastPage.nextCursor || undefined
    };
  });
  return { query };
};

// Consumer (+page.svelte)
const { query } = useFrames(() => ({
  video_frame_collection_id: collectionId,
  filter: currentFilter
}));
// query is a reactive proxy — access query.isSuccess, query.data directly (no $ prefix)

For non-TanStack hooks with static parameters, pass values via SvelteKit's page load function or direct props.

State management & hooks

Create small, reusable hooks in src/lib/hooks - avoid monolithic stores. We do not use a services folder; hooks handle data fetching and state. Ref: lightly_studio_view/src/lib/hooks/useTags/useTags.ts, lightly_studio_view/src/lib/hooks/useFeatureFlags/useFeatureFlags.ts.

Generic hooks go in src/lib/hooks; component-specific hooks go in the component's folder.

For data fetching and API work:

  • Use TanStack Query for all data fetching. TanStack Query v6 is runes-based — hooks that call createQuery/createInfiniteQuery must be .svelte.ts files.
  • The query result is a reactive proxy (not a Svelte store). Access properties like query.isSuccess, query.data directly — no $ prefix needed.
  • Implement proper request handling and response formatting in API routes.

Store-based hook example (playground):

// useCounter.ts
import { writable } from 'svelte/store';

const count = writable<number>(0);

export function useCounter() {
  function increment() {
    count.update((c) => c + 1);
  }

  function resetCount() {
    count.set(0);
  }

  return { count, increment, resetCount };
}

// Counter.svelte
<script lang="ts">
  import { useCounter } from './useCounter';
  const { count, increment } = useCounter();
</script>

<button onclick={() => increment()}>Count: {$count}</button>

Avoiding props drilling

Prefer these solutions in order:

  1. Svelte Context API (setContext/getContext) - for state shared within a component subtree.
  2. Svelte stores - for truly global state (auth, preferences) shared across unrelated trees.
  3. $app/state - for server-loaded page data accessible by any component on the page.
  4. Svelte 5 Snippets (Snippet type) - for UI composition / layout customization without passing content as props.

Routing and Pages

  • Use SvelteKit's file-based routing in src/routes/.
  • Use dynamic routes with slug syntax. E.g. sample details at lightly_studio_view/src/routes/datasets/[dataset_id]/[collection_type]/[collection_id]/images/[sampleId]/.
  • Use +layout.svelte for shared layouts. E.g. the collection layout at lightly_studio_view/src/routes/datasets/[dataset_id]/[collection_type]/[collection_id]/+layout.svelte.

Storybook

Use simplified story syntax - no explicit {#snippet children()} for text content:

<Story name="H1" args={{ variant: 'h1' }}>
    Heading 1 - Large Page Title
</Story>

Testing

Testing levels: Unit tests for isolated components/functions, integration tests for component interactions, end-to-end tests for full application flows.

Unit test example:

import { render, screen } from "@testing-library/svelte";
import MyComponent from "./MyComponent.svelte";

describe("MyComponent", () => {
  it("renders the title", () => {
    render(MyComponent, { props: { title: "Hello World" } });
    expect(screen.getByText("Hello World")).toBeInTheDocument();
  });
});

Test optimization rules

  • Use defaultProps helper objects to avoid repeating prop definitions across tests. Override individual props with spread: { ...defaultProps, isUploading: true }.
  • No mirror tests for simple boolean toggles - testing the truthy case is sufficient when a single boolean controls the state.
  • Combine related assertions into one test rather than creating separate tests for closely related checks (e.g., placeholder + accessible label).
  • Test behavior, not implementation - don't assert on CSS classes, internal structure, or how hooks are called. Assert on user-visible outcomes.
  • Remove duplicate tests that verify the same behavior with different queries.

Running tests

Before submitting code:

# Static checks (TypeScript, ESLint, Prettier, Svelte check)
make static-checks

# Unit tests
npm run test:unit

All checks must pass before committing.

Download now (ZIP)

  • Other versions2 projects use a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 2

    1. BojanKocijan/bk-charterline★ 4same name
    2. BojanKocijan/design-forge★ 4same name

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

  • In this repository8 other files it gives its agents.

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.