frontend — すべてのユーザー、すべての状態で動作し、変更しやすい画面を実現する
まずリポジトリの CLAUDE.md、docs/conventions.md、docs/design-system.md に従ってください。このスキルはそれらを補完し、上書きしません。ビジュアルに関する作業(トークン、レイアウト、色、CSS)は css-design に属します。ユーザーとしてアプリを実行し、最終的な判断を下すのは verify-app の役割です。
スタックを選ぶ。 Vue 3 と React はどちらも第一級のサポートで、同じルールに従います。リポジトリで既に使われているフレームワークを使用してください。新しいアプリの場合は、デリバリープロファイルのフロントエンドデフォルト(リポジトリのREADMEを参照)を使用し、それ以外はトリアージ時に一度質問します。Vue: TypeScript + Vite(npm create vue@latest -- --ts --router --pinia --vitest --playwright --eslint --prettier)+ references/vue-patterns.md。React: TypeScript + Vite(またはリポジトリのメタフレームワーク)+ references/react-patterns.md。1つのアプリでフレームワークを混在させないでください。SEOのためにSSRまたはSSGが必要ですか?ADR(architect)にNuxtまたはNext.jsを記録してください。このスキルはどちらの場合もクライアントサイド部分をカバーします。
ワークフロー
- リポジトリの規約とインベントリを読み込む。
CLAUDE.md、docs/conventions.md、docs/design-system.mdを読み、再利用可能な部品(ls src/components src/features src/composables src/hooks src/stores)を一覧表示します。データレイヤー、ルーター、フォーム、i18nライブラリを検出します(Vue:grep -E '"(@tanstack/vue-query|pinia|vue-router|vee-validate|vue-i18n)"' package.json; React:grep -E '"(@tanstack/react-query|@tanstack/react-router|react-router|react-hook-form|i18next|react-intl)"' package.json)。2番目のデータレイヤー、ルーター、ストア、フォーム、i18nライブラリを追加しないでください。証拠: ストーリーのメモに、再利用されたコンポーネントとコンポーザブル/フック、および新しいものを、理由とともにリストします。 - コードの前に状態テーブルを書く。 各ビューについて: すべてのデータとその所在 — サーバー(クエリ)、URL(ルートパラメータ/検索)、共有クライアント(Piniaストア、Reactコンテキスト、またはリポジトリのストア)、ローカル(
ref/useState)、派生(computed/レンダー内のconst)、外部(ブラウザAPIをラップするコンポーザブルまたはuseSyncExternalStore)。次に、4つの状態(ローディング、空、エラー、成功)とそれぞれのコピーキーをリストします。証拠: ストーリー内の表。 - 最初に失敗するテストを書く。 Vitest +
@testing-library/vueまたは@testing-library/react、userEvent.setup()、ロールとアクセシブル名によるクエリ、テストごとに1つの振る舞い、ネットワークはサービス境界でフェイク(MSWまたはsrc/test-utils内のリポジトリのフェイククライアント)。各状態、キーボードパス、1つのネガティブケースをカバーします。証拠: 実行結果が正しい理由で赤色になること。 - 機能フォルダ内でビルドする。
src/features/<name>/services(型付けされたAPIコール、フレームワークなし)→composables/hooks(useQuery/useMutationとキーファクトリをラップするuseX)→components(<script setup lang="ts">付きのName.vue、またはName.tsx)→ それらを構成するルートビュー。Propsとイベントは型付けされ、anyなし、非null!なし、@/インポート。証拠:git diff --statが機能フォルダと意図的な共有変更の範囲内に収まっていること。 - コピーとフォーマット。 表示されるすべての文字列は、ロケールファイル(vue-i18n
useI18n()、またはReactリポジトリのi18nライブラリまたはコピーモジュール)から取得し、すべてのロケールで同じキーを使用します。テストでパリティを強制します。数値と日付は、i18nフォーマッターまたはIntlを使用します。証拠: パリティテストが緑色になること。ハードコードされたコピーがないこと(lintで強制:@intlify/vue-i18n/no-raw-text; React:react/jsx-no-literalsまたはi18nプラグインのルール) — lintの終了コードを引用します。 css-designでスタイルを設定する。 トークンのみ、BEMブロック = コンポーネント名、すべてのスタイルが各状態に対応。.vueスタイルはpostcss-htmlを通じてlintされます。証拠: Stylelintが緑色。- ストーリー(コンポーネントライブラリ)。 各状態に1つのストーリー、両方のテーマ。Storybookのaxeが合格すること。証拠: ストーリーのテストとaxeの実行。
- ゲート、終了コードで連鎖。
npm run lint && npm run type-check && npm run test:unit -- --run(Vue:vue-tsc --build; React:tsc -b)、またはmake ci。tailからの出力で判断しないでください。証拠: 終了コード0、カバレッジがリポジトリのしきい値以上。 - 本番ビルドに対して実際のブラウザでジャーニーを実行する。
npm run buildして出力を提供する(vite preview、またはリポジトリが使用する静的サーバー)。@axe-core/playwrightを使用してメインパス用のPlaywrightスペックを追加または拡張します(タグwcag2a wcag2aa wcag21aa wcag22aa、深刻度と重大度 = 0)。docs/delivery/<slug>/evidence/<view>-390.pngおよび-1280.pngをキャプチャし、アプリにダークテーマがある場合はダークテーマもキャプチャします。引用する前にすべてのスクリーンショットを開いて確認します。証拠: スペックが緑色、PNGファイル。 - キーボードとスクリーンリーダーの合格。 新しいビューをTabキーで移動する: 順序、可視フォーカス、Escapeキーでオーバーレイを閉じてフォーカスをトリガーに戻す。ナビゲーション後、フォーカスが新しい見出しに移動し、タイトルが変更される。スクリーンリーダー(VoiceOver
Cmd+F5、またはNVDA)で名前とアナウンスメントをスポットチェックする。証拠: ストーリーのQAセクションに3行のメモ。 - ハンドオーバー。
verify-appを実行してエンドツーエンドの主張を確認する。deliver-loop内で独立したレビューがアイテムを評価する。ここで彼らの手順を再記述しないでください。
コアチェックリスト
- 派生できるものは保存しない:
computed(React: レンダー内のconst)を使用する。他の状態を設定するだけのウォッチャーやエフェクトは使用しない。 - ウォッチャーとエフェクトは外部(DOM API、サブスクリプション、タイマー、サードパーティウィジェット)とのみ同期し、クリーンアップする(
onWatcherCleanup/onUnmounted; React: エフェクトの戻り値)。ユーザーアクションによって引き起こされる作業はイベントハンドラーで実行する。 - サーバーデータをローカル状態やストアにコピーしない。クエリキーはフェッチが使用するすべての入力を含める(Vueではrefまたはgetterとして、変更時に再フェッチされるようにする)。
- ユーザーが共有したりリロードしたりする選択、フィルター、タブ、ページネーションはURLクエリに配置する。
- すべてのデータビューはローディング、空、エラー(再試行付き)、成功をレンダリングし、テストは4つすべてをカバーする。
- すべての入力には可視の
<label for>がある(両方のフレームワークでuseId()からのID)。エラーはaria-describedbyで関連付けられたテキスト、入力は保持され、送信時にフォーカスが最初の無効なフィールドに移動する。 - ネイティブ要素を優先する(
button、a href/ルーターリンク、dialog、details、input type)。div @click/div onClickは使用しない。 - カスタムウィジェットは対応するWAI-ARIA APGパターンに従い、キーボードも含む。
- フォーカスを移動しない非同期結果は、一度だけレンダリングされ空の
role="status"/role="alert"リージョンを介してアナウンスする(Vue: bodyにテレポート; React: ビューより長生きする必要がある場合はポータル)。 - ルート変更:
document.titleを更新し、フォーカスをページのh1またはmainに移動する(Vue:router.afterEach+ テンプレートref; React: ルーターの同等のもの)。 - エラーはルートごとおよびリスクのあるウィジェットごとにキャッチする(Vue:
onErrorCaptured境界コンポーネント; React: エラーバウンダリ)。再試行を表示し、空白のページにしない。 - ハードコードされた表示文字列は使用しない。すべてのロケールが同じキーを持つ。
- テストはロール/ラベル/テキストでクエリし、
user-eventで操作し、クラス名、コンポーネント内部(wrapper.vm、状態値)、またはコンポーザブル/フックの呼び出しをアサートしない。 - 使用直前にDOMPurifyでサニタイズしない限り、
v-html/dangerouslySetInnerHTMLは使用しない。データからのURLはhttps:/mailto:/相対パスに許可リスト化する。 shallowRef/markRaw/v-memo/KeepAlive(React:memo/useMemo/useCallback)は、プロファイラーの計測または文書化された必要性がある場合のみ使用する。- チャートとデータ可視化には、アクセシブルな名前と要約、データテーブル(またはテキスト)の代替、色覚障害者に安全なパレット(シリーズはラベル、形状、パターンでも区別する)、すべてのインタラクションへのキーボードアクセスを用意する。リポジトリが使用しているチャートライブラリに従う。
アンチパターン
Vue:
watch(items, v => { filtered.value = v.filter(...) })—computedを使用する。const { count } = reactive(state)またはconst { name } = store— リアクティビティが失われる。toRefs/storeToRefsを使用するか、refを優先する。watch(props.id, ...)または分割されたpropをコンポーザブルに渡す — getter() => idを渡す。- propまたは
useQueryデータを変更する。クエリデータを直接v-modelにバインドする — フォームにコピーする。 - 同じ要素に
v-ifとv-forを使用する。並び替え可能なリストで:key="index"を使用する。 onWatcherCleanupの前、またはコンポーザブルでライフサイクルフックを登録する前にawaitを使用する。- ルートコンポーネントに
defineAsyncComponentを使用する(ルートは() => import(...)を直接受け取る)。 - ナビゲーションガードで
next()を使用する —false、ロケーション、または何も返さない。 wrapper.vmにアクセスしたり、setDataを呼び出したりするテスト。
React:
useEffect(() => setFiltered(items.filter(...)), [items])— レンダー内で計算する。- IDが変更されたときにエフェクトでフォームをリセットする —
key={id}を渡す。 - 条件やループ内でフックを呼び出す。状態を置き換える代わりにその場で変更する。
- 並び替え可能なリストで
key={index}を使用する。計測された理由でメモ化された子に、毎回のレンダーで新しいオブジェクトや関数のpropを渡す。
両方:
- キャンセル、キャッシュ、再試行、エラーブランチなしでマウント時にフェッチする — データレイヤーを使用する。
- 空またはエラーブランチのないスピナー。エラーを
console.logで握りつぶす。 - 毎日変化するデータに対して
staleTimeを0のままにするため、フォーカスするたびに再フェッチされる。 - スナップショット、ロールバック、確定時の無効化なしの楽観的更新。
- プレースホルダーをラベルとして使用する。エラーを示すために色だけに頼る。キーなしの
role="button"。 - いたるところで
getByTestIdを使用する。ページ全体のスナップショット。実装の詳細をテストする。 - すべてを最初のルートチャンクにプルする機能バレルファイル。
- スキームをチェックせずに
:href="user.url"/href={user.url}を使用する。
「完了」のエビデンス
- 各状態とキーボードパスに対する、失敗してから合格する単体テスト。
- Lint、
vue-tscまたはtsc、テストを連鎖し、終了コード0。カバレッジがしきい値以上。 - 本番ビルドに対するPlaywrightジャーニー、axeの深刻度/重大度 = 0。
docs/delivery/<slug>/evidence/内の390pxと1280pxのスクリーンショット(およびダークテーマ)、それぞれ開いて確認済み。- ストーリーのQAセクション内のキーボードとスクリーンリーダーのメモ。
- 次に
verify-app。
参考文献
references/vue-patterns.md— Vueチェックリスト: SFCおよびコンポーネントAPI、リアクティビティの落とし穴、computed vs watch、コンポーザブル、Pinia、TanStack Query for Vue、Vue Router(URL状態、ガード、フォーカス)、フォーム、i18n、アクセシビリティの詳細、パフォーマンス、セキュリティ、ツール。各項目に出典あり。references/react-patterns.md— Reactチェックリスト: 同じトピックに加え、静的エクスポート(Next.js / Nuxt generate)のバリアント。references/a11y-and-testing.md— UIコードのためのWCAG 2.2 AA項目、ARIA/APG、キーボードとライブリージョン、チャート、VueとReactのためのTesting Library、MSW、Playwright + axe、テストすべきでないもの。