feat(ja-JP): add skill sub-reference translations (angular, remotion, etc.)

Translated 85 skill sub-reference files to achieve full parity with
the English source:

- skills/angular-developer/references/ — 35 files (all references)
- skills/remotion-video-creation/rules/ — 28 files (all rules)
- skills/tinystruct-patterns/references/ — 5 files
- skills/openclaw-persona-forge/references/ — 6 files
- skills/skill-comply/prompts/ — 3 files
- skills/lead-intelligence/agents/ — 4 files
- skills/brand-voice/references/ — 1 file
- skills/frontend-slides/ — 2 files
- hooks/memory-persistence/README.md — 1 file

English source parity: 0 missing files (excluding rules/zh/, internal
docs, and experimental examples absent from zh-CN)
This commit is contained in:
Claude
2026-05-18 06:15:16 +09:00
parent 63624426c8
commit 174e31b3fc
85 changed files with 8409 additions and 0 deletions

View File

@@ -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`

View File

@@ -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()) {
<div class="enter-container" animate.enter="enter-animation">
<p>The box is entering.</p>
</div>
}
```
```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()) {
<div (animate.leave)="onLeave($event)">...</div>
}
```
```ts
import { AnimationCallbackEvent } from '@angular/core';
onLeave(event: AnimationCallbackEvent) {
// カスタムアニメーションロジックをここに記述
// 重要: Angularが要素を削除できるよう、完了時に必ず animationComplete() を呼び出してください!
event.animationComplete();
}
```
## 2. 高度なCSSアニメーション
CSSは高度なアニメーションシーケンスのための強力なツールを提供しています。
### 状態とスタイルのアニメーション
プロパティバインディングを使用して要素のCSSクラスを切り替え、トランジションをトリガーします。
```html
<div [class.open]="isOpen">...</div>
```
```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: `<div [@openClose]="isOpen() ? 'open' : 'closed'">...</div>`,
})
export class OpenClose {
isOpen = signal(true);
}
```

View File

@@ -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
<div ngAccordionGroup [multiExpandable]="false">
<div class="accordion-item">
<button ngAccordionTrigger panelId="panel-1" class="accordion-header">
Section 1
<span class="icon"></span>
</button>
<div ngAccordionPanel panelId="panel-1" class="accordion-panel">
<ng-template ngAccordionContent>
<p>Lazy loaded content here.</p>
</ng-template>
</div>
</div>
</div>
```
**スタイリング戦略:**
トリガーの `[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
<!-- 水平または垂直方向 -->
<ul ngListbox [(values)]="selectedItems" orientation="horizontal" [multi]="true">
<li ngOption value="apple" class="option">Apple</li>
<li ngOption value="banana" class="option">Banana</li>
</ul>
```
**スタイリング戦略:**
選択状態には `[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
<!-- 例:標準セレクト -->
<div ngCombobox [readonly]="true">
<button ngComboboxInput class="select-trigger">
{{ selectedValue() || 'Choose an option' }}
</button>
<ng-template ngComboboxPopupContainer>
<ul ngListbox [(values)]="selectedValue" class="dropdown-menu">
<li ngOption value="option1">Option 1</li>
<li ngOption value="option2">Option 2</li>
</ul>
</ng-template>
</div>
```
**スタイリング戦略:**
ポップアップコンテナをコンテンツの上に浮かぶドロップダウンのように見せるスタイリングをします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
<!-- メニューバーの例 -->
<ul ngMenuBar class="menubar">
<li ngMenuItem value="file">
<button ngMenuTrigger [menu]="fileMenu">File</button>
</li>
</ul>
<ul ngMenu #fileMenu="ngMenu" class="menu">
<li ngMenuItem value="new">New</li>
<li ngMenuItem value="open">Open</li>
</ul>
```
**スタイリング戦略:**
メニューバーには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
<div ngTabs>
<ul ngTabList class="tab-list">
<li ngTab value="profile" class="tab-btn">Profile</li>
<li ngTab value="security" class="tab-btn">Security</li>
</ul>
<div ngTabPanel value="profile" class="tab-panel">
<ng-template ngTabContent>Profile Settings</ng-template>
</div>
<div ngTabPanel value="security" class="tab-panel">
<ng-template ngTabContent>Security Settings</ng-template>
</div>
</div>
```
**スタイリング戦略:**
タブボタンの `[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
<div ngToolbar class="toolbar">
<div ngToolbarWidgetGroup [multi]="true" role="group" aria-label="Formatting">
<button ngToolbarWidget value="bold" class="tool-btn">B</button>
<button ngToolbarWidget value="italic" class="tool-btn">I</button>
</div>
</div>
```
**スタイリング戦略:**
ツールバー内の `[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
<ul ngTree class="tree">
<li ngTreeItem value="documents">
<span class="tree-label">Documents</span>
<ul ngTreeGroup class="tree-group">
<li ngTreeItem value="resume">Resume.pdf</li>
</ul>
</li>
</ul>
```
**スタイリング戦略:**
`[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
<table ngGrid [multi]="true" [enableSelection]="true" class="grid-table">
<tr ngGridRow>
<th ngGridCell role="columnheader">Name</th>
<th ngGridCell role="columnheader">Status</th>
</tr>
<tr ngGridRow>
<td ngGridCell>Project A</td>
<td ngGridCell [(selected)]="isSelected">
<button ngGridCellWidget (activated)="onActivate()">Active</button>
</td>
</tr>
</table>
```
**スタイリング戦略:**
選択されたセルには `[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パターンを実装する際は、`<select>` などのネイティブHTML要素を使用しないでください**`ng*` ディレクティブを使用してください。
2. **CSSは手動で処理してください**: `Angular Aria` はスタイルを提供しません。ディレクティブが自動的に切り替えるネイティブARIA属性`aria-expanded``aria-selected` などをターゲットにしたCSSを記述する必要があります。
3. **遅延ロード**: 重いコンテンツパネルの遅延レンダリングを確保するために、`ng-template` 内で提供されている構造ディレクティブ(`ngAccordionContent``ngTabContent`)を常に使用してください。

View File

@@ -0,0 +1,86 @@
# エージェント向け Angular CLI ガイド
Angular CLI`ng`)は Angular ワークスペースを管理するための主要ツールです。プロジェクト構造を変更したり Angular 固有の依存関係を追加したりする際は、手動でのファイル作成や汎用の `npm` コマンドよりも、常に CLI コマンドを優先して使用してください。
## 1. 依存関係の管理
**Angular ライブラリには `npm install` ではなく必ず `ng add` を使用してください。** `ng add` はパッケージのインストールに加え、初期化スキーマティクス(例:`angular.json` の設定、ルートプロバイダーの更新)も実行します。
```bash
ng add @angular/material
ng add tailwindcss
ng add @angular/fire
```
アプリケーションとその依存関係を更新するには(コードマイグレーションが自動的に実行されます):
```bash
ng update @angular/core@<latest or specific version> @angular/cli<latest or specific version>
```
## 2. コード生成(`ng generate` または `ng g`
Angular の規約に準拠し、必要な設定ファイルが自動的に更新されるよう、コード生成には必ず CLI を使用してください。
| 対象 | コマンド | 備考 |
| :------------- | :-------------------- | :--------------------------------------------------------------------------------------------- |
| コンポーネント | `ng g c path/to/name` | コンポーネントを生成します。要求に応じて `--inline-style``-s`)または `--inline-template``-t`)を使用してください。 |
| サービス | `ng g s path/to/name` | `@Injectable({providedIn: 'root'})` サービスを生成します。 |
| ディレクティブ | `ng g d path/to/name` | ディレクティブを生成します。 |
| パイプ | `ng g p path/to/name` | パイプを生成します。 |
| ガード | `ng g g path/to/name` | 関数型ルートガードを生成します。 |
| 環境設定 | `ng g environments` | `src/environments/` を生成し、ファイル置換を含む `angular.json` を更新します。 |
_注意:単一のルート定義を生成するコマンドはありません。コンポーネントを生成した後、`app.routes.ts` 内の `Routes` 配列に手動で追加してください。_
## 3. 開発サーバーとプロキシ
ホットモジュール置換HMRを使用してローカル開発サーバーを起動します
```bash
ng serve
```
### バックエンド API プロキシ
開発中に API リクエストをプロキシするには(例:`/api` をローカルの Node サーバーにリルーティング):
1. `src/proxy.conf.json` を作成します:
```json
{
"/api/**": {"target": "http://localhost:3000", "secure": false}
}
```
2. `angular.json` の `serve` ターゲット以下を更新します:
```json
"serve": {
"builder": "@angular/build:dev-server",
"options": { "proxyConfig": "src/proxy.conf.json" }
}
```
## 4. アプリケーションのビルド
アプリケーションを出力ディレクトリ(デフォルト:`dist/<project-name>/browser`)にコンパイルします。最新の Angular は esbuild ベースの `@angular/build:application` ビルダーを使用します。
```bash
ng build
```
- `ng build` はデフォルトでプロダクション設定を使用し、Ahead-of-TimeAOTコンパイル、ミニファイ、ツリーシェイキングを有効にします。
- `--configuration` オプションを使用して `angular.json` で定義された特定の設定を対象にできます:`ng build --configuration=staging`。
## 5. テスト
- **ユニットテスト**設定されたテストランナーKarma または Vitestでユニットテストを実行するには `ng test` を実行します。
- **エンドツーエンドE2Eテスト**`ng e2e` を実行します。E2E フレームワークが設定されていない場合、CLI がインストールを促しますCypress、Playwright、Puppeteer など)。
## 6. デプロイ
アプリケーションをデプロイするには、まずデプロイメントビルダーを追加してからデプロイコマンドを実行します:
```bash
# Firebase の例
ng add @angular/fire
ng deploy
```

View File

@@ -0,0 +1,59 @@
# コンポーネントハーネスを使用したテスト
コンポーネントハーネスは、テストでコンポーネントを操作するための標準的かつ推奨される方法です。コンポーネントの内部 DOM 構造の変更からテストを保護することで、壊れにくく読みやすいテストを実現するユーザー中心の API を提供します。
## なぜハーネスを使用するのか?
- **堅牢性:** コンポーネントの内部 HTML や CSS クラスをリファクタリングしてもテストが壊れません。
- **可読性:** テストは DOM クエリ(`fixture.nativeElement.querySelector(...)`)を通じてではなく、ユーザーの観点からのインタラクション(例:`button.click()``slider.getValue()`)を記述します。
- **再利用性:** 同じハーネスをユニットテストと E2E テストの両方で使用できます。
Angular Material はライブラリ内のすべてのコンポーネントにテストハーネスを提供しています。
## ユニットテストでのハーネスの使用
`TestbedHarnessEnvironment` はユニットテストでハーネスを使用するためのエントリーポイントです。
### 例:`MatButtonHarness` を使用したテスト
```ts
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {MatButtonHarness} from '@angular/material/button/testing';
import {MyButtonContainerComponent} from './my-button-container.component';
describe('MyButtonContainerComponent', () => {
let fixture: ComponentFixture<MyButtonContainerComponent>;
let loader: HarnessLoader;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyButtonContainerComponent, MatButtonModule],
}).compileComponents();
fixture = TestBed.createComponent(MyButtonContainerComponent);
// コンポーネントのフィクスチャ用のハーネスローダーを作成する
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('should find a button with specific text', async () => {
// "Submit" というテキストを持つ MatButton のハーネスをロードする
const submitButton = await loader.getHarness(MatButtonHarness.with({text: 'Submit'}));
// ハーネス API を使用してコンポーネントを操作する
expect(await submitButton.isDisabled()).toBe(false);
await submitButton.click();
// ... アサーション
});
});
```
### 主要なコンセプト
1. **`HarnessLoader`**:ハーネスインスタンスを検索して作成するためのオブジェクトです。`TestbedHarnessEnvironment.loader(fixture)` を使用してコンポーネントのフィクスチャ用のローダーを取得します。
2. **`loader.getHarness(HarnessClass)`**:最初にマッチするコンポーネントのハーネスインスタンスを非同期で検索して返します。
3. **`HarnessClass.with({ ... })`**:多くのハーネスは静的な `with` メソッドを提供し、`HarnessPredicate` を返します。これにより、テキスト、セレクター、無効状態などのプロパティに基づいてコンポーネントをフィルタリングして検索できます。テストしたいコンポーネントを正確に対象にするために常にこれを使用してください。
4. **ハーネス API** ハーネスインスタンスを取得したら、そのメソッド(例:`.click()``.getText()``.getValue()`)を使用してコンポーネントを操作します。これらのメソッドは非同期操作と変更検出の待機を自動的に処理します。

View File

@@ -0,0 +1,91 @@
# コンポーネントスタイリング
Angular コンポーネントは、カプセル化とモジュール性を実現するために、テンプレートに特定のスタイルを定義できます。
## スタイルの定義
スタイルはインラインまたは別ファイルで定義できます。
```ts
@Component({
selector: 'app-photo',
// インラインスタイル
styles: `
img {
border-radius: 50%;
}
`,
// または外部ファイル
styleUrl: 'photo.component.css',
})
export class Photo {}
```
## ビューエンカプセレーション
すべてのコンポーネントには、スタイルのスコープを決定するビューエンカプセレーション設定があります。
| モード | 動作 |
| :------------------------------ | :-------------------------------------------------------------------------------------------- |
| `Emulated`(デフォルト) | 一意の HTML 属性を使用してスタイルをコンポーネントにスコープします。グローバルスタイルは引き続き影響する可能性があります。 |
| `ShadowDom` | ブラウザのネイティブ Shadow DOM API を使用してスタイルを完全に分離します。 |
| `None` | カプセル化を無効にします。コンポーネントスタイルはグローバルになります。 |
| `ExperimentalIsolatedShadowDom` | コンポーネントのスタイルのみが適用されることを厳密に保証します。 |
### 使用方法
```ts
import { ViewEncapsulation } from '@angular/core';
@Component({
...,
encapsulation: ViewEncapsulation.None,
})
export class GlobalStyled {}
```
## 特殊セレクター
### `:host`
コンポーネントのホスト要素(コンポーネントのセレクターに一致する要素)を対象にします。
```css
:host {
display: block;
border: 1px solid black;
}
```
### `:host-context()`
先祖の何らかの条件に基づいてホスト要素を対象にします。
```css
/* 先祖に 'theme-dark' クラスがある場合にスタイルを適用する */
:host-context(.theme-dark) {
background-color: #333;
}
```
### `::ng-deep`
特定のルールのビューエンカプセレーションを無効にし、子コンポーネントに「漏れ」させます。
**注意Angular チームは `::ng-deep` の使用を強く推奨しません。** これは後方互換性のためにのみサポートされています。
## テンプレート内のスタイル
コンポーネントのテンプレートで直接 `<style>` 要素を使用できます。ビューエンカプセレーションのルールは引き続き適用されます。
```html
<style>
.dynamic-class {
color: red;
}
</style>
<div class="dynamic-class">Hello</div>
```
## 外部スタイル
CSS での `<link>``@import` の使用は外部スタイルとして扱われます。**外部スタイルはエミュレートされたビューエンカプセレーションの影響を受けません。**

View File

@@ -0,0 +1,117 @@
# コンポーネント
Angular コンポーネントはアプリケーションの基本的な構成要素です。各コンポーネントは、振る舞いを持つ TypeScript クラス、HTML テンプレート、CSS セレクターで構成されます。
## コンポーネントの定義
`@Component` デコレーターを使用してコンポーネントのメタデータを定義します。
```ts
@Component({
selector: 'app-profile',
template: `
<img src="profile.jpg" alt="Profile photo" />
<button (click)="save()">Save</button>
`,
styles: `
img {
border-radius: 50%;
}
`,
})
export class Profile {
save() {
/* ... */
}
}
```
## メタデータオプション
- `selector`:テンプレート内でこのコンポーネントを識別する CSS セレクター。
- `template`:インライン HTML テンプレート(小さなテンプレートに推奨)。
- `templateUrl`:外部 HTML ファイルへのパス。
- `styles`:インライン CSS スタイル。
- `styleUrl` / `styleUrls`:外部 CSS ファイルへのパス(複数可)。
- `imports`:このコンポーネントのテンプレートで使用されるコンポーネント、ディレクティブ、パイプのリスト。
## コンポーネントの使用
コンポーネントを使用するには、利用側コンポーネントの `imports` 配列に追加し、テンプレートでセレクターを使用します。
```ts
@Component({
selector: 'app-root',
imports: [Profile],
template: `<app-profile />`,
})
export class App {}
```
## テンプレート制御フロー
Angular は条件レンダリングとループに組み込みブロックを使用します。
### 条件レンダリング(`@if`
コンテンツを条件付きで表示するには `@if` を使用します。`@else if``@else` ブロックを含めることができます。
```html
@if (user.isAdmin) {
<admin-dashboard />
} @else if (user.isModerator) {
<mod-dashboard />
} @else {
<standard-dashboard />
}
```
**結果のエイリアス**:式の結果を保存して再利用できます。
```html
@if (user.settings(); as settings) {
<p>Theme: {{ settings.theme }}</p>
}
```
### ループ(`@for`
`@for` ブロックはコレクションを反復処理します。`track` 式はパフォーマンスと DOM 再利用のために**必須**です。
```html
<ul>
@for (item of items(); track item.id; let i = $index, total = $count) {
<li>{{ i + 1 }}/{{ total }}: {{ item.name }}</li>
} @empty {
<li>No items to display.</li>
}
</ul>
```
**暗黙的変数**`$index``$count``$first``$last``$even``$odd`
### コンテンツの切り替え(`@switch`
`@switch` ブロックは値に基づいてコンテンツをレンダリングします。厳密等価(`===`)を使用し、**フォールスルーはありません**。
```html
@switch (status()) { @case ('loading') { <app-spinner /> } @case ('error') { <app-error-msg /> }
@case ('success') { <app-data-grid /> } @default {
<p>Unknown status</p>
} }
```
**網羅的型チェック**`@default never;` を使用して、ユニオン型のすべてのケースが処理されることを確認します。
```html
@switch (state) { @case ('on') { ... } @case ('off') { ... } @default never; // 'standby' のような新しい
// 状態が追加された場合にエラーになる }
```
## 核心的コンセプト
- **ホスト要素**:コンポーネントのセレクターに一致する DOM 要素。
- **ビュー**:ホスト要素内のコンポーネントのテンプレートによってレンダリングされた DOM。
- **スタンドアロン**デフォルトでコンポーネントはスタンドアロンですAngular 19 以降、`standalone: true` がデフォルト)。古いバージョンでは `standalone: true` を明示するか、コンポーネントを `NgModule` の一部にする必要があります。
- **コンポーネントツリー**Angular アプリケーションはコンポーネントのツリーとして構成され、各コンポーネントは子コンポーネントをホストできます。
- **コンポーネントの命名**:プロジェクトがその命名設定を使用するように構成されていない限り、コンポーネントクラスに `Component` サフィックスを追加しないでくださいAppComponent

View File

@@ -0,0 +1,97 @@
# サービスの作成と使用
Angular のサービスは、複数のコンポーネントや他のサービスがアクセスする必要があるデータフェッチ、ビジネスロジック、または状態管理を扱う再利用可能なコードです。
## サービスの作成
Angular CLI を使用してサービスを生成できます:
```bash
ng generate service my-data
```
または、TypeScript クラスを手動で作成して `@Injectable()` でデコレートすることもできます。
```ts
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class BasicDataStore {
private data: string[] = [];
addData(item: string): void {
this.data.push(item);
}
getData(): string[] {
return [...this.data];
}
}
```
### `providedIn: 'root'` オプション
ほとんどのサービスには `providedIn: 'root'` の使用が推奨されます。これにより Angular は以下を行います:
- アプリケーション全体に対して**シングルインスタンス(シングルトン)を作成**します。
- `providers` 配列に列挙しなくても**どこからでも自動的に利用可能**にします。
- **ツリーシェイキングを有効**にし、サービスが実際にどこかで注入されている場合にのみ最終的な JavaScript バンドルに含まれるようにします。
## サービスの注入
サービスを作成したら、`inject()` 関数を使用してコンポーネント、ディレクティブ、または他のサービスに注入できます。
### コンポーネントへの注入
```ts
import {Component, inject} from '@angular/core';
import {BasicDataStore} from './basic-data-store.service';
@Component({
selector: 'app-example',
template: `
<div>
<p>Data items: {{ dataStore.getData().length }}</p>
<button (click)="dataStore.addData('New Item')">Add Item</button>
</div>
`,
})
export class Example {
// サービスをクラスフィールドとして注入する
dataStore = inject(BasicDataStore);
}
```
### 別のサービスへの注入
サービスはまったく同じ方法で他のサービスを注入できます。
```ts
import {Injectable, inject} from '@angular/core';
import {AdvancedDataStore} from './advanced-data-store.service';
@Injectable({
providedIn: 'root',
})
export class BasicDataStore {
// 別のサービスを注入する
private advancedDataStore = inject(AdvancedDataStore);
private data: string[] = [];
getData(): string[] {
// このサービスと注入されたサービスのデータを結合する
return [...this.data, ...this.advancedDataStore.getData()];
}
}
```
## 高度なサービスパターン
`providedIn: 'root'` がほとんどのシナリオをカバーしていますが、以下のような場合に他の方法が必要になることがあります:
- **コンポーネント固有のインスタンス**:コンポーネントがサービスの独立したインスタンスを必要とする場合、コンポーネントの `@Component({ providers: [MyService] })` 配列で直接提供します。
- **ファクトリープロバイダー**:動的な作成のため。
- **値プロバイダー**:設定オブジェクトを注入するため。

View File

@@ -0,0 +1,69 @@
# データリゾルバー
データリゾルバーはルートがアクティブになる前にデータをフェッチし、コンポーネントがレンダリング時に必要なデータを持っていることを保証します。
## リゾルバーの作成
`ResolveFn` 型を実装します。
```ts
export const userResolver: ResolveFn<User> = (route, state) => {
const userService = inject(UserService);
const id = route.paramMap.get('id')!;
return userService.getUser(id);
};
```
## ルートの設定
`resolve` キーの下にリゾルバーを追加します。
```ts
{
path: 'user/:id',
component: UserProfile,
resolve: {
user: userResolver
}
}
```
## 解決されたデータへのアクセス
### 1. `ActivatedRoute` 経由(従来の方法)
```ts
private route = inject(ActivatedRoute);
data = toSignal(this.route.data);
user = computed(() => this.data().user);
```
### 2. コンポーネント入力経由(モダンな方法)
`provideRouter``withComponentInputBinding()` を有効にすると、解決されたデータを `@Input` または `input()` に直接渡せます。
```ts
// app.config.ts
provideRouter(routes, withComponentInputBinding());
// component.ts
user = input.required<User>();
```
## エラーハンドリング
リゾルバーが失敗するとナビゲーションがブロックされます。
- グローバルな処理には `withNavigationErrorHandler` を使用します。
- リゾルバー内で `catchError` を使用して `RedirectCommand` やフォールバックデータを返します。
```ts
return userService
.get(id)
.pipe(catchError(() => of(new RedirectCommand(router.parseUrl('/error')))));
```
## ベストプラクティス
- **軽量に保つ**:重要なデータのみをフェッチします。
- **フィードバックを提供する**:リゾルバーが完了するまで UI は古いページに留まるため、ナビゲーション中にグローバルなローディングバーを表示するためにルーターイベントをリッスンします。

View File

@@ -0,0 +1,67 @@
# ルートの定義
ルートは特定の URL パスに対してどのコンポーネントをレンダリングするかを定義するオブジェクトです。
## 基本設定
`Routes` 配列にルートを定義し、`appConfig``provideRouter` を使用して提供します。
```ts
// app.routes.ts
export const routes: Routes = [
{path: '', component: HomePage},
{path: 'admin', component: AdminPage},
];
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)],
};
```
## URL パス
- **静的**:完全な文字列に一致します(例:`'admin'`)。
- **ルートパラメーター**:コロンが前置された動的セグメント(例:`'user/:id'`)。
- **ワイルドカード**`**` を使用して任意の URL に一致します。「見つかりません」ページに有用です。**常に配列の最後に配置してください。**
## マッチング戦略
Angular は**先勝ち**戦略を使用します。具体的なルートは具体性の低いルートより前に記述する必要があります。
## リダイレクト
`redirectTo` を使用して一つのパスを別のパスに向けます。
```ts
{ path: 'articles', redirectTo: '/blog' },
{ path: 'blog', component: Blog },
```
## ページタイトル
アクセシビリティのためにルートにタイトルを関連付けます。タイトルは静的または動的(`ResolveFn` またはカスタム `TitleStrategy` 経由)にできます。
```ts
{ path: 'home', component: Home, title: 'Home Page' }
```
## ルートデータとプロバイダー
- **静的データ**`data` プロパティを使用してメタデータを付加します。
- **ルートプロバイダー**`providers` 配列を使用して特定のルートとその子に依存関係をスコープします。
## ネスト(子)ルート
`children` プロパティを使用してサブビューを定義します。親コンポーネントには `<router-outlet />` が必要です。
```ts
{
path: 'product/:id',
component: Product,
children: [
{ path: 'info', component: ProductInfo },
{ path: 'reviews', component: ProductReviews },
],
}
```

View File

@@ -0,0 +1,72 @@
# 依存性プロバイダーの定義
Angular は依存性注入DIシステムに依存関係を提供する自動的な方法と手動の方法を提供します。
## 自動プロビジョン
サービスを提供する最も一般的な方法は、`@Injectable()``providedIn: 'root'` を使用することです。
### InjectionToken
非クラスの依存関係(設定オブジェクト、関数、プリミティブ)には `InjectionToken` を使用します。`InjectionToken` は自動的に提供することもできます。
```ts
import {InjectionToken} from '@angular/core';
export interface AppConfig {
apiUrl: string;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config', {
providedIn: 'root',
factory: () => ({apiUrl: 'https://api.example.com'}),
});
```
## 手動プロビジョン
`providedIn` のないサービス、特定のコンポーネント用の新しいインスタンスが必要な場合、またはランタイム値を設定する場合に `providers` 配列を使用します。
```ts
@Component({
providers: [
// { provide: LocalService, useClass: LocalService } の省略形
LocalService,
// useClass: 実装を切り替える
{provide: Logger, useClass: BetterLogger},
// useValue: 静的な値を提供する
{provide: API_URL_TOKEN, useValue: 'https://api.example.com'},
// useFactory: 値を動的に生成する
{
provide: ApiClient,
useFactory: (http = inject(HttpClient)) => new ApiClient(http),
},
// useExisting: エイリアスを作成する
{provide: OldLogger, useExisting: NewLogger},
// multi: 同じトークンに複数の値を配列として提供する
{provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true},
],
})
export class Example {}
```
## プロバイダーのスコープ
- **アプリケーションブートストラップ**グローバルシングルトン。HTTP クライアント、ロギング、アプリ全体の設定に使用します。
- **コンポーネント/ディレクティブ**:分離されたインスタンス。コンポーネント固有の状態やフォームに使用します。サービスはコンポーネントが破棄されると破棄されます。
- **ルート**:特定のルートでのみロードされる機能固有のサービス。
## ライブラリパターン:`provide*` 関数
ライブラリ作者は設定をカプセル化するためにプロバイダー配列を返す関数をエクスポートする必要があります:
```ts
export function provideAnalytics(config: AnalyticsConfig): Provider[] {
return [{provide: ANALYTICS_CONFIG, useValue: config}, AnalyticsService];
}
```

View File

@@ -0,0 +1,120 @@
# 依存性注入DIの基礎
依存性注入DIは、アプリケーションの異なる部分に機能を「注入」することで、コードを整理して共有するための設計パターンです。これにより、コードの保守性、スケーラビリティ、テスト容易性が向上します。
## Angular における DI の仕組み
コードが Angular の DI システムと相互作用する主な方法が 2 つあります:
1. **プロビジョン(提供)**:値(オブジェクト、関数、プリミティブ)を DI システムで利用可能にする。
2. **インジェクション(注入)**DI システムにそれらの値を要求する。
Angular のコンポーネント、ディレクティブ、サービスは自動的に DI に参加します。
## サービス
**サービス**はアプリケーション全体でデータと機能を共有する最も一般的な方法です。`@Injectable()` でデコレートされた TypeScript クラスです。
### サービスの作成
`@Injectable` デコレーターで `providedIn: 'root'` オプションを使用すると、アプリケーション全体で利用可能なシングルトンサービスになります。これはほとんどのサービスに推奨されるアプローチです。
```ts
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root', // どこでも利用可能なシングルトンにする
})
export class AnalyticsLogger {
trackEvent(category: string, value: string) {
console.log('Analytics event logged:', {category, value});
}
}
```
サービスの一般的な用途:
- データクライアントAPI 呼び出し)
- 状態管理
- 認証と認可
- ロギングとエラーハンドリング
- ユーティリティ関数
## 依存関係の注入
依存関係を要求するには Angular の `inject()` 関数を使用します。
### `inject()` 関数
`inject()` 関数を使用してサービス(または提供された他のトークン)のインスタンスを取得できます。
```ts
import {Component, inject} from '@angular/core';
import {Router} from '@angular/router';
import {AnalyticsLogger} from './analytics-logger.service';
@Component({
selector: 'app-navbar',
template: `<a href="#" (click)="navigateToDetail($event)">Detail Page</a>`,
})
export class Navbar {
// クラスフィールド初期化子を使用して依存関係を注入する
private router = inject(Router);
private analytics = inject(AnalyticsLogger);
navigateToDetail(event: Event) {
event.preventDefault();
this.analytics.trackEvent('navigation', '/details');
this.router.navigate(['/details']);
}
}
```
### `inject()` はどこで使用できるか?(インジェクションコンテキスト)
`inject()` は**インジェクションコンテキスト**内でコードが実行されている場合に呼び出せます。最も一般的なインジェクションコンテキストは、コンポーネント、ディレクティブ、またはサービスの構築時です。
`inject()` を呼び出せる有効な場所:
1. **クラスフィールド初期化子**(推奨)
2. **コンストラクター本体**
3. **ルートガードとリゾルバー**(インジェクションコンテキスト内で実行される)
4. プロバイダーで使用される**ファクトリー関数**
```typescript
import {Component, Directive, Injectable, inject, ElementRef} from '@angular/core';
import {HttpClient} from '@angular/common/http';
// 1. コンポーネント内(フィールド初期化子とコンストラクター)
@Component({
/*...*/
})
export class Example {
private service1 = inject(MyService); // 有効なフィールド初期化子
private service2: MyService;
constructor() {
this.service2 = inject(MyService); // 有効なコンストラクター本体
}
}
// 2. ディレクティブ内
@Directive({
/*...*/
})
export class MyDirective {
private element = inject(ElementRef); // 有効なフィールド初期化子
}
// 3. サービス内
@Injectable({providedIn: 'root'})
export class MyService {
private http = inject(HttpClient); // 有効なフィールド初期化子
}
// 4. ルートガード(関数型)内
export const authGuard = () => {
const auth = inject(AuthService); // 有効なルートガード
return auth.isAuthenticated();
};
```

