diff --git a/docs/ja-JP/hooks/memory-persistence/README.md b/docs/ja-JP/hooks/memory-persistence/README.md new file mode 100644 index 00000000..cac252f9 --- /dev/null +++ b/docs/ja-JP/hooks/memory-persistence/README.md @@ -0,0 +1,44 @@ +# メモリ永続化フック + +これらのライフサイクルフック定義は、Claude CodeプラグインおよびマニュアルインストールにおけるECCのメモリ永続化コントラクトを文書化します。 + +実行可能な実装は `scripts/hooks/` にあります: + +- `session-start.js` は境界付けられた事前コンテキストを読み込み、プロジェクト状態を検出し、セッションメタデータを準備します。 +- `pre-compact.js` はコンテキスト圧縮前に状態をキャプチャします。 +- `session-end.js` はトランスクリプトメタデータが利用可能な場合にセッション終了サマリーを永続化します。 +- `observe-runner.js` は継続学習のためのツール使用観察を記録します。 +- `session-activity-tracker.js` はECC2のステータスと可観測性のためのツール使用とファイルアクティビティを記録します。 + +インストール済みフックグラフは引き続き `hooks/hooks.json` です。このディレクトリは、ハーネス監査と長文ドキュメントが参照する安定した人間が読めるライフサイクル定義サーフェスです。 + +## ライフサイクルコントラクト + +| イベント | フック | 目的 | ブロッキング | +|---|---|---|---| +| `SessionStart` | `session:start` | 境界付けられた事前コンテキストとプロジェクトメタデータを読み込む | いいえ | +| `PreCompact` | `pre:compact` | 圧縮前に状態を保存する | いいえ | +| `PreToolUse` | `pre:observe:continuous-learning` | 学習シグナルのためのツール意図をキャプチャ | いいえ | +| `PostToolUse` | `post:observe:continuous-learning` | 学習シグナルのためのツール結果をキャプチャ | いいえ | +| `PostToolUse` | `post:session-activity-tracker` | ECC2メトリクス用のツールとファイルアクティビティを記録 | いいえ | +| `Stop` | `stop:format-typecheck` | 編集後のバッチ品質ゲート | フック失敗時にブロック | +| `Stop` | `stop:check-console-log` | 変更されたファイルのデバッグログを監査 | フック出力による警告/エラー | + +## オペレーター期待値 + +- デフォルトでは永続化をローカルに保つ。 +- ユーザーが明示的にインテグレーションを有効にしない限り、トランスクリプトやツールトレースをホストサービスに送信しない。 +- `ECC_SESSION_START_MAX_CHARS` でセッション開始時に読み込まれるコンテキストを制限する。 +- `ECC_SESSION_START_CONTEXT=off` でオプトアウトを許可する。 +- `ECC_HOOK_PROFILE` と `ECC_DISABLED_HOOKS` でライフサイクルフックをプロファイルゲート管理する。 + +## 関連ファイル + +- `hooks/hooks.json` +- `hooks/README.md` +- `scripts/hooks/session-start.js` +- `scripts/hooks/pre-compact.js` +- `scripts/hooks/session-end.js` +- `scripts/hooks/observe-runner.js` +- `scripts/hooks/session-activity-tracker.js` +- `docs/architecture/observability-readiness.md` diff --git a/docs/ja-JP/skills/angular-developer/references/angular-animations.md b/docs/ja-JP/skills/angular-developer/references/angular-animations.md new file mode 100644 index 00000000..0b14d048 --- /dev/null +++ b/docs/ja-JP/skills/angular-developer/references/angular-animations.md @@ -0,0 +1,160 @@ +# Angular アニメーション + +Angularで要素をアニメーションさせる場合は、**まず `package.json` でプロジェクトのAngularバージョンを確認してください**。 +モダンなアプリケーション(**Angular v20.2以上**)では、`animate.enter` と `animate.leave` を使ったネイティブCSSを優先してください。古いアプリケーションでは、非推奨の `@angular/animations` パッケージを使用する必要がある場合があります。 + +## 1. ネイティブCSSアニメーション(v20.2以上 推奨) + +モダンなAngularは `animate.enter` と `animate.leave` を提供し、要素がDOMに追加・削除される際にアニメーションさせます。これらは適切なタイミングでCSSクラスを適用します。 + +### `animate.enter` と `animate.leave` + +要素に直接使用して、enter・leaveフェーズ中にCSSクラスを適用します。Angularはアニメーション完了時にenterクラスを自動的に削除します。`animate.leave` の場合、Angularはアニメーションが完了するまで要素をDOMから削除しません。 + +`animate.enter` の例: + +```html +@if (isShown()) { +
+

The box is entering.

