@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 +23 -0
- package/README.md +8 -1
- package/content/hooks/context-warning.py +93 -7
- package/content/rules/app-styling.md +23 -16
- package/content/rules/lint-and-format.md +1 -1
- package/content/rules/styling.md +1 -1
- package/content/skills/handoff/SKILL.md +2 -1
- package/content/skills/query/SKILL.md +71 -66
- package/content/skills/sdk-docs/SKILL.md +4 -4
- package/content/skills/styleguide/SKILL.md +1 -1
- package/content/skills/theming/SKILL.md +15 -8
- package/content/skills/timetrack/SKILL.md +47 -1
- package/migrations.json +5 -0
- package/package.json +2 -2
- package/src/index.js +37 -17
- package/src/index.js.map +1 -1
- package/src/lib/cli-args.d.ts +9 -0
- package/src/lib/cli-args.js +48 -0
- package/src/lib/cli-args.js.map +1 -0
- package/src/lib/config.d.ts +1 -0
- package/src/lib/config.js +37 -2
- package/src/lib/config.js.map +1 -1
- package/src/lib/package-runner.d.ts +2 -0
- package/src/lib/package-runner.js +45 -10
- package/src/lib/package-runner.js.map +1 -1
- package/src/lib/plan.js +20 -0
- package/src/lib/plan.js.map +1 -1
- package/src/lib/sync.js +2 -1
- package/src/lib/sync.js.map +1 -1
- package/src/lib/timetrack-command.js +88 -2
- package/src/lib/timetrack-command.js.map +1 -1
- package/src/lib/timetrack.d.ts +65 -0
- package/src/lib/timetrack.js +28 -1
- package/src/lib/timetrack.js.map +1 -1
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.
|
|
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
|
|
729
|
+
return empty
|
|
662
730
|
try:
|
|
663
731
|
state = json.loads(raw)
|
|
664
732
|
if isinstance(state, dict):
|
|
665
|
-
return {
|
|
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)
|
|
741
|
+
return {**empty, "tier": int(raw or 0)}
|
|
670
742
|
except ValueError:
|
|
671
|
-
return
|
|
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
|
-
|
|
751
|
-
|
|
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
|
|
13
|
-
theme utilities (`bg-et-surface-bg`,
|
|
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.
|
|
26
|
-
`@layer utilities` — never in `@layer components`, and that includes the global
|
|
27
|
-
`@layer components` at runtime, after your
|
|
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.
|
|
40
|
-
`--spacing: 0.4rem` in the app's `@theme
|
|
41
|
-
own Storybook does
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
48
|
-
(`--et-surface-*-solid`, `--et-theme-color-*`)
|
|
49
|
-
|
|
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. `
|
|
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
|
|
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
|
|
package/content/rules/styling.md
CHANGED
|
@@ -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. `
|
|
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/
|
|
32
|
-
| {%docsBaseUrl%}/query/
|
|
33
|
-
| {%docsBaseUrl%}/query/
|
|
34
|
-
| {%docsBaseUrl%}/query/
|
|
35
|
-
| {%docsBaseUrl%}/query/
|
|
36
|
-
| {%docsBaseUrl%}/query/
|
|
37
|
-
| {%docsBaseUrl%}/query/
|
|
38
|
-
| {%docsBaseUrl%}/query/
|
|
39
|
-
| {%docsBaseUrl%}/query/migrating-from-
|
|
40
|
-
| {%docsBaseUrl%}/query-
|
|
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
|
|
47
|
-
|
|
|
48
|
-
| One client per API
|
|
49
|
-
| REST reads (auto-executing)
|
|
50
|
-
| REST writes (manual)
|
|
51
|
-
| The same behind a bearer token
|
|
52
|
-
| Upload/download progress bar
|
|
53
|
-
| Args that follow signals (route params, inputs, filters)
|
|
54
|
-
| A filtered / searched / sorted / paged list synced to the URL
|
|
55
|
-
| Refetch on an interval
|
|
56
|
-
| Next request once the previous settled, with a cursor from it
|
|
57
|
-
| Refetch when some other signal changes
|
|
58
|
-
| Toast / log / react on success or failure
|
|
59
|
-
| Patch a loaded response in place (e.g. from a socket message)
|
|
60
|
-
| Page falls out of range after a filter shrinks the results
|
|
61
|
-
| Package your own reusable query behavior
|
|
62
|
-
| Refresh reads after a mutation (here and in other tabs)
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
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` `
|
|
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
|
|
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`**: `
|
|
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, `
|
|
89
|
-
theme) and either bind it
|
|
90
|
-
|
|
91
|
-
|
|
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. `
|
|
110
|
-
is a hard requirement wherever it is
|
|
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.
|