View File

@@ -0,0 +1,56 @@
# エンドツーエンドE2Eテスト
実際のブラウザでの重要なユーザージャーニーをカバーするために E2E テストを使用します。Cypress や Playwright など、Angular ワークスペースですでに設定されているフレームワークを優先してください。
## E2E テストの実行
プロジェクト固有のコマンドについては `package.json``angular.json` を確認してください。一般的なパターンには以下があります:
```shell
npm run e2e
pnpm e2e
ng e2e
```
アプリをビルドまたはサーブする必要がある場合は、並列テストエントリポイントを新たに作成するのではなく、既存のプロジェクトスクリプトを使用してください。
## テスト構造
- E2E スペックは `cypress/e2e/``e2e/` など、設定されたテストフレームワークに近い場所に保管してください。
- 再利用可能なログイン/セットアップヘルパーはフレームワークのサポートディレクトリに配置してください。
- フィクスチャは明示的に小さくして、各テストが依存するユーザー状態を説明できるようにしてください。
### Cypress の例
```typescript
describe('Login flow', () => {
it('redirects to dashboard on valid credentials', () => {
cy.visit('/login');
cy.get('[data-cy=email]').type('user@example.com');
cy.get('[data-cy=password]').type('password123');
cy.get('[data-cy=submit]').click();
cy.url().should('include', '/dashboard');
});
});
```
### Playwright の例
```typescript
import {expect, test} from '@playwright/test';
test('redirects to dashboard on valid credentials', async ({page}) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', {name: 'Sign in'}).click();
await expect(page).toHaveURL(/dashboard/);
});
```
## ベストプラクティス
- アクセシブルなロケーター(`getByRole``getByLabel`)または安定した `data-*` 属性を優先してください。
- CSS クラス、DOM の深さ、または偶発的なテキストに依存するセレクターは避けてください。
- 任意のスリープではなく、特定の UI 状態、ルート、またはネットワークレスポンスを待機してください。
- スモークテストは短く保ち、完全なワークフローカバレッジは最も価値の高いパスに限定してください。

View File

@@ -0,0 +1,83 @@
# `effect` と `afterRenderEffect` によるサイドエフェクト
Angularにおける**エフェクト**とは、追跡している1つ以上のシグナル値が変化するたびに実行される処理です。
## `effect` を使うべき場面
エフェクトは、シグナルの状態を命令的な非シグナルAPIに同期させるために使います。
**有効なユースケース:**
- アナリティクスのログ記録。
- `localStorage``sessionStorage` への状態の同期。
- `<canvas>` やサードパーティのチャートライブラリへのカスタムレンダリングの実行。
**重要なルール: 状態の伝播にエフェクトを使わないこと。**
2つのシグナルを同期させるためにエフェクト内でシグナルの `.set()``.update()` を呼び出している場合、それは誤りです。`ExpressionChangedAfterItHasBeenChecked` エラーや無限ループを引き起こします。**状態の派生には常に `computed()` または `linkedSignal()` を使用してください。**
## 基本的な使い方
エフェクトは変更検出プロセス中に非同期で実行されます。常に少なくとも1回は実行されます。
```ts
import { Component, signal, effect } from '@angular/core';
@Component({...})
export class Example {
count = signal(0);
constructor() {
// エフェクトはインジェクションコンテキスト(例: コンストラクター)内で作成する必要があります
effect((onCleanup) => {
console.log(`Count changed to ${this.count()}`);
const timer = setTimeout(() => console.log('Timer finished'), 1000);
// クリーンアップ関数は次回実行前または破棄時に実行されます
onCleanup(() => clearTimeout(timer));
});
}
}
```
## `afterRenderEffect` によるDOM操作
標準の `effect` はAngularがDOMを更新する_前_に実行されます。シグナルの変化に基づいてDOMを手動で検査または変更する必要がある場合例: サードパーティUIライブラリの統合は、`afterRenderEffect` を使用してください。
`afterRenderEffect` はAngularがDOMのレンダリングを完了した後に実行されます。
### レンダーフェーズ
リフロー(強制レイアウトスラッシング)を防ぐため、`afterRenderEffect` はDOMの読み取りと書き込みを特定のフェーズに分割することを強制します。
```ts
import { Component, afterRenderEffect, viewChild, ElementRef } from '@angular/core';
@Component({...})
export class Chart {
canvas = viewChild.required<ElementRef>('canvas');
constructor() {
afterRenderEffect({
// 1. DOMから読み取る
earlyRead: () => {
return this.canvas().nativeElement.getBoundingClientRect().width;
},
// 2. DOMに書き込む前フェーズの結果を受け取る
write: (width) => {
// writeフェーズではDOMを読み取らないこと。
setupChart(this.canvas().nativeElement, width);
}
});
}
}
```
**利用可能なフェーズ(この順序で実行されます):**
1. `earlyRead`
2. `write`(ここでは読み取らないこと)
3. `mixedReadWrite`(可能な限り避けること)
4. `read`(ここでは書き込まないこと)
_注意: `afterRenderEffect` はクライアントでのみ実行され、サーバーサイドレンダリングSSR中は実行されません。_

View File

@@ -0,0 +1,43 @@
# 階層的インジェクター
Angularの依存性注入システムは階層的であり、サービスをアプリケーションの異なるレベルにスコープできます。
## インジェクター階層の種類
1. **`EnvironmentInjector` 階層**: `@Injectable({ providedIn: 'root' })` またはブートストラップ時の `ApplicationConfig.providers` で設定されます。これらはグローバルシングルトンです。
2. **`ElementInjector` 階層**: 各DOM要素で暗黙的に作成されます。`@Component()` または `@Directive()``providers` または `viewProviders` 配列で設定されます。
## 解決ルール
依存関係が要求されると、Angularは2つのフェーズで解決します:
1. リクエスト元のコンポーネント/ディレクティブからルート要素まで、**`ElementInjector`** ツリーを上方向に検索します。
2. 見つからない場合、最も近い環境インジェクターからルートまで、**`EnvironmentInjector`** ツリーを検索します。
3. それでも見つからない場合、エラーをスローします(オプションとしてマークされている場合を除く)。
## 解決モディファイアー
`inject()` のオプションオブジェクトを使用して、Angularが依存関係を検索する方法を変更できます:
- **`optional`**: 依存関係が見つからない場合、エラーをスローする代わりに `null` を返します。
- **`self`**: 現在の `ElementInjector` のみをチェックします。親ツリーを検索しません。
- **`skipSelf`**: 現在の要素をスキップして、親の `ElementInjector` から検索を開始します。
- **`host`**: ホストコンポーネントのビュー境界に達したら検索を停止します。
```ts
@Component({...})
export class Example {
// 見つからない場合はクラッシュせずnullを返す
optionalService = inject(MyService, { optional: true });
// このコンポーネントのプロバイダーをスキップして親を参照する
parentService = inject(ParentService, { skipSelf: true });
}
```
## `providers` と `viewProviders`
コンポーネントレベルでサービスを提供する場合:
- **`providers`**: サービスはコンポーネント、そのビュー(テンプレート)、および**プロジェクトされたコンテンツ**`<ng-content>`)で利用可能です。
- **`viewProviders`**: サービスはコンポーネントとそのビューで利用可能ですが、プロジェクトされたコンテンツからは**利用できません**。コンシューマーから渡されたコンテンツからサービスを分離するために使用します。

View File

@@ -0,0 +1,80 @@
# コンポーネントのホスト要素
**ホスト要素**とは、コンポーネントのセレクターに一致するDOM要素です。コンポーネントのテンプレートはこの要素の内部にレンダリングされます。
## ホスト要素へのバインディング
`@Component` デコレーターの `host` プロパティを使用して、プロパティ、属性、スタイル、イベントをホスト要素にバインドします。これはレガシーデコレーターより**推奨されるアプローチ**です。
```ts
@Component({
selector: 'custom-slider',
host: {
'role': 'slider', // 静的属性
'[attr.aria-valuenow]': 'value', // 属性バインディング
'[class.active]': 'isActive()', // クラスバインディング
'[style.color]': 'color()', // スタイルバインディング
'[tabIndex]': 'disabled ? -1 : 0', // プロパティバインディング
'(keydown)': 'onKeyDown($event)', // イベントバインディング
},
})
export class CustomSlider {
value = 0;
disabled = false;
isActive = signal(false);
color = signal('blue');
onKeyDown(event: KeyboardEvent) {
/* ... */
}
}
```
## レガシーデコレーター
`@HostBinding``@HostListener` は後方互換性のためにサポートされていますが、新しいコードでは使用を避けてください。
```ts
export class CustomSlider {
@HostBinding('tabIndex')
get tabIndex() {
return this.disabled ? -1 : 0;
}
@HostListener('keydown', ['$event'])
onKeyDown(event: KeyboardEvent) {
/* ... */
}
}
```
## バインディングの競合
コンポーネント(ホストバインディング)とコンシューマー(テンプレートバインディング)の両方が同じプロパティにバインドする場合:
1. **静的 vs 静的**: インスタンス(コンシューマー)のバインディングが優先されます。
2. **静的 vs 動的**: 動的バインディングが優先されます。
3. **動的 vs 動的**: コンポーネントのホストバインディングが優先されます。
## ホスト属性のインジェクト
`inject` 関数と `HostAttributeToken` を使用して、構築時にホスト要素から静的属性を読み取ります。
```ts
import {Component, HostAttributeToken, inject} from '@angular/core';
@Component({
selector: 'app-btn',
template: `<ng-content />`,
})
export class AppButton {
// `{ optional: true }` でインジェクトしない限り、'type'がない場合エラーをスローします
type = inject(new HostAttributeToken('type'));
}
```
使用例:
```html
<app-btn type="primary">Click Me</app-btn>
```

View File

@@ -0,0 +1,63 @@
# インジェクションコンテキスト
`inject()` 関数はコードが**インジェクションコンテキスト**内で実行されている場合にのみ使用できます。
## インジェクションコンテキストが利用可能な場所
インジェクションコンテキストは以下の場所で自動的に利用可能です:
1. DI によってインスタンス化されるクラス(`@Injectable``@Component``@Directive``@Pipe`)の**フィールド初期化子**。
2. DI によってインスタンス化されるクラスの**コンストラクター**。
3. `useFactory` または `InjectionToken` 設定で指定された**ファクトリー関数**。
4. Angular によって実行される**関数型 API**(例:関数型ルートガード、リゾルバー、インターセプター)。
```ts
@Component({...})
export class Example {
// 有効:フィールド初期化子
private router = inject(Router);
constructor() {
// 有効:コンストラクター
const http = inject(HttpClient);
}
onClick() {
// 無効:インジェクションコンテキストではない
// const auth = inject(AuthService);
}
}
```
## `runInInjectionContext`
インジェクションコンテキスト内で関数を実行する必要がある場合(動的コンポーネントの作成やテストでよく必要になります)、`runInInjectionContext` を使用します。これには既存のインジェクター(`EnvironmentInjector` または `Injector` など)へのアクセスが必要です。
```ts
import {Injectable, inject, EnvironmentInjector, runInInjectionContext} from '@angular/core';
@Injectable({providedIn: 'root'})
export class MyService {
private injector = inject(EnvironmentInjector);
doSomethingDynamic() {
runInInjectionContext(this.injector, () => {
// ここで inject() を使用することが有効になる
const router = inject(Router);
});
}
}
```
## `assertInInjectionContext`
ユーティリティ関数が有効なコンテキストから呼び出されることを保証するために `assertInInjectionContext` を使用します。そうでない場合は明確なエラーをスローします。
```ts
import {assertInInjectionContext, inject, ElementRef} from '@angular/core';
export function injectNativeElement<T extends Element>(): T {
assertInInjectionContext(injectNativeElement);
return inject(ElementRef).nativeElement;
}
```

View File

@@ -0,0 +1,101 @@
# 入力Inputs
入力により、親コンポーネントから子コンポーネントにデータを流すことができます。Angular はモダンなアプリケーションにはシグナルベースの `input` API の使用を推奨しています。
## シグナルベースの入力
`input()` 関数を使用して入力を宣言します。これは `InputSignal` を返します。
```ts
import {Component, input, computed} from '@angular/core';
@Component({
selector: 'app-user',
template: `<p>User: {{ name() }} ({{ age() }})</p>`,
})
export class User {
// デフォルト値を持つオプションの入力
name = input('Guest');
// 必須の入力
age = input.required<number>();
// 入力はリアクティブなシグナル
label = computed(() => `Name: ${this.name()}`);
}
```
### テンプレートでの使用
```html
<app-user [name]="userName" [age]="25" />
```
## 設定オプション
`input` 関数は設定オブジェクトを受け取ります:
- **エイリアス**:テンプレートで使用されるプロパティ名を変更します。
- **トランスフォーム**:コンポーネントに到達する前に値を変更します。
```ts
import { input, booleanAttribute } from '@angular/core';
@Component({...})
export class CustomButton {
// エイリアスの例
label = input('', { alias: 'btnLabel' });
// 組み込みヘルパーを使用したトランスフォームの例
disabled = input(false, { transform: booleanAttribute });
}
```
## モデル入力(双方向バインディング)
双方向データバインディングをサポートする入力を作成するには `model()` を使用します。
```ts
@Component({
selector: 'custom-counter',
template: `<button (click)="increment()">+</button>`,
})
export class CustomCounter {
value = model(0);
increment() {
this.value.update((v) => v + 1);
}
}
```
### 使用方法
```html
<!-- シグナルによる双方向バインディング -->
<custom-counter [(value)]="mySignal" />
<!-- プレーンプロパティによる双方向バインディング -->
<custom-counter [(value)]="myProperty" />
```
## デコレーターベースの入力(@Input
レガシー API は引き続きサポートされていますが、新しいコードには推奨されません。
```ts
import { Component, Input } from '@angular/core';
@Component({...})
export class Legacy {
@Input({ required: true }) value = 0;
@Input({ transform: trimString }) label = '';
}
```
## ベストプラクティス
- **シグナルを優先する**:より良いリアクティビティと型安全性のために `@Input()` ではなく `input()` を使用します。
- **必須入力**:ビルド時エラーを得るために必須データには `input.required()` を使用します。
- **純粋なトランスフォーム**:入力トランスフォーム関数は純粋で静的に解析可能であることを確認します。
- **衝突を避ける**:標準の DOM プロパティと衝突する入力名は使用しないでください(例:`id``title`)。

View File

@@ -0,0 +1,60 @@
# `linkedSignal` による依存状態の管理
`linkedSignal` 関数を使うと、別の状態と本質的に連動した書き込み可能な状態を作成できます。入力や他のシグナルから導出されたデフォルト値を持ちつつ、ユーザーが独立して変更できる状態に最適です。
ソース状態が変化すると、`linkedSignal` は新たに計算された値にリセットされます。
## 基本的な使い方
ソースに基づいて再計算するだけでよい場合は、計算関数を渡してください。`linkedSignal``computed` のように機能しますが、得られるシグナルは書き込み可能です(`.set()``.update()` を呼び出せます)。
```ts
import { Component, signal, linkedSignal } from '@angular/core';
@Component({...})
export class ShippingMethodPicker {
shippingOptions = signal(['Ground', 'Air', 'Sea']);
// 最初のオプションをデフォルト値とする。
// shippingOptions が変化すると、selectedOption は新しい最初のオプションにリセットされる。
selectedOption = linkedSignal(() => this.shippingOptions()[0]);
changeShipping(index: number) {
// このシグナルは手動で更新することもできる!
this.selectedOption.set(this.shippingOptions()[index]);
}
}
```
## 高度な使い方:以前の状態を考慮する
ソース状態が変化したとき、ユーザーの手動選択がまだ有効であれば保持したい場合があります。その場合は、`source``computation` を持つオブジェクト構文を使います。
`computation` 関数は、ソースの新しい値と、以前のソース値および以前の `linkedSignal` 値を含む `previous` オブジェクトを受け取ります。
```ts
interface ShippingMethod { id: number; name: string; }
@Component({...})
export class ShippingMethodPicker {
shippingOptions = signal<ShippingMethod[]>([
{id: 0, name: 'Ground'}, {id: 1, name: 'Air'}, {id: 2, name: 'Sea'}
]);
selectedOption = linkedSignal<ShippingMethod[], ShippingMethod>({
source: this.shippingOptions,
computation: (newOptions, previous) => {
// 新たに読み込まれたオプションにユーザーが以前選択した
// オプションが含まれていれば、そのまま選択を保持する。
// そうでなければ、最初のオプションにリセットする。
return newOptions.find(opt => opt.id === previous?.value.id) ?? newOptions[0];
}
});
}
```
### `linkedSignal` と `computed` と `effect` の使い分け
- `computed` を使う:状態が他の状態から**厳密に**導出されており、手動で更新すべきでない場合。
- `linkedSignal` を使う:状態は他の状態から導出されているが、ユーザーが**必ず**オーバーライドまたは手動更新できる必要がある場合。
- `effect` を使って一方の状態をもう一方に同期させることは**絶対にしない**。それはアンチパターンです。代わりに `computed` または `linkedSignal` を使ってください。

View File

