mcpField
S
shadcn-svelte-inertia

shadcn-svelte-inertia

github.com/inertia-rails開発7094 回インストール37 週間前に更新
リポジトリ

shadcn-svelte-inertia について

Inertia.js と Svelte を使う Rails アプリ向けに、shadcn-svelte と bits-ui のパターンを適用するスキルです。SvelteKit ではなく、フォーム、ナビゲーション、ダイアログ、テーマ設定や連携時の注意点を扱います。

主な機能

  1. 01shadcn-svelte のパターンを Inertia と Rails 向けに適用
  2. 02Inertia のナビゲーションとフォームの規約を解説
  3. 03ダイアログ、テーブル、トースト連携を網羅
  4. 04ダークモードとリアクティブなページ状態を解説
  5. 05トラブルシューティングとコンポーネント資料を提供

活用シーン

  1. 01Rails と Inertia のアプリで shadcn-svelte の画面を構築
  2. 02SvelteKit の例を Inertia の規約に合わせて変更
  3. 03フォーム送信や古いページプロパティの問題を調査
  4. 04フラッシュメッセージを使ったトースト通知の追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/inertia-rails/skills --skill shadcn-svelte-inertia

インストール後、Claude Code で/shadcn-svelte-inertiaを入力して読み込まれたか確認します。

同梱ファイルreferences/components.mdreferences/flash-toast.md

詳細

Inertia Rails 向け shadcn-svelte

Inertia.js + Rails + Svelte 向けに調整した shadcn-svelte(bits-ui)のパターンです。SvelteKit ではありません。

shadcn-svelte の例を使う前に、次の点を確認してください。

  • SvelteKit 固有の API を使っていますか?(goto、$app/navigation、load 関数、+page.svelte)→ Inertia の router、サーバープロパティ、ページコンポーネントに置き換えます
  • sveltekit-superforms + zod を使っていますか? → Inertia の <Form> + name 属性に置き換えます。Inertia が CSRF、エラー、リダイレクト、処理状態を処理します。

SvelteKit のデフォルトとの主な違い

shadcn-svelte のデフォルト(SvelteKit)Inertia での相当機能
$app/navigation の goto()@inertiajs/svelte の router
load 関数Rails コントローラー経由のサーバーレンダリング済みプロパティ
+page.svelte / +layout.svelteモジュールスクリプトのレイアウトを含むデフォルトエクスポート
sveltekit-superforms + zodInertia の <Form> コンポーネント
<svelte:head>(SvelteKit が自動管理)<svelte:head>(同じです。Svelte には Inertia の <Head> はありません)

セットアップ

npx shadcn-svelte@latest init を実行します。tsconfig.json に @/ 解決エイリアスがなければ追加します。 vite.config.ts に @/ 解決エイリアスを追加しないでください — vite-plugin-ruby がすでに提供しています。

Inertia の <Form> で使う shadcn-svelte の入力要素

Inertia の <Form> 内では、name 属性を指定した通常の shadcn-svelte の Input/Label/Button を使います。 <Form> API 全体については、inertia-rails-forms スキル(および references/svelte.md)を参照してください。

