@ethlete/agent-rules 0.1.0-next.19 → 0.1.0-next.20

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 CHANGED
@@ -1,5 +1,28 @@
1
1
  # @ethlete/agent-rules
2
2
 
3
+ ## 0.1.0-next.20
4
+
5
+ ### Minor Changes
6
+
7
+ - `ethlete-agents` rejects unknown flags, derives the lint and Storybook commands from `package.json`, names a config file that does not parse, and warns about unknown config keys.
8
+ - `timetrack approvals` lists the writes that wait for approval, and `timetrack approval <id> --reject` rejects one the way the Reject button does; approving stays the user's click in the app.
9
+ - `timetrack ask` asks auto mode again about a stand-in or an unnamed band of any day, as the app's "Ask auto mode again" does.
10
+ - `timetrack standins --resolve <id> --issue <KEY>` resolves a stand-in to a Jira issue, or moves a resolved one to another, once the user approves it in the app.
11
+ - Read a day's call transcript through the read-only `transcript.day` agent op and `ethlete-agents timetrack transcript [day]`.
12
+
13
+ ### Patch Changes
14
+
15
+ - `ethlete-agents sync`, `check` and `migrate` now reject a value on a flag that takes none (`--dry-run=false`) and ignore empty `--targets` entries.
16
+ - The `context-warning` hook no longer asks to finish or hand off while a handoff is being saved.
17
+ - The theming skill and styling rules recommend `injectSemanticColorTheme(type)` over the deprecated `injectErrorTheme()` and its siblings.
18
+ - The `sdk-docs` skill lists the `testing` component docs page.
19
+ - The theming skill covers `injectSemanticColorTheme()` and `SurfaceTheme.semanticColorThemes`.
20
+ - The `app-styling` rule now also fits apps on Tailwind 3 and apps that define their own colour palette.
21
+ - The `tslib` peer is the range `^2.3.0` instead of an exact version.
22
+ - Mark every migration with a `level`, so `et update` runs only the required ones.
23
+ - The query skill names `createQueryGroup` and `withOptimisticUpdate` and links the query groups page; the sdk-docs skill lists the `paged-query-trigger` domain.
24
+ - List the stat tile in the component domains of the `sdk-docs` skill.
25
+
3
26
  ## 0.1.0-next.19
4
27
 
5
28
  ### Minor Changes
package/README.md CHANGED
@@ -15,6 +15,10 @@ npx ethlete-agents init # writes ethlete-agents.config.json
15
15
  npx ethlete-agents sync # writes the generated rules and skills
16
16
  ```
17
17
 
18
+ `init` pre-fills `vars` with the lint, format and Storybook commands for your repo; check them before
19
+ the first sync. `sync`, `check`, `init` and `migrate` reject a flag they do not know and print the usage
20
+ instead of running, and a config file that does not parse is reported by name.
21
+
18
22
  Commit the generated files, and add a drift check to CI:
19
23
 
20
24
  ```bash
@@ -111,7 +115,10 @@ Prettier rewrites them and `check` then reports drift on every run:
111
115
  `package.json`). Without one, the git-commit guide presents the format as the repo's
112
116
  convention and never mentions a `commitlint` run - an agent that goes looking for a
113
117
  promised validator and finds nothing reports the discrepancy instead of just
114
- committing. Setting any derived var in `vars` overrides the detection.
118
+ committing. `lintCommand` / `lintFixCommand` run the repo's `lint` script, else
119
+ `nx lint <project>` when Nx is installed, and `storybookStartCommand` runs its `storybook`
120
+ script; `sync` warns when a lint var names a script `package.json` does not have. Setting
121
+ any derived var in `vars` overrides the detection.
115
122
  - **`exclude`** - rule or skill names to skip entirely for every configured agent and
116
123
  developer. For example, `"exclude": ["git-flow", "handoff"]` prevents those skills
117
124
  from being generated; the next `sync` also removes copies generated previously. Unknown
@@ -61,6 +61,11 @@ one it took: finish, hand off, or continue past the boundary for a reason it
61
61
  states in one sentence. A third tier at 95% withdraws only the "finish" option,
62
62
  because a budget that small covers no task.
63
63
 
64
+ Goes quiet while a handoff is being saved: from a prompt that requests save mode, or the
65
+ auto-mode save this hook ordered, until the next prompt; and for the rest of a turn in
66
+ which the agent invoked the handoff skill or wrote a handoff file. A save that is told to
67
+ choose between finishing and handing off again never ends. `resume` is not a save.
68
+
64
69
  Warns once per tier per session (state kept in a temp file); re-arms itself