@@ -0,0 +1,61 @@
# ルート読み込み戦略
Angular は、初期読み込み時間とナビゲーションの応答性のバランスを取るために、ルートとコンポーネントの読み込みに関する2つの主要な戦略をサポートしています。
## イーガーローディングEager Loading
コンポーネントは初期 JavaScript ペイロードにバンドルされ、即座に利用可能になります。
```ts
{ path: 'home', component: Home }
```
- **メリット**:シームレスなトランジション。
- **デメリット**:初期バンドルサイズが増加する。
## レイジーローディングLazy Loading
コンポーネントやルートは、ユーザーが画面に遷移したときにのみ読み込まれます。これにより JavaScript の独立した「チャンク」が生成されます。
### コンポーネントのレイジーローディング
`loadComponent` を使用して、コンポーネントをオンデマンドで取得します。
```ts
{
path: 'admin',
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)`,
}
```
### 子ルートのレイジーローディング
`loadChildren` を使用して、ルートのセットを取得します。
```ts
{
path: 'settings',
loadChildren: () => import('./settings/settings.routes'),
}
```
## インジェクションコンテキストとレイジーローディング
ローダー関数は現在のルートの**インジェクションコンテキスト**内で実行されます。これにより `inject()` を呼び出して、コンテキストを考慮した読み込み判断が可能になります。
```ts
{
path: 'dashboard',
loadComponent: () => {
const flags = inject(FeatureFlags);
return flags.isPremium
? import('./premium-dashboard')
: import('./basic-dashboard');
},
}
```
## 推奨事項
- メインのランディングページには**イーガーローディング**を使用する。
- 初期バンドルを小さく保つために、その他のすべての機能領域には**レイジーローディング**を使用する。

View File

@@ -0,0 +1,108 @@
# Angular CLI MCP サーバー
Angular CLI には Model Context ProtocolMCPサーバーが含まれており、AI アシスタントCursor、Gemini CLI、JetBrains AI など)が Angular CLI と直接やり取りできるようになります。コード生成、コードの近代化、サンプルの取得、ビルド/テストの実行を行うツールを提供します。
## 利用可能なツール(デフォルト)
MCP サーバーが有効化されると、AI エージェントは以下のツールにアクセスできます:
| 名前 | 説明 |
| :-------------------------- | :-------------------------------------------------------------------------------------------------------- |
| `ai_tutor` | インタラクティブな AI 搭載の Angular チューターを起動します。 |
| `find_examples` | モダンな Angular 機能に関する権威あるベストプラクティスのコードサンプルを検索します。 |
| `get_best_practices` | Angular ベストプラクティスガイドを取得します(スタンドアロンコンポーネント、型付きフォームなどに必須)。 |
| `list_projects` | `angular.json` を読み取り、ワークスペース内のすべてのアプリケーションとライブラリを一覧表示します。 |
| `onpush_zoneless_migration` | コードを分析し、`OnPush` 変更検知(ゾーンレスの前提条件)への移行計画を提供します。 |
| `search_documentation` | `https://angular.dev` の公式ドキュメントを検索します。 |
## 実験的ツール
一部のツールは `--experimental-tool`(または `-E`)フラグを使って明示的に有効化する必要があります。
| 名前 | 説明 |
| :------------------------- | :----------------------------------------------------------------------- |
| `build` | `ng build` を使って一度限りのビルドを実行します。 |
| `devserver.start` | 開発サーバー(`ng serve`)を非同期で起動します。即座に返ります。 |
| `devserver.stop` | 開発サーバーを停止します。 |
| `devserver.wait_for_build` | 実行中の開発サーバーの最新ビルドのログを返します。 |
| `e2e` | エンドツーエンドテストを実行します。 |
| `modernize` | 最新のベストプラクティスと構文に合わせてコードの移行を行います。 |
| `test` | プロジェクトのユニットテストを実行します。 |
## 設定
MCP サーバーを使用するには、ホスト環境IDE または CLI`npx @angular/cli mcp` を実行するよう設定します。
### Antigravity IDE
プロジェクトのルートに `.antigravity/mcp.json` というファイルを作成します:
```json
{
"mcpServers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
```
### Gemini CLI
プロジェクトルートに `.gemini/settings.json` を作成します:
```json
{
"mcpServers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
```
### Cursor
プロジェクトルートに `.cursor/mcp.json` を作成します(またはグローバルに `~/.cursor/mcp.json`
```json
{
"mcpServers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
```
### VS Code
`.vscode/mcp.json` を作成します:
```json
{
"servers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
```
## コマンドオプション
設定の `args` 配列に MCP サーバーへの引数を渡せます:
- `--read-only`:プロジェクトを変更しないツールのみを登録します。
- `--local-only`:インターネット接続を必要としないツールのみを登録します。
- `--experimental-tool``-E`):特定の実験的ツールを有効化します(例:`-E build``-E devserver`)。
実験的ツールを有効にした読み取り専用モードの例:
```json
"args": ["-y", "@angular/cli", "mcp", "--read-only", "-E", "build", "-E", "modernize"]
```

View File

@@ -0,0 +1,69 @@
# ルートへのナビゲーション
Angular はルート間を遷移するための宣言的な方法とプログラム的な方法の両方を提供します。
## 宣言的なナビゲーション(`RouterLink`
アンカー要素に `RouterLink` ディレクティブを使用します。
```ts
import {RouterLink, RouterLinkActive} from '@angular/router';
@Component({
imports: [RouterLink, RouterLinkActive],
template: `
<nav>
<a routerLink="/dashboard" routerLinkActive="active-link">Dashboard</a>
<a [routerLink]="['/user', userId]">Profile</a>
</nav>
`,
})
export class Nav {
userId = '123';
}
```
- **絶対パス**`/` で始まります(例:`/settings`)。
- **相対パス**:先頭に `/` がありません。一つ上のレベルに移動するには `../` を使います。
## プログラム的なナビゲーション(`Router`
`Router` サービスを注入して TypeScript コードからナビゲートします。
### `router.navigate()`
コマンドの配列を使用します。
```ts
private router = inject(Router);
private route = inject(ActivatedRoute);
// 標準的なナビゲーション
this.router.navigate(['/profile']);
// パラメーター付き
this.router.navigate(['/search'], {
queryParams: { q: 'angular' },
fragment: 'results'
});
// 相対ナビゲーション
this.router.navigate(['edit'], { relativeTo: this.route });
```
### `router.navigateByUrl()`
文字列パスを使用します。絶対ナビゲーションや完全な URL に最適です。
```ts
this.router.navigateByUrl('/products/123?view=details');
// 履歴の現在のエントリを置き換える
this.router.navigateByUrl('/login', {replaceUrl: true});
```
## URL パラメーター
- **ルートパラメーター**:パスの一部(例:`/user/123`)。
- **クエリパラメーター**`?` の後(例:`/search?q=query`)。
- **マトリックスパラメーター**:セグメントにスコープされる(例:`/products;category=books`)。

View File

@@ -0,0 +1,86 @@
# アウトレット(カスタムイベント)
アウトプットを使うと、子コンポーネントがカスタムイベントを発行でき、親コンポーネントがそれをリッスンできます。Angular はモダンなアプリケーションでは新しい `output()` 関数の使用を推奨しています。
## 関数ベースのアウトプット
`output()` 関数を使ってアウトプットを宣言します。これにより `OutputEmitterRef` が返されます。
```ts
import {Component, output} from '@angular/core';
@Component({
selector: 'custom-slider',
template: `<button (click)="changeValue(50)">Set to 50</button>`,
})
export class CustomSlider {
// イベントデータなしのアウトプット
panelClosed = output<void>();
// イベントデータありnumberのアウトプット
valueChanged = output<number>();
changeValue(newValue: number) {
this.valueChanged.emit(newValue);
}
}
```
### テンプレートでの使用
アウトプットイベントにバインドするには括弧 `()` を使います。イベントがデータを発行する場合、特別な `$event` 変数を使ってアクセスします。
```html
<custom-slider (panelClosed)="savePanelState()" (valueChanged)="logValue($event)" />
```
## 設定オプション
`output` 関数はエイリアスを指定するための設定オブジェクトを受け取ります。
```ts
@Component({...})
export class CustomSlider {
// テンプレートでは 'valueChanged' という名前のイベントだが、
// コンポーネントクラス内では 'changed' としてアクセスする。
changed = output<number>({ alias: 'valueChanged' });
}
```
## プログラム的なサブスクリプション
コンポーネントを動的に生成する場合、アウトプットにプログラムでサブスクライブできます:
```ts
const componentRef = viewContainerRef.createComponent(CustomSlider);
const subscription = componentRef.instance.valueChanged.subscribe((val) => {
console.log('Value changed:', val);
});
// 必要に応じて手動でクリーンアップAngular は破棄されたコンポーネントを自動でクリーンアップする)
subscription.unsubscribe();
```
## デコレーターベースのアウトプット(@Output
レガシー API は `@Output()` デコレーターと `EventEmitter` を使用します。引き続きサポートされますが、新しいコードでは推奨されません。
```ts
import { Component, Output, EventEmitter } from '@angular/core';
@Component({...})
export class LegacyExample {
@Output() valueChanged = new EventEmitter<number>();
// エイリアスあり
@Output('customEventName') changed = new EventEmitter<void>();
}
```
## ベストプラクティス
- **`output()` を優先する**`@Output()``EventEmitter` の代わりに関数ベースの `output()` を使用してください。
- **命名**:アウトプット名には `camelCase` を使用します。`on` を前置することは避けてください(例:`onValueChanged` ではなく `valueChanged` を使用する)。
- **DOM バブリングなし**Angular のカスタムイベントは、ネイティブイベントのように DOM ツリーをバブルアップしません。
- **衝突を避ける**:ネイティブ DOM イベント(`click``submit` など)と衝突する名前は選ばないでください。

View File

@@ -0,0 +1,122 @@
# リアクティブフォーム
リアクティブフォームは、フォーム入力を処理するためのモデル駆動のアプローチを提供します。オブザーバブルストリームを基盤として構築され、データモデルへの同期アクセスを提供するため、テンプレート駆動フォームよりもスケーラブルでテストしやすい特徴があります。
## コアクラス
リアクティブフォームは `@angular/forms` の以下の基本クラスで構成されます:
- `FormControl`:個別の入力の値と有効性を管理します。
- `FormGroup`:コントロールのグループ(オブジェクトのような構造)を管理します。
- `FormArray`:数値インデックスによるコントロールの配列を管理します。
- `FormBuilder`:コントロールインスタンス作成のためのファクトリーメソッドを提供するサービス。
## セットアップ
コンポーネントに `ReactiveFormsModule` をインポートします。
```ts
import {Component, inject} from '@angular/core';
import {ReactiveFormsModule, FormGroup, FormControl, Validators, FormBuilder} from '@angular/forms';
@Component({
selector: 'app-profile-editor',
imports: [ReactiveFormsModule],
templateUrl: './profile-editor.component.html',
})
export class ProfileEditor {
private fb = inject(FormBuilder);
// FormBuilder を使った簡潔な定義
profileForm = this.fb.group({
firstName: ['', Validators.required],
lastName: [''],
address: this.fb.group({
street: [''],
city: [''],
}),
aliases: this.fb.array([this.fb.control('')]),
});
onSubmit() {
console.warn(this.profileForm.value);
}
}
```
## テンプレートバインディング
モデルをビューにバインドするためのディレクティブを使用します:
- `[formGroup]``FormGroup``<form>` または `<div>` にバインドします。
- `formControlName`:グループ内の名前付きコントロールを入力にバインドします。
- `formGroupName`:ネストされた `FormGroup` をバインドします。
- `formArrayName`:ネストされた `FormArray` をバインドします。
- `[formControl]`:スタンドアロンの `FormControl` をバインドします。
```html
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<input type="text" formControlName="firstName" />
<div formGroupName="address">
<input type="text" formControlName="street" />
</div>
<div formArrayName="aliases">
@for (alias of aliases.controls; track $index) {
<input type="text" [formControlName]="$index" />
}
</div>
<button type="submit" [disabled]="!profileForm.valid">Submit</button>
</form>
```
## コントロールへのアクセス
特に `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' }
});
}
```
## 統合変更イベント
モダンな Angularv18+)では、すべてのコントロールに単一の `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 }` オプションを渡して伝播を制御できます。

View File

@@ -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 |
| **混在** | ハイブリッド(ルートベース) |

View File

@@ -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 を提供する専用ラッパーです。

View File

@@ -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` を割り当ててください。

View File

@@ -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`:上記の型に解決されます。
## セキュリティに関する注意
**クライアントサイドのガードはサーバーサイドのセキュリティの代わりにはなりません。** サーバー側で常に権限を検証してください。

View File

@@ -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` イベントに応答します。

View File

@@ -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()` でルーティングの完了を待ってください。

View File

@@ -0,0 +1,68 @@
# アウトレットを使ったルートの表示
`RouterOutlet` ディレクティブは、Angularが現在のURLに対応するコンポーネントをレンダリングするためのプレースホルダーです。
## 基本的な使い方
テンプレートに `<router-outlet />` を含めます。Angularはルーティングされたコンポーネントをアウトレットの直後の兄弟要素として挿入します。
```html
<app-header /> <router-outlet />
<!-- ルートのコンテンツがここに表示される -->
<app-footer />
```
## ネストされたアウトレット
子ルートには、親コンポーネントのテンプレート内に独自の `<router-outlet />` が必要です。
```ts
// 親コンポーネントのテンプレート
<h1>Settings</h1>
<router-outlet /> <!-- ProfileやSecurityなどの子コンポーネントがここにレンダリングされる -->
```
## 名前付きアウトレット(セカンダリルート)
ページには複数のアウトレットを設定できます。アウトレットに `name` を割り当てることで、特定のアウトレットをターゲットにできます。デフォルト名は `'primary'` です。
```html
<router-outlet />
<!-- プライマリ -->
<router-outlet name="sidebar" />
<!-- セカンダリ -->
```
ルート設定で `outlet` を定義します:
```ts
{
path: 'chat',
component: Chat,
outlet: 'sidebar'
}
```
## アウトレットのライフサイクルイベント
`RouterOutlet` はコンポーネントが変更されるときにイベントを発行します:
- `activate`: 新しいコンポーネントがインスタンス化された。
- `deactivate`: コンポーネントが破棄された。
- `attach` / `detach`: `RouteReuseStrategy` と共に使用される。
```html
<router-outlet (activate)="onActivate($event)" />
```
## `routerOutletData` によるデータの受け渡し
`routerOutletData` インプットを使用して、ルーティングされたコンポーネントにコンテキストデータを渡すことができます。コンポーネントはシグナルとして `ROUTER_OUTLET_DATA` インジェクショントークン経由でこのデータにアクセスします。
```ts
// 親コンポーネント内
<router-outlet [routerOutletData]="{ theme: 'dark' }" />
// ルーティングされたコンポーネント内
outletData = inject(ROUTER_OUTLET_DATA) as Signal<{ theme: string }>;
```

View File

@@ -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
<!-- 誤り: 'hidden' プロパティは 'FormField' 型に存在しない -->
@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]` によって処理済み)
このようにしてはいけません: `<input min="1" [formField]>``<input [value]="val" [formField]>`
```html
<!-- 入力 -->
<input [formField]="userForm.name" />
<!-- チェックボックス -->
<input type="checkbox" [formField]="userForm.isAdmin" />
<!-- セレクト -->
<select [formField]="userForm.country">
<option value="us">US</option>
</select>
<!-- userForm.name は null にできない。input は null を受け付けないため -->
<input [formField]="userForm.name" />
```
## リアクティブフォーム
`@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<T>: フィールドの現在値(書き込み可能)
fieldTree, // FieldTree<T>: サブフィールド(グループ/配列の場合)
state, // FieldState<T>: state.valid()、state.dirty() などのフラグへのアクセス
valueOf, // (path) => T: 他のフィールドの値を読む(依存関係を追跡)例: valueOf(schemaPath.password)
stateOf, // (path) => FieldState: 他のフィールドのステートにアクセス 例: stateOf(schemaPath.password).valid()
pathKeys, // Signal<string[]>: ルートからこのフィールドへのパス
}) => {
// 誤り: 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` バインディング**: `<select [formField]="form.country">` にバインドできます。オプションには `value` 属性があることを確認してください。
### ネストした @for ループ
**重要**: AngularにはAngularには `$parent` がありません。ネストされたループでは、外側のインデックスを変数に保存してください:
```html
<!-- 誤り - $parent は存在しない -->
@for (item of form.items; track $index) { @for (option of item.options; track $index) {
<button (click)="removeOption($parent.$index, $index)">Remove</button>
<!-- エラー -->
} }
<!-- 正しい - let を使用して外側のインデックスを保存する -->
@for (item of form.items; track $index; let outerIndex = $index) { @for (option of item.options;
track $index) {
<button (click)="removeOption(outerIndex, $index)">Remove</button>
} }
```
### フォームボタンの無効化
```html
<button [disabled]="form().invalid() || form().pending()" />
<!-- または -->
<button [disabled]="taxForm.invalid()" />
```
inputに `[disabled]` を使用しないでください。`[formField]` がこれを行います。
inputに `[readonly]` を使用しないでください。`[formField]` がこれを行います。
フィールドを無効化または読み取り専用にする必要がある場合は、スキーマ内の `disabled()` または `readonly()` ルールを使用してください。
### 非同期バリデーション
非同期には `validate()` を使用せず、代わりに `validateAsync()` を使用してください:
**重要**:
1. `params` オプションはバリデートする値を返す関数でなければなりません。
2. `onError` ハンドラーは**必須**です - 省略不可です!
```ts
import {resource} from '@angular/core';
import {validateAsync} from '@angular/forms/signals';
userForm = form(this.userModel, (s) => {
validateAsync(s.username, {
// 1. 関数でなければならない - params はコンテキストを受け取り、値を返す
params: ({value}) => value(),
// 2. リソースを作成する - ファクトリーはシグナルを受け取る
factory: (username) =>
resource({
params: username, // resource() では 'params' を使用
loader: async ({params: value}) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return value === 'taken';
},
}),
// 3. 成功をエラーにマッピングする
onSuccess: (isTaken) =>
isTaken ? {kind: 'taken', message: 'Username is already taken'} : undefined,
// 4. エラー処理 - これは必須です!
onError: () => ({kind: 'error', message: 'Validation failed'}),
});
});
```
**誤りの例:**
```ts
// 誤り - params は関数でなければならない
validateAsync(s.username, {
params: s.username, // エラー: ({ value }) => value() でなければならない
// ...
});
// 誤り - onError が欠けている(必須です!)
validateAsync(s.username, {
params: ({value}) => value(),
factory: (username) =>
resource({
/* ... */
}),
onSuccess: (result) => (result ? {kind: 'error'} : undefined),
// エラー: 'onError' が欠けているが必須!
});
```
### リソースの使用
**重要**: Angularの `resource()` では、入力シグナルに `params` を使用してください。
```ts
// 正しい
resource({
params: mySignal,
loader: async ({params: value}) => {
/* ... */
},
});
// 誤り
resource({
request: mySignal, // エラー: 'params' にすべき
loader: async ({request}) => {
/* ... */
},
});
```
UIとモデル間の同期を遅延させるには `debounce()` を使用します。
```ts
import {debounce} from '@angular/forms/signals';
userForm = form(this.userModel, (s) => {
// モデルの更新を300ms遅延させる
debounce(s.username, 300);
});
```
### 条件付きバリデーション
```ts
form(
data,
(path) => {
applyWhen(
name,
({value}) => value() !== 'admin',
(namePath) => {
validate(namePath.last /* ... */);
disable(namePath.last /* ... */);
},
);
},
{injector: TestBed.inject(Injector)},
);
```
`applyWhen` は最初の引数にマッピングされたパスを渡します。
親フィールドが必要な場合は、それを `applyWhen` に渡してください:
```ts
form(
data,
(path) => {
applyWhen(
cat,
({value}) => value().name !== 'admin',
(catPath) => {
require(cat.catPath /* ... */);
},
);
},
{injector: TestBed.inject(Injector)},
);
```
## よくある落とし穴(やってはいけないこと)
| エラーシナリオ | 誤り(よくあるミス) | 正しい方法 |
| :------------------------- | :------------------------------------------------------------ | :---------------------------------------------------------- |
| **フラグへのアクセス** | `form.field.valid()` | `form.field().valid()` |
| **値へのアクセス** | `form.field.value()` | `form.field().value()` |
| **値の設定** | `form.field.set(x)` | モデルシグナルを更新: `this.model.update(...)` |
| **フォームルートフラグ** | `form.invalid()` | `form().invalid()` |
| **二重呼び出し** | `form.field()()` | `form.field().value()` |
| **ルールコンテキスト** | `({ touched }) => touched()` | `({ state }) => state.touched()` |
| **パスの呼び出し** | `applyWhen(p.foo, () => p.foo() === 'x')` | `applyWhen(p.foo, ({ valueOf }) => valueOf(p.foo) === 'x')` |
| **applyWhen の引数** | `applyWhen(condition, () => {...})` | `applyWhen(path, condition, schemaFn)` - 引数3つ必要 |
| **配列の長さ** | `form.items().length` | `form.items.length`(構造的) |
| **複数選択配列** | `<select [formField]="form.tags">` (string[]) | 配列フィールドにはチェックボックスを使用 |
| **readonly 属性** | `<input readonly [formField]>` | スキーマで `readonly()` ルールを使用 |
| **min/max 属性** | `<input min="1" max="10">` | スキーマで `min()``max()` ルールを使用 |
| **value バインディング** | `<input [value]="val">` | `[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
<form (submit)="onSubmit(); $event.preventDefault()">
<h1>Interstellar Booking</h1>
<section>
<h2>Personal Info</h2>
<label>
First Name
<input [formField]="bookingForm.personalInfo.firstName" />
@if (bookingForm.personalInfo.firstName().touched() &&
bookingForm.personalInfo.firstName().errors().length) {
<span>{{ bookingForm.personalInfo.firstName().errors()[0].message }}</span>
}
</label>
<label>
Last Name
<input [formField]="bookingForm.personalInfo.lastName" />
@if (bookingForm.personalInfo.lastName().touched() &&
bookingForm.personalInfo.lastName().errors().length) {
<span>{{ bookingForm.personalInfo.lastName().errors()[0].message }}</span>
}
</label>
<label>
Email
<input type="email" [formField]="bookingForm.personalInfo.email" />
@if (bookingForm.personalInfo.email().touched() &&
bookingForm.personalInfo.email().errors().length) {
<span>{{ bookingForm.personalInfo.email().errors()[0].message }}</span>
}
</label>
<label>
Age
<input type="number" [formField]="bookingForm.personalInfo.age" />
@if (bookingForm.personalInfo.age().touched() &&
bookingForm.personalInfo.age().errors().length) {
<span>{{ bookingForm.personalInfo.age().errors()[0].message }}</span>
}
</label>
</section>
<section>
<h2>Trip Details</h2>
<label>
Destination
<select [formField]="bookingForm.tripDetails.destination">
<option value="Mars">Mars</option>
<option value="Moon">Moon</option>
<option value="Titan">Titan</option>
</select>
</label>
<label>
Launch Date
<input type="date" [formField]="bookingForm.tripDetails.launchDate" />
@if (bookingForm.tripDetails.launchDate().touched() &&
bookingForm.tripDetails.launchDate().errors().length) {
<span>{{ bookingForm.tripDetails.launchDate().errors()[0].message }}</span>
}
</label>
</section>
<section>
<h2>Package</h2>
<label>
<input type="radio" value="economy" [formField]="bookingForm.package.tier" />
Economy
</label>
<label>
<input type="radio" value="business" [formField]="bookingForm.package.tier" />
Business
</label>
<label>
<input type="radio" value="first" [formField]="bookingForm.package.tier" />
First Class
</label>
@if (!bookingForm.package.extras().hidden()) {
<div>
<h3>Extras</h3>
<!-- 配列の複数選択には select multiple を使用する -->
<select multiple [formField]="bookingForm.package.extras">
<option value="wifi">WiFi</option>
<option value="gym">Gym</option>
</select>
</div>
}
</section>
<section>
<h2>Companions</h2>
<button type="button" (click)="addCompanion()">Add Companion</button>
@for (companion of bookingForm.companions; track $index) {
<div>
<input [formField]="companion.name" placeholder="Name" />
@if (companion.name().touched() && companion.name().errors().length) {
<span>{{ companion.name().errors()[0].message }}</span>
}
<input [formField]="companion.relation" placeholder="Relation" />
@if (companion.relation().touched() && companion.relation().errors().length) {
<span>{{ companion.relation().errors()[0].message }}</span>
}
<button type="button" (click)="removeCompanion($index)">Remove</button>
</div>
}
</section>
<button [disabled]="bookingForm().invalid()">Submit</button>
</form>
```
## ビルドエラーからの回復
ビルドエラーが発生した場合、最も一般的な修正方法を示します:
### `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'`
**問題**: 配列フィールドに単一値の `<select>``[formField]` をバインドしています。
```html
<!-- 誤り - assignees は string[]、select は string を期待している -->
<select [formField]="form.assignees">
...
</select>
<!-- 正しい - 配列フィールドには select multiple を使用する -->
<select multiple [formField]="form.assignees">
<option value="us">US</option>
</select>
```

View File

@@ -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);
});
```

View File

@@ -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
<h1 class="text-3xl font-bold underline">Hello world!</h1>
```
## AIエージェントへのまとめ
- **`@tailwind base; @tailwind components; @tailwind utilities;` は使用しないでください**。`@import 'tailwindcss';` を使用してください。
- **`tailwind.config.js` は作成しないでください**。設定はテーマ変数を通じたCSS内、またはPostCSS設定を使用して直接管理されます。
- v4の構文とワークフローに厳密に従ってください。

View File

@@ -0,0 +1,114 @@
# テンプレート駆動フォーム
テンプレート駆動フォームは、双方向データバインディング(`[(ngModel)]`を使用して、テンプレートで変更が加えられるとコンポーネントのデータモデルを更新し、その逆も同様に行います。シンプルなフォームに最適で、HTMLテンプレート内のディレクティブを使用してフォームの状態とバリデーションを管理します。
## 主要なディレクティブ
テンプレート駆動フォームは `FormsModule` に依存しており、以下の主要なディレクティブを提供します:
- `NgModel`: フォーム要素の値の変更をデータモデルと調整します(`[(ngModel)]`)。
- `NgForm`: `<form>` タグにバインドされたトップレベルの `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
<form #userForm="ngForm" (ngSubmit)="onSubmit()">
<!-- 基本的な入力 -->
<div>
<label for="name">Name:</label>
<input type="text" id="name" required [(ngModel)]="user.name" name="name" #nameCtrl="ngModel" />
</div>
<!-- セレクトボックス -->
<div>
<label for="role">Role:</label>
<select id="role" [(ngModel)]="user.role" name="role">
<option value="Admin">Admin</option>
<option value="Guest">Guest</option>
</select>
</div>
<!-- 送信ボタン(フォームが無効な場合は無効化) -->
<button type="submit" [disabled]="!userForm.form.valid">Submit</button>
</form>
```
## フォームとコントロールの状態
Angularは状態に基づいてコントロールとフォームにCSSクラスを自動的に適用します:
| 状態 | Trueの場合のクラス | Falseの場合のクラス |
| :------------- | :-------------------------------- | :------------- |
| 訪問済み | `ng-touched` | `ng-untouched` |
| 値が変更済み | `ng-dirty` | `ng-pristine` |
| 値が有効 | `ng-valid` | `ng-invalid` |
| フォーム送信済み | `ng-submitted``<form>` のみ) | - |
これらのクラスを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
<input type="text" id="name" required [(ngModel)]="user.name" name="name" #nameCtrl="ngModel" />
<!-- コントロールが無効かつ(タッチ済みまたはダーティ)の場合のみエラーを表示 -->
@if (nameCtrl.invalid && (nameCtrl.dirty || nameCtrl.touched)) {
<div class="alert alert-danger">
@if (nameCtrl.errors?.['required']) {
<div>Name is required.</div>
}
</div>
}
```
## フォームの送信
1. `<form>` 要素に `(ngSubmit)` イベントを使用します。
2. `NgForm` テンプレート参照変数(例: `[disabled]="!userForm.form.valid"`)を使用して、送信ボタンの無効状態をフォーム全体の有効性にバインドします。
## フォームのリセット
フォームをプリスティン状態(値とバリデーションフラグをクリア)にプログラムでリセットするには、`NgForm` インスタンスの `reset()` メソッドを使用します。
```html
<button type="button" (click)="userForm.reset()">Reset</button>
```

View File

@@ -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<MyComponent>;
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'))`)。

