Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .claude/rules/svelte-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ Import from `flowbite-svelte`. Use Tailwind CSS v4 utility classes. Dark mode: `

When copying button styles from a reference component, always check all three axes: `color`, `size`, and `class`. Omitting `color` applies Flowbite's default (filled blue).

## Complex `{#if}` Conditions — Extract to Named Functions

When a template condition requires API-specific knowledge or combines multiple null checks, extract it to a private function in `<script>`. A named function communicates intent at the call site; the implementation detail stays in one place. This applies even when the function is used only once.

```svelte
<!-- Bad: requires knowing $app/state's null semantics to understand -->
{#if navigating.from !== null && navigating.from.route.id !== navigating.to?.route.id}

<!-- Good: intent readable without knowing the API -->
function isCrossRouteNavigation(): boolean { ... }

{#if isCrossRouteNavigation()}
```

## `{@const}` Placement

`{@const}` must be an **immediate child** of a block statement (`{#if}`, `{#each}`, `{:else}`, `{#snippet}`, etc.). Placing it inside an HTML element is a compile error:
Expand Down
6 changes: 6 additions & 0 deletions .claude/rules/sveltekit.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ const grade = gradeRaw as TaskGrade;

The same pattern applies to `url.searchParams.get()` in `+server.ts` handlers.

## `$app/state`: navigating Idle Check

`navigating` from `$app/state` always exists as an object. Use `navigating.from === null` to detect the idle state — not `navigating === null`.

To limit spinner display to cross-route navigation only, compare `navigating.from.route.id` with `navigating.to?.route.id`. Same-route query-param changes produce equal ids.

## Page Component Props

SvelteKit page components (`+page.svelte`) accept only `data` and `form` as props (`svelte/valid-prop-names-in-kit-pages`). Commented-out features that reference other props are not "dead code" — remove only the violating prop declaration, preserve the feature code.
Expand Down
17 changes: 12 additions & 5 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
// See:
// https://github.com/oekazuma/svelte-meta-tags
// https://oekazuma.github.io/svelte-meta-tags/ja/migration-guide/
import { page } from '$app/state';
import { navigating } from '$app/stores';
import { navigating, page } from '$app/state';

import { MetaTags, deepMerge } from 'svelte-meta-tags';

Expand All @@ -20,6 +19,16 @@
let { data, children } = $props();

let metaTags = $derived(deepMerge(data.baseMetaTags, page.data.pageMetaTags));

// $app/state's navigating has from === null when no navigation is occurring (unlike $app/stores
// where the entire object is null). route.id is a route path pattern (e.g. "/workbooks"),
// so same-route param changes produce equal ids and do not trigger the spinner.
//
// See:
// https://svelte.dev/docs/kit/$app-state#navigating
function isCrossRouteNavigation(): boolean {
return navigating.from !== null && navigating.from.route.id !== navigating.to?.route.id;
}
</script>

<Header />
Expand All @@ -29,9 +38,7 @@

<ErrorMessageToast errorMessage={$errorMessageStore} />

<!-- See: -->
<!-- https://svelte.dev/docs/kit/$app-stores#navigating -->
{#if $navigating}
{#if isCrossRouteNavigation()}
<SpinnerWrapper />
{:else}
{@render children?.()}
Expand Down
Loading