=
+
+ {{#each @items as |item|}}
+
+ {{item.name}}
+
+ {{/each}}
+
+ ;
+
+export default TypedComponent;
+```
+
+Explicit helper imports enable better tree-shaking, make dependencies clear, and improve IDE support with proper type checking.
+
+Reference: [https://github.com/ember-template-imports/ember-template-imports](https://github.com/ember-template-imports/ember-template-imports)
+
+### 6.4 No helper() Wrapper for Plain Functions
+
+**Impact: LOW-MEDIUM (Simpler code, better performance)**
+
+In modern Ember, plain functions can be used directly as helpers without wrapping them with `helper()`. The `helper()` wrapper is legacy and adds unnecessary complexity.
+
+**Incorrect (using helper() wrapper):**
+
+```javascript
+// app/utils/format-date.js
+import { helper } from '@ember/component/helper';
+
+function formatDate([date]) {
+ return new Date(date).toLocaleDateString();
+}
+
+export default helper(formatDate);
+```
+
+**Correct: plain function**
+
+```javascript
+// app/utils/format-date.js
+export function formatDate(date) {
+ return new Date(date).toLocaleDateString();
+}
+```
+
+**Usage in templates:**
+
+```glimmer-js
+// app/components/post-card.gjs
+import { formatDate } from '../utils/format-date';
+
+
+
+ {{@post.title}}
+ {{formatDate @post.publishedAt}}
+
+
+```
+
+**With Multiple Arguments:**
+
+```glimmer-js
+// app/components/price.gjs
+import { formatCurrency } from '../utils/format-currency';
+
+
+
+ {{formatCurrency @amount @currency}}
+
+
+```
+
+**For Helpers that Need Services: use class-based**
+
+```javascript
+// app/utils/format-relative-time.js
+export class FormatRelativeTime {
+ constructor(owner) {
+ this.intl = owner.lookup('service:intl');
+ }
+
+ compute(date) {
+ return this.intl.formatRelative(date);
+ }
+}
+```
+
+When you need dependency injection, use a class instead of `helper()`:
+
+**Why Avoid helper():**
+
+1. **Simpler**: Plain functions are easier to understand
+
+2. **Standard JavaScript**: No Ember-specific wrapper needed
+
+3. **Better Testing**: Plain functions are easier to test
+
+4. **Performance**: No wrapper overhead
+
+5. **Modern Pattern**: Aligns with modern Ember conventions
+
+**Migration from helper():**
+
+```javascript
+// Before
+import { helper } from '@ember/component/helper';
+
+function capitalize([text]) {
+ return text.charAt(0).toUpperCase() + text.slice(1);
+}
+
+export default helper(capitalize);
+
+// After
+export function capitalize(text) {
+ return text.charAt(0).toUpperCase() + text.slice(1);
+}
+```
+
+**Common Helper Patterns:**
+
+```glimmer-js
+// Usage
+import { capitalize, truncate, pluralize } from '../utils/string-helpers';
+
+
+ {{capitalize @title}}
+ {{truncate @description 100}}
+ {{@count}} {{pluralize @count "item" "items"}}
+
+```
+
+Plain functions are the modern way to create helpers in Ember. Only use classes when you need dependency injection.
+
+Reference: [https://guides.emberjs.com/release/components/helper-functions/](https://guides.emberjs.com/release/components/helper-functions/)
+
+### 6.5 Optimize Conditional Rendering
+
+**Impact: HIGH (Reduces unnecessary rerenders in dynamic template branches)**
+
+Use efficient conditional rendering patterns to minimize unnecessary DOM updates and improve rendering performance.
+
+Inefficient conditional logic causes excessive re-renders, creates complex template code, and can lead to poor performance in lists and dynamic UIs.
+
+**Incorrect:**
+
+```glimmer-js
+// app/components/user-list.gjs
+import Component from '@glimmer/component';
+
+class UserList extends Component {
+
+ {{#each @users as |user|}}
+
+ {{! Recomputes every time}}
+ {{#if (eq user.role "admin")}}
+ {{user.name}} (Admin)
+ {{/if}}
+ {{#if (eq user.role "moderator")}}
+ {{user.name}} (Mod)
+ {{/if}}
+ {{#if (eq user.role "user")}}
+ {{user.name}}
+ {{/if}}
+
+ {{/each}}
+
+}
+```
+
+Use `{{#if}}` / `{{#else if}}` / `{{#else}}` chains and extract computed logic to getters for better performance and readability.
+
+**Correct:**
+
+```glimmer-js
+// app/components/task-list.gjs
+import Component from '@glimmer/component';
+
+class TaskList extends Component {
+ get hasTasks() {
+ return this.args.tasks?.length > 0;
+ }
+
+
+ {{#if this.hasTasks}}
+
+ {{#each @tasks as |task|}}
+
+ {{task.title}}
+ {{#if task.completed}}
+ ✓
+ {{/if}}
+
+ {{/each}}
+
+ {{else}}
+ No tasks yet
+ {{/if}}
+
+}
+```
+
+For complex conditions, use getters:
+
+Use `{{#if}}` to guard `{{#each}}` and avoid rendering empty states:
+
+**Bad:**
+
+```glimmer-js
+{{#if @user}}
+ {{#if @user.isPremium}}
+ {{#if @user.hasAccess}}
+
+ {{/if}}
+ {{/if}}
+{{/if}}
+```
+
+**Good:**
+
+```glimmer-js
+// app/components/data-display.gjs
+import Component from '@glimmer/component';
+import { Resource } from 'ember-resources';
+import { resource } from 'ember-resources';
+
+class DataResource extends Resource {
+ @tracked data = null;
+ @tracked isLoading = true;
+ @tracked error = null;
+
+ modify(positional, named) {
+ this.fetchData(named.url);
+ }
+
+ async fetchData(url) {
+ this.isLoading = true;
+ this.error = null;
+ try {
+ const response = await fetch(url);
+ this.data = await response.json();
+ } catch (e) {
+ this.error = e.message;
+ } finally {
+ this.isLoading = false;
+ }
+ }
+}
+
+class DataDisplay extends Component {
+ @resource data = DataResource.from(() => ({
+ url: this.args.url,
+ }));
+
+
+ {{#if this.data.isLoading}}
+ Loading...
+ {{else if this.data.error}}
+ Error: {{this.data.error}}
+ {{else}}
+
+ {{this.data.data}}
+
+ {{/if}}
+
+}
+```
+
+Use conditional rendering for component selection:
+
+Pattern for async data with loading/error states:
+
+- **Chained if/else**: 40-60% faster than multiple independent {{#if}} blocks
+
+- **Extracted getters**: ~20% faster for complex conditions (cached)
+
+- **Component switching**: Same performance as {{#if}} but better code organization
+
+- **{{#if}}/{{#else}}**: For simple true/false conditions
+
+- **Extracted getters**: For complex or reused conditions
+
+- **Component switching**: For different component types based on state
+
+- **Guard clauses**: To avoid rendering large subtrees when not needed
+
+- [Ember Guides - Conditionals](https://guides.emberjs.com/release/components/conditional-content/)
+
+- [Glimmer VM Performance](https://github.com/glimmerjs/glimmer-vm)
+
+- [@cached decorator](https://api.emberjs.com/ember/release/functions/@glimmer%2Ftracking/cached)
+
+### 6.6 Template-Only Components with In-Scope Functions
+
+**Impact: MEDIUM (Clean, performant patterns for template-only components)**
+
+For template-only components (components without a class and `this`), use in-scope functions to keep logic close to the template while avoiding unnecessary caching overhead.
+
+**Incorrect: using class-based component for simple logic**
+
+```glimmer-js
+// app/components/product-card.gjs
+import Component from '@glimmer/component';
+
+export class ProductCard extends Component {
+ // Unnecessary class and overhead for simple formatting
+ formatPrice(price) {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ }).format(price);
+ }
+
+
+
+
{{@product.name}}
+
{{this.formatPrice @product.price}}
+
+
+}
+```
+
+**Correct: template-only component with in-scope functions**
+
+```glimmer-js
+// app/components/product-card.gjs
+function formatPrice(price) {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ }).format(price);
+}
+
+function calculateDiscount(price, discountPercent) {
+ return price * (1 - discountPercent / 100);
+}
+
+function isOnSale(product) {
+ return product.discountPercent > 0;
+}
+
+
+
+
{{@product.name}}
+
+ {{#if (isOnSale @product)}}
+
+ {{formatPrice @product.price}}
+
+ {{formatPrice (calculateDiscount @product.price @product.discountPercent)}}
+
+
+ {{else}}
+
{{formatPrice @product.price}}
+ {{/if}}
+
+
{{@product.description}}
+
+
+```
+
+**When to use class-based vs template-only:**
+
+```glimmer-js
+// Use template-only when:
+// - Simple transformations
+// - Functions accessed once
+// - No state or services needed
+
+function formatDate(date) {
+ return new Date(date).toLocaleDateString();
+}
+
+
+
+ Last updated:
+ {{formatDate @lastUpdate}}
+
+
+```
+
+**Combining in-scope functions for readability:**
+
+```glimmer-js
+// app/components/user-badge.gjs
+function getInitials(name) {
+ return name
+ .split(' ')
+ .map((part) => part[0])
+ .join('')
+ .toUpperCase();
+}
+
+function getBadgeColor(status) {
+ const colors = {
+ active: 'green',
+ pending: 'yellow',
+ inactive: 'gray',
+ };
+ return colors[status] || 'gray';
+}
+
+
+
+ {{getInitials @user.name}}
+ {{@user.name}}
+
+
+```
+
+**Anti-pattern - Complex nested calls:**
+
+```glimmer-js
+// ❌ Hard to read, lots of nesting
+
+
+ {{formatCurrency (multiply (add @basePrice @taxAmount) @quantity)}}
+
+
+
+// ✅ Better - use intermediate function
+function calculateTotal(basePrice, taxAmount, quantity) {
+ return (basePrice + taxAmount) * quantity;
+}
+
+function formatCurrency(amount) {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ }).format(amount);
+}
+
+
+
+ {{formatCurrency (calculateTotal @basePrice @taxAmount @quantity)}}
+
+
+```
+
+**Key differences from class-based components:**
+
+| Aspect | Template-Only | Class-Based |
+
+| ---------------- | ------------------------ | ------------------------ |
+
+| `this` context | ❌ No `this` | ✅ Has `this` |
+
+| Function caching | ❌ Recreated each render | ✅ `@cached` available |
+
+| Services | ❌ Cannot inject | ✅ `@service` decorator |
+
+| Tracked state | ❌ No instance state | ✅ `@tracked` properties |
+
+| Best for | Simple, stateless | Complex, stateful |
+
+**Best practices:**
+
+```glimmer-js
+// app/components/stats-display.gjs
+export function average(numbers) {
+ if (numbers.length === 0) return 0;
+ return numbers.reduce((sum, n) => sum + n, 0) / numbers.length;
+}
+
+export function round(number, decimals = 2) {
+ return Math.round(number * Math.pow(10, decimals)) / Math.pow(10, decimals);
+}
+
+
+
+ Average:
+ {{round (average @scores)}}
+
+
+```
+
+1. **Keep functions simple** - If computation is complex, consider a class with `@cached`
+
+2. **One responsibility per function** - Makes them reusable and testable
+
+3. **Minimize nesting** - Use intermediate functions for readability
+
+4. **No side effects** - Functions should be pure transformations
+
+5. **Export for testing** - Export functions so they can be tested independently
+
+Reference: [https://guides.emberjs.com/release/components/component-types/](https://guides.emberjs.com/release/components/component-types/), [https://guides.emberjs.com/release/components/conditional-content/](https://guides.emberjs.com/release/components/conditional-content/)
+
+### 6.7 Use {{#each}} with @key for Lists
+
+**Impact: MEDIUM (50-100% faster list updates)**
+
+Use the `key=` parameter with `{{#each}}` when objects are recreated between renders (e.g., via `.map()` or fresh API data). The default behavior uses object identity (`@identity`), which works when object references are stable.
+
+**Incorrect: no key**
+
+```glimmer-js
+// app/components/user-list.gjs
+import UserCard from './user-card';
+
+
+
+ {{#each this.users as |user|}}
+
+
+
+ {{/each}}
+
+
+```
+
+**Correct: with key**
+
+```glimmer-js
+// app/components/user-list.gjs
+import UserCard from './user-card';
+
+
+
+ {{#each this.users key="id" as |user|}}
+
+
+
+ {{/each}}
+
+
+```
+
+**For arrays of primitives: strings, numbers**
+
+```glimmer-js
+// app/components/tag-list.gjs
+
+ {{! @identity is implicit, no need to write it }}
+ {{#each this.tags as |tag|}}
+ {{tag}}
+ {{/each}}
+
+```
+
+`@identity` is the default, so you rarely need to specify it explicitly. It compares items by value for primitives.
+
+**For complex scenarios with @index:**
+
+```glimmer-js
+// app/components/item-list.gjs
+
+ {{#each this.items key="@index" as |item index|}}
+
+ {{item.name}}
+
+ {{/each}}
+
+```
+
+Using proper keys allows Ember's rendering engine to efficiently update, reorder, and remove items without re-rendering the entire list.
+
+**When to use `key=`:**
+
+- Objects recreated between renders (`.map()`, generators, fresh API responses) → use `key="id"` or similar
+
+- High-frequency updates (animations, real-time data) → always specify a key
+
+- Stable object references (Apollo cache, Ember Data) → default `@identity` is fine
+
+- Items never reorder → `key="@index"` is acceptable
+
+**Performance comparison: dbmon benchmark, 40 rows at 60fps**
+
+- Without key (objects recreated): Destroys/recreates DOM every frame
+
+- With `key="data.db.id"`: DOM reuse, **2x FPS improvement**
+
+- [Ember API: each helper](https://api.emberjs.com/ember/release/classes/Ember.Templates.helpers/methods/each)
+
+- [Ember template lint: equire-each-key](https://github.com/ember-template-lint/ember-template-lint/blob/main/docs/rule/require-each-key.md)
+
+- [Example PR showing the fps improvement on updated lists](https://github.com/universal-ember/table/pull/68)
+
+### 6.8 Use {{#let}} to Avoid Recomputation
+
+**Impact: MEDIUM (30-50% reduction in duplicate work)**
+
+Use `{{#let}}` to compute expensive values once and reuse them in the template instead of calling getters or helpers multiple times.
+
+**Incorrect: recomputes on every reference**
+
+```glimmer-js
+// app/components/user-card.gjs
+
+
+ {{#if (and this.user.isActive (not this.user.isDeleted))}}
+
{{this.user.fullName}}
+
Status: Active
+ {{/if}}
+
+ {{#if (and this.user.isActive (not this.user.isDeleted))}}
+
Edit
+ {{/if}}
+
+ {{#if (and this.user.isActive (not this.user.isDeleted))}}
+
Delete
+ {{/if}}
+
+
+```
+
+**Correct: compute once, reuse**
+
+```glimmer-js
+// app/components/user-card.gjs
+
+ {{#let (and this.user.isActive (not this.user.isDeleted)) as |isEditable|}}
+
+ {{#if isEditable}}
+
{{this.user.fullName}}
+
Status: Active
+ {{/if}}
+
+ {{#if isEditable}}
+
Edit
+ {{/if}}
+
+ {{#if isEditable}}
+
Delete
+ {{/if}}
+
+ {{/let}}
+
+```
+
+**Multiple values:**
+
+```glimmer-js
+// app/components/checkout.gjs
+
+ {{#let
+ (this.calculateTotal this.items) (this.formatCurrency this.total) (this.hasDiscount this.user)
+ as |total formattedTotal showDiscount|
+ }}
+
+
Total: {{formattedTotal}}
+
+ {{#if showDiscount}}
+
Original: {{total}}
+
Discount Applied!
+ {{/if}}
+
+ {{/let}}
+
+```
+
+`{{#let}}` computes values once and caches them for the block scope, reducing redundant calculations.
+
+### 6.9 Use {{fn}} for Partial Application Only
+
+**Impact: LOW-MEDIUM (Clearer code, avoid unnecessary wrapping)**
+
+The `{{fn}}` helper is used for partial application (binding arguments), similar to JavaScript's `.bind()`. Only use it when you need to pre-bind arguments to a function. Don't use it to simply pass a function reference.
+
+**Incorrect: unnecessary use of {{fn}}**
+
+```glimmer-js
+// app/components/search.gjs
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+class Search extends Component {
+ @action
+ handleSearch(event) {
+ console.log('Searching:', event.target.value);
+ }
+
+
+ {{! Wrong - no arguments being bound}}
+
+
+}
+```
+
+**Correct: direct function reference**
+
+```glimmer-js
+// app/components/search.gjs
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+class Search extends Component {
+ @action
+ handleSearch(event) {
+ console.log('Searching:', event.target.value);
+ }
+
+
+ {{! Correct - pass function directly}}
+
+
+}
+```
+
+**When to Use {{fn}} - Partial Application:**
+
+```glimmer-js
+// app/components/user-list.gjs
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+class UserList extends Component {
+ @action
+ deleteUser(userId, event) {
+ console.log('Deleting user:', userId);
+ this.args.onDelete(userId);
+ }
+
+
+
+ {{#each @users as |user|}}
+
+ {{user.name}}
+ {{! Correct - binding user.id as first argument}}
+
+ Delete
+
+
+ {{/each}}
+
+
+}
+```
+
+Use `{{fn}}` when you need to pre-bind arguments to a function, similar to JavaScript's `.bind()`:
+
+**Multiple Arguments:**
+
+```glimmer-js
+// app/components/data-grid.gjs
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+class DataGrid extends Component {
+ @action
+ updateCell(rowId, columnKey, event) {
+ const newValue = event.target.value;
+ this.args.onUpdate(rowId, columnKey, newValue);
+ }
+
+
+ {{#each @rows as |row|}}
+ {{#each @columns as |column|}}
+
+ {{/each}}
+ {{/each}}
+
+}
+```
+
+**Think of {{fn}} like .bind():**
+
+```javascript
+// JavaScript comparison
+const boundFn = this.deleteUser.bind(this, userId); // .bind() pre-binds args
+// Template equivalent: {{fn this.deleteUser userId}}
+
+// Direct reference
+const directFn = this.handleSearch; // No pre-binding
+// Template equivalent: {{this.handleSearch}}
+```
+
+**Common Patterns:**
+
+```javascript
+// ❌ Wrong - no partial application
+Save
+
+// ✅ Correct - direct reference
+Save
+
+// ✅ Correct - partial application with argument
+Save Draft
+
+// ❌ Wrong - no partial application
+
+
+// ✅ Correct - direct reference
+
+
+// ✅ Correct - partial application with field name
+
+```
+
+Only use `{{fn}}` when you're binding arguments. For simple function references, pass them directly.
+
+Reference: [https://guides.emberjs.com/release/components/template-lifecycle-dom-and-modifiers/#toc_passing-arguments-to-functions](https://guides.emberjs.com/release/components/template-lifecycle-dom-and-modifiers/#toc_passing-arguments-to-functions)
+
+### 6.10 Use Helper Libraries Effectively
+
+**Impact: MEDIUM (Reduces custom helper maintenance and keeps templates concise)**
+
+Leverage community helper libraries to write cleaner templates and avoid creating unnecessary custom helpers for common operations.
+
+Reinventing common functionality with custom helpers adds maintenance burden and bundle size when well-maintained helper libraries already provide the needed functionality.
+
+**Incorrect:**
+
+```glimmer-js
+// app/utils/is-equal.js - Unnecessary custom helper
+export function isEqual(a, b) {
+ return a === b;
+}
+
+// app/components/user-badge.gjs
+import { isEqual } from '../utils/is-equal';
+
+class UserBadge extends Component {
+
+ {{#if (isEqual @user.role "admin")}}
+ Admin
+ {{/if}}
+
+}
+```
+
+**Note:** These helpers will be built into Ember 7 core, but currently require installing the respective addon packages.
+
+**Installation:**
+
+```bash
+npm install ember-truth-helpers ember-composable-helpers
+```
+
+Use helper libraries like `ember-truth-helpers` and `ember-composable-helpers`:
+
+**Correct:**
+
+```glimmer-js
+// app/components/conditional-inline.gjs
+import Component from '@glimmer/component';
+import { if as ifHelper } from '@ember/helper'; // Built-in to Ember
+
+class ConditionalInline extends Component {
+
+ {{! Ternary-like behavior }}
+
+ {{@user.name}}
+
+
+ {{! Conditional attribute }}
+
+ {{ifHelper @isProcessing "Processing..." "Submit"}}
+
+
+ {{! With default value }}
+ {{ifHelper @description @description "No description provided"}}
+
+}
+```
+
+**Installation:** `npm install ember-truth-helpers`
+
+**Installation:** `npm install ember-composable-helpers`
+
+**Dynamic Classes:**
+
+```glimmer-js
+// app/components/dynamic-classes.gjs
+import Component from '@glimmer/component';
+import { concat, if as ifHelper } from '@ember/helper'; // Built-in to Ember
+import { and, not } from 'ember-truth-helpers';
+
+class DynamicClasses extends Component {
+
+
+
{{@title}}
+
+
+}
+```
+
+**List Filtering:**
+
+```glimmer-js
+// app/components/user-profile-card.gjs
+import Component from '@glimmer/component';
+import { concat, if as ifHelper, fn } from '@ember/helper'; // Built-in to Ember
+import { eq, not, and, or } from 'ember-truth-helpers';
+import { hash, array, get } from 'ember-composable-helpers/helpers';
+import { on } from '@ember/modifier';
+
+class UserProfileCard extends Component {
+ updateField = (field, value) => {
+ this.args.onUpdate(field, value);
+ };
+
+
+
+
{{concat @user.firstName " " @user.lastName}}
+
+ {{#if (or (eq @user.role "admin") (eq @user.role "moderator"))}}
+
+ {{get (hash admin="Administrator" moderator="Moderator") @user.role}}
+
+ {{/if}}
+
+ {{#if (and @canEdit (not @user.locked))}}
+
+ {{#each (array "profile" "settings" "privacy") as |section|}}
+
+ Edit
+ {{section}}
+
+ {{/each}}
+
+ {{/if}}
+
+
+ {{ifHelper @user.bio @user.bio "No bio provided"}}
+
+
+
+}
+```
+
+- **Library helpers**: ~0% overhead (compiled into efficient bytecode)
+
+- **Custom helpers**: 5-15% overhead per helper call
+
+- **Inline logic**: Cleaner templates, better tree-shaking
+
+- **Library helpers**: For all common operations (equality, logic, arrays, strings)
+
+- **Custom helpers**: Only for domain-specific logic not covered by library helpers
+
+- **Component logic**: For complex operations that need @cached or multiple dependencies
+
+**Note:** These helpers will be built into Ember 7 core. Until then:
+
+**Actually Built-in to Ember (from `@ember/helper`):**
+
+- `concat` - Concatenate strings
+
+- `fn` - Partial application / bind arguments
+
+- `if` - Ternary-like conditional value
+
+- `mut` - Create settable binding (use sparingly)
+
+**From `ember-truth-helpers` package:**
+
+- `eq` - Equality (===)
+
+- `not` - Negation (!)
+
+- `and` - Logical AND
+
+- `or` - Logical OR
+
+- `lt`, `lte`, `gt`, `gte` - Numeric comparisons
+
+**From `ember-composable-helpers` package:**
+
+- `array` - Create array inline
+
+- `hash` - Create object inline
+
+- `get` - Dynamic property access
+
+- [Ember Built-in Helpers](https://guides.emberjs.com/release/templates/built-in-helpers/)
+
+- [Template Helpers API](https://api.emberjs.com/ember/release/modules/@ember%2Fhelper)
+
+- [fn Helper Guide](https://guides.emberjs.com/release/components/helper-functions/)
+
+- [ember-truth-helpers](https://github.com/jmurphyau/ember-truth-helpers)
+
+- [ember-composable-helpers](https://github.com/DockYard/ember-composable-helpers)
+
+---
+
+## 7. Performance Optimization
+
+**Impact: MEDIUM**
+
+Performance-focused rendering and event handling patterns help reduce unnecessary work in hot UI paths.
+
+### 7.1 Use {{on}} Modifier Instead of Event Handler Properties
+
+**Impact: MEDIUM (Better performance and clearer event handling)**
+
+Always use the `{{on}}` modifier for event handling instead of HTML event handler properties. The `{{on}}` modifier provides better memory management, automatic cleanup, and clearer intent.
+
+**Why {{on}} is Better:**
+
+- Automatic cleanup when element is removed (prevents memory leaks)
+
+- Supports event options (`capture`, `passive`, `once`)
+
+- More explicit and searchable in templates
+
+**Incorrect: HTML event properties**
+
+```glimmer-js
+// app/components/button.gjs
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+export default class Button extends Component {
+ @action
+ handleClick() {
+ console.log('clicked');
+ }
+
+
+
+ Click Me
+
+
+}
+```
+
+**Correct: {{on}} modifier**
+
+```glimmer-js
+// app/components/scrollable.gjs
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { on } from '@ember/modifier';
+
+export default class Scrollable extends Component {
+ @action
+ handleScroll(event) {
+ console.log('scrolled', event.target.scrollTop);
+ }
+
+
+ {{! passive: true improves scroll performance }}
+
+ {{yield}}
+
+
+}
+```
+
+The `{{on}}` modifier supports standard event listener options:
+
+**Available options:**
+
+```glimmer-js
+// app/components/todo-list.gjs
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { on } from '@ember/modifier';
+
+export default class TodoList extends Component {
+ @action
+ handleClick(event) {
+ // Find which todo was clicked
+ const todoId = event.target.closest('[data-todo-id]')?.dataset.todoId;
+ if (todoId) {
+ this.args.onTodoClick?.(todoId);
+ }
+ }
+
+
+ {{! Single listener for all todos - better than one per item }}
+
+ {{#each @todos as |todo|}}
+
+ {{todo.title}}
+
+ {{/each}}
+
+
+}
+```
+
+- `capture` - Use capture phase instead of bubble phase
+
+- `once` - Remove listener after first invocation
+
+- `passive` - Indicates handler won't call `preventDefault()` (better scroll performance)
+
+Handle these in your action, not in the template:
+
+For lists with many items, use event delegation on the parent:
+
+**❌ Don't bind directly without @action:**
+
+```glimmer-js
+// This won't work - loses 'this' context
+Bad
+```
+
+**✅ Use @action decorator:**
+
+```glimmer-js
+@action
+myMethod() {
+ // 'this' is correctly bound
+}
+
+Good
+```
+
+**❌ Don't use string event handlers:**
+
+```glimmer-js
+{{! Security risk and doesn't work in strict mode }}
+Bad
+```
+
+Always use the `{{on}}` modifier for cleaner, safer, and more performant event handling in Ember applications.
+
+**References:**
+
+- [Ember Modifiers Guide](https://guides.emberjs.com/release/components/template-lifecycle-dom-and-modifiers/)
+
+- [{{on}} Modifier RFC](https://github.com/emberjs/rfcs/blob/master/text/0471-on-modifier.md)
+
+- [Event Listener Options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#parameters)
+
+---
+
+## 8. Testing Best Practices
+
+**Impact: MEDIUM**
+
+Modern testing patterns, waiters, and abstraction utilities improve test reliability and maintainability.
+
+### 8.1 MSW (Mock Service Worker) Setup for Testing
+
+**Impact: HIGH (Proper API mocking without ORM complexity)**
+
+Use MSW (Mock Service Worker) for API mocking in tests. MSW provides a cleaner approach than Mirage by intercepting requests at the network level without introducing unnecessary ORM patterns or abstractions.
+
+Create a test helper to set up MSW in your tests:
+
+MSW works in integration tests too:
+
+1. **Define handlers per test** - Use `server.use()` in individual tests rather than global handlers
+
+2. **Reset between tests** - The helper automatically resets handlers after each test
+
+3. **Use JSON:API format** - Keep responses consistent with your API format
+
+4. **Test error states** - Mock various HTTP error codes (400, 401, 403, 404, 500)
+
+5. **Capture requests** - Use the request object to verify what your app sent
+
+6. **Use fixtures** - Create reusable test data to keep tests DRY
+
+7. **Simulate delays** - Test loading states with artificial delays
+
+8. **Type-safe responses** - In TypeScript, type your response payloads
+
+**Incorrect: using Mirage with ORM complexity**
+
+```javascript
+// mirage/config.js
+export default function () {
+ this.namespace = '/api';
+
+ // Complex schema and factories
+ this.get('/users', (schema) => {
+ return schema.users.all();
+ });
+
+ // Need to maintain schema, factories, serializers
+ this.post('/users', (schema, request) => {
+ let attrs = JSON.parse(request.requestBody);
+ return schema.users.create(attrs);
+ });
+}
+```
+
+**Correct: using MSW with simple network mocking**
+
+```javascript
+// tests/helpers/msw.js
+import { http, HttpResponse } from 'msw';
+
+// Simple request/response mocking
+export const handlers = [
+ http.get('/api/users', () => {
+ return HttpResponse.json([
+ { id: 1, name: 'Alice' },
+ { id: 2, name: 'Bob' },
+ ]);
+ }),
+
+ http.post('/api/users', async ({ request }) => {
+ const user = await request.json();
+ return HttpResponse.json({ id: 3, ...user }, { status: 201 });
+ }),
+];
+```
+
+**Why MSW over Mirage:**
+
+- **Better conventions** - Mock at the network level, not with an ORM
+
+- **Simpler mental model** - Define request handlers, return responses
+
+- **Doesn't lead developers astray** - No schema migrations or factories to maintain
+
+- **Works everywhere** - Same mocks work in tests, Storybook, and development
+
+- **More realistic** - Actually intercepts fetch/XMLHttpRequest
+
+**Default handlers for all tests:**
+
+```javascript
+// tests/helpers/msw.js
+const defaultHandlers = [
+ // Always return current user
+ http.get('/api/current-user', () => {
+ return HttpResponse.json({
+ data: {
+ id: '1',
+ type: 'user',
+ attributes: { name: 'Test User', role: 'admin' },
+ },
+ });
+ }),
+];
+```
+
+**One-time handlers (don't persist):**
+
+```javascript
+// MSW handlers persist until resetHandlers() is called
+// The test helper automatically resets after each test
+// For a one-time handler within a test, manually reset:
+test('one-time response', async function (assert) {
+ server.use(
+ http.get('/api/special', () => {
+ return HttpResponse.json({ data: 'special' });
+ }),
+ );
+
+ // First request gets mocked response
+ await visit('/special');
+ assert.dom('[data-test-data]').hasText('special');
+
+ // Reset to remove this handler
+ server.resetHandlers();
+
+ // Subsequent requests will use default handlers or be unhandled
+});
+```
+
+**Conditional responses:**
+
+```javascript
+http.post('/api/login', async ({ request }) => {
+ const { email, password } = await request.json();
+
+ if (email === 'test@example.com' && password === 'password') {
+ return HttpResponse.json({
+ data: { token: 'abc123' },
+ });
+ }
+
+ return HttpResponse.json({ errors: [{ title: 'Invalid credentials' }] }, { status: 401 });
+});
+```
+
+If migrating from Mirage:
+
+1. Remove `ember-cli-mirage` dependency
+
+2. Delete `mirage/` directory (models, factories, scenarios)
+
+3. Install MSW: `npm install --save-dev msw`
+
+4. Create the MSW test helper (see above)
+
+5. Replace `setupMirage(hooks)` with `setupMSW(hooks)`
+
+6. Convert Mirage handlers:
+
+ - `this.server.get()` → `http.get()`
+
+ - `this.server.create()` → Return inline JSON
+
+ - `this.server.createList()` → Return array of JSON objects
+
+**Before: Mirage**
+
+```javascript
+test('lists posts', async function (assert) {
+ this.server.createList('post', 3);
+ await visit('/posts');
+ assert.dom('[data-test-post]').exists({ count: 3 });
+});
+```
+
+**After: MSW**
+
+```javascript
+test('lists posts', async function (assert) {
+ server.use(
+ http.get('/api/posts', () => {
+ return HttpResponse.json({
+ data: [
+ { id: '1', type: 'post', attributes: { title: 'Post 1' } },
+ { id: '2', type: 'post', attributes: { title: 'Post 2' } },
+ { id: '3', type: 'post', attributes: { title: 'Post 3' } },
+ ],
+ });
+ }),
+ );
+ await visit('/posts');
+ assert.dom('[data-test-post]').exists({ count: 3 });
+});
+```
+
+Reference: [https://discuss.emberjs.com/t/my-cookbook-for-various-emberjs-things/19679](https://discuss.emberjs.com/t/my-cookbook-for-various-emberjs-things/19679), [https://mswjs.io/docs/](https://mswjs.io/docs/)
+
+### 8.2 Provide DOM-Abstracted Test Utilities for Library Components
+
+**Impact: MEDIUM (Stabilizes consumer tests against internal DOM refactors)**
+
+When building reusable components or libraries, consumers should not need to know implementation details or interact directly with the component's DOM. DOM structure should be considered **private** unless the author of the tests is the **owner** of the code being tested.
+
+Without abstracted test utilities:
+
+- Component refactoring breaks consumer tests
+
+- Tests are tightly coupled to implementation details
+
+- Teams waste time updating tests when internals change
+
+- Testing becomes fragile and maintenance-heavy
+
+**Library authors should provide test utilities that fully abstract the DOM.** These utilities expose a public API for testing that remains stable even when internal implementation changes.
+
+**Incorrect: exposing DOM to consumers**
+
+```glimmer-js
+// Consumer's test - tightly coupled to DOM
+import { render, click } from '@ember/test-helpers';
+import { DataGrid } from 'my-library';
+
+test('sorting works', async function (assert) {
+ await render( );
+
+ // Fragile: breaks if class names or structure change
+ await click('.data-grid__header .sort-button[data-column="name"]');
+
+ assert.dom('.data-grid__row:first-child').hasText('Alice');
+});
+```
+
+**Problems:**
+
+- Consumer knows about `.data-grid__header`, `.sort-button`, `[data-column]`
+
+- Refactoring component structure breaks consumer tests
+
+- No clear public API for testing
+
+**Correct: providing DOM-abstracted test utilities**
+
+```glimmer-js
+// Consumer's test - abstracted from DOM
+import { render } from '@ember/test-helpers';
+import { DataGrid } from 'my-library';
+import { getDataGrid } from 'my-library/test-support';
+
+test('sorting works', async function (assert) {
+ await render( );
+
+ const grid = getDataGrid();
+
+ // Clean API: no DOM knowledge required
+ await grid.sortBy('name');
+
+ assert.strictEqual(grid.getRow(0), 'Alice');
+ assert.deepEqual(grid.getRows(), ['Alice', 'Bob', 'Charlie']);
+});
+```
+
+**Benefits:**
+
+```javascript
+// addon/test-support/modal.js
+import { click, find, waitUntil } from '@ember/test-helpers';
+
+export class ModalTestHelper {
+ constructor(container = document) {
+ this.container = container;
+ }
+
+ get element() {
+ return find('[data-test-modal]', this.container);
+ }
+
+ isOpen() {
+ return this.element !== null;
+ }
+
+ async waitForOpen() {
+ await waitUntil(() => this.isOpen(), { timeout: 1000 });
+ }
+
+ async waitForClose() {
+ await waitUntil(() => !this.isOpen(), { timeout: 1000 });
+ }
+
+ getTitle() {
+ const titleEl = find('[data-test-modal-title]', this.element);
+ return titleEl ? titleEl.textContent.trim() : null;
+ }
+
+ getBody() {
+ const bodyEl = find('[data-test-modal-body]', this.element);
+ return bodyEl ? bodyEl.textContent.trim() : null;
+ }
+
+ async close() {
+ if (!this.isOpen()) {
+ throw new Error('Cannot close modal: modal is not open');
+ }
+ await click('[data-test-modal-close]', this.element);
+ }
+
+ async clickButton(buttonText) {
+ const buttons = findAll('[data-test-modal-button]', this.element);
+ const button = buttons.find((btn) => btn.textContent.trim() === buttonText);
+ if (!button) {
+ const available = buttons.map((b) => b.textContent.trim()).join(', ');
+ throw new Error(`Button "${buttonText}" not found. Available: ${available}`);
+ }
+ await click(button);
+ }
+}
+
+export function getModal(container) {
+ return new ModalTestHelper(container);
+}
+```
+
+- Component internals can change without breaking consumer tests
+
+- Clear, documented testing API
+
+- Consumer tests are declarative and readable
+
+- Library maintains API stability contract
+
+On projects with teams, DOM abstraction prevents:
+
+- Merge conflicts from test changes
+
+- Cross-team coordination overhead
+
+- Broken tests from uncoordinated refactoring
+
+- Knowledge silos about component internals
+
+For solo projects, the benefit is smaller but still valuable:
+
+- Easier refactoring without test maintenance
+
+- Better separation of concerns
+
+- Professional API design practice
+
+**Before:** ~30-50% of test maintenance time spent updating selectors
+
+**After:** Minimal test maintenance when refactoring components
+
+- **component-avoid-classes-in-examples.md** - Avoid exposing implementation details
+
+- **testing-modern-patterns.md** - Modern testing approaches
+
+- **testing-render-patterns.md** - Component testing patterns
+
+- [Testing Best Practices - ember-learn](https://guides.emberjs.com/release/testing/)
+
+- [ember-test-selectors](https://github.com/mainmatter/ember-test-selectors) - Addon for stripping test selectors from production
+
+- [Page Objects Pattern](https://martinfowler.com/bliki/PageObject.html) - Related testing abstraction pattern
+
+### 8.3 Use Appropriate Render Patterns in Tests
+
+**Impact: MEDIUM (Simpler test code and better readability)**
+
+Choose the right rendering pattern based on whether your component needs arguments, blocks, or attributes in the test.
+
+**Incorrect: using template tag unnecessarily**
+
+```javascript
+// tests/integration/components/loading-spinner-test.js
+import { render } from '@ember/test-helpers';
+import LoadingSpinner from 'my-app/components/loading-spinner';
+
+test('it renders', async function (assert) {
+ // ❌ Unnecessary template wrapper for component with no args
+ await render(
+
+
+ ,
+ );
+
+ assert.dom('[data-test-spinner]').exists();
+});
+```
+
+**Correct: direct component render when no args needed**
+
+```glimmer-js
+// tests/integration/components/user-card-test.js
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import UserCard from 'my-app/components/user-card';
+
+module('Integration | Component | user-card', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders with arguments', async function (assert) {
+ const user = { name: 'John Doe', email: 'john@example.com' };
+
+ // ✅ Use template tag when passing arguments
+ await render( );
+
+ assert.dom('[data-test-user-name]').hasText('John Doe');
+ });
+
+ test('it renders with block content', async function (assert) {
+ // ✅ Use template tag when providing blocks
+ await render(
+
+
+ <:header>Custom Header
+ <:body>Custom Content
+
+ ,
+ );
+
+ assert.dom('[data-test-header]').hasText('Custom Header');
+ assert.dom('[data-test-body]').hasText('Custom Content');
+ });
+
+ test('it renders with HTML attributes', async function (assert) {
+ // ✅ Use template tag when passing HTML attributes
+ await render( );
+
+ assert.dom('[data-test-featured]').exists();
+ assert.dom('[data-test-featured]').hasClass('featured');
+ });
+});
+```
+
+**Pattern 1: Direct component render (no args/blocks/attributes):**
+
+**Pattern 2: Template tag render (with args/blocks/attributes):**
+
+**Complete example showing both patterns:**
+
+```glimmer-js
+// tests/integration/components/button-test.js
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, click } from '@ember/test-helpers';
+import Button from 'my-app/components/button';
+
+module('Integration | Component | button', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders default button', async function (assert) {
+ // ✅ No args needed - use direct render
+ await render(Button);
+
+ assert.dom('button').exists();
+ assert.dom('button').hasText('Click me');
+ });
+
+ test('it renders with custom text', async function (assert) {
+ // ✅ Needs block content - use template tag
+ await render(
+
+ Submit Form
+ ,
+ );
+
+ assert.dom('button').hasText('Submit Form');
+ });
+
+ test('it handles click action', async function (assert) {
+ assert.expect(1);
+
+ const handleClick = () => {
+ assert.ok(true, 'Click handler called');
+ };
+
+ // ✅ Needs argument - use template tag
+ await render(
+
+ Click me
+ ,
+ );
+
+ await click('button');
+ });
+
+ test('it applies variant styling', async function (assert) {
+ // ✅ Needs argument - use template tag
+ await render(
+
+ Primary Button
+ ,
+ );
+
+ assert.dom('button').hasClass('btn-primary');
+ });
+});
+```
+
+**Testing template-only components:**
+
+```glimmer-js
+// tests/integration/components/icon-test.js
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import Icon from 'my-app/components/icon';
+
+module('Integration | Component | icon', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders default icon', async function (assert) {
+ // ✅ Template-only component with no args - use direct render
+ await render(Icon);
+
+ assert.dom('[data-test-icon]').exists();
+ });
+
+ test('it renders specific icon', async function (assert) {
+ // ✅ Needs @name argument - use template tag
+ await render( );
+
+ assert.dom('[data-test-icon]').hasAttribute('data-icon', 'check');
+ assert.dom('[data-test-icon]').hasClass('icon-large');
+ });
+});
+```
+
+**Decision guide:**
+
+| Scenario | Pattern | Example |
+
+| ----------------------------------- | ---------------------------------- | ----------------------------------------------------------- |
+
+| No arguments, blocks, or attributes | `render(Component)` | `render(LoadingSpinner)` |
+
+| Component needs arguments | `render(... )` | `render( )` |
+
+| Component receives block content | `render(... )` | `render(Content )` |
+
+| Component needs HTML attributes | `render(... )` | `render( )` |
+
+| Multiple test context properties | `render(... )` | `render( )` |
+
+**Why this matters:**
+
+- **Simplicity**: Direct render reduces boilerplate for simple cases
+
+- **Clarity**: Template syntax makes data flow explicit when needed
+
+- **Consistency**: Clear pattern helps teams write maintainable tests
+
+- **Type Safety**: Both patterns work with TypeScript for component types
+
+**Common patterns:**
+
+```glimmer-js
+// ✅ Simple component, no setup needed
+await render(LoadingSpinner);
+await render(Divider);
+await render(Logo);
+
+// ✅ Component with arguments from test context
+await render(
+ ,
+);
+
+// ✅ Component with named blocks
+await render(
+
+
+ <:header>Title
+ <:body>Content
+ <:footer>Close
+
+ ,
+);
+
+// ✅ Component with splattributes
+await render(
+
+
+ Card content
+
+ ,
+);
+```
+
+Using the appropriate render pattern keeps tests clean and expressive.
+
+Reference: [https://guides.emberjs.com/release/testing/](https://guides.emberjs.com/release/testing/)
+
+### 8.4 Use Modern Testing Patterns
+
+**Impact: HIGH (Better test coverage and maintainability)**
+
+Use modern Ember testing patterns with `@ember/test-helpers` and `qunit-dom` for better test coverage and maintainability.
+
+**Incorrect: old testing patterns**
+
+```glimmer-js
+// tests/integration/components/user-card-test.js
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find, click } from '@ember/test-helpers';
+import UserCard from 'my-app/components/user-card';
+
+module('Integration | Component | user-card', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ await render( );
+
+ // Using find() instead of qunit-dom
+ assert.ok(find('.user-card'));
+ });
+});
+```
+
+**Correct: modern testing patterns**
+
+```glimmer-js
+// tests/integration/components/user-card-test.js
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, click } from '@ember/test-helpers';
+import { setupIntl } from 'ember-intl/test-support';
+import UserCard from 'my-app/components/user-card';
+
+module('Integration | Component | user-card', function (hooks) {
+ setupRenderingTest(hooks);
+ setupIntl(hooks);
+
+ test('it renders user information', async function (assert) {
+ const user = {
+ name: 'John Doe',
+ email: 'john@example.com',
+ avatarUrl: '/avatar.jpg',
+ };
+
+ await render( );
+
+ // qunit-dom assertions
+ assert.dom('[data-test-user-name]').hasText('John Doe');
+ assert.dom('[data-test-user-email]').hasText('john@example.com');
+ assert
+ .dom('[data-test-user-avatar]')
+ .hasAttribute('src', '/avatar.jpg')
+ .hasAttribute('alt', 'John Doe');
+ });
+
+ test('it handles edit action', async function (assert) {
+ assert.expect(1);
+
+ const user = { name: 'John Doe', email: 'john@example.com' };
+ const handleEdit = (editedUser) => {
+ assert.deepEqual(editedUser, user, 'Edit handler called with user');
+ };
+
+ await render( );
+
+ await click('[data-test-edit-button]');
+ });
+});
+```
+
+**Component testing with reactive state:**
+
+```glimmer-ts
+// tests/integration/components/search-box-test.ts
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, fillIn } from '@ember/test-helpers';
+import { trackedObject } from '@ember/reactive/collections';
+import SearchBox from 'my-app/components/search-box';
+
+module('Integration | Component | search-box', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it performs search', async function (assert) {
+ // Use trackedObject for reactive state in tests
+ const state = trackedObject({
+ results: [] as string[],
+ });
+
+ const handleSearch = (query: string) => {
+ state.results = [`Result for ${query}`];
+ };
+
+ await render(
+
+
+
+ {{#each state.results as |result|}}
+ {{result}}
+ {{/each}}
+
+ ,
+ );
+
+ await fillIn('[data-test-search-input]', 'ember');
+
+ // State updates reactively - no waitFor needed when using test-waiters
+ assert.dom('[data-test-results] li').hasText('Result for ember');
+ });
+});
+```
+
+**Testing with ember-concurrency tasks:**
+
+```glimmer-js
+// tests/integration/components/async-button-test.js
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, click } from '@ember/test-helpers';
+import AsyncButton from 'my-app/components/async-button';
+
+module('Integration | Component | async-button', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it shows loading state during task execution', async function (assert) {
+ let resolveTask;
+ const onSave = () => {
+ return new Promise((resolve) => {
+ resolveTask = resolve;
+ });
+ };
+
+ await render(
+
+
+ Save
+
+ ,
+ );
+
+ // Trigger the task
+ await click('[data-test-button]');
+
+ // ember-concurrency automatically registers test waiters
+ // The button will be disabled while the task runs
+ assert.dom('[data-test-button]').hasAttribute('disabled');
+ assert.dom('[data-test-loading-spinner]').hasText('Saving...');
+
+ // Resolve the task
+ resolveTask();
+ // No need to call settled() - ember-concurrency's test waiters handle this
+
+ assert.dom('[data-test-button]').doesNotHaveAttribute('disabled');
+ assert.dom('[data-test-loading-spinner]').doesNotExist();
+ assert.dom('[data-test-button]').hasText('Save');
+ });
+});
+```
+
+**When to use test-waiters with ember-concurrency:**
+
+- **ember-concurrency auto-registers test waiters** - You don't need to manually register test waiters for ember-concurrency tasks. The library automatically waits for tasks to complete before test helpers like `click()`, `fillIn()`, etc. resolve.
+
+- **You still need test-waiters when:**
+
+ - Using raw Promises outside of ember-concurrency tasks
+
+ - Working with third-party async operations that don't integrate with Ember's test waiter system
+
+ - Creating custom async behavior that needs to pause test execution
+
+- **You DON'T need additional test-waiters when:**
+
+ - Using ember-concurrency tasks (already handled)
+
+ - Using Ember Data operations (already handled)
+
+ - Using `settled()` from `@ember/test-helpers` (already coordinates with test waiters)
+
+ - **Note**: `waitFor()` and `waitUntil()` from `@ember/test-helpers` are code smells - if you need them, it indicates missing test-waiters in your code. Instrument your async operations with test-waiters instead.
+
+**Route testing with MSW: Mock Service Worker**
+
+```javascript
+// tests/acceptance/posts-test.js
+import { module, test } from 'qunit';
+import { visit, currentURL, click } from '@ember/test-helpers';
+import { setupApplicationTest } from 'ember-qunit';
+import { http, HttpResponse } from 'msw';
+import { setupMSW } from 'my-app/tests/helpers/msw';
+
+module('Acceptance | posts', function (hooks) {
+ setupApplicationTest(hooks);
+ const { server } = setupMSW(hooks);
+
+ test('visiting /posts', async function (assert) {
+ server.use(
+ http.get('/api/posts', () => {
+ return HttpResponse.json({
+ data: [
+ { id: '1', type: 'post', attributes: { title: 'Post 1' } },
+ { id: '2', type: 'post', attributes: { title: 'Post 2' } },
+ { id: '3', type: 'post', attributes: { title: 'Post 3' } },
+ ],
+ });
+ }),
+ );
+
+ await visit('/posts');
+
+ assert.strictEqual(currentURL(), '/posts');
+ assert.dom('[data-test-post-item]').exists({ count: 3 });
+ });
+
+ test('clicking a post navigates to detail', async function (assert) {
+ server.use(
+ http.get('/api/posts', () => {
+ return HttpResponse.json({
+ data: [{ id: '1', type: 'post', attributes: { title: 'Test Post', slug: 'test-post' } }],
+ });
+ }),
+ http.get('/api/posts/test-post', () => {
+ return HttpResponse.json({
+ data: { id: '1', type: 'post', attributes: { title: 'Test Post', slug: 'test-post' } },
+ });
+ }),
+ );
+
+ await visit('/posts');
+ await click('[data-test-post-item]:first-child');
+
+ assert.strictEqual(currentURL(), '/posts/test-post');
+ assert.dom('[data-test-post-title]').hasText('Test Post');
+ });
+});
+```
+
+**Note:** Use MSW (Mock Service Worker) for API mocking instead of Mirage. MSW provides better conventions and doesn't lead developers astray. See `testing-msw-setup.md` for detailed setup instructions.
+
+**Accessibility testing:**
+
+```glimmer-js
+// tests/integration/components/modal-test.js
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, click } from '@ember/test-helpers';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
+import Modal from 'my-app/components/modal';
+
+module('Integration | Component | modal', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it passes accessibility audit', async function (assert) {
+ await render(
+
+
+ Modal content
+
+ ,
+ );
+
+ await a11yAudit();
+ assert.ok(true, 'no a11y violations');
+ });
+
+ test('it traps focus', async function (assert) {
+ await render(
+
+
+ First
+ Last
+
+ ,
+ );
+
+ assert.dom('[data-test-first]').isFocused();
+
+ // Tab should stay within modal
+ await click('[data-test-last]');
+ assert.dom('[data-test-last]').isFocused();
+ });
+});
+```
+
+**Testing with data-test attributes:**
+
+```glimmer-js
+// app/components/user-profile.gjs
+import Component from '@glimmer/component';
+
+class UserProfile extends Component {
+
+
+
+
{{@user.name}}
+
{{@user.email}}
+
+ {{#if @onEdit}}
+
+ Edit
+
+ {{/if}}
+
+
+}
+```
+
+Modern testing patterns with `@ember/test-helpers`, `qunit-dom`, and data-test attributes provide better test reliability, readability, and maintainability.
+
+Reference: [https://guides.emberjs.com/release/testing/](https://guides.emberjs.com/release/testing/)
+
+### 8.5 Use qunit-dom for Better Test Assertions
+
+**Impact: MEDIUM (More readable and maintainable tests)**
+
+Use `qunit-dom` for DOM assertions in tests. It provides expressive, chainable assertions that make tests more readable and provide better error messages than raw QUnit assertions.
+
+**Why qunit-dom:**
+
+- More expressive and readable test assertions
+
+- Better error messages when tests fail
+
+- Type-safe with TypeScript
+
+- Reduces boilerplate in DOM testing
+
+**Incorrect: verbose QUnit assertions**
+
+```javascript
+// tests/integration/components/greeting-test.js
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+
+
+module('Integration | Component | greeting', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ await render( );
+
+ const element = this.element.querySelector('.greeting');
+ assert.ok(element, 'greeting element exists');
+ assert.equal(element.textContent.trim(), 'Hello, World!', 'shows greeting');
+ assert.ok(element.classList.contains('greeting'), 'has greeting class');
+ });
+});
+```
+
+**Correct: expressive qunit-dom**
+
+```javascript
+// tests/integration/components/greeting-test.js
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+
+
+module('Integration | Component | greeting', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ await render( );
+
+ assert.dom('.greeting').exists('greeting element exists');
+ assert.dom('.greeting').hasText('Hello, World!', 'shows greeting');
+ });
+});
+```
+
+**Existence and Visibility:**
+
+```javascript
+test('element visibility', async function (assert) {
+ await render(
+
+
+ ,
+ );
+
+ // Element exists in DOM
+ assert.dom('[data-test-output]').exists();
+
+ // Element doesn't exist
+ assert.dom('[data-test-deleted]').doesNotExist();
+
+ // Element is visible (not display: none or visibility: hidden)
+ assert.dom('[data-test-visible]').isVisible();
+
+ // Element is not visible
+ assert.dom('[data-test-hidden]').isNotVisible();
+
+ // Count elements
+ assert.dom('[data-test-item]').exists({ count: 3 });
+});
+```
+
+**Text Content:**
+
+```javascript
+test('text assertions', async function (assert) {
+ await render( );
+
+ // Exact text match
+ assert.dom('h1').hasText('Hello World');
+
+ // Contains text (partial match)
+ assert.dom('p').containsText('Hello');
+
+ // Any text exists
+ assert.dom('h1').hasAnyText();
+
+ // No text
+ assert.dom('.empty').hasNoText();
+});
+```
+
+**Attributes:**
+
+```javascript
+test('attribute assertions', async function (assert) {
+ await render( );
+
+ // Has attribute (any value)
+ assert.dom('button').hasAttribute('disabled');
+
+ // Has specific attribute value
+ assert.dom('button').hasAttribute('type', 'submit');
+
+ // Attribute value matches regex
+ assert.dom('a').hasAttribute('href', /^https:\/\//);
+
+ // Doesn't have attribute
+ assert.dom('button').doesNotHaveAttribute('aria-hidden');
+
+ // Has ARIA attributes
+ assert.dom('[role="button"]').hasAttribute('aria-label', 'Close dialog');
+});
+```
+
+**Classes:**
+
+```javascript
+test('class assertions', async function (assert) {
+ await render( );
+
+ // Has single class
+ assert.dom('.card').hasClass('active');
+
+ // Doesn't have class
+ assert.dom('.card').doesNotHaveClass('disabled');
+
+ // Has no classes at all
+ assert.dom('.plain').hasNoClass();
+});
+```
+
+**Form Elements:**
+
+```javascript
+test('accessibility', async function (assert) {
+ await render( );
+
+ // ARIA roles
+ assert.dom('[role="dialog"]').exists();
+ assert.dom('[role="dialog"]').hasAttribute('aria-modal', 'true');
+
+ // Labels
+ assert.dom('[aria-label="Close modal"]').exists();
+
+ // Focus management
+ assert.dom('[data-test-close-button]').isFocused();
+
+ // Required fields
+ assert.dom('input[name="email"]').hasAttribute('aria-required', 'true');
+});
+```
+
+You can chain multiple assertions on the same element:
+
+Add custom messages to make failures clearer:
+
+Use qunit-dom for basic accessibility checks:
+
+1. **Use data-test attributes** for test selectors instead of classes:
+
+ ```javascript
+
+ // Good
+
+ assert.dom('[data-test-submit-button]').exists();
+
+ // Avoid - classes can change
+
+ assert.dom('.btn.btn-primary').exists();
+
+ ```
+
+2. **Make assertions specific**:
+
+ ```javascript
+
+ // Better - exact match
+
+ assert.dom('h1').hasText('Welcome');
+
+ // Less specific - could miss issues
+
+ assert.dom('h1').containsText('Welc');
+
+ ```
+
+3. **Use meaningful custom messages**:
+
+ ```javascript
+
+ assert.dom('[data-test-error]').hasText('Invalid email', 'shows correct validation error');
+
+ ```
+
+4. **Combine with @ember/test-helpers**:
+
+ ```javascript
+
+ import { click, fillIn } from '@ember/test-helpers';
+
+ await fillIn('[data-test-email]', 'user@example.com');
+
+ await click('[data-test-submit]');
+
+ assert.dom('[data-test-success]').exists();
+
+ ```
+
+5. **Test user-visible behavior**, not implementation:
+
+ ```javascript
+
+ // Good - tests what user sees
+
+ assert.dom('[data-test-greeting]').hasText('Hello, Alice');
+
+ // Avoid - tests implementation details
+
+ assert.ok(this.component.internalState === 'ready');
+
+ ```
+
+qunit-dom makes your tests more maintainable and easier to understand. It comes pre-installed with `ember-qunit`, so you can start using it immediately.
+
+**References:**
+
+- [qunit-dom Documentation](https://github.com/mainmatter/qunit-dom)
+
+- [qunit-dom API](https://github.com/mainmatter/qunit-dom/blob/master/API.md)
+
+- [Ember Testing Guide](https://guides.emberjs.com/release/testing/)
+
+### 8.6 Use Test Waiters for Async Operations
+
+**Impact: HIGH (Reliable tests that don't depend on implementation details)**
+
+Instrument async code with test waiters instead of using `waitFor()` or `waitUntil()` in tests. Test waiters abstract async implementation details so tests focus on user behavior rather than timing.
+
+**Why Test Waiters Matter:**
+
+Test waiters allow `settled()` and other test helpers to automatically wait for your async operations. This means:
+
+- Tests don't need to know about implementation details (timeouts, polling intervals, etc.)
+
+- Tests are written from a user's perspective ("click button, see result")
+
+- Code refactoring doesn't break tests
+
+- Tests are more reliable and less flaky
+
+**Incorrect: testing implementation details**
+
+```glimmer-js
+// tests/integration/components/data-loader-test.js
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, click, waitFor } from '@ember/test-helpers';
+import DataLoader from 'my-app/components/data-loader';
+
+module('Integration | Component | data-loader', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it loads data', async function (assert) {
+ await render( );
+
+ await click('[data-test-load-button]');
+
+ // BAD: Test knows about implementation details
+ // If the component changes from polling every 100ms to 200ms, test breaks
+ await waitFor('[data-test-data]', { timeout: 5000 });
+
+ assert.dom('[data-test-data]').hasText('Loaded data');
+ });
+});
+```
+
+**Correct: using test waiters**
+
+```glimmer-js
+// tests/integration/components/data-loader-test.js
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, click, settled } from '@ember/test-helpers';
+import DataLoader from 'my-app/components/data-loader';
+
+module('Integration | Component | data-loader', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it loads data', async function (assert) {
+ await render( );
+
+ await click('[data-test-load-button]');
+
+ // GOOD: settled() automatically waits for test waiters
+ // No knowledge of timing needed - tests from user's perspective
+ await settled();
+
+ assert.dom('[data-test-data]').hasText('Loaded data');
+ });
+});
+```
+
+**Test waiter with cleanup:**
+
+```glimmer-js
+// app/components/polling-widget.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { registerDestructor } from '@ember/destroyable';
+import { buildWaiter } from '@ember/test-waiters';
+
+const waiter = buildWaiter('polling-widget');
+
+export class PollingWidget extends Component {
+ @tracked status = 'idle';
+ intervalId = null;
+ token = null;
+
+ constructor(owner, args) {
+ super(owner, args);
+
+ registerDestructor(this, () => {
+ this.stopPolling();
+ });
+ }
+
+ startPolling = () => {
+ // Register async operation
+ this.token = waiter.beginAsync();
+
+ this.intervalId = setInterval(() => {
+ this.checkStatus();
+ }, 1000);
+ };
+
+ stopPolling = () => {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ this.intervalId = null;
+ }
+
+ // End async operation on cleanup
+ if (this.token) {
+ waiter.endAsync(this.token);
+ this.token = null;
+ }
+ };
+
+ checkStatus = async () => {
+ const response = await fetch('/api/status');
+ this.status = await response.text();
+
+ if (this.status === 'complete') {
+ this.stopPolling();
+ }
+ };
+
+
+
+
+ Start Polling
+
+
{{this.status}}
+
+
+}
+```
+
+**Test waiter with Services:**
+
+```glimmer-js
+// tests/unit/services/data-sync-test.js
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+import { settled } from '@ember/test-helpers';
+
+module('Unit | Service | data-sync', function (hooks) {
+ setupTest(hooks);
+
+ test('it syncs data', async function (assert) {
+ const service = this.owner.lookup('service:data-sync');
+
+ // Start async operation
+ const syncPromise = service.sync();
+
+ // No need for manual waiting - settled() handles it
+ await settled();
+
+ const result = await syncPromise;
+ assert.ok(result, 'Sync completed successfully');
+ });
+});
+```
+
+**Multiple concurrent operations:**
+
+```glimmer-js
+// app/components/parallel-loader.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { buildWaiter } from '@ember/test-waiters';
+
+const waiter = buildWaiter('parallel-loader');
+
+export class ParallelLoader extends Component {
+ @tracked results = [];
+
+ loadAll = async () => {
+ const urls = ['/api/data1', '/api/data2', '/api/data3'];
+
+ // Each request gets its own token
+ const requests = urls.map(async (url) => {
+ const token = waiter.beginAsync();
+
+ try {
+ const response = await fetch(url);
+ return await response.json();
+ } finally {
+ waiter.endAsync(token);
+ }
+ });
+
+ this.results = await Promise.all(requests);
+ };
+
+
+
+ Load All
+
+
+ {{#each this.results as |result|}}
+ {{result}}
+ {{/each}}
+
+}
+```
+
+**Benefits:**
+
+1. **User-focused tests**: Tests describe user actions, not implementation
+
+2. **Resilient to refactoring**: Change timing/polling without breaking tests
+
+3. **No arbitrary timeouts**: Tests complete as soon as operations finish
+
+4. **Automatic waiting**: `settled()`, `click()`, etc. wait for all registered operations
+
+5. **Better debugging**: Test waiters show pending operations when tests hang
+
+**When to use test waiters:**
+
+- Network requests (fetch, XHR)
+
+- Timers and intervals (setTimeout, setInterval)
+
+- Animations and transitions
+
+- Polling operations
+
+- Any async operation that affects rendered output
+
+**When NOT needed:**
+
+- ember-concurrency already registers test waiters automatically
+
+- Promises that complete before render (data preparation in constructors)
+
+- Operations that don't affect the DOM or component state
+
+**Key principle:** If your code does something async that users care about, register it with a test waiter. Tests should never use `waitFor()` or `waitUntil()` - those are code smells indicating missing test waiters.
+
+Reference: [https://github.com/emberjs/ember-test-waiters](https://github.com/emberjs/ember-test-waiters)
+
+---
+
+## 9. Tooling and Configuration
+
+**Impact: MEDIUM**
+
+Consistent editor setup and tooling recommendations improve team productivity and reduce environment drift.
+
+### 9.1 VSCode Extensions and MCP Configuration for Ember Projects
+
+**Impact: HIGH (Improves editor consistency and AI-assisted debugging setup)**
+
+Set up recommended VSCode extensions and Model Context Protocol (MCP) servers for optimal Ember development experience.
+
+**Incorrect: no extension recommendations**
+
+```json
+{
+ "recommendations": []
+}
+```
+
+**Correct: recommended extensions for Ember**
+
+```json
+{
+ "recommendations": [
+ "emberjs.vscode-ember",
+ "vunguyentuan.vscode-glint",
+ "esbenp.prettier-vscode",
+ "dbaeumer.vscode-eslint"
+ ]
+}
+```
+
+Create a `.vscode/extensions.json` file in your project root to recommend extensions to all team members:
+
+**ember-extension-pack** (or individual extensions):**
+
+- `emberjs.vscode-ember` - Ember.js language support
+
+- Syntax highlighting for `.hbs`, `.gjs`, `.gts` files
+
+- IntelliSense for Ember-specific patterns
+
+- Code snippets for common Ember patterns
+
+**Glint 2 Extension** (for TypeScript projects):**
+
+```json
+{
+ "github.copilot.enable": {
+ "*": true,
+ "yaml": false,
+ "plaintext": false,
+ "markdown": false
+ },
+ "mcp.servers": {
+ "ember-mcp": {
+ "command": "npx",
+ "args": ["@ember/mcp-server"],
+ "description": "Ember.js MCP Server - Provides Ember-specific context"
+ },
+ "chrome-devtools": {
+ "command": "npx",
+ "args": ["@modelcontextprotocol/server-chrome-devtools"],
+ "description": "Chrome DevTools MCP Server - Browser debugging integration"
+ },
+ "playwright": {
+ "command": "npx",
+ "args": ["@playwright/mcp-server"],
+ "description": "Playwright MCP Server - Browser automation and testing"
+ }
+ }
+}
+```
+
+- `vunguyentuan.vscode-glint` - Type checking for Glimmer templates
+
+- Real-time type errors in `.gts`/`.gjs` files
+
+- Template-aware autocomplete
+
+- Hover information for template helpers and components
+
+Install instructions:
+
+Configure MCP servers in `.vscode/settings.json` to integrate AI coding assistants with Ember-specific context:
+
+**Ember MCP Server** (`@ember/mcp-server`):**
+
+- Ember API documentation lookup
+
+- Component and helper discovery
+
+- Addon documentation integration
+
+- Routing and data layer context
+
+**Chrome DevTools MCP** (`@modelcontextprotocol/server-chrome-devtools`):**
+
+- Live browser inspection
+
+- Console debugging assistance
+
+- Network request analysis
+
+- Performance profiling integration
+
+**Playwright MCP** (optional, `@playwright/mcp-server`):**
+
+```json
+{
+ "compilerOptions": {
+ // ... standard TS options
+ },
+ "glint": {
+ "environment": ["ember-loose", "ember-template-imports"]
+ }
+}
+```
+
+- Test generation assistance
+
+- Browser automation context
+
+- E2E testing patterns
+
+- Debugging test failures
+
+Ensure your `tsconfig.json` has Glint configuration:
+
+1. **Install extensions** (prompted automatically when opening project with `.vscode/extensions.json`)
+
+2. **Install Glint** (if using TypeScript):
+
+ ```bash
+
+ npm install --save-dev @glint/core @glint/environment-ember-loose @glint/environment-ember-template-imports
+
+ ```
+
+3. **Configure MCP servers** in `.vscode/settings.json`
+
+4. **Reload VSCode** to activate all extensions and MCP integrations
+
+- **Consistent team setup**: All developers get same extensions
+
+- **Type safety**: Glint provides template type checking
+
+- **AI assistance**: MCP servers give AI tools Ember-specific context
+
+- **Better DX**: Autocomplete, debugging, and testing integration
+
+- **Reduced onboarding**: New team members get productive faster
+
+- [VSCode Ember Extension](https://marketplace.visualstudio.com/items?itemName=emberjs.vscode-ember)
+
+- [Glint Documentation](https://typed-ember.gitbook.io/glint/)
+
+- [MCP Protocol Specification](https://modelcontextprotocol.io/)
+
+- [Ember Primitives VSCode Setup Example](https://github.com/universal-ember/ember-primitives/tree/main/.vscode)
+
+---
+
+## 10. Advanced Patterns
+
+**Impact: MEDIUM-HIGH**
+
+Modern Ember patterns including Resources for lifecycle management, ember-concurrency for async operations, modifiers for DOM side effects, helpers for reusable logic, and comprehensive testing patterns with render strategies.
+
+### 10.1 Use Ember Concurrency Correctly - User Concurrency Not Data Loading
+
+**Impact: HIGH (Prevents infinite render loops and improves performance)**
+
+ember-concurrency is designed for **user-initiated concurrency patterns** (debouncing, throttling, preventing double-clicks), not data loading. Use task return values, don't set tracked state inside tasks.
+
+- [TaskInstance API](https://ember-concurrency.com/api/TaskInstance.html)
+
+- [Task API](https://ember-concurrency.com/api/Task.html)
+
+- [ember-concurrency](https://ember-concurrency.com/)
+
+- [warp-drive/reactiveweb](https://github.com/emberjs/data/tree/main/packages/reactiveweb)
+
+**Incorrect: using ember-concurrency for data loading with tracked state**
+
+```glimmer-js
+// app/components/user-profile.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { task } from 'ember-concurrency';
+
+class UserProfile extends Component {
+ @tracked userData = null;
+ @tracked error = null;
+
+ // WRONG: Setting tracked state inside task
+ loadUserTask = task(async () => {
+ try {
+ const response = await fetch(`/api/users/${this.args.userId}`);
+ this.userData = await response.json(); // Anti-pattern!
+ } catch (e) {
+ this.error = e; // Anti-pattern!
+ }
+ });
+
+
+ {{#if this.loadUserTask.isRunning}}
+ Loading...
+ {{else if this.userData}}
+ {{this.userData.name}}
+ {{/if}}
+
+}
+```
+
+**Why This Is Wrong:**
+
+- Setting tracked state during render can cause infinite render loops
+
+- ember-concurrency adds overhead unnecessary for simple data loading
+
+- Makes component state harder to reason about
+
+- Can trigger multiple re-renders
+
+**Correct: use getPromiseState from warp-drive/reactiveweb for data loading**
+
+```glimmer-js
+// app/components/user-profile.gjs
+import Component from '@glimmer/component';
+import { cached } from '@glimmer/tracking';
+import { getPromiseState } from '@warp-drive/reactiveweb';
+
+class UserProfile extends Component {
+ @cached
+ get userData() {
+ const promise = fetch(`/api/users/${this.args.userId}`).then((r) => r.json());
+ return getPromiseState(promise);
+ }
+
+
+ {{#if this.userData.isPending}}
+ Loading...
+ {{else if this.userData.isRejected}}
+ Error: {{this.userData.error.message}}
+ {{else if this.userData.isFulfilled}}
+ {{this.userData.value.name}}
+ {{/if}}
+
+}
+```
+
+**Correct: use ember-concurrency for USER input with derived data patterns**
+
+```glimmer-js
+// app/components/search.gjs
+import Component from '@glimmer/component';
+import { restartableTask, timeout } from 'ember-concurrency';
+import { on } from '@ember/modifier';
+import { pick } from 'ember-composable-helpers';
+
+class Search extends Component {
+ // CORRECT: For user-initiated search with debouncing
+ // Use derived data from TaskInstance API - lastSuccessful
+ searchTask = restartableTask(async (query) => {
+ await timeout(300); // Debounce user typing
+ const response = await fetch(`/api/search?q=${query}`);
+ return response.json(); // Return value, don't set tracked state
+ });
+
+
+
+
+ {{! Use derived data from task state - no tracked properties needed }}
+ {{#if this.searchTask.isRunning}}
+ Searching...
+ {{/if}}
+
+ {{! lastSuccessful persists previous results while new search runs }}
+ {{#if this.searchTask.lastSuccessful}}
+
+ {{#each this.searchTask.lastSuccessful.value as |result|}}
+ {{result.name}}
+ {{/each}}
+
+ {{/if}}
+
+ {{! Show error from most recent failed attempt }}
+ {{#if this.searchTask.last.isError}}
+ Error: {{this.searchTask.last.error.message}}
+ {{/if}}
+
+}
+```
+
+**Good Use Cases for ember-concurrency:**
+
+```glimmer-js
+// app/components/form-submit.gjs
+import Component from '@glimmer/component';
+import { dropTask } from 'ember-concurrency';
+import { on } from '@ember/modifier';
+import { fn } from '@ember/helper';
+
+class FormSubmit extends Component {
+ // dropTask prevents double-submit - perfect for user actions
+ submitTask = dropTask(async (formData) => {
+ const response = await fetch('/api/save', {
+ method: 'POST',
+ body: JSON.stringify(formData),
+ });
+ return response.json();
+ });
+
+
+
+ {{#if this.submitTask.isRunning}}
+ Saving...
+ {{else}}
+ Save
+ {{/if}}
+
+
+ {{! Use lastSuccessful for success message - derived data }}
+ {{#if this.submitTask.lastSuccessful}}
+ Saved successfully!
+ {{/if}}
+
+ {{#if this.submitTask.last.isError}}
+ Error: {{this.submitTask.last.error.message}}
+ {{/if}}
+
+}
+```
+
+1. **User input debouncing** - prevent API spam from typing
+
+2. **Form submission** - prevent double-click submits with `dropTask`
+
+3. **Autocomplete** - restart previous searches as user types
+
+4. **Polling** - user-controlled refresh intervals
+
+5. **Multi-step wizards** - sequential async operations
+
+**Bad Use Cases for ember-concurrency:**
+
+1. ❌ **Loading data on component init** - use `getPromiseState` instead
+
+2. ❌ **Route model hooks** - just return promises directly
+
+3. ❌ **Simple API calls** - async/await is sufficient
+
+4. ❌ **Setting tracked state inside tasks** - causes render loops
+
+**Key Principles:**
+
+- **Derive data, don't set it** - Use `task.lastSuccessful`, `task.last`, `task.isRunning` (derived from TaskInstance API)
+
+- **Use task return values** - Read from `task.lastSuccessful.value` or `task.last.value`, never set tracked state
+
+- **User-initiated only** - ember-concurrency is for handling user concurrency patterns
+
+- **Data loading** - Use `getPromiseState` from warp-drive/reactiveweb for non-user-initiated loading
+
+- **Avoid side effects** - Don't modify component state inside tasks that's read during render
+
+**TaskInstance API for Derived Data:**
+
+ember-concurrency provides a powerful derived data API through Task and TaskInstance:
+
+- `task.last` - The most recent TaskInstance (successful or failed)
+
+- `task.lastSuccessful` - The most recent successful TaskInstance (persists during new attempts)
+
+- `task.isRunning` - Derived boolean if any instance is running
+
+- `taskInstance.value` - The returned value from the task
+
+- `taskInstance.isError` - Derived boolean if this instance failed
+
+- `taskInstance.error` - The error if this instance failed
+
+This follows the **derived data pattern** - all state comes from the task itself, no tracked properties needed!
+
+**Migration from tracked state pattern:**
+
+```glimmer-js
+// BEFORE (anti-pattern - setting tracked state)
+class Bad extends Component {
+ @tracked data = null;
+
+ fetchTask = task(async () => {
+ this.data = await fetch('/api/data').then((r) => r.json());
+ });
+
+ // template reads: {{this.data}}
+}
+
+// AFTER (correct - using derived data from TaskInstance API)
+class Good extends Component {
+ fetchTask = restartableTask(async () => {
+ return fetch('/api/data').then((r) => r.json());
+ });
+
+ // template reads: {{this.fetchTask.lastSuccessful.value}}
+ // All state derived from task - no tracked properties!
+}
+
+// Or better yet, for non-user-initiated loading:
+class Better extends Component {
+ @cached
+ get data() {
+ return getPromiseState(fetch('/api/data').then((r) => r.json()));
+ }
+
+ // template reads: {{#if this.data.isFulfilled}}{{this.data.value}}{{/if}}
+}
+```
+
+ember-concurrency is a powerful tool for **user concurrency patterns**. For data loading, use `getPromiseState` instead.
+
+### 10.2 Use Ember Concurrency for User Input Concurrency
+
+**Impact: HIGH (Better control of user-initiated async operations)**
+
+Use ember-concurrency for managing **user-initiated** async operations like search, form submission, and autocomplete. It provides automatic cancelation, debouncing, and prevents race conditions from user actions.
+
+**Incorrect: manual async handling with race conditions**
+
+```glimmer-js
+// app/components/search.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+
+class Search extends Component {
+ @tracked results = [];
+ @tracked isSearching = false;
+ @tracked error = null;
+ currentRequest = null;
+
+ @action
+ async search(event) {
+ const query = event.target.value;
+
+ // Manual cancelation - easy to get wrong
+ if (this.currentRequest) {
+ this.currentRequest.abort();
+ }
+
+ this.isSearching = true;
+ this.error = null;
+
+ const controller = new AbortController();
+ this.currentRequest = controller;
+
+ try {
+ const response = await fetch(`/api/search?q=${query}`, {
+ signal: controller.signal,
+ });
+ this.results = await response.json();
+ } catch (e) {
+ if (e.name !== 'AbortError') {
+ this.error = e.message;
+ }
+ } finally {
+ this.isSearching = false;
+ }
+ }
+
+
+
+ {{#if this.isSearching}}Loading...{{/if}}
+ {{#if this.error}}Error: {{this.error}}{{/if}}
+
+}
+```
+
+**Correct: using ember-concurrency with task return values**
+
+```glimmer-js
+// app/components/search.gjs
+import Component from '@glimmer/component';
+import { restartableTask } from 'ember-concurrency';
+
+class Search extends Component {
+ // restartableTask automatically cancels previous searches
+ // IMPORTANT: Return the value, don't set tracked state inside tasks
+ searchTask = restartableTask(async (query) => {
+ const response = await fetch(`/api/search?q=${query}`);
+ return response.json(); // Return, don't set @tracked
+ });
+
+
+
+
+ {{#if this.searchTask.isRunning}}
+ Loading...
+ {{/if}}
+
+ {{#if this.searchTask.last.isSuccessful}}
+
+ {{#each this.searchTask.last.value as |result|}}
+ {{result.name}}
+ {{/each}}
+
+ {{/if}}
+
+ {{#if this.searchTask.last.isError}}
+ {{this.searchTask.last.error.message}}
+ {{/if}}
+
+}
+```
+
+**With debouncing for user typing:**
+
+```glimmer-js
+// app/components/autocomplete.gjs
+import Component from '@glimmer/component';
+import { restartableTask, timeout } from 'ember-concurrency';
+
+class Autocomplete extends Component {
+ searchTask = restartableTask(async (query) => {
+ // Debounce user typing - wait 300ms
+ await timeout(300);
+
+ const response = await fetch(`/api/autocomplete?q=${query}`);
+ return response.json(); // Return value, don't set tracked state
+ });
+
+
+
+
+ {{#if this.searchTask.isRunning}}
+
+ {{/if}}
+
+ {{#if this.searchTask.lastSuccessful}}
+
+ {{#each this.searchTask.lastSuccessful.value as |item|}}
+ {{item.title}}
+ {{/each}}
+
+ {{/if}}
+
+}
+```
+
+**Task modifiers for different user concurrency patterns:**
+
+```glimmer-js
+import Component from '@glimmer/component';
+import { dropTask, enqueueTask, restartableTask } from 'ember-concurrency';
+
+class FormActions extends Component {
+ // dropTask: Prevents double-click - ignores new while running
+ saveTask = dropTask(async (data) => {
+ const response = await fetch('/api/save', {
+ method: 'POST',
+ body: JSON.stringify(data),
+ });
+ return response.json();
+ });
+
+ // enqueueTask: Queues user actions sequentially
+ processTask = enqueueTask(async (item) => {
+ const response = await fetch('/api/process', {
+ method: 'POST',
+ body: JSON.stringify(item),
+ });
+ return response.json();
+ });
+
+ // restartableTask: Cancels previous, starts new (for search)
+ searchTask = restartableTask(async (query) => {
+ const response = await fetch(`/api/search?q=${query}`);
+ return response.json();
+ });
+
+
+
+ Save
+
+
+}
+```
+
+**Key Principles for ember-concurrency:**
+
+1. **User-initiated only** - Use for handling user actions, not component initialization
+
+2. **Return values** - Use `task.last.value`, never set `@tracked` state inside tasks
+
+3. **Avoid side effects** - Don't modify component state that's read during render inside tasks
+
+4. **Choose right modifier**:
+
+ - `restartableTask` - User typing/search (cancel previous)
+
+ - `dropTask` - Form submit/save (prevent double-click)
+
+ - `enqueueTask` - Sequential processing (queue user actions)
+
+**When NOT to use ember-concurrency:**
+
+- ❌ Component initialization data loading (use `getPromiseState` instead)
+
+- ❌ Setting tracked state inside tasks (causes infinite render loops)
+
+- ❌ Route model hooks (return promises directly)
+
+- ❌ Simple async without user concurrency concerns (use async/await)
+
+See **advanced-data-loading-with-ember-concurrency.md** for correct data loading patterns.
+
+ember-concurrency provides automatic cancelation, derived state (isRunning, isIdle), and better patterns for **user-initiated** async operations.
+
+Reference: [https://ember-concurrency.com/](https://ember-concurrency.com/)
+
+### 10.3 Use Helper Functions for Reusable Logic
+
+**Impact: LOW-MEDIUM (Better code reuse and testability)**
+
+Extract reusable template logic into helper functions that can be tested independently and used across templates.
+
+**Incorrect: logic duplicated in components**
+
+```javascript
+// app/components/user-card.js
+class UserCard extends Component {
+ get formattedDate() {
+ const date = new Date(this.args.user.createdAt);
+ const now = new Date();
+ const diffMs = now - date;
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+
+ if (diffDays === 0) return 'Today';
+ if (diffDays === 1) return 'Yesterday';
+ if (diffDays < 7) return `${diffDays} days ago`;
+ return date.toLocaleDateString();
+ }
+}
+
+// app/components/post-card.js - same logic duplicated!
+class PostCard extends Component {
+ get formattedDate() {
+ // Same implementation...
+ }
+}
+```
+
+**Correct: reusable helper**
+
+```javascript
+// app/components/blog/format-relative-date.js
+export function formatRelativeDate(date) {
+ const dateObj = new Date(date);
+ const now = new Date();
+ const diffMs = now - dateObj;
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+
+ if (diffDays === 0) return 'Today';
+ if (diffDays === 1) return 'Yesterday';
+ if (diffDays < 7) return `${diffDays} days ago`;
+ return dateObj.toLocaleDateString();
+}
+```
+
+For single-use helpers, keep them in the same file as the component:
+
+For helpers shared across multiple components in a feature, use a subdirectory:
+
+**Alternative: shared helper in utils**
+
+```glimmer-js
+// app/components/post-card.gjs
+import { formatRelativeDate } from '../utils/format-relative-date';
+
+
+ Posted: {{formatRelativeDate @post.createdAt}}
+
+```
+
+For truly shared helpers used across the whole app, use `app/utils/`:
+
+**Note**: Keep utils flat (`app/utils/format-relative-date.js`), not nested (`app/utils/date/format-relative-date.js`). If you need cleaner top-level imports, configure subpath-imports in package.json instead of nesting files.
+
+**For helpers with state, use class-based helpers:**
+
+```javascript
+// app/utils/helpers/format-currency.js
+export class FormatCurrencyHelper {
+ constructor(owner) {
+ this.intl = owner.lookup('service:intl');
+ }
+
+ compute(amount, { currency = 'USD' } = {}) {
+ return this.intl.formatNumber(amount, {
+ style: 'currency',
+ currency,
+ });
+ }
+}
+```
+
+**Common helpers to create:**
+
+- Date/time formatting
+
+- Number formatting
+
+- String manipulation
+
+- Array operations
+
+- Conditional logic
+
+Helpers promote code reuse, are easier to test, and keep components focused on behavior.
+
+Reference: [https://guides.emberjs.com/release/components/helper-functions/](https://guides.emberjs.com/release/components/helper-functions/)
+
+### 10.4 Use Modifiers for DOM Side Effects
+
+**Impact: LOW-MEDIUM (Better separation of concerns)**
+
+Use modifiers (element modifiers) to handle DOM side effects and lifecycle events in a reusable, composable way.
+
+**Incorrect: manual DOM manipulation in component**
+
+```glimmer-js
+// app/components/chart.gjs
+import Component from '@glimmer/component';
+
+class Chart extends Component {
+ chartInstance = null;
+
+ constructor() {
+ super(...arguments);
+ // Can't access element here - element doesn't exist yet!
+ }
+
+ willDestroy() {
+ super.willDestroy();
+ this.chartInstance?.destroy();
+ }
+
+
+
+ {{! Manual setup is error-prone and not reusable }}
+
+}
+```
+
+**Correct: function modifier - preferred for simple side effects**
+
+```javascript
+// app/modifiers/chart.js
+import { modifier } from 'ember-modifier';
+
+export default modifier((element, [config]) => {
+ // Initialize chart
+ const chartInstance = new Chart(element, config);
+
+ // Return cleanup function
+ return () => {
+ chartInstance.destroy();
+ };
+});
+```
+
+**Also correct: class-based modifier for complex state**
+
+```glimmer-js
+// app/components/chart.gjs
+import chart from '../modifiers/chart';
+
+
+
+
+```
+
+**Use function modifiers** for simple side effects. Use class-based modifiers only when you need complex state management.
+
+**For commonly needed modifiers, use ember-modifier helpers:**
+
+```glimmer-js
+// app/components/input-field.gjs
+import autofocus from '../modifiers/autofocus';
+
+
+```
+
+**Use ember-resize-observer-modifier for resize handling:**
+
+```glimmer-js
+// app/components/resizable.gjs
+import onResize from 'ember-resize-observer-modifier';
+
+
+
+ Content that responds to size changes
+
+
+```
+
+Modifiers provide a clean, reusable way to manage DOM side effects without coupling to specific components.
+
+Reference: [https://guides.emberjs.com/release/components/template-lifecycle-dom-and-modifiers/](https://guides.emberjs.com/release/components/template-lifecycle-dom-and-modifiers/)
+
+### 10.5 Use Reactive Collections from @ember/reactive/collections
+
+**Impact: HIGH (Enables reactive arrays, maps, and sets)**
+
+Use reactive collections from `@ember/reactive/collections` to make arrays, Maps, and Sets reactive in Ember. Standard JavaScript collections don't trigger Ember's reactivity system when mutated—reactive collections solve this.
+
+**The Problem:**
+
+Standard arrays, Maps, and Sets are not reactive in Ember when you mutate them. Changes won't trigger template updates.
+
+**The Solution:**
+
+Use Ember's built-in reactive collections from `@ember/reactive/collections`.
+
+**Incorrect: non-reactive array**
+
+```glimmer-js
+// app/components/todo-list.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+
+export default class TodoList extends Component {
+ @tracked todos = []; // ❌ Array mutations (push, splice, etc.) won't trigger updates
+
+ @action
+ addTodo(text) {
+ // This won't trigger a re-render!
+ this.todos.push({ id: Date.now(), text });
+ }
+
+ @action
+ removeTodo(id) {
+ // This also won't trigger a re-render!
+ const index = this.todos.findIndex((t) => t.id === id);
+ this.todos.splice(index, 1);
+ }
+
+
+
+ {{#each this.todos as |todo|}}
+
+ {{todo.text}}
+ Remove
+
+ {{/each}}
+
+ Add
+
+}
+```
+
+**Correct: reactive array with @ember/reactive/collections**
+
+```glimmer-js
+// app/components/tag-selector.gjs
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { trackedSet } from '@ember/reactive/collections';
+
+export default class TagSelector extends Component {
+ selectedTags = trackedSet();
+
+ @action
+ toggleTag(tag) {
+ if (this.selectedTags.has(tag)) {
+ this.selectedTags.delete(tag);
+ } else {
+ this.selectedTags.add(tag);
+ }
+ }
+
+ get selectedCount() {
+ return this.selectedTags.size;
+ }
+
+
+
+ {{#each @availableTags as |tag|}}
+
+
+ {{tag}}
+
+ {{/each}}
+
+ Selected: {{this.selectedCount}} tags
+
+}
+```
+
+Maps are useful for key-value stores with non-string keys:
+
+Sets are useful for unique collections:
+
+| Type | Use Case |
+
+| -------------- | ------------------------------------------------------------------ |
+
+| `trackedArray` | Ordered lists that need mutation methods (push, pop, splice, etc.) |
+
+| `trackedMap` | Key-value pairs with non-string keys or when you need `size` |
+
+| `trackedSet` | Unique values, membership testing |
+
+**Initialize with data:**
+
+```javascript
+import { trackedArray, trackedMap, trackedSet } from '@ember/reactive/collections';
+
+// Array
+const todos = trackedArray([
+ { id: 1, text: 'First' },
+ { id: 2, text: 'Second' },
+]);
+
+// Map
+const userMap = trackedMap([
+ [1, { name: 'Alice' }],
+ [2, { name: 'Bob' }],
+]);
+
+// Set
+const tags = trackedSet(['javascript', 'ember', 'web']);
+```
+
+**Convert to plain JavaScript:**
+
+```javascript
+// Array
+const plainArray = [...trackedArray];
+const plainArray2 = Array.from(trackedArray);
+
+// Map
+const plainObject = Object.fromEntries(trackedMap);
+
+// Set
+const plainArray3 = [...trackedSet];
+```
+
+**Functional array methods still work:**
+
+```javascript
+import { tracked } from '@glimmer/tracking';
+
+export default class TodoList extends Component {
+ @tracked todos = [];
+
+ @action
+ addTodo(text) {
+ // Reassignment is reactive
+ this.todos = [...this.todos, { id: Date.now(), text }];
+ }
+
+ @action
+ removeTodo(id) {
+ // Reassignment is reactive
+ this.todos = this.todos.filter((t) => t.id !== id);
+ }
+}
+```
+
+If you prefer immutability, you can use regular `@tracked` with reassignment:
+
+**When to use each approach:**
+
+- Use reactive collections when you need mutable operations (better performance for large lists)
+
+- Use immutable updates when you want simpler mental model or need history/undo
+
+1. **Don't mix approaches** - choose either reactive collections or immutable updates
+
+2. **Initialize in class field** - no need for constructor
+
+3. **Use appropriate type** - Map for key-value, Set for unique values, Array for ordered lists
+
+4. **Export from modules** if shared across components
+
+Reactive collections from `@ember/reactive/collections` provide the best of both worlds: mutable operations with full reactivity. They're especially valuable for large lists or frequent updates where immutable updates would be expensive.
+
+**References:**
+
+- [Ember Reactivity System](https://guides.emberjs.com/release/in-depth-topics/autotracking-in-depth/)
+
+- [JavaScript Built-in Objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects)
+
+- [Reactive Collections RFC](https://github.com/emberjs/rfcs/blob/master/text/0869-reactive-collections.md)
+
+---
+
+## References
+
+1. [https://emberjs.com](https://emberjs.com)
+2. [https://guides.emberjs.com](https://guides.emberjs.com)
+3. [https://guides.emberjs.com/release/accessibility/](https://guides.emberjs.com/release/accessibility/)
+4. [https://warp-drive.io/](https://warp-drive.io/)
+5. [https://github.com/ember-a11y/ember-a11y-testing](https://github.com/ember-a11y/ember-a11y-testing)
+6. [https://github.com/embroider-build/embroider](https://github.com/embroider-build/embroider)
+7. [https://github.com/tracked-tools/tracked-toolbox](https://github.com/tracked-tools/tracked-toolbox)
+8. [https://github.com/NullVoxPopuli/ember-resources](https://github.com/NullVoxPopuli/ember-resources)
+9. [https://ember-concurrency.com/](https://ember-concurrency.com/)
+10. [https://octane-guides.emberjs.com](https://octane-guides.emberjs.com)
diff --git a/.agents/skills/ember-best-practices/README.md b/.agents/skills/ember-best-practices/README.md
new file mode 100644
index 0000000..5bfff64
--- /dev/null
+++ b/.agents/skills/ember-best-practices/README.md
@@ -0,0 +1,101 @@
+# Ember.js Best Practices
+
+A structured repository for creating and maintaining Ember.js Best Practices optimized for agents and LLMs.
+
+## Structure
+
+- `rules/` - Individual rule files (one per rule)
+ - `_sections.md` - Section metadata (titles, impacts, descriptions)
+ - `_template.md` - Template for creating new rules
+ - `area-description.md` - Individual rule files
+- `metadata.json` - Document metadata (version, organization, abstract)
+- **`AGENTS.md`** - Compiled output (generated)
+- **`SKILL.md`** - Skill definition for Claude Code
+
+## Rule Categories
+
+Rules are organized by prefix:
+
+- `route-` for Route Loading and Data Fetching (Section 1)
+- `bundle-` for Build and Bundle Optimization (Section 2)
+- `component-` for Component and Reactivity (Section 3)
+- `a11y-` for Accessibility Best Practices (Section 4)
+- `service-` for Service and State Management (Section 5)
+- `template-` for Template Optimization (Section 6)
+- `advanced-` for Advanced Patterns (Section 7)
+
+## Rule File Structure
+
+Each rule file should follow this structure:
+
+````markdown
+---
+title: Rule Title Here
+impact: MEDIUM
+impactDescription: Optional description
+tags: tag1, tag2, tag3
+---
+
+## Rule Title Here
+
+Brief explanation of the rule and why it matters.
+
+**Incorrect (description of what's wrong):**
+
+```javascript
+// Bad code example
+```
+
+**Correct (description of what's right):**
+
+```javascript
+// Good code example
+```
+
+Optional explanatory text after examples.
+
+Reference: [Link](https://example.com)
+````
+
+## File Naming Convention
+
+- Files starting with `_` are special (excluded from build)
+- Rule files: `area-description.md` (e.g., `route-parallel-model.md`)
+- Section is automatically inferred from filename prefix
+- Rules are sorted alphabetically by title within each section
+- IDs (e.g., 1.1, 1.2) are auto-generated during build
+
+## Impact Levels
+
+- `CRITICAL` - Highest priority, major performance gains
+- `HIGH` - Significant performance improvements
+- `MEDIUM-HIGH` - Moderate-high gains
+- `MEDIUM` - Moderate performance improvements
+- `LOW-MEDIUM` - Low-medium gains
+- `LOW` - Incremental improvements
+
+## Contributing
+
+When adding or modifying rules:
+
+1. Use the correct filename prefix for your section
+2. Follow the `_template.md` structure
+3. Include clear bad/good examples with explanations
+4. Add appropriate tags
+5. Rules are automatically sorted by title - no need to manage numbers!
+
+## Accessibility Focus
+
+This guide emphasizes Ember's strong accessibility ecosystem:
+
+- **ember-a11y-testing** - Automated testing with axe-core
+- **ember-a11y** - Route announcements and focus management
+- **ember-focus-trap** - Modal focus trapping
+- **ember-page-title** - Accessible page titles
+- **Semantic HTML** - Proper use of native elements
+- **ARIA attributes** - When custom elements are needed
+- **Keyboard navigation** - Full keyboard support patterns
+
+## Acknowledgments
+
+Built for the Ember.js community, drawing from official guides, Octane patterns, and accessibility best practices.
diff --git a/.agents/skills/ember-best-practices/SKILL.md b/.agents/skills/ember-best-practices/SKILL.md
new file mode 100644
index 0000000..51c077f
--- /dev/null
+++ b/.agents/skills/ember-best-practices/SKILL.md
@@ -0,0 +1,161 @@
+---
+name: ember-best-practices
+description: Ember.js performance optimization and accessibility guidelines. This skill should be used when writing, reviewing, or refactoring Ember.js code to ensure optimal performance patterns and accessibility. Triggers on tasks involving Ember components, routes, data fetching, bundle optimization, or accessibility improvements.
+license: MIT
+metadata:
+ author: Ember.js Community
+ version: '1.0.0'
+---
+
+# Ember.js Best Practices
+
+Comprehensive performance optimization and accessibility guide for Ember.js applications. Contains 58 rules across 10 categories, prioritized by impact to guide automated refactoring and code generation.
+
+## When to Apply
+
+Reference these guidelines when:
+
+- Writing new Ember components or routes
+- Implementing data fetching with WarpDrive
+- Reviewing code for performance issues
+- Refactoring existing Ember.js code
+- Optimizing bundle size or load times
+- Implementing accessibility features
+
+## Rule Categories by Priority
+
+| Priority | Category | Impact | Prefix |
+| -------- | ------------------------------- | ----------- | ------------------------ |
+| 1 | Route Loading and Data Fetching | CRITICAL | `route-` |
+| 2 | Build and Bundle Optimization | CRITICAL | `bundle-` |
+| 3 | Component and Reactivity | HIGH | `component-`, `exports-` |
+| 4 | Accessibility Best Practices | HIGH | `a11y-` |
+| 5 | Service and State Management | MEDIUM-HIGH | `service-` |
+| 6 | Template Optimization | MEDIUM | `template-`, `helper-` |
+| 7 | Performance Optimization | MEDIUM | `performance-` |
+| 8 | Testing Best Practices | MEDIUM | `testing-` |
+| 9 | Tooling and Configuration | MEDIUM | `vscode-` |
+| 10 | Advanced Patterns | MEDIUM-HIGH | `advanced-` |
+
+## Quick Reference
+
+### 1. Route Loading and Data Fetching (CRITICAL)
+
+- `route-parallel-model` - Use RSVP.hash() for parallel data loading
+- `route-loading-substates` - Implement loading substates for better UX
+- `route-lazy-routes` - Use route-based code splitting with Embroider
+- `route-templates` - Use route templates with co-located syntax
+- `route-model-caching` - Implement smart route model caching
+
+### 2. Build and Bundle Optimization (CRITICAL)
+
+- `bundle-direct-imports` - Import directly, avoid entire namespaces
+- `bundle-embroider-static` - Enable Embroider static mode for tree-shaking
+- `bundle-lazy-dependencies` - Lazy load heavy dependencies
+
+### 3. Component and Reactivity Optimization (HIGH)
+
+- `component-use-glimmer` - Use Glimmer components over classic components
+- `component-cached-getters` - Use @cached for expensive computations
+- `component-minimal-tracking` - Only track properties that affect rendering
+- `component-tracked-toolbox` - Use tracked-built-ins for complex state
+- `component-composition-patterns` - Use yield blocks and contextual components
+- `component-reactive-chains` - Build reactive chains with dependent getters
+- `component-class-fields` - Use class fields for component composition
+- `component-controlled-forms` - Implement controlled form patterns
+- `component-on-modifier` - Use {{on}} modifier for event handling
+- `component-args-validation` - Validate component arguments
+- `component-memory-leaks` - Prevent memory leaks in components
+- `component-strict-mode` - Use strict mode and template-only components
+- `component-avoid-classes-in-examples` - Avoid unnecessary classes in component examples
+- `component-avoid-constructors` - Avoid constructors in Glimmer components
+- `component-avoid-lifecycle-hooks` - Avoid legacy lifecycle hooks
+- `component-file-conventions` - Follow proper file naming conventions
+- `exports-named-only` - Use named exports only
+
+### 4. Accessibility Best Practices (HIGH)
+
+- `a11y-automated-testing` - Use ember-a11y-testing for automated checks
+- `a11y-semantic-html` - Use semantic HTML and proper ARIA attributes
+- `a11y-keyboard-navigation` - Ensure full keyboard navigation support
+- `a11y-form-labels` - Associate labels with inputs, announce errors
+- `a11y-route-announcements` - Announce route transitions to screen readers
+
+### 5. Service and State Management (MEDIUM-HIGH)
+
+- `service-cache-responses` - Cache API responses in services
+- `service-shared-state` - Use services for shared state
+- `service-ember-data-optimization` - Optimize WarpDrive queries
+- `service-owner-linkage` - Manage service owner and linkage patterns
+- `service-data-requesting` - Implement robust data requesting patterns
+
+### 6. Template Optimization (MEDIUM)
+
+- `template-let-helper` - Use {{#let}} to avoid recomputation
+- `template-each-key` - Use {{#each}} with @key for efficient list updates
+- `template-avoid-computation` - Move expensive work to cached getters
+- `template-helper-imports` - Import helpers directly in templates
+- `template-conditional-rendering` - Optimize conditional rendering
+- `template-fn-helper` - Use {{fn}} helper for partial application
+- `template-only-component-functions` - Use template-only components
+- `helper-composition` - Compose helpers for reusable logic
+- `helper-builtin-functions` - Use built-in helpers effectively
+- `helper-plain-functions` - Write helpers as plain functions
+
+### 7. Performance Optimization (MEDIUM)
+
+- `performance-on-modifier-vs-handlers` - Use {{on}} modifier instead of event handler properties
+
+### 8. Testing Best Practices (MEDIUM)
+
+- `testing-modern-patterns` - Use modern testing patterns
+- `testing-qunit-dom-assertions` - Use qunit-dom for better test assertions
+- `testing-test-waiters` - Use @ember/test-waiters for async testing
+- `testing-render-patterns` - Use correct render patterns for components
+- `testing-msw-setup` - Mock API requests with MSW
+- `testing-library-dom-abstraction` - Use Testing Library patterns
+
+### 9. Tooling and Configuration (MEDIUM)
+
+- `vscode-setup-recommended` - VS Code extensions and MCP server setup
+
+### 10. Advanced Patterns (MEDIUM-HIGH)
+
+- `advanced-modifiers` - Use modifiers for DOM side effects
+- `advanced-helpers` - Extract reusable logic into helpers
+- `advanced-tracked-built-ins` - Use reactive collections from @ember/reactive/collections
+- `advanced-concurrency` - Use ember-concurrency for task management
+- `advanced-data-loading-with-ember-concurrency` - Data loading patterns with ember-concurrency
+
+## How to Use
+
+Read individual rule files for detailed explanations and code examples:
+
+```
+rules/route-parallel-model.md
+rules/bundle-embroider-static.md
+rules/a11y-automated-testing.md
+```
+
+Each rule file contains:
+
+- Brief explanation of why it matters
+- Incorrect code example with explanation
+- Correct code example with explanation
+- Additional context and references
+
+## Accessibility with OSS Tools
+
+Ember has excellent accessibility support through community addons:
+
+- **ember-a11y-testing** - Automated accessibility testing in your test suite
+- **ember-a11y** - Route announcements and focus management
+- **ember-focus-trap** - Focus trapping for modals and dialogs
+- **ember-page-title** - Accessible page title management
+- **Platform-native validation** - Use browser's Constraint Validation API for accessible form validation
+
+These tools, combined with native web platform features, provide comprehensive a11y support with minimal configuration.
+
+## Full Compiled Document
+
+For the complete guide with all rules expanded: `AGENTS.md`
diff --git a/.agents/skills/ember-best-practices/build-agents.sh b/.agents/skills/ember-best-practices/build-agents.sh
new file mode 100755
index 0000000..3a6af01
--- /dev/null
+++ b/.agents/skills/ember-best-practices/build-agents.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+set -e
+
+OUTPUT="AGENTS.md"
+RULES_DIR="rules"
+
+# Start with the header
+cat > "$OUTPUT" << 'HEADER'
+# Ember Best Practices
+
+Comprehensive performance optimization and accessibility patterns for modern Ember.js applications. Includes rules across 7 categories using gjs/gts format and modern Ember patterns.
+
+---
+
+HEADER
+
+# Add sections
+cat "$RULES_DIR/_sections.md" >> "$OUTPUT"
+
+echo "" >> "$OUTPUT"
+echo "---" >> "$OUTPUT"
+echo "" >> "$OUTPUT"
+
+# Add all rules
+for file in "$RULES_DIR"/*.md; do
+ # Skip the _sections.md file
+ if [[ "$(basename "$file")" == "_sections.md" ]]; then
+ continue
+ fi
+
+ echo "Adding $(basename "$file")..." >&2
+ cat "$file" >> "$OUTPUT"
+ echo "" >> "$OUTPUT"
+ echo "---" >> "$OUTPUT"
+ echo "" >> "$OUTPUT"
+done
+
+echo "Built $OUTPUT successfully!" >&2
diff --git a/.agents/skills/ember-best-practices/rules/_sections.md b/.agents/skills/ember-best-practices/rules/_sections.md
new file mode 100644
index 0000000..8964488
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/_sections.md
@@ -0,0 +1,57 @@
+# Sections
+
+This file defines all sections, their ordering, impact levels, and descriptions.
+The section ID (in parentheses) is the filename prefix used to group rules.
+When multiple prefixes map to one section, all supported prefixes are listed.
+
+---
+
+## 1. Route Loading and Data Fetching (route)
+
+**Impact:** CRITICAL
+**Description:** Efficient route loading and parallel data fetching eliminate waterfalls. Using route model hooks effectively and loading data in parallel yields the largest performance gains.
+
+## 2. Build and Bundle Optimization (bundle)
+
+**Impact:** CRITICAL
+**Description:** Using Embroider with static build optimizations, route-based code splitting, and proper imports reduces bundle size and improves Time to Interactive.
+
+## 3. Component and Reactivity Optimization (component, exports)
+
+**Impact:** HIGH
+**Description:** Proper use of Glimmer components, modern file conventions, tracked properties, and avoiding unnecessary recomputation improves rendering performance.
+
+## 4. Accessibility Best Practices (a11y)
+
+**Impact:** HIGH
+**Description:** Making applications accessible is critical. Use ember-a11y-testing, semantic HTML, proper ARIA attributes, and keyboard navigation support.
+
+## 5. Service and State Management (service)
+
+**Impact:** MEDIUM-HIGH
+**Description:** Efficient service patterns, proper dependency injection, and state management reduce redundant computations and API calls.
+
+## 6. Template Optimization (template, helper)
+
+**Impact:** MEDIUM
+**Description:** Optimizing templates with proper helpers, avoiding expensive computations in templates, and using {{#each}} efficiently improves rendering speed.
+
+## 7. Performance Optimization (performance)
+
+**Impact:** MEDIUM
+**Description:** Performance-focused rendering and event handling patterns help reduce unnecessary work in hot UI paths.
+
+## 8. Testing Best Practices (testing)
+
+**Impact:** MEDIUM
+**Description:** Modern testing patterns, waiters, and abstraction utilities improve test reliability and maintainability.
+
+## 9. Tooling and Configuration (vscode)
+
+**Impact:** MEDIUM
+**Description:** Consistent editor setup and tooling recommendations improve team productivity and reduce environment drift.
+
+## 10. Advanced Patterns (advanced)
+
+**Impact:** MEDIUM-HIGH
+**Description:** Modern Ember patterns including Resources for lifecycle management, ember-concurrency for async operations, modifiers for DOM side effects, helpers for reusable logic, and comprehensive testing patterns with render strategies.
diff --git a/.agents/skills/ember-best-practices/rules/_template.md b/.agents/skills/ember-best-practices/rules/_template.md
new file mode 100644
index 0000000..0d7ce15
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/_template.md
@@ -0,0 +1,28 @@
+---
+title: Rule Title Here
+impact: MEDIUM
+impactDescription: Optional description of impact (e.g., "20-50% improvement")
+tags: tag1, tag2
+---
+
+## Rule Title Here
+
+**Impact: MEDIUM (optional impact description)**
+
+Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
+
+**Incorrect (description of what's wrong):**
+
+```glimmer-ts
+// Bad code example here
+const bad = example();
+```
+
+**Correct (description of what's right):**
+
+```glimmer-ts
+// Good code example here
+const good = example();
+```
+
+Reference: [Link to documentation or resource](https://example.com)
diff --git a/.agents/skills/ember-best-practices/rules/a11y-automated-testing.md b/.agents/skills/ember-best-practices/rules/a11y-automated-testing.md
new file mode 100644
index 0000000..1fe447d
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/a11y-automated-testing.md
@@ -0,0 +1,74 @@
+---
+title: Use ember-a11y-testing for Automated Checks
+impact: HIGH
+impactDescription: Catch 30-50% of a11y issues automatically
+tags: accessibility, a11y, testing, ember-a11y-testing
+---
+
+## Use ember-a11y-testing for Automated Checks
+
+Integrate ember-a11y-testing into your test suite to automatically catch common accessibility violations during development. This addon uses axe-core to identify issues before they reach production.
+
+**Incorrect (no accessibility testing):**
+
+```glimmer-js
+// tests/integration/components/user-form-test.js
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, fillIn, click } from '@ember/test-helpers';
+import UserForm from 'my-app/components/user-form';
+
+module('Integration | Component | user-form', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it submits the form', async function (assert) {
+ await render( );
+ await fillIn('input', 'John');
+ await click('button');
+ assert.ok(true);
+ });
+});
+```
+
+**Correct (with a11y testing):**
+
+```glimmer-js
+// tests/integration/components/user-form-test.js
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, fillIn, click } from '@ember/test-helpers';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
+import UserForm from 'my-app/components/user-form';
+
+module('Integration | Component | user-form', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it submits the form', async function (assert) {
+ await render( );
+
+ // Automatically checks for a11y violations
+ await a11yAudit();
+
+ await fillIn('input', 'John');
+ await click('button');
+ assert.ok(true);
+ });
+});
+```
+
+**Setup (install and configure):**
+
+```bash
+ember install ember-a11y-testing
+```
+
+```javascript
+// tests/test-helper.js
+import { setupGlobalA11yHooks } from 'ember-a11y-testing/test-support';
+
+setupGlobalA11yHooks(); // Runs on every test automatically
+```
+
+ember-a11y-testing catches issues like missing labels, insufficient color contrast, invalid ARIA, and keyboard navigation problems automatically.
+
+Reference: [ember-a11y-testing](https://github.com/ember-a11y/ember-a11y-testing)
diff --git a/.agents/skills/ember-best-practices/rules/a11y-form-labels.md b/.agents/skills/ember-best-practices/rules/a11y-form-labels.md
new file mode 100644
index 0000000..c7b4eae
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/a11y-form-labels.md
@@ -0,0 +1,147 @@
+---
+title: Form Labels and Error Announcements
+impact: HIGH
+impactDescription: Essential for screen reader users
+tags: accessibility, a11y, forms, aria-live
+---
+
+## Form Labels and Error Announcements
+
+All form inputs must have associated labels, and validation errors should be announced to screen readers using ARIA live regions.
+
+**Incorrect (missing labels and announcements):**
+
+```glimmer-js
+// app/components/form.gjs
+
+
+
+```
+
+**Correct (with labels and announcements):**
+
+```glimmer-js
+// app/components/form.gjs
+
+
+
+```
+
+**For complex forms, use platform-native validation with custom logic:**
+
+```glimmer-js
+// app/components/user-form.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { on } from '@ember/modifier';
+
+class UserForm extends Component {
+ @tracked errorMessages = {};
+
+ validateEmail = (event) => {
+ // Custom business logic validation
+ const input = event.target;
+ const value = input.value;
+
+ if (!value) {
+ input.setCustomValidity('Email is required');
+ return false;
+ }
+
+ if (!input.validity.valid) {
+ input.setCustomValidity('Must be a valid email');
+ return false;
+ }
+
+ // Additional custom validation (e.g., check if email is already taken)
+ if (value === 'taken@example.com') {
+ input.setCustomValidity('This email is already registered');
+ return false;
+ }
+
+ input.setCustomValidity('');
+ return true;
+ };
+
+ handleSubmit = async (event) => {
+ event.preventDefault();
+ const form = event.target;
+
+ // Run custom validations
+ const emailInput = form.querySelector('[name="email"]');
+ const fakeEvent = { target: emailInput };
+ this.validateEmail(fakeEvent);
+
+ // Use native validation check
+ if (!form.checkValidity()) {
+ form.reportValidity();
+ return;
+ }
+
+ const formData = new FormData(form);
+ await this.args.onSubmit(formData);
+ };
+
+
+
+
+}
+```
+
+Always associate labels with inputs and announce dynamic changes to screen readers using aria-live regions.
+
+Reference: [Ember Accessibility - Application Considerations](https://guides.emberjs.com/release/accessibility/application-considerations/)
diff --git a/.agents/skills/ember-best-practices/rules/a11y-keyboard-navigation.md b/.agents/skills/ember-best-practices/rules/a11y-keyboard-navigation.md
new file mode 100644
index 0000000..a5ecd36
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/a11y-keyboard-navigation.md
@@ -0,0 +1,163 @@
+---
+title: Keyboard Navigation Support
+impact: HIGH
+impactDescription: Critical for keyboard-only users
+tags: accessibility, a11y, keyboard, focus-management
+---
+
+## Keyboard Navigation Support
+
+Ensure all interactive elements are keyboard accessible and focus management is handled properly, especially in modals and dynamic content.
+
+**Incorrect (no keyboard support):**
+
+```glimmer-js
+// app/components/dropdown.gjs
+
+
+ Menu
+ {{#if this.isOpen}}
+
+ {{/if}}
+
+
+```
+
+**Correct (full keyboard support with custom modifier):**
+
+```javascript
+// app/modifiers/focus-first.js
+import { modifier } from 'ember-modifier';
+
+export default modifier((element, [selector = 'button']) => {
+ // Focus first matching element when modifier runs
+ element.querySelector(selector)?.focus();
+});
+```
+
+```glimmer-js
+// app/components/dropdown.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import { fn } from '@ember/helper';
+import focusFirst from '../modifiers/focus-first';
+
+class Dropdown extends Component {
+ @tracked isOpen = false;
+
+ @action
+ toggleMenu() {
+ this.isOpen = !this.isOpen;
+ }
+
+ @action
+ handleButtonKeyDown(event) {
+ if (event.key === 'ArrowDown') {
+ event.preventDefault();
+ this.isOpen = true;
+ }
+ }
+
+ @action
+ handleMenuKeyDown(event) {
+ if (event.key === 'Escape') {
+ this.isOpen = false;
+ // Return focus to button
+ event.target.closest('.dropdown').querySelector('button').focus();
+ }
+ // Handle arrow key navigation between menu items
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
+ event.preventDefault();
+ this.moveFocus(event.key === 'ArrowDown' ? 1 : -1);
+ }
+ }
+
+ moveFocus(direction) {
+ const items = Array.from(document.querySelectorAll('[role="menuitem"] button'));
+ const currentIndex = items.indexOf(document.activeElement);
+ const nextIndex = (currentIndex + direction + items.length) % items.length;
+ items[nextIndex]?.focus();
+ }
+
+ @action
+ selectOption(value) {
+ this.args.onSelect?.(value);
+ this.isOpen = false;
+ }
+
+
+
+
+ Menu
+
+
+ {{#if this.isOpen}}
+
+ {{/if}}
+
+
+}
+```
+
+**For focus trapping in modals, use ember-focus-trap:**
+
+```bash
+ember install ember-focus-trap
+```
+
+```glimmer-js
+// app/components/modal.gjs
+import FocusTrap from 'ember-focus-trap/components/focus-trap';
+
+
+ {{#if this.showModal}}
+
+
+
{{@title}}
+ {{yield}}
+ Close
+
+
+ {{/if}}
+
+```
+
+**Alternative: Use libraries for keyboard support:**
+
+For complex keyboard interactions, consider using libraries that abstract keyboard support patterns:
+
+```bash
+npm install @fluentui/keyboard-keys
+```
+
+Or use [tabster](https://tabster.io/) for comprehensive keyboard navigation management including focus trapping, arrow key navigation, and modalizers.
+
+Proper keyboard navigation ensures all users can interact with your application effectively.
+
+Reference: [Ember Accessibility - Keyboard](https://guides.emberjs.com/release/accessibility/keyboard/)
diff --git a/.agents/skills/ember-best-practices/rules/a11y-route-announcements.md b/.agents/skills/ember-best-practices/rules/a11y-route-announcements.md
new file mode 100644
index 0000000..1db0be7
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/a11y-route-announcements.md
@@ -0,0 +1,174 @@
+---
+title: Announce Route Transitions to Screen Readers
+impact: HIGH
+impactDescription: Critical for screen reader navigation
+tags: accessibility, a11y, routing, screen-readers
+---
+
+## Announce Route Transitions to Screen Readers
+
+Announce page title changes and route transitions to screen readers so users know when navigation has occurred.
+
+**Incorrect (no announcements):**
+
+```javascript
+// app/router.js
+export default class Router extends EmberRouter {
+ location = config.locationType;
+ rootURL = config.rootURL;
+}
+```
+
+**Correct (using a11y-announcer library - recommended):**
+
+Use the [a11y-announcer](https://github.com/ember-a11y/a11y-announcer) library for robust route announcements:
+
+```bash
+ember install @ember-a11y/a11y-announcer
+```
+
+```javascript
+// app/router.js
+import EmberRouter from '@ember/routing/router';
+import config from './config/environment';
+
+export default class Router extends EmberRouter {
+ location = config.locationType;
+ rootURL = config.rootURL;
+}
+
+Router.map(function () {
+ this.route('about');
+ this.route('dashboard');
+ this.route('posts', function () {
+ this.route('post', { path: '/:post_id' });
+ });
+});
+```
+
+The a11y-announcer library automatically handles route announcements. For custom announcements in your routes:
+
+```javascript
+// app/routes/dashboard.js
+import Route from '@ember/routing/route';
+import { service } from '@ember/service';
+
+export default class DashboardRoute extends Route {
+ @service announcer;
+
+ afterModel() {
+ this.announcer.announce('Loaded dashboard with latest data');
+ }
+}
+```
+
+**Alternative: DIY approach with ARIA live regions:**
+
+If you prefer not to use a library, you can implement route announcements yourself:
+
+```javascript
+// app/router.js
+import EmberRouter from '@ember/routing/router';
+import config from './config/environment';
+
+export default class Router extends EmberRouter {
+ location = config.locationType;
+ rootURL = config.rootURL;
+}
+
+Router.map(function () {
+ this.route('about');
+ this.route('dashboard');
+ this.route('posts', function () {
+ this.route('post', { path: '/:post_id' });
+ });
+});
+```
+
+```javascript
+// app/routes/application.js
+import Route from '@ember/routing/route';
+import { service } from '@ember/service';
+
+export default class ApplicationRoute extends Route {
+ @service router;
+
+ constructor() {
+ super(...arguments);
+
+ this.router.on('routeDidChange', (transition) => {
+ // Update document title
+ const title = this.getPageTitle(transition.to);
+ document.title = title;
+
+ // Announce to screen readers
+ this.announceRouteChange(title);
+ });
+ }
+
+ getPageTitle(route) {
+ // Get title from route metadata or generate it
+ return route.metadata?.title || route.name;
+ }
+
+ announceRouteChange(title) {
+ const announcement = document.getElementById('route-announcement');
+ if (announcement) {
+ announcement.textContent = `Navigated to ${title}`;
+ }
+ }
+}
+```
+
+```glimmer-js
+// app/routes/application.gjs
+
+
+
+ {{outlet}}
+
+```
+
+```css
+/* app/styles/app.css */
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+}
+```
+
+**Alternative: Use ember-page-title with announcements:**
+
+```bash
+ember install ember-page-title
+```
+
+```glimmer-js
+// app/routes/dashboard.gjs
+import { pageTitle } from 'ember-page-title';
+
+
+ {{pageTitle "Dashboard"}}
+
+
+ {{outlet}}
+
+
+```
+
+Route announcements ensure screen reader users know when navigation occurs, improving the overall accessibility experience.
+
+Reference: [Ember Accessibility - Page Titles](https://guides.emberjs.com/release/accessibility/page-template-considerations/)
diff --git a/.agents/skills/ember-best-practices/rules/a11y-semantic-html.md b/.agents/skills/ember-best-practices/rules/a11y-semantic-html.md
new file mode 100644
index 0000000..9c9ae45
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/a11y-semantic-html.md
@@ -0,0 +1,102 @@
+---
+title: Semantic HTML and ARIA Attributes
+impact: HIGH
+impactDescription: Essential for screen reader users
+tags: accessibility, a11y, semantic-html, aria
+---
+
+## Semantic HTML and ARIA Attributes
+
+Use semantic HTML elements and proper ARIA attributes to make your application accessible to screen reader users. **The first rule of ARIA is to not use ARIA** - prefer native semantic HTML elements whenever possible.
+
+**Key principle:** Native HTML elements have built-in keyboard support, roles, and behaviors. Only add ARIA when semantic HTML can't provide the needed functionality.
+
+**Incorrect (divs with insufficient semantics):**
+
+```glimmer-js
+// app/components/example.gjs
+
+
+ Submit
+
+
+
+
+
+ {{this.message}}
+
+
+```
+
+**Correct (semantic HTML with proper ARIA):**
+
+```glimmer-js
+// app/components/example.gjs
+import { LinkTo } from '@ember/routing';
+
+
+
+ Submit
+
+
+
+
+
+
+
+ {{this.message}}
+
+
+```
+
+**For interactive custom elements:**
+
+```glimmer-js
+// app/components/custom-button.gjs
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import XIcon from './x-icon';
+
+class CustomButton extends Component {
+ @action
+ handleKeyDown(event) {
+ // Support Enter and Space keys for keyboard users
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ this.handleClick();
+ }
+ }
+
+ @action
+ handleClick() {
+ this.args.onClick?.();
+ }
+
+
+
+
+
+
+}
+```
+
+Always use native semantic elements when possible. When creating custom interactive elements, ensure they're keyboard accessible and have proper ARIA attributes.
+
+**References:**
+
+- [ARIA Authoring Practices Guide (W3C)](https://www.w3.org/WAI/ARIA/apg/)
+- [Using ARIA (W3C)](https://www.w3.org/TR/using-aria/)
+- [ARIA in HTML (WHATWG)](https://html.spec.whatwg.org/multipage/aria.html#aria)
+- [Ember Accessibility Guide](https://guides.emberjs.com/release/accessibility/)
diff --git a/.agents/skills/ember-best-practices/rules/advanced-concurrency.md b/.agents/skills/ember-best-practices/rules/advanced-concurrency.md
new file mode 100644
index 0000000..47aa018
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/advanced-concurrency.md
@@ -0,0 +1,198 @@
+---
+title: Use Ember Concurrency for User Input Concurrency
+impact: HIGH
+impactDescription: Better control of user-initiated async operations
+tags: ember-concurrency, tasks, user-input, concurrency-patterns
+---
+
+## Use Ember Concurrency for User Input Concurrency
+
+Use ember-concurrency for managing **user-initiated** async operations like search, form submission, and autocomplete. It provides automatic cancelation, debouncing, and prevents race conditions from user actions.
+
+**Incorrect (manual async handling with race conditions):**
+
+```glimmer-js
+// app/components/search.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+
+class Search extends Component {
+ @tracked results = [];
+ @tracked isSearching = false;
+ @tracked error = null;
+ currentRequest = null;
+
+ @action
+ async search(event) {
+ const query = event.target.value;
+
+ // Manual cancelation - easy to get wrong
+ if (this.currentRequest) {
+ this.currentRequest.abort();
+ }
+
+ this.isSearching = true;
+ this.error = null;
+
+ const controller = new AbortController();
+ this.currentRequest = controller;
+
+ try {
+ const response = await fetch(`/api/search?q=${query}`, {
+ signal: controller.signal,
+ });
+ this.results = await response.json();
+ } catch (e) {
+ if (e.name !== 'AbortError') {
+ this.error = e.message;
+ }
+ } finally {
+ this.isSearching = false;
+ }
+ }
+
+
+
+ {{#if this.isSearching}}Loading...{{/if}}
+ {{#if this.error}}Error: {{this.error}}{{/if}}
+
+}
+```
+
+**Correct (using ember-concurrency with task return values):**
+
+```glimmer-js
+// app/components/search.gjs
+import Component from '@glimmer/component';
+import { restartableTask } from 'ember-concurrency';
+
+class Search extends Component {
+ // restartableTask automatically cancels previous searches
+ // IMPORTANT: Return the value, don't set tracked state inside tasks
+ searchTask = restartableTask(async (query) => {
+ const response = await fetch(`/api/search?q=${query}`);
+ return response.json(); // Return, don't set @tracked
+ });
+
+
+
+
+ {{#if this.searchTask.isRunning}}
+ Loading...
+ {{/if}}
+
+ {{#if this.searchTask.last.isSuccessful}}
+
+ {{#each this.searchTask.last.value as |result|}}
+ {{result.name}}
+ {{/each}}
+
+ {{/if}}
+
+ {{#if this.searchTask.last.isError}}
+ {{this.searchTask.last.error.message}}
+ {{/if}}
+
+}
+```
+
+**With debouncing for user typing:**
+
+```glimmer-js
+// app/components/autocomplete.gjs
+import Component from '@glimmer/component';
+import { restartableTask, timeout } from 'ember-concurrency';
+
+class Autocomplete extends Component {
+ searchTask = restartableTask(async (query) => {
+ // Debounce user typing - wait 300ms
+ await timeout(300);
+
+ const response = await fetch(`/api/autocomplete?q=${query}`);
+ return response.json(); // Return value, don't set tracked state
+ });
+
+
+
+
+ {{#if this.searchTask.isRunning}}
+
+ {{/if}}
+
+ {{#if this.searchTask.lastSuccessful}}
+
+ {{#each this.searchTask.lastSuccessful.value as |item|}}
+ {{item.title}}
+ {{/each}}
+
+ {{/if}}
+
+}
+```
+
+**Task modifiers for different user concurrency patterns:**
+
+```glimmer-js
+import Component from '@glimmer/component';
+import { dropTask, enqueueTask, restartableTask } from 'ember-concurrency';
+
+class FormActions extends Component {
+ // dropTask: Prevents double-click - ignores new while running
+ saveTask = dropTask(async (data) => {
+ const response = await fetch('/api/save', {
+ method: 'POST',
+ body: JSON.stringify(data),
+ });
+ return response.json();
+ });
+
+ // enqueueTask: Queues user actions sequentially
+ processTask = enqueueTask(async (item) => {
+ const response = await fetch('/api/process', {
+ method: 'POST',
+ body: JSON.stringify(item),
+ });
+ return response.json();
+ });
+
+ // restartableTask: Cancels previous, starts new (for search)
+ searchTask = restartableTask(async (query) => {
+ const response = await fetch(`/api/search?q=${query}`);
+ return response.json();
+ });
+
+
+
+ Save
+
+
+}
+```
+
+**Key Principles for ember-concurrency:**
+
+1. **User-initiated only** - Use for handling user actions, not component initialization
+2. **Return values** - Use `task.last.value`, never set `@tracked` state inside tasks
+3. **Avoid side effects** - Don't modify component state that's read during render inside tasks
+4. **Choose right modifier**:
+ - `restartableTask` - User typing/search (cancel previous)
+ - `dropTask` - Form submit/save (prevent double-click)
+ - `enqueueTask` - Sequential processing (queue user actions)
+
+**When NOT to use ember-concurrency:**
+
+- ❌ Component initialization data loading (use `getPromiseState` instead)
+- ❌ Setting tracked state inside tasks (causes infinite render loops)
+- ❌ Route model hooks (return promises directly)
+- ❌ Simple async without user concurrency concerns (use async/await)
+
+See **advanced-data-loading-with-ember-concurrency.md** for correct data loading patterns.
+
+ember-concurrency provides automatic cancelation, derived state (isRunning, isIdle), and better patterns for **user-initiated** async operations.
+
+Reference: [ember-concurrency](https://ember-concurrency.com/)
diff --git a/.agents/skills/ember-best-practices/rules/advanced-data-loading-with-ember-concurrency.md b/.agents/skills/ember-best-practices/rules/advanced-data-loading-with-ember-concurrency.md
new file mode 100644
index 0000000..45e96c4
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/advanced-data-loading-with-ember-concurrency.md
@@ -0,0 +1,243 @@
+---
+title: Use Ember Concurrency Correctly - User Concurrency Not Data Loading
+impact: HIGH
+impactDescription: Prevents infinite render loops and improves performance
+tags: ember-concurrency, tasks, data-loading, anti-pattern
+---
+
+## Use Ember Concurrency Correctly - User Concurrency Not Data Loading
+
+ember-concurrency is designed for **user-initiated concurrency patterns** (debouncing, throttling, preventing double-clicks), not data loading. Use task return values, don't set tracked state inside tasks.
+
+**Incorrect (using ember-concurrency for data loading with tracked state):**
+
+```glimmer-js
+// app/components/user-profile.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { task } from 'ember-concurrency';
+
+class UserProfile extends Component {
+ @tracked userData = null;
+ @tracked error = null;
+
+ // WRONG: Setting tracked state inside task
+ loadUserTask = task(async () => {
+ try {
+ const response = await fetch(`/api/users/${this.args.userId}`);
+ this.userData = await response.json(); // Anti-pattern!
+ } catch (e) {
+ this.error = e; // Anti-pattern!
+ }
+ });
+
+
+ {{#if this.loadUserTask.isRunning}}
+ Loading...
+ {{else if this.userData}}
+ {{this.userData.name}}
+ {{/if}}
+
+}
+```
+
+**Why This Is Wrong:**
+
+- Setting tracked state during render can cause infinite render loops
+- ember-concurrency adds overhead unnecessary for simple data loading
+- Makes component state harder to reason about
+- Can trigger multiple re-renders
+
+**Correct (use getPromiseState from warp-drive/reactiveweb for data loading):**
+
+```glimmer-js
+// app/components/user-profile.gjs
+import Component from '@glimmer/component';
+import { cached } from '@glimmer/tracking';
+import { getPromiseState } from '@warp-drive/reactiveweb';
+
+class UserProfile extends Component {
+ @cached
+ get userData() {
+ const promise = fetch(`/api/users/${this.args.userId}`).then((r) => r.json());
+ return getPromiseState(promise);
+ }
+
+
+ {{#if this.userData.isPending}}
+ Loading...
+ {{else if this.userData.isRejected}}
+ Error: {{this.userData.error.message}}
+ {{else if this.userData.isFulfilled}}
+ {{this.userData.value.name}}
+ {{/if}}
+
+}
+```
+
+**Correct (use ember-concurrency for USER input with derived data patterns):**
+
+```glimmer-js
+// app/components/search.gjs
+import Component from '@glimmer/component';
+import { restartableTask, timeout } from 'ember-concurrency';
+import { on } from '@ember/modifier';
+import { pick } from 'ember-composable-helpers';
+
+class Search extends Component {
+ // CORRECT: For user-initiated search with debouncing
+ // Use derived data from TaskInstance API - lastSuccessful
+ searchTask = restartableTask(async (query) => {
+ await timeout(300); // Debounce user typing
+ const response = await fetch(`/api/search?q=${query}`);
+ return response.json(); // Return value, don't set tracked state
+ });
+
+
+
+
+ {{! Use derived data from task state - no tracked properties needed }}
+ {{#if this.searchTask.isRunning}}
+ Searching...
+ {{/if}}
+
+ {{! lastSuccessful persists previous results while new search runs }}
+ {{#if this.searchTask.lastSuccessful}}
+
+ {{#each this.searchTask.lastSuccessful.value as |result|}}
+ {{result.name}}
+ {{/each}}
+
+ {{/if}}
+
+ {{! Show error from most recent failed attempt }}
+ {{#if this.searchTask.last.isError}}
+ Error: {{this.searchTask.last.error.message}}
+ {{/if}}
+
+}
+```
+
+**Good Use Cases for ember-concurrency:**
+
+1. **User input debouncing** - prevent API spam from typing
+2. **Form submission** - prevent double-click submits with `dropTask`
+3. **Autocomplete** - restart previous searches as user types
+4. **Polling** - user-controlled refresh intervals
+5. **Multi-step wizards** - sequential async operations
+
+```glimmer-js
+// app/components/form-submit.gjs
+import Component from '@glimmer/component';
+import { dropTask } from 'ember-concurrency';
+import { on } from '@ember/modifier';
+import { fn } from '@ember/helper';
+
+class FormSubmit extends Component {
+ // dropTask prevents double-submit - perfect for user actions
+ submitTask = dropTask(async (formData) => {
+ const response = await fetch('/api/save', {
+ method: 'POST',
+ body: JSON.stringify(formData),
+ });
+ return response.json();
+ });
+
+
+
+ {{#if this.submitTask.isRunning}}
+ Saving...
+ {{else}}
+ Save
+ {{/if}}
+
+
+ {{! Use lastSuccessful for success message - derived data }}
+ {{#if this.submitTask.lastSuccessful}}
+ Saved successfully!
+ {{/if}}
+
+ {{#if this.submitTask.last.isError}}
+ Error: {{this.submitTask.last.error.message}}
+ {{/if}}
+
+}
+```
+
+**Bad Use Cases for ember-concurrency:**
+
+1. ❌ **Loading data on component init** - use `getPromiseState` instead
+2. ❌ **Route model hooks** - just return promises directly
+3. ❌ **Simple API calls** - async/await is sufficient
+4. ❌ **Setting tracked state inside tasks** - causes render loops
+
+**Key Principles:**
+
+- **Derive data, don't set it** - Use `task.lastSuccessful`, `task.last`, `task.isRunning` (derived from TaskInstance API)
+- **Use task return values** - Read from `task.lastSuccessful.value` or `task.last.value`, never set tracked state
+- **User-initiated only** - ember-concurrency is for handling user concurrency patterns
+- **Data loading** - Use `getPromiseState` from warp-drive/reactiveweb for non-user-initiated loading
+- **Avoid side effects** - Don't modify component state inside tasks that's read during render
+
+**TaskInstance API for Derived Data:**
+
+ember-concurrency provides a powerful derived data API through Task and TaskInstance:
+
+- `task.last` - The most recent TaskInstance (successful or failed)
+- `task.lastSuccessful` - The most recent successful TaskInstance (persists during new attempts)
+- `task.isRunning` - Derived boolean if any instance is running
+- `taskInstance.value` - The returned value from the task
+- `taskInstance.isError` - Derived boolean if this instance failed
+- `taskInstance.error` - The error if this instance failed
+
+This follows the **derived data pattern** - all state comes from the task itself, no tracked properties needed!
+
+References:
+
+- [TaskInstance API](https://ember-concurrency.com/api/TaskInstance.html)
+- [Task API](https://ember-concurrency.com/api/Task.html)
+
+**Migration from tracked state pattern:**
+
+```glimmer-js
+// BEFORE (anti-pattern - setting tracked state)
+class Bad extends Component {
+ @tracked data = null;
+
+ fetchTask = task(async () => {
+ this.data = await fetch('/api/data').then((r) => r.json());
+ });
+
+ // template reads: {{this.data}}
+}
+
+// AFTER (correct - using derived data from TaskInstance API)
+class Good extends Component {
+ fetchTask = restartableTask(async () => {
+ return fetch('/api/data').then((r) => r.json());
+ });
+
+ // template reads: {{this.fetchTask.lastSuccessful.value}}
+ // All state derived from task - no tracked properties!
+}
+
+// Or better yet, for non-user-initiated loading:
+class Better extends Component {
+ @cached
+ get data() {
+ return getPromiseState(fetch('/api/data').then((r) => r.json()));
+ }
+
+ // template reads: {{#if this.data.isFulfilled}}{{this.data.value}}{{/if}}
+}
+```
+
+ember-concurrency is a powerful tool for **user concurrency patterns**. For data loading, use `getPromiseState` instead.
+
+Reference:
+
+- [ember-concurrency](https://ember-concurrency.com/)
+- [warp-drive/reactiveweb](https://github.com/emberjs/data/tree/main/packages/reactiveweb)
diff --git a/.agents/skills/ember-best-practices/rules/advanced-helpers.md b/.agents/skills/ember-best-practices/rules/advanced-helpers.md
new file mode 100644
index 0000000..fa1857f
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/advanced-helpers.md
@@ -0,0 +1,156 @@
+---
+title: Use Helper Functions for Reusable Logic
+impact: LOW-MEDIUM
+impactDescription: Better code reuse and testability
+tags: helpers, templates, reusability, advanced
+---
+
+## Use Helper Functions for Reusable Logic
+
+Extract reusable template logic into helper functions that can be tested independently and used across templates.
+
+**Incorrect (logic duplicated in components):**
+
+```javascript
+// app/components/user-card.js
+class UserCard extends Component {
+ get formattedDate() {
+ const date = new Date(this.args.user.createdAt);
+ const now = new Date();
+ const diffMs = now - date;
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+
+ if (diffDays === 0) return 'Today';
+ if (diffDays === 1) return 'Yesterday';
+ if (diffDays < 7) return `${diffDays} days ago`;
+ return date.toLocaleDateString();
+ }
+}
+
+// app/components/post-card.js - same logic duplicated!
+class PostCard extends Component {
+ get formattedDate() {
+ // Same implementation...
+ }
+}
+```
+
+**Correct (reusable helper):**
+
+For single-use helpers, keep them in the same file as the component:
+
+```glimmer-js
+// app/components/post-list.gjs
+import Component from '@glimmer/component';
+
+// Helper co-located in same file
+function formatRelativeDate(date) {
+ const dateObj = new Date(date);
+ const now = new Date();
+ const diffMs = now - dateObj;
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+
+ if (diffDays === 0) return 'Today';
+ if (diffDays === 1) return 'Yesterday';
+ if (diffDays < 7) return `${diffDays} days ago`;
+ return dateObj.toLocaleDateString();
+}
+
+class PostList extends Component {
+
+ {{#each @posts as |post|}}
+
+ {{post.title}}
+ {{formatRelativeDate post.createdAt}}
+
+ {{/each}}
+
+}
+```
+
+For helpers shared across multiple components in a feature, use a subdirectory:
+
+```javascript
+// app/components/blog/format-relative-date.js
+export function formatRelativeDate(date) {
+ const dateObj = new Date(date);
+ const now = new Date();
+ const diffMs = now - dateObj;
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+
+ if (diffDays === 0) return 'Today';
+ if (diffDays === 1) return 'Yesterday';
+ if (diffDays < 7) return `${diffDays} days ago`;
+ return dateObj.toLocaleDateString();
+}
+```
+
+**Alternative (shared helper in utils):**
+
+For truly shared helpers used across the whole app, use `app/utils/`:
+
+```javascript
+// app/utils/format-relative-date.js
+// Flat structure - use subpath-imports in package.json for nicer imports if needed
+export function formatRelativeDate(date) {
+ const dateObj = new Date(date);
+ const now = new Date();
+ const diffMs = now - dateObj;
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+
+ if (diffDays === 0) return 'Today';
+ if (diffDays === 1) return 'Yesterday';
+ if (diffDays < 7) return `${diffDays} days ago`;
+ return dateObj.toLocaleDateString();
+}
+```
+
+**Note**: Keep utils flat (`app/utils/format-relative-date.js`), not nested (`app/utils/date/format-relative-date.js`). If you need cleaner top-level imports, configure subpath-imports in package.json instead of nesting files.
+
+```glimmer-js
+// app/components/user-card.gjs
+import { formatRelativeDate } from '../utils/format-relative-date';
+
+
+ Joined: {{formatRelativeDate @user.createdAt}}
+
+```
+
+```glimmer-js
+// app/components/post-card.gjs
+import { formatRelativeDate } from '../utils/format-relative-date';
+
+
+ Posted: {{formatRelativeDate @post.createdAt}}
+
+```
+
+**For helpers with state, use class-based helpers:**
+
+```javascript
+// app/utils/helpers/format-currency.js
+export class FormatCurrencyHelper {
+ constructor(owner) {
+ this.intl = owner.lookup('service:intl');
+ }
+
+ compute(amount, { currency = 'USD' } = {}) {
+ return this.intl.formatNumber(amount, {
+ style: 'currency',
+ currency,
+ });
+ }
+}
+```
+
+**Common helpers to create:**
+
+- Date/time formatting
+- Number formatting
+- String manipulation
+- Array operations
+- Conditional logic
+
+Helpers promote code reuse, are easier to test, and keep components focused on behavior.
+
+Reference: [Ember Helpers](https://guides.emberjs.com/release/components/helper-functions/)
diff --git a/.agents/skills/ember-best-practices/rules/advanced-modifiers.md b/.agents/skills/ember-best-practices/rules/advanced-modifiers.md
new file mode 100644
index 0000000..7d0c11d
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/advanced-modifiers.md
@@ -0,0 +1,129 @@
+---
+title: Use Modifiers for DOM Side Effects
+impact: LOW-MEDIUM
+impactDescription: Better separation of concerns
+tags: modifiers, dom, lifecycle, advanced
+---
+
+## Use Modifiers for DOM Side Effects
+
+Use modifiers (element modifiers) to handle DOM side effects and lifecycle events in a reusable, composable way.
+
+**Incorrect (manual DOM manipulation in component):**
+
+```glimmer-js
+// app/components/chart.gjs
+import Component from '@glimmer/component';
+
+class Chart extends Component {
+ chartInstance = null;
+
+ constructor() {
+ super(...arguments);
+ // Can't access element here - element doesn't exist yet!
+ }
+
+ willDestroy() {
+ super.willDestroy();
+ this.chartInstance?.destroy();
+ }
+
+
+
+ {{! Manual setup is error-prone and not reusable }}
+
+}
+```
+
+**Correct (function modifier - preferred for simple side effects):**
+
+```javascript
+// app/modifiers/chart.js
+import { modifier } from 'ember-modifier';
+
+export default modifier((element, [config]) => {
+ // Initialize chart
+ const chartInstance = new Chart(element, config);
+
+ // Return cleanup function
+ return () => {
+ chartInstance.destroy();
+ };
+});
+```
+
+**Also correct (class-based modifier for complex state):**
+
+```javascript
+// app/modifiers/chart.js
+import Modifier from 'ember-modifier';
+import { registerDestructor } from '@ember/destroyable';
+
+export default class ChartModifier extends Modifier {
+ chartInstance = null;
+
+ modify(element, [config]) {
+ // Cleanup previous instance if config changed
+ if (this.chartInstance) {
+ this.chartInstance.destroy();
+ }
+
+ this.chartInstance = new Chart(element, config);
+
+ // Register cleanup
+ registerDestructor(this, () => {
+ this.chartInstance?.destroy();
+ });
+ }
+}
+```
+
+```glimmer-js
+// app/components/chart.gjs
+import chart from '../modifiers/chart';
+
+
+
+
+```
+
+**Use function modifiers** for simple side effects. Use class-based modifiers only when you need complex state management.
+
+**For commonly needed modifiers, use ember-modifier helpers:**
+
+```javascript
+// app/modifiers/autofocus.js
+import { modifier } from 'ember-modifier';
+
+export default modifier((element) => {
+ element.focus();
+});
+```
+
+```glimmer-js
+// app/components/input-field.gjs
+import autofocus from '../modifiers/autofocus';
+
+
+```
+
+**Use ember-resize-observer-modifier for resize handling:**
+
+```bash
+ember install ember-resize-observer-modifier
+```
+
+```glimmer-js
+// app/components/resizable.gjs
+import onResize from 'ember-resize-observer-modifier';
+
+
+
+ Content that responds to size changes
+
+
+```
+
+Modifiers provide a clean, reusable way to manage DOM side effects without coupling to specific components.
+
+Reference: [Ember Modifiers](https://guides.emberjs.com/release/components/template-lifecycle-dom-and-modifiers/)
diff --git a/.agents/skills/ember-best-practices/rules/advanced-tracked-built-ins.md b/.agents/skills/ember-best-practices/rules/advanced-tracked-built-ins.md
new file mode 100644
index 0000000..ddd618a
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/advanced-tracked-built-ins.md
@@ -0,0 +1,277 @@
+---
+title: Use Reactive Collections from @ember/reactive/collections
+impact: HIGH
+impactDescription: Enables reactive arrays, maps, and sets
+tags: reactivity, tracked, collections, advanced
+---
+
+## Use Reactive Collections from @ember/reactive/collections
+
+Use reactive collections from `@ember/reactive/collections` to make arrays, Maps, and Sets reactive in Ember. Standard JavaScript collections don't trigger Ember's reactivity system when mutated—reactive collections solve this.
+
+**The Problem:**
+Standard arrays, Maps, and Sets are not reactive in Ember when you mutate them. Changes won't trigger template updates.
+
+**The Solution:**
+Use Ember's built-in reactive collections from `@ember/reactive/collections`.
+
+### Reactive Arrays
+
+**Incorrect (non-reactive array):**
+
+```glimmer-js
+// app/components/todo-list.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+
+export default class TodoList extends Component {
+ @tracked todos = []; // ❌ Array mutations (push, splice, etc.) won't trigger updates
+
+ @action
+ addTodo(text) {
+ // This won't trigger a re-render!
+ this.todos.push({ id: Date.now(), text });
+ }
+
+ @action
+ removeTodo(id) {
+ // This also won't trigger a re-render!
+ const index = this.todos.findIndex((t) => t.id === id);
+ this.todos.splice(index, 1);
+ }
+
+
+
+ {{#each this.todos as |todo|}}
+
+ {{todo.text}}
+ Remove
+
+ {{/each}}
+
+ Add
+
+}
+```
+
+**Correct (reactive array with @ember/reactive/collections):**
+
+```glimmer-js
+// app/components/todo-list.gjs
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { trackedArray } from '@ember/reactive/collections';
+
+export default class TodoList extends Component {
+ todos = trackedArray([]); // ✅ Mutations are reactive
+
+ @action
+ addTodo(text) {
+ // Now this triggers re-render!
+ this.todos.push({ id: Date.now(), text });
+ }
+
+ @action
+ removeTodo(id) {
+ // This also triggers re-render!
+ const index = this.todos.findIndex((t) => t.id === id);
+ this.todos.splice(index, 1);
+ }
+
+
+
+ {{#each this.todos as |todo|}}
+
+ {{todo.text}}
+ Remove
+
+ {{/each}}
+
+ Add
+
+}
+```
+
+### Reactive Maps
+
+Maps are useful for key-value stores with non-string keys:
+
+```glimmer-js
+// app/components/user-cache.gjs
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { trackedMap } from '@ember/reactive/collections';
+
+export default class UserCache extends Component {
+ userCache = trackedMap(); // key: userId, value: userData
+
+ @action
+ cacheUser(userId, userData) {
+ this.userCache.set(userId, userData);
+ }
+
+ @action
+ clearUser(userId) {
+ this.userCache.delete(userId);
+ }
+
+ get cachedUsers() {
+ return Array.from(this.userCache.values());
+ }
+
+
+
+ {{#each this.cachedUsers as |user|}}
+ {{user.name}}
+ {{/each}}
+
+ Cache size: {{this.userCache.size}}
+
+}
+```
+
+### Reactive Sets
+
+Sets are useful for unique collections:
+
+```glimmer-js
+// app/components/tag-selector.gjs
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { trackedSet } from '@ember/reactive/collections';
+
+export default class TagSelector extends Component {
+ selectedTags = trackedSet();
+
+ @action
+ toggleTag(tag) {
+ if (this.selectedTags.has(tag)) {
+ this.selectedTags.delete(tag);
+ } else {
+ this.selectedTags.add(tag);
+ }
+ }
+
+ get selectedCount() {
+ return this.selectedTags.size;
+ }
+
+
+
+ {{#each @availableTags as |tag|}}
+
+
+ {{tag}}
+
+ {{/each}}
+
+ Selected: {{this.selectedCount}} tags
+
+}
+```
+
+### When to Use Each Type
+
+| Type | Use Case |
+| -------------- | ------------------------------------------------------------------ |
+| `trackedArray` | Ordered lists that need mutation methods (push, pop, splice, etc.) |
+| `trackedMap` | Key-value pairs with non-string keys or when you need `size` |
+| `trackedSet` | Unique values, membership testing |
+
+### Common Patterns
+
+**Initialize with data:**
+
+```javascript
+import { trackedArray, trackedMap, trackedSet } from '@ember/reactive/collections';
+
+// Array
+const todos = trackedArray([
+ { id: 1, text: 'First' },
+ { id: 2, text: 'Second' },
+]);
+
+// Map
+const userMap = trackedMap([
+ [1, { name: 'Alice' }],
+ [2, { name: 'Bob' }],
+]);
+
+// Set
+const tags = trackedSet(['javascript', 'ember', 'web']);
+```
+
+**Convert to plain JavaScript:**
+
+```javascript
+// Array
+const plainArray = [...trackedArray];
+const plainArray2 = Array.from(trackedArray);
+
+// Map
+const plainObject = Object.fromEntries(trackedMap);
+
+// Set
+const plainArray3 = [...trackedSet];
+```
+
+**Functional array methods still work:**
+
+```javascript
+const todos = trackedArray([...]);
+
+// All of these work and are reactive
+const completed = todos.filter(t => t.done);
+const titles = todos.map(t => t.title);
+const allDone = todos.every(t => t.done);
+const firstIncomplete = todos.find(t => !t.done);
+```
+
+### Alternative: Immutable Updates
+
+If you prefer immutability, you can use regular `@tracked` with reassignment:
+
+```javascript
+import { tracked } from '@glimmer/tracking';
+
+export default class TodoList extends Component {
+ @tracked todos = [];
+
+ @action
+ addTodo(text) {
+ // Reassignment is reactive
+ this.todos = [...this.todos, { id: Date.now(), text }];
+ }
+
+ @action
+ removeTodo(id) {
+ // Reassignment is reactive
+ this.todos = this.todos.filter((t) => t.id !== id);
+ }
+}
+```
+
+**When to use each approach:**
+
+- Use reactive collections when you need mutable operations (better performance for large lists)
+- Use immutable updates when you want simpler mental model or need history/undo
+
+### Best Practices
+
+1. **Don't mix approaches** - choose either reactive collections or immutable updates
+2. **Initialize in class field** - no need for constructor
+3. **Use appropriate type** - Map for key-value, Set for unique values, Array for ordered lists
+4. **Export from modules** if shared across components
+
+Reactive collections from `@ember/reactive/collections` provide the best of both worlds: mutable operations with full reactivity. They're especially valuable for large lists or frequent updates where immutable updates would be expensive.
+
+**References:**
+
+- [Ember Reactivity System](https://guides.emberjs.com/release/in-depth-topics/autotracking-in-depth/)
+- [JavaScript Built-in Objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects)
+- [Reactive Collections RFC](https://github.com/emberjs/rfcs/blob/master/text/0869-reactive-collections.md)
diff --git a/.agents/skills/ember-best-practices/rules/bundle-direct-imports.md b/.agents/skills/ember-best-practices/rules/bundle-direct-imports.md
new file mode 100644
index 0000000..2c4d33a
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/bundle-direct-imports.md
@@ -0,0 +1,62 @@
+---
+title: Avoid Importing Entire Addon Namespaces
+impact: CRITICAL
+impactDescription: 200-500ms import cost reduction
+tags: bundle, imports, tree-shaking, performance
+---
+
+## Avoid Importing Entire Addon Namespaces
+
+Import specific utilities and components directly rather than entire addon namespaces to enable better tree-shaking and reduce bundle size.
+
+**Incorrect (imports entire namespace):**
+
+```javascript
+import { tracked } from '@glimmer/tracking';
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+// OK - these are already optimized
+
+// But avoid this pattern with utility libraries:
+import * as lodash from 'lodash';
+import * as moment from 'moment';
+
+class My extends Component {
+ someMethod() {
+ return lodash.debounce(this.handler, 300);
+ }
+}
+```
+
+**Correct (direct imports):**
+
+```javascript
+import { tracked } from '@glimmer/tracking';
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import debounce from 'lodash/debounce';
+import dayjs from 'dayjs'; // moment alternative, smaller
+
+class My extends Component {
+ someMethod() {
+ return debounce(this.handler, 300);
+ }
+}
+```
+
+**Even better (use Ember utilities when available):**
+
+```javascript
+import { tracked } from '@glimmer/tracking';
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { debounce } from '@ember/runloop';
+
+class My extends Component {
+ someMethod() {
+ return debounce(this, this.handler, 300);
+ }
+}
+```
+
+Direct imports and using built-in Ember utilities reduce bundle size by avoiding unused code.
diff --git a/.agents/skills/ember-best-practices/rules/bundle-embroider-static.md b/.agents/skills/ember-best-practices/rules/bundle-embroider-static.md
new file mode 100644
index 0000000..3826392
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/bundle-embroider-static.md
@@ -0,0 +1,69 @@
+---
+title: Use Embroider Build Pipeline
+impact: CRITICAL
+impactDescription: Modern build system with better performance
+tags: bundle, embroider, build-performance, vite
+---
+
+## Use Embroider Build Pipeline
+
+Use Embroider, Ember's modern build pipeline, with Vite for faster builds, better tree-shaking, and smaller bundles.
+
+**Incorrect (classic build pipeline):**
+
+```javascript
+// ember-cli-build.js
+const EmberApp = require('ember-cli/lib/broccoli/ember-app');
+
+module.exports = function (defaults) {
+ const app = new EmberApp(defaults, {});
+ return app.toTree();
+};
+```
+
+**Correct (Embroider with Vite):**
+
+```javascript
+// ember-cli-build.js
+const EmberApp = require('ember-cli/lib/broccoli/ember-app');
+const { compatBuild } = require('@embroider/compat');
+
+module.exports = async function (defaults) {
+ const { buildOnce } = await import('@embroider/vite');
+
+ let app = new EmberApp(defaults, {
+ // Add options here
+ });
+
+ return compatBuild(app, buildOnce);
+};
+```
+
+**For stricter static analysis (optimized mode):**
+
+```javascript
+// ember-cli-build.js
+const EmberApp = require('ember-cli/lib/broccoli/ember-app');
+const { compatBuild } = require('@embroider/compat');
+
+module.exports = async function (defaults) {
+ const { buildOnce } = await import('@embroider/vite');
+
+ let app = new EmberApp(defaults, {
+ // Add options here
+ });
+
+ return compatBuild(app, buildOnce, {
+ // Enable static analysis for better tree-shaking
+ staticAddonTestSupportTrees: true,
+ staticAddonTrees: true,
+ staticHelpers: true,
+ staticModifiers: true,
+ staticComponents: true,
+ });
+};
+```
+
+Embroider provides a modern build pipeline with Vite that offers faster builds and better optimization compared to the classic Ember CLI build system.
+
+Reference: [Embroider Documentation](https://github.com/embroider-build/embroider)
diff --git a/.agents/skills/ember-best-practices/rules/bundle-lazy-dependencies.md b/.agents/skills/ember-best-practices/rules/bundle-lazy-dependencies.md
new file mode 100644
index 0000000..b39f7b1
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/bundle-lazy-dependencies.md
@@ -0,0 +1,71 @@
+---
+title: Lazy Load Heavy Dependencies
+impact: CRITICAL
+impactDescription: 30-50% initial bundle reduction
+tags: bundle, lazy-loading, dynamic-imports, performance
+---
+
+## Lazy Load Heavy Dependencies
+
+Use dynamic imports to load heavy libraries only when needed, reducing initial bundle size.
+
+**Incorrect (loaded upfront):**
+
+```javascript
+import Component from '@glimmer/component';
+import Chart from 'chart.js/auto'; // 300KB library loaded immediately
+import hljs from 'highlight.js'; // 500KB library loaded immediately
+
+class Dashboard extends Component {
+ get showChart() {
+ return this.args.hasData;
+ }
+}
+```
+
+**Correct (lazy loaded with error/loading state handling):**
+
+```glimmer-js
+import Component from '@glimmer/component';
+import { getPromiseState } from 'reactiveweb/promise';
+
+class Dashboard extends Component {
+ // Use getPromiseState to model promise state for error/loading handling
+ chartLoader = getPromiseState(async () => {
+ const { default: Chart } = await import('chart.js/auto');
+ return Chart;
+ });
+
+ highlighterLoader = getPromiseState(async () => {
+ const { default: hljs } = await import('highlight.js');
+ return hljs;
+ });
+
+ loadChart = () => {
+ // Triggers lazy load, handles loading/error states automatically
+ return this.chartLoader.value;
+ };
+
+ highlightCode = (code) => {
+ const hljs = this.highlighterLoader.value;
+ if (hljs) {
+ return hljs.highlightAuto(code);
+ }
+ return code;
+ };
+
+
+ {{#if this.chartLoader.isLoading}}
+ Loading chart library...
+ {{else if this.chartLoader.isError}}
+ Error loading chart: {{this.chartLoader.error.message}}
+ {{else if this.chartLoader.isResolved}}
+
+ {{/if}}
+
+}
+```
+
+**Note**: Always model promise state (loading/error/resolved) using `getPromiseState` from `reactiveweb/promise` to handle slow networks and errors properly.
+
+Dynamic imports reduce initial bundle size by 30-50%, improving Time to Interactive.
diff --git a/.agents/skills/ember-best-practices/rules/component-args-validation.md b/.agents/skills/ember-best-practices/rules/component-args-validation.md
new file mode 100644
index 0000000..d4b622a
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/component-args-validation.md
@@ -0,0 +1,174 @@
+---
+title: Validate Component Arguments
+impact: MEDIUM
+impactDescription: Better error messages and type safety
+tags: components, validation, arguments, typescript
+---
+
+## Validate Component Arguments
+
+Validate component arguments for better error messages, documentation, and type safety.
+
+**Incorrect (no argument validation):**
+
+```glimmer-js
+// app/components/user-card.gjs
+import Component from '@glimmer/component';
+
+class UserCard extends Component {
+
+
+
{{@user.name}}
+
{{@user.email}}
+
+
+}
+```
+
+**Correct (with TypeScript signature):**
+
+```glimmer-ts
+// app/components/user-card.gts
+import Component from '@glimmer/component';
+
+interface UserCardSignature {
+ Args: {
+ user: {
+ name: string;
+ email: string;
+ avatarUrl?: string;
+ };
+ onEdit?: (user: UserCardSignature['Args']['user']) => void;
+ };
+ Blocks: {
+ default: [];
+ };
+ Element: HTMLDivElement;
+}
+
+class UserCard extends Component {
+
+
+
{{@user.name}}
+
{{@user.email}}
+
+ {{#if @user.avatarUrl}}
+
+ {{/if}}
+
+ {{#if @onEdit}}
+
Edit
+ {{/if}}
+
+ {{yield}}
+
+
+}
+```
+
+**Runtime validation with assertions (using getters):**
+
+```glimmer-js
+// app/components/data-table.gjs
+import Component from '@glimmer/component';
+import { assert } from '@ember/debug';
+
+class DataTable extends Component {
+ // Use getters so validation runs on each access and catches arg changes
+ get columns() {
+ assert(
+ 'DataTable requires @columns argument',
+ this.args.columns && Array.isArray(this.args.columns),
+ );
+
+ assert(
+ '@columns must be an array of objects with "key" and "label" properties',
+ this.args.columns.every((col) => col.key && col.label),
+ );
+
+ return this.args.columns;
+ }
+
+ get rows() {
+ assert('DataTable requires @rows argument', this.args.rows && Array.isArray(this.args.rows));
+
+ return this.args.rows;
+ }
+
+
+
+
+
+ {{#each this.columns as |column|}}
+ {{column.label}}
+ {{/each}}
+
+
+
+ {{#each this.rows as |row|}}
+
+ {{#each this.columns as |column|}}
+ {{get row column.key}}
+ {{/each}}
+
+ {{/each}}
+
+
+
+}
+```
+
+**Template-only component with TypeScript:**
+
+```glimmer-ts
+// app/components/icon.gts
+import type { TOC } from '@ember/component/template-only';
+
+interface IconSignature {
+ Args: {
+ name: string;
+ size?: 'small' | 'medium' | 'large';
+ };
+ Element: HTMLSpanElement;
+}
+
+const Icon: TOC =
+
+ ;
+
+export default Icon;
+```
+
+**Documentation with JSDoc:**
+
+```glimmer-js
+// app/components/modal.gjs
+import Component from '@glimmer/component';
+
+/**
+ * Modal dialog component
+ *
+ * @param {Object} args
+ * @param {boolean} args.isOpen - Controls modal visibility
+ * @param {() => void} args.onClose - Called when modal should close
+ * @param {string} [args.title] - Optional modal title
+ * @param {string} [args.size='medium'] - Modal size: 'small', 'medium', 'large'
+ */
+class Modal extends Component {
+
+ {{#if @isOpen}}
+
+ {{#if @title}}
+
{{@title}}
+ {{/if}}
+ {{yield}}
+ Close
+
+ {{/if}}
+
+}
+```
+
+Argument validation provides better error messages during development, serves as documentation, and enables better IDE support.
+
+Reference: [TypeScript in Ember](https://guides.emberjs.com/release/typescript/)
diff --git a/.agents/skills/ember-best-practices/rules/component-avoid-classes-in-examples.md b/.agents/skills/ember-best-practices/rules/component-avoid-classes-in-examples.md
new file mode 100644
index 0000000..0eb825c
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/component-avoid-classes-in-examples.md
@@ -0,0 +1,174 @@
+---
+title: Avoid CSS Classes in Learning Examples
+impact: LOW-MEDIUM
+impactDescription: Cleaner, more focused learning materials
+tags: documentation, examples, learning, css, classes
+---
+
+## Avoid CSS Classes in Learning Examples
+
+Don't add CSS classes to learning content and examples unless they provide actual value above the surrounding context. Classes add visual noise and distract from the concepts being taught.
+
+**Incorrect (unnecessary classes in learning example):**
+
+```glimmer-js
+// app/components/user-card.gjs
+import Component from '@glimmer/component';
+
+export class UserCard extends Component {
+
+
+
+
+ {{#if @user.avatarUrl}}
+
+ {{/if}}
+
+ {{#if @onEdit}}
+
+ Edit
+
+ {{/if}}
+
+
+ {{yield}}
+
+
+
+}
+```
+
+**Why This Is Wrong:**
+
+- Classes add visual clutter that obscures the actual concepts
+- Learners focus on naming conventions instead of the pattern being taught
+- Makes copy-paste more work (need to remove or change class names)
+- Implies these specific class names are required or best practice
+- Distracts from structural HTML and component logic
+
+**Correct (focused on concepts):**
+
+```glimmer-js
+// app/components/user-card.gjs
+import Component from '@glimmer/component';
+
+export class UserCard extends Component {
+
+
+
{{@user.name}}
+
{{@user.email}}
+
+ {{#if @user.avatarUrl}}
+
+ {{/if}}
+
+ {{#if @onEdit}}
+
Edit
+ {{/if}}
+
+ {{yield}}
+
+
+}
+```
+
+**Benefits:**
+
+- **Clarity**: Easier to understand the component structure
+- **Focus**: Reader attention stays on the concepts being taught
+- **Simplicity**: Less code to process mentally
+- **Flexibility**: Reader can add their own classes without conflict
+- **Reusability**: Examples are easier to adapt to real code
+
+**When Classes ARE Appropriate in Examples:**
+
+```glimmer-js
+// Example: Teaching about conditional classes
+export class StatusBadge extends Component {
+ get statusClass() {
+ return this.args.status === 'active' ? 'badge-success' : 'badge-error';
+ }
+
+
+
+ {{@status}}
+
+
+}
+```
+
+```glimmer-js
+// Example: Teaching about ...attributes for styling flexibility
+export class Card extends Component {
+
+ {{! Caller can add their own classes via ...attributes }}
+
+ {{yield}}
+
+
+}
+
+{{! Usage: ... }}
+```
+
+**When to Include Classes:**
+
+1. **Teaching class binding** - Example explicitly about conditional classes or class composition
+2. **Demonstrating ...attributes** - Showing how callers add classes
+3. **Accessibility** - Using classes for semantic meaning (e.g., `aria-*` helpers)
+4. **Critical to example** - Class name is essential to understanding (e.g., `selected`, `active`)
+
+**Examples Where Classes Add Value:**
+
+```glimmer-js
+// Good: Teaching about dynamic classes
+export class TabButton extends Component {
+
+
+ {{yield}}
+
+
+}
+```
+
+```glimmer-js
+// Good: Teaching about class composition
+import { cn } from 'ember-cn';
+
+export class Button extends Component {
+
+
+ {{yield}}
+
+
+}
+```
+
+**Default Stance:**
+
+When writing learning examples or documentation:
+
+1. **Start without classes** - Add them only if needed
+2. **Ask**: Does this class help explain the concept?
+3. **Remove** any decorative or structural classes that aren't essential
+4. **Use** `...attributes` to show styling flexibility
+
+**Real-World Context:**
+
+In production code, you'll have classes for styling. But in learning materials, strip them away unless they're teaching something specific about classes themselves.
+
+**Common Violations:**
+
+❌ BEM classes in examples (`user-card__header`)
+❌ Utility classes unless teaching utilities (`flex`, `mt-4`)
+❌ Semantic classes that don't teach anything (`container`, `wrapper`)
+❌ Design system classes unless teaching design system integration
+
+**Summary:**
+
+Keep learning examples focused on the concept being taught. CSS classes should appear only when they're essential to understanding the pattern or when demonstrating styling flexibility with `...attributes`.
+
+Reference: [Ember Components Guide](https://guides.emberjs.com/release/components/)
diff --git a/.agents/skills/ember-best-practices/rules/component-avoid-constructors.md b/.agents/skills/ember-best-practices/rules/component-avoid-constructors.md
new file mode 100644
index 0000000..de6e26c
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/component-avoid-constructors.md
@@ -0,0 +1,162 @@
+---
+title: Avoid Constructors in Components
+impact: HIGH
+impactDescription: Prevents infinite render loops and simplifies code
+tags: components, constructors, initialization, anti-pattern
+---
+
+## Avoid Constructors in Components
+
+**Strongly discourage constructor usage.** Modern Ember components rarely need constructors. Use class fields, @service decorators, and getPromiseState for initialization instead. Constructors with function calls that set tracked state can cause infinite render loops.
+
+**Incorrect (using constructor):**
+
+```glimmer-js
+// app/components/user-profile.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { service } from '@ember/service';
+
+class UserProfile extends Component {
+ constructor() {
+ super(...arguments);
+
+ // Anti-pattern: Manual service lookup
+ this.store = this.owner.lookup('service:store');
+ this.router = this.owner.lookup('service:router');
+
+ // Anti-pattern: Imperative initialization
+ this.data = null;
+ this.isLoading = false;
+ this.error = null;
+
+ // Anti-pattern: Side effects in constructor
+ this.loadUserData();
+ }
+
+ async loadUserData() {
+ this.isLoading = true;
+ try {
+ this.data = await this.store.request({
+ url: `/users/${this.args.userId}`,
+ });
+ } catch (e) {
+ this.error = e;
+ } finally {
+ this.isLoading = false;
+ }
+ }
+
+
+ {{#if this.isLoading}}
+ Loading...
+ {{else if this.error}}
+ Error: {{this.error.message}}
+ {{else if this.data}}
+ {{this.data.name}}
+ {{/if}}
+
+}
+```
+
+**Correct (use class fields and declarative async state):**
+
+```glimmer-js
+// app/components/user-profile.gjs
+import Component from '@glimmer/component';
+import { cached } from '@glimmer/tracking';
+import { service } from '@ember/service';
+import { getRequestState } from '@warp-drive/ember';
+
+class UserProfile extends Component {
+ @service store;
+
+ @cached
+ get userRequest() {
+ return this.store.request({
+ url: `/users/${this.args.userId}`,
+ });
+ }
+
+
+ {{#let (getRequestState this.userRequest) as |state|}}
+ {{#if state.isPending}}
+ Loading...
+ {{else if state.isError}}
+ Error loading user
+ {{else}}
+ {{state.value.name}}
+ {{/if}}
+ {{/let}}
+
+}
+```
+
+**When You Might Need a Constructor (Very Rare):**
+
+Very rarely, you might need a constructor for truly exceptional cases. Even then, use modern patterns:
+
+```glimmer-js
+// app/components/complex-setup.gjs
+import Component from '@glimmer/component';
+import { service } from '@ember/service';
+import { tracked } from '@glimmer/tracking';
+
+class ComplexSetup extends Component {
+ @service store;
+
+ @tracked state = null;
+
+ constructor(owner, args) {
+ super(owner, args);
+
+ // Only if you absolutely must do something that can't be done with class fields
+ // Even then, prefer resources or modifiers
+ if (this.args.legacyInitMode) {
+ this.initializeLegacyMode();
+ }
+ }
+
+ initializeLegacyMode() {
+ // Rare edge case initialization
+ }
+
+
+
+
+}
+```
+
+**Why Strongly Avoid Constructors:**
+
+1. **Infinite Render Loops**: Setting tracked state in constructor that's read during render causes infinite loops
+2. **Service Injection**: Use `@service` decorator instead of `owner.lookup()`
+3. **Testability**: Class fields are easier to mock and test
+4. **Clarity**: Declarative class fields show state at a glance
+5. **Side Effects**: getPromiseState and modifiers handle side effects better
+6. **Memory Leaks**: getPromiseState auto-cleanup; constructor code doesn't
+7. **Reactivity**: Class fields integrate better with tracking
+8. **Initialization Order**: No need to worry about super() call timing
+9. **Argument Validation**: Constructor validation runs only once; use getters to catch arg changes
+
+**Modern Alternatives:**
+
+| Old Pattern | Modern Alternative |
+| -------------------------------------------------------------- | -------------------------------------------------------- |
+| `constructor() { this.store = owner.lookup('service:store') }` | `@service store;` |
+| `constructor() { this.data = null; }` | `@tracked data = null;` |
+| `constructor() { this.loadData(); }` | Use `@cached get` with getPromiseState |
+| `constructor() { this.interval = setInterval(...) }` | Use modifier with registerDestructor |
+| `constructor() { this.subscription = ... }` | Use modifier or constructor with registerDestructor ONLY |
+
+**Performance Impact:**
+
+- **Before**: Constructor runs on every instantiation, manual cleanup risk, infinite loop danger
+- **After**: Class fields initialize efficiently, getPromiseState auto-cleanup, no render loops
+
+**Strongly discourage constructors** - they add complexity and infinite render loop risks. Use declarative class fields and getPromiseState instead.
+
+Reference:
+
+- [Ember Octane Guide](https://guides.emberjs.com/release/upgrading/current-edition/)
+- [warp-drive/reactiveweb](https://github.com/emberjs/data/tree/main/packages/reactiveweb)
diff --git a/.agents/skills/ember-best-practices/rules/component-avoid-lifecycle-hooks.md b/.agents/skills/ember-best-practices/rules/component-avoid-lifecycle-hooks.md
new file mode 100644
index 0000000..176d6e7
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/component-avoid-lifecycle-hooks.md
@@ -0,0 +1,322 @@
+---
+title: Avoid Legacy Lifecycle Hooks (did-insert, will-destroy, did-update)
+impact: HIGH
+impactDescription: Prevents memory leaks and enforces modern patterns
+tags: components, lifecycle, anti-pattern, modifiers, derived-data
+---
+
+## Avoid Legacy Lifecycle Hooks (did-insert, will-destroy, did-update)
+
+**Never use `{{did-insert}}`, `{{will-destroy}}`, or `{{did-update}}` in new code.** These legacy helpers create coupling between templates and component lifecycle, making code harder to test and maintain. Modern Ember provides better alternatives through derived data and custom modifiers.
+
+### Why These Are Problematic
+
+1. **Memory Leaks**: Easy to forget cleanup, especially with `did-insert`
+2. **Tight Coupling**: Mixes template concerns with JavaScript logic
+3. **Poor Testability**: Lifecycle hooks are harder to unit test
+4. **Not Composable**: Can't be easily shared across components
+5. **Deprecated Pattern**: Not recommended in modern Ember
+
+### Alternative 1: Use Derived Data
+
+For computed values or reactive transformations, use getters and `@cached`:
+
+**❌ Incorrect (did-update):**
+
+```glimmer-js
+// app/components/user-greeting.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+
+class UserGreeting extends Component {
+ @tracked displayName = '';
+
+ @action
+ updateDisplayName() {
+ // Runs on every render - inefficient and error-prone
+ this.displayName = `${this.args.user.firstName} ${this.args.user.lastName}`;
+ }
+
+
+
+ Hello,
+ {{this.displayName}}
+
+
+}
+```
+
+**✅ Correct (derived data with getter):**
+
+```glimmer-js
+// app/components/user-greeting.gjs
+import Component from '@glimmer/component';
+
+class UserGreeting extends Component {
+ // Automatically reactive - updates when args change
+ get displayName() {
+ return `${this.args.user.firstName} ${this.args.user.lastName}`;
+ }
+
+
+
+ Hello,
+ {{this.displayName}}
+
+
+}
+```
+
+**✅ Even better (use @cached for expensive computations):**
+
+```glimmer-js
+// app/components/user-stats.gjs
+import Component from '@glimmer/component';
+import { cached } from '@glimmer/tracking';
+
+class UserStats extends Component {
+ @cached
+ get sortedPosts() {
+ // Expensive computation only runs when @posts changes
+ return [...this.args.posts].sort((a, b) => b.createdAt - a.createdAt);
+ }
+
+ @cached
+ get statistics() {
+ return {
+ total: this.args.posts.length,
+ published: this.args.posts.filter((p) => p.published).length,
+ drafts: this.args.posts.filter((p) => !p.published).length,
+ };
+ }
+
+
+
+
Total: {{this.statistics.total}}
+
Published: {{this.statistics.published}}
+
Drafts: {{this.statistics.drafts}}
+
+
+ {{#each this.sortedPosts as |post|}}
+ {{post.title}}
+ {{/each}}
+
+
+
+}
+```
+
+### Alternative 2: Use Custom Modifiers
+
+For DOM side effects, element setup, or cleanup, use custom modifiers:
+
+**❌ Incorrect (did-insert + will-destroy):**
+
+```glimmer-js
+// app/components/chart.gjs
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+class Chart extends Component {
+ chartInstance = null;
+
+ @action
+ setupChart(element) {
+ this.chartInstance = new Chart(element, this.args.config);
+ }
+
+ willDestroy() {
+ super.willDestroy();
+ // Easy to forget cleanup!
+ this.chartInstance?.destroy();
+ }
+
+
+
+
+}
+```
+
+**✅ Correct (custom modifier with automatic cleanup):**
+
+```javascript
+// app/modifiers/chart.js
+import { modifier } from 'ember-modifier';
+import { registerDestructor } from '@ember/destroyable';
+
+export default modifier((element, [config]) => {
+ // Setup
+ const chartInstance = new Chart(element, config);
+
+ // Cleanup happens automatically
+ registerDestructor(element, () => {
+ chartInstance.destroy();
+ });
+});
+```
+
+```glimmer-js
+// app/components/chart.gjs
+import chart from '../modifiers/chart';
+
+
+
+
+```
+
+### Alternative 3: Use Resources for Lifecycle Management
+
+For complex state management with automatic cleanup, use `ember-resources`:
+
+**❌ Incorrect (did-insert for data fetching):**
+
+```glimmer-js
+// app/components/user-profile.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+
+class UserProfile extends Component {
+ @tracked userData = null;
+ @tracked loading = true;
+ controller = new AbortController();
+
+ @action
+ async loadUser() {
+ this.loading = true;
+ try {
+ const response = await fetch(`/api/users/${this.args.userId}`, {
+ signal: this.controller.signal,
+ });
+ this.userData = await response.json();
+ } finally {
+ this.loading = false;
+ }
+ }
+
+ willDestroy() {
+ super.willDestroy();
+ this.controller.abort(); // Easy to forget!
+ }
+
+
+
+ {{#if this.loading}}
+ Loading...
+ {{else}}
+ {{this.userData.name}}
+ {{/if}}
+
+
+}
+```
+
+**✅ Correct (Resource with automatic cleanup):**
+
+```javascript
+// app/resources/user-data.js
+import { Resource } from 'ember-resources';
+import { tracked } from '@glimmer/tracking';
+
+export default class UserDataResource extends Resource {
+ @tracked data = null;
+ @tracked loading = true;
+ controller = new AbortController();
+
+ modify(positional, named) {
+ const [userId] = positional;
+ this.loadUser(userId);
+ }
+
+ async loadUser(userId) {
+ this.loading = true;
+ try {
+ const response = await fetch(`/api/users/${userId}`, {
+ signal: this.controller.signal,
+ });
+ this.data = await response.json();
+ } finally {
+ this.loading = false;
+ }
+ }
+
+ willDestroy() {
+ // Cleanup happens automatically
+ this.controller.abort();
+ }
+}
+```
+
+```glimmer-js
+// app/components/user-profile.gjs
+import Component from '@glimmer/component';
+import UserDataResource from '../resources/user-data';
+
+class UserProfile extends Component {
+ userData = UserDataResource.from(this, () => [this.args.userId]);
+
+
+ {{#if this.userData.loading}}
+ Loading...
+ {{else}}
+ {{this.userData.data.name}}
+ {{/if}}
+
+}
+```
+
+### When to Use Each Alternative
+
+| Use Case | Solution | Why |
+| ---------------- | ----------------------------------- | ----------------------------------------- |
+| Computed values | Getters + `@cached` | Reactive, efficient, no lifecycle needed |
+| DOM manipulation | Custom modifiers | Encapsulated, reusable, automatic cleanup |
+| Data fetching | getPromiseState from warp-drive | Declarative, automatic cleanup |
+| Event listeners | `{{on}}` modifier | Built-in, automatic cleanup |
+| Focus management | Custom modifier or ember-focus-trap | Proper lifecycle, accessibility |
+
+### Migration Strategy
+
+If you have existing code using these hooks:
+
+1. **Identify the purpose**: What is the hook doing?
+2. **Choose the right alternative**:
+ - Deriving data? → Use getters/`@cached`
+ - DOM setup/teardown? → Use a custom modifier
+ - Async data loading? → Use getPromiseState from warp-drive
+3. **Test thoroughly**: Ensure cleanup happens correctly
+4. **Remove the legacy hook**: Delete `{{did-insert}}`, `{{will-destroy}}`, or `{{did-update}}`
+
+### Performance Benefits
+
+Modern alternatives provide better performance:
+
+- **Getters**: Only compute when dependencies change
+- **@cached**: Memoizes expensive computations
+- **Modifiers**: Scoped to specific elements, composable
+- **getPromiseState**: Declarative data loading, automatic cleanup
+
+### Common Pitfalls to Avoid
+
+❌ **Don't use `willDestroy()` for cleanup when a modifier would work**
+❌ **Don't use `@action` + `did-insert` when a getter would suffice**
+❌ **Don't manually track changes when `@cached` handles it automatically**
+❌ **Don't forget `registerDestructor` in custom modifiers**
+
+### Summary
+
+Modern Ember provides superior alternatives to legacy lifecycle hooks:
+
+- **Derived Data**: Use getters and `@cached` for reactive computations
+- **DOM Side Effects**: Use custom modifiers with `registerDestructor`
+- **Async Data Loading**: Use getPromiseState from warp-drive/reactiveweb
+- **Better Code**: More testable, composable, and maintainable
+
+**Never use `{{did-insert}}`, `{{will-destroy}}`, or `{{did-update}}` in new code.**
+
+Reference:
+
+- [Ember Modifiers](https://github.com/ember-modifier/ember-modifier)
+- [warp-drive/reactiveweb](https://github.com/emberjs/data/tree/main/packages/reactiveweb)
+- [Glimmer Tracking](https://guides.emberjs.com/release/in-depth-topics/autotracking-in-depth/)
diff --git a/.agents/skills/ember-best-practices/rules/component-cached-getters.md b/.agents/skills/ember-best-practices/rules/component-cached-getters.md
new file mode 100644
index 0000000..cb9a853
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/component-cached-getters.md
@@ -0,0 +1,53 @@
+---
+title: Use @cached for Expensive Getters
+impact: HIGH
+impactDescription: 50-90% reduction in recomputation
+tags: components, performance, caching, tracked
+---
+
+## Use @cached for Expensive Getters
+
+Use `@cached` from `@glimmer/tracking` to memoize expensive computations that depend on tracked properties. The cached value is automatically invalidated when dependencies change.
+
+**Incorrect (recomputes on every access):**
+
+```javascript
+import Component from '@glimmer/component';
+
+class DataTable extends Component {
+ get filteredAndSortedData() {
+ // Expensive: runs on every access, even if nothing changed
+ return this.args.data
+ .filter((item) => item.status === this.args.filter)
+ .sort((a, b) => a[this.args.sortBy] - b[this.args.sortBy])
+ .map((item) => this.transformItem(item));
+ }
+}
+```
+
+**Correct (cached computation):**
+
+```javascript
+import Component from '@glimmer/component';
+import { cached } from '@glimmer/tracking';
+
+class DataTable extends Component {
+ @cached
+ get filteredAndSortedData() {
+ // Computed once per unique combination of dependencies
+ return this.args.data
+ .filter((item) => item.status === this.args.filter)
+ .sort((a, b) => a[this.args.sortBy] - b[this.args.sortBy])
+ .map((item) => this.transformItem(item));
+ }
+
+ transformItem(item) {
+ // Expensive transformation
+ return { ...item, computed: this.expensiveCalculation(item) };
+ }
+}
+```
+
+`@cached` memoizes the getter result and only recomputes when tracked dependencies change, providing 50-90% reduction in unnecessary work.
+
+Reference: [@cached decorator](https://guides.emberjs.com/release/in-depth-topics/autotracking-in-depth/#toc_caching)
diff --git a/.agents/skills/ember-best-practices/rules/component-class-fields.md b/.agents/skills/ember-best-practices/rules/component-class-fields.md
new file mode 100644
index 0000000..1c152be
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/component-class-fields.md
@@ -0,0 +1,324 @@
+---
+title: Use Class Fields for Component Composition
+impact: MEDIUM-HIGH
+impactDescription: Better composition and initialization patterns
+tags: components, class-fields, composition, initialization
+---
+
+## Use Class Fields for Component Composition
+
+Use class fields for clean component composition, initialization, and dependency injection patterns. Tracked class fields should be **roots of state** - representing the minimal independent state that your component owns. In most apps, you should have very few tracked fields.
+
+**Incorrect (imperative initialization, scattered state):**
+
+```glimmer-js
+// app/components/data-manager.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { service } from '@ember/service';
+
+class DataManager extends Component {
+ @service store;
+ @service router;
+
+ // Scattered state management - hard to track relationships
+ @tracked currentUser = null;
+ @tracked isLoading = false;
+ @tracked error = null;
+
+ loadData = async () => {
+ this.isLoading = true;
+ try {
+ this.currentUser = await this.store.request({ url: '/users/me' });
+ } catch (e) {
+ this.error = e;
+ } finally {
+ this.isLoading = false;
+ }
+ };
+
+
+ {{this.currentUser.name}}
+
+}
+```
+
+**Correct (class fields with proper patterns):**
+
+```glimmer-js
+// app/components/data-manager.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { service } from '@ember/service';
+import { cached } from '@glimmer/tracking';
+import { getPromiseState } from '@warp-drive/reactiveweb';
+
+class DataManager extends Component {
+ // Service injection as class fields
+ @service store;
+ @service router;
+
+ // Tracked state as class fields - this is a "root of state"
+ // Most components should have very few of these
+ @tracked selectedFilter = 'all';
+
+ // Data loading with getPromiseState
+ @cached
+ get currentUser() {
+ const promise = this.store.request({
+ url: '/users/me',
+ });
+ return getPromiseState(promise);
+ }
+
+
+ {{#if this.currentUser.isFulfilled}}
+ {{this.currentUser.value.name}}
+ {{else if this.currentUser.isRejected}}
+ Error: {{this.currentUser.error.message}}
+ {{/if}}
+
+}
+```
+
+**Understanding "roots of state":**
+
+Tracked fields should represent **independent state** that your component owns - not derived data or loaded data. Examples of good tracked fields:
+
+- User selections (selected tab, filter option)
+- UI state (is modal open, is expanded)
+- Form input values (not yet persisted)
+
+In most apps, you'll have very few tracked fields because most data comes from arguments, services, or computed getters.
+
+**Composition through class field assignment:**
+
+```glimmer-js
+// app/components/form-container.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { TrackedObject } from 'tracked-built-ins';
+
+class FormContainer extends Component {
+ // Compose form state
+ @tracked formData = new TrackedObject({
+ firstName: '',
+ lastName: '',
+ email: '',
+ preferences: {
+ newsletter: false,
+ notifications: true,
+ },
+ });
+
+ // Compose validation state
+ @tracked errors = new TrackedObject({});
+
+ // Compose UI state
+ @tracked ui = new TrackedObject({
+ isSubmitting: false,
+ isDirty: false,
+ showErrors: false,
+ });
+
+ // Computed field based on composed state
+ get isValid() {
+ return Object.keys(this.errors).length === 0 && this.formData.email && this.formData.firstName;
+ }
+
+ get canSubmit() {
+ return this.isValid && !this.ui.isSubmitting && this.ui.isDirty;
+ }
+
+ updateField = (field, value) => {
+ this.formData[field] = value;
+ this.ui.isDirty = true;
+ this.validate(field, value);
+ };
+
+ validate(field, value) {
+ if (field === 'email' && !value.includes('@')) {
+ this.errors.email = 'Invalid email';
+ } else {
+ delete this.errors[field];
+ }
+ }
+
+
+
+
+}
+```
+
+**Mixin-like composition with class fields:**
+
+```javascript
+// app/utils/pagination-mixin.js
+import { tracked } from '@glimmer/tracking';
+
+export class PaginationState {
+ @tracked page = 1;
+ @tracked perPage = 20;
+
+ get offset() {
+ return (this.page - 1) * this.perPage;
+ }
+
+ nextPage = () => {
+ this.page++;
+ };
+
+ prevPage = () => {
+ if (this.page > 1) this.page--;
+ };
+
+ goToPage = (page) => {
+ this.page = page;
+ };
+}
+```
+
+```glimmer-js
+// app/components/paginated-list.gjs
+import Component from '@glimmer/component';
+import { cached } from '@glimmer/tracking';
+import { PaginationState } from '../utils/pagination-mixin';
+
+class PaginatedList extends Component {
+ // Compose pagination functionality
+ pagination = new PaginationState();
+
+ @cached
+ get paginatedItems() {
+ const start = this.pagination.offset;
+ const end = start + this.pagination.perPage;
+ return this.args.items.slice(start, end);
+ }
+
+ get totalPages() {
+ return Math.ceil(this.args.items.length / this.pagination.perPage);
+ }
+
+
+
+ {{#each this.paginatedItems as |item|}}
+
{{item.name}}
+ {{/each}}
+
+
+
+
+}
+```
+
+**Shareable state objects:**
+
+```javascript
+// app/utils/selection-state.js
+import { tracked } from '@glimmer/tracking';
+import { TrackedSet } from 'tracked-built-ins';
+
+export class SelectionState {
+ @tracked selectedIds = new TrackedSet();
+
+ get count() {
+ return this.selectedIds.size;
+ }
+
+ get hasSelection() {
+ return this.selectedIds.size > 0;
+ }
+
+ isSelected(id) {
+ return this.selectedIds.has(id);
+ }
+
+ toggle = (id) => {
+ if (this.selectedIds.has(id)) {
+ this.selectedIds.delete(id);
+ } else {
+ this.selectedIds.add(id);
+ }
+ };
+
+ selectAll = (items) => {
+ items.forEach((item) => this.selectedIds.add(item.id));
+ };
+
+ clear = () => {
+ this.selectedIds.clear();
+ };
+}
+```
+
+```glimmer-js
+// app/components/selectable-list.gjs
+import Component from '@glimmer/component';
+import { SelectionState } from '../utils/selection-state';
+
+class SelectableList extends Component {
+ // Compose selection behavior
+ selection = new SelectionState();
+
+ get selectedItems() {
+ return this.args.items.filter((item) => this.selection.isSelected(item.id));
+ }
+
+
+
+
+ Select All
+
+
+ Clear
+
+ {{this.selection.count}} selected
+
+
+
+
+ {{#if this.selection.hasSelection}}
+
+ Delete {{this.selection.count}} items
+
+ {{/if}}
+
+}
+```
+
+Class fields provide clean composition patterns, better initialization, and shareable state objects that can be tested independently.
+
+Reference: [JavaScript Class Fields](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields)
diff --git a/.agents/skills/ember-best-practices/rules/component-composition-patterns.md b/.agents/skills/ember-best-practices/rules/component-composition-patterns.md
new file mode 100644
index 0000000..1c99f99
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/component-composition-patterns.md
@@ -0,0 +1,241 @@
+---
+title: Use Component Composition Patterns
+impact: HIGH
+impactDescription: Better code reuse and maintainability
+tags: components, composition, yield, blocks, contextual-components
+---
+
+## Use Component Composition Patterns
+
+Use component composition with yield blocks, named blocks, and contextual components for flexible, reusable UI patterns.
+
+**Named blocks** are for invocation consistency in design systems where you **don't want the caller to have full markup control**. They provide structured extension points while maintaining design system constraints - the same concept as named slots in other frameworks.
+
+**Incorrect (monolithic component):**
+
+```glimmer-js
+// app/components/user-card.gjs
+import Component from '@glimmer/component';
+
+class UserCard extends Component {
+
+
+
+
+ {{#if @showActions}}
+
+ Edit
+ Delete
+
+ {{/if}}
+
+ {{#if @showStats}}
+
+ Posts: {{@user.postCount}}
+ Followers: {{@user.followers}}
+
+ {{/if}}
+
+
+}
+```
+
+**Correct (composable with named blocks):**
+
+```glimmer-js
+// app/components/user-card.gjs
+import Component from '@glimmer/component';
+
+class UserCard extends Component {
+
+
+ {{#if (has-block "header")}}
+ {{yield to="header"}}
+ {{else}}
+
+ {{/if}}
+
+ {{yield @user to="default"}}
+
+ {{#if (has-block "actions")}}
+
+ {{yield @user to="actions"}}
+
+ {{/if}}
+
+ {{#if (has-block "footer")}}
+
+ {{/if}}
+
+
+}
+```
+
+**Usage with flexible composition:**
+
+```glimmer-js
+// app/components/user-list.gjs
+import UserCard from './user-card';
+
+
+ {{#each @users as |user|}}
+
+ <:header>
+
+
+
+ <:default as |u|>
+ {{u.bio}}
+ {{u.email}}
+
+
+ <:actions as |u|>
+ Edit
+ Delete
+
+
+ <:footer as |u|>
+
+ Posts:
+ {{u.postCount}}
+ | Followers:
+ {{u.followers}}
+
+
+
+ {{/each}}
+
+```
+
+**Contextual components pattern:**
+
+```glimmer-js
+// app/components/data-table.gjs
+import Component from '@glimmer/component';
+import { hash } from '@ember/helper';
+
+class HeaderCell extends Component {
+
+
+ {{yield}}
+ {{#if @sorted}}
+ {{if @ascending "↑" "↓"}}
+ {{/if}}
+
+
+}
+
+class Row extends Component {
+
+
+ {{yield}}
+
+
+}
+
+class Cell extends Component {
+
+ {{yield}}
+
+}
+
+class DataTable extends Component {
+
+
+ {{yield (hash Header=HeaderCell Row=Row Cell=Cell)}}
+
+
+}
+```
+
+**Using contextual components:**
+
+```glimmer-js
+// app/components/users-table.gjs
+import DataTable from './data-table';
+
+
+
+
+
+ Name
+ Email
+ Role
+
+
+
+ {{#each @users as |user|}}
+
+ {{user.name}}
+ {{user.email}}
+ {{user.role}}
+
+ {{/each}}
+
+
+
+```
+
+**Renderless component pattern:**
+
+```glimmer-js
+// app/components/dropdown.gjs
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import { hash } from '@ember/helper';
+
+class Dropdown extends Component {
+ @tracked isOpen = false;
+
+ @action
+ toggle() {
+ this.isOpen = !this.isOpen;
+ }
+
+ @action
+ close() {
+ this.isOpen = false;
+ }
+
+ {{yield (hash isOpen=this.isOpen toggle=this.toggle close=this.close)}}
+}
+```
+
+```glimmer-js
+// Usage
+import Dropdown from './dropdown';
+
+
+
+
+ Menu
+ {{if dd.isOpen "▲" "▼"}}
+
+
+ {{#if dd.isOpen}}
+
+ {{/if}}
+
+
+```
+
+Component composition provides flexibility, reusability, and clean separation of concerns while maintaining type safety and clarity.
+
+Reference: [Ember Components - Block Parameters](https://guides.emberjs.com/release/components/block-content/)
diff --git a/.agents/skills/ember-best-practices/rules/component-controlled-forms.md b/.agents/skills/ember-best-practices/rules/component-controlled-forms.md
new file mode 100644
index 0000000..78beb5b
--- /dev/null
+++ b/.agents/skills/ember-best-practices/rules/component-controlled-forms.md
@@ -0,0 +1,328 @@
+---
+title: Use Native Forms with Platform Validation
+impact: HIGH
+impactDescription: Reduces JavaScript form complexity and improves built-in a11y
+tags: components, forms, validation, accessibility, platform
+---
+
+## Use Native Forms with Platform Validation
+
+Rely on native `