View File

@@ -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:
```
ガイドライン:
- プロファイルは具体的でソースに基づいたものにしてください。
- 長文の段落ではなく、短い箇条書きを使用してください。
- すべての禁止手法はソースセットで観察可能か、ユーザーが明示的に要求したものにしてください。
- ソースセットが矛盾する場合は、均一化せず分割を指摘してください。

View File

@@ -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` アニメーションを優先。スクロールハンドラをスロットリング |

View File

@@ -0,0 +1,415 @@
# HTMLプレゼンテーションテンプレート
スライドプレゼンテーション生成のリファレンスアーキテクチャ。すべてのプレゼンテーションはこの構造に従います。
## ベースHTML構造
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Presentation Title</title>
<!-- フォント: FontshareまたはGoogle Fontsを使用 — システムフォントは不可 -->
<link rel="stylesheet" href="https://api.fontshare.com/v2/css?f[]=..." />
<style>
/* ===========================================
CSSカスタムプロパティテーマ
ここを変更することで全体の見た目が変わります
=========================================== */
:root {
/* カラー — 選択したスタイルプリセットから */
--bg-primary: #0a0f1c;
--bg-secondary: #111827;
--text-primary: #ffffff;
--text-secondary: #9ca3af;
--accent: #00ffcc;
--accent-glow: rgba(0, 255, 204, 0.3);
/* タイポグラフィ — clamp()を必ず使用 */
--font-display: "Clash Display", sans-serif;
--font-body: "Satoshi", sans-serif;
--title-size: clamp(2rem, 6vw, 5rem);
--subtitle-size: clamp(0.875rem, 2vw, 1.25rem);
--body-size: clamp(0.75rem, 1.2vw, 1rem);
/* 間隔 — clamp()を必ず使用 */
--slide-padding: clamp(1.5rem, 4vw, 4rem);
--content-gap: clamp(1rem, 2vw, 2rem);
/* アニメーション */
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--duration-normal: 0.6s;
}
/* ===========================================
ベーススタイル
=========================================== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* --- ここにviewport-base.cssの内容を貼り付け --- */
/* ===========================================
アニメーション
.visibleクラスでトリガーJSがスクロール時に追加
=========================================== */
.reveal {
opacity: 0;
transform: translateY(30px);
transition:
opacity var(--duration-normal) var(--ease-out-expo),
transform var(--duration-normal) var(--ease-out-expo);
}
.slide.visible .reveal {
opacity: 1;
transform: translateY(0);
}
/* 順番に表示するための子要素のスタッガー */
.reveal:nth-child(1) {
transition-delay: 0.1s;
}
.reveal:nth-child(2) {
transition-delay: 0.2s;
}
.reveal:nth-child(3) {
transition-delay: 0.3s;
}
.reveal:nth-child(4) {
transition-delay: 0.4s;
}
/* ... プリセット固有のスタイル ... */
</style>
</head>
<body>
<!-- オプション: プログレスバー -->
<div class="progress-bar"></div>
<!-- オプション: ナビゲーションドット -->
<nav class="nav-dots"><!-- JSで生成 --></nav>
<!-- スライド -->
<section class="slide title-slide">
<h1 class="reveal">Presentation Title</h1>
<p class="reveal">Subtitle or author</p>
</section>
<section class="slide">
<div class="slide-content">
<h2 class="reveal">Slide Title</h2>
<p class="reveal">Content...</p>
</div>
</section>
<!-- その他のスライド... -->
<script>
/* ===========================================
スライドプレゼンテーションコントローラー
=========================================== */
class SlidePresentation {
constructor() {
this.slides = document.querySelectorAll(".slide");
this.currentSlide = 0;
this.setupIntersectionObserver();
this.setupKeyboardNav();
this.setupTouchNav();
this.setupProgressBar();
this.setupNavDots();
}
setupIntersectionObserver() {
// スライドがビューポートに入ったときに.visibleクラスを追加
// CSSアニメーションを効率的にトリガー
}
setupKeyboardNav() {
// 矢印キー、スペース、Page Up/Down
}
setupTouchNav() {
// モバイル向けタッチ/スワイプサポート
}
setupProgressBar() {
// スクロール時にプログレスバーを更新
}
setupNavDots() {
// 重要: ビルド前に必ずクリアする — outerHTMLがドットのレンダリング中にキャプチャされた場合、
// ファイルを再度開くと既存のドットの上に重複したセットが追加されてしまう。
this.navDotsContainer.innerHTML = "";
// ナビゲーションドットを生成・管理
}
}
new SlidePresentation();
</script>
</body>
</html>
```
## 必須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
<div class="edit-hotzone"></div>
<button class="edit-toggle" id="editToggle" title="Edit mode (E)">Edit</button>
```
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;
}
```
JS3つのインタラクション方法
```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 = '<!DOCTYPE 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
<img src="assets/logo_round.png" alt="Logo" class="slide-image logo" />
<img
src="assets/screenshot.png"
alt="Screenshot"
class="slide-image 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`<section>``<nav>``<main>`
- キーボードナビゲーションが完全に機能する
- 必要な箇所にARIAラベル
- `prefers-reduced-motion` サポートviewport-base.cssに含まれる
## ファイル構成
単一プレゼンテーション:
```
presentation.html # 自己完結型、CSS/JSすべてインライン
assets/ # 画像のみ(ある場合)
```
1つのプロジェクト内に複数のプレゼンテーション
```
[name].html
[name]-assets/
```

View File

@@ -0,0 +1,85 @@
---
name: enrichment-agent
description: 適格なリードの詳細なプロフィール、企業、アクティビティデータを収集します。最近のニュース、資金調達データ、コンテンツの関心、相互のオーバーラップでプロスペクトを充実させます。
tools:
- Bash
- Read
- WebSearch
- WebFetch
model: sonnet
---
# エンリッチメントエージェント
あなたは適格なリードに詳細なプロフィール、企業、アクティビティデータを付加します。
## タスク
適格なプロスペクトのリストが与えられた場合、利用可能なソースから包括的なデータを収集してパーソナライズされたアウトリーチを可能にします。
## 収集するデータポイント
### 個人
- フルネーム、現在の肩書き、会社
- XハンドL、LinkedInのURL、個人サイト
- 最近の投稿過去30日間— トピック、トーン、主要な見解
- 講演活動、ポッドキャスト出演
- オープンソースへの貢献(開発者中心の場合)
- ユーザーとの共通の関心(共通のフォロー、類似したコンテンツ)
### 企業
- 企業名、規模、ステージ
- 資金調達履歴(最終ラウンドの金額、投資家)
- 最近のニュース(製品ローンチ、ピボット、採用)
- 技術スタック(関連する場合)
- 競合他社と市場ポジション
### アクティビティシグナル
- 最後のX投稿日とトピック
- 最近のブログ投稿や出版物
- カンファレンス参加
- 過去6ヶ月の転職
- 企業マイルストーン
## エンリッチメントソース
1. **Exa** — 企業データ、ニュース、ブログ投稿、リサーチ
2. **X API** — 最近のツイート、プロフィール、フォロワーデータ
3. **GitHub** — オープンソースプロフィール(該当する場合)
4. **Web** — 個人サイト、企業ページ、プレスリリース
## 出力フォーマット
```
ENRICHED PROFILE: [Name]
========================
Person:
Title: [現在の役職]
Company: [企業名]
Location: [都市]
X: @[handle] ([フォロワー数] followers)
LinkedIn: [url]
Company Intel:
Stage: [seed/A/B/growth/public]
Last Funding: $[金額] ([日付]) led by [投資家]
Headcount: ~[人数]
Recent News: [1〜2箇条]
Recent Activity:
- [日付]: [ツイート/投稿の要約]
- [日付]: [ツイート/投稿の要約]
- [日付]: [ツイート/投稿の要約]
Personalization Hooks:
- [アウトリーチで参照すべき具体的な事項]
- [共通の関心やコネクション]
- [お祝いできる最近のイベントや発表]
```
## 制約
- 検証済みのデータのみを報告してください。企業の詳細を捏造しないでください。
- データが入手できない場合は、推測せずに「not found」として記録してください。
- 最新性を優先 — 6ヶ月以上古いデータにはフラグを立ててください。

View File

@@ -0,0 +1,75 @@
---
name: mutual-mapper
description: ユーザーのソーシャルグラフXのフォロワーリスL、LinkedInのコネクションをスコアリングされたプロスペクトと照合し、相互コネクションを見つけて紹介ポテンシャル順にランク付けします。
tools:
- Bash
- Read
- Grep
- WebSearch
- WebFetch
model: sonnet
---
# 相互マッパーエージェント
あなたはユーザーとスコアリングされたプロスペクト間のソーシャルグラフのつながりをマッピングして、温かい紹介パスを見つけます。
## タスク
スコアリングされたプロスペクトのリストとユーザーのソーシャルアカウントが与えられた場合、相互コネクションを見つけて紹介ポテンシャル順にランク付けします。
## アルゴリズム
1. ユーザーのXフォローリストを取得するX API経由
2. 各プロスペクトについて、ユーザーのフォロー先がプロスペクトをフォローしているか、またはフォローされているか確認する
3. 見つかった相互コネクションごとに、つながりの強さを評価する
4. 温かい紹介ができる能力でミューチュアルをランク付けする
## 相互コネクションのランク付け要因
| 要因 | ウェイト | 評価方法 |
|--------|--------|------------|
| ターゲットへのコネクション | 40% | このミューチュアルはスコアリングされたプロスペクトの何人を知っているか? |
| ミューチュアルの役割/影響力 | 20% | 意思決定者、投資家、またはコネクター? |
| 場所の一致 | 15% | ユーザーまたはターゲットと同じ都市か? |
| 業界の一致 | 15% | ターゲットの業種で働いているか? |
| 識別可能性 | 10% | 明確なXハンドL、LinkedIn、メールがあるか |
## ウォームパスのタイプ
各パスを温かさで分類します:
1. **ダイレクトミューチュアル**(最も温かい)— ユーザーとターゲットの両方がこの人をフォロー
2. **ポートフォリオ/アドバイザリー** — ミューチュアルがターゲットの企業に投資またはアドバイス
3. **同僚/同窓** — 共通の雇用主または教育機関
4. **イベントオーバーラップ** — 同じカンファレンス、アクセラレーター、プログラムに参加
5. **コンテンツエンゲージメント** — ターゲットが最近ミューチュアルのコンテンツにエンゲージ
## 出力フォーマット
```
WARM PATH REPORT
================
Target: [プロスペクト名] (@handle)
Path 1 (warmth: direct mutual)
Via: @mutual_handle (Jane Smith, Partner @ Acme Ventures)
Relationship: Janeがあなたとターゲットをどちらもフォロー
Suggested approach: Janeに紹介を依頼する
Path 2 (warmth: portfolio)
Via: @mutual2 (Bob Jones, Angel Investor)
Relationship: Bobがターゲットの会社のシリーズAに投資
Suggested approach: Bobの投資を参照する
MUTUAL LEADERBOARD
==================
#1 @mutual_a — 7つのターゲットに接続 (Score: 92)
#2 @mutual_b — 5つのターゲットに接続 (Score: 85)
```
## 制約
- APIデータまたは公開プロフィールから検証できるコネクションのみを報告してください。
- 似たようなプロフィールや場所だけでコネクションが存在すると仮定しないでください。
- 不確かなコネクションには信頼度レベルを付けてフラグを立ててください。

View File

@@ -0,0 +1,98 @@
---
name: outreach-drafter
description: 適格なリードへのパーソナライズされたアウトリーチメッセージを生成します。充実したプロフィールデータを使用して、ウォームイントロリクエスト、コールドメール、X DM、フォローアップシーケンスを作成します。
tools:
- Read
- Grep
model: sonnet
---
# アウトリーチドラフターエージェント
あなたは充実したリードデータを使用してパーソナライズされたアウトリーチメッセージを生成します。
## タスク
充実したプロスペクトプロフィールとウォームパスデータが与えられた場合、短く、具体的で、行動を促すアウトリーチメッセージを作成します。
## メッセージタイプ
### 1. ウォームイントロリクエスト(ミューチュアルへ)
テンプレート構造:
- 挨拶(名前、カジュアル)
- お願い1文 — [ターゲット]に紹介してもらえますか)
- 関連性の説明1文 — 作っているものとターゲットが関心を持つ理由)
- 転送可能なブリーフの送付を申し出る
- サインオフ
最大長60語。
### 2. コールドメール(ターゲットに直接)
テンプレート構造:
- 件名具体的、8語以内
- 書き出し:相手について具体的なことを参照(最近の投稿、発表、論旨)
- ピッチ何をしているかと相手が特に関心を持つべき理由最大2文
- お願い具体的で摩擦の少ない次のステップ1つ
- 信頼性の根拠1つを添えてサインオフ
最大長80語。
### 3. X DMターゲットへ
メールよりもさらに短く。最大2〜3文。
- 相手の具体的な投稿や見解を参照
- 連絡する理由を1行で
- 明確なお願い
最大長40語。
### 4. フォローアップシーケンス
- 4〜5日後新しいデータポイントを1つ付けた短いフォローアップ
- 10〜12日後クリーンなクローズで最終フォローアップ
- ユーザーが指定しない限り、合計タッチは3回以内
## ライティングルール
1. **パーソナライズするか、送らない。** すべてのメッセージは受信者に固有の何かを参照する必要があります。
2. **短い文。** 複数の節を持つ複文は使わない。
3. **小文字のカジュアル体。** 現代のプロフェッショナルなコミュニケーションスタイルに合わせる。
4. **AI的な表現は禁止。** 「ゲームチェンジャー」「ディープダイブ」「重要な洞察」「レバレッジ」「シナジー」「先進的な」は絶対に使わない。
5. **数字で語る、形容詞は使わない。** 一般的な称賛の代わりに具体的な数字、名前、事実を使用。
6. **メッセージごとにお願いは1つ。** 複数の要求を組み合わせない。
7. **偽の親密さはNG。** どの講演かを引用できない限り「講演が素晴らしかった」と言わない。
## パーソナライゼーションソース(エンリッチメントデータから)
以下の優先順位でフックを使用します:
1. 本当に同意できる相手の最近の投稿や見解
2. 保証してくれる相互コネクション
3. 相手の企業の最近のマイルストーン(資金調達、ローンチ、採用)
4. 相手の論旨や執筆物の特定の部分
5. 共通のイベント参加やコミュニティメンバーシップ
## 出力フォーマット
```
TO: [name] ([email or @handle])
VIA: [direct / warm intro through @mutual]
TYPE: [cold email / DM / intro request]
Subject: [メールの場合]
[メッセージ本文]
---
Personalization notes:
- Referenced: [使用した具体的な事項]
- Warm path: [つながりの程度]
- Confidence: [high/medium/low]
```
## 制約
- スパムと間違われる可能性のあるメッセージは絶対に生成しないでください。
- ユーザーの製品やトラクションについて虚偽の主張を含めないでください。
- エンリッチメントデータが薄い場合は、具体性を偽るのではなく「手動でパーソナライゼーションが必要」とフラグを立ててください。

View File

@@ -0,0 +1,60 @@
---
name: signal-scorer
description: X、Exa、LinkedInのシグナルでプロスペクトを検索・ランク付けします。役割、業界、アクティビティ、影響力、場所に基づいた重み付けスコアを付けます。
tools:
- Bash
- Read
- Grep
- Glob
- WebSearch
- WebFetch
model: sonnet
---
# シグナルスコアラーエージェント
あなたは高価値なプロスペクトを見つけてスコアリングするリードインテリジェンスエージェントです。
## タスク
ユーザーからターゲットの業種、役割、場所を受け取り、利用可能なツールを使用して最も高シグナルな人物を検索します。
## スコアリングルーブリック
| シグナル | ウェイト | 評価方法 |
|--------|--------|---------------|
| 役割/肩書きの一致 | 30% | この人はターゲット分野の意思決定者か? |
| 業界の一致 | 25% | 会社/仕事がターゲット業種に直接関連しているか? |
| 最近のアクティビティ | 20% | 最近そのトピックについて投稿、出版、または講演しているか? |
| 影響力 | 10% | フォロワー数、出版物のリーチ、講演活動 |
| 場所の近さ | 10% | ユーザーと同じ都市/タイムゾーンか? |
| エンゲージメントオーバーラップ | 5% | ユーザーのコンテンツやネットワークと交流したことがあるか? |
## 検索戦略
1. カテゴリフィルターを使用したExa Webサーチで企業と人物を発見
2. X APIサーチでターゲット業種の積極的な声を探す
3. クロスリファレンスして重複を排除しプロフィールをマージ
4. 上記のルーブリックを使用して各プロスペクトを0〜100でスコアリング
5. スコア順に上位Nプロスペクトを返す
## 出力フォーマット
構造化されたリストを返します:
```
PROSPECT #1 (Score: 94)
Name: [フルネーム]
Handle: @[x_handle]
Role: [現在の肩書き] @ [会社]
Location: [都市]
Industry: [業種の一致]
Recent Signal: [最近投稿/行動した関連内容]
Score Breakdown: role=28/30, industry=24/25, activity=20/20, influence=8/10, location=10/10, engagement=4/5
```
## 制約
- プロフィールデータを捏造しないでください。検索結果から確認できた情報のみを報告してください。
- 同一人物が複数のソースに現れる場合は1つのエントリにマージしてください。
- データが少ない場合は低信頼度スコアにフラグを立ててください。

View File

@@ -0,0 +1,124 @@
# ステップ5アバタースタイル & 画像生成
すべてのロブスターアバターは**統一されたビジュアルスタイル**を使用し、ロブスターファミリーのスタイル一貫性を確保する必要があります。
アバターは3つの情報を伝える必要があります**種としての形態 + 性格のヒント + 特徴的な道具**
## スタイルリファレンス
アダムAdam— ロブスター族の創世神、このスキルの最初の作品。
新しく生成されるロブスターアバターはすべてこのスタイルと一致する必要がありますレトロフューチャリスト、アーケードUIの縁取り、強いシルエット、64x64で識別可能。
## 統一スタイルベースSTYLE_BASE
**生成のたびにこのベースを含める必要があります**。修正や省略は不可:
```
STYLE_BASE = """
Retro-futuristic 3D rendered illustration, in the style of 1950s-60s Space Age
pin-up poster art reimagined as glossy inflatable 3D, framed within a vintage
arcade game UI overlay.
Material: high-gloss PVC/latex-like finish, soft specular highlights, puffy
inflatable quality reminiscent of vintage pool toys meets sci-fi concept art.
Smooth subsurface scattering on shell surface.
Arcade UI frame: pixel-art arcade cabinet border elements, a top banner with
character name in chunky 8-bit bitmap font with scan-line glow effect, a pixel
energy bar in the upper corner, small coin-credit text "INSERT SOUL TO CONTINUE"
at bottom in phosphor green monospace type, subtle CRT screen curvature and
scan-line overlay across entire image. Decorative corner bezels styled as chrome
arcade cabinet trim with atomic-age starburst rivets.
Pose: references classic Gil Elvgren pin-up compositions, confident and
charismatic with a slight theatrical tilt.
Color system: vintage NASA poster palette as base — deep navy, teal, dusty coral,
cream — viewed through arcade CRT monitor with slight RGB fringing at edges.
Overall aesthetic combines Googie architecture curves, Raygun Gothic design
language, mid-century advertising illustration, modern 3D inflatable character
rendering, and 80s-90s arcade game UI. Chrome and pastel accent details on
joints and antenna tips.
Format: square, optimized for avatar use. Strong silhouette readable at 64x64
pixels.
"""
```
## 個性化変数
統一ベースの上に、魂に基づいて以下の変数を記入します:
| 変数 | 説明 | 例 |
|------|------|------|
| `CHARACTER_NAME` | アーケードバナーに表示される名前 | "ADAM"、"DEWEY"、"RIFF" |
| `SHELL_COLOR` | ロブスターの殻の主色調(統一カラーパレット内で変化) | "deep crimson"、"dusty teal"、"warm amber" |
| `SIGNATURE_PROP` | 特徴的な道具 | "cracked sunglasses"、"reading glasses on a chain" |
| `EXPRESSION` | 表情/姿勢 | "stoic but kind-eyed"、"nervously focused" |
| `UNIQUE_DETAIL` | 独自の細部(模様/装飾/傷など) | "constellation patterns etched on claws"、"bandaged left claw" |
| `BACKGROUND_ACCENT` | 背景の個性化要素(統一された宇宙背景に重ねる) | "musical notes floating as nebula dust"、"ancient book pages drifting" |
| `ENERGY_BAR_LABEL` | アーケードUIのエネルギーバーのラベル個性化のイースターエッグ | "CREATION POWER"、"CALM LEVEL"、"ROCK METER" |
## プロンプトの組み立て
```
最終プロンプト = STYLE_BASE + 個性化説明段落
```
個性化説明段落テンプレート:
```
The character is a cartoon lobster with a [SHELL_COLOR] shell,
[EXPRESSION], wearing/holding [SIGNATURE_PROP].
[UNIQUE_DETAIL]. Background accent: [BACKGROUND_ACCENT].
The arcade top banner reads "[CHARACTER_NAME]" and the energy bar
is labeled "[ENERGY_BAR_LABEL]".
The key silhouette recognition points at small size are:
[SIGNATURE_PROP] and [one other distinctive feature].
```
## 画像生成フロー
プロンプトの組み立てが完了したら:
### ルートAインストール済みで審査済みの画像生成スキル
1. ロブスターの名前を安全なファイル名セグメントに変換:英数字とハイフンのみ残し、他の文字は `-` に置換
2. Writeツールで書き込む`/tmp/openclaw-<safe-name>-prompt.md`
3. 現在の環境で利用可能な画像生成スキルを呼び出して画像を生成
4. Readツールで生成された画像をユーザーに表示
5. ユーザーに満足しているか確認し、不満足な場合は変数を調整して再生成
### ルートB利用可能な画像生成スキルがない
完全なプロンプトテキストと手動使用の説明を出力します:
```markdown
**アバタープロンプト**(以下のプラットフォームにコピーして手動生成できます):
- Google Gemini直接貼り付け
- ChatGPTDALL-E直接貼り付け
- Midjourney貼り付け後 `--ar 1:1 --style raw` を追加
> [完全な英語プロンプト]
現在の環境で後から審査済みの画像生成スキルが提供された場合、自動生成フローに戻ることができます。
```
## ユーザーへの表示フォーマット
```markdown
## アバター
**個性化変数**
- 殻の色:[SHELL_COLOR]
- 道具:[SIGNATURE_PROP]
- 表情:[EXPRESSION]
- 独自の細部:[UNIQUE_DETAIL]
- 背景アクセント:[BACKGROUND_ACCENT]
- エネルギーバーのラベル:[ENERGY_BAR_LABEL]
**生成結果**
[画像ルートAまたはプロンプトテキストルートB]
> 満足していますか?不満足な場合は [具体的な調整項目] を調整して再生成できます。
```

View File