65
70
  if the context shrinks again (e.g. after a compaction). The Stop event keeps its
66
71
  own per-tier counter, so crossing a tier mid-run costs at most one notice while
@@ -81,6 +86,7 @@ a tool batch or the end of a turn.
81
86
 
82
87
  import json
83
88
  import os
89
+ import re
84
90
  import sys
85
91
  import tempfile
86
92
 
@@ -435,6 +441,67 @@ def codex_context_state(transcript_path):
435
441
  CONTEXT_READERS = {"claude": claude_context_state, "codex": codex_context_state}
436
442
 
437
443
 
444
+ HANDOFF_SKILLS = ("handoff", "ethlete-handoff")
445
+ COMMAND_PATTERN = re.compile(r"<command-name>/?([^<]*)</command-name>")
446
+ ARGS_PATTERN = re.compile(r"<command-args>([^<]*)</command-args>")
447
+
448
+
449
+ def is_handoff_save(skill, args):
450
+ return skill in HANDOFF_SKILLS and not (args or "").strip().lower().startswith("resume")
451
+
452
+
453
+ def handoff_save_requested(prompt):
454
+ """True for a prompt that runs the handoff skill in save mode - a slash command or a plain one."""
455
+ if not isinstance(prompt, str):
456
+ return False
457
+ command = COMMAND_PATTERN.search(prompt)
458
+ if command:
459
+ args = ARGS_PATTERN.search(prompt)
460
+ return is_handoff_save(command.group(1).strip(), args.group(1) if args else "")
461
+ words = prompt.strip().split(None, 1)
462
+ return bool(words) and is_handoff_save(words[0].lstrip("/").lower(), words[1] if len(words) > 1 else "")
463
+
464
+
465
+ def is_turn_start(obj):
466
+ if obj.get("type") != "user" or obj.get("isSidechain") or obj.get("isMeta"):
467
+ return False
468
+ origin = obj.get("origin")
469
+ if isinstance(origin, dict):
470
+ return origin.get("kind") == "human"
471
+ content = (obj.get("message") or {}).get("content")
472
+ return isinstance(content, str)
473
+
474
+
475
+ def starts_handoff(block):
476
+ if not isinstance(block, dict) or block.get("type") != "tool_use":
477
+ return False
478
+ tool_input = block.get("input") or {}
479
+ if block.get("name") == "Skill":
480
+ return is_handoff_save(tool_input.get("skill"), tool_input.get("args"))
481
+ path = tool_input.get("file_path")
482
+ return isinstance(path, str) and path.endswith(".md") and "handoffs" in path.replace("\\", "/").split("/")
483
+
484
+
485
+ def handoff_under_way_in_turn(transcript_path):
486
+ """True when the current Claude turn requested, invoked or wrote a handoff save."""
487
+ lines, _ = tail_lines(transcript_path, TAIL_BYTES)
488
+ for line in lines:
489
+ if not line.strip():
490
+ continue
491
+ try:
492
+ obj = json.loads(line)
493
+ except (json.JSONDecodeError, UnicodeDecodeError):
494
+ continue
495
+ if obj.get("isSidechain"):
496
+ continue
497
+ content = (obj.get("message") or {}).get("content")
498
+ if is_turn_start(obj):
499
+ return handoff_save_requested(content if isinstance(content, str) else None)
500
+ if obj.get("type") == "assistant" and isinstance(content, list) and any(map(starts_handoff, content)):
501
+ return True
502
+ return False
503
+
504
+
438
505
  def headroom(tokens, budget):
439
506
  """How much budget is left, as a short human string."""
440
507
  left = max(budget - tokens, 0)
@@ -653,22 +720,27 @@ def emit(event, additional_context, system_message=None):
653
720
 
654
721
 
655
722
  def read_state(path):
656
- """{"tier": n, "stop_tier": n} from the state file; zeroes when absent or unreadable."""
723
+ """{"tier": n, "stop_tier": n, "saving": bool} from the state file; zeroes when absent or unreadable."""
724
+ empty = {"tier": 0, "stop_tier": 0, "saving": False}
657
725
  try:
658
726
  with open(path, encoding="utf-8") as f:
659
727
  raw = f.read().strip()
660
728
  except OSError:
661
- return {"tier": 0, "stop_tier": 0}
729
+ return empty
662
730
  try:
663
731
  state = json.loads(raw)
664
732
  if isinstance(state, dict):
665
- return {"tier": int(state.get("tier", 0)), "stop_tier": int(state.get("stop_tier", 0))}
733
+ return {
734
+ "tier": int(state.get("tier", 0)),
735
+ "stop_tier": int(state.get("stop_tier", 0)),
736
+ "saving": state.get("saving") is True,
737
+ }
666
738
  except (ValueError, TypeError):
667
739
  pass
668
740
  try:
669
- return {"tier": int(raw or 0), "stop_tier": 0}
741
+ return {**empty, "tier": int(raw or 0)}
670
742
  except ValueError:
671
- return {"tier": 0, "stop_tier": 0}
743
+ return empty
672
744
 
673
745
 
674
746
  def write_state(path, state):
@@ -734,6 +806,16 @@ def main():
734
806
  state = read_state(state_file)
735
807
  prev_tier = state["tier"]
736
808
 
809
+ if event == "UserPromptSubmit":
810
+ saving = handoff_save_requested(data.get("prompt"))
811
+ if saving != state["saving"]:
812
+ state = {**state, "saving": saving}
813
+ write_state(state_file, state)
814
+ if saving:
815
+ return
816
+ elif tier and (state["saving"] or (agent == "claude" and handoff_under_way_in_turn(transcript_path))):
817
+ return
818
+
737
819
  if event == "Stop":
738
820
  # Below the critical tier the mid-run notice is enough; interrupting the end of a turn
739
821
  # at 70% costs more than it saves.
@@ -747,8 +829,12 @@ def main():
747
829
  emit(event, additional_context)
748
830
  return
749
831
 
750
- if tier != prev_tier:
751
- write_state(state_file, {**state, "tier": tier, "stop_tier": min(state["stop_tier"], tier)})
832
+ orders_save = auto_mode and tier >= 2 and tier > prev_tier and not is_subagent
833
+ if tier != prev_tier or orders_save:
834
+ write_state(
835
+ state_file,
836
+ {**state, "tier": tier, "stop_tier": min(state["stop_tier"], tier), "saving": state["saving"] or orders_save},
837
+ )
752
838
 
753
839
  if tier <= prev_tier:
754
840
  # Below the first threshold, or the context shrank - the state was re-armed above.
@@ -9,8 +9,10 @@ requires: ['@ethlete/core']
9
9
  ## Styling
10
10
 
11
11
  Every component in this workspace (views, pages, shells, shared UI in `libs/`) is laid out
12
- and styled with **Tailwind utility classes in its template**, including the generated
13
- theme utilities (`bg-et-surface-bg`, `border-et-surface-border`, `text-et-<theme>`). Do not
12
+ and styled with **Tailwind utility classes in its template**, including the colour utilities
13
+ the app's theme provides: the generated SDK theme utilities (`bg-et-surface-bg`,
14
+ `border-et-surface-border`, `text-et-<theme>`) where the app generates them, or the palette its
15
+ Tailwind config defines. Do not
14
16
  invent a BEM class system, and do not write a `.css` file for layout, spacing or
15
17
  typography a utility expresses. Every app component sets `encapsulation: ViewEncapsulation.None`,
16
18
  as the `require-view-encapsulation-none` lint rule requires. Its CSS is then global, so give the
@@ -22,10 +24,12 @@ In an existing repo, apply this to the component you are editing, not to the res
22
24
  legacy component that relies on emulated encapsulation keeps it until its CSS is scoped under a
23
25
  host class: without that scope, `ViewEncapsulation.None` leaks its CSS into every view.
24
26
 
25
- Write CSS only for what utilities cannot express. Keep that CSS **unlayered** or in
26
- `@layer utilities` — never in `@layer components`, and that includes the global stylesheet. SDK component styles are injected into
27
- `@layer components` at runtime, after your stylesheet, so an app rule in the same layer
28
- ties on layer and loses on source order.
27
+ Write CSS only for what utilities cannot express. With Tailwind 4, keep that CSS **unlayered**
28
+ or in `@layer utilities` — never in `@layer components`, and that includes the global
29
+ stylesheet. SDK component styles are injected into `@layer components` at runtime, after your
30
+ stylesheet, so an app rule in the same layer ties on layer and loses on source order. With
31
+ Tailwind 3, `@layer` is a Tailwind directive that emits unlayered CSS, so app CSS already beats
32
+ the SDK's layered styles.
29
33
 
