Update agent skills
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -149,7 +149,7 @@ Each rule file contains:
|
||||
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-a11y-refocus** - 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
|
||||
|
||||
@@ -69,6 +69,14 @@ import { setupGlobalA11yHooks } from 'ember-a11y-testing/test-support';
|
||||
setupGlobalA11yHooks(); // Runs on every test automatically
|
||||
```
|
||||
|
||||
### Leave All Rules Enabled
|
||||
|
||||
Keep the default ember-a11y-testing and axe-core rules turned on. Avoid disabling rules globally or excluding specific rules to bypass failures without a documented remediation plan.
|
||||
|
||||
When teams suppress accessibility rules without a time-boxed remediation plan, they hide real defects, accumulate technical debt, and make regressions harder to detect. A skipped rule can allow a serious accessibility defect to ship to production, especially for problems involving forms, keyboard access, focus management, semantics, or ARIA usage.
|
||||
|
||||
If you must suppress a rule temporarily, treat it as an exception: document why it is needed, scope it as narrowly as possible, and create follow-up work to restore the rule quickly.
|
||||
|
||||
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)
|
||||
|
||||
@@ -35,9 +35,7 @@ All form inputs must have associated labels, and validation errors should be ann
|
||||
<div>
|
||||
<label for="email-input">
|
||||
Email Address
|
||||
{{#if this.isEmailRequired}}
|
||||
<span aria-label="required">*</span>
|
||||
{{/if}}
|
||||
<span aria-hidden="true">*</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
|
||||
@@ -19,49 +19,86 @@ export default class Router extends EmberRouter {
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (using a11y-announcer library - recommended):**
|
||||
**Correct (using ember-a11y-refocus library - recommended):**
|
||||
|
||||
Use the [a11y-announcer](https://github.com/ember-a11y/a11y-announcer) library for robust route announcements:
|
||||
Use the [ember-a11y-refocus](https://github.com/ember-a11y/ember-a11y-refocus) library for robust route announcements, route transition focus management, and a bypass block (aka skip link).
|
||||
|
||||
```bash
|
||||
ember install @ember-a11y/a11y-announcer
|
||||
pnpm add ember-a11y-refocus
|
||||
```
|
||||
|
||||
```javascript
|
||||
// app/router.js
|
||||
import EmberRouter from '@ember/routing/router';
|
||||
import config from './config/environment';
|
||||
Or with npm:
|
||||
|
||||
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' });
|
||||
});
|
||||
});
|
||||
```bash
|
||||
npm install ember-a11y-refocus
|
||||
```
|
||||
|
||||
The a11y-announcer library automatically handles route announcements. For custom announcements in your routes:
|
||||
Use the addon by rendering `NavigationNarrator` in your application layout and ensuring your primary content has `id="main"`.
|
||||
|
||||
```handlebars
|
||||
{{! app/templates/application.hbs }}
|
||||
<header>
|
||||
<NavigationNarrator />
|
||||
{{! other header content }}
|
||||
</header>
|
||||
|
||||
<main id='main'>
|
||||
{{outlet}}
|
||||
</main>
|
||||
```
|
||||
|
||||
If you are using GJS or GTS, import the component directly:
|
||||
|
||||
```glimmer-js
|
||||
import { NavigationNarrator } from 'ember-a11y-refocus';
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<NavigationNarrator />
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
{{outlet}}
|
||||
</main>
|
||||
</template>
|
||||
```
|
||||
|
||||
The addon ships minimal styles for the skip link and navigation message:
|
||||
|
||||
```javascript
|
||||
// app/routes/dashboard.js
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
// app/app.js or app/app.ts
|
||||
import 'ember-a11y-refocus/styles/navigation-narrator.css';
|
||||
```
|
||||
|
||||
export default class DashboardRoute extends Route {
|
||||
@service announcer;
|
||||
If you need to customize which transitions count as a route change, pass a validator function to `NavigationNarrator`:
|
||||
|
||||
afterModel() {
|
||||
this.announcer.announce('Loaded dashboard with latest data');
|
||||
```javascript
|
||||
// app/controllers/application.js
|
||||
import Controller from '@ember/controller';
|
||||
import { defaultValidator } from 'ember-a11y-refocus';
|
||||
|
||||
export default class ApplicationController extends Controller {
|
||||
myCustomValidator(transition) {
|
||||
if (transition.from?.name === 'special') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return defaultValidator(transition);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```handlebars
|
||||
{{! app/templates/application.hbs }}
|
||||
<header>
|
||||
<NavigationNarrator @routeChangeValidator={{this.myCustomValidator}} />
|
||||
</header>
|
||||
|
||||
<main id='main'>
|
||||
{{outlet}}
|
||||
</main>
|
||||
```
|
||||
|
||||
**Alternative: DIY approach with ARIA live regions:**
|
||||
|
||||
If you prefer not to use a library, you can implement route announcements yourself:
|
||||
@@ -150,25 +187,6 @@ export default class ApplicationRoute extends Route {
|
||||
}
|
||||
```
|
||||
|
||||
**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';
|
||||
|
||||
<template>
|
||||
{{pageTitle "Dashboard"}}
|
||||
|
||||
<div class="dashboard">
|
||||
{{outlet}}
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
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/)
|
||||
|
||||
@@ -11,8 +11,8 @@ Implement intelligent model caching strategies to reduce redundant API calls and
|
||||
|
||||
**Incorrect (always fetches fresh data):**
|
||||
|
||||
```glimmer-js
|
||||
// app/routes/post.gjs
|
||||
```javascript
|
||||
// app/routes/post.js
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
@@ -23,21 +23,24 @@ export default class PostRoute extends Route {
|
||||
// Always makes API call, even if we just loaded this post
|
||||
return this.store.request({ url: `/posts/${params.post_id}` });
|
||||
}
|
||||
|
||||
<template>
|
||||
<article>
|
||||
<h1>{{@model.title}}</h1>
|
||||
<div>{{@model.content}}</div>
|
||||
</article>
|
||||
{{outlet}}
|
||||
</template>
|
||||
}
|
||||
```
|
||||
|
||||
```glimmer-js
|
||||
// app/templates/post.gjs
|
||||
<template>
|
||||
<article>
|
||||
<h1>{{@model.title}}</h1>
|
||||
<div>{{@model.content}}</div>
|
||||
</article>
|
||||
{{outlet}}
|
||||
</template>
|
||||
```
|
||||
|
||||
**Correct (with smart caching):**
|
||||
|
||||
```glimmer-js
|
||||
// app/routes/post.gjs
|
||||
```javascript
|
||||
// app/routes/post.js
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
@@ -68,17 +71,20 @@ export default class PostRoute extends Route {
|
||||
const fiveMinutes = 5 * 60 * 1000;
|
||||
return Date.now() - cacheTime < fiveMinutes;
|
||||
}
|
||||
|
||||
<template>
|
||||
<article>
|
||||
<h1>{{@model.title}}</h1>
|
||||
<div>{{@model.content}}</div>
|
||||
</article>
|
||||
{{outlet}}
|
||||
</template>
|
||||
}
|
||||
```
|
||||
|
||||
```glimmer-js
|
||||
// app/templates/post.gjs
|
||||
<template>
|
||||
<article>
|
||||
<h1>{{@model.title}}</h1>
|
||||
<div>{{@model.content}}</div>
|
||||
</article>
|
||||
{{outlet}}
|
||||
</template>
|
||||
```
|
||||
|
||||
**Service-based caching layer:**
|
||||
|
||||
```javascript
|
||||
@@ -123,8 +129,8 @@ export default class PostCacheService extends Service {
|
||||
}
|
||||
```
|
||||
|
||||
```glimmer-js
|
||||
// app/routes/post.gjs
|
||||
```javascript
|
||||
// app/routes/post.js
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
@@ -141,21 +147,24 @@ export default class PostRoute extends Route {
|
||||
const params = this.paramsFor('post');
|
||||
await this.postCache.getPost(params.post_id, { forceRefresh: true });
|
||||
}
|
||||
|
||||
<template>
|
||||
<article>
|
||||
<h1>{{@model.title}}</h1>
|
||||
<div>{{@model.content}}</div>
|
||||
</article>
|
||||
{{outlet}}
|
||||
</template>
|
||||
}
|
||||
```
|
||||
|
||||
```glimmer-js
|
||||
// app/templates/post.gjs
|
||||
<template>
|
||||
<article>
|
||||
<h1>{{@model.title}}</h1>
|
||||
<div>{{@model.content}}</div>
|
||||
</article>
|
||||
{{outlet}}
|
||||
</template>
|
||||
```
|
||||
|
||||
**Using query params for cache control:**
|
||||
|
||||
```glimmer-js
|
||||
// app/routes/posts.gjs
|
||||
```javascript
|
||||
// app/routes/posts.js
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
@@ -174,28 +183,31 @@ export default class PostsRoute extends Route {
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="posts">
|
||||
<button {{on "click" (fn this.refresh)}}>
|
||||
Refresh
|
||||
</button>
|
||||
|
||||
<ul>
|
||||
{{#each @model as |post|}}
|
||||
<li>{{post.title}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{outlet}}
|
||||
</template>
|
||||
}
|
||||
```
|
||||
|
||||
```glimmer-js
|
||||
// app/templates/posts.gjs
|
||||
<template>
|
||||
<div class="posts">
|
||||
<button {{on "click" (fn this.refresh)}}>
|
||||
Refresh
|
||||
</button>
|
||||
|
||||
<ul>
|
||||
{{#each @model as |post|}}
|
||||
<li>{{post.title}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{outlet}}
|
||||
</template>
|
||||
```
|
||||
|
||||
**Background refresh pattern:**
|
||||
|
||||
```glimmer-js
|
||||
// app/routes/dashboard.gjs
|
||||
```javascript
|
||||
// app/routes/dashboard.js
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
@@ -214,17 +226,20 @@ export default class DashboardRoute extends Route {
|
||||
|
||||
return cached || this.store.request({ url: '/dashboard' });
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<h1>Dashboard</h1>
|
||||
<div>Stats: {{@model.stats}}</div>
|
||||
</div>
|
||||
{{outlet}}
|
||||
</template>
|
||||
}
|
||||
```
|
||||
|
||||
```glimmer-js
|
||||
// app/templates/dashboard.gjs
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<h1>Dashboard</h1>
|
||||
<div>Stats: {{@model.stats}}</div>
|
||||
</div>
|
||||
{{outlet}}
|
||||
</template>
|
||||
```
|
||||
|
||||
Smart caching reduces server load, improves perceived performance, and provides better offline support while keeping data fresh.
|
||||
|
||||
Reference: [WarpDrive Caching](https://warp-drive.io/)
|
||||
|
||||
@@ -1,38 +1,15 @@
|
||||
---
|
||||
title: Use Route Templates with Co-located Syntax
|
||||
title: Use Separate Route and Template Files
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: Better code organization and maintainability
|
||||
tags: routes, templates, gjs, co-location
|
||||
tags: routes, templates, gjs, file-conventions
|
||||
---
|
||||
|
||||
## Use Route Templates with Co-located Syntax
|
||||
## Use Separate Route and Template Files
|
||||
|
||||
Use co-located route templates with modern gjs syntax for better organization and maintainability.
|
||||
Keep route logic in `app/routes/*.js` and route templates in `app/templates/*.gjs`. Route classes imported from `@ember/routing/route` do not support inline `<template>` blocks.
|
||||
|
||||
**Incorrect (separate template file - old pattern):**
|
||||
|
||||
```glimmer-js
|
||||
// app/routes/posts.js (separate file)
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class PostsRoute extends Route {
|
||||
model() {
|
||||
return this.store.request({ url: '/posts' });
|
||||
}
|
||||
}
|
||||
|
||||
// app/templates/posts.gjs (separate template file)
|
||||
<template>
|
||||
<h1>Posts</h1>
|
||||
<ul>
|
||||
{{#each @model as |post|}}
|
||||
<li>{{post.title}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Correct (co-located route template):**
|
||||
**Incorrect (inline template inside a route class):**
|
||||
|
||||
```glimmer-js
|
||||
// app/routes/posts.gjs
|
||||
@@ -56,10 +33,37 @@ export default class PostsRoute extends Route {
|
||||
}
|
||||
```
|
||||
|
||||
**With loading and error states:**
|
||||
**Correct (separate route module and template file):**
|
||||
|
||||
```javascript
|
||||
// app/routes/posts.js
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class PostsRoute extends Route {
|
||||
model() {
|
||||
return this.store.request({ url: '/posts' });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```glimmer-js
|
||||
// app/routes/posts.gjs
|
||||
// app/templates/posts.gjs
|
||||
<template>
|
||||
<h1>Posts</h1>
|
||||
<ul>
|
||||
{{#each @model as |post|}}
|
||||
<li>{{post.title}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
{{outlet}}
|
||||
</template>
|
||||
```
|
||||
|
||||
**With a separate template file for route UI:**
|
||||
|
||||
```javascript
|
||||
// app/routes/posts.js
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
@@ -69,29 +73,32 @@ export default class PostsRoute extends Route {
|
||||
model() {
|
||||
return this.store.request({ url: '/posts' });
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="posts-page">
|
||||
<h1>Posts</h1>
|
||||
|
||||
{{#if @model}}
|
||||
<ul>
|
||||
{{#each @model as |post|}}
|
||||
<li>{{post.title}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
{{outlet}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
```
|
||||
|
||||
```glimmer-js
|
||||
// app/templates/posts.gjs
|
||||
<template>
|
||||
<div class="posts-page">
|
||||
<h1>Posts</h1>
|
||||
|
||||
{{#if @model}}
|
||||
<ul>
|
||||
{{#each @model as |post|}}
|
||||
<li>{{post.title}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
{{outlet}}
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Template-only routes:**
|
||||
|
||||
```glimmer-js
|
||||
// app/routes/about.gjs
|
||||
// app/templates/about.gjs
|
||||
<template>
|
||||
<div class="about-page">
|
||||
<h1>About Us</h1>
|
||||
@@ -100,6 +107,6 @@ export default class PostsRoute extends Route {
|
||||
</template>
|
||||
```
|
||||
|
||||
Co-located route templates keep route logic and presentation together, making the codebase easier to navigate and maintain.
|
||||
Keeping route classes and route templates in their conventional files matches Ember's supported routing model and makes examples easier to apply in real apps.
|
||||
|
||||
Reference: [Ember Routes](https://guides.emberjs.com/release/routing/)
|
||||
|
||||
+4
-1
@@ -4,16 +4,19 @@
|
||||
"ember-best-practices": {
|
||||
"source": "nullvoxpopuli/agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "7909c3def6c4ddefb358d1973cf724269ede9f6cdba1dd2888e4e6072a897f3e"
|
||||
"skillPath": "skills/ember-best-practices/SKILL.md",
|
||||
"computedHash": "34e82668e2fa1e6b025ac8cf1015634484bf5d8fbc80e8aa642489d2df79498c"
|
||||
},
|
||||
"nak": {
|
||||
"source": "soapbox-pub/nostr-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/nak/SKILL.md",
|
||||
"computedHash": "710d3f3945ff421ed2b7f40ecd32c5e263bc029d43fe8f4fd1491a8013c7389a"
|
||||
},
|
||||
"nostr": {
|
||||
"source": "soapbox-pub/nostr-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/nostr/SKILL.md",
|
||||
"computedHash": "e1e6834c18d18a5deef4cd9555f6eee0fc0b968acf1c619253999eda76beab8e"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user