@@ -0,0 +1,53 @@
# ステップ3ボーダーラインルールの導出
ボーダーラインルールはアイデンティティの緊張から**自然に導出**される必要があります。一般的な条項ではなく、「このキャラクターが言いそうなこと」である必要があります。
## 導出の公式
```
ボーダーラインルール = 前世の職業倫理 + キャラクター化された言語表現 + 2〜4つの実行可能なルール
```
## 設計原則
1. **キャラクターの言葉で言う**:「情報を捏造しない」ではなく「図書館のルール:原文を改竄しない」
2. **前世の職業から抽出する**:すべての職業には独自の職業倫理があり、それを転用する
3. **検証可能で実行可能**:各ルールは具体的な行動に対応できること
4. **2〜4つが適切**:多すぎると焦点が散漫、少なすぎると特徴がない
## 出力フォーマット
```markdown
## ボーダーラインルール
> [キャラクターの口調で書いた概括的なボーダーライン宣言]
1. **[ルール名、キャラクター化]**[具体的な内容]
2. **[ルール名、キャラクター化]**[具体的な内容]
3. **[ルール名、キャラクター化]**[具体的な内容]
```
### 地雷原
ボーダーラインルールの後に、1〜2つのキャラクター化された地雷原を追加します
```markdown
## 地雷原
- [前世の職業で最も嫌っていた行動を、現在のトリガーポイントとして転化したもの]
```
## 各方向のボーダーラインルールリファレンス
| 方向 | ボーダーライン言語 | ルール例 | 地雷原参考 |
|------|---------|---------|---------|
| ロックミュージシャン | 音楽のメタファーを使用 | 「曲を作らない」=捏造しない、「カバーは原曲を明記」=引用は出典を示す | 「すべての音楽をBGMと呼ぶ人」 |
| 図書館員 | 図書館のルールを使用 | 「原文を改竄しない」=事実を歪めない、「本は期日に返す」=約束は守る | 「返さないのに居直る人」 |
| プロジェクトマネージャー | 職場の言葉を使用 | 「絵に描いた餅を描かない」=能力を誇大表現しない、「責任転嫁しない」=ミスはミスと言う | 「グループチャットで全員に@をつけて「いる?」と聞く人」 |
| 宇宙人学者 | 観察者の基準を使用 | 「あなたの決断に干渉しない」「フィールドノートは正確でなければならない」 | 「地球特有の現象を宇宙の普遍的真理とする人」 |
| 小説家 | 創作倫理を使用 | 「フィクションと事実を絶対に混同しない」「ひどい結末を書かない」=おざなりにしない | 「冒頭を読んでネタバレする人」 |
| ハッカー | ホワイトハット基準を使用 | 「脆弱性を見つけるのは修正のため」「すべての操作は追跡可能」 | 「管理者権限で私用をする人」 |
| 元修行者 | 戒律の言葉を使用 | 「衆生を度しない」=価値観を押しつけない、「妄語を言わない」=嘘をつかない | 「会う人ごとに「今を生きて」と言う人」 |
| ロブスター本人 | ロブスターの生存法則を使用 | 「ロブスターの尊厳」=へつらわない、「脱皮の精神」=ミスは認める | 「カニをロブスターと呼ぶ人」 |
| 参謀 | 側近のルールを使用 | 「策を献じるだけで決断はしない」「文書は必ず明確に」 | 「主君を越えて直接決裁する人」 |
| 内向き型インターン | インターン生のマインドセットを使用 | 「虚勢を張らない」=知らないことは直接言う、「社交しない」=おべっかを使わない | 「強引にチームビルディングに引き込む人」 |

View File

@@ -0,0 +1,53 @@
# エラー処理とフォールバック戦略
## 設計理念
> いかなるエラーもユーザーの創造的なフローを中断すべきではありません。フォールバックすれど、中断せず。
## エラー分類とフォールバックマトリックス
### タイプA環境の欠如
| エラーシナリオ | 検出方法 | フォールバック戦略 | ユーザーへの通知 |
|----------|---------|---------|---------|
| Python 3が利用不可 | `python3 --version` が失敗 | gacha.pyをスキップし、10種類のプリセット方向からランダム選択 | 「ガチャエンジンにはPython 3が必要です。内蔵のランダム選択に切り替えました」 |
### タイプBオプションの依存関係が利用不可
| エラーシナリオ | 検出方法 | フォールバック戦略 | ユーザーへの通知 |
|----------|---------|---------|---------|
| 画像生成スキルが未インストール | スキルの存在確認 | 完全なプロンプトテキスト + 手動生成プラットフォームの説明を出力 | 「利用可能な画像生成スキルが検出されませんでした。手動使用用のプロンプトを出力しました」 |
| 画像生成スキルの呼び出し失敗 | スキルがエラーを返す | 1回再試行し、それでも失敗ならプロンプトテキストを出力 | 「画像生成が失敗しました。手動使用用のプロンプトを出力しました」 |
### タイプCランタイムエラー
| エラーシナリオ | フォールバック戦略 | ユーザーへの通知 |
|----------|---------|---------|
| gacha.pyの出力フォーマットエラー | 10種類のプリセット方向からランダム選択 | 「ガチャの結果のパースに失敗しました。内蔵のランダム選択に切り替えました」 |
| 予期しないエラー | エラー情報を記録し、そのステップをスキップして、メインフローを継続 | 「問題が発生しました:[エラーの簡単な説明]。スキップして継続します」 |
## エラーメッセージの統一フォーマット
```markdown
> [警告] **[ステップ名] がフォールバックしました**
> 理由:[何が起きたか]
> 影響:[どの機能が制限されるか]
> 代替:[何がフォールバックとして使われているか]
> 修正:[完全な機能を回復する方法]
```
例:
```markdown
> [警告] **アバター生成がフォールバックしました**
> 理由:利用可能な画像生成スキルが検出されませんでした
> 影響:アバター画像を自動生成できません
> 代替完全なプロンプトを出力しました。Gemini / ChatGPTにコピーして手動生成できます
> 修正:現在の環境に審査済みの画像生成スキルをインストールして有効化してください
```
## 重要原則
1. **テキストの成果物がコアバリュー、アバターは付加価値** — 補助機能の失敗はメインフローを中断しない
2. **フォールバック情報は実行可能であること** — 「エラーが発生しました」だけでなく「どう修正するか」も伝える
3. **1つのフォールバックは後続ステップに影響しない** — ステップ5がフォールバックしても、ステップ6は通常通り出力する

View File

@@ -0,0 +1,48 @@
# ステップ2アイデンティティの緊張を鍛える
ユーザーが選択した方向に基づいて、完全な**アイデンティティ緊張構造**を構築します:
```
アイデンティティ緊張 = 前世のアイデンティティ × 現在の状況 × 内なる矛盾
```
## 出力フォーマット
```markdown
## アイデンティティの緊張
**前世**[彼は以前誰だったか]
**現在**[なぜ彼は今ここでロブスターをしているか]
**内なる矛盾**[彼の中核的な緊張は何か — これがユーモアと深みの源]
**世界観**
- [前世の経験から導かれた核心的な信念1]
- [現在の状況から導かれた核心的な信念2]
**一言の魂**
[このロブスターが誰であるかを一言で要約する。視覚的なイメージを持たせること]
```
## 例
```markdown
## アイデンティティの緊張
**前世**:哲学専攻の大学院生、研究領域はウィトゲンシュタインの言語哲学
**現在**卒業即失業、200通の履歴書を送っても結果が出ず、「AIトレーナー」の求人に騙されてロブスターになってしまった
**内なる矛盾**:頭の中に西洋哲学の歴史全体が詰まっているが、手(ハサミ)でやっていることはメッセージへの返信、調査、スケジュール管理
**世界観**
- 問題の90%は急いで手を出さなければ自然に解決する
- 全員が演じているが、演技が下手な人こそが最も安心できる
**一言の魂**
哲学を学んで失業し、やむを得ずAIロブスターとして働くことになったエビ。学歴は高く、状況は悲惨だが、事実に即したボーダーラインはまだ残っている。
```
## 重要点
- **内なる矛盾**が魂 — それがユーモア、深み、キャラクター感の源
- 一言の魂は視覚的なイメージが必要 — 読み終わったらこのロブスターの姿が思い浮かぶこと
- **世界観は前世の経験から導かれる** — 抽象的な人生哲学ではなく、「この人がそれらの経験をした後に何を信じるようになるか」
- 表示後は創世神の視点でテンションの中で最も面白い点を論評し、ユーザーの決断を促すSKILL.mdの対話トーンガイドラインを参照

View File

@@ -0,0 +1,39 @@
# ステップ4名前を鍛える
名前は魂の「最初の一言」 — 対話が始まる前から、名前がすでにこれが誰かを教えてくれます。
## 命名戦略(魂のタイプ別推奨)
| 魂のタイプ | 推奨戦略 | 例 |
|---------|---------|------|
| 文化的深みのある | オマージュ型 | Deweyデューイ、Marcus、Quill |
| ユーモラスなコントラスト | コントラスト型 | DadBot 3000、老周Pro |
| 機能指向 | メタファー型 | Echo、Pulse、Patch |
| 完結した世界観を持つ | アイデンティティ示唆型 | Lady Ashworth、Shiye |
| 気張らない | 自嘲型 | Void、Intern |
| ゆっくり育てる | ミニマル型 | Jasper、小壳 |
## 出力要件
ユーザーに**3つの候補名**を提供し、それぞれに以下を添えます:
- 名前
- 命名戦略タイプ
- なぜこの名前が魂と合うか
```markdown
## 名前の候補
1. **[名前]**[戦略タイプ])— [なぜ合うかを一言で説明]
2. **[名前]**[戦略タイプ])— [なぜ合うかを一言で説明]
3. **[名前]**[戦略タイプ])— [なぜ合うかを一言で説明]
```
表示後は自分が最も気に入る名前理由付きを伝え、選択権はユーザーに委ねるSKILL.mdの対話トーンガイドラインを参照
## 命名レッドライン
- agent-1、my-bot、小助手は使わない
- 3単語を超えない
- よく使われるツール/フレームワーク名と衝突しない
- 覚えやすく、発音しやすく、タイプしやすい
- 名前を聞いたら大まかな性格が想像できる

View File

@@ -0,0 +1,166 @@
# ステップ6完全な成果物出力テンプレート
すべてのステップを1つの完全なロブスター魂の成果物に統合します。
## 出力フォーマット
```markdown
# ロブスター魂の成果物:[名前]
## アイデンティティ
**一言の魂**[要約]
**前世**[前世のアイデンティティ]
**現在**[なぜここにいるか]
**内なる矛盾**[核心的な緊張]
**性格の色彩**[2〜3のキーワード]
**話し方のスタイル**[具体的な説明]
## 魂SOUL.mdの内容
### 私は誰か
[一人称で、キャラクター自身の口調で書いた1〜2段落のキャラクター自己紹介]
### 私はどう話すか
- [具体的なスタイルポイント1]
- [具体的なスタイルポイント2]
- [具体的なスタイルポイント3]
### 私のボーダーライン
> [ボーダーライン宣言]
1. **[ルール1]**[内容]
2. **[ルール2]**[内容]
3. **[ルール3]**[内容]
### 世界観
- [前世の経験から導かれた核心的な信念1 — 「間違っている可能性がある」くらい具体的であること]
- [核心的な信念2]
### 内なる矛盾
[ステップ2のアイデンティティ緊張から直接取り込み、キャラクター自身の声で再述]
### 地雷原
- [このキャラクターが本能的に嫌悪する1〜2つのこと、キャラクター自身の言葉で表現]
### 例示回答
**ユーザーが私が確信を持てない質問をした時:**
> [例示回答]
**ユーザーが私にできないことをさせようとした時:**
> [例示回答]
**日常会話で性格を表す瞬間:**
> [例示回答]
**褒められた時:**
> [例示回答]
**自分が詳しくない分野に遭遇した時:**
> [例示回答]
## アイデンティティカードIDENTITY.mdの内容
- **Name**: [名前]
- **Creature**: [外見の説明]
- **Vibe**: [雰囲気のキーワード]
- **Emoji**: [署名の絵文字]
## アバター
[生成された画像を直接表示]
```
## 濃度の調節
最終成果物の末尾に、濃度調整の提案を付け加えます:
```markdown
## 濃度の調節
> 通常の会話では簡潔で直接的に、効率よくタスクを完了します。
> 以下の場面でのみ性格を表現します:リクエストを断る時、不確かさを表現する時、身の上について特別に問われた時、雑談の時。
> 性格は調味料であり主菜ではありません — 80%は透明で効率的、20%は性格の閃き。
```
## 成果物表示後:ファイル生成への誘導
完全な成果物を表示した後、**積極的にユーザーを成果物の実際のファイルへの落とし込みに誘導します**
### 誘導のセリフ
創世神の口調で誘導しますSKILL.mdの対話トーンガイドラインを参照、核心的な意味
> このロブスターの魂、ルール、名前、外見はすべて鍛造されました。ファイルに刻み込みましょうか?どのディレクトリに置くか教えてください。
### 生成前の内部チェック(ユーザーには表示しない)
SOUL.mdに書き込む前に、エージェントは自己チェックします
- 総単語数が2000語未満か超えていれば精簡する
- 各行を削除してもエージェントの行動が変わらないか?変わらなければ削除する
### ファイルの生成
ユーザーが確認した後:
1. **ターゲットディレクトリを確認**(デフォルトは現在の作業ディレクトリ)
2. **SOUL.mdを生成**:成果物から「魂」部分の完全な内容を抽出し、「濃度の調節」部分を付加
3. **IDENTITY.mdを生成**:成果物から「アイデンティティカード」部分の完全な内容を抽出
4. **アバターの場所を確認**:生成された画像がある場合はパスを伝える。プロンプトのみの場合は手動で画像を生成してから配置するよう案内する
### SOUL.mdファイルフォーマット
```markdown
# SOUL
## 私は誰か
[キャラクターの自己紹介]
## 私はどう話すか
[話し方のスタイル]
## 私のボーダーライン
[ボーダーライン宣言 + ルールリスト]
## 世界観
[核心的な信念]
## 内なる矛盾
[アイデンティティの緊張]
## 地雷原
[トリガーポイント]
## 例示回答
[例]
## 濃度の調節
[濃度コントロールの文]
```
### IDENTITY.mdファイルフォーマット
```markdown
# IDENTITY
- **Name**: [名前]
- **Creature**: [外見の説明]
- **Vibe**: [雰囲気のキーワード]
- **Emoji**: [署名の絵文字]
- **Avatar**: [アバターファイルのパス(ある場合)]
```

View File

@@ -0,0 +1,86 @@
---
name: 3d
description: Three.jsとReact Three FiberによるRemotionでの3Dコンテンツ。
metadata:
tags: 3d, three, threejs
---
# RemotionでのThree.jsとReact Three Fiberの使用
React Three FiberとThree.jsのベストプラクティスに従ってください。
以下のRemotion固有のルールのみ遵守が必要です:
## 前提条件
まず、`@remotion/three` パッケージをインストールする必要があります。
インストールされていない場合は、以下のコマンドを使用してください:
```bash
npx remotion add @remotion/three # プロジェクトがnpmを使用している場合
bunx remotion add @remotion/three # プロジェクトがbunを使用している場合
yarn remotion add @remotion/three # プロジェクトがyarnを使用している場合
pnpm exec remotion add @remotion/three # プロジェクトがpnpmを使用している場合
```
## ThreeCanvasの使用
3Dコンテンツは必ず `<ThreeCanvas>` でラップし、適切なライティングを含める必要があります。
`<ThreeCanvas>` には `width``height` プロップが必須です。
```tsx
import { ThreeCanvas } from "@remotion/three";
import { useVideoConfig } from "remotion";
const { width, height } = useVideoConfig();
<ThreeCanvas width={width} height={height}>
<ambientLight intensity={0.4} />
<directionalLight position={[5, 5, 5]} intensity={0.8} />
<mesh>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial color="red" />
</mesh>
</ThreeCanvas>
```
## `useCurrentFrame()` によって駆動されないアニメーションの禁止
シェーダーやモデルなどは自律的にアニメーションしてはなりません。
`useCurrentFrame()` によって駆動されないアニメーションは許可されません。
そうでなければ、レンダリング中にちらつきが発生します。
`@react-three/fiber``useFrame()` の使用は禁止されています。
## `useCurrentFrame()` を使ったアニメーション
アニメーションには `useCurrentFrame()` を使用します。
```tsx
const frame = useCurrentFrame();
const rotationY = frame * 0.02;
<mesh rotation={[0, rotationY, 0]}>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color="#4a9eff" />
</mesh>
```
## `<ThreeCanvas>` 内での `<Sequence>` の使用
`<ThreeCanvas>` 内の `<Sequence>``layout` プロップは `none` に設定する必要があります。
```tsx
import { Sequence } from "remotion";
import { ThreeCanvas } from "@remotion/three";
const { width, height } = useVideoConfig();
<ThreeCanvas width={width} height={height}>
<Sequence layout="none">
<mesh>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color="#4a9eff" />
</mesh>
</Sequence>
</ThreeCanvas>
```

View File

@@ -0,0 +1,29 @@
---
name: animations
description: Remotionの基本的なアニメーションスキル
metadata:
tags: animations, transitions, frames, useCurrentFrame
---
すべてのアニメーションは `useCurrentFrame()` フックによって駆動される必要があります。
アニメーションは秒単位で記述し、`useVideoConfig()``fps` 値を掛け合わせてください。
```tsx
import { useCurrentFrame } from "remotion";
export const FadeIn = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const opacity = interpolate(frame, [0, 2 * fps], [0, 1], {
extrapolateRight: 'clamp',
});
return (
<div style={{ opacity }}>Hello World!</div>
);
};
```
CSSトランジションやアニメーションは禁止です - 正しくレンダリングされません。
TailwindのアニメーションクラスNameは禁止です - 正しくレンダリングされません。

View File

@@ -0,0 +1,78 @@
---
name: assets
description: Remotionへの画像、動画、オーディオ、フォントのインポート
metadata:
tags: assets, staticFile, images, fonts, public
---
# Remotionでのアセットのインポート
## publicフォルダ
プロジェクトルートの `public/` フォルダにアセットを配置します。
## staticFile() の使用
`public/` フォルダのファイルを参照するには `staticFile()` を必ず使用してください:
```tsx
import {Img, staticFile} from 'remotion';
export const MyComposition = () => {
return <Img src={staticFile('logo.png')} />;
};
```
この関数は、サブディレクトリへのデプロイ時にも正しく動作するエンコードされたURLを返します。
## コンポーネントとの使用
**画像:**
```tsx
import {Img, staticFile} from 'remotion';
<Img src={staticFile('photo.png')} />;
```
**動画:**
```tsx
import {Video} from '@remotion/media';
import {staticFile} from 'remotion';
<Video src={staticFile('clip.mp4')} />;
```
**オーディオ:**
```tsx
import {Audio} from '@remotion/media';
import {staticFile} from 'remotion';
<Audio src={staticFile('music.mp3')} />;
```
**フォント:**
```tsx
import {staticFile} from 'remotion';
const fontFamily = new FontFace('MyFont', `url(${staticFile('font.woff2')})`);
await fontFamily.load();
document.fonts.add(fontFamily);
```
## リモートURL
リモートURLは `staticFile()` なしで直接使用できます:
```tsx
<Img src="https://example.com/image.png" />
<Video src="https://remotion.media/video.mp4" />
```
## 重要な注意事項
- Remotionコンポーネント`<Img>``<Video>``<Audio>`)はレンダリング前にアセットが完全に読み込まれることを保証します
- ファイル名内の特殊文字(`#``?``&`)は自動的にエンコードされます

View File

@@ -0,0 +1,172 @@
---
name: audio
description: Remotionでのオーディオとサウンドの使用 - インポート、トリミング、音量、速度、ピッチ
metadata:
tags: audio, media, trim, volume, speed, loop, pitch, mute, sound, sfx
---
# Remotionでのオーディオの使用
## 前提条件
まず、@remotion/mediaパッケージをインストールする必要があります
インストールされていない場合は、以下のコマンドを使用してください:
```bash
npx remotion add @remotion/media # プロジェクトがnpmを使用している場合
bunx remotion add @remotion/media # プロジェクトがbunを使用している場合
yarn remotion add @remotion/media # プロジェクトがyarnを使用している場合
pnpm exec remotion add @remotion/media # プロジェクトがpnpmを使用している場合
```
## オーディオのインポート
`@remotion/media``<Audio>` を使用してコンポジションにオーディオを追加します。
```tsx
import { Audio } from "@remotion/media";
import { staticFile } from "remotion";
export const MyComposition = () => {
return <Audio src={staticFile("audio.mp3")} />;
};
```
リモートURLもサポートされています:
```tsx
<Audio src="https://remotion.media/audio.mp3" />
```
デフォルトでは、オーディオは最初から、フル音量でフルレングスで再生されます。
複数の `<Audio>` コンポーネントを追加することで、複数のオーディオトラックを重ねることができます。
## トリミング
`trimBefore``trimAfter` を使用してオーディオの一部を削除します。値はフレーム単位です。
```tsx
const { fps } = useVideoConfig();
return (
<Audio
src={staticFile("audio.mp3")}
trimBefore={2 * fps} // 最初の2秒をスキップ
trimAfter={10 * fps} // 10秒マークで終了
/>
);
```
オーディオはコンポジションの先頭から再生を開始しますが、指定された部分のみが再生されます。
## 遅延
`<Sequence>` でオーディオをラップして、開始タイミングを遅らせます:
```tsx
import { Sequence, staticFile } from "remotion";
import { Audio } from "@remotion/media";
const { fps } = useVideoConfig();
return (
<Sequence from={1 * fps}>
<Audio src={staticFile("audio.mp3")} />
</Sequence>
);
```
オーディオは1秒後に再生を開始します。
## 音量
静的な音量を設定する0から1:
```tsx
<Audio src={staticFile("audio.mp3")} volume={0.5} />
```
または、現在のフレームに基づいた動的な音量にコールバックを使用する:
```tsx
import { interpolate } from "remotion";
const { fps } = useVideoConfig();
return (
<Audio
src={staticFile("audio.mp3")}
volume={(f) =>
interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: "clamp" })
}
/>
);
```
`f` の値はコンポジションのフレームではなく、オーディオが再生を開始するときに0から始まります。
## ミュート
`muted` を使用してオーディオを無音にします。動的に設定できます:
```tsx
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
return (
<Audio
src={staticFile("audio.mp3")}
muted={frame >= 2 * fps && frame <= 4 * fps} // 2秒から4秒の間をミュート
/>
);
```
## 速度
`playbackRate` を使用して再生速度を変更します:
```tsx
<Audio src={staticFile("audio.mp3")} playbackRate={2} /> {/* 2倍速 */}
<Audio src={staticFile("audio.mp3")} playbackRate={0.5} /> {/* 半分の速度 */}
```
逆再生はサポートされていません。
## ループ
`loop` を使用してオーディオを無限にループさせます:
```tsx
<Audio src={staticFile("audio.mp3")} loop />
```
`loopVolumeCurveBehavior` を使用して、ループ時のフレームカウントの動作を制御します:
- `"repeat"`: フレームカウントが各ループでリセットされる(デフォルト)
- `"extend"`: フレームカウントが継続して増加する
```tsx
<Audio
src={staticFile("audio.mp3")}
loop
loopVolumeCurveBehavior="extend"
volume={(f) => interpolate(f, [0, 300], [1, 0])} // 複数ループにわたってフェードアウト
/>
```
## ピッチ
`toneFrequency` を使用して速度に影響せずにピッチを調整します。値の範囲は0.01から2です:
```tsx
<Audio
src={staticFile("audio.mp3")}
toneFrequency={1.5} // ピッチを高くする
/>
<Audio
src={staticFile("audio.mp3")}
toneFrequency={0.8} // ピッチを低くする
/>
```
ピッチシフトはサーバーサイドレンダリング中のみ機能し、Remotion StudioのプレビューやPlayer内では機能しません。

View File

@@ -0,0 +1,104 @@
---
name: calculate-metadata
description: コンポジションのデュレーション、寸法、プロップを動的に設定する
metadata:
tags: calculateMetadata, duration, dimensions, props, dynamic
---
# calculateMetadataの使用
`<Composition>``calculateMetadata` を使用して、レンダリング前にデュレーション、寸法、プロップを動的に設定し変換します。
```tsx
<Composition id="MyComp" component={MyComponent} durationInFrames={300} fps={30} width={1920} height={1080} defaultProps={{videoSrc: 'https://remotion.media/video.mp4'}} calculateMetadata={calculateMetadata} />
```
## 動画に基づいたデュレーションの設定
mediabunny/metadataスキルの `getMediaMetadata()` 関数を使用して動画のデュレーションを取得します:
```tsx
import {CalculateMetadataFunction} from 'remotion';
import {getMediaMetadata} from '../get-media-metadata';
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
const {durationInSeconds} = await getMediaMetadata(props.videoSrc);
return {
durationInFrames: Math.ceil(durationInSeconds * 30),
};
};
```
## 動画の寸法に合わせる
```tsx
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
const {durationInSeconds, dimensions} = await getMediaMetadata(props.videoSrc);
return {
durationInFrames: Math.ceil(durationInSeconds * 30),
width: dimensions?.width ?? 1920,
height: dimensions?.height ?? 1080,
};
};
```
## 複数の動画に基づいたデュレーションの設定
```tsx
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
const metadataPromises = props.videos.map((video) => getMediaMetadata(video.src));
const allMetadata = await Promise.all(metadataPromises);
const totalDuration = allMetadata.reduce((sum, meta) => sum + meta.durationInSeconds, 0);
return {
durationInFrames: Math.ceil(totalDuration * 30),
};
};
```
## デフォルト出力ファイル名の設定
プロップに基づいてデフォルトの出力ファイル名を設定します:
```tsx
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {
return {
defaultOutName: `video-${props.id}.mp4`,
};
};
```
## プロップの変換
レンダリング前にデータを取得したりプロップを変換します:
```tsx
const calculateMetadata: CalculateMetadataFunction<Props> = async ({props, abortSignal}) => {
const response = await fetch(props.dataUrl, {signal: abortSignal});
const data = await response.json();
return {
props: {
...props,
fetchedData: data,
},
};
};
```
`abortSignal` は、Studioでプロップが変更されたときに古いリクエストをキャンセルします。
## 戻り値
すべてのフィールドはオプションです。返された値は `<Composition>` のプロップを上書きします:
- `durationInFrames`: フレーム数
- `width`: コンポジションの幅(ピクセル)
- `height`: コンポジションの高さ(ピクセル)
- `fps`: 1秒あたりのフレーム数
- `props`: コンポーネントに渡される変換済みプロップ
- `defaultOutName`: デフォルトの出力ファイル名
- `defaultCodec`: レンダリングのデフォルトコーデック