30
34
  To change an SDK component's look, set its `--et-*` tokens first (documented per
31
35
  component), then use a utility class or unlayered CSS on the element — see "Overriding
@@ -36,17 +40,20 @@ The SDK sizes everything in `rem` on a **10px root**: the app's global styleshee
36
40
  `1rem` is `10px`, so the app's own `rem` values stay easy to read.
37
41
 
38
42
  The 10px root also shrinks every rem-based Tailwind scale to 62.5% of its nominal size (1.6×
39
- smaller): `p-4` is 10px instead of 16px, and `max-w-3xl` is 480px instead of 768px. Set
40
- `--spacing: 0.4rem` in the app's `@theme` so the spacing scale keeps its 4px step, as the SDK's
41
- own Storybook does. In an app that already uses utilities without it, setting it rescales every
42
- view: tell the user rather than set it. Redefine any other rem scale the app uses (`--text-*`, `--container-*`,
43
- `--radius-*`) the same way, or use px where a size matters. Breakpoints are not affected: a
44
- media query's `rem` ignores the root font size.
43
+ smaller): `p-4` is 10px instead of 16px, and `max-w-3xl` is 480px instead of 768px. Keep the
44
+ spacing scale on its 4px step: with Tailwind 4, set `--spacing: 0.4rem` in the app's `@theme`,
45
+ as the SDK's own Storybook does; with Tailwind 3, redefine `theme.spacing` in
46
+ `tailwind.config`. In an app that already uses utilities without it, the change rescales every
47
+ view: tell the user rather than make it. Redefine any other rem scale the app uses (text,
48
+ container, radius) the same way, or use px where a size matters. Breakpoints are not affected:
49
+ a media query's `rem` ignores the root font size.
45
50
 
46
51
  **Never use a hardcoded colour as the primary value.** Backgrounds, text, borders and
47
- interaction states resolve from the surface and colour theming tokens
48
- (`--et-surface-*-solid`, `--et-theme-color-*`) or their generated utilities. A static
49
- fallback inside `var(--token, <fallback>)` is permitted, but not required.
52
+ interaction states resolve from the app's theme: the SDK surface and colour tokens
53
+ (`--et-surface-*-solid`, `--et-theme-color-*`) and their generated utilities, or the palette
54
+ the app's Tailwind config defines. Never write an arbitrary colour value such as
55
+ `bg-[#1a1a1a]` in a template. A static fallback inside `var(--token, <fallback>)` is
56
+ permitted, but not required.
50
57
 
51
58
  Theme **names** (`brand`, `danger`, `dark-elevated`, …) are registered by this app; the SDK
52
- ships none. Semantic colours resolve by theme `type` (e.g. `injectErrorTheme()`).
59
+ ships none. Semantic colours resolve by theme `type` (e.g. `injectSemanticColorTheme('error')`).
@@ -12,7 +12,7 @@ Run lint with `--fix` — most styleguide rules in `@ethlete/eslint-plugin` ship
12
12
  so let them do the work before correcting anything by hand:
13
13
 
14
14
  ```bash
15
- {%lintFixCommand%} # auto-fixes first (case, ordering, $ suffix, metadata, …)
15
+ {%lintFixCommand%} # auto-fixes first (case, ordering, metadata, …); rename to the $ suffix by hand
16
16
  {%lintCommand%} # then re-run to see what needs a manual fix
17
17
  ```
18
18
 
@@ -34,4 +34,4 @@ interaction states resolve from the surface and colour theming tokens
34
34
  Theme **names** (`brand`, `danger`, `dark-elevated`, …) are registered by the application;
35
35
  the SDK ships none. Never hardcode them as an SDK-defined union or reusable API contract.
36
36
  If an app-specific example names one, label it as belonging to that app. Semantic colours
37
- resolve by theme `type` (e.g. `injectErrorTheme()`).
37
+ resolve by theme `type` (e.g. `injectSemanticColorTheme('error')`).
@@ -67,7 +67,8 @@ you can act. It adds a third option to the two above:
67
67
 
68
68
  Option 3 is a judgment call, not a way out of the other two. Naming a reason you cannot
69
69
  state plainly is the same as taking option 2 late and expensively. The hook asks once per