重要なパターン: {#snippet} を使ってフォームの状態にアクセスします。

<script lang="ts">
  import { Form } from '@inertiajs/svelte'
  import { Input } from '$lib/components/ui/input'
  import { Label } from '$lib/components/ui/label'
  import { Button } from '$lib/components/ui/button'
</script>

<Form method="post" action="/users">
  {#snippet children({ errors, processing })}
    <div class="space-y-4">
      <div>
        <Label for="name">Name</Label>
        <Input id="name" name="name" />
        {#if errors.name}<p class="text-sm text-destructive">{errors.name}</p>{/if}
      </div>

      <div>
        <Label for="email">Email</Label>
        <Input id="email" name="email" type="email" />
        {#if errors.email}<p class="text-sm text-destructive">{errors.email}</p>{/if}
      </div>

      <Button type="submit" disabled={processing}>
        {processing ? 'Creating...' : 'Create User'}
      </Button>
    </div>
  {/snippet}
</Form>

Svelte 4 では、{#snippet} の代わりに <Form let:errors let:processing> を使います。

Inertia の <Form> と連携するには、<Select> に name プロパティが必要です。

<Select name="role" value="member">
  <SelectTrigger><SelectValue placeholder="Select role" /></SelectTrigger>
  <SelectContent>
    <SelectItem value="admin">Admin</SelectItem>
    <SelectItem value="member">Member</SelectItem>
  </SelectContent>
</Select>

Inertia ナビゲーションで使うダイアログ

<script lang="ts">
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from '$lib/components/ui/dialog'
  import { router } from '@inertiajs/svelte'

  let { open, user }: { open: boolean; user: User } = $props()
</script>

<Dialog
  {open}
  onOpenChange={(isOpen) => { if (!isOpen) router.replaceProp('show_dialog', false) }}
>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>{user.name}</DialogTitle>
    </DialogHeader>
    
  </DialogContent>
</Dialog>

Svelte 4 では、onOpenChange の代わりに on:openChange を使います。

サーバーサイドソートを使うテーブル

<script lang="ts">
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '$lib/components/ui/table'
  import { router } from '@inertiajs/svelte'

  let { users, sort }: { users: User[]; sort: string } = $props()

  const handleSort = (column: string) => {
    router.get('/users', { sort: column }, { preserveState: true })
  }
</script>

<Table>
  <TableHeader>
    <TableRow>
      <TableHead class="cursor-pointer" onclick={() => handleSort('name')}>
        Name {sort === 'name' ? '↑' : ''}
      </TableHead>
      <TableHead>Email</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    {#each users as user (user.id)}
      <TableRow>
        <TableCell>{user.name}</TableCell>
        <TableCell>{user.email}</TableCell>
      </TableRow>
    {/each}
  </TableBody>
</Table>

SPA ナビゲーションを維持するには、行リンクに <a> ではなく <Link> または use:inertia を使います。

フラッシュメッセージを使うトースト

フラッシュ設定(flash_keys)は inertia-rails-controllers にあります。フラッシュへのアクセス($page.flash)は inertia-rails-pages にあります。このセクションでは、トースト UI の接続のみを説明します。

必須 — Sonner でフラッシュベースのトーストを実装する場合は、ファイル全体を読んでください。 references/flash-toast.md(約80行)— フラッシュ監視と svelte-sonner 連携の全体を説明しています。トースト UI を使わずにフラッシュ値を読むだけの場合は、読み込まないでください。

ダークモード

npx shadcn-svelte@latest init は、ライト/ダーク用の CSS 変数と CSS(Tailwind v4)の @custom-variant dark (&:is(.dark *)); を生成します。

重要 — テーマのちらつき(FOUC)を防止してください。 <head> 内に、Svelte のハイドレーション前に実行されるインラインスクリプトを追加します。

<%# app/views/layouts/application.html.erb — in <head>, before any stylesheets %>
<script>
  document.documentElement.classList.toggle(
    "dark",
    localStorage.appearance === "dark" ||
      (!("appearance" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches),
  );
</script>

useAppearance パターン(ライト/ダーク/システムモード、localStorage への永続化、 matchMedia リスナー)を使います。<html> の .dark クラスを切り替えます。

<Head> の代わりに <svelte:head> を使用

Svelte ではネイティブの <svelte:head> を使います。Svelte 向けの Inertia の <Head> コンポーネントはありません。 これは shadcn のパターンにも当てはまります(たとえば、ダイアログビューでページタイトルを設定する場合)。

<svelte:head>
  <title>{user.name} - Profile</title>
</svelte:head>

Svelte 固有の注意点

bind:value は Inertia の <Form> では動作しません。 <Form> は Svelte のリアクティブバインディングではなく、送信時に入力要素の name 属性から値を読み取ります。 bind:value を使うと真実の情報源が二重になり、<Form> はその値を無視します。


<Form method="post" action="/users">
  <Input bind:value={name} />
</Form>


<Form method="post" action="/users">
  <Input name="name" />
</Form>

bind:value は、$form.name を明示的に管理する useForm でのみ使います。

$page ストアの更新はリアクティブですが、分割代入した値はリアクティブではありません。

<script lang="ts">
  import { page } from '@inertiajs/svelte'

  // BAD — snapshot, won't update after navigation:
  // let user = $page.props.auth.user

  // GOOD — use $derived for reactive access:
  let user = $derived($page.props.auth.user)
</script>

Svelte 4 では、$: user = $page.props.auth.user(リアクティブステートメント)を使います。

<Link> の代替として使う use:inertia ディレクティブ — <Link> にできない要素(テーブル行やカスタムコンポーネントなど)には、このアクションを使います。

<script lang="ts">
  import { inertia } from '@inertiajs/svelte'
</script>

<tr use:inertia={{ href: `/users/${user.id}` }} class="cursor-pointer">
  <td>{user.name}</td>
</tr>

bits-ui のトランジションプロパティと Inertia ナビゲーション — transition* プロパティを持つ bits-ui コンポーネントは、退出アニメーションがナビゲーションより長く続くと、Inertia のページ遷移中に古いコンテンツを表示する場合があります。期間を短く設定するか、ページプロパティに依存するコンテンツで forceMount を使ってください。

トラブルシューティング

症状原因対処方法
フォームコンポーネントがクラッシュするsuperforms に依存する shadcn-svelte のフォームコンポーネントを使用しているInput/Label と errors.field の表示に置き換えます
Select の値が送信されないname プロパティがない<Select> に name="field" を追加します
ダイアログが予期せず閉じるonOpenChange ハンドラーがない、または誤っているonOpenChange={(open) => { if (!open) closeHandler() }} を使用します
テーマが一瞬ちらつく(FOUC)<head> にインラインの <script> がないスタイルシートより前にダークモード用スクリプトを追加します
bind:value の値が送信されない<Form> は Svelte のバインディングではなく name 属性を読み取るname 属性を使い、bind:value は useForm の場合にのみ使用します
ナビゲーション後、共有 props が古いままになる$derived を使わずに $page を分割代入しているリアクティブにアクセスするには $derived($page.props.auth.user) を使います

関連スキル

  • フォームコンポーネント → inertia-rails-forms + references/svelte.md(<Form> のスニペット、useForm)
  • Flash の設定 → inertia-rails-controllers(flash_keys initializer)
  • Flash へのアクセス → inertia-rails-pages + references/svelte.md($page.flash)
  • URL 駆動のダイアログ → inertia-rails-pages + references/svelte.md(router.get パターン)
  • use:inertia ディレクティブ → inertia-rails-pages + references/svelte.md

参考資料

上記に示されていない shadcn-svelte コンポーネント(Accordion、Sheet、Tabs、DropdownMenu、 Inertia パターンを使った AlertDialog)を構築する場合は、references/components.md(約200行)を読み込みます。

基本的な Form、Select、Dialog、Table の使用方法については、components.md を読み込まないでください。 上記の例で十分です。

コメント