@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +5 -4
  3. package/content/rules/app-styling.md +47 -0
  4. package/content/rules/comments.md +4 -1
  5. package/content/rules/nx-layout.md +47 -0
  6. package/content/rules/styling.md +1 -1
  7. package/content/skills/angular-patterns/SKILL.md +23 -0
  8. package/content/skills/app-testing/SKILL.md +141 -0
  9. package/content/skills/git-flow/SKILL.md +4 -4
  10. package/content/skills/query/SKILL.md +148 -81
  11. package/content/skills/rxjs-signals/SKILL.md +20 -0
  12. package/content/skills/sdk-docs/SKILL.md +35 -7
  13. package/content/skills/sdk-update/SKILL.md +12 -7
  14. package/content/skills/story-styling/SKILL.md +7 -6
  15. package/content/skills/styleguide/lint-rule-lookup.md +1 -1
  16. package/content/skills/theming/SKILL.md +3 -4
  17. package/content/skills/timetrack/SKILL.md +61 -26
  18. package/content/skills/verify-in-app/SKILL.md +107 -0
  19. package/migrations/app-styling-utilities.md +114 -0
  20. package/migrations/list-state-query-form.md +103 -0
  21. package/migrations/nx-layout.md +23 -0
  22. package/migrations/sdk-components-over-hand-built-ui.md +69 -0
  23. package/migrations/search-query-field.md +45 -0
  24. package/migrations.json +43 -0
  25. package/package.json +4 -1
  26. package/src/index.js +5 -4
  27. package/src/index.js.map +1 -1
  28. package/src/lib/config.d.ts +2 -0
  29. package/src/lib/config.js +16 -3
  30. package/src/lib/config.js.map +1 -1
  31. package/src/lib/filter.d.ts +2 -0
  32. package/src/lib/filter.js +4 -4
  33. package/src/lib/filter.js.map +1 -1
  34. package/src/lib/package-runner.d.ts +1 -0
  35. package/src/lib/package-runner.js +34 -0
  36. package/src/lib/package-runner.js.map +1 -0
  37. package/src/lib/plan.js +10 -0
  38. package/src/lib/plan.js.map +1 -1
  39. package/src/lib/sync.js +19 -12
  40. package/src/lib/sync.js.map +1 -1
  41. package/src/lib/timetrack-command.js +98 -1
  42. package/src/lib/timetrack-command.js.map +1 -1
  43. package/src/lib/timetrack.d.ts +67 -0
  44. package/src/lib/timetrack.js +16 -1
  45. 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/polling/pagination, or bridges a query into UI or RxJS.
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 is the index plus the load-bearing facts, so you
17
- don't re-derive them from source.
18
-
19
- | Page | Covers |
20
- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
21
- | {%docsBaseUrl%}/query/ | Overview + the two-generations note |
22
- | {%docsBaseUrl%}/query/queries | **Start here** - client, creators, the query object's signals, auto-execution |
23
- | {%docsBaseUrl%}/query/features | `withArgs`, `withPolling`, `withLongPolling`, `withAutoRefresh`, side-effect handlers |
24
- | {%docsBaseUrl%}/query/http | REST creators, typing requests, response transforms, upload progress |
25
- | {%docsBaseUrl%}/query/auth | Bearer auth: login/refresh, auto token refresh, multi-tab sync |
26
- | {%docsBaseUrl%}/query/caching · `/stacks` · `/errors` · `/gql` · `/ws` | Caching/dedup, pagination, error/retry, GraphQL, WebSockets |
27
- | {%docsBaseUrl%}/query/multi-tab | Opt-in cross-tab sync: shared responses, per-key polling election, mutation fan-out |
28
- | {%docsBaseUrl%}/query/query-forms | Router-synced filter/search forms |
29
- | {%docsBaseUrl%}/query/legacy | The maintenance-mode `V2QueryClient` |
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
- - **Current (use this):** signals-first, provider-based. `createQueryClient`,
34
- `createGetQuery`/`createPostQuery`/…, `withArgs`. Everything imports from the
35
- single entry `@ethlete/query`.
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 endpoint, one live query per component instance:
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 getPost = createGetQuery(apiClient)<GetPostArgs>((p) => `/posts/${p.pathParams.postId}`);
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({ args })`. A function route (`pathParams`)
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`** - a `Signal` that also has
64
- `.asObservable()`. So each is both a signal (call it) and a stream:
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
- `query.response.asObservable()` binds to the query's own injector, so callers get
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
- if (response === null) throw snapshot.error();
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
- return response.items;
116
- }),
117
- );
118
- }
119
- }
120
- ```
185
+ ## Bridging into RxJS or callbacks
121
186
 
122
- Do not set a search signal and immediately return the shared `response` stream: the
123
- previous response is retained during re-execution and can be the first non-null emission.
124
- Unsubscribing from the wrapper stops result delivery but does not by itself abort the
125
- promise-backed execution; use the query's reactive `withArgs` lifecycle when cancellation
126
- is a requirement rather than a callback contract.
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
- - Signals-first: read `query.response()` in templates/computeds; it's **nullable**
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` `bracket` `bracket-rounds-list` `breadcrumb` `button` `calendar` `carousel`
38
- `cascader` `chip` `choice-inputs` `date-time-inputs` `dropzone` `error-codes`
39
- `filter-overlay` `floating-action` `focus-ring` `forms` `grid` `icon` `loader`
40
- `localization` `masonry` `match` `menu` `mixed-state` `notification` `overlay-openers`
41
- `overlays` `pagination` `picture` `query-devtools` `query-error` `rich-text-editor`
42
- `scrollable` `select` `skeleton` `slider` `sport-recipes` `standings` `stream` `table`
43
- `tabs` `text-inputs` `time-picker` `toggletip` `tooltip`
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
- yarn et update --check
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
- yarn et update core # only @ethlete/core
30
- yarn et update core --to 5.0.0-next.55
31
- yarn et update --tag latest # leave the prerelease line
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
- yarn et update
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, and writes what is left to `.ethlete/update`.
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
- yarn et update --continue
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. **Component source is plain CSS, never Tailwind.** The `.css` next to a
16
- component, wrapped in `@layer components`, using surface/color tokens - see
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 is
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 -nE -- '--(color|text|font|spacing)-' {%themeStylesheet%}
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` | `@typescript-eslint/consistent-type-definitions` |
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()`, are what let a Tailwind utility override component
113
- CSS.** Wrap every component CSS file in `@layer components { … }`; `:where()` only
114
- flattens a component's own modifiers to single-class weight. See the `styling` rule.
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.