Skip to content

Commit c6772a3

Browse files
committed
Document Svelte 5 runes const/let pitfall and fix violations
Critical updates to frontend-development skill: - Added #5: Cannot assign to constant with $props/$bindable - Added #6: Cannot bind to constant with bind: directives - Added #7: Missing page title in E2E tests - Added Svelte 5 Runes Critical Rules section Fixes: - RetroInput: Changed const to let for $props destructuring - +layout: Added <svelte:head> with title for E2E tests Next: Fix ModernJourneyFull.svelte const violation
1 parent 06daf38 commit c6772a3

File tree

3 files changed

+125
-3
lines changed

3 files changed

+125
-3
lines changed

.claude-skills/frontend-development_skill/SKILL.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,121 @@ console.log('User logged in:', user);
606606
- Cause: Client not regenerated after backend changes
607607
- Fix: `cd frontend && bun run gen`
608608

609+
**5. "Cannot assign to constant" with $props() or $bindable()**
610+
- **Cause:** Using `const` instead of `let` in destructuring assignment with Svelte 5 runes
611+
- **Symptom:** `[plugin:vite-plugin-svelte] Cannot assign to constant https://svelte.dev/e/constant_assignment`
612+
- **Fix:** Use `let` for destructuring when you need to reassign values
613+
614+
```typescript
615+
// ❌ WRONG - Cannot mutate const
616+
const {
617+
value = $bindable(""),
618+
items = $state([])
619+
} = $props();
620+
621+
// ✅ CORRECT - Use let for mutable runes
622+
let {
623+
value = $bindable(""),
624+
items = $state([])
625+
} = $props();
626+
627+
// For $bindable in components:
628+
let {
629+
value = $bindable("")
630+
} = $props();
631+
632+
// Then in template:
633+
<input
634+
{value}
635+
oninput={(e) => {
636+
value = e.currentTarget.value; // This requires 'let'
637+
}}
638+
/>
639+
```
640+
641+
**6. "Cannot bind to constant" with bind: directives**
642+
- **Cause:** Trying to use `bind:value` with $bindable() (Svelte 5 two-way binding)
643+
- **Fix:** Use value + oninput pattern instead
644+
645+
```svelte
646+
<!-- ❌ WRONG - Can't use bind: with $bindable -->
647+
<input bind:value />
648+
649+
<!-- ✅ CORRECT - Manual two-way binding -->
650+
<input
651+
{value}
652+
oninput={(e) => {
653+
value = e.currentTarget.value;
654+
oninput?.(e); // Call optional handler
655+
}}
656+
/>
657+
```
658+
659+
**7. Missing page title in E2E tests**
660+
- **Cause:** No `<svelte:head>` with `<title>` in root layout
661+
- **Symptom:** Playwright tests fail with `expected page to have title`
662+
- **Fix:** Add title in root layout AFTER closing `</script>` tag
663+
664+
```svelte
665+
<script lang="ts">
666+
// ... your code
667+
</script>
668+
669+
<svelte:head>
670+
<title>ScreenGraph</title>
671+
</svelte:head>
672+
673+
<!-- rest of layout -->
674+
```
675+
676+
---
677+
678+
## Svelte 5 Runes Critical Rules
679+
680+
### $props() Destructuring
681+
682+
**ALWAYS use `let` not `const`:**
683+
```typescript
684+
// ❌ BAD - causes "cannot assign to constant"
685+
const { value = $bindable("") } = $props();
686+
687+
// ✅ GOOD
688+
let { value = $bindable("") } = $props();
689+
```
690+
691+
### $bindable() Two-Way Binding
692+
693+
**Cannot use `bind:` directive:**
694+
```svelte
695+
<!-- ❌ BAD -->
696+
<script lang="ts">
697+
let { value = $bindable("") } = $props();
698+
</script>
699+
<input bind:value />
700+
701+
<!-- ✅ GOOD -->
702+
<script lang="ts">
703+
let { value = $bindable("") } = $props();
704+
</script>
705+
<input
706+
{value}
707+
oninput={(e) => value = e.currentTarget.value}
708+
/>
709+
```
710+
711+
### $state() Reactive Variables
712+
713+
**Use `let` for mutable state:**
714+
```typescript
715+
// ❌ BAD
716+
const count = $state(0);
717+
count++; // Error: cannot assign
718+
719+
// ✅ GOOD
720+
let count = $state(0);
721+
count++; // Works!
722+
```
723+
609724
---
610725

611726
## Quality Checklist

frontend/src/lib/components/RetroInput.svelte

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Usage:
1818
const inputId = `retro-input-${Math.random().toString(36).substring(2, 9)}`;
1919
2020
/** Input label text */
21-
const {
21+
let {
2222
label = undefined,
2323
/** Input type */
2424
type = "text",
@@ -58,9 +58,12 @@ const {
5858
{type}
5959
{placeholder}
6060
{required}
61-
bind:value
61+
{value}
62+
oninput={(e) => {
63+
value = e.currentTarget.value;
64+
oninput?.(e);
65+
}}
6266
{onchange}
63-
{oninput}
6467
class="w-full px-4 py-3 bg-white retro-shadow rounded-xl text-[var(--color-charcoal)] placeholder:text-[var(--color-text-secondary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-charcoal)] focus:ring-offset-2 transition-all"
6568
/>
6669
</div>

frontend/src/routes/+layout.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ function isActive(path: string): boolean {
3131
}
3232
</script>
3333

34+
<svelte:head>
35+
<title>ScreenGraph</title>
36+
</svelte:head>
37+
3438
<!-- App Header with Skeleton v3 -->
3539
<header class="bg-surface-100-900-token border-b border-surface-300-700-token">
3640
<div class="container mx-auto flex h-14 items-center px-4">

0 commit comments

Comments
 (0)