70
- tier, so it will not block the end of a turn twice for the same budget.
70
+ tier, so it will not block the end of a turn twice for the same budget. It stays quiet while a
71
+ handoff is being saved, until the user's next prompt.
71
72
 
72
73
  ## Save mode
73
74
 
@@ -16,77 +16,80 @@ paged queries, bearer auth, GraphQL, and a socket.io realtime client.
16
16
  non-trivial query work.** This guide maps every need to its API and page, plus the
17
17
  load-bearing facts, so you don't re-derive them from source or hand-build what ships.
18
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()` |
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/groups | Several queries behind one state: loading, error and retry follow the one that ran last |
32
+ | {%docsBaseUrl%}/query/batching | Bulk writes with bounded concurrency, per-item results, retry |
33
+ | {%docsBaseUrl%}/query/errors | Error object, opt-in parsers, form submission, violations, retries |
34
+ | {%docsBaseUrl%}/query/testing | Specs: answering requests, `@ethlete/query/testing` helpers and fakes |
35
+ | {%docsBaseUrl%}/query/query-forms | **Any filtered, searched, sorted or paged list** - `defineQueryForm` |
36
+ | {%docsBaseUrl%}/query/gql | GraphQL creators over GET/POST |
37
+ | {%docsBaseUrl%}/query/ws | socket.io rooms and live-updating responses |
38
+ | {%docsBaseUrl%}/query/legacy | The maintenance-mode `V2QueryClient` and its replacements |
39
+ | {%docsBaseUrl%}/query/migrating-from-v2 | Codemods and the screen-by-screen move off the legacy client |
40
+ | {%docsBaseUrl%}/query/migrating-from-ngrx-toolkit | The generator and interop that move a store off `@tomtomb/ngrx-toolkit` |
41
+ | {%docsBaseUrl%}/query-devtools/ | The devtools panel and `provideQueryDevtools()` |
41
42
 
42
43
  ## You need → use → read
43
44
 
44
45
  Before writing a helper, find your need here. Pages are under `{%docsBaseUrl%}/query/`.
45
46
 
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/` |
47
+ | You need | Use | Read |
48
+ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
49
+ | One client per API | `createQueryClient`, `injectApi()` | `queries` |
50
+ | REST reads (auto-executing) | `createGetQuery`, `createHeadQuery`, `createOptionsQuery` | `http` |
51
+ | REST writes (manual) | `createPostQuery`, `createPutQuery`, `createPatchQuery`, `createDeleteQuery` | `http` |
52
+ | The same behind a bearer token | `createSecureGetQuery`, `createSecureHeadQuery`, `createSecureOptionsQuery`, `createSecurePostQuery`, `createSecurePutQuery`, `createSecurePatchQuery`, `createSecureDeleteQuery` | `http`, `auth` |
53
+ | Upload/download progress bar | creator option `reportProgress: true`, then `loading().progress` | `http` |
54
+ | Args that follow signals (route params, inputs, filters) | `withArgs` | `features` |
55
+ | A filtered / searched / sorted / paged list synced to the URL | `defineQueryForm`, `queryField`, `searchQueryField`, `sortQueryField` | `query-forms` |
56
+ | Refetch on an interval | `withPolling` | `features` |
57
+ | Next request once the previous settled, with a cursor from it | `withLongPolling` | `features` |
58
+ | Refetch when some other signal changes | `withAutoRefresh` | `features` |
59
+ | Toast / log / react on success or failure | `withSuccessHandling`, `withErrorHandling`, `withLogging` | `features` |
60
+ | Patch a loaded response in place (e.g. from a socket message) | `withResponseUpdate` | `features`, `ws` |
61
+ | Page falls out of range after a filter shrinks the results | `withPageResetOnError`, `isPageOutOfRangeError` | `features` |
62
+ | Package your own reusable query behavior | `createQueryFeature` | `features` |
63
+ | Refresh reads after a mutation (here and in other tabs) | `injectApi().invalidateQueries({ url })` | `caching` |
64
+ | Show a mutation's result in cached reads before the server answers | `withOptimisticUpdate` | `caching` |
65
+ | One loading, error and retry state for whichever of several mutations ran last | `createQueryGroup` | `groups` |
66
+ | Refetch everything after a client-wide header changed | `injectApi().refreshQueriesInUse()` | `caching` |
67
+ | Show the previous data instantly on back navigation | `keepUnusedFor` + `executionState().cachedResponse` | `caching` |
68
+ | Last known data after a reload or offline | `withQueryPersistence`, `createIndexedDbQueryPersistenceAdapter`, `createNoopQueryPersistenceAdapter` | `persistence` |
69
+ | Tabs share responses, poll once, see each other's mutations | `withMultiTabSync` | `multi-tab` |
70
+ | Parallel detail requests for a list of ids | `createQueryStack`, `transformArrayResponse` | `stacks` |
71
+ | Load more / infinite scroll / classic paging | `createPagedQueryStack`, `ethletePaginationAdapter` (and other adapters) | `stacks` |
72
+ | A GET whose args come from another GET's response | `withArgs` reading the first query's `response()` | `dependent-queries` |
73
+ | Run mutations in order, each using the previous result | `querySequence` | `dependent-queries` |
74
+ | Many edits with progress, time remaining and retry of failures | `createQueryBatch` | `batching` |
75
+ | Submit a signal form through a mutation, violations on fields | `createQuerySubmission` | `errors` |
76
+ | Own submit handler, or server-side validation while typing | `executeUntilSettled`, `executeUntilSettled$`, `mapViolationsToFormErrors`, `validateWithQuery` | `errors` |
77
+ | Understand the error shapes your API returns | `withEthleteApiErrors` (all), `withSymfonyErrors`, `withHtmlErrorParsing` | `errors` |
78
+ | Error text for display, or an error built by hand (tests) | `queryErrorMessage`, `queryErrorMessages`, `createQueryErrorResponse`, `<et-query-error>` | `errors` |
79
+ | Retry failed requests | `withDefaultRetry`, `createDefaultRetryFn` | `errors` |
80
+ | Login / refresh / logout with bearer tokens | `createBearerAuthProvider`, `withAuthenticationQuery`, `withRefreshQuery` | `auth` |
81
+ | Protect routes, check roles or permissions | `createAuthGuard`, `canMatchWith` | `auth` |
82
+ | "Remember me" auto-login | `withPersistentAuth` | `auth` |
83
+ | Log out idle users | `withInactivityLogout` | `auth` |
84
+ | Warn before the session expires | `withTokenExpirationWarning` | `auth` |
85
+ | Revoke the token on logout | `withTokenRevocation` | `auth` |
86
+ | Share login/logout across tabs | `withBearerAuthMultiTabSync` | `auth` |
87
+ | Auth telemetry events | `withTracking` | `auth` |
88
+ | Tokens from SSO or a native shell | `provider.setTokens(access, refresh)` | `auth` |
89
+ | Which of several queries is busy (v2 query collections) | each query's `executionState()` in a `computed`; auth: `provider.executionState()` | `migrating-from-v2` |
90
+ | GraphQL | `createGqlQueryViaGet`, `createGqlQueryViaPost`, `createGqlMutationViaGet`, `createGqlMutationViaPost`, `createSecureGqlQueryViaGet`, `createSecureGqlQueryViaPost`, `createSecureGqlMutationViaGet`, `createSecureGqlMutationViaPost` | `gql` |
91
+ | Realtime rooms over socket.io | `createWebSocketClient` | `ws` |
92
+ | Inspect queries, stacks, auth and cache at runtime | `provideQueryDevtools()` + `@ethlete/query-devtools` | `/query-devtools/` |
90
93
 