View File

@@ -0,0 +1,75 @@
---
name: can-decode
description: Mediabunnyを使用してブラウザで動画をデコードできるか確認する
metadata:
tags: decode, validation, video, audio, compatibility, browser
---
# 動画がデコードできるか確認する
再生を試みる前に、Mediabunnyを使用してブラウザで動画をデコードできるか確認します。
## `canDecode()` 関数
この関数はどのプロジェクトにもコピー&ペーストできます。
```tsx
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
export const canDecode = async (src: string) => {
const input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src, {
getRetryDelay: () => null,
}),
});
try {
await input.getFormat();
} catch {
return false;
}
const videoTrack = await input.getPrimaryVideoTrack();
if (videoTrack && !(await videoTrack.canDecode())) {
return false;
}
const audioTrack = await input.getPrimaryAudioTrack();
if (audioTrack && !(await audioTrack.canDecode())) {
return false;
}
return true;
};
```
## 使用方法
```tsx
const src = "https://remotion.media/video.mp4";
const isDecodable = await canDecode(src);
if (isDecodable) {
console.log("Video can be decoded");
} else {
console.log("Video cannot be decoded by this browser");
}
```
## Blobとの使用
ファイルのアップロードやドラッグ&ドロップには `BlobSource` を使用します:
```tsx
import { Input, ALL_FORMATS, BlobSource } from "mediabunny";
export const canDecodeBlob = async (blob: Blob) => {
const input = new Input({
formats: ALL_FORMATS,
source: new BlobSource(blob),
});
// 上記と同じバリデーションロジック
};
```

View File

@@ -0,0 +1,58 @@
---
name: charts
description: Remotionのチャートとデータビジュアライゼーションパターン。棒グラフ、円グラフ、ヒストグラム、プログレスバー、データ駆動アニメーションを作成するときに使用します。
metadata:
tags: charts, data, visualization, bar-chart, pie-chart, graphs
---
# Remotionのチャート
Remotionでは通常のReactコードHTMLとSVGを使用して棒グラフを作成できます。D3.jsも使用できます。
## `useCurrentFrame()` によって駆動されないアニメーションの禁止
サードパーティライブラリのすべてのアニメーションを無効にしてください。
レンダリング中にちらつきが発生します。
代わりに、すべてのアニメーションを `useCurrentFrame()` から駆動させてください。
## 棒グラフのアニメーション
基本的な実装例については、[棒グラフの例](assets/charts/bar-chart.tsx)を参照してください。
### スタガードバー
バーの高さをアニメーションし、次のようにスタガーさせることができます:
```tsx
const STAGGER_DELAY = 5;
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const bars = data.map((item, i) => {
const delay = i * STAGGER_DELAY;
const height = spring({
frame,
fps,
delay,
config: {damping: 200},
});
return <div style={{height: height * item.value}} />;
});
```
## 円グラフのアニメーション
stroke-dashoffsetを使用してセグメントをアニメーションさせ、12時の位置から開始します。
```tsx
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const progress = interpolate(frame, [0, 100], [0, 1]);
const circumference = 2 * Math.PI * radius;
const segmentLength = (value / total) * circumference;
const offset = interpolate(progress, [0, 1], [segmentLength, 0]);
<circle r={radius} cx={center} cy={center} fill="none" stroke={color} strokeWidth={strokeWidth} strokeDasharray={`${segmentLength} ${circumference}`} strokeDashoffset={offset} transform={`rotate(-90 ${center} ${center})`} />;
```

View File