+
+} +``` + +```css +/* トランジションを使用する場合は開始スタイルを定義してください */ +.enter-container { + border: 1px solid #dddddd; + margin-top: 1em; + padding: 20px; + font-weight: bold; + font-size: 20px; +} +.enter-container p { + margin: 0; +} +.enter-animation { + animation: slide-fade 1s; +} +@keyframes slide-fade { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} +``` + +_注意: `animate.leave` は削除される子要素に追加できます。_ + +### イベントバインディングとサードパーティライブラリ + +`(animate.enter)` と `(animate.leave)` にバインドして、関数を呼び出したりGSAPなどのJSライブラリを使用することができます。 + +```html +@if(show()) { +
...
+} +``` + +```ts +import { AnimationCallbackEvent } from '@angular/core'; + +onLeave(event: AnimationCallbackEvent) { + // カスタムアニメーションロジックをここに記述 + // 重要: Angularが要素を削除できるよう、完了時に必ず animationComplete() を呼び出してください! + event.animationComplete(); +} +``` + +## 2. 高度なCSSアニメーション + +CSSは高度なアニメーションシーケンスのための強力なツールを提供しています。 + +### 状態とスタイルのアニメーション + +プロパティバインディングを使用して要素のCSSクラスを切り替え、トランジションをトリガーします。 + +```html +
...
+``` + +```css +div { + transition: height 0.3s ease-out; + height: 100px; +} +div.open { + height: 200px; +} +``` + +### Autoハイトのアニメーション + +`css-grid` を使用してautoハイトへのアニメーションが可能です。 + +```css +.container { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 0.3s; +} +.container.open { + grid-template-rows: 1fr; +} +.container > div { + overflow: hidden; +} +``` + +### スタッガリングと並列アニメーション + +- **スタッガリング**: リスト内のアイテムに異なる値の `animation-delay` または `transition-delay` を使用します。 +- **並列**: `animation` ショートハンドで複数のアニメーションを適用します(例:`animation: rotate 3s, fade-in 2s;`)。 + +### プログラムによる制御 + +標準のWeb APIを使用してアニメーションを直接取得します: + +```ts +const animations = element.getAnimations(); +animations.forEach((anim) => anim.pause()); +``` + +## 3. レガシーアニメーションDSL(非推奨) + +古いプロジェクト(v20.2以前、または `@angular/animations` が既に大量に使用されているプロジェクト)では、コンポーネントメタデータDSLを使用します。 + +**重要:** レガシーアニメーションと `animate.enter`/`leave` を同じコンポーネント内で混在させないでください。 + +### セットアップ + +```ts +bootstrapApplication(App, { + providers: [provideAnimationsAsync()], +}); +``` + +### トランジションの定義 + +```ts +import {signal} from '@angular/core'; +import {trigger, state, style, animate, transition} from '@angular/animations'; + +@Component({ + animations: [ + trigger('openClose', [ + state('open', style({opacity: 1})), + state('closed', style({opacity: 0})), + transition('open <=> closed', [animate('0.5s')]), + ]), + ], + template: `
...
`, +}) +export class OpenClose { + isOpen = signal(true); +} +``` diff --git a/docs/ja-JP/skills/angular-developer/references/angular-aria.md b/docs/ja-JP/skills/angular-developer/references/angular-aria.md new file mode 100644 index 00000000..d41a7b52 --- /dev/null +++ b/docs/ja-JP/skills/angular-developer/references/angular-aria.md @@ -0,0 +1,410 @@ +# Angular Aria + +Angular Aria(`@angular/aria`)は、一般的なWAI-ARIAパターンを実装するヘッドレスでアクセシブルなディレクティブのコレクションです。これらのディレクティブは、キーボードインタラクション、ARIA属性、フォーカス管理、スクリーンリーダーのサポートを処理します。 + +**AIエージェントとしての役割は、HTMLの構造とCSSスタイリングを提供すること**であり、ディレクティブが複雑なアクセシビリティロジックを処理します。 + +## ヘッドレスコンポーネントのスタイリング + +Angular Ariaコンポーネントはヘッドレスであるため、デフォルトのスタイルは付属していません。ディレクティブが自動的に適用するARIA属性や構造クラスに基づいて、CSSで各状態をスタイリングする**必要があります**。 + +CSSでターゲットとする一般的なARIA属性: + +- `[aria-expanded="true"]` / `[aria-expanded="false"]` +- `[aria-selected="true"]` +- `[aria-disabled="true"]` +- `[aria-current="page"]`(ナビゲーション用) + +--- + +**重要**: このパッケージを使用する前に、パッケージマネージャーでインストールする必要があります。プロジェクトにインストールされていることを確認してください。必要な場合は `npm install @angular/aria` でインストールしてください。 + +## 1. アコーディオン + +関連するコンテンツを展開・折りたたみ可能なセクションに整理します。 + +**使用場面:** アコーディオンは、コンテンツの多いページでスクロールを減らすために、ユーザーが一度に1つずつ展開できる論理的なグループにコンテンツを整理するレイアウトコンポーネントです。FAQ、長いフォーム、または情報の段階的な開示に使用しますが、プライマリナビゲーションや、ユーザーが複数のコンテンツセクションを同時に表示する必要があるシナリオには使用しないでください。 + +**インポート:** `import { AccordionContent, AccordionGroup, AccordionPanel, AccordionTrigger } from '@angular/aria/accordion';` + +**ディレクティブ:** `ngAccordionGroup`、`ngAccordionTrigger`、`ngAccordionPanel`、`ngAccordionContent`(遅延ロード用)。 + +```ts +@Component({ + selector: 'app-cmp', + imports: [AccordionContent, AccordionGroup, AccordionPanel, AccordionTrigger], + template: `...`, + styles: [], +}) +export class App { + protected readonly title = signal('angular-app'); +} +``` + +```html +
+
+ +
+ +

Lazy loaded content here.

+
+
+
+
+``` + +**スタイリング戦略:** +トリガーの `[aria-expanded]` 属性をターゲットにしてアイコンを回転させ、パネルの表示をスタイリングします。 + +```css +.accordion-header[aria-expanded='true'] .icon { + transform: rotate(180deg); +} + +/* パネルディレクティブがDOM削除を処理しますが、トランジションをスタイリングできます */ +.accordion-panel { + padding: 1rem; + border-top: 1px solid #ccc; +} +``` + +--- + +## 2. リストボックス + +オプションのリストを表示するための基本的なディレクティブです。可視の選択リスト(ドロップダウンではない)に使用します。 + +**使用場面:** 可視の選択可能なリスト(単一または複数選択)。 + +**インポート:** `import {Listbox, Option} from '@angular/aria/listbox';` + +**ディレクティブ:** `ngListbox`、`ngOption`。 + +```ts +@Component({ + selector: 'app-cmp', + imports: [Listbox, Option], + template: `...`, + styles: [], +}) +export class App { + protected readonly title = signal('angular-app'); +} +``` + +```html + + +``` + +**スタイリング戦略:** +選択状態には `[aria-selected="true"]`、フォーカスされたアイテムには `:focus-visible` または `[data-active]` をターゲットにします(Angular Ariaはroving tabindexまたはactivedescendantを使用します)。 + +```css +.option { + padding: 8px; + cursor: pointer; +} +.option[aria-selected='true'] { + background: #e0f7fa; + font-weight: bold; +} +/* フォーカス状態はariaで管理されます */ +.option:focus-visible { + outline: 2px solid blue; +} +``` + +--- + +## 3. コンボボックス、セレクト、マルチセレクト + +これらのパターンは、`ngCombobox` とポップアップ内の `ngListbox` を組み合わせます。 + +- **コンボボックス**: テキスト入力 + ポップアップ(オートコンプリートに使用)。 +- **セレクト**: 読み取り専用コンボボックス + 単一選択リストボックス。 +- **マルチセレクト**: 読み取り専用コンボボックス + 複数選択リストボックス。 + +**使用場面:** コンボボックスは、テキスト入力をポップアップと同期させる低レベルのプリミティブディレクティブで、オートコンプリート、セレクト、マルチセレクトパターンの基盤となるロジックとして機能します。カスタムフィルタリング、独自の選択要件、または標準の文書化されたコンポーネントから逸脱した特殊な入力-ポップアップ連携を構築する場合に特に使用してください。 + +**インポート:** + +``` + import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox'; + import {Listbox, Option} from '@angular/aria/listbox'; +``` + +**ディレクティブ:** `ngCombobox`、`ngComboboxInput`、`ngComboboxPopupContainer`、`ngListbox`、`ngOption`。 + +```html + +
+ + + + + +
+``` + +**スタイリング戦略:** +ポップアップコンテナをコンテンツの上に浮かぶドロップダウンのように見せるスタイリングをします(CDK Overlayと組み合わせることが多い)。 + +```css +.select-trigger { + width: 200px; + padding: 8px; + text-align: left; +} +.dropdown-menu { + list-style: none; + padding: 0; + margin: 0; + border: 1px solid #ccc; + background: white; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} +``` + +--- + +## 4. メニューとメニューバー + +アクション、コマンド、コンテキストメニュー用(フォーム選択には使用しない)。 + +**使用場面:** メニューバーは、インターフェース全体に渡って持続するデスクトップスタイルのアプリケーションコマンドバー(例:ファイル、編集、表示)を構築するための高レベルのナビゲーションパターンです。完全な水平キーボードサポートを備えた論理的なトップレベルカテゴリへの複雑なコマンドの整理に最適ですが、シンプルなスタンドアロンアクションリストや水平スペースが制約されるモバイルファーストのレイアウトには避けてください。 + +**インポート:** `import {MenuBar, Menu, MenuContent, MenuItem} from '@angular/aria/menu';` + +**ディレクティブ:** `ngMenuBar`、`ngMenu`、`ngMenuItem`、`ngMenuTrigger`。 + +```html + + + + +``` + +**スタイリング戦略:** +メニューバーにはflexboxを使用します。トリガーの状態に基づいてサブメニューを表示・非表示にします。 + +```css +.menubar { + display: flex; + gap: 10px; + list-style: none; + padding: 0; +} +.menu { + background: white; + border: 1px solid #ccc; + padding: 5px 0; +} +.menu li { + padding: 5px 15px; + cursor: pointer; +} +``` + +--- + +## 5. タブ + +1つのパネルのみが表示される層状のコンテンツセクション。 + +**使用場面:** タブコンポーネントは、関連するコンテンツを明確なナビゲート可能なセクションに整理し、ユーザーがページを離れることなくカテゴリやビューを切り替えられるようにします。設定パネル、複数トピックのドキュメント、ダッシュボードに理想的ですが、順次ワークフロー(ステッパー)や7〜8セクションを超えるナビゲーションには避けてください。 + +**インポート:** `import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';` + +**ディレクティブ:** `ngTabs`、`ngTabList`、`ngTab`、`ngTabPanel`、`ngTabContent`。 + +```html +
+ + +
+ Profile Settings +
+
+ Security Settings +
+
+``` + +**スタイリング戦略:** +タブボタンの `[aria-selected="true"]` をターゲットにします。 + +```css +.tab-list { + display: flex; + border-bottom: 2px solid #ccc; + list-style: none; + padding: 0; +} +.tab-btn { + padding: 10px 20px; + cursor: pointer; + border-bottom: 2px solid transparent; +} +.tab-btn[aria-selected='true'] { + border-bottom-color: blue; + font-weight: bold; +} +.tab-panel { + padding: 20px; +} +``` + +--- + +## 6. ツールバー + +関連するコントロールをグループ化します(テキストフォーマットなど)。 + +**使用場面:** ツールバーは、頻繁にアクセスされる関連コントロールを1つの論理コンテナにグループ化するための組織コンポーネントです。テキストフォーマットやメディアコントロールなど、繰り返しアクションが必要なワークフローのキーボード効率(矢印キーナビゲーション)と視覚的構造を強化するために最適です。 + +**インポート:** `import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';` + +**ディレクティブ:** `ngToolbar`、`ngToolbarWidget`、`ngToolbarWidgetGroup`。 + +```html +
+
+ + +
+
+``` + +**スタイリング戦略:** +ツールバー内の `[aria-pressed="true"]`(トグルボタン用)または `[aria-checked="true"]`(ラジオグループ用)をターゲットにします。 + +```css +.toolbar { + display: flex; + gap: 5px; + padding: 8px; + background: #f5f5f5; +} +.tool-btn { + padding: 5px 10px; + border: 1px solid #ccc; +} +.tool-btn[aria-pressed='true'], +.tool-btn[aria-checked='true'] { + background: #ddd; +} +``` + +--- + +## 7. ツリー + +階層データを表示します(ファイルシステム、ネストされたナビゲーションなど)。 + +**使用場面:** ツリーコンポーネントは、ファイルシステム、組織図、複雑なサイトアーキテクチャなど、深くネストされた階層データ構造のナビゲーションと表示のために設計されています。ユーザーがブランチを展開・折りたたむ必要がある複数レベルの関係に特に使用しますが、フラットリスト、データテーブル、またはシンプルな選択メニューには避けてください。 + +**インポート:** `import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';` + +**ディレクティブ:** `ngTree`、`ngTreeItem`、`ngTreeGroup`。 + +```html + +``` + +**スタイリング戦略:** +`[aria-expanded]` をターゲットにして子を表示・非表示にするか、シェブロンアイコンを回転させます。ネストされたグループに `padding-left` を使用して階層を表示します。 + +```css +.tree, +.tree-group { + list-style: none; + padding-left: 20px; +} +.tree-label::before { + content: '> '; + display: inline-block; + transition: transform 0.2s; +} +li[aria-expanded='true'] > .tree-label::before { + transform: rotate(90deg); +} +``` + +## 8. グリッド + +矢印キーでナビゲーションを可能にする、セルの双方向インタラクティブコレクションです。 + +**使用場面:** データテーブル、カレンダー、スプレッドシート、インタラクティブ要素のレイアウトパターン。 +**ディレクティブ:** `ngGrid`、`ngGridRow`、`ngGridCell`、`ngGridCellWidget`。 + +```html + + + + + + + + + +
NameStatus
Project A + +
+``` + +**スタイリング戦略:** +選択されたセルには `[aria-selected="true"]`、アクティブなセルには `:focus-visible`(roving tabindex)または コンテナの `[aria-activedescendant]` をターゲットにします。 + +```css +.grid-table { + border-collapse: collapse; +} +[ngGridCell] { + padding: 8px; + border: 1px solid #ddd; +} +[ngGridCell][aria-selected='true'] { + background: #e3f2fd; +} +/* フォーカス状態はroving tabindexで管理されます */ +[ngGridCell]:focus-visible { + outline: 2px solid #2196f3; + outline-offset: -2px; +} +``` + +## エージェントへの一般的なルール + +1. **これらの特定のAriaパターンを実装する際は、` + +
+ +
+ +
+ @for (alias of aliases.controls; track $index) { + + } +
+ + + +``` + +## コントロールへのアクセス + +特に `FormArray` のコントロールに簡単にアクセスするためのゲッターを使用します。 + +```ts +get aliases() { + return this.profileForm.get('aliases') as FormArray; +} + +addAlias() { + this.aliases.push(this.fb.control('')); +} +``` + +## 値の更新 + +- `patchValue()`:指定されたプロパティのみを更新します。構造の不一致があっても警告なく無視します。 +- `setValue()`:モデル全体を置き換えます。フォームの構造を厳密に強制します。 + +```ts +updateProfile() { + this.profileForm.patchValue({ + firstName: 'Nancy', + address: { street: '123 Drew Street' } + }); +} +``` + +## 統合変更イベント + +モダンな Angular(v18+)では、すべてのコントロールに単一の `events` オブザーバブルが提供され、値、ステータス、pristine、touched、リセット、サブミットイベントを追跡できます。 + +```ts +import {ValueChangeEvent, StatusChangeEvent} from '@angular/forms'; + +this.profileForm.events.subscribe((event) => { + if (event instanceof ValueChangeEvent) { + console.log('New value:', event.value); + } +}); +``` + +## 手動ステート管理 + +- `markAsTouched()` / `markAllAsTouched()`:サブミット時にバリデーションエラーを表示するのに便利です。 +- `markAsDirty()` / `markAsPristine()`:値が変更されたかどうかを追跡します。 +- `updateValueAndValidity()`:値とステータスの再計算を手動でトリガーします。 +- ほとんどのメソッドに `{ emitEvent: false }` または `{ onlySelf: true }` オプションを渡して伝播を制御できます。 diff --git a/docs/ja-JP/skills/angular-developer/references/rendering-strategies.md b/docs/ja-JP/skills/angular-developer/references/rendering-strategies.md new file mode 100644 index 00000000..2688bb10 --- /dev/null +++ b/docs/ja-JP/skills/angular-developer/references/rendering-strategies.md @@ -0,0 +1,44 @@ +# レンダリング戦略 + +Angular は SEO、パフォーマンス、インタラクティビティを最適化するために、複数のレンダリング戦略をサポートしています。 + +## 1. クライアントサイドレンダリング(CSR) + +**デフォルト戦略。** コンテンツはブラウザで完全にレンダリングされます。 + +- **ユースケース**:インタラクティブなダッシュボード、社内ツール。 +- **メリット**:設定が最もシンプルで、サーバーコストが低い。 +- **デメリット**:SEO が弱く、初期コンテンツの表示が遅い(JS を待つ必要がある)。 + +## 2. 静的サイト生成(SSG / プリレンダリング) + +コンテンツは**ビルド時**に静的 HTML ファイルとして事前にレンダリングされます。 + +- **ユースケース**:マーケティングページ、ブログ、ドキュメント。 +- **メリット**:最速の初期読み込み、優れた SEO、CDN フレンドリー。 +- **デメリット**:コンテンツ更新のたびに再ビルドが必要で、ユーザー固有のデータには対応できない。 + +## 3. サーバーサイドレンダリング(SSR) + +コンテンツは**初回リクエスト**に対してサーバー上でレンダリングされます。その後のナビゲーションはクライアントサイドで行われます(SPA スタイル)。 + +- **ユースケース**:EC サイトの商品ページ、ニュースサイト、パーソナライズされた動的コンテンツ。 +- **メリット**:優れた SEO、初期コンテンツの表示が速い。 +- **デメリット**:サーバー(Node.js)が必要で、サーバーコスト/レイテンシが高い。 + +## ハイドレーション + +ハイドレーションは、サーバーレンダリングされた HTML をブラウザでインタラクティブにするプロセスです。 + +- **フルハイドレーション**:アプリ全体が一度にインタラクティブになります。 +- **インクリメンタルハイドレーション**:(高度)`@defer` ブロックを使用して必要に応じてパーツがインタラクティブになります。 +- **イベントリプレイ**:ハイドレーション完了前に発生したユーザーイベントをキャプチャして再生します。 + +## 判断マトリックス + +| 要件 | 戦略 | +| :------------------------------ | :------------------- | +| **SEO + 静的コンテンツ** | SSG | +| **SEO + 動的コンテンツ** | SSR | +| **SEO不要 + 高インタラクティビティ** | CSR | +| **混在** | ハイブリッド(ルートベース) | diff --git a/docs/ja-JP/skills/angular-developer/references/resource.md b/docs/ja-JP/skills/angular-developer/references/resource.md new file mode 100644 index 00000000..e6df642c --- /dev/null +++ b/docs/ja-JP/skills/angular-developer/references/resource.md @@ -0,0 +1,77 @@ +# `resource` を使った非同期リアクティビティ + +> [!IMPORTANT] +> `resource` API は現在 Angular で実験的な機能です。 + +`Resource` は非同期データフェッチを Angular のシグナルベースのリアクティビティに組み込みます。依存関係が変わるたびに非同期ローダー関数を実行し、状態と結果を同期的なシグナルとして公開します。 + +## 基本的な使い方 + +`resource` 関数は主に2つのプロパティを持つオプションオブジェクトを受け取ります: + +1. `params`:リアクティブな計算(`computed` のようなもの)。ここで読み取られるシグナルが変わると、リソースは再フェッチします。 +2. `loader`:パラメーターに基づいてデータを取得する非同期関数。 + +```ts +import { Component, resource, signal, computed } from '@angular/core'; + +@Component({...}) +export class UserProfile { + userId = signal('123'); + + userResource = resource({ + // userId をリアクティブに追跡 + params: () => ({ id: this.userId() }), + + // params が変わるたびに実行される + loader: async ({ params, abortSignal }) => { + const response = await fetch(`/api/users/${params.id}`, { signal: abortSignal }); + if (!response.ok) throw new Error('Network error'); + return response.json(); + } + }); + + // 計算済みシグナル内でリソースの値を使用する + userName = computed(() => { + if (this.userResource.hasValue()) { + return this.userResource.value()?.name; + } else { + return 'Loading...'; + } + }); +} +``` + +## リクエストの中断 + +前のローダーがまだ実行中に `params` シグナルが変わった場合、`Resource` は提供された `abortSignal` を使って実行中のリクエストを中断しようとします。**`fetch` の呼び出しには常に `abortSignal` を渡してください。** + +## データの再読み込み + +params を変えずにローダーを強制的に再実行させるには、`.reload()` を呼び出します。 + +```ts +this.userResource.reload(); +``` + +## リソースのステータスシグナル + +`Resource` オブジェクトは現在の状態を読み取るためのいくつかのシグナルを提供します: + +- `value()`:解決済みデータ、または `undefined`。 +- `hasValue()`:型ガードとなるブール値。値が存在する場合は `true`。 +- `isLoading()`:ローダーが現在実行中かどうかを示すブール値。 +- `error()`:ローダーがスローしたエラー、または `undefined`。 +- `status()`:正確な状態を表す文字列定数(`'idle'`、`'loading'`、`'resolved'`、`'error'`、`'reloading'`、`'local'`)。 + +## ローカルミューテーション + +リソースの値を直接楽観的に更新できます。これにより状態が `'local'` に変わります。 + +```ts +this.userResource.value.set({name: 'Optimistic Update'}); +``` + +## `httpResource` を使ったリアクティブなデータフェッチ + +Angular の `HttpClient` を使用している場合は、`httpResource` の使用を優先してください。これはインターセプターを含む Angular の HTTP スタックを活用しながら、同じシグナルベースのリソース API を提供する専用ラッパーです。 diff --git a/docs/ja-JP/skills/angular-developer/references/route-animations.md b/docs/ja-JP/skills/angular-developer/references/route-animations.md new file mode 100644 index 00000000..ced347b4 --- /dev/null +++ b/docs/ja-JP/skills/angular-developer/references/route-animations.md @@ -0,0 +1,56 @@ +# ルートトランジションアニメーション + +Angular ルーターは、ルート間のスムーズなビジュアルトランジションのためにブラウザの **View Transitions API** をサポートしています。 + +## ビュートランジションの有効化 + +ルーター設定に `withViewTransitions()` を追加します。 + +```ts +provideRouter(routes, withViewTransitions()); +``` + +これは**プログレッシブエンハンスメント**です。API をサポートしていないブラウザでも、ルーターは引き続き動作しますが、トランジションアニメーションは行われません。 + +## 仕組み + +1. ブラウザが古い状態のスクリーンショットを撮ります。 +2. ルーターが DOM を更新します(新しいコンポーネントをアクティベートします)。 +3. ブラウザが新しい状態のスクリーンショットを撮ります。 +4. ブラウザが2つの状態の間をアニメーションします。 + +## CSS によるカスタマイズ + +トランジションは**グローバル CSS ファイル**でカスタマイズします(コンポーネントスコープの CSS ではありません)。 + +`::view-transition-old()` と `::view-transition-new()` 疑似要素を使用します。 + +```css +/* 例:クロスフェード + スライド */ +::view-transition-old(root) { + animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out; +} +::view-transition-new(root) { + animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in; +} +``` + +## 高度な制御 + +`onViewTransitionCreated` を使用して、ナビゲーションコンテキストに基づいてトランジションをスキップしたり、動作をカスタマイズしたりします。 + +```ts +withViewTransitions({ + onViewTransitionCreated: ({transition, from, to}) => { + // 特定のルートに対してアニメーションをスキップ + if (to.url === '/no-animation') { + transition.skipTransition(); + } + }, +}); +``` + +## ベストプラクティス + +- **グローバルスタイル**:ビューカプセル化の問題を避けるため、トランジションアニメーションは常に `styles.css` で定義してください。 +- **ビュートランジション名**:ルートをまたいでスムーズにトランジションさせたい要素(ヘッダー画像など)には、一意の `view-transition-name` を割り当ててください。 diff --git a/docs/ja-JP/skills/angular-developer/references/route-guards.md b/docs/ja-JP/skills/angular-developer/references/route-guards.md new file mode 100644 index 00000000..20ae1a8a --- /dev/null +++ b/docs/ja-JP/skills/angular-developer/references/route-guards.md @@ -0,0 +1,52 @@ +# ルートガード + +ルートガードは、ユーザーがルートへ遷移できるか、またはルートから離れられるかを制御します。 + +## ガードの種類 + +- **`CanActivate`**:ユーザーはこのルートにアクセスできるか?(例:認証チェック) +- **`CanActivateChild`**:ユーザーはこのルートの子にアクセスできるか? +- **`CanDeactivate`**:ユーザーはこのルートから離れられるか?(例:未保存の変更) +- **`CanMatch`**:このルートはマッチングの対象として考慮すべきか?(例:フィーチャーフラグ)`false` を返すと、ルーターは他のルートのチェックを継続します。 + +## ガードの作成 + +Angular 15 以降、ガードは通常、関数として定義します。 + +```ts +export const authGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isLoggedIn()) { + return true; + } + + // ログインページへリダイレクト + return router.parseUrl('/login'); +}; +``` + +## ガードの適用 + +ルート設定に配列として追加します。順番通りに実行されます。 + +```ts +{ + path: 'admin', + component: Admin, + canActivate: [authGuard], + canActivateChild: [adminChildGuard], + canDeactivate: [unsavedChangesGuard] +} +``` + +## 戻り値 + +- `boolean`:`true` で許可、`false` でブロック。 +- `UrlTree` または `RedirectCommand`:別のルートへリダイレクト。 +- `Observable` または `Promise`:上記の型に解決されます。 + +## セキュリティに関する注意 + +**クライアントサイドのガードはサーバーサイドのセキュリティの代わりにはなりません。** サーバー側で常に権限を検証してください。 diff --git a/docs/ja-JP/skills/angular-developer/references/router-lifecycle.md b/docs/ja-JP/skills/angular-developer/references/router-lifecycle.md new file mode 100644 index 00000000..5f985465 --- /dev/null +++ b/docs/ja-JP/skills/angular-developer/references/router-lifecycle.md @@ -0,0 +1,45 @@ +# ルーターのライフサイクルとイベント + +Angular ルーターは `Router.events` オブザーバブルを通じてイベントを発行し、開始から終了までのナビゲーションのライフサイクルを追跡できます。 + +## 主要なルーターイベント(時系列順) + +1. **`NavigationStart`**:ナビゲーションが開始されます。 +2. **`RoutesRecognized`**:ルーターが URL をルートにマッチさせます。 +3. **`GuardsCheckStart` / `End`**:`canActivate`、`canMatch` などの評価が行われます。 +4. **`ResolveStart` / `End`**:データ解決フェーズ(リゾルバーによるデータの取得)。 +5. **`NavigationEnd`**:ナビゲーションが正常に完了しました。 +6. **`NavigationCancel`**:ナビゲーションがキャンセルされました(例:ガードが `false` を返した)。 +7. **`NavigationError`**:ナビゲーションが失敗しました(例:リゾルバーでのエラー)。 + +## イベントのサブスクライブ + +`Router` を注入して `events` オブザーバブルをフィルタリングします。 + +```ts +import {Router, NavigationStart, NavigationEnd} from '@angular/router'; + +export class MyService { + private router = inject(Router); + + constructor() { + this.router.events.pipe(filter((e) => e instanceof NavigationEnd)).subscribe((event) => { + console.log('Navigated to:', event.url); + }); + } +} +``` + +## デバッグ + +アプリケーション起動時にすべてのルーティングイベントの詳細なコンソールログを有効化します。 + +```ts +provideRouter(routes, withDebugTracing()); +``` + +## よくあるユースケース + +- **ローディングインジケーター**:`NavigationStart` が発火したときにスピナーを表示し、`NavigationEnd`/`Cancel`/`Error` で非表示にします。 +- **アナリティクス**:`NavigationEnd` をリッスンしてページビューを追跡します。 +- **スクロール管理**:カスタムスクロール動作のために `Scroll` イベントに応答します。 diff --git a/docs/ja-JP/skills/angular-developer/references/router-testing.md b/docs/ja-JP/skills/angular-developer/references/router-testing.md new file mode 100644 index 00000000..0fa8fbb4 --- /dev/null +++ b/docs/ja-JP/skills/angular-developer/references/router-testing.md @@ -0,0 +1,87 @@ +# RouterTestingHarness を使ったテスト + +ルーティングを伴うコンポーネントをテストする場合、**ルーターや関連サービスをモック化しない**ことが重要です。代わりに `RouterTestingHarness` を使用してください。これにより、実際のアプリケーションに近い環境でルーティングロジックをテストするための堅牢で信頼性の高い方法が提供されます。 + +ハーネスを使用することで、実際のルーター設定、ガード、リゾルバーをテストでき、より意味のあるテストにつながります。 + +## ルーターテストのセットアップ + +`RouterTestingHarness` はルーティングシナリオをテストするための主要なツールです。また、`TestBed` 設定内で `provideRouter` 関数を使ってテスト用ルートを提供する必要があります。 + +### セットアップ例 + +```ts +import {TestBed} from '@angular/core/testing'; +import {provideRouter} from '@angular/router'; +import {RouterTestingHarness} from '@angular/router/testing'; +import {Dashboard} from './dashboard.component'; +import {HeroDetail} from './hero-detail.component'; + +describe('Dashboard Component Routing', () => { + let harness: RouterTestingHarness; + + beforeEach(async () => { + // 1. テスト用ルートで TestBed を設定 + await TestBed.configureTestingModule({ + providers: [ + // テスト固有のルートで provideRouter を使用 + provideRouter([ + {path: '', component: Dashboard}, + {path: 'heroes/:id', component: HeroDetail}, + ]), + ], + }).compileComponents(); + + // 2. RouterTestingHarness を作成 + harness = await RouterTestingHarness.create(); + }); +}); +``` + +### 主要なコンセプト + +1. **`provideRouter([...])`**:テスト用のルーティング設定を提供します。テスト対象のコンポーネントが正しく機能するために必要なルートを含める必要があります。 +2. **`RouterTestingHarness.create()`**:ハーネスを非同期で作成・初期化し、ルート URL(`/`)への初回ナビゲーションを実行します。 + +## ルーターテストの作成 + +ハーネスを作成したら、ナビゲーションを駆動し、ルーターの状態やアクティベートされたコンポーネントの状態に対してアサーションを行えます。 + +### 例:ナビゲーションのテスト + +```ts +it('should navigate to a hero detail when a hero is selected', async () => { + // 1. 初期コンポーネントにナビゲートし、そのインスタンスを取得 + const dashboard = await harness.navigateByUrl('/', Dashboard); + + // ダッシュボードにヒーローを選択するメソッドがあると仮定 + const heroToSelect = {id: 42, name: 'Test Hero'}; + dashboard.selectHero(heroToSelect); + + // ナビゲーションをトリガーするアクション後の安定を待つ + await harness.fixture.whenStable(); + + // 2. URL に対してアサート + expect(harness.router.url).toEqual('/heroes/42'); + + // 3. ナビゲーション後にアクティベートされたコンポーネントを取得 + const heroDetail = await harness.getHarness(HeroDetail); + + // 4. 新しいコンポーネントの状態に対してアサート + expect(await heroDetail.componentInstance.hero.name).toBe('Test Hero'); +}); + +it('should get the activated component directly', async () => { + // 一度にナビゲートしてコンポーネントインスタンスを取得 + const dashboardInstance = await harness.navigateByUrl('/', Dashboard); + + expect(dashboardInstance).toBeInstanceOf(Dashboard); +}); +``` + +### ベストプラクティス + +- **ハーネスでナビゲートする**:ナビゲーションをシミュレートするには常に `harness.navigateByUrl()` を使用してください。このメソッドはアクティベートされたコンポーネントのインスタンスで解決される Promise を返します。 +- **ルーターの状態にアクセスする**:`harness.router` を使用してライブのルーターインスタンスにアクセスし、状態に対してアサートします(例:`harness.router.url`)。 +- **アクティベートされたコンポーネントの取得**:現在アクティベートされているルーティングコンポーネントのコンポーネントハーネスインスタンスを取得するには `harness.getHarness(ComponentType)` を、`DebugElement` を取得するには `harness.routeDebugElement` を使用してください。 +- **安定を待つ**:ナビゲーションを引き起こすアクションを実行した後は、アサーションを行う前に必ず `await harness.fixture.whenStable()` でルーティングの完了を待ってください。 diff --git a/docs/ja-JP/skills/angular-developer/references/show-routes-with-outlets.md b/docs/ja-JP/skills/angular-developer/references/show-routes-with-outlets.md new file mode 100644 index 00000000..c2e558dc --- /dev/null +++ b/docs/ja-JP/skills/angular-developer/references/show-routes-with-outlets.md @@ -0,0 +1,68 @@ +# アウトレットを使ったルートの表示 + +`RouterOutlet` ディレクティブは、Angularが現在のURLに対応するコンポーネントをレンダリングするためのプレースホルダーです。 + +## 基本的な使い方 + +テンプレートに `` を含めます。Angularはルーティングされたコンポーネントをアウトレットの直後の兄弟要素として挿入します。 + +```html + + + +``` + +## ネストされたアウトレット + +子ルートには、親コンポーネントのテンプレート内に独自の `` が必要です。 + +```ts +// 親コンポーネントのテンプレート +

Settings

+ +``` + +## 名前付きアウトレット(セカンダリルート) + +ページには複数のアウトレットを設定できます。アウトレットに `name` を割り当てることで、特定のアウトレットをターゲットにできます。デフォルト名は `'primary'` です。 + +```html + + + + +``` + +ルート設定で `outlet` を定義します: + +```ts +{ + path: 'chat', + component: Chat, + outlet: 'sidebar' +} +``` + +## アウトレットのライフサイクルイベント + +`RouterOutlet` はコンポーネントが変更されるときにイベントを発行します: + +- `activate`: 新しいコンポーネントがインスタンス化された。 +- `deactivate`: コンポーネントが破棄された。 +- `attach` / `detach`: `RouteReuseStrategy` と共に使用される。 + +```html + +``` + +## `routerOutletData` によるデータの受け渡し + +`routerOutletData` インプットを使用して、ルーティングされたコンポーネントにコンテキストデータを渡すことができます。コンポーネントはシグナルとして `ROUTER_OUTLET_DATA` インジェクショントークン経由でこのデータにアクセスします。 + +```ts +// 親コンポーネント内 + + +// ルーティングされたコンポーネント内 +outletData = inject(ROUTER_OUTLET_DATA) as Signal<{ theme: string }>; +``` diff --git a/docs/ja-JP/skills/angular-developer/references/signal-forms.md b/docs/ja-JP/skills/angular-developer/references/signal-forms.md new file mode 100644 index 00000000..066be26d --- /dev/null +++ b/docs/ja-JP/skills/angular-developer/references/signal-forms.md @@ -0,0 +1,795 @@ +# シグナルフォーム + +シグナルフォームは、対象のAngularバージョンがサポートしている場合、新しいフォームに推奨されます。Angularシグナルを使用して、リアクティブかつ型安全なモデル駆動型のフォーム状態管理を提供します。 + +シグナルフォームを使用する場合、フィールドの値や型に `null` を使用しないでください。 + +## インポート + +`@angular/forms/signals` から以下をインポートできます: + +```ts +import { + form, + FormField, + submit, + // フィールド状態のルール + disabled, + hidden, + readonly, + debounce, + // スキーマヘルパー + applyWhen, + applyEach, + schema, + // カスタムバリデーション + validate, + validateHttp, + validateStandardSchema, + // メタデータ + metadata, +} from '@angular/forms/signals'; +``` + +## フォームの作成 + +シグナルモデルとともに `form()` 関数を使用します。フォームの構造はモデルから直接導出されます。 + +```ts +import {Component, signal} from '@angular/core'; +import {form, FormField} from '@angular/forms/signals'; + +@Component({ + // ... + imports: [FormField], +}) +export class Example { + // 1. 初期値でモデルを定義する(undefinedは避ける) + userModel = signal({ + name: '', // 重要: 初期値にnullやundefinedは絶対に使用しない + email: '', + age: 0, // 数値には0を使用し、nullは使用しない + address: { + street: '', + city: '', + }, + hobbies: [] as string[], // 配列には[]を使用し、nullは使用しない + }); + + // 誤り - このようにしてはいけない: + // badModel = signal({ + // name: null, // エラー: '' を使用すること + // age: null, // エラー: 0 を使用すること + // items: null // エラー: [] を使用すること + // }); + + // 2. フォームを作成する + userForm = form(this.userModel); +} +``` + +## バリデーション + +バリデーターを `@angular/forms/signals` からインポートします。 + +```ts +import {required, email, min, max, minLength, maxLength, pattern} from '@angular/forms/signals'; +``` + +`form()` に渡すスキーマ関数内で使用します: + +```ts +userForm = form(this.userModel, (schemaPath) => { + // 必須 + required(schemaPath.name, {message: 'Name is required'}); + + // 条件付き必須 + required(schemaPath.name, { + when({valueOf}) { + return valueOf(schemaPath.age) > 10; + }, + }); + // when は required にのみ使用可能 + // このようにしてはいけない: pattern(p.name, /xxx/, {when /* ERROR */) + + // メールアドレス + email(schemaPath.email, {message: 'Invalid email'}); + + // 数値の最小値/最大値 + min(schemaPath.age, 18); + max(schemaPath.age, 100); + + // 文字列/配列の最小長/最大長 + minLength(schemaPath.password, 8); + maxLength(schemaPath.description, 500); + + // パターン(正規表現) + pattern(schemaPath.zipCode, /^\d{5}$/); +}); +``` + +## FieldState と FormField: 親要素の要件 + +**FormField**(構造)と **FieldState**(実際のデータ/シグナル)の違いを理解することが重要です。 + +**ルール**: フィールドを関数として**呼び出す**ことで、そのステートシグナル(valid、touched、dirty、hidden など)にアクセスできます。 + +```ts +// f は FormField(構造的) +const f = form(signal({cat: {name: 'pirojok-the-cat', age: 5}})); + +f.cat.name; // FormField: ここからフラグは取得できない! +f.cat.name.touched(); // エラー: touched() は FormField に存在しない + +f.cat.name(); // FieldState: 呼び出すことでシグナルへのアクセスが可能になる +f.cat.name().touched(); // 有効: シグナルへのアクセス +f.cat().name.touched(); // エラー: f.cat() はステートであり、子要素を持たない! +``` + +テンプレートでも同様です: + +```html + +@if (bookingForm.hotelDetails.hidden()) { ... } + + +@if (bookingForm.hotelDetails().hidden()) { ... } +``` + +## Disabled / Readonly / Hidden + +スキーマ内のルールを使用してフィールドのステータスを制御します。 + +```ts +import {disabled, readonly, hidden} from '@angular/forms/signals'; + +userForm = form(this.userModel, (schemaPath) => { + // 条件付き無効化 + disabled(schemaPath.password, ({valueOf}) => !valueOf(schemaPath.createAccount)); + + // 条件付き非表示(モデルからは削除せず、非表示としてマークするのみ) + hidden(schemaPath.shippingAddress, ({valueOf}) => valueOf(schemaPath.sameAsBilling)); + + // 読み取り専用 + readonly(schemaPath.username); +}); +``` + +## バインディング + +`FormField` をインポートし、`[formField]` ディレクティブを使用します。 + +```ts +import {FormField} from '@angular/forms/signals'; +``` + +`disabled`、`hidden`、`readonly`、`name` などのステート上のすべてのプロパティは自動的にバインドされます。 +`name` フィールドは手動でバインド_しないでください_。 + +**重要: 禁止属性** +`[formField]` を使用する場合、テンプレートで以下の属性を設定してはなりません(静的またはバインドされた形式のいずれも): + +- `min`、`max`(代わりにスキーマ内のバリデーターを使用) +- `value`、`[value]`、`[attr.value]`(`[formField]` によって処理済み) +- `[attr.min]`、`[attr.max]` +- `[disabled]`、`[readonly]`(`[formField]` によって処理済み) + +このようにしてはいけません: `` や ``。 + +```html + + + + + + + + + + + +``` + +## リアクティブフォーム + +`@angular/forms` から `FormControl`、`FormGroup`、`FormArray`、`FormBuilder` を**インポートしないでください**。シグナルフォームはこれらのコンセプトを完全に置き換えます。 +シグナルフォームにはビルダーがありません。 + +## ステートへのアクセス + +フォーム内の各フィールドは、そのステートを返す関数です。 + +```ts +// フィールドを呼び出してアクセスする +const emailState = this.userForm.email(); + +// 値(WritableSignal) +const value = this.userForm().value(); + +// バリデーションステート(シグナル) +const isValid = this.userForm().valid(); +const isInvalid = this.userForm().invalid(); +const errors = this.userForm().errors(); // エラーの配列 +const isPending = this.userForm().pending(); // 非同期バリデーション待ち + +// インタラクションステート(シグナル) +const isTouched = this.userForm().touched(); +const isDirty = this.userForm().dirty(); + +// 可用性ステート(シグナル) +const isDisabled = this.userForm().disabled(); +const isHidden = this.userForm().hidden(); +const isReadonly = this.userForm().readonly(); +``` + +重要!: ステートを取得するには、必ずフィールドを呼び出してください。 + +```ts +form().invalid() +form.field().dirty() +form.field.subfield().touched() +form.a.b.c.d().value() +form.address.ssn().pending() +form().reset() + +// 唯一の例外は length です: +form.children.length +form.length // 注意: 括弧なし! +form.client.addresses.length // "()" なし + +@for (income of form.addresses; track $index) {/**/} +``` + +## 送信 + +`submit()` 関数を使用します。アクションを実行する前に、すべてのフィールドを自動的にタッチ済みとしてマークします。 + +**重要**: `submit()` へのコールバックは `async` でなければならず、Promise を返す必要があります。 + +```ts +import { submit } from '@angular/forms/signals'; + +// 正しい - async コールバック +onSubmit() { + submit(this.userForm, async () => { + // フォームが有効な場合のみ実行される + await this.apiService.save(this.userModel()); + console.log('Saved!'); + }); +} + +// 誤り - async キーワードが欠けている +onSubmit() { + submit(this.userForm, () => { // エラー: async でなければならない + console.log('Saved!'); + }); +} +``` + +## エラー処理 + +`field().errors()` は ValidationError の配列を返します: + +```ts +interface ValidationError { + readonly kind: string; + readonly message?: string; +} +``` + +バリデーターから null を返さないでください。 +エラーがない場合は undefined を返してください。 + +### コンテキスト + +`validate()`、`disabled()`、`applyWhen` などのルールに渡される関数は、コンテキストオブジェクトを受け取ります。その構造を理解することが**重要**です: + +```ts +validate( + schemaPath.username, + ({ + value, // Signal: フィールドの現在値(書き込み可能) + fieldTree, // FieldTree: サブフィールド(グループ/配列の場合) + state, // FieldState: state.valid()、state.dirty() などのフラグへのアクセス + valueOf, // (path) => T: 他のフィールドの値を読む(依存関係を追跡)例: valueOf(schemaPath.password) + stateOf, // (path) => FieldState: 他のフィールドのステートにアクセス 例: stateOf(schemaPath.password).valid() + pathKeys, // Signal: ルートからこのフィールドへのパス + }) => { + // 誤り: if (touched()) ... (touched はコンテキスト内にない) + // 正しい: if (state.touched()) ... + + if (value() === 'admin') { + return {kind: 'reserved', message: 'Username admin is reserved'}; + } + }, +); +``` + +### 重要: パスはシグナルではない + +`form()` コールバック内で、`schemaPath` とその子要素(例: `schemaPath.user.name`)は**シグナルではなく**、**呼び出し可能でもありません**。 + +```ts +// 誤り - エラーが発生します: +applyWhen(p.ssn, () => p.ssn().touched(), (ssnField) => { ... }); + +// 正しい - stateOf() を使用してパスのステートを取得する: +applyWhen(p.ssn, ({ stateOf }) => stateOf(p.ssn).touched(), (ssnField) => { ... }); + +// 正しい - valueOf() を使用してパスの値を取得する: +applyWhen(p.ssn, ({ valueOf }) => valueOf(p.ssn) !== '', (ssnField) => { ... }); +``` + +### 複数アイテム + +- アイテムごとにルールを適用するには `applyEach` を使用します。 +- **重要**: `applyEach` のコールバックは引数を**1つだけ**取ります(アイテムパス)。2つではありません: + +```ts +// 正しい - 引数1つ +applyEach(s.items, (item) => { + required(item.name); +}); + +// 誤り - インデックスを渡さない +applyEach(s.items, (item, index) => { + // エラー: コールバックは引数を1つしか取らない + required(item.name); +}); +``` + +- テンプレートではアイテムの反復に `@for` を使用します。 +- 配列からアイテムを削除するには、データ内の該当アイテムをそのまま削除してください。 +- **`select` バインディング**: `` (string[]) | 配列フィールドにはチェックボックスを使用 | +| **readonly 属性** | `` | スキーマで `readonly()` ルールを使用 | +| **min/max 属性** | `` | スキーマで `min()` と `max()` ルールを使用 | +| **value バインディング** | `` | `[formField]` と共に `[value]` を使用しない | +| **when オプション** | `pattern(p.x, /.../, {when: ...})` | `when` は `required()` でのみ動作する | +| **Submit コールバック** | `submit(form, () => { ... })` | `submit(form, async () => { ... })` | +| **Async params** | `params: s.field` | `params: ({ value }) => value()` | +| **Async onError** | `onError` を省略する | `validateAsync` では `onError` は必須 | +| **resource() API** | `request: signal` | `params: signal` | +| **applyEach の引数** | `applyEach(s.items, (item, index) => ...)` | `applyEach(s.items, (item) => ...)` | +| **ネストした @for** | `$parent.$index` | `let outerIndex = $index` を使用 | +| **FormState インポート** | `import { FormState }` | `FormState` は存在しない。`FieldState` を使用 | +| **モデル内の Null** | `signal({ name: null })` | `signal({ name: '' })` または `signal({ age: 0 })` | +| **Validate 構文** | `validate(s.field, { value } => ...)` | `validate(s.field, ({ value }) => ...)` | +| **チェックボックス配列** | `[formField]="form.tags"` (string[]) | チェックボックスは `boolean` にのみバインドする | + +## 大規模フォームの例 + +### `src/app/app.ts` + +```ts +import {Component, signal, ChangeDetectionStrategy} from '@angular/core'; +import { + form, + FormField, + submit, + required, + email, + min, + hidden, + applyEach, + validate, +} from '@angular/forms/signals'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [FormField], + templateUrl: './app.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + model = signal({ + personalInfo: { + firstName: '', + lastName: '', + email: '', + age: 0, + }, + tripDetails: { + destination: 'Mars', + launchDate: '', + }, + package: { + tier: 'economy', + extras: [] as string[], + }, + companions: [] as Array<{name: string; relation: string}>, + }); + + bookingForm = form(this.model, (s) => { + required(s.personalInfo.firstName, {message: 'First name is required'}); + required(s.personalInfo.lastName, {message: 'Last name is required'}); + required(s.personalInfo.email, {message: 'Email is required'}); + email(s.personalInfo.email, {message: 'Invalid email address'}); + required(s.personalInfo.age, {message: 'Age is required'}); + min(s.personalInfo.age, 18, {message: 'Must be at least 18'}); + + required(s.tripDetails.destination); + required(s.tripDetails.launchDate); + validate(s.tripDetails.launchDate, ({value}) => { + const date = new Date(value()); + if (isNaN(date.getTime())) return undefined; + const today = new Date(); + if (date < today) { + return {kind: 'pastData', message: 'Launch date must be in the future'}; + } + return undefined; + }); + + // valueOf は、ルール内で他のフィールドの値にアクセスするために使用される + hidden(s.package.extras, ({valueOf}) => valueOf(s.package.tier) === 'economy'); + + applyEach(s.companions, (companion) => { + required(companion.name, {message: 'Companion name required'}); + required(companion.relation, {message: 'Relation required'}); + }); + }); + + addCompanion() { + this.model.update((m) => ({ + ...m, + companions: [...m.companions, {name: '', relation: ''}], + })); + } + + removeCompanion(index: number) { + this.model.update((m) => ({ + ...m, + companions: m.companions.filter((_, i) => i !== index), + })); + } + + onSubmit() { + // 重要: submit コールバックは async でなければならない + submit(this.bookingForm, async () => { + console.log('Booking Confirmed:', this.model()); + // 非同期処理が必要な場合: + // await this.apiService.save(this.model()); + }); + } +} +``` + +### `src/app/app.html` + +```html +
+

Interstellar Booking

+ +
+

Personal Info

+ + + + + + + + +
+ +
+

Trip Details

+ + + + +
+ +
+

Package

+ + + + + + @if (!bookingForm.package.extras().hidden()) { +
+

Extras

+ + +
+ } +
+ +
+

Companions

+ + + @for (companion of bookingForm.companions; track $index) { +
+ + @if (companion.name().touched() && companion.name().errors().length) { + {{ companion.name().errors()[0].message }} + } + + + @if (companion.relation().touched() && companion.relation().errors().length) { + {{ companion.relation().errors()[0].message }} + } + + +
+ } +
+ + +
+``` + +## ビルドエラーからの回復 + +ビルドエラーが発生した場合、最も一般的な修正方法を示します: + +### `Property 'value' does not exist on type 'FieldTree'` + +**問題**: 最初に呼び出さずにフィールドの `.value()` に直接アクセスしています。 + +```ts +// 誤り +const val = this.form.field.value(); +// 正しい +const val = this.form.field().value(); +``` + +### `Property 'set' does not exist on type 'FieldTree'` + +**問題**: フォームツリーに対して値を設定しようとしています。シグナルフォームはモデル駆動型です。 + +```ts +// 誤り +this.form.address.street.set('Main St'); +// 正しい - 代わりにモデルシグナルを更新する +this.model.update((m) => ({...m, address: {...m.address, street: 'Main St'}})); +``` + +### `Type 'string[]' is not assignable to type 'string'` + +**問題**: 配列フィールドに単一値の ` + ... + + + + +``` diff --git a/docs/ja-JP/skills/angular-developer/references/signals-overview.md b/docs/ja-JP/skills/angular-developer/references/signals-overview.md new file mode 100644 index 00000000..42970d96 --- /dev/null +++ b/docs/ja-JP/skills/angular-developer/references/signals-overview.md @@ -0,0 +1,94 @@ +# Angular シグナル概要 + +シグナルは、モダンなAngularアプリケーションにおけるリアクティビティの基盤です。**シグナル**とは、値が変更されたときに関心のあるコンシューマーに通知する、値のラッパーです。 + +## 書き込み可能なシグナル(`signal`) + +`signal()` を使用して、直接更新できる状態を作成します。 + +```ts +import {signal} from '@angular/core'; + +// 書き込み可能なシグナルを作成 +const count = signal(0); + +// 値を読み取る(常にゲッター関数を呼び出す必要がある) +console.log(count()); + +// 値を直接更新する +count.set(3); + +// 前の値に基づいて更新する +count.update((value) => value + 1); +``` + +### 読み取り専用として公開する + +サービスから状態を公開する場合、外部からの変更を防ぐために読み取り専用バージョンを公開するのがベストプラクティスです。 + +```ts +private readonly _count = signal(0); +// コンシューマーはこれを読み取れるが、.set() や .update() は呼び出せない +readonly count = this._count.asReadonly(); +``` + +## 算出シグナル(`computed`) + +`computed()` を使用して、他のシグナルから値を導出する読み取り専用のシグナルを作成します。 + +- **遅延評価**: 導出関数は算出シグナルが読み取られるまで実行されません。 +- **メモ化**: 結果はキャッシュされます。依存するシグナルのいずれかが変更されたときのみ再計算されます。 +- **動的な依存関係**: 導出中に_実際に読み取られた_シグナルのみが追跡されます。 + +```ts +import {signal, computed} from '@angular/core'; + +const count = signal(0); +const doubleCount = computed(() => count() * 2); + +// doubleCount は count が変更されると自動的に更新される。 +``` + +## リアクティブコンテキスト + +**リアクティブコンテキスト**とは、Angularが依存関係を確立するためにシグナルの読み取りを監視するランタイム状態です。 + +Angularは以下を評価する際に自動的にリアクティブコンテキストに入ります: + +- `computed` シグナル +- `effect` コールバック +- `linkedSignal` の計算 +- コンポーネントテンプレート + +### 追跡なしの読み取り(`untracked`) + +リアクティブコンテキスト内でシグナルを読み取る際に、依存関係を作成_せずに_(シグナルが変更されてもコンテキストが再実行されないように)読み取る必要がある場合は、`untracked()` を使用します。 + +```ts +import {effect, untracked} from '@angular/core'; + +effect(() => { + // このエフェクトは currentUser が変更されたときのみ実行される。 + // counter がここで読み取られても、counter が変更されたときには実行されない。 + console.log(`User: ${currentUser()}, Count: ${untracked(counter)}`); +}); +``` + +### リアクティブコンテキストでの非同期操作 + +リアクティブコンテキストは**同期**コードに対してのみ有効です。`await` の後のシグナル読み取りは追跡されません。**常に非同期境界の前にシグナルを読み取ってください。** + +```ts +// 誤り: theme() は await の後に読み取られるため追跡されない +effect(async () => { + const data = await fetchUserData(); + console.log(theme()); +}); + +// 正しい: await の前にシグナルを読み取る +effect(async () => { + const currentTheme = theme(); + const data = await fetchUserData(); + console.log(currentTheme); +}); +``` diff --git a/docs/ja-JP/skills/angular-developer/references/tailwind-css.md b/docs/ja-JP/skills/angular-developer/references/tailwind-css.md new file mode 100644 index 00000000..bcbc4177 --- /dev/null +++ b/docs/ja-JP/skills/angular-developer/references/tailwind-css.md @@ -0,0 +1,69 @@ +# AngularでのTailwind CSSの使用 + +Tailwind CSSはユーティリティファーストのCSSフレームワークで、Angularとシームレスに統合できます。 + +**エージェントへの重要な注意: 常にTailwind CSS v4のプラクティスに従ってください。古いTailwind v3のパターン(`tailwind.config.js` を作成して `@tailwind` ディレクティブを使用するなど)に戻らないでください。これはアプリケーションのビルドを壊す原因となります。モダンなAngularプロジェクトはTailwind v4を使用します。** + +## 自動セットアップ(推奨) + +Angular CLIを使用してAngularプロジェクトにTailwind CSSを追加する最も簡単な方法: + +```shell +ng add tailwindcss +``` + +これにより自動的に以下が実行されます: + +1. `tailwindcss` と必要な依存関係のインストール。 +2. プロジェクトをTailwind CSSを使用するように設定。 +3. グローバルスタイルへの適切なインポートの追加。 + +## 手動セットアップ(Tailwind v4) + +手動でセットアップする場合は、以下のTailwind v4パターンを使用してください: + +### 1. 依存関係のインストール + +Tailwind CSSとPostCSSをインストールします: + +```shell +npm install tailwindcss @tailwindcss/postcss postcss +``` + +### 2. PostCSSの設定 + +プロジェクトルートに `.postcssrc.json` ファイルを作成します: + +```json +{ + "plugins": { + "@tailwindcss/postcss": {} + } +} +``` + +_(`tailwind.config.js` ファイルは作成しないでください!v4での設定はCSS変数で処理されます)。_ + +### 3. Tailwind CSSのインポート + +グローバルスタイルファイル(例: `src/styles.css`)に、標準的なv4インポートを追加します: + +```css +@import 'tailwindcss'; +``` + +_(SCSSを使用している場合は `@use 'tailwindcss';` を使用してください)。_ + +### 4. ユーティリティクラスの使用 + +コンポーネントテンプレートでTailwindクラスを直接使用できます: + +```html +

Hello world!

+``` + +## AIエージェントへのまとめ + +- **`@tailwind base; @tailwind components; @tailwind utilities;` は使用しないでください**。`@import 'tailwindcss';` を使用してください。 +- **`tailwind.config.js` は作成しないでください**。設定はテーマ変数を通じたCSS内、またはPostCSS設定を使用して直接管理されます。 +- v4の構文とワークフローに厳密に従ってください。 diff --git a/docs/ja-JP/skills/angular-developer/references/template-driven-forms.md b/docs/ja-JP/skills/angular-developer/references/template-driven-forms.md new file mode 100644 index 00000000..cd7eb5a0 --- /dev/null +++ b/docs/ja-JP/skills/angular-developer/references/template-driven-forms.md @@ -0,0 +1,114 @@ +# テンプレート駆動フォーム + +テンプレート駆動フォームは、双方向データバインディング(`[(ngModel)]`)を使用して、テンプレートで変更が加えられるとコンポーネントのデータモデルを更新し、その逆も同様に行います。シンプルなフォームに最適で、HTMLテンプレート内のディレクティブを使用してフォームの状態とバリデーションを管理します。 + +## 主要なディレクティブ + +テンプレート駆動フォームは `FormsModule` に依存しており、以下の主要なディレクティブを提供します: + +- `NgModel`: フォーム要素の値の変更をデータモデルと調整します(`[(ngModel)]`)。 +- `NgForm`: `
` タグにバインドされたトップレベルの `FormGroup` を自動的に作成します。 +- `NgModelGroup`: DOM要素にバインドされたネストされた `FormGroup` を作成します。 + +## セットアップ + +まず、コンポーネントまたはモジュールに `FormsModule` をインポートします。 + +```ts +import {Component} from '@angular/core'; +import {FormsModule} from '@angular/forms'; + +@Component({ + selector: 'app-user-form', + imports: [FormsModule], + templateUrl: './user-form.component.html', +}) +export class UserForm { + user = {name: '', role: 'Guest'}; + + onSubmit() { + console.log('Form submitted!', this.user); + } +} +``` + +## フォームテンプレートの構築 + +### `[(ngModel)]` による双方向バインディング + +入力要素に `[(ngModel)]` を使用します。**`[(ngModel)]` を使用するすべての要素には `name` 属性が必須です。** Angularは `name` 属性を使用して、親 `NgForm` にコントロールを登録します。 + +```html + + +
+ + +
+ + +
+ + +
+ + + +
+``` + +## フォームとコントロールの状態 + +Angularは状態に基づいてコントロールとフォームにCSSクラスを自動的に適用します: + +| 状態 | Trueの場合のクラス | Falseの場合のクラス | +| :------------- | :-------------------------------- | :------------- | +| 訪問済み | `ng-touched` | `ng-untouched` | +| 値が変更済み | `ng-dirty` | `ng-pristine` | +| 値が有効 | `ng-valid` | `ng-invalid` | +| フォーム送信済み | `ng-submitted`(`
` のみ) | - | + +これらのクラスをCSSでビジュアルフィードバックとして使用できます: + +```css +.ng-valid[required], +.ng-valid.required { + border-left: 5px solid #42a948; /* 緑 */ +} +.ng-invalid:not(form) { + border-left: 5px solid #a94442; /* 赤 */ +} +``` + +## バリデーションとエラーメッセージ + +条件付きでエラーメッセージを表示するには、`ngModel` ディレクティブをテンプレート参照変数(例: `#nameCtrl="ngModel"`)にエクスポートします。 + +```html + + + +@if (nameCtrl.invalid && (nameCtrl.dirty || nameCtrl.touched)) { +
+ @if (nameCtrl.errors?.['required']) { +
Name is required.
+ } +
+} +``` + +## フォームの送信 + +1. `` 要素に `(ngSubmit)` イベントを使用します。 +2. `NgForm` テンプレート参照変数(例: `[disabled]="!userForm.form.valid"`)を使用して、送信ボタンの無効状態をフォーム全体の有効性にバインドします。 + +## フォームのリセット + +フォームをプリスティン状態(値とバリデーションフラグをクリア)にプログラムでリセットするには、`NgForm` インスタンスの `reset()` メソッドを使用します。 + +```html + +``` diff --git a/docs/ja-JP/skills/angular-developer/references/testing-fundamentals.md b/docs/ja-JP/skills/angular-developer/references/testing-fundamentals.md new file mode 100644 index 00000000..1a474a92 --- /dev/null +++ b/docs/ja-JP/skills/angular-developer/references/testing-fundamentals.md @@ -0,0 +1,65 @@ +# テストの基礎 + +このガイドでは、Angularのユニットテストおよびコンポーネントテストを記述するための基本的な原則と実践を説明します。プロジェクトにすでに設定されているテストランナーを使用してください。 + +## 核心哲学: 非同期ファースト + +最新のAngularアプリケーションは、特にシグナルやゾーンレス変更検知を使用する場合、状態変更を非同期にスケジュールすることが多いです。テストはこれを考慮する必要があります。 + +「Act(実行)、Wait(待機)、Assert(検証)」パターンを推奨します: + +1. **Act(実行):** 状態を更新するかアクションを実行します(例: コンポーネントの入力を設定する、ボタンをクリックする)。 +2. **Wait(待機):** `await fixture.whenStable()` を使用して、フレームワークがスケジュールされた更新を処理しレンダリングするのを待ちます。 +3. **Assert(検証):** 結果を確認します。 + +### 基本的なテスト構造の例 + +```ts +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MyComponent} from './my.component'; + +describe('MyComponent', () => { + let component: MyComponent; + let fixture: ComponentFixture; + let h1: HTMLElement; + + beforeEach(async () => { + // 1. テストモジュールを設定する + await TestBed.configureTestingModule({ + imports: [MyComponent], + }).compileComponents(); + + // 2. コンポーネントフィクスチャーを作成する + fixture = TestBed.createComponent(MyComponent); + component = fixture.componentInstance; + h1 = fixture.nativeElement.querySelector('h1'); + }); + + it('should display the default title', async () => { + // ACT: (暗黙的)コンポーネントはデフォルト状態で作成される。 + // 初期データバインディングを待機する。 + await fixture.whenStable(); + // 初期状態を検証する。 + expect(h1.textContent).toContain('Default Title'); + }); + + it('should display a different title after a change', async () => { + // ACT: コンポーネントのタイトルプロパティを変更する。 + component.title.set('New Test Title'); + + // 非同期更新が完了するのを待機する。 + await fixture.whenStable(); + + // DOMが更新されたことを検証する。 + expect(h1.textContent).toContain('New Test Title'); + }); +}); +``` + +## TestBed と ComponentFixture + +- **`TestBed`**: テスト専用のAngularモジュールを作成するための主要なユーティリティです。テストに必要なコンポーネントの宣言、サービスの提供、インポートのセットアップのために `beforeEach` 内で `TestBed.configureTestingModule({...})` を使用します。 +- **`ComponentFixture`**: 作成されたコンポーネントのインスタンスとその環境を扱うためのハンドルです。 + - `fixture.componentInstance`: コンポーネントのクラスインスタンスにアクセスします。 + - `fixture.nativeElement`: コンポーネントのルートDOM要素にアクセスします。 + - `fixture.debugElement`: `nativeElement` をAngular固有のラッパーでラップしたもので、DOMをより安全かつプラットフォームに依存しない方法でクエリできます(例: `debugElement.query(By.css('p'))`)。 diff --git a/docs/ja-JP/skills/brand-voice/references/voice-profile-schema.md b/docs/ja-JP/skills/brand-voice/references/voice-profile-schema.md new file mode 100644 index 00000000..7239144a --- /dev/null +++ b/docs/ja-JP/skills/brand-voice/references/voice-profile-schema.md @@ -0,0 +1,55 @@ +# ボイスプロファイルスキーマ + +再利用可能なボイスプロファイルを作成する際は、以下の構造を正確に使用してください: + +```text +VOICE PROFILE +============= +Author: +Goal: +Confidence: + +Source Set +- source 1 +- source 2 +- source 3 + +Rhythm +- 文の長さ、ペーシング、断片化についての短いメモ + +Compression +- 文章の密度や説明の詳しさ + +Capitalization +- 通常、混在、または状況依存 + +Parentheticals +- 使用方法と使用しない場合 + +Question Use +- 稀、頻繁、修辞的、直接的、またはほぼ不使用 + +Claim Style +- 主張のフレーミング、根拠付け、鋭くする方法 + +Preferred Moves +- 著者が実際に使用する具体的な手法 + +Banned Moves +- 著者が使用しない特定のパターン + +CTA Rules +- クローズをどのように、いつ、または使用するかどうか + +Channel Notes +- X: +- LinkedIn: +- Email: +``` + +ガイドライン: + +- プロファイルは具体的でソースに基づいたものにしてください。 +- 長文の段落ではなく、短い箇条書きを使用してください。 +- すべての禁止手法はソースセットで観察可能か、ユーザーが明示的に要求したものにしてください。 +- ソースセットが矛盾する場合は、均一化せず分割を指摘してください。 diff --git a/docs/ja-JP/skills/frontend-slides/animation-patterns.md b/docs/ja-JP/skills/frontend-slides/animation-patterns.md new file mode 100644 index 00000000..b5941f9e --- /dev/null +++ b/docs/ja-JP/skills/frontend-slides/animation-patterns.md @@ -0,0 +1,122 @@ +# アニメーションパターンリファレンス + +プレゼンテーション生成時にこのリファレンスを使用してください。意図する印象に合わせてアニメーションを選択します。 + +## 効果と印象の対応表 + +| 印象 | アニメーション | ビジュアルの手がかり | +|---------|-----------|-------------| +| **ドラマチック / シネマティック** | スローフェードイン(1〜1.5秒)、大スケールのトランジション(0.9→1)、パララックススクロール | 暗い背景、スポットライト効果、フルブリード画像 | +| **テクノロジー系 / 未来的** | ネオングロウ(box-shadow)、グリッチ/スクランブルテキスト、グリッドリビール | パーティクルシステム(canvas)、グリッドパターン、等幅フォントのアクセント、シアン/マゼンタ/エレクトリックブルー | +| **遊び心 / フレンドリー** | バウンシーイージング(スプリング物理)、フローティング/ボビング | 角丸、パステル/ブライトカラー、手描き風要素 | +| **プロフェッショナル / コーポレート** | 繊細で速いアニメーション(200〜300ms)、クリーンなスライド | ネイビー/スレート/チャコール、正確な間隔、データビジュアライゼーション重視 | +| **穏やか / ミニマル** | 非常にゆっくりとした繊細な動き、ソフトなフェード | 広いホワイトスペース、くすんだカラーパレット、セリフタイポグラフィ、余裕のあるパディング | +| **エディトリアル / マガジン** | スタッガードテキストリビール、画像とテキストの連動 | 強いタイプ階層、プルクォート、グリッドを崩したレイアウト、セリフの見出し+サンセリフの本文 | + +## 入場アニメーション + +```css +/* フェード + スライドアップ(最も汎用性が高い) */ +.reveal { + opacity: 0; + transform: translateY(30px); + transition: opacity 0.6s var(--ease-out-expo), + transform 0.6s var(--ease-out-expo); +} +.visible .reveal { + opacity: 1; + transform: translateY(0); +} + +/* スケールイン */ +.reveal-scale { + opacity: 0; + transform: scale(0.9); + transition: opacity 0.6s, transform 0.6s var(--ease-out-expo); +} +.visible .reveal-scale { + opacity: 1; + transform: scale(1); +} + +/* 左からスライド */ +.reveal-left { + opacity: 0; + transform: translateX(-50px); + transition: opacity 0.6s, transform 0.6s var(--ease-out-expo); +} +.visible .reveal-left { + opacity: 1; + transform: translateX(0); +} + +/* ブラーイン */ +.reveal-blur { + opacity: 0; + filter: blur(10px); + transition: opacity 0.8s, filter 0.8s var(--ease-out-expo); +} +.visible .reveal-blur { + opacity: 1; + filter: blur(0); +} +``` + +## 背景エフェクト + +```css +/* グラデーションメッシュ — 奥行きのための放射状グラデーションの重ね合わせ */ +.gradient-bg { + background: + radial-gradient(ellipse at 20% 80%, rgba(120, 0, 255, 0.3) 0%, transparent 50%), + radial-gradient(ellipse at 80% 20%, rgba(0, 255, 200, 0.2) 0%, transparent 50%), + var(--bg-primary); +} + +/* ノイズテクスチャ — グレイン用のインラインSVG */ +.noise-bg { + background-image: url("data:image/svg+xml,..."); /* インラインSVGノイズ */ +} + +/* グリッドパターン — 繊細な構造的ライン */ +.grid-bg { + background-image: + linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px); + background-size: 50px 50px; +} +``` + +## インタラクティブエフェクト + +```javascript +/* ホバー時の3Dチルト — カード/パネルに奥行きを追加 */ +class TiltEffect { + constructor(element) { + this.element = element; + this.element.style.transformStyle = 'preserve-3d'; + this.element.style.perspective = '1000px'; + + this.element.addEventListener('mousemove', (e) => { + const rect = this.element.getBoundingClientRect(); + const x = (e.clientX - rect.left) / rect.width - 0.5; + const y = (e.clientY - rect.top) / rect.height - 0.5; + this.element.style.transform = `rotateY(${x * 10}deg) rotateX(${-y * 10}deg)`; + }); + + this.element.addEventListener('mouseleave', () => { + this.element.style.transform = 'rotateY(0) rotateX(0)'; + }); + } +} +``` + +## トラブルシューティング + +| 問題 | 解決策 | +|---------|-----| +| フォントが読み込まれない | FontshareまたはGoogle FontsのURLを確認。CSSでフォント名が一致しているか確認 | +| アニメーションが起動しない | Intersection Observerが動作しているか確認。`.visible` クラスが追加されているか確認 | +| スクロールスナップが機能しない | htmlに `scroll-snap-type: y mandatory` があるか確認。各スライドに `scroll-snap-align: start` が必要 | +| モバイルの問題 | 768pxのブレークポイントで重いエフェクトを無効化。タッチイベントをテスト。パーティクル数を減らす | +| パフォーマンスの問題 | `will-change` を控えめに使用。`transform`/`opacity` アニメーションを優先。スクロールハンドラをスロットリング | diff --git a/docs/ja-JP/skills/frontend-slides/html-template.md b/docs/ja-JP/skills/frontend-slides/html-template.md new file mode 100644 index 00000000..3d84fd73 --- /dev/null +++ b/docs/ja-JP/skills/frontend-slides/html-template.md @@ -0,0 +1,415 @@ +# HTMLプレゼンテーションテンプレート + +スライドプレゼンテーション生成のリファレンスアーキテクチャ。すべてのプレゼンテーションはこの構造に従います。 + +## ベースHTML構造 + +```html + + + + + + Presentation Title + + + + + + + + +
+ + + + + +
+

Presentation Title

+

Subtitle or author

+
+ +
+
+

Slide Title

+

Content...

+
+
+ + + + + + +``` + +## 必須JavaScriptの機能 + +すべてのプレゼンテーションに以下が必要です: + +1. **SlidePresentationクラス** — メインコントローラー(以下を含む): + - キーボードナビゲーション(矢印キー、スペース、Page Up/Down) + - タッチ/スワイプサポート + - マウスホイールナビゲーション + - プログレスバーの更新 + - ナビゲーションドット + +2. **Intersection Observer** — スクロールトリガーアニメーション用: + - スライドがビューポートに入ったときに `.visible` クラスを追加 + - CSSトランジションを効率的にトリガー + +3. **オプションの拡張機能**(選択したスタイルに合わせる): + - カスタムカーソルとトレイル + - パーティクルシステム背景(canvas) + - パララックスエフェクト + - ホバー時の3Dチルト + - マグネティックボタン + - カウンターアニメーション + +4. **インライン編集**(フェーズ1でユーザーが選択した場合のみ — 「いいえ」の場合は完全にスキップ): + - 編集トグルボタン(デフォルト非表示、ホバーホットゾーンまたはEキーで表示) + - localStorageへの自動保存 + - ファイルのエクスポート/保存機能 + - 以下の「インライン編集の実装」セクションを参照 + +## インライン編集の実装(オプトインのみ) + +**フェーズ1でユーザーがインライン編集に「いいえ」を選択した場合、編集関連のHTML、CSS、JSを一切生成しないでください。** + +**CSS `~` 兄弟セレクターをホバーベースの表示/非表示に使用しないでください。** CSS単体のアプローチ(`edit-hotzone:hover ~ .edit-toggle`)は、トグルボタンの `pointer-events: none` がホバーチェーンを壊すため失敗します:ユーザーがホットゾーンにホバー → ボタンが表示 → マウスがボタンに向かって移動 → ホットゾーンを離れる → クリック前にボタンが消える。 + +**必須アプローチ:400msの遅延タイムアウトを持つJSベースのホバー。** + +HTML: + +```html +
+ +``` + +CSS(表示/非表示はJSクラスのみで制御): + +```css +/* ここではCSS ~兄弟セレクターを使用しないでください! + pointer-events: noneがホバーチェーンを壊します。 + 遅延タイムアウトを持つJSを使用する必要があります。 */ +.edit-hotzone { + position: fixed; + top: 0; + left: 0; + width: 80px; + height: 80px; + z-index: 10000; + cursor: pointer; +} +.edit-toggle { + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + z-index: 10001; +} +.edit-toggle.show, +.edit-toggle.active { + opacity: 1; + pointer-events: auto; +} +``` + +JS(3つのインタラクション方法): + +```javascript +// 1. トグルボタンのクリックハンドラー +document.getElementById("editToggle").addEventListener("click", () => { + editor.toggleEditMode(); +}); + +// 2. 400msのグレース期間を持つホットゾーンのホバー +const hotzone = document.querySelector(".edit-hotzone"); +const editToggle = document.getElementById("editToggle"); +let hideTimeout = null; + +hotzone.addEventListener("mouseenter", () => { + clearTimeout(hideTimeout); + editToggle.classList.add("show"); +}); +hotzone.addEventListener("mouseleave", () => { + hideTimeout = setTimeout(() => { + if (!editor.isActive) editToggle.classList.remove("show"); + }, 400); +}); +editToggle.addEventListener("mouseenter", () => { + clearTimeout(hideTimeout); +}); +editToggle.addEventListener("mouseleave", () => { + hideTimeout = setTimeout(() => { + if (!editor.isActive) editToggle.classList.remove("show"); + }, 400); +}); + +// 3. ホットゾーンの直接クリック +hotzone.addEventListener("click", () => { + editor.toggleEditMode(); +}); + +// 4. キーボードショートカット(Eキー、テキスト編集中はスキップ) +document.addEventListener("keydown", (e) => { + if ( + (e.key === "e" || e.key === "E") && + !e.target.getAttribute("contenteditable") + ) { + editor.toggleEditMode(); + } +}); +``` + +**重要: `exportFile()` はouterHTMLをキャプチャする前に編集状態を除去する必要があります。** + +ユーザーが編集モードでCtrl+Sを押すと、`document.documentElement.outerHTML` がライブDOM(`body.edit-active`、すべてのテキスト要素の `contenteditable="true"`、トグルボタンとバナーの `.active`/`.show` クラスを含む)をキャプチャします。保存されたファイルを開く人は、点線のアウトライン、チェックマークボタン、編集バナーが表示され、永久に編集モードのように見えます。 + +`exportFile()` は常にこのように実装してください: + +```javascript +exportFile() { + // 保存されたファイルがクリーンに開くよう、一時的に編集状態を除去 + const editableEls = Array.from(document.querySelectorAll('[contenteditable]')); + editableEls.forEach(el => el.removeAttribute('contenteditable')); + document.body.classList.remove('edit-active'); + + // トグルボタンとバナーからUIクラスも除去 + const editToggle = document.getElementById('editToggle'); + const editBanner = document.querySelector('.edit-banner'); + editToggle?.classList.remove('active', 'show'); + editBanner?.classList.remove('active', 'show'); + + const html = '\n' + document.documentElement.outerHTML; + + // ユーザーが編集を続けられるよう編集状態を復元 + document.body.classList.add('edit-active'); + editableEls.forEach(el => el.setAttribute('contenteditable', 'true')); + editToggle?.classList.add('active'); + editBanner?.classList.add('active'); + + const blob = new Blob([html], { type: 'text/html' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = 'presentation.html'; + a.click(); + URL.revokeObjectURL(a.href); +} +``` + +## 画像パイプライン(画像がない場合はスキップ) + +フェーズ1でユーザーが「画像なし」を選択した場合は完全にスキップします。画像が提供された場合は、HTML生成前に処理します。 + +**依存関係:** `pip install Pillow` + +### 画像処理 + +```python +from PIL import Image, ImageDraw + +# 円形クロップ(モダン/クリーンなスタイルのロゴ用) +def crop_circle(input_path, output_path): + img = Image.open(input_path).convert('RGBA') + w, h = img.size + size = min(w, h) + left, top = (w - size) // 2, (h - size) // 2 + img = img.crop((left, top, left + size, top + size)) + mask = Image.new('L', (size, size), 0) + ImageDraw.Draw(mask).ellipse([0, 0, size, size], fill=255) + img.putalpha(mask) + img.save(output_path, 'PNG') + +# リサイズ(HTMLを肥大化させる大きすぎる画像用) +def resize_max(input_path, output_path, max_dim=1200): + img = Image.open(input_path) + img.thumbnail((max_dim, max_dim), Image.LANCZOS) + img.save(output_path, quality=85) +``` + +| 状況 | 操作 | +| -------------------------------- | ----------------------------- | +| 角丸デザインの正方形ロゴ | `crop_circle()` | +| 1MB超の画像 | `resize_max(max_dim=1200)` | +| アスペクト比が不正 | `img.crop()` で手動クロップ | + +処理済み画像は `_processed` サフィックスで保存します。元ファイルは上書きしないでください。 + +### 画像の配置 + +**ローカルで表示されるため直接ファイルパスを使用**(base64は不可): + +```html + +Screenshot +``` + +```css +.slide-image { + max-width: 100%; + max-height: min(50vh, 400px); + object-fit: contain; + border-radius: 8px; +} +.slide-image.screenshot { + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +} +.slide-image.logo { + max-height: min(30vh, 200px); +} +``` + +**ボーダー/シャドウカラーは選択したスタイルのアクセントカラーに合わせてください。** 複数のスライドで同じ画像を繰り返さないでください(タイトルとクロージングのロゴは例外)。 + +**配置パターン:** タイトルスライドでロゴを中央に。スクリーンショットはテキストとの2カラムレイアウトで。フルブリード画像はテキストオーバーレイのあるスライド背景として(控えめに)。 + +--- + +## コード品質 + +**コメント:** 各セクションに何をするのか、どう変更するのかを説明する明確なコメントを付けてください。 + +**アクセシビリティ:** + +- セマンティックHTML(`
`、`