91
94
  ## Two generations - use the current one
92
95
 
@@ -182,6 +185,8 @@ injection context of your own (it emits `null` first).
182
185
 
183
186
  - **`withArgs(() => ({ pathParams, queryParams, body }))`** runs like a `computed` and
184
187
  re-executes on change. Return `null` to park the query (pauses polling/auto-refresh).
188
+ Calling `.execute()` on a parked query is ignored and warns in dev mode
189
+ (`queryExecutedWhileParkedMessage`); pass `execute({ args })` or let the source return args.
185
190
  - **Prefer `withArgs` over `execute({ args })`.** Declared args keep a `GET` re-executing and
186
191
  polling/auto-refresh restarting off the same signal; a function route throws without it. A
187
192
  mutation with `withArgs` is just `.execute()`. Reserve `execute({ args })` for a one-off
@@ -46,10 +46,10 @@ Component domains under `/components/`:
46
46
  `choice-inputs` `command-palette` `copy-button` `date-time-inputs` `description-list`
47
47
  `divider` `dropzone` `empty-state` `error-codes` `filter-overlay` `floating-action`
48
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`
49
+ `match` `menu` `mixed-state` `notification` `overlay-openers` `overlays` `paged-query-trigger`
50
+ `pagination` `picture` `pie-chart` `progress-steps` `query-error` `rich-text-editor` `sankey-chart`
51
+ `scheduler` `scrollable` `scrollbar` `select` `setup` `skeleton` `slider` `sport-recipes` `stat-tile`
52
+ `standings` `standings-pick` `stream` `table` `tabs` `testing` `text-inputs` `time-picker`
53
53
  `timeline` `toggletip` `toolbar` `tooltip` `tree`
54
54
 
55
55
  **Check this list before you build any UI by hand.** Some needs hide behind a name you
@@ -14,7 +14,7 @@ Don't hand-check or hand-fix those - **run lint with `--fix` first**; many rules
14
14
  ship auto-fixers, so most violations are corrected for you:
15
15
 
16
16
  ```bash
17
- {%lintFixCommand%} # auto-fixes first (case, ordering, $ suffix, metadata, …)
17
+ {%lintFixCommand%} # auto-fixes first (case, ordering, metadata, …); rename to the $ suffix by hand
18
18
  {%lintCommand%} # then re-run to see what needs a manual fix
19
19
  ```
20
20
 
@@ -39,8 +39,8 @@ every scope class - so the tokens are always resolvable and components just read
39
39
 
40
40
  **Theme names are yours.** Never hardcode them as an SDK-defined union or reusable API
41
41
  contract. App-specific docs may name their own themes when they label them as app values.
42
- The portable handle is the theme **`type`**: `injectErrorTheme()` finds whichever theme
43
- the app registered with `type: 'error'`.
42
+ The portable handle is the theme **`type`**: `injectSemanticColorTheme('error')` finds
43
+ whichever theme the app registered with `type: 'error'`.
44
44
 
45
45
  ## Tokens components may consume
46
46
 
@@ -85,10 +85,17 @@ the interactive element itself, never on a wrapper.
85
85
  color and `resolveSurfaceByElevation(themes, type, elevation + 1)` for the surface.
86
86
  - **Semantic colors come through DI, not CSS.** There is no global "error color"
87
87
  variable - error/warning/success are just color themes. To render something in the
88
- error color, `injectErrorTheme()` (it throws if the app registered no `type: 'error'`
89
- theme) and either bind it (`[etProvideColor]="errorColorTheme"`) or force it
90
- programmatically (`provideColor.forceColor(theme)` / `clearForcedColor()`). Inside
91
- that scope, `--et-theme-color-primary-*` _is_ the error color.
88
+ error color, `injectSemanticColorTheme('error')` (reading it throws if the app
89
+ registered no `type: 'error'` theme) and either bind it
90
+ (`[etProvideColor]="errorColorTheme()"`) or force it programmatically
91
+ (`provideColor.forceColor(theme)` / `clearForcedColor()`). Inside that scope,
92
+ `--et-theme-color-primary-*` _is_ the error color.
93
+ - **`injectSemanticColorTheme(type)` follows the surface**: it is a signal of the theme
94
+ the nearest surface names in `SurfaceTheme.semanticColorThemes` (e.g.
95
+ `{ success: 'success-on-light' }`, an extra theme registered without a `type`), else
96
+ the theme of that `type`. Never add per-surface logic to a color theme or new CSS vars
97
+ for it. `injectErrorTheme()`, `injectWarningTheme()` and `injectSuccessTheme()` are
98
+ deprecated: they ignore the surface.
92
99
 
93
100
  ## Pitfalls
94
101
 
@@ -106,8 +113,8 @@ the interactive element itself, never on a wrapper.
106
113
  hardcoded semantic color in CSS can't be replaced by it unless the right theme is
107
114
  provided on that element.
108
115
  - A static fallback (`var(--et-surface-border-solid, rgb(255 255 255 / 0.1))`) is
109
- permitted for theme-less setups, but themes make it unnecessary. `injectErrorTheme()`
110
- is a hard requirement wherever it is used.
116
+ permitted for theme-less setups, but themes make it unnecessary. `injectSemanticColorTheme()`
117
+ is a hard requirement wherever it is read.
111
118
  - **Cascade layers, not `:where()`, decide who wins against SDK component CSS.** SDK
112
119
  styles live in `@layer components`, so a utility or unlayered app CSS overrides them.
113
120
  An app rule inside `@layer components` loses. See the `app-styling` rule.