@ethlete/agent-rules 0.1.0-next.16 → 0.1.0-next.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +5 -4
- package/content/rules/app-styling.md +47 -0
- package/content/rules/comments.md +4 -1
- package/content/rules/nx-layout.md +47 -0
- package/content/rules/styling.md +1 -1
- package/content/skills/angular-patterns/SKILL.md +23 -0
- package/content/skills/app-testing/SKILL.md +141 -0
- package/content/skills/git-flow/SKILL.md +4 -4
- package/content/skills/query/SKILL.md +148 -81
- package/content/skills/rxjs-signals/SKILL.md +20 -0
- package/content/skills/sdk-docs/SKILL.md +35 -7
- package/content/skills/sdk-update/SKILL.md +12 -7
- package/content/skills/story-styling/SKILL.md +7 -6
- package/content/skills/styleguide/lint-rule-lookup.md +1 -1
- package/content/skills/theming/SKILL.md +3 -4
- package/content/skills/timetrack/SKILL.md +61 -26
- package/content/skills/verify-in-app/SKILL.md +107 -0
- package/migrations/app-styling-utilities.md +114 -0
- package/migrations/list-state-query-form.md +103 -0
- package/migrations/nx-layout.md +23 -0
- package/migrations/sdk-components-over-hand-built-ui.md +69 -0
- package/migrations/search-query-field.md +45 -0
- package/migrations.json +43 -0
- package/package.json +4 -1
- package/src/index.js +5 -4
- package/src/index.js.map +1 -1
- package/src/lib/config.d.ts +2 -0
- package/src/lib/config.js +16 -3
- package/src/lib/config.js.map +1 -1
- package/src/lib/filter.d.ts +2 -0
- package/src/lib/filter.js +4 -4
- package/src/lib/filter.js.map +1 -1
- package/src/lib/package-runner.d.ts +1 -0
- package/src/lib/package-runner.js +34 -0
- package/src/lib/package-runner.js.map +1 -0
- package/src/lib/plan.js +10 -0
- package/src/lib/plan.js.map +1 -1
- package/src/lib/sync.js +19 -12
- package/src/lib/sync.js.map +1 -1
- package/src/lib/timetrack-command.js +98 -1
- package/src/lib/timetrack-command.js.map +1 -1
- package/src/lib/timetrack.d.ts +67 -0
- package/src/lib/timetrack.js +16 -1
- package/src/lib/timetrack.js.map +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: query
|
|
3
|
-
description: The signals-first @ethlete/query data-fetching system - the query client, typed query creators, reactive args, and reading results as signals or observables. Read BEFORE writing or reviewing code that fetches data, wires search/autocomplete to an API, adds auth
|
|
3
|
+
description: The signals-first @ethlete/query data-fetching system - the query client, typed query creators, reactive args, and reading results as signals or observables. Read BEFORE writing or reviewing code that fetches data, builds a list with filters/search/sort/paging bound to URL query params (defineQueryForm), wires search/autocomplete to an API, adds auth, route guards or polling, or bridges a query into UI or RxJS.
|
|
4
4
|
kind: skill
|
|
5
5
|
scope: consumer
|
|
6
6
|
requires: ['@ethlete/query']
|
|
@@ -13,38 +13,106 @@ Signals-first, typesafe data fetching for Angular: request dedup, caching, polli
|
|
|
13
13
|
paged queries, bearer auth, GraphQL, and a socket.io realtime client.
|
|
14
14
|
|
|
15
15
|
**The written docs are the source of truth - read the relevant page before
|
|
16
|
-
non-trivial query work.** This guide
|
|
17
|
-
don't re-derive them from source.
|
|
18
|
-
|
|
19
|
-
| Page
|
|
20
|
-
|
|
|
21
|
-
| {%docsBaseUrl%}/query/
|
|
22
|
-
| {%docsBaseUrl%}/query/queries
|
|
23
|
-
| {%docsBaseUrl%}/query/features
|
|
24
|
-
| {%docsBaseUrl%}/query/http
|
|
25
|
-
| {%docsBaseUrl%}/query/auth
|
|
26
|
-
| {%docsBaseUrl%}/query/caching
|
|
27
|
-
| {%docsBaseUrl%}/query/multi-tab
|
|
28
|
-
| {%docsBaseUrl%}/query/
|
|
29
|
-
| {%docsBaseUrl%}/query/
|
|
16
|
+
non-trivial query work.** This guide maps every need to its API and page, plus the
|
|
17
|
+
load-bearing facts, so you don't re-derive them from source or hand-build what ships.
|
|
18
|
+
|
|
19
|
+
| Page | Covers |
|
|
20
|
+
| ------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
21
|
+
| {%docsBaseUrl%}/query/ | Overview, the two generations, what else the package ships |
|
|
22
|
+
| {%docsBaseUrl%}/query/queries | **Start here** - client, creators, the query object's signals, auto-execution |
|
|
23
|
+
| {%docsBaseUrl%}/query/features | `withArgs`, polling, auto-refresh, side-effect handlers, custom features |
|
|
24
|
+
| {%docsBaseUrl%}/query/http | REST creators, typing requests, response transforms, upload progress |
|
|
25
|
+
| {%docsBaseUrl%}/query/auth | Bearer auth provider, guards, token refresh, auth features |
|
|
26
|
+
| {%docsBaseUrl%}/query/caching | Cache keys, dedup, `keepUnusedFor`, freshness, refresh and invalidation |
|
|
27
|
+
| {%docsBaseUrl%}/query/multi-tab | Cross-tab response sharing, one poller per key, mutation fan-out |
|
|
28
|
+
| {%docsBaseUrl%}/query/persistence | Successful reads kept in IndexedDB for reloads and offline cold starts |
|
|
29
|
+
| {%docsBaseUrl%}/query/stacks | Many queries of one creator, infinite lists, paged data |
|
|
30
|
+
| {%docsBaseUrl%}/query/dependent-queries | GET → GET dependencies and ordered mutation chains |
|
|
31
|
+
| {%docsBaseUrl%}/query/batching | Bulk writes with bounded concurrency, per-item results, retry |
|
|
32
|
+
| {%docsBaseUrl%}/query/errors | Error object, opt-in parsers, form submission, violations, retries |
|
|
33
|
+
| {%docsBaseUrl%}/query/testing | Specs: answering requests, `@ethlete/query/testing` helpers and fakes |
|
|
34
|
+
| {%docsBaseUrl%}/query/query-forms | **Any filtered, searched, sorted or paged list** - `defineQueryForm` |
|
|
35
|
+
| {%docsBaseUrl%}/query/gql | GraphQL creators over GET/POST |
|
|
36
|
+
| {%docsBaseUrl%}/query/ws | socket.io rooms and live-updating responses |
|
|
37
|
+
| {%docsBaseUrl%}/query/legacy | The maintenance-mode `V2QueryClient` and its replacements |
|
|
38
|
+
| {%docsBaseUrl%}/query/migrating-from-v2 | Codemods and the screen-by-screen move off the legacy client |
|
|
39
|
+
| {%docsBaseUrl%}/query/migrating-from-ngrx-toolkit | The generator and interop that move a store off `@tomtomb/ngrx-toolkit` |
|
|
40
|
+
| {%docsBaseUrl%}/query-devtools/ | The devtools panel and `provideQueryDevtools()` |
|
|
41
|
+
|
|
42
|
+
## You need → use → read
|
|
43
|
+
|
|
44
|
+
Before writing a helper, find your need here. Pages are under `{%docsBaseUrl%}/query/`.
|
|
45
|
+
|
|
46
|
+
| You need | Use | Read |
|
|
47
|
+
| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
|
|
48
|
+
| One client per API | `createQueryClient`, `injectApi()` | `queries` |
|
|
49
|
+
| REST reads (auto-executing) | `createGetQuery`, `createHeadQuery`, `createOptionsQuery` | `http` |
|
|
50
|
+
| REST writes (manual) | `createPostQuery`, `createPutQuery`, `createPatchQuery`, `createDeleteQuery` | `http` |
|
|
51
|
+
| The same behind a bearer token | `createSecureGetQuery`, `createSecureHeadQuery`, `createSecureOptionsQuery`, `createSecurePostQuery`, `createSecurePutQuery`, `createSecurePatchQuery`, `createSecureDeleteQuery` | `http`, `auth` |
|
|
52
|
+
| Upload/download progress bar | creator option `reportProgress: true`, then `loading().progress` | `http` |
|
|
53
|
+
| Args that follow signals (route params, inputs, filters) | `withArgs` | `features` |
|
|
54
|
+
| A filtered / searched / sorted / paged list synced to the URL | `defineQueryForm`, `queryField`, `searchQueryField`, `sortQueryField` | `query-forms` |
|
|
55
|
+
| Refetch on an interval | `withPolling` | `features` |
|
|
56
|
+
| Next request once the previous settled, with a cursor from it | `withLongPolling` | `features` |
|
|
57
|
+
| Refetch when some other signal changes | `withAutoRefresh` | `features` |
|
|
58
|
+
| Toast / log / react on success or failure | `withSuccessHandling`, `withErrorHandling`, `withLogging` | `features` |
|
|
59
|
+
| Patch a loaded response in place (e.g. from a socket message) | `withResponseUpdate` | `features`, `ws` |
|
|
60
|
+
| Page falls out of range after a filter shrinks the results | `withPageResetOnError`, `isPageOutOfRangeError` | `features` |
|
|
61
|
+
| Package your own reusable query behavior | `createQueryFeature` | `features` |
|
|
62
|
+
| Refresh reads after a mutation (here and in other tabs) | `injectApi().invalidateQueries({ url })` | `caching` |
|
|
63
|
+
| Refetch everything after a client-wide header changed | `injectApi().refreshQueriesInUse()` | `caching` |
|
|
64
|
+
| Show the previous data instantly on back navigation | `keepUnusedFor` + `executionState().cachedResponse` | `caching` |
|
|
65
|
+
| Last known data after a reload or offline | `withQueryPersistence`, `createIndexedDbQueryPersistenceAdapter`, `createNoopQueryPersistenceAdapter` | `persistence` |
|
|
66
|
+
| Tabs share responses, poll once, see each other's mutations | `withMultiTabSync` | `multi-tab` |
|
|
67
|
+
| Parallel detail requests for a list of ids | `createQueryStack`, `transformArrayResponse` | `stacks` |
|
|
68
|
+
| Load more / infinite scroll / classic paging | `createPagedQueryStack`, `ethletePaginationAdapter` (and other adapters) | `stacks` |
|
|
69
|
+
| A GET whose args come from another GET's response | `withArgs` reading the first query's `response()` | `dependent-queries` |
|
|
70
|
+
| Run mutations in order, each using the previous result | `querySequence` | `dependent-queries` |
|
|
71
|
+
| Many edits with progress, time remaining and retry of failures | `createQueryBatch` | `batching` |
|
|
72
|
+
| Submit a signal form through a mutation, violations on fields | `createQuerySubmission` | `errors` |
|
|
73
|
+
| Own submit handler, or server-side validation while typing | `executeUntilSettled`, `executeUntilSettled$`, `mapViolationsToFormErrors`, `validateWithQuery` | `errors` |
|
|
74
|
+
| Understand the error shapes your API returns | `withEthleteApiErrors` (all), `withSymfonyErrors`, `withHtmlErrorParsing` | `errors` |
|
|
75
|
+
| Error text for display, or an error built by hand (tests) | `queryErrorMessage`, `queryErrorMessages`, `createQueryErrorResponse`, `<et-query-error>` | `errors` |
|
|
76
|
+
| Retry failed requests | `withDefaultRetry`, `createDefaultRetryFn` | `errors` |
|
|
77
|
+
| Login / refresh / logout with bearer tokens | `createBearerAuthProvider`, `withAuthenticationQuery`, `withRefreshQuery` | `auth` |
|
|
78
|
+
| Protect routes, check roles or permissions | `createAuthGuard`, `canMatchWith` | `auth` |
|
|
79
|
+
| "Remember me" auto-login | `withPersistentAuth` | `auth` |
|
|
80
|
+
| Log out idle users | `withInactivityLogout` | `auth` |
|
|
81
|
+
| Warn before the session expires | `withTokenExpirationWarning` | `auth` |
|
|
82
|
+
| Revoke the token on logout | `withTokenRevocation` | `auth` |
|
|
83
|
+
| Share login/logout across tabs | `withBearerAuthMultiTabSync` | `auth` |
|
|
84
|
+
| Auth telemetry events | `withTracking` | `auth` |
|
|
85
|
+
| Tokens from SSO or a native shell | `provider.setTokens(access, refresh)` | `auth` |
|
|
86
|
+
| Which of several queries is busy (v2 query collections) | each query's `executionState()` in a `computed`; auth: `provider.executionState()` | `migrating-from-v2` |
|
|
87
|
+
| GraphQL | `createGqlQueryViaGet`, `createGqlQueryViaPost`, `createGqlMutationViaGet`, `createGqlMutationViaPost`, `createSecureGqlQueryViaGet`, `createSecureGqlQueryViaPost`, `createSecureGqlMutationViaGet`, `createSecureGqlMutationViaPost` | `gql` |
|
|
88
|
+
| Realtime rooms over socket.io | `createWebSocketClient` | `ws` |
|
|
89
|
+
| Inspect queries, stacks, auth and cache at runtime | `provideQueryDevtools()` + `@ethlete/query-devtools` | `/query-devtools/` |
|
|
30
90
|
|
|
31
91
|
## Two generations - use the current one
|
|
32
92
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- **Legacy (maintenance mode):** class-based `V2QueryClient`, `.prepare().execute()`,
|
|
37
|
-
`queryComputed`. Don't write new code against it.
|
|
93
|
+
Write new code against the signals-first system (`createQueryClient`, `createGetQuery`, …,
|
|
94
|
+
`withArgs`), all from the single entry `@ethlete/query`. The class-based `V2QueryClient`
|
|
95
|
+
(`.prepare().execute()`, `queryComputed`) is in maintenance mode.
|
|
38
96
|
|
|
39
97
|
## Core usage
|
|
40
98
|
|
|
41
|
-
One client per API, one creator per
|
|
99
|
+
One client per API, one bound creator per method, one query per endpoint, one live query
|
|
100
|
+
per component instance:
|
|
42
101
|
|
|
43
102
|
```ts
|
|
44
103
|
import { createQueryClient, createGetQuery, withArgs } from '@ethlete/query';
|
|
45
104
|
|
|
105
|
+
// api.ts
|
|
46
106
|
export const apiClient = createQueryClient({ name: 'api', baseUrl: API_URL });
|
|
47
|
-
export const
|
|
107
|
+
export const getQuery = createGetQuery(apiClient);
|
|
108
|
+
|
|
109
|
+
// posts.queries.ts
|
|
110
|
+
export type GetPostQueryArgs = {
|
|
111
|
+
response: Post;
|
|
112
|
+
pathParams: { postId: string };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const getPost = getQuery<GetPostQueryArgs>((p) => `/posts/${p.postId}`);
|
|
48
116
|
|
|
49
117
|
// in a component (injection context):
|
|
50
118
|
postId = input.required<string>();
|
|
@@ -54,82 +122,81 @@ post = computed(() => this.postQuery.response());
|
|
|
54
122
|
|
|
55
123
|
- `GET`/`HEAD`/`OPTIONS` **auto-execute** - immediately when static/argless, or
|
|
56
124
|
whenever `withArgs` produces new args. Mutations (`POST`/`PUT`/`PATCH`/`DELETE`)
|
|
57
|
-
never auto-execute; call `.execute(
|
|
125
|
+
never auto-execute; declare their args with `withArgs` too and call `.execute()`. A function route (`pathParams`)
|
|
58
126
|
requires `withArgs` (dev-mode error otherwise).
|
|
127
|
+
- A route function receives the path params themselves: `(p) => \`/posts/${p.postId}\``.
|
|
128
|
+
- With bearer auth, bind the secure creators the same way:
|
|
129
|
+
`const secureGetQuery = createSecureGetQuery(apiClient, authProviderRef)`.
|
|
59
130
|
- Queries live in a child injector tied to the creating component; destroyed with it.
|
|
60
131
|
|
|
132
|
+
## Lists with filters, search, sort or paging: `defineQueryForm`
|
|
133
|
+
|
|
134
|
+
Never wire list controls to the URL by hand (`injectQueryParams` + `router.navigate` +
|
|
135
|
+
drafts + effects). `defineQueryForm` does URL sync, debounce, defaults and page resets:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
qf = defineQueryForm({
|
|
139
|
+
fields: {
|
|
140
|
+
search: searchQueryField(),
|
|
141
|
+
sort: sortQueryField(),
|
|
142
|
+
page: queryField<number>({ defaultValue: 1, isResetBy: ['search', 'sort'] }),
|
|
143
|
+
},
|
|
144
|
+
}).observe();
|
|
145
|
+
|
|
146
|
+
users = getUsers(
|
|
147
|
+
withArgs(() => {
|
|
148
|
+
const { search, sort, page } = this.qf.value();
|
|
149
|
+
|
|
150
|
+
return { queryParams: { query: search, sortBy: sort?.active, sortOrder: sort?.direction, page } };
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Bind form controls with `[formField]="qf.fields.search"`. `et-pagination` and
|
|
156
|
+
`et-page-size-select` are not form controls: bind their `model` to the field's value signal,
|
|
157
|
+
`[(page)]="qf.fields.page().value"`. `qf.value()` is the committed, debounced value. Each
|
|
158
|
+
change pushes a history entry; pass `observe({ replaceUrl: true })` to replace it instead. See {%docsBaseUrl%}/query/query-forms for the other field creators,
|
|
159
|
+
filter overlays and `activeFilterCount`.
|
|
160
|
+
|
|
61
161
|
## The query object
|
|
62
162
|
|
|
63
|
-
Every state member is an **`ObservableSignal`** -
|
|
64
|
-
|
|
163
|
+
Every state member is an **`ObservableSignal`** - call it, or `.asObservable()` it without an
|
|
164
|
+
injection context of your own (it emits `null` first).
|
|
65
165
|
|
|
66
166
|
- `response()` → `TResponse | null` (kept while re-executing; cleared on a failed re-exec).
|
|
167
|
+
To keep showing the last good data after a failure, read `cachedResponse` from the
|
|
168
|
+
`loading` or `failure` variant of `executionState()` - do not copy responses into a signal.
|
|
67
169
|
- `loading()`, `error()` (normalized `QueryErrorResponse`), `args()`,
|
|
68
170
|
`executionState()` (`{ type: 'loading' | 'success' | 'failure', … } | null`, great for `@switch`).
|
|
69
171
|
- Methods: `execute({ args?, options? })`, `reset()`, `createSnapshot()`, `asReadonly()`.
|
|
70
172
|
|
|
71
|
-
|
|
72
|
-
an `Observable<T | null>` **without** needing their own injection context (unlike
|
|
73
|
-
raw `toObservable`). It emits `null` first - `pipe(filter(r => r !== null))`.
|
|
74
|
-
|
|
75
|
-
## Reactive args & features
|
|
76
|
-
|
|
77
|
-
- **`withArgs(() => ({ pathParams, queryParams, body }))`** - runs like a `computed`;
|
|
78
|
-
re-runs when a signal it reads changes and re-executes the query. This is how you
|
|
79
|
-
drive **search-as-you-type**: back it with a search signal
|
|
80
|
-
(`withArgs(() => ({ queryParams: { search: this.search() } }))`). Return `null`
|
|
81
|
-
to park the query - args reset to `null`, pausing polling/auto-refresh.
|
|
82
|
-
- **Prefer `withArgs` over passing `args` to `execute()`.** Args declared on the query
|
|
83
|
-
stay reactive: a `GET` re-executes itself when they change, and `withPolling` /
|
|
84
|
-
`withAutoRefresh` restart off the same signal - none of which happens for args handed
|
|
85
|
-
to `execute()`. A function route additionally throws without it. With `withArgs` in
|
|
86
|
-
place a mutation is just `.execute()`, which reuses the current `args()`. Reserve
|
|
87
|
-
`execute({ args })` for a one-off payload no signal holds (a form submit).
|
|
88
|
-
- `withPolling({ interval })`, `withAutoRefresh({ onSignalChanges: [...] })`.
|
|
89
|
-
- **`withLongPolling({ nextArgs })`** for a completion-driven chain instead of an interval: each
|
|
90
|
-
round starts once the previous settled, with args (a cursor) derived from its response. `nextArgs`
|
|
91
|
-
returning `null` ends the chain. Not `withPolling` with a small interval - and the two throw when
|
|
92
|
-
combined.
|
|
93
|
-
- Side-effects: `withSuccessHandling`, `withErrorHandling`, `withLogging`.
|
|
94
|
-
|
|
95
|
-
There is no built-in debounce operator - dedup/caching handles repeated identical
|
|
96
|
-
requests; debounce at the input if you need it.
|
|
97
|
-
|
|
98
|
-
## Bridging a query into RxJS / other APIs
|
|
99
|
-
|
|
100
|
-
For a callback that must start one request and return one correlated result, use a
|
|
101
|
-
manual query with `executeUntilSettled()`. Its frozen snapshot cannot be replaced by a
|
|
102
|
-
later execution, and the observable completes after that one result.
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
class ItemSource {
|
|
106
|
-
private itemsQuery = getItems({ onlyManualExecution: true });
|
|
107
|
-
|
|
108
|
-
fetch(query: string) {
|
|
109
|
-
return defer(() => executeUntilSettled(this.itemsQuery, { args: { queryParams: { q: query } } })).pipe(
|
|
110
|
-
map((snapshot) => {
|
|
111
|
-
const response = snapshot.response();
|
|
173
|
+
## Reactive args
|
|
112
174
|
|
|
113
|
-
|
|
175
|
+
- **`withArgs(() => ({ pathParams, queryParams, body }))`** runs like a `computed` and
|
|
176
|
+
re-executes on change. Return `null` to park the query (pauses polling/auto-refresh).
|
|
177
|
+
- **Prefer `withArgs` over `execute({ args })`.** Declared args keep a `GET` re-executing and
|
|
178
|
+
polling/auto-refresh restarting off the same signal; a function route throws without it. A
|
|
179
|
+
mutation with `withArgs` is just `.execute()`. Reserve `execute({ args })` for a one-off
|
|
180
|
+
payload no signal holds.
|
|
181
|
+
- Search-as-you-type: read a debounced value (`searchQueryField()` debounces 300ms; every
|
|
182
|
+
query field takes `debounce`) - a raw input signal sends one request per keystroke.
|
|
183
|
+
- `withLongPolling` and `withPolling` throw when combined.
|
|
114
184
|
|
|
115
|
-
|
|
116
|
-
}),
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
```
|
|
185
|
+
## Bridging into RxJS or callbacks
|
|
121
186
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
187
|
+
For a callback that must return one correlated result, run a manual query
|
|
188
|
+
(`{ onlyManualExecution: true }`) through `executeUntilSettled$(query, { args })` - cold, its
|
|
189
|
+
snapshot is frozen to that execution, and unsubscribing aborts the request. Keep the Promise
|
|
190
|
+
`executeUntilSettled` for an `async` signal-forms `submit()`. Never set a signal and return the shared `response`
|
|
191
|
+
stream: the retained previous response can be the first non-null emission. See
|
|
192
|
+
{%docsBaseUrl%}/query/queries#the-query-object.
|
|
127
193
|
|
|
128
194
|
## Gotchas
|
|
129
195
|
|
|
130
|
-
-
|
|
131
|
-
(`?? []` / `filter(Boolean)` as needed).
|
|
132
|
-
- Don't reach for the legacy client for new code.
|
|
196
|
+
- `query.response()` is **nullable** (`?? []` / `filter(Boolean)` as needed).
|
|
133
197
|
- `.execute()` defaults `args` to the current `args()` when omitted.
|
|
134
198
|
- Anything under a query's `subtle` namespace is an unsupported escape hatch - never
|
|
135
199
|
treat it as public API.
|
|
200
|
+
- Use `withArgs` for mutations too. Never reach for `execute({ args })` plus
|
|
201
|
+
`silenceMissingWithArgsFeatureError` unless the args really exist only at call time - the
|
|
202
|
+
flag is an escape hatch, not the mutation pattern.
|
|
@@ -63,6 +63,26 @@ toObservable(page)
|
|
|
63
63
|
.subscribe();
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
## An effect that only writes a signal is a derivation
|
|
67
|
+
|
|
68
|
+
If an `effect()` does nothing but `.set()` a signal, even behind an `if`, the value is derived
|
|
69
|
+
state: use `computed()` when it is read-only, `linkedSignal()` when the user can still
|
|
70
|
+
overwrite it. `ethlete/prefer-linked-signal` flags the plain cases. Keep `effect()` for work
|
|
71
|
+
that leaves the signal graph: DOM, storage, logging, third-party APIs.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// ❌
|
|
75
|
+
effect(() => {
|
|
76
|
+
if (this.items().length) this.selected.set(this.items()[0]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ✅ the previous value stays when the list is empty
|
|
80
|
+
selected = linkedSignal<Item[], Item | null>({
|
|
81
|
+
source: this.items,
|
|
82
|
+
computation: (items, previous) => items[0] ?? previous?.value ?? null,
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
66
86
|
## Prefer `@ethlete/core` helpers
|
|
67
87
|
|
|
68
88
|
Lint nudges these, but reach for them by default: `injectViewportSize()`,
|
|
@@ -20,6 +20,13 @@ Two sources, both authoritative for different things:
|
|
|
20
20
|
| **Docs site** | {%docsBaseUrl%} | Prose guides: what a thing is for, options, defaults, behaviour, migration notes |
|
|
21
21
|
| **Storybook** | {%sdkStorybookUrl%} | The live component: every variant rendered, the real controls, and the exact markup a story uses |
|
|
22
22
|
|
|
23
|
+
## First: app setup
|
|
24
|
+
|
|
25
|
+
Before the first component, work through `{%docsBaseUrl%}/components/setup` - the 62.5% root
|
|
26
|
+
font size, Tailwind 4 theme generation, a `type: 'error'` color theme, and the providers
|
|
27
|
+
(`provideOverlay`, `provideDateLocale`, label tokens, …). Missing any of them fails at runtime,
|
|
28
|
+
not at compile time.
|
|
29
|
+
|
|
23
30
|
## Finding the right page
|
|
24
31
|
|
|
25
32
|
Page URLs follow `{%docsBaseUrl%}/<lib>/<topic>`. The library sections:
|
|
@@ -34,13 +41,34 @@ Page URLs follow `{%docsBaseUrl%}/<lib>/<topic>`. The library sections:
|
|
|
34
41
|
|
|
35
42
|
Component domains under `/components/`:
|
|
36
43
|
|
|
37
|
-
`accordion` `
|
|
38
|
-
`
|
|
39
|
-
`
|
|
40
|
-
`
|
|
41
|
-
`
|
|
42
|
-
`
|
|
43
|
-
`
|
|
44
|
+
`accordion` `avatar` `badge` `banner` `bracket` `bracket-prediction` `bracket-rounds-list`
|
|
45
|
+
`breadcrumb` `button` `calendar` `card` `carousel` `cascader` `chart` `chip`
|
|
46
|
+
`choice-inputs` `command-palette` `copy-button` `date-time-inputs` `description-list`
|
|
47
|
+
`divider` `dropzone` `empty-state` `error-codes` `filter-overlay` `floating-action`
|
|
48
|
+
`focus-ring` `forms` `grid` `icon` `kbd` `line-chart` `loader` `localization` `masonry`
|
|
49
|
+
`match` `menu` `mixed-state` `notification` `overlay-openers` `overlays` `pagination`
|
|
50
|
+
`picture` `pie-chart` `progress-steps` `query-error` `rich-text-editor` `sankey-chart`
|
|
51
|
+
`scheduler` `scrollable` `scrollbar` `select` `setup` `skeleton` `slider` `sport-recipes`
|
|
52
|
+
`standings` `standings-pick` `stream` `table` `tabs` `text-inputs` `time-picker`
|
|
53
|
+
`timeline` `toggletip` `toolbar` `tooltip` `tree`
|
|
54
|
+
|
|
55
|
+
**Check this list before you build any UI by hand.** Some needs hide behind a name you
|
|
56
|
+
would not guess:
|
|
57
|
+
|
|
58
|
+
| You need | Read |
|
|
59
|
+
| ------------------------------------------ | ----------------------------------------- |
|
|
60
|
+
| Bar or stacked bar chart, series legend | `chart` |
|
|
61
|
+
| Line, pie, flow chart | `line-chart`, `pie-chart`, `sankey-chart` |
|
|
62
|
+
| Progress bar, spinner | `loader` |
|
|
63
|
+
| Initials or user picture in a circle | `avatar` |
|
|
64
|
+
| "No results" or empty table state | `empty-state` |
|
|
65
|
+
| Failed request with a retry button | `query-error` |
|
|
66
|
+
| Status pill, count | `badge` |
|
|
67
|
+
| Tab strip that switches a view or a filter | `tabs` (nav tabs) |
|
|
68
|
+
| Dialog, bottom sheet, side panel | `overlays`, `overlay-openers` |
|
|
69
|
+
| Label and value pairs | `description-list` |
|
|
70
|
+
| Stepper, wizard progress | `progress-steps` |
|
|
71
|
+
| Providers and styles a new app needs | `setup` |
|
|
44
72
|
|
|
45
73
|
So the table guide is `{%docsBaseUrl%}/components/table`, the menu guide
|
|
46
74
|
`{%docsBaseUrl%}/components/menu`, and so on.
|
|
@@ -16,7 +16,7 @@ the migrations, so a hand-written bump skips every one of them silently.
|
|
|
16
16
|
## 1. See what is pending
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
19
|
+
{%packageRunner%} et update --check
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
It prints one line per package, `installed → target`, and exits 1 while an update is pending. It
|
|
@@ -26,9 +26,9 @@ prerelease stays on `next`.
|
|
|
26
26
|
Name a package to limit the run, short or in full:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
{%packageRunner%} et update core # only @ethlete/core
|
|
30
|
+
{%packageRunner%} et update core --to 5.0.0-next.55
|
|
31
|
+
{%packageRunner%} et update --tag latest # leave the prerelease line
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## 2. Run it
|
|
@@ -37,19 +37,24 @@ The working tree must be clean - the codemods rewrite files, and you need a diff
|
|
|
37
37
|
Commit or stash first, then:
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
|
-
|
|
40
|
+
{%packageRunner%} et update
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
In order, it writes the new ranges into every `package.json` in the repo that declares the package -
|
|
44
44
|
an Nx library manifest included, not only the root one - runs the install, reads the migrations out of
|
|
45
|
-
the freshly installed packages, runs every codemod,
|
|
45
|
+
the freshly installed packages, runs every codemod, regenerates the agent rules and skills with
|
|
46
|
+
`ethlete-agents sync` when `@ethlete/agent-rules` moved, and writes what is left to `.ethlete/update`.
|
|
47
|
+
|
|
48
|
+
An older `@ethlete/cli` skips the sync. If the run moved `@ethlete/agent-rules` and printed no
|
|
49
|
+
`ethlete-agents sync` line, run `{%packageRunner%} ethlete-agents sync` yourself before the first task: the
|
|
50
|
+
tasks assume the guidance of the new version.
|
|
46
51
|
|
|
47
52
|
If the install or a codemod fails, the run stops and leaves `.ethlete/update/pending.json` behind.
|
|
48
53
|
Fix the cause, then continue - do not start over, or the migrations of the versions already installed
|
|
49
54
|
are skipped:
|
|
50
55
|
|
|
51
56
|
```bash
|
|
52
|
-
|
|
57
|
+
{%packageRunner%} et update --continue
|
|
53
58
|
```
|
|
54
59
|
|
|
55
60
|
## 3. Work the task list
|
|
@@ -12,9 +12,10 @@ vars: [themeStylesheet]
|
|
|
12
12
|
|
|
13
13
|
Two separate rules, often confused:
|
|
14
14
|
|
|
15
|
-
1. **
|
|
16
|
-
component, wrapped in `@layer components`, using
|
|
17
|
-
{%skill:theming%}.
|
|
15
|
+
1. **A library component's source is plain CSS, never Tailwind.** The `.css` next
|
|
16
|
+
to a component a library ships, wrapped in `@layer components`, using
|
|
17
|
+
surface/color tokens - see {%skill:theming%}. Application components are the
|
|
18
|
+
opposite: they use Tailwind utilities in their templates.
|
|
18
19
|
2. **Story files may use Tailwind** (`*.stories.ts`, anything under a `stories/`
|
|
19
20
|
folder) for demo layout only - the frame around the component, not the
|
|
20
21
|
component's own look.
|
|
@@ -33,11 +34,11 @@ error and no warning, the element just renders unstyled. A theme that does this:
|
|
|
33
34
|
leaves `bg-blue-500`, `text-gray-700` and `text-sm` as dead strings, even though
|
|
34
35
|
they are "real" Tailwind classes.
|
|
35
36
|
|
|
36
|
-
**Read the theme before reaching for an unfamiliar utility.** This project's
|
|
37
|
-
`{%themeStylesheet%}
|
|
37
|
+
**Read the theme before reaching for an unfamiliar utility.** This project's lives in
|
|
38
|
+
`{%themeStylesheet%}` (a file, or a folder of theme files):
|
|
38
39
|
|
|
39
40
|
```bash
|
|
40
|
-
grep -
|
|
41
|
+
grep -rnE -- '--(color|text|font|spacing)-' {%themeStylesheet%}
|
|
41
42
|
```
|
|
42
43
|
|
|
43
44
|
If `@theme` doesn't define the token, the class doesn't exist. Check rather than
|
|
@@ -7,7 +7,7 @@ convention. Run lint with `--fix`; do not hand-check this table before linting.
|
|
|
7
7
|
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
8
8
|
| No `any` / `$any()`; narrow `unknown` | `@typescript-eslint/no-explicit-any`, `@angular-eslint/template/no-any` |
|
|
9
9
|
| Static strings and booleans use attributes | `@angular-eslint/template/prefer-static-string-properties`, `ethlete/prefer-static-boolean-properties` |
|
|
10
|
-
| `type`, not `interface`
|
|
10
|
+
| `type`, not `interface` (module augmentations stay interfaces) | `ethlete/consistent-type-definitions` |
|
|
11
11
|
| No `enum` or `const enum` | `ethlete/no-enum` |
|
|
12
12
|
| `const` by default; one declaration per statement | `no-var`, `prefer-const`, `one-var` |
|
|
13
13
|
| Strict equality | `eqeqeq` |
|
|
@@ -102,16 +102,15 @@ the interactive element itself, never on a wrapper.
|
|
|
102
102
|
with no error. Percentages and viewport units (`vh`, `vw`, `dvh`) are fine. When the
|
|
103
103
|
default has to be relative, use `syntax: '*'` with no `initial-value` and put the
|
|
104
104
|
default in a fallback at each use site: `var(--et-skeleton-size, 1em)`.
|
|
105
|
-
`yarn lint:css-properties` checks this, and pre-commit runs it on staged files.
|
|
106
105
|
- `--et-theme-color-primary-*` always resolves to the **nearest color scope**. A
|
|
107
106
|
hardcoded semantic color in CSS can't be replaced by it unless the right theme is
|
|
108
107
|
provided on that element.
|
|
109
108
|
- A static fallback (`var(--et-surface-border-solid, rgb(255 255 255 / 0.1))`) is
|
|
110
109
|
permitted for theme-less setups, but themes make it unnecessary. `injectErrorTheme()`
|
|
111
110
|
is a hard requirement wherever it is used.
|
|
112
|
-
- **Cascade layers, not `:where()`,
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
- **Cascade layers, not `:where()`, decide who wins against SDK component CSS.** SDK
|
|
112
|
+
styles live in `@layer components`, so a utility or unlayered app CSS overrides them.
|
|
113
|
+
An app rule inside `@layer components` loses. See the `app-styling` rule.
|
|
115
114
|
|
|
116
115
|
Styling a **story** file rather than a component? The colour rule is the same. Check the
|
|
117
116
|
repository's Storybook guidance before assuming a Tailwind utility exists.
|