--- name: frontend-guide description: 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. --- # 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](https://vitest.dev/) for unit and integration tests. - Follow [Svelte](https://svelte.dev/docs) and [SvelteKit](https://kit.svelte.dev/docs) official documentation. ## Framework-agnostic approach Minimize framework-specific syntax to reduce coupling and improve testability: - Prefer [writable](https://svelte.dev/docs/svelte/svelte-store)/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: ```typescript // ✅ 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: ```typescript 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: ```typescript type UseDataParams = Parameters[0]; type UseDataReturn = ReturnType; type TitleOnly = Pick; type WithoutTitle = Omit; ``` ## UI and Styling - **[Shadcn](https://next.shadcn-svelte.com/)** 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](https://www.bits-ui.com/)** as the base component library underlying Shadcn. - **[Lucide Icons](https://lucide.dev/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: ```typescript {#if 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: ```typescript ``` **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`: ```typescript 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: ```typescript // 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](https://tanstack.com/query/latest/docs/framework/svelte/overview) 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](https://svelte.dev/playground/hello-world?version=5.32.1#H4sIAAAAAAAAE3WRwW6DMBBEf2W1qhSioqS9koBU9TNKD8TZVFZhjex1kwr53yvjhkCVXjisZ2aHtwNy0xEW6B29Gs9CFujSdH1LmONJt-SweBtQvvuoigPMr56Xvt-4L2olzg6No3tzZViIxWGBe6es7gXahj_KGsXVWNUMAKC73liBAWY1Apys6WC12d6GG3GrXc3JpAy76FHxLQfNylJHLBCgnAVl613N-23aXUXz_uBFDINh1Wr1WQ7ZGsrqFpAtw2-vMaKA4WHcEWJO8lY1Y45CF8FCrKeQ_8No0X2J6e_TjNTE42y1NIeWJhqJ69aJsZRQJAxjQSgnw559dyBbZU_rUUWXMfHkWYk2vOADQ_rb9J0kMx5XRcLuWTa-PzZCWaZGdAoe4TkuioJwPdCUZMmRjOvuRTmS1HJhtiTe8p3z5rM4CLuaw_IS7zlKo9uz5iMWp6Z1FH4Aaa7hNOYCAAA=)): ```typescript // useCounter.ts import { writable } from 'svelte/store'; const count = writable(0); export function useCounter() { function increment() { count.update((c) => c + 1); } function resetCount() { count.set(0); } return { count, increment, resetCount }; } // Counter.svelte ``` ### 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](https://svelte.dev/tutorial/kit/layouts) in `src/routes/`. - Use dynamic routes with [slug](https://svelte.dev/tutorial/kit/params) 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: ```typescript Heading 1 - Large Page Title ``` ## 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:** ```typescript 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: ```bash # Static checks (TypeScript, ESLint, Prettier, Svelte check) make static-checks # Unit tests npm run test:unit ``` All checks must pass before committing.