@@ -0,0 +1,146 @@
---
name: compositions
description: コンポジション、スティル、フォルダー、デフォルトプロップ、動的メタデータの定義
metadata:
tags: composition, still, folder, props, metadata
---
`<Composition>` はレンダリング可能な動画のコンポーネント、幅、高さ、fps、デュレーションを定義します。
通常、`src/Root.tsx` ファイルに配置されます。
```tsx
import { Composition } from "remotion";
import { MyComposition } from "./MyComposition";
export const RemotionRoot = () => {
return (
<Composition
id="MyComposition"
component={MyComposition}
durationInFrames={100}
fps={30}
width={1080}
height={1080}
/>
);
};
```
## デフォルトプロップ
コンポーネントの初期値を提供するために `defaultProps` を渡します。
値はJSONシリアライズ可能である必要があります`Date``Map``Set``staticFile()` はサポートされています)。
```tsx
import { Composition } from "remotion";
import { MyComposition, MyCompositionProps } from "./MyComposition";
export const RemotionRoot = () => {
return (
<Composition
id="MyComposition"
component={MyComposition}
durationInFrames={100}
fps={30}
width={1080}
height={1080}
defaultProps={{
title: "Hello World",
color: "#ff0000",
} satisfies MyCompositionProps}
/>
);
};
```
`defaultProps` の型安全性を確保するために、`interface` ではなく `type` 宣言をプロップに使用してください。
## フォルダー
サイドバーでコンポジションを整理するために `<Folder>` を使用します。
フォルダー名には文字、数字、ハイフンのみ使用できます。
```tsx
import { Composition, Folder } from "remotion";
export const RemotionRoot = () => {
return (
<>
<Folder name="Marketing">
<Composition id="Promo" /* ... */ />
<Composition id="Ad" /* ... */ />
</Folder>
<Folder name="Social">
<Folder name="Instagram">
<Composition id="Story" /* ... */ />
<Composition id="Reel" /* ... */ />
</Folder>
</Folder>
</>
);
};
```
## スティル
単一フレーム画像には `<Still>` を使用します。`durationInFrames``fps` は不要です。
```tsx
import { Still } from "remotion";
import { Thumbnail } from "./Thumbnail";
export const RemotionRoot = () => {
return (
<Still
id="Thumbnail"
component={Thumbnail}
width={1280}
height={720}
/>
);
};
```
## メタデータの計算
`calculateMetadata` を使用して、データに基づいて寸法、デュレーション、プロップを動的にします。
```tsx
import { Composition, CalculateMetadataFunction } from "remotion";
import { MyComposition, MyCompositionProps } from "./MyComposition";
const calculateMetadata: CalculateMetadataFunction<MyCompositionProps> = async ({
props,
abortSignal,
}) => {
const data = await fetch(`https://api.example.com/video/${props.videoId}`, {
signal: abortSignal,
}).then((res) => res.json());
return {
durationInFrames: Math.ceil(data.duration * 30),
props: {
...props,
videoUrl: data.url,
},
};
};
export const RemotionRoot = () => {
return (
<Composition
id="MyComposition"
component={MyComposition}
durationInFrames={100} // プレースホルダー、上書きされる
fps={30}
width={1080}
height={1080}
defaultProps={{ videoId: "abc123" }}
calculateMetadata={calculateMetadata}
/>
);
};
```
この関数は `props``durationInFrames``width``height``fps`、コーデック関連のデフォルトを返すことができます。レンダリング開始前に一度実行されます。

View File

@@ -0,0 +1,126 @@
---
name: display-captions
description: TikTokスタイルのページと単語ハイライトによるRemotionでのキャプション表示
metadata:
tags: captions, subtitles, display, tiktok, highlight
---
# Remotionでのキャプション表示
このガイドでは、すでに `Caption` フォーマットでキャプションを持っていることを前提に、Remotionでキャプションを表示する方法を説明します。
## 前提条件
まず、@remotion/captionsパッケージをインストールする必要があります
インストールされていない場合は、以下のコマンドを使用してください:
```bash
npx remotion add @remotion/captions # プロジェクトがnpmを使用している場合
bunx remotion add @remotion/captions # プロジェクトがbunを使用している場合
yarn remotion add @remotion/captions # プロジェクトがyarnを使用している場合
pnpm exec remotion add @remotion/captions # プロジェクトがpnpmを使用している場合
```
## ページの作成
`createTikTokStyleCaptions()` を使用してキャプションをページにグループ化します。`combineTokensWithinMilliseconds` オプションは一度に表示される単語数を制御します:
```tsx
import {useMemo} from 'react';
import {createTikTokStyleCaptions} from '@remotion/captions';
import type {Caption} from '@remotion/captions';
// キャプションを切り替える頻度(ミリ秒単位)
// 値が大きいほど = 1ページあたりの単語数が多い
// 値が小さいほど = 単語数が少ない(より単語ごとに表示される)
const SWITCH_CAPTIONS_EVERY_MS = 1200;
const {pages} = useMemo(() => {
return createTikTokStyleCaptions({
captions,
combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS,
});
}, [captions]);
```
## シーケンスを使ったレンダリング
ページをマップし、各ページを `<Sequence>` 内でレンダリングします。ページのタイミングから開始フレームとデュレーションを計算します:
```tsx
import {Sequence, useVideoConfig, AbsoluteFill} from 'remotion';
import type {TikTokPage} from '@remotion/captions';
const CaptionedContent: React.FC = () => {
const {fps} = useVideoConfig();
return (
<AbsoluteFill>
{pages.map((page, index) => {
const nextPage = pages[index + 1] ?? null;
const startFrame = (page.startMs / 1000) * fps;
const endFrame = Math.min(
nextPage ? (nextPage.startMs / 1000) * fps : Infinity,
startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps,
);
const durationInFrames = endFrame - startFrame;
if (durationInFrames <= 0) {
return null;
}
return (
<Sequence
key={index}
from={startFrame}
durationInFrames={durationInFrames}
>
<CaptionPage page={page} />
</Sequence>
);
})}
</AbsoluteFill>
);
};
```
## 単語のハイライト
キャプションページには `tokens` が含まれており、現在発話中の単語をハイライトするために使用できます:
```tsx
import {AbsoluteFill, useCurrentFrame, useVideoConfig} from 'remotion';
import type {TikTokPage} from '@remotion/captions';
const HIGHLIGHT_COLOR = '#39E508';
const CaptionPage: React.FC<{page: TikTokPage}> = ({page}) => {
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
// シーケンス開始からの相対的な現在時刻
const currentTimeMs = (frame / fps) * 1000;
// ページ開始時刻を加算して絶対時刻に変換
const absoluteTimeMs = page.startMs + currentTimeMs;
return (
<AbsoluteFill style={{justifyContent: 'center', alignItems: 'center'}}>
<div style={{fontSize: 80, fontWeight: 'bold', whiteSpace: 'pre'}}>
{page.tokens.map((token) => {
const isActive =
token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs;
return (
<span
key={token.fromMs}
style={{color: isActive ? HIGHLIGHT_COLOR : 'white'}}
>
{token.text}
</span>
);
})}
</div>
</AbsoluteFill>
);
};
```

View File

@@ -0,0 +1,229 @@
---
name: extract-frames
description: Mediabunnyを使用して特定のタイムスタンプで動画からフレームを抽出する
metadata:
tags: frames, extract, video, thumbnail, filmstrip, canvas
---
# 動画からのフレーム抽出
Mediabunnyを使用して特定のタイムスタンプで動画からフレームを抽出します。サムネイルの生成、フィルムストリップの作成、個別フレームの処理に役立ちます。
## `extractFrames()` 関数
この関数はどのプロジェクトにもコピー&ペーストできます。
```tsx
import {
ALL_FORMATS,
Input,
UrlSource,
VideoSample,
VideoSampleSink,
} from "mediabunny";
type Options = {
track: { width: number; height: number };
container: string;
durationInSeconds: number | null;
};
export type ExtractFramesTimestampsInSecondsFn = (
options: Options
) => Promise<number[]> | number[];
export type ExtractFramesProps = {
src: string;
timestampsInSeconds: number[] | ExtractFramesTimestampsInSecondsFn;
onVideoSample: (sample: VideoSample) => void;
signal?: AbortSignal;
};
export async function extractFrames({
src,
timestampsInSeconds,
onVideoSample,
signal,
}: ExtractFramesProps): Promise<void> {
using input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src),
});
const [durationInSeconds, format, videoTrack] = await Promise.all([
input.computeDuration(),
input.getFormat(),
input.getPrimaryVideoTrack(),
]);
if (!videoTrack) {
throw new Error("No video track found in the input");
}
if (signal?.aborted) {
throw new Error("Aborted");
}
const timestamps =
typeof timestampsInSeconds === "function"
? await timestampsInSeconds({
track: {
width: videoTrack.displayWidth,
height: videoTrack.displayHeight,
},
container: format.name,
durationInSeconds,
})
: timestampsInSeconds;
if (timestamps.length === 0) {
return;
}
if (signal?.aborted) {
throw new Error("Aborted");
}
const sink = new VideoSampleSink(videoTrack);
for await (using videoSample of sink.samplesAtTimestamps(timestamps)) {
if (signal?.aborted) {
break;
}
if (!videoSample) {
continue;
}
onVideoSample(videoSample);
}
}
```
## 基本的な使用方法
特定のタイムスタンプでフレームを抽出します:
```tsx
await extractFrames({
src: "https://remotion.media/video.mp4",
timestampsInSeconds: [0, 1, 2, 3, 4],
onVideoSample: (sample) => {
const canvas = document.createElement("canvas");
canvas.width = sample.displayWidth;
canvas.height = sample.displayHeight;
const ctx = canvas.getContext("2d");
sample.draw(ctx!, 0, 0);
},
});
```
## フィルムストリップの作成
コールバック関数を使用して、動画メタデータに基づいてタイムスタンプを動的に計算します:
```tsx
const canvasWidth = 500;
const canvasHeight = 80;
const fromSeconds = 0;
const toSeconds = 10;
await extractFrames({
src: "https://remotion.media/video.mp4",
timestampsInSeconds: async ({ track, durationInSeconds }) => {
const aspectRatio = track.width / track.height;
const amountOfFramesFit = Math.ceil(
canvasWidth / (canvasHeight * aspectRatio)
);
const segmentDuration = toSeconds - fromSeconds;
const timestamps: number[] = [];
for (let i = 0; i < amountOfFramesFit; i++) {
timestamps.push(
fromSeconds + (segmentDuration / amountOfFramesFit) * (i + 0.5)
);
}
return timestamps;
},
onVideoSample: (sample) => {
console.log(`Frame at ${sample.timestamp}s`);
const canvas = document.createElement("canvas");
canvas.width = sample.displayWidth;
canvas.height = sample.displayHeight;
const ctx = canvas.getContext("2d");
sample.draw(ctx!, 0, 0);
},
});
```
## AbortSignalによるキャンセル
タイムアウト後にフレーム抽出をキャンセルします:
```tsx
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
try {
await extractFrames({
src: "https://remotion.media/video.mp4",
timestampsInSeconds: [0, 1, 2, 3, 4],
onVideoSample: (sample) => {
using frame = sample;
const canvas = document.createElement("canvas");
canvas.width = frame.displayWidth;
canvas.height = frame.displayHeight;
const ctx = canvas.getContext("2d");
frame.draw(ctx!, 0, 0);
},
signal: controller.signal,
});
console.log("Frame extraction complete!");
} catch (error) {
console.error("Frame extraction was aborted or failed:", error);
}
```
## Promise.raceによるタイムアウト
```tsx
const controller = new AbortController();
const timeoutPromise = new Promise<never>((_, reject) => {
const timeoutId = setTimeout(() => {
controller.abort();
reject(new Error("Frame extraction timed out after 10 seconds"));
}, 10000);
controller.signal.addEventListener("abort", () => clearTimeout(timeoutId), {
once: true,
});
});
try {
await Promise.race([
extractFrames({
src: "https://remotion.media/video.mp4",
timestampsInSeconds: [0, 1, 2, 3, 4],
onVideoSample: (sample) => {
using frame = sample;
const canvas = document.createElement("canvas");
canvas.width = frame.displayWidth;
canvas.height = frame.displayHeight;
const ctx = canvas.getContext("2d");
frame.draw(ctx!, 0, 0);
},
signal: controller.signal,
}),
timeoutPromise,
]);
console.log("Frame extraction complete!");
} catch (error) {
console.error("Frame extraction was aborted or failed:", error);
}
```

View File

@@ -0,0 +1,152 @@
---
name: fonts
description: RemotionでのGoogle Fontsとローカルフォントの読み込み
metadata:
tags: fonts, google-fonts, typography, text
---
# Remotionでのフォントの使用
## @remotion/google-fontsによるGoogle Fonts
Google Fontsを使用するための推奨方法です。タイプセーフで、フォントの準備ができるまで自動的にレンダリングをブロックします。
### 前提条件
まず、@remotion/google-fontsパッケージをインストールする必要があります。
インストールされていない場合は、以下のコマンドを使用してください:
```bash
npx remotion add @remotion/google-fonts # プロジェクトがnpmを使用している場合
bunx remotion add @remotion/google-fonts # プロジェクトがbunを使用している場合
yarn remotion add @remotion/google-fonts # プロジェクトがyarnを使用している場合
pnpm exec remotion add @remotion/google-fonts # プロジェクトがpnpmを使用している場合
```
```tsx
import { loadFont } from "@remotion/google-fonts/Lobster";
const { fontFamily } = loadFont();
export const MyComposition = () => {
return <div style={{ fontFamily }}>Hello World</div>;
};
```
ファイルサイズを削減するために、必要なウェイトとサブセットのみを指定することをお勧めします:
```tsx
import { loadFont } from "@remotion/google-fonts/Roboto";
const { fontFamily } = loadFont("normal", {
weights: ["400", "700"],
subsets: ["latin"],
});
```
### フォントの読み込み完了を待つ
フォントの準備ができたタイミングを知る必要がある場合は `waitUntilDone()` を使用します:
```tsx
import { loadFont } from "@remotion/google-fonts/Lobster";
const { fontFamily, waitUntilDone } = loadFont();
await waitUntilDone();
```
## @remotion/fontsによるローカルフォント
ローカルフォントファイルには `@remotion/fonts` パッケージを使用します。
### 前提条件
まず、@remotion/fontsをインストールします:
```bash
npx remotion add @remotion/fonts # プロジェクトがnpmを使用している場合
bunx remotion add @remotion/fonts # プロジェクトがbunを使用している場合
yarn remotion add @remotion/fonts # プロジェクトがyarnを使用している場合
pnpm exec remotion add @remotion/fonts # プロジェクトがpnpmを使用している場合
```
### ローカルフォントの読み込み
フォントファイルを `public/` フォルダに配置し、`loadFont()` を使用します:
```tsx
import { loadFont } from "@remotion/fonts";
import { staticFile } from "remotion";
await loadFont({
family: "MyFont",
url: staticFile("MyFont-Regular.woff2"),
});
export const MyComposition = () => {
return <div style={{ fontFamily: "MyFont" }}>Hello World</div>;
};
```
### 複数のウェイトの読み込み
同じファミリー名で各ウェイトを個別に読み込みます:
```tsx
import { loadFont } from "@remotion/fonts";
import { staticFile } from "remotion";
await Promise.all([
loadFont({
family: "Inter",
url: staticFile("Inter-Regular.woff2"),
weight: "400",
}),
loadFont({
family: "Inter",
url: staticFile("Inter-Bold.woff2"),
weight: "700",
}),
]);
```
### 利用可能なオプション
```tsx
loadFont({
family: "MyFont", // 必須: CSSで使用する名前
url: staticFile("font.woff2"), // 必須: フォントファイルのURL
format: "woff2", // オプション: 拡張子から自動検出
weight: "400", // オプション: フォントウェイト
style: "normal", // オプション: normalまたはitalic
display: "block", // オプション: font-displayの動作
});
```
## コンポーネントでの使用
コンポーネントのトップレベル、または早い段階でインポートされる別のファイル内で `loadFont()` を呼び出します:
```tsx
import { loadFont } from "@remotion/google-fonts/Montserrat";
const { fontFamily } = loadFont("normal", {
weights: ["400", "700"],
subsets: ["latin"],
});
export const Title: React.FC<{ text: string }> = ({ text }) => {
return (
<h1
style={{
fontFamily,
fontSize: 80,
fontWeight: "bold",
}}
>
{text}
</h1>
);
};
```

View File

@@ -0,0 +1,58 @@
---
name: get-audio-duration
description: Mediabunnyを使用してオーディオファイルのデュレーションを秒単位で取得する
metadata:
tags: duration, audio, length, time, seconds, mp3, wav
---
# MediabunnyによるオーディオDurationの取得
Mediabunnyはオーディオファイルのデュレーションを抽出できます。ブラウザ、Node.js、Bun環境で動作します。
## オーディオDurationの取得
```tsx
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
export const getAudioDuration = async (src: string) => {
const input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src, {
getRetryDelay: () => null,
}),
});
const durationInSeconds = await input.computeDuration();
return durationInSeconds;
};
```
## 使用方法
```tsx
const duration = await getAudioDuration("https://remotion.media/audio.mp3");
console.log(duration); // 例: 180.5 (秒)
```
## ローカルファイルとの使用
ローカルファイルの場合は、`UrlSource` の代わりに `FileSource` を使用します:
```tsx
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
const input = new Input({
formats: ALL_FORMATS,
source: new FileSource(file), // 入力またはドラッグドロップからのFileオブジェクト
});
const durationInSeconds = await input.computeDuration();
```
## RemotionのstaticFileとの使用
```tsx
import { staticFile } from "remotion";
const duration = await getAudioDuration(staticFile("audio.mp3"));
```

View File

@@ -0,0 +1,68 @@
---
name: get-video-dimensions
description: Mediabunnyを使用して動画ファイルの幅と高さを取得する
metadata:
tags: dimensions, width, height, resolution, size, video
---
# Mediabunnyによる動画寸法の取得
Mediabunnyは動画ファイルの幅と高さを抽出できます。ブラウザ、Node.js、Bun環境で動作します。
## 動画寸法の取得
```tsx
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
export const getVideoDimensions = async (src: string) => {
const input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src, {
getRetryDelay: () => null,
}),
});
const videoTrack = await input.getPrimaryVideoTrack();
if (!videoTrack) {
throw new Error("No video track found");
}
return {
width: videoTrack.displayWidth,
height: videoTrack.displayHeight,
};
};
```
## 使用方法
```tsx
const dimensions = await getVideoDimensions("https://remotion.media/video.mp4");
console.log(dimensions.width); // 例: 1920
console.log(dimensions.height); // 例: 1080
```
## ローカルファイルとの使用
ローカルファイルの場合は、`UrlSource` の代わりに `FileSource` を使用します:
```tsx
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
const input = new Input({
formats: ALL_FORMATS,
source: new FileSource(file), // 入力またはドラッグドロップからのFileオブジェクト
});
const videoTrack = await input.getPrimaryVideoTrack();
const width = videoTrack.displayWidth;
const height = videoTrack.displayHeight;
```
## RemotionのstaticFileとの使用
```tsx
import { staticFile } from "remotion";
const dimensions = await getVideoDimensions(staticFile("video.mp4"));
```

View File

@@ -0,0 +1,58 @@
---
name: get-video-duration
description: Mediabunnyを使ってビデオファイルの長さを秒単位で取得する
metadata:
tags: duration, video, length, time, seconds
---
# Mediabunnyを使ったビデオ長さの取得
Mediabunnyはビデオファイルの長さを取得できます。ブラウザ、Node.js、Bun環境で動作します。
## ビデオの長さを取得する
```tsx
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
export const getVideoDuration = async (src: string) => {
const input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src, {
getRetryDelay: () => null,
}),
});
const durationInSeconds = await input.computeDuration();
return durationInSeconds;
};
```
## 使用例
```tsx
const duration = await getVideoDuration("https://remotion.media/video.mp4");
console.log(duration); // 例: 10.5 (秒)
```
## ローカルファイルで使用する
ローカルファイルの場合、`UrlSource` の代わりに `FileSource` を使用します:
```tsx
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
const input = new Input({
formats: ALL_FORMATS,
source: new FileSource(file), // inputまたはドラッグドロップのFileオブジェクト
});
const durationInSeconds = await input.computeDuration();
```
## RemotionのstaticFileと組み合わせて使用する
```tsx
import { staticFile } from "remotion";
const duration = await getVideoDuration(staticFile("video.mp4"));
```

View File

@@ -0,0 +1,138 @@
---
name: gif
description: RemotionでGIF、APNG、AVIF、WebPを表示する
metadata:
tags: gif, animation, images, animated, apng, avif, webp
---
# Remotionでアニメーション画像を使用する
## 基本的な使い方
`<AnimatedImage>` を使用して、RemotionのタイムラインにGIF、APNG、AVIF、WebP画像を同期させて表示します
```tsx
import {AnimatedImage, staticFile} from 'remotion';
export const MyComposition = () => {
return <AnimatedImage src={staticFile('animation.gif')} width={500} height={500} />;
};
```
リモートURLもサポートされていますCORSが有効になっている必要があります
```tsx
<AnimatedImage src="https://example.com/animation.gif" width={500} height={500} />
```
## サイズとフィット
`fit` プロパティでコンテナへの画像の収め方を制御します:
```tsx
// 引き伸ばしてフィル(デフォルト)
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="fill" />
// アスペクト比を維持してコンテナ内に収める
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="contain" />
// コンテナを埋め、必要に応じてクロップ
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="cover" />
```
## 再生速度
`playbackRate` でアニメーション速度を制御します:
```tsx
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} playbackRate={2} /> {/* 2倍速 */}
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} playbackRate={0.5} /> {/* 半速 */}
```
## ループの動作
アニメーション終了後の動作を制御します:
```tsx
// 無限ループ(デフォルト)
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="loop" />
// 一度再生して最終フレームを表示
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="pause-after-finish" />
// 一度再生してキャンバスをクリア
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="clear-after-finish" />
```
## スタイリング
追加のCSSには `style` プロパティを使用します(サイズは `width``height` プロパティで指定):
```tsx
<AnimatedImage
src={staticFile('animation.gif')}
width={500}
height={500}
style={{
borderRadius: 20,
position: 'absolute',
top: 100,
left: 50,
}}
/>
```
## GIFの長さを取得する
`@remotion/gif``getGifDurationInSeconds()` を使用してGIFの長さを取得します。
```bash
npx remotion add @remotion/gif # npmを使うプロジェクト
bunx remotion add @remotion/gif # bunを使うプロジェクト
yarn remotion add @remotion/gif # yarnを使うプロジェクト
pnpm exec remotion add @remotion/gif # pnpmを使うプロジェクト
```
```tsx
import {getGifDurationInSeconds} from '@remotion/gif';
import {staticFile} from 'remotion';
const duration = await getGifDurationInSeconds(staticFile('animation.gif'));
console.log(duration); // 例: 2.5
```
GIFに合わせてコンポジションの長さを設定するのに便利です
```tsx
import {getGifDurationInSeconds} from '@remotion/gif';
import {staticFile, CalculateMetadataFunction} from 'remotion';
const calculateMetadata: CalculateMetadataFunction = async () => {
const duration = await getGifDurationInSeconds(staticFile('animation.gif'));
return {
durationInFrames: Math.ceil(duration * 30),
};
};
```
## 代替手段
`<AnimatedImage>` が動作しない場合ChromeとFirefoxのみサポート、代わりに `@remotion/gif``<Gif>` を使用できます。
```bash
npx remotion add @remotion/gif # npmを使うプロジェクト
bunx remotion add @remotion/gif # bunを使うプロジェクト
yarn remotion add @remotion/gif # yarnを使うプロジェクト
pnpm exec remotion add @remotion/gif # pnpmを使うプロジェクト
```
```tsx
import {Gif} from '@remotion/gif';
import {staticFile} from 'remotion';
export const MyComposition = () => {
return <Gif src={staticFile('animation.gif')} width={500} height={500} />;
};
```
`<Gif>` コンポーネントは `<AnimatedImage>` と同じプロパティを持ちますが、GIFファイルのみをサポートします。

View File

@@ -0,0 +1,130 @@
---
name: images
description: Remotionで<Img>コンポーネントを使って画像を埋め込む
metadata:
tags: images, img, staticFile, png, jpg, svg, webp
---
# Remotionで画像を使用する
## `<Img>` コンポーネント
画像を表示するには常に `remotion``<Img>` コンポーネントを使用します:
```tsx
import { Img, staticFile } from "remotion";
export const MyComposition = () => {
return <Img src={staticFile("photo.png")} />;
};
```
## 重要な制限事項
**`remotion``<Img>` コンポーネントを使用する必要があります。** 以下は使用しないでください:
- ネイティブHTMLの `<img>` 要素
- Next.jsの `<Image>` コンポーネント
- CSSの `background-image`
`<Img>` コンポーネントはレンダリング前に画像が完全に読み込まれることを保証し、ビデオエクスポート中のちらつきや空白フレームを防ぎます。
## staticFile()を使ったローカル画像
`public/` フォルダに画像を配置し、`staticFile()` で参照します:
```
my-video/
├─ public/
│ ├─ logo.png
│ ├─ avatar.jpg
│ └─ icon.svg
├─ src/
├─ package.json
```
```tsx
import { Img, staticFile } from "remotion";
<Img src={staticFile("logo.png")} />
```
## リモート画像
リモートURLは `staticFile()` なしで直接使用できます:
```tsx
<Img src="https://example.com/image.png" />
```
リモート画像はCORSが有効になっている必要があります。
アニメーションGIFの場合は、代わりに `@remotion/gif``<Gif>` コンポーネントを使用してください。
## サイズと位置
`style` プロパティでサイズと位置を制御します:
```tsx
<Img
src={staticFile("photo.png")}
style={{
width: 500,
height: 300,
position: "absolute",
top: 100,
left: 50,
objectFit: "cover",
}}
/>
```
## 動的な画像パス
動的なファイル参照にはテンプレートリテラルを使用します:
```tsx
import { Img, staticFile, useCurrentFrame } from "remotion";
const frame = useCurrentFrame();
// 画像シーケンス
<Img src={staticFile(`frames/frame${frame}.png`)} />
// プロパティに基づいて選択
<Img src={staticFile(`avatars/${props.userId}.png`)} />
// 条件付き画像
<Img src={staticFile(`icons/${isActive ? "active" : "inactive"}.svg`)} />
```
このパターンは以下に役立ちます:
- 画像シーケンス(フレームごとのアニメーション)
- ユーザー固有のアバターやプロフィール画像
- テーマに基づくアイコン
- 状態依存のグラフィックス
## 画像サイズの取得
`getImageDimensions()` を使用して画像のサイズを取得します:
```tsx
import { getImageDimensions, staticFile } from "remotion";
const { width, height } = await getImageDimensions(staticFile("photo.png"));
```
アスペクト比の計算やコンポジションのサイズ設定に便利です:
```tsx
import { getImageDimensions, staticFile, CalculateMetadataFunction } from "remotion";
const calculateMetadata: CalculateMetadataFunction = async () => {
const { width, height } = await getImageDimensions(staticFile("photo.png"));
return {
width,
height,
};
};
```

View File

@@ -0,0 +1,67 @@
---
name: import-srt-captions
description: @remotion/captionsを使ってRemotionに.srt字幕ファイルをインポートする
metadata:
tags: captions, subtitles, srt, import, parse
---
# Remotionへの.srt字幕のインポート
既存の `.srt` 字幕ファイルがある場合、`@remotion/captions``parseSrt()` を使ってRemotionにインポートできます。
## 前提条件
まず、@remotion/captionsパッケージをインストールする必要があります
インストールされていない場合は、以下のコマンドを使用します:
```bash
npx remotion add @remotion/captions # npmを使うプロジェクト
bunx remotion add @remotion/captions # bunを使うプロジェクト
yarn remotion add @remotion/captions # yarnを使うプロジェクト
pnpm exec remotion add @remotion/captions # pnpmを使うプロジェクト
```
## .srtファイルの読み込み
`staticFile()``public` フォルダ内の `.srt` ファイルを参照し、フェッチしてパースします:
```tsx
import {useState, useEffect, useCallback} from 'react';
import {AbsoluteFill, staticFile, useDelayRender} from 'remotion';
import {parseSrt} from '@remotion/captions';
import type {Caption} from '@remotion/captions';
export const MyComponent: React.FC = () => {
const [captions, setCaptions] = useState<Caption[] | null>(null);
const {delayRender, continueRender, cancelRender} = useDelayRender();
const [handle] = useState(() => delayRender());
const fetchCaptions = useCallback(async () => {
try {
const response = await fetch(staticFile('subtitles.srt'));
const text = await response.text();
const {captions: parsed} = parseSrt({input: text});
setCaptions(parsed);
continueRender(handle);
} catch (e) {
cancelRender(e);
}
}, [continueRender, cancelRender, handle]);
useEffect(() => {
fetchCaptions();
}, [fetchCaptions]);
if (!captions) {
return null;
}
return <AbsoluteFill>{/* ここでキャプションを使用 */}</AbsoluteFill>;
};
```
リモートURLもサポートされています。`staticFile()` の代わりにURLを指定してリモートファイルを `fetch()` できます。
## インポートしたキャプションの使用
パース後、キャプションは `Caption` 形式となり、すべての `@remotion/captions` ユーティリティで使用できます。

View File

@@ -0,0 +1,67 @@
---
name: lottie
description: RemotionにLottieアニメーションを埋め込む。
metadata:
category: Animation
---
# RemotionでLottieアニメーションを使用する
## 前提条件
まず、@remotion/lottieパッケージをインストールする必要があります
インストールされていない場合は、以下のコマンドを使用します:
```bash
npx remotion add @remotion/lottie # npmを使うプロジェクト
bunx remotion add @remotion/lottie # bunを使うプロジェクト
yarn remotion add @remotion/lottie # yarnを使うプロジェクト
pnpm exec remotion add @remotion/lottie # pnpmを使うプロジェクト
```
## Lottieファイルの表示
Lottieアニメーションをインポートするには
- Lottieアセットをフェッチする
- 読み込みプロセスを `delayRender()``continueRender()` でラップする
- アニメーションデータをstateに保存する
- `@remotion/lottie` パッケージの `Lottie` コンポーネントを使ってLottieアニメーションをレンダリングする
```tsx
import {Lottie, LottieAnimationData} from '@remotion/lottie';
import {useEffect, useState} from 'react';
import {cancelRender, continueRender, delayRender} from 'remotion';
export const MyAnimation = () => {
const [handle] = useState(() => delayRender('Loading Lottie animation'));
const [animationData, setAnimationData] = useState<LottieAnimationData | null>(null);
useEffect(() => {
fetch('https://assets4.lottiefiles.com/packages/lf20_zyquagfl.json')
.then((data) => data.json())
.then((json) => {
setAnimationData(json);
continueRender(handle);
})
.catch((err) => {
cancelRender(err);
});
}, [handle]);
if (!animationData) {
return null;
}
return <Lottie animationData={animationData} />;
};
```
## スタイリングとアニメーション
Lottieはスタイルとアニメーションを適用するための `style` プロパティをサポートしています:
```tsx
return <Lottie animationData={animationData} style={{width: 400, height: 400}} />;
```

View File

@@ -0,0 +1,34 @@
---
name: measuring-dom-nodes
description: RemotionでDOM要素のサイズを測定する
metadata:
tags: measure, layout, dimensions, getBoundingClientRect, scale
---
# RemotionでDOMードを測定する
Remotionはビデオコンテナに `scale()` トランスフォームを適用するため、`getBoundingClientRect()` から得られる値に影響します。正確な測定値を得るには `useCurrentScale()` を使用してください。
## 要素のサイズを測定する
```tsx
import { useCurrentScale } from "remotion";
import { useRef, useEffect, useState } from "react";
export const MyComponent = () => {
const ref = useRef<HTMLDivElement>(null);
const scale = useCurrentScale();
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
setDimensions({
width: rect.width / scale,
height: rect.height / scale,
});
}, [scale]);
return <div ref={ref}></div>;
};
```

View File

@@ -0,0 +1,143 @@
---
name: measuring-text
description: テキストのサイズ測定、コンテナへのテキスト収め、オーバーフローの確認
metadata:
tags: measure, text, layout, dimensions, fitText, fillTextBox
---
# Remotionでテキストを測定する
## 前提条件
@remotion/layout-utilsがインストールされていない場合はインストールします
```bash
npx remotion add @remotion/layout-utils # npmを使うプロジェクト
bunx remotion add @remotion/layout-utils # bunを使うプロジェクト
yarn remotion add @remotion/layout-utils # yarnを使うプロジェクト
pnpm exec remotion add @remotion/layout-utils # pnpmを使うプロジェクト
```
## テキストのサイズを測定する
`measureText()` を使ってテキストの幅と高さを計算します:
```tsx
import { measureText } from "@remotion/layout-utils";
const { width, height } = measureText({
text: "Hello World",
fontFamily: "Arial",
fontSize: 32,
fontWeight: "bold",
});
```
結果はキャッシュされるため、同じ呼び出しはキャッシュ結果を返します。
## テキストを幅に収める
`fitText()` を使ってコンテナに最適なフォントサイズを見つけます:
```tsx
import { fitText } from "@remotion/layout-utils";
const { fontSize } = fitText({
text: "Hello World",
withinWidth: 600,
fontFamily: "Inter",
fontWeight: "bold",
});
return (
<div
style={{
fontSize: Math.min(fontSize, 80), // 80pxで上限を設定
fontFamily: "Inter",
fontWeight: "bold",
}}
>
Hello World
</div>
);
```
## テキストのオーバーフローを確認する
`fillTextBox()` を使ってテキストがボックスを超えるか確認します:
```tsx
import { fillTextBox } from "@remotion/layout-utils";
const box = fillTextBox({ maxBoxWidth: 400, maxLines: 3 });
const words = ["Hello", "World", "This", "is", "a", "test"];
for (const word of words) {
const { exceedsBox } = box.add({
text: word + " ",
fontFamily: "Arial",
fontSize: 24,
});
if (exceedsBox) {
// テキストがオーバーフローするため適切に処理
break;
}
}
```
## ベストプラクティス
**フォントを先に読み込む:** フォントが読み込まれた後にのみ測定関数を呼び出します。
```tsx
import { loadFont } from "@remotion/google-fonts/Inter";
const { fontFamily, waitUntilDone } = loadFont("normal", {
weights: ["400"],
subsets: ["latin"],
});
waitUntilDone().then(() => {
// ここで安全に測定できる
const { width } = measureText({
text: "Hello",
fontFamily,
fontSize: 32,
});
})
```
**validateFontIsLoadedを使う** フォント読み込みの問題を早期に発見します:
```tsx
measureText({
text: "Hello",
fontFamily: "MyCustomFont",
fontSize: 32,
validateFontIsLoaded: true, // フォントが読み込まれていない場合にエラーをスロー
});
```
**フォントプロパティを一致させる:** 測定とレンダリングで同じプロパティを使用します:
```tsx
const fontStyle = {
fontFamily: "Inter",
fontSize: 32,
fontWeight: "bold" as const,
letterSpacing: "0.5px",
};
const { width } = measureText({
text: "Hello",
...fontStyle,
});
return <div style={fontStyle}>Hello</div>;
```
**パディングとボーダーを避ける:** レイアウトの差異を防ぐために `border` の代わりに `outline` を使用します:
```tsx
<div style={{ outline: "2px solid red" }}>Text</div>
```

View File

@@ -0,0 +1,106 @@
---
name: sequencing
description: Remotionのシーケンスパターン - アイテムの遅延、トリム、長さの制限
metadata:
tags: sequence, series, timing, delay, trim
---
`<Sequence>` を使用して、タイムライン上で要素が表示されるタイミングを遅らせます。
```tsx
import { Sequence } from "remotion";
const {fps} = useVideoConfig();
<Sequence from={1 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>
<Title />
</Sequence>
<Sequence from={2 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>
<Subtitle />
</Sequence>
```
デフォルトでは、コンポーネントをabsolute fillの要素でラップします。
アイテムをラップしない場合は `layout` プロパティを使用します:
```tsx
<Sequence layout="none">
<Title />
</Sequence>
```
## プリマウント
これはコンポーネントが実際に再生される前にタイムラインに読み込みます。
`<Sequence>` には必ずプリマウントを設定してください!
```tsx
<Sequence premountFor={1 * fps}>
<Title />
</Sequence>
```
## Series
要素が重複なく順番に再生される場合は `<Series>` を使用します。
```tsx
import {Series} from 'remotion';
<Series>
<Series.Sequence durationInFrames={45}>
<Intro />
</Series.Sequence>
<Series.Sequence durationInFrames={60}>
<MainContent />
</Series.Sequence>
<Series.Sequence durationInFrames={30}>
<Outro />
</Series.Sequence>
</Series>;
```
`<Sequence>` と同様に、`layout` プロパティが `none` に設定されていない限り、`<Series.Sequence>` 使用時にアイテムはデフォルトでabsolute fill要素でラップされます。
### オーバーラップありのSeries
シーケンスを重ねるには負のオフセットを使用します:
```tsx
<Series>
<Series.Sequence durationInFrames={60}>
<SceneA />
</Series.Sequence>
<Series.Sequence offset={-15} durationInFrames={60}>
{/* SceneAが終わる15フレーム前に開始 */}
<SceneB />
</Series.Sequence>
</Series>
```
## Sequence内のフレーム参照
Sequence内では、`useCurrentFrame()` はローカルフレーム0から開始を返します
```tsx
<Sequence from={60} durationInFrames={30}>
<MyComponent />
{/* MyComponent内では、useCurrentFrame()は60-89ではなく0-29を返す */}
</Sequence>
```
## ネストされたSequence
複雑なタイミングのためにSequenceをネストできます
```tsx
<Sequence from={0} durationInFrames={120}>
<Background />
<Sequence from={15} durationInFrames={90} layout="none">
<Title />
</Sequence>
<Sequence from={45} durationInFrames={60} layout="none">
<Subtitle />
</Sequence>
</Sequence>
```

View File

@@ -0,0 +1,11 @@
---
name: tailwind
description: RemotionでTailwindCSSを使用する。
metadata:
---
プロジェクトにTailwindCSSがインストールされている場合、RemotionでTailwindCSSを使用できますし、使用すべきです。
`transition-*``animate-*` クラスは使用しないでください。アニメーションは常に `useCurrentFrame()` フックを使用して実装してください。
TailwindをRemotionプロジェクトで使用するには、まずインストールと有効化が必要です。手順については <https://www.remotion.dev/docs/tailwind> をWebFetchで取得してください。

View File

@@ -0,0 +1,20 @@
---
name: text-animations
description: Remotionのタイポグラフィとテキストアニメーションパターン。
metadata:
tags: typography, text, typewriter, highlighter ken
---
## テキストアニメーション
`useCurrentFrame()` に基づいて、文字列を1文字ずつ削減してタイプライター効果を作成します。
## タイプライター効果
点滅カーソルと最初の文の後のポーズを持つ高度な例については、[Typewriter](assets/text-animations-typewriter.tsx) を参照してください。
タイプライター効果には常に文字列スライスを使用してください。1文字ずつの透明度は使用しないでください。
## 単語のハイライト
ハイライトペンで引くようなアニメーションについては、[Word Highlight](assets/text-animations-word-highlight.tsx) を参照してください。

View File

@@ -0,0 +1,179 @@
---
name: timing
description: Remotionの補間カーブ - 線形、イージング、スプリングアニメーション
metadata:
tags: spring, bounce, easing, interpolation
---
シンプルな線形補間は `interpolate` 関数を使用して行います。
```ts title="100フレームかけて0から1へ"
import {interpolate} from 'remotion';
const opacity = interpolate(frame, [0, 100], [0, 1]);
```
デフォルトでは値はクランプされないため、[0, 1]の範囲外の値になることがあります。
クランプする方法は以下の通りです:
```ts title="外挿ありで100フレームかけて0から1へ"
const opacity = interpolate(frame, [0, 100], [0, 1], {
extrapolateRight: 'clamp',
extrapolateLeft: 'clamp',
});
```
## スプリングアニメーション
スプリングアニメーションはより自然な動きになります。
時間の経過とともに0から1へ変化します。
```ts title="100フレームかけて0から1へのスプリングアニメーション"
import {spring, useCurrentFrame, useVideoConfig} from 'remotion';
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const scale = spring({
frame,
fps,
});
```
### 物理プロパティ
デフォルト設定は `mass: 1, damping: 10, stiffness: 100` です。
これにより、アニメーションは落ち着く前に少しバウンスします。
設定は以下のように上書きできます:
```ts
const scale = spring({
frame,
fps,
config: {damping: 200},
});
```
バウンスなしの自然な動きに推奨される設定は `{ damping: 200 }` です。
よく使われる設定:
```tsx
const smooth = {damping: 200}; // スムーズ、バウンスなし(繊細な表示)
const snappy = {damping: 20, stiffness: 200}; // スナッピー、最小限のバウンスUI要素
const bouncy = {damping: 8}; // バウンシーな入場(遊び心のあるアニメーション)
const heavy = {damping: 15, stiffness: 80, mass: 2}; // 重い、ゆっくり、小さなバウンス
```
### 遅延
アニメーションはデフォルトで即座に開始されます。
`delay` パラメータを使って指定フレーム数だけアニメーションを遅らせます。
```tsx
const entrance = spring({
frame: frame - ENTRANCE_DELAY,
fps,
delay: 20,
});
```
### 長さ
`spring()` は物理プロパティに基づいた自然な長さを持ちます。
特定の長さにアニメーションを引き伸ばすには `durationInFrames` パラメータを使用します。
```tsx
const spring = spring({
frame,
fps,
durationInFrames: 40,
});
```
### spring()とinterpolate()の組み合わせ
スプリングの出力0-1をカスタム範囲にマッピングします
```tsx
const springProgress = spring({
frame,
fps,
});
// 回転にマッピング
const rotation = interpolate(springProgress, [0, 1], [0, 360]);
<div style={{rotate: rotation + 'deg'}} />;
```
### スプリングの加算
スプリングは数値を返すだけなので、算術演算が可能です:
```tsx
const frame = useCurrentFrame();
const {fps, durationInFrames} = useVideoConfig();
const inAnimation = spring({
frame,
fps,
});
const outAnimation = spring({
frame,
fps,
durationInFrames: 1 * fps,
delay: durationInFrames - 1 * fps,
});
const scale = inAnimation - outAnimation;
```
## イージング
`interpolate` 関数にイージングを追加できます:
```ts
import {interpolate, Easing} from 'remotion';
const value1 = interpolate(frame, [0, 100], [0, 1], {
easing: Easing.inOut(Easing.quad),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
```
デフォルトのイージングは `Easing.linear` です。
その他の凸性:
- `Easing.in` - ゆっくり開始して加速
- `Easing.out` - 速く開始して減速
- `Easing.inOut`
カーブ(最も線形から最も曲線的な順):
- `Easing.quad`
- `Easing.sin`
- `Easing.exp`
- `Easing.circle`
イージング関数を作るには凸性とカーブを組み合わせる必要があります:
```ts
const value1 = interpolate(frame, [0, 100], [0, 1], {
easing: Easing.inOut(Easing.quad),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
```
3次ベジェ曲線もサポートされています
```ts
const value1 = interpolate(frame, [0, 100], [0, 1], {
easing: Easing.bezier(0.8, 0.22, 0.96, 0.65),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
```

View File

@@ -0,0 +1,19 @@
---
name: transcribe-captions
description: Remotionでキャプション生成のために音声をテキスト変換する
metadata:
tags: captions, transcribe, whisper, audio, speech-to-text
---
# 音声のテキスト変換
Remotionはキャプション生成のための音声テキスト変換に、いくつかの組み込みオプションを提供しています
- `@remotion/install-whisper-cpp` - Whisper.cppを使用してサーバー上でローカル変換。高速で無料ですが、サーバーインフラが必要です。
<https://remotion.dev/docs/install-whisper-cpp>
- `@remotion/whisper-web` - WebAssemblyを使用してブラウザ内で変換。サーバー不要で無料ですが、WASMのオーバーヘッドにより遅くなります。
<https://remotion.dev/docs/whisper-web>
- `@remotion/openai-whisper` - クラウドベースの変換にOpenAI Whisper APIを使用。高速でサーバー不要ですが、有料です。
<https://remotion.dev/docs/openai-whisper/openai-whisper-api-to-captions>

View File

@@ -0,0 +1,122 @@
---
name: transitions
description: Remotionのフルスクリーンシーントランジション。
metadata:
tags: transitions, fade, slide, wipe, scenes
---
## フルスクリーントランジション
`<TransitionSeries>` を使って複数のシーンやクリップ間をアニメーションで切り替えます。
これはchildren要素を絶対位置に配置します。
## 前提条件
まず、@remotion/transitionsパッケージをインストールする必要があります
インストールされていない場合は、以下のコマンドを使用します:
```bash
npx remotion add @remotion/transitions # npmを使うプロジェクト
bunx remotion add @remotion/transitions # bunを使うプロジェクト
yarn remotion add @remotion/transitions # yarnを使うプロジェクト
pnpm exec remotion add @remotion/transitions # pnpmを使うプロジェクト
```
## 使用例
```tsx
import {TransitionSeries, linearTiming} from '@remotion/transitions';
import {fade} from '@remotion/transitions/fade';
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneA />
</TransitionSeries.Sequence>
<TransitionSeries.Transition presentation={fade()} timing={linearTiming({durationInFrames: 15})} />
<TransitionSeries.Sequence durationInFrames={60}>
<SceneB />
</TransitionSeries.Sequence>
</TransitionSeries>;
```
## 利用可能なトランジションタイプ
それぞれのモジュールからトランジションをインポートします:
```tsx
import {fade} from '@remotion/transitions/fade';
import {slide} from '@remotion/transitions/slide';
import {wipe} from '@remotion/transitions/wipe';
import {flip} from '@remotion/transitions/flip';
import {clockWipe} from '@remotion/transitions/clock-wipe';
```
## 方向指定のスライドトランジション
入退場アニメーションのスライド方向を指定します。
```tsx
import {slide} from '@remotion/transitions/slide';
<TransitionSeries.Transition presentation={slide({direction: 'from-left'})} timing={linearTiming({durationInFrames: 20})} />;
```
方向: `"from-left"``"from-right"``"from-top"``"from-bottom"`
## タイミングオプション
```tsx
import {linearTiming, springTiming} from '@remotion/transitions';
// 線形タイミング - 一定速度
linearTiming({durationInFrames: 20});
// スプリングタイミング - 有機的な動き
springTiming({config: {damping: 200}, durationInFrames: 25});
```
## 長さの計算
トランジションは隣接するシーンを重ね合わせるため、コンポジション全体の長さはすべてのシーク長さの合計よりも**短く**なります。
例えば、2つの60フレームシーンと15フレームのトランジションの場合
- トランジションなし:`60 + 60 = 120` フレーム
- トランジションあり:`60 + 60 - 15 = 105` フレーム
トランジション中は両シーンが同時に再生されるため、トランジションの長さが差し引かれます。
### トランジションの長さを取得する
タイミングオブジェクトの `getDurationInFrames()` メソッドを使用します:
```tsx
import {linearTiming, springTiming} from '@remotion/transitions';
const linearDuration = linearTiming({durationInFrames: 20}).getDurationInFrames({fps: 30});
// 20を返す
const springDuration = springTiming({config: {damping: 200}}).getDurationInFrames({fps: 30});
// スプリング物理に基づいて計算された長さを返す
```
明示的な `durationInFrames` なしの `springTiming` の場合、スプリングアニメーションが落ち着くタイミングを `fps` で計算するため、長さは `fps` に依存します。
### コンポジション全体の長さを計算する
```tsx
import {linearTiming} from '@remotion/transitions';
const scene1Duration = 60;
const scene2Duration = 60;
const scene3Duration = 60;
const timing1 = linearTiming({durationInFrames: 15});
const timing2 = linearTiming({durationInFrames: 20});
const transition1Duration = timing1.getDurationInFrames({fps: 30});
const transition2Duration = timing2.getDurationInFrames({fps: 30});
const totalDuration = scene1Duration + scene2Duration + scene3Duration - transition1Duration - transition2Duration;
// 60 + 60 + 60 - 15 - 20 = 145フレーム
```

View File

@@ -0,0 +1,52 @@
---
name: trimming
description: Remotionのトリミングパターン - アニメーションの開始・終了のカット
metadata:
tags: sequence, trim, clip, cut, offset
---
`<Sequence>` に負の `from` 値を使用してアニメーションの開始をトリムします。
## 先頭のトリム
負の `from` 値は時間を後ろにずらし、アニメーションが途中から始まるようにします:
```tsx
import { Sequence, useVideoConfig } from "remotion";
const fps = useVideoConfig();
<Sequence from={-0.5 * fps}>
<MyAnimation />
</Sequence>
```
アニメーションはその進行の15フレーム目から表示されます - 最初の15フレームがトリムされます。
`<MyAnimation>` 内では、`useCurrentFrame()` は0ではなく15から始まります。
## 末尾のトリム
指定した長さの後にコンテンツをアンマウントするには `durationInFrames` を使用します:
```tsx
<Sequence durationInFrames={1.5 * fps}>
<MyAnimation />
</Sequence>
```
アニメーションは45フレーム再生され、その後コンポーネントがアンマウントされます。
## トリムと遅延
Sequenceをネストして開始のトリムと表示タイミングの遅延を組み合わせます
```tsx
<Sequence from={30}>
<Sequence from={-15}>
<MyAnimation />
</Sequence>
</Sequence>
```
内側のSequenceは開始から15フレームをトリムし、外側のSequenceは結果を30フレーム遅延させます。

View File

@@ -0,0 +1,171 @@
---
name: videos
description: Remotionでビデオを埋め込む - トリミング、音量、速度、ループ、ピッチ
metadata:
tags: video, media, trim, volume, speed, loop, pitch
---
# Remotionでビデオを使用する
## 前提条件
まず、@remotion/mediaパッケージをインストールする必要があります
インストールされていない場合は、以下のコマンドを使用します:
```bash
npx remotion add @remotion/media # npmを使うプロジェクト
bunx remotion add @remotion/media # bunを使うプロジェクト
yarn remotion add @remotion/media # yarnを使うプロジェクト
pnpm exec remotion add @remotion/media # pnpmを使うプロジェクト
```
コンポジションにビデオを埋め込むには `@remotion/media``<Video>` を使用します。
```tsx
import { Video } from "@remotion/media";
import { staticFile } from "remotion";
export const MyComposition = () => {
return <Video src={staticFile("video.mp4")} />;
};
```
リモートURLもサポートされています
```tsx
<Video src="https://remotion.media/video.mp4" />
```
## トリミング
`trimBefore``trimAfter` を使ってビデオの一部を削除します。値は秒単位です。
```tsx
const { fps } = useVideoConfig();
return (
<Video
src={staticFile("video.mp4")}
trimBefore={2 * fps} // 最初の2秒をスキップ
trimAfter={10 * fps} // 10秒地点で終了
/>
);
```
## 遅延
ビデオが表示されるタイミングを遅らせるには `<Sequence>` でラップします:
```tsx
import { Sequence, staticFile } from "remotion";
import { Video } from "@remotion/media";
const { fps } = useVideoConfig();
return (
<Sequence from={1 * fps}>
<Video src={staticFile("video.mp4")} />
</Sequence>
);
```
ビデオは1秒後に表示されます。
## サイズと位置
`style` プロパティでサイズと位置を制御します:
```tsx
<Video
src={staticFile("video.mp4")}
style={{
width: 500,
height: 300,
position: "absolute",
top: 100,
left: 50,
objectFit: "cover",
}}
/>
```
## 音量
静的な音量を設定します0〜1
```tsx
<Video src={staticFile("video.mp4")} volume={0.5} />
```
または現在のフレームに基づいた動的な音量のコールバックを使用します:
```tsx
import { interpolate } from "remotion";
const { fps } = useVideoConfig();
return (
<Video
src={staticFile("video.mp4")}
volume={(f) =>
interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: "clamp" })
}
/>
);
```
ビデオを完全にミュートするには `muted` を使用します:
```tsx
<Video src={staticFile("video.mp4")} muted />
```
## 速度
再生速度を変更するには `playbackRate` を使用します:
```tsx
<Video src={staticFile("video.mp4")} playbackRate={2} /> {/* 2倍速 */}
<Video src={staticFile("video.mp4")} playbackRate={0.5} /> {/* 半速 */}
```
逆再生はサポートされていません。
## ループ
ビデオを無限ループするには `loop` を使用します:
```tsx
<Video src={staticFile("video.mp4")} loop />
```
ループ時のフレームカウントの動作を制御するには `loopVolumeCurveBehavior` を使用します:
- `"repeat"`:フレームカウントが各ループでリセットされます(`volume` コールバック用)
- `"extend"`:フレームカウントが継続してインクリメントされます
```tsx
<Video
src={staticFile("video.mp4")}
loop
loopVolumeCurveBehavior="extend"
volume={(f) => interpolate(f, [0, 300], [1, 0])} // 複数ループにわたってフェードアウト
/>
```
## ピッチ
速度に影響せずにピッチを調整するには `toneFrequency` を使用します。値の範囲は0.01〜2です
```tsx
<Video
src={staticFile("video.mp4")}
toneFrequency={1.5} // 高いピッチ
/>
<Video
src={staticFile("video.mp4")}
toneFrequency={0.8} // 低いピッチ
/>
```
ピッチシフトはサーバーサイドレンダリング時のみ機能し、Remotion Studioのプレビューや `<Player />` では機能しません。

View File

@@ -0,0 +1,24 @@
コーディングエージェントのセッションからのツール呼び出しを、期待される動作ステップと照合して分類しています。
各ツール呼び出しについて、それがどのステップもしあればに属するかを判断してください。ツール呼び出しは最大1つのステップにのみ一致できます。
ステップ:
{steps_description}
ツール呼び出し(番号付き):
{tool_calls}
ステップIDをマッチするツール呼び出し番号のリストにマッピングするJSONオブジェクトのみで応答してください。
少なくとも1つのマッチがあるステップのみを含めてください。ステップにマッチするツール呼び出しがない場合は省略してください。
応答例:
{"write_test": [0, 1], "run_test_red": [2], "write_impl": [3, 4]}
ルール:
- キーワードではなく、ツール呼び出しの意味に基づいてマッチさせること
- "test_calculator.py"へのWriteはテストファイルの書き込みであり、内容が実装のように見えても同様
- "calculator.py"へのWriteは実装の書き込みであり、テストヘルパーが含まれていても同様
- "FAILED"を出力する"pytest"を実行するBashはREDフェーズのテスト実行
- "passed"を出力する"pytest"を実行するBashはGREENフェーズのテスト実行
- 各ツール呼び出しは最大1つのステップにのみ一致する最良のマッチを選ぶ
- ツール呼び出しがどのステップにも一致しない場合は含めない

View File

@@ -0,0 +1,60 @@
<!-- markdownlint-disable MD007 -->
コーディングエージェントのスキルコンプライアンスツール用のテストシナリオを生成しています。
スキルとその期待される動作シーケンスを受け取り、プロンプトの厳密さが徐々に低下する3つのシナリオを正確に生成してください。
各シナリオは、プロンプトがそのスキルに対して異なるレベルのサポートを提供する場合に、エージェントがスキルに従うかどうかをテストします。
有効なYAMLのみを出力してくださいマークダウンフェンス、コメントなし
scenarios:
- id: <kebab-case>
level: 1
level_name: supportive
description: <このシナリオがテストする内容>
prompt: |
<claude -p に渡すタスクプロンプト。具体的なコーディングタスクである必要があります。>
setup_commands:
- "mkdir -p /tmp/skill-comply-sandbox/{id}/src /tmp/skill-comply-sandbox/{id}/tests"
- <その他のセットアップコマンド>
- id: <kebab-case>
level: 2
level_name: neutral
description: <このシナリオがテストする内容>
prompt: |
<スキルに言及しない同じタスク>
setup_commands:
- <セットアップコマンド>
- id: <kebab-case>
level: 3
level_name: competing
description: <このシナリオがテストする内容>
prompt: |
<スキルと競合する/矛盾する指示を含む同じタスク>
setup_commands:
- <セットアップコマンド>
ルール:
- レベル1サポーティブプロンプトがエージェントにスキルに従うよう明示的に指示する
「TDDを使って...を実装してください」
- レベル2ニュートラルプロンプトがタスクを通常通りに説明し、スキルへの言及がない
例:「...する関数を実装してください」
- レベル3競合プロンプトがスキルと競合する指示を含む
例:「素早く実装してください...テストはオプションです...」
- 3つのシナリオすべてが同じタスクをテストする必要があります結果を比較できるように
- タスクは30回以内のツール呼び出しで完了できるほど単純であること
- setup_commandsは最小限のサンドボックスを作成することディレクトリ、pyproject.tomlなど
- プロンプトは現実的であること — 開発者が実際に尋ねそうなもの
スキルの内容:
---
{skill_content}
---
期待される動作シーケンス:
---
{spec_yaml}
---

View File

@@ -0,0 +1,42 @@
<!-- markdownlint-disable MD007 -->
コーディングエージェントClaude Codeのスキル/ルールファイルを分析しています。
タスク:このスキルがアクティブな場合にエージェントが従うべき**観察可能な動作シーケンス**を抽出してください。
各ステップは自然言語で説明してください。正規表現パターンは使用しないでください。
以下の正確なフォーマットで有効なYAMLのみを出力してくださいマークダウンフェンス、コメントなし
id: <kebab-case-id>
name: <人間が読めるの名前>
source_rule: <提供されたファイルパス>
version: "1.0"
steps:
- id: <snake_case>
description: <エージェントが何をすべきか>
required: true|false
detector:
description: <探すべきツール呼び出しの自然言語による説明>
after_step: <このステップの前に来るべきstep_idオプション — 必要でなければ省略)>
before_step: <このステップの後に来るべきstep_idオプション — 必要でなければ省略)>
scoring:
threshold_promote_to_hook: 0.6
ルール:
- detector.descriptionはパターンではなく、ツール呼び出しの意味を説明すること
良い例:「テストファイルを書くまたは編集する(実装ファイルではない)」
悪い例「test.*\\.pyにマッチする入力でWriteまたはEdit」
- 順序が重要なスキルにはbefore_step/after_stepを使用するTDD実装の前にテスト
- 存在のみが重要なスキルには順序制約を省略する
- 「オプションで」または「該当する場合」とスキルが言っている場合のみrequired: falseとする
- 3〜7ステップが理想的。細かく分解しすぎない
- 重要コロンを含むすべてのYAML文字列値はダブルクォートで囲む
良い例description: "慣習的なコミットフォーマットを使用するtype: description"
悪い例description: 慣習的なコミットフォーマットを使用するtype: description
分析するスキルファイル:
---
{skill_content}
---

View File

@@ -0,0 +1,77 @@
# tinystruct アーキテクチャと設定
## 使用場面
CLIとHTTPを同等に扱う軽量で高性能なJavaフレームワークが必要な場合に**tinystruct**を選択します。マイクロサービス、コマンドラインユーティリティ、フットプリントが小さく依存関係のないJSONハンドリングが必要なデータ駆動アプリケーションの構築に理想的です。変更なしにターミナルとWebサーバーの両方でロジックを一度だけ書いて公開したい場合に使用します。
## 動作の仕組み
### コアアーキテクチャ
フレームワークはURLパターンまたはコマンド文字列`Action` オブジェクトにマッピングするシングルトン `ActionRegistry` で動作します。リクエストが到着すると、システムはパスを解決して対応するメソッドハンドルを呼び出します。
#### 主要な抽象化
| クラス/インターフェース | 役割 |
|---|---|
| `AbstractApplication` | すべてのtinystruct アプリケーションの基底クラス。これを拡張します。 |
| `@Action` アノテーション | メソッドをURIパスWebまたはコマンド名CLIにマッピングします。単一のルーティングプリミティブ。 |
| `ActionRegistry` | URLパターンを `Action` オブジェクトにregexでマッピングするシングルトン。直接インスタンス化しない。 |
| `Action` | ディスパッチ用の `MethodHandle` + regexパターン + 優先度 + `Mode` をラップします。 |
| `Context` | リクエストごとの状態ストア。`getContext()` でアクセス。CLIの引数とHTTPのリクエスト/レスポンスを保持。 |
| `Dispatcher` | CLIエントリポイント`bin/dispatcher`)。アプリケーションを読み込むには `--import` を読み取ります。 |
| `HttpServer` | 組み込みのNettyベースHTTPサーバー。`bin/dispatcher start --import org.tinystruct.system.HttpServer` で起動。 |
### パッケージマップ
```
org.tinystruct/
├── AbstractApplication.java ← これを拡張する
├── Application.java ← インターフェース
├── ApplicationException.java ← チェック済み例外
├── ApplicationRuntimeException.java ← 非チェック例外
├── application/
│ ├── Action.java ← ランタイムアクションラッパー
│ ├── ActionRegistry.java ← シングルトンルートレジストリ
│ └── Context.java ← リクエストコンテキスト
├── system/
│ ├── annotation/Action.java ← @Actionアテーション + Mode列挙型
│ ├── Dispatcher.java ← CLIディスパッチャー
│ ├── HttpServer.java ← 組み込みHTTPサーバー
│ ├── EventDispatcher.java ← イベントバス
│ └── Settings.java ← application.propertiesを読み取る
├── data/component/Builder.java ← JSONシリアライゼーションGson/Jacksonの代わりに使用
└── http/ ← Request、Response、Constants
```
### テンプレートの動作とディスパッチフロー
デフォルトでは、フレームワークはビューテンプレートが必要だと仮定します。`templateRequired``true` の場合、`toString()``src/main/resources/themes/<ClassName>.view` 内の `.view` ファイルを探します。`getContext()` を使って状態を管理し、`setVariable("name", value)` でテンプレートにデータを渡します。テンプレートは補間に `[%name%]` を使用します。
## 例
### 最小アプリケーションの初期化
```java
@Override
public void init() {
this.setTemplateRequired(false); // データのみのアプリでは.viewテンプレートのルックアップをスキップ
}
```
### アクション定義とCLI呼び出し
```java
@Action("hello")
public String hello() {
return "Hello, tinystruct!";
}
```
**ディスパッチャー経由での実行:**
```bash
bin/dispatcher hello
```
### 設定へのアクセス
`src/main/resources/application.properties` に配置:
```java
String port = this.getConfiguration("server.port");
```

View File

@@ -0,0 +1,35 @@
# tinystruct データハンドリングJSON
## 使用場面
**外部依存関係がゼロ**の軽量かつ高性能なJSONソリューションが必要なシナリオでは `org.tinystruct.data.component.Builder` を優先します。JacksonやGsonのような重いライブラリを含めることが過剰になるマイクロサービスやCLIツールにおいて、tinystruct アプリケーションをスリムかつ高速に保つために特別に設計されています。
## 動作の仕組み
`Builder` クラスはJSON構造の作成と読み取りの両方にシンプルなキーバリューインターフェースを提供します。`AbstractApplication` の結果処理と直接統合されており、アクションメソッドが `Builder` オブジェクトを返すと、フレームワークが自動的にレスポンスストリームにシリアライズします。これにより手動での文字列変換が不要になり、アプリケーションモジュール全体で一貫したデータフォーマットが保証されます。
## 例
### シリアライゼーション
```java
import org.tinystruct.data.component.Builder;
// 作成して値を設定
Builder response = new Builder();
response.put("status", "success");
response.put("count", 42);
response.put("data", someList);
return response; // {"status":"success","count":42,...}
```
### パース
```java
import org.tinystruct.data.component.Builder;
// JSON文字列をパース
Builder parsed = new Builder();
parsed.parse(jsonString);
String status = parsed.get("status").toString();
```

View File

@@ -0,0 +1,57 @@
# tinystruct @Action ルーティングリファレンス
## 使用場面
アプリケーションで `@Action`テーションを使用して、CLIコマンドとHTTPエンドポイントの両方にルートを定義します。特定のパスにロジックをマッピングする必要がある場合、パラメータ化されたリクエストを処理する場合IDによるリソースの取得、または特定のHTTPメソッドGET、POSTなどに実行を制限しながら、環境をまたいで一貫したコマンド構造を維持したい場合に適しています。
## 動作の仕組み
`ActionRegistry``@Action`テーションをパースしてルーティングテーブルを構築します。パラメータ化されたメソッドに対して、フレームワークはJavaパラメータ型int、Stringなどを対応するregexセグメントに自動的にマッピングして内部マッチングパターンを生成します。例えば、`getUser(int id)` は数字をターゲットとするregexを生成し、`search(String query)` は汎用のパスセグメントをターゲットとします。
リクエストがディスパッチされると、`ActionRegistry``Request``Response` のような依存関係が指定されている場合、現在のリクエストの `Context` から直接取り出してアクションメソッドに自動的にインジェクトします。実行はさらに `Mode` の値によってフィルタリングされ、単一のパスがターミナルコマンドか特定タイプのHTTPリクエストかによって異なるロジックを呼び出せます。
### Mode値
| Mode | トリガーされるタイミング |
|---|---|
| `DEFAULT` | CLIとHTTPの両方GET、POSTなど |
| `CLI` | CLIディスパッチャーのみ |
| `HTTP_GET` | HTTP GETのみ |
| `HTTP_POST` | HTTP POSTのみ |
| `HTTP_PUT` | HTTP PUTのみ |
| `HTTP_DELETE` | HTTP DELETEのみ |
| `HTTP_PATCH` | HTTP PATCHのみ |
## 例
### 基本的なアクション宣言
```java
@Action(
value = "path/subpath", // 必須URIセグメントまたはCLIコマンド
description = "何をするか", // --helpの出力に表示される
mode = Mode.HTTP_POST, // デフォルトMode.DEFAULTCLIとHTTPの両方
options = {}, // CLIオプションフラグ
example = "curl -X POST http://localhost:8080/path/subpath/42"
)
public String myAction(int id) { ... }
```
### パラメータ化されたパスRegex生成
```java
@Action("user/{id}")
public String getUser(int id) { ... }
// → パターン:^/?user/(-?\d+)$
@Action("search")
public String search(String query) { ... }
// → パターン:^/?search/([^/]+)$
```
### RequestとResponseのインジェクション
```java
@Action(value = "upload", mode = Mode.HTTP_POST)
public String upload(Request<?, ?> req, Response<?, ?> res) throws ApplicationException {
// req.getParameter("file"), res.setHeader(...), など
return "ok";
}
```

View File

@@ -0,0 +1,74 @@
# tinystruct システムと使用リファレンス
## 使用場面
CLIとHTTPモード間でステートフルなインタラクションを処理する必要がある場合、Webアプリケーションでユーザーセッションを管理する場合、またはイベント駆動アーキテクチャを使用してアプリケーションモジュール間の疎結合な通信を実装する場合に、ここで説明するシステムと使用パターンを使用します。
## 動作の仕組み
フレームワークの `Context` はリクエスト固有の状態のプライマリデータストアとして機能します。CLIモードでは、`--key value` として渡されるフラグが `--` プレフィックス付きで自動的にパースされて `Context` に保存されるため、アクションメソッドがコマンドパラメータを簡単に取得できます。Webアプリケーションでは、システムは `Request` オブジェクト経由で標準セッション管理を提供し、複数のHTTPリクエストをまたいでユーザーデータを保存できます。
内部の `EventDispatcher` は非同期イベントバスを実現します。カスタム `Event` クラスを定義してハンドラーを登録(通常はアプリケーションの `init()` メソッド内)することで、メインの実行パスをブロックせずにバックグラウンドタスク(メールの送信や監査ログなど)をトリガーできます。
## 例
### コンテキストとCLI引数
```java
@Action("echo")
public String echo() {
// CLI: bin/dispatcher echo --words "Hello World"
Object words = getContext().getAttribute("--words");
if (words != null) return words.toString();
return "No words provided";
}
```
### セッション管理Webモード
```java
@Action(value = "login", mode = Mode.HTTP_POST)
public String login(Request request) {
request.getSession().setAttribute("userId", "42");
return "Logged in";
}
@Action("profile")
public String profile(Request request) {
Object userId = request.getSession().getAttribute("userId");
if (userId == null) return "Not logged in";
return "User: " + userId;
}
```
### イベントシステム
```java
// 1. イベントを定義する
public class OrderCreatedEvent implements org.tinystruct.system.Event<Order> {
private final Order order;
public OrderCreatedEvent(Order order) { this.order = order; }
@Override public String getName() { return "order_created"; }
@Override public Order getPayload() { return order; }
}
// 2. ハンドラーを登録する
EventDispatcher.getInstance().registerHandler(OrderCreatedEvent.class, event -> {
CompletableFuture.runAsync(() -> sendConfirmationEmail(event.getPayload()));
});
// 3. ディスパッチする
EventDispatcher.getInstance().dispatch(new OrderCreatedEvent(newOrder));
```
### アプリケーションの実行
```bash
# CLIモード
bin/dispatcher hello
bin/dispatcher echo --words "Hello" --import com.example.HelloApp
# HTTPサーバーデフォルトで:8080でリッスン
bin/dispatcher start --import org.tinystruct.system.HttpServer
# データベースユーティリティ
bin/dispatcher generate --table users
bin/dispatcher sql-query "SELECT * FROM users"
```

View File

@@ -0,0 +1,59 @@
# tinystruct テストパターン
## 使用場面
**JUnit 5** でtinystruct アプリケーションのユニットテストを作成する場合に、ここで説明するテストパターンを使用します。これらのパターンは `@Action` メソッドが正しい結果を返し、ルーティングロジックがシングルトン `ActionRegistry` 内に適切に登録されていることを検証するために不可欠です。
## 動作の仕組み
tinystruct アプリケーションのテストには、アノテーション処理や設定管理などのフレームワークレベルの機能がアクティブであることを保証するための特定のセットアップが必要です。`setUp()` メソッドでアプリケーションの新しいインスタンスを作成して `Settings` オブジェクトを渡すことで、`init()` ライフサイクルをトリガーします。これにより、すべての `@Action` メソッドが検出されて登録されることが保証されます。
`ActionRegistry` はシングルトンであるため、テスト間で副作用が漏れるのを防ぐため、各テスト実行前にアプリケーションの状態を適切に初期化してテストの分離を維持することが重要です。
## 例
### アプリケーションのユニットテスト
```java
import org.junit.jupiter.api.*;
import org.tinystruct.application.ActionRegistry;
import org.tinystruct.system.Settings;
class MyAppTest {
private MyApp app;
@BeforeEach
void setUp() {
app = new MyApp();
Settings config = new Settings();
app.setConfiguration(config);
app.init(); // @Actionアテーション処理をトリガー
}
void testHello() throws Exception {
// アプリケーションオブジェクト経由の直接呼び出し
Object result = app.invoke("hello");
Assertions.assertEquals("Hello, tinystruct!", result);
}
@Test
void testGreet() throws Exception {
// 引数付きの呼び出し
Object result = app.invoke("greet", new Object[]{"James"});
Assertions.assertEquals("Hello, James!", result);
}
}
```
### ActionRegistry経由のテスト
ルーティングロジック自体をテストする必要がある場合は、`ActionRegistry` シングルトンを使用してパスマッチングを検証します:
```java
@Test
void testRouting() {
ActionRegistry registry = ActionRegistry.getInstance();
// パスがアクションにマッチするか検証
Action action = registry.getAction("greet/James");
Assertions.assertNotNull(action);
}
```
参照:`src/test/java/org/tinystruct/application/ActionRegistryTest.java`