@ethlete/agent-rules 0.1.0-next.3 → 0.1.0-next.31

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 (200) hide show
  1. package/AGENTS.md +28 -0
  2. package/CHANGELOG.md +480 -0
  3. package/CLAUDE.md +1 -0
  4. package/README.md +362 -29
  5. package/THIRD-PARTY-LICENSES.md +35 -0
  6. package/content/defaults.json +1 -1
  7. package/content/git-hooks/post-checkout.sh +10 -0
  8. package/content/git-hooks/pre-push.sh +5 -0
  9. package/content/hooks/context-warning.py +872 -110
  10. package/content/hooks/subagent-model-policy.py +194 -0
  11. package/content/mods/.claude-plugin/marketplace.json +11 -0
  12. package/content/mods/plugins/e2e-progress/.claude-plugin/plugin.json +6 -0
  13. package/content/mods/plugins/e2e-progress/hooks/hooks.json +1 -0
  14. package/content/mods/plugins/e2e-progress/hooks/register.tsx +442 -0
  15. package/content/mods/plugins/e2e-progress/tsconfig.json +3 -0
  16. package/content/mods/plugins/e2e-progress/types/index.d.ts +23 -0
  17. package/content/output-styles/ste-clarity.md +131 -0
  18. package/content/rules/agent-sessions-wake-up.md +11 -0
  19. package/content/rules/app-styling.md +66 -0
  20. package/content/rules/comments.md +4 -1
  21. package/content/rules/lint-and-format.md +7 -5
  22. package/content/rules/nx-layout.md +50 -0
  23. package/content/rules/reactive-state.md +1 -1
  24. package/content/rules/styling.md +9 -7
  25. package/content/rules/subagent-models.md +39 -0
  26. package/content/skills/agent-sessions/SKILL.md +81 -0
  27. package/content/skills/agent-sessions/agent-sessions.mjs +498 -0
  28. package/content/skills/angular-patterns/SKILL.md +30 -7
  29. package/content/skills/api-source/SKILL.md +156 -0
  30. package/content/skills/app-testing/SKILL.md +141 -0
  31. package/content/skills/codex-subagent/SKILL.md +90 -0
  32. package/content/skills/codex-subagent/codex-agent.mjs +229 -0
  33. package/content/skills/design-exploration/SKILL.md +687 -0
  34. package/content/skills/design-exploration/check-story.mjs +139 -0
  35. package/content/skills/design-exploration/shoot-template.mjs +61 -0
  36. package/content/skills/domain-modeling/ADR-FORMAT.md +47 -0
  37. package/content/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  38. package/content/skills/domain-modeling/SKILL.md +80 -0
  39. package/content/skills/e2e-triage/SKILL.md +80 -0
  40. package/content/skills/figma-export/SKILL.md +191 -0
  41. package/content/skills/figma-export/dump-figma-layers.py +83 -0
  42. package/content/skills/figma-export/dump-figma-svg.py +235 -0
  43. package/content/skills/figma-export/measure-template.mjs +87 -0
  44. package/content/skills/git-commit/SKILL.md +11 -7
  45. package/content/skills/git-flow/SKILL.md +87 -0
  46. package/content/skills/grill-with-docs/SKILL.md +13 -0
  47. package/content/skills/grilling/SKILL.md +51 -0
  48. package/content/skills/handoff/SKILL.md +125 -5
  49. package/content/skills/query/SKILL.md +165 -64
  50. package/content/skills/rxjs-signals/SKILL.md +33 -4
  51. package/content/skills/sdk-docs/SKILL.md +50 -14
  52. package/content/skills/sdk-local-build/SKILL.md +26 -8
  53. package/content/skills/sdk-local-build/sdk-local-baseline.mjs +64 -0
  54. package/content/skills/sdk-source/SKILL.md +37 -34
  55. package/content/skills/sdk-update/SKILL.md +103 -0
  56. package/content/skills/story-styling/SKILL.md +10 -8
  57. package/content/skills/styleguide/SKILL.md +12 -7
  58. package/content/skills/styleguide/assets.md +54 -0
  59. package/content/skills/styleguide/changesets.md +17 -0
  60. package/content/skills/styleguide/file-structure.md +73 -0
  61. package/content/skills/styleguide/lint-rule-lookup.md +51 -0
  62. package/content/skills/styleguide/storybook-structure.md +25 -0
  63. package/content/skills/theming/SKILL.md +50 -28
  64. package/content/skills/timetrack/SKILL.md +397 -0
  65. package/content/skills/verify-in-app/SKILL.md +107 -0
  66. package/migrations/app-styling-utilities.md +63 -0
  67. package/migrations/list-state-query-form.md +20 -0
  68. package/migrations/nx-layout.md +29 -0
  69. package/migrations/sdk-components-over-hand-built-ui.md +27 -0
  70. package/migrations/search-query-field.md +52 -0
  71. package/migrations.json +48 -0
  72. package/package.json +19 -3
  73. package/src/index.js +85 -30
  74. package/src/index.js.map +1 -1
  75. package/src/lib/cli-args.d.ts +9 -0
  76. package/src/lib/cli-args.js +48 -0
  77. package/src/lib/cli-args.js.map +1 -0
  78. package/src/lib/commitlint.d.ts +10 -0
  79. package/src/lib/commitlint.js +51 -0
  80. package/src/lib/commitlint.js.map +1 -0
  81. package/src/lib/config.d.ts +41 -5
  82. package/src/lib/config.js +117 -10
  83. package/src/lib/config.js.map +1 -1
  84. package/src/lib/filter.d.ts +2 -0
  85. package/src/lib/filter.js +4 -4
  86. package/src/lib/filter.js.map +1 -1
  87. package/src/lib/frontmatter.d.ts +2 -0
  88. package/src/lib/frontmatter.js +33 -5
  89. package/src/lib/frontmatter.js.map +1 -1
  90. package/src/lib/git-flow/build.d.ts +35 -0
  91. package/src/lib/git-flow/build.js +24 -0
  92. package/src/lib/git-flow/build.js.map +1 -0
  93. package/src/lib/git-flow/config.d.ts +59 -0
  94. package/src/lib/git-flow/config.js +50 -0
  95. package/src/lib/git-flow/config.js.map +1 -0
  96. package/src/lib/git-flow/index.d.ts +6 -0
  97. package/src/lib/git-flow/index.js +23 -0
  98. package/src/lib/git-flow/index.js.map +1 -0
  99. package/src/lib/git-flow/parse.d.ts +49 -0
  100. package/src/lib/git-flow/parse.js +300 -0
  101. package/src/lib/git-flow/parse.js.map +1 -0
  102. package/src/lib/git-flow/rename.d.ts +24 -0
  103. package/src/lib/git-flow/rename.js +70 -0
  104. package/src/lib/git-flow/rename.js.map +1 -0
  105. package/src/lib/git-flow/start.d.ts +49 -0
  106. package/src/lib/git-flow/start.js +57 -0
  107. package/src/lib/git-flow/start.js.map +1 -0
  108. package/src/lib/git-flow/validate.d.ts +34 -0
  109. package/src/lib/git-flow/validate.js +72 -0
  110. package/src/lib/git-flow/validate.js.map +1 -0
  111. package/src/lib/git-flow-command.d.ts +4 -0
  112. package/src/lib/git-flow-command.js +165 -0
  113. package/src/lib/git-flow-command.js.map +1 -0
  114. package/src/lib/git-flow-repair.d.ts +17 -0
  115. package/src/lib/git-flow-repair.js +165 -0
  116. package/src/lib/git-flow-repair.js.map +1 -0
  117. package/src/lib/git-flow-start.d.ts +20 -0
  118. package/src/lib/git-flow-start.js +140 -0
  119. package/src/lib/git-flow-start.js.map +1 -0
  120. package/src/lib/git.d.ts +64 -0
  121. package/src/lib/git.js +131 -0
  122. package/src/lib/git.js.map +1 -0
  123. package/src/lib/gitlab.d.ts +35 -0
  124. package/src/lib/gitlab.js +116 -0
  125. package/src/lib/gitlab.js.map +1 -0
  126. package/src/lib/index.d.ts +2 -0
  127. package/src/lib/index.js +26 -11
  128. package/src/lib/index.js.map +1 -1
  129. package/src/lib/migrate.js +6 -1
  130. package/src/lib/migrate.js.map +1 -1
  131. package/src/lib/output-style-command.d.ts +3 -0
  132. package/src/lib/output-style-command.js +69 -0
  133. package/src/lib/output-style-command.js.map +1 -0
  134. package/src/lib/output-style.d.ts +38 -0
  135. package/src/lib/output-style.js +134 -0
  136. package/src/lib/output-style.js.map +1 -0
  137. package/src/lib/owned-paths.js +27 -3
  138. package/src/lib/owned-paths.js.map +1 -1
  139. package/src/lib/package-runner.d.ts +4 -0
  140. package/src/lib/package-runner.js +86 -0
  141. package/src/lib/package-runner.js.map +1 -0
  142. package/src/lib/plain-text.d.ts +9 -0
  143. package/src/lib/plain-text.js +22 -0
  144. package/src/lib/plain-text.js.map +1 -0
  145. package/src/lib/plan.d.ts +6 -6
  146. package/src/lib/plan.js +164 -40
  147. package/src/lib/plan.js.map +1 -1
  148. package/src/lib/prompt.d.ts +8 -0
  149. package/src/lib/prompt.js +35 -0
  150. package/src/lib/prompt.js.map +1 -0
  151. package/src/lib/render.d.ts +25 -2
  152. package/src/lib/render.js +43 -10
  153. package/src/lib/render.js.map +1 -1
  154. package/src/lib/sessions-command.d.ts +4 -0
  155. package/src/lib/sessions-command.js +16 -0
  156. package/src/lib/sessions-command.js.map +1 -0
  157. package/src/lib/sync.d.ts +0 -1
  158. package/src/lib/sync.js +30 -17
  159. package/src/lib/sync.js.map +1 -1
  160. package/src/lib/targets/agents-skills.js +1 -1
  161. package/src/lib/targets/agents-skills.js.map +1 -1
  162. package/src/lib/targets/claude-hooks.d.ts +3 -23
  163. package/src/lib/targets/claude-hooks.js +22 -86
  164. package/src/lib/targets/claude-hooks.js.map +1 -1
  165. package/src/lib/targets/claude-mods.d.ts +20 -0
  166. package/src/lib/targets/claude-mods.js +47 -0
  167. package/src/lib/targets/claude-mods.js.map +1 -0
  168. package/src/lib/targets/claude.js +2 -3
  169. package/src/lib/targets/claude.js.map +1 -1
  170. package/src/lib/targets/codex-hooks.d.ts +12 -0
  171. package/src/lib/targets/codex-hooks.js +35 -0
  172. package/src/lib/targets/codex-hooks.js.map +1 -0
  173. package/src/lib/targets/codex.js +4 -4
  174. package/src/lib/targets/codex.js.map +1 -1
  175. package/src/lib/targets/copilot.js +4 -4
  176. package/src/lib/targets/copilot.js.map +1 -1
  177. package/src/lib/targets/cursor.js +3 -3
  178. package/src/lib/targets/cursor.js.map +1 -1
  179. package/src/lib/targets/git-hooks.d.ts +25 -0
  180. package/src/lib/targets/git-hooks.js +71 -0
  181. package/src/lib/targets/git-hooks.js.map +1 -0
  182. package/src/lib/targets/hooks-shared.d.ts +60 -0
  183. package/src/lib/targets/hooks-shared.js +130 -0
  184. package/src/lib/targets/hooks-shared.js.map +1 -0
  185. package/src/lib/targets/mcp.d.ts +16 -0
  186. package/src/lib/targets/mcp.js +68 -0
  187. package/src/lib/targets/mcp.js.map +1 -0
  188. package/src/lib/targets/shared.d.ts +1 -9
  189. package/src/lib/targets/shared.js +4 -8
  190. package/src/lib/targets/shared.js.map +1 -1
  191. package/src/lib/timetrack-command.d.ts +11 -0
  192. package/src/lib/timetrack-command.js +1063 -0
  193. package/src/lib/timetrack-command.js.map +1 -0
  194. package/src/lib/timetrack-snapshot.d.ts +55 -0
  195. package/src/lib/timetrack-snapshot.js +120 -0
  196. package/src/lib/timetrack-snapshot.js.map +1 -0
  197. package/src/lib/timetrack.d.ts +561 -0
  198. package/src/lib/timetrack.js +279 -0
  199. package/src/lib/timetrack.js.map +1 -0
  200. package/content/skills/styleguide/STYLEGUIDE.md +0 -520
@@ -15,11 +15,92 @@ continue in a fresh session. Two modes:
15
15
  - **save** ("handoff", "wrap up") - write a handoff file.
16
16
  - **resume** ("continue from the handoff") - read one and continue the work.
17
17
 
18
+ Text after the slug or the name is an instruction from the user, in both modes. In save mode it is
19
+ for the session that resumes, so it must reach that session word for word. In resume mode it wins
20
+ over the Next steps of the file where the two conflict.
21
+
22
+ Name one invocation form to the user - your own - and never both. The skill is
23
+ installed as `ethlete-handoff`, so under Claude Code the commands are
24
+ `/ethlete-handoff` and `/ethlete-handoff resume [name]`; there is no `/handoff`.
25
+ Codex has no slash commands, so there the same two are plain prompts without the
26
+ leading `/`.
27
+
18
28
  Handoff files live in `{%handoffDir%}/` (gitignored - they are personal,
19
29
  ephemeral working state, not team docs).
20
30
 
31
+ ## Handoff mode
32
+
33
+ Where this repo installs the `context-warning` hook, it names the budget at session start,
34
+ turns handoff mode on at 70% of it, and repeats a short note with the remaining budget on
35
+ every prompt after that. It reaches you at three points, not only when the user types: at
36
+ the start of the session, after a batch of your own tool calls, and as a turn ends. So a
37
+ long autonomous run learns its token count while it works.
38
+
39
+ Handoff mode is not an order to stop. It is the signal to change how you work, so that
40
+ either ending - you finish, or a fresh session takes over - stays cheap:
41
+
42
+ - Work in slices that each end in a committable state. Never leave two half-finished edits
43
+ in the tree.
44
+ - Send bulky reads, searches and test runs to a sub-agent. Its output stays out of your
45
+ context; its answer does not.
46
+ - Write down each decision and dead end as you reach it, in the commit message or in the
47
+ handoff file. Do not save them for a final summary you may have no room for.
48
+ - Start nothing you cannot finish or hand off inside the remaining budget.
49
+
50
+ At the critical tier (85%) the hook asks you to choose one of two, and to tell the user
51
+ which you chose:
52
+
53
+ 1. **Finish the task.** Only when you can name every step that is left, and they plainly
54
+ fit the remaining budget. Finish it, commit, then apply the save test below - the answer
55
+ is then usually that no file is needed at all.
56
+ 2. **Hand off.** Everything else. Finish the in-flight atomic edit, nothing new, then save.
57
+
58
+ "Nearly done" means you can name the last steps now. It does not mean the end feels close.
59
+ At the last tier (95%) the hook takes option 1 away, because a budget that small covers no
60
+ task.
61
+
62
+ ### Ending a turn over the budget
63
+
64
+ From 85% on, the hook also fires as you try to end the turn, and the turn then continues so
65
+ you can act. It adds a third option to the two above:
66
+
67
+ 3. **Cross the boundary on purpose.** Some sessions are worth the premium rate: the task is
68
+ one step from done, or this session holds reasoning that no handoff file survives. Take
69
+ it only for a reason you can name in one sentence right now. Say the reason, say what it
70
+ costs, and keep working.
71
+
72
+ Option 3 is a judgment call, not a way out of the other two. Naming a reason you cannot
73
+ state plainly is the same as taking option 2 late and expensively. The hook asks once per
74
+ tier, so it will not block the end of a turn twice for the same budget. It stays quiet while a
75
+ handoff is being saved, until the user's next prompt.
76
+
21
77
  ## Save mode
22
78
 
79
+ **Decide first whether a handoff is needed at all.** One exists so work can resume, so
80
+ write one only when at least one of these holds:
81
+
82
+ - Uncommitted work from this session sits in the tree.
83
+ - A step the user asked for is unfinished, blocked, or waiting on a result.
84
+ - A decision, dead end, or user constraint from this session is written down nowhere else -
85
+ not in a commit message, a changeset, a docs page, or a plan file.
86
+
87
+ Check it, never assume it: `git status`, `git log`, and the task as the user stated it. A
88
+ context warning is a token count, not evidence that work is left.
89
+
90
+ If none of them holds, write no file. Tell the user in two or three lines what landed, name
91
+ the commits, and say a fresh session needs nothing from this one. A handoff nobody resumes is
92
+ noise, and it costs the next session the tokens to read it.
93
+
94
+ Then give a prompt the user can copy into the fresh session, in a code block: the next task, the
95
+ plan file or finding it lives in, and any rule of this session that the repo does not record. Keep
96
+ it to a few lines.
97
+
98
+ An open item counts as the next task: a known gap, an open finding, or an unbuilt step in the plan,
99
+ in the task, or in your own report of this session. "Everything waits for the user" is true only
100
+ when every open item needs a decision of the user. Then name each decision; do not stop silently.
101
+ In a `tmux` session, restart yourself with the next task as the prompt (see the next section) instead
102
+ of handing the prompt to the user.
103
+
23
104
  Write `{%handoffDir%}/<slug>.md` where `<slug>` is a short kebab-case name for
24
105
  the task (use the user-provided slug if they gave one). If the file exists,
25
106
  overwrite it - a handoff always describes the _current_ state.
@@ -81,18 +162,57 @@ map, not a transcript.
81
162
  After writing, tell the user where it landed and that a fresh session should
82
163
  resume from it.
83
164
 
165
+ ## Clear and resume on your own
166
+
167
+ Run inside a `tmux` session started with the agent-sessions `run` command or the shell functions from `shell-install` (`echo $TMUX` is not empty and
168
+ the agent-sessions script `restart` finds the session)? Then you may clear and
169
+ resume yourself after the save, instead of asking the user to. You decide. Do it only if every line
170
+ is true:
171
+
172
+ - No sub-agent, workflow or background command still runs. A background command from the old
173
+ session reports back into the new one, and a sub-agent answer has no reader after the clear.
174
+ - No answer you wait for is open: a peer session, a mailbox reply, a CI run.
175
+ - The tree is committed, or the handoff file names every uncommitted change.
176
+ - The handoff file is saved and its Next steps are concrete.
177
+
178
+ If one line is false, finish or collect that first. If you cannot, hand the prompt to the user as
179
+ before. Then run, with the slug of the file you saved:
180
+
181
+ ```bash
182
+ node <sessions-skill-dir>/agent-sessions.mjs restart <name> '<resume command> <slug> [<text after the slug>]'
183
+ ```
184
+
185
+ If the user wrote text after the slug of the save command, append it to the prompt word for word.
186
+ Escape each `'` in it as `'\''`. The restart drops anything you leave out, and the fresh session has
187
+ no other way to see it.
188
+
189
+ `<resume command>` is your own resume form from the top of this guide, with the name this repo
190
+ installed the skill under: `/ethlete-handoff resume` under Claude Code, `ethlete-handoff resume`
191
+ under Codex. A repo that ships its own copy can name it `handoff`, so check the skill directory
192
+ name. A wrong name runs no skill, and the fresh session starts without the file.
193
+
194
+ `<name>` is the sender name of your own session. Read it first with `tmux display-message -p -t "$TMUX_PANE" '#{session_name}'` and drop the `@<repo>` part: `claude-2@ethlete-sdk` gives `claude-2`. Never take the name from an example: a second Claude session is `claude-2`, not `claude`. The command refuses a session you do not run in. It returns at once. It
195
+ types `/clear` (`/new` for a `codex` name) and the prompt into your pane a few seconds later, so
196
+ end your turn after you run it and do not start another tool call. The `/clear` wipes the pane, so
197
+ the user never sees text you write after the command. Before you run it, put the reason as the
198
+ first line of the handoff file: `Restarted because: <one line>`. End the turn with no text.
199
+
84
200
  ## Resume mode
85
201
 
86
202
  1. List `{%handoffDir%}/`. Pick the file matching the given name, or the most
87
203
  recently modified one if no name was given. If the directory is empty, say so
88
204
  and stop.
89
- 2. Read the file fully.
205
+ 2. Read the file fully. If its first line starts with `Restarted because:`, show that line to the
206
+ user first, then delete it from the file.
90
207
  3. Verify reality still matches: current branch, `git status`, last commit. If
91
208
  they diverge from the handoff (e.g. someone committed in between), say what
92
209
  changed and adapt - the repo is the truth, the handoff is the guide.
93
- 4. Read any guides the handoff's work obviously needs (e.g. {%skill:theming%}
94
- before CSS work) - same rules as always.
210
+ 4. **Read only what you are about to change.** The handoff already carries the decisions,
211
+ the constraints and the dead ends, so do not re-read the plan files it summarises, and do
212
+ not survey the directory. Open a source file when you are going to edit it, and load a
213
+ focused guidance file only when the work needs one.
95
214
  5. Continue with the **Next steps** section. Don't redo work listed under
96
215
  _Done_; don't re-open questions under _Decisions_.
97
- 6. When every next step is complete (including changeset/docs follow-ups),
98
- delete the handoff file so the directory only contains live handoffs.
216
+ 6. When every next step is complete, decide whether the handoff can be removed. Delete it
217
+ only if this workflow created it and the save workflow or user authorized cleanup.
218
+ Otherwise report that it is no longer needed and let the user decide.
@@ -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,109 @@ 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`, `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/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()` |
42
+
43
+ ## You need → use → read
44
+
45
+ Before writing a helper, find your need here. Pages are under `{%docsBaseUrl%}/query/`.
46
+
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/` |
30
93
 
31
94
  ## Two generations - use the current one
32
95
 
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.
96
+ Write new code against the signals-first system (`createQueryClient`, `createGetQuery`, …,
97
+ `withArgs`), all from the single entry `@ethlete/query`. The class-based `V2QueryClient`
98
+ (`.prepare().execute()`, `queryComputed`) is in maintenance mode.
38
99
 
39
100
  ## Core usage
40
101
 
41
- One client per API, one creator per endpoint, one live query per component instance:
102
+ One client per API, one bound creator per method, one query per endpoint, one live query
103
+ per component instance:
42
104
 
43
105
  ```ts
44
106
  import { createQueryClient, createGetQuery, withArgs } from '@ethlete/query';
45
107
 
108
+ // api.ts
46
109
  export const apiClient = createQueryClient({ name: 'api', baseUrl: API_URL });
47
- export const getPost = createGetQuery(apiClient)<GetPostArgs>((p) => `/posts/${p.pathParams.postId}`);
110
+ export const getQuery = createGetQuery(apiClient);
111
+
112
+ // posts.queries.ts
113
+ export type GetPostQueryArgs = {
114
+ response: Post;
115
+ pathParams: { postId: string };
116
+ };
117
+
118
+ export const getPost = getQuery<GetPostQueryArgs>((p) => `/posts/${p.postId}`);
48
119
 
49
120
  // in a component (injection context):
50
121
  postId = input.required<string>();
@@ -54,61 +125,91 @@ post = computed(() => this.postQuery.response());
54
125
 
55
126
  - `GET`/`HEAD`/`OPTIONS` **auto-execute** - immediately when static/argless, or
56
127
  whenever `withArgs` produces new args. Mutations (`POST`/`PUT`/`PATCH`/`DELETE`)
57
- never auto-execute; call `.execute({ args })`. A function route (`pathParams`)
58
- requires `withArgs` (dev-mode error otherwise).
128
+ never auto-execute; declare their args with `withArgs` too and call `.execute()`. A function route (`pathParams`)
129
+ requires `withArgs` (a type error, and a dev-mode error).
130
+ - A route function receives the path params themselves: `(p) => \`/posts/${p.postId}\``.
131
+ - With bearer auth, bind the secure creators the same way:
132
+ `const secureGetQuery = createSecureGetQuery(apiClient, authProviderRef)`.
59
133
  - Queries live in a child injector tied to the creating component; destroyed with it.
60
134
 
61
- ## The query object
135
+ ## Lists with filters, search, sort or paging: `defineQueryForm`
62
136
 
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:
137
+ Never wire list controls to the URL by hand (`injectQueryParams` + `router.navigate` +
138
+ drafts + effects). `defineQueryForm` does URL sync, debounce, defaults and page resets:
65
139
 
66
- - `response()` → `TResponse | null` (kept while re-executing; cleared on a failed re-exec).
67
- - `loading()`, `error()` (normalized `QueryErrorResponse`), `args()`,
68
- `executionState()` (`{ type: 'loading' | 'success' | 'failure', … } | null`, great for `@switch`).
69
- - Methods: `execute({ args?, options? })`, `reset()`, `createSnapshot()`, `asReadonly()`.
70
-
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))`.
140
+ ```ts
141
+ qf = defineQueryForm({
142
+ fields: {
143
+ search: searchQueryField(),
144
+ sort: sortQueryField(),
145
+ page: queryField<number>({ defaultValue: 1, isResetBy: ['search', 'sort'] }),
146
+ },
147
+ }).observe();
148
+
149
+ users = getUsers(
150
+ withArgs(() => {
151
+ const { search, sort, page } = this.qf.value();
152
+
153
+ return { queryParams: { query: search, sortBy: sort?.active, sortOrder: sort?.direction, page } };
154
+ }),
155
+ );
156
+ ```
74
157
 
75
- ## Reactive args & features
158
+ Bind form controls with `[formField]="qf.fields.search"`. `et-pagination` and
159
+ `et-page-size-select` are not form controls: bind their `model` to the field's value signal,
160
+ `[(page)]="qf.fields.page().value"`; an `et-table` sort binds the same way to a
161
+ `tableSortQueryField()`, `[(sort)]="qf.fields.sort().value"`, with no mapping. `qf.value()` is the committed, debounced value. Each
162
+ change pushes a history entry; pass `observe({ replaceUrl: true })` to replace it instead. See {%docsBaseUrl%}/query/query-forms for the other field creators,
163
+ filter overlays and `activeFilterCount`.
76
164
 
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
81
- `CLEAR_QUERY_ARGS` to reset args to `null` (pauses polling/auto-refresh).
82
- - `withPolling({ interval })`, `withAutoRefresh({ onSignalChanges: [...] })`.
83
- - Side-effects: `withSuccessHandling`, `withErrorHandling`, `withLogging`.
165
+ When you move a list that syncs its params by hand, keep the param names the URL already uses, so
166
+ saved links keep working (`queryParamPrefix` when two lists share a route). Keep each old default as
167
+ the field's `defaultValue`. A URL with sort and direction in two params (`?sort=name&dir=asc`)
168
+ keeps two `queryField`s, `dir` with `skipInFilterCount: true`; `sortQueryField()` writes one
169
+ `name:asc` param, so it breaks old links. A date field bound to `et-date-input`,
170
+ `et-date-time-input` or `et-time-input` needs `dateQueryField({ as: 'string' })`.
84
171
 
85
- There is no built-in debounce operator - dedup/caching handles repeated identical
86
- requests; debounce at the input if you need it.
172
+ ## The query object
87
173
 
88
- ## Bridging a query into RxJS / other APIs
174
+ Every state member is an **`ObservableSignal`** - call it, or `.asObservable()` it without an
175
+ injection context of your own (it emits `null` first).
89
176
 
90
- To hand a query's results to something that wants an `Observable<T[]>` (e.g. a
91
- `(query) => Observable<...>` source): drive the query by a search signal and return
92
- its response stream.
177
+ - `response()` → `TResponse | null` (kept while re-executing; cleared on a failed re-exec).
178
+ To keep showing the last good data after a failure, read `cachedResponse` from the
179
+ `loading` or `failure` variant of `executionState()` - do not copy responses into a signal.
180
+ - `loading()`, `error()` (normalized `QueryErrorResponse`), `args()`,
181
+ `executionState()` (`{ type: 'loading' | 'success' | 'failure', … } | null`, great for `@switch`).
182
+ - Methods: `execute({ args?, options? })`, `reset()`, `createSnapshot()`, `asReadonly()`.
93
183
 
94
- ```ts
95
- private search = signal('');
96
- private q = getItems(withArgs(() => ({ queryParams: { q: this.search() } })));
97
-
98
- fetch(query: string) {
99
- this.search.set(query);
100
- return this.q.response.asObservable().pipe(
101
- filter((r): r is ItemsRes => r !== null),
102
- map((r) => r.items),
103
- );
104
- }
105
- ```
184
+ ## Reactive args
185
+
186
+ - **`withArgs(() => ({ pathParams, queryParams, body }))`** runs like a `computed` and
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.
190
+ - **Prefer `withArgs` over `execute({ args })`.** Declared args keep a `GET` re-executing and
191
+ polling/auto-refresh restarting off the same signal; a function route throws without it. A
192
+ mutation with `withArgs` is just `.execute()`. Reserve `execute({ args })` for a one-off
193
+ payload no signal holds.
194
+ - Search-as-you-type: read a debounced value (`searchQueryField()` debounces 300ms; every
195
+ query field takes `debounce`) - a raw input signal sends one request per keystroke.
196
+ - `withLongPolling` and `withPolling` throw when combined.
197
+
198
+ ## Bridging into RxJS or callbacks
199
+
200
+ For a callback that must return one correlated result, run a manual query
201
+ (`{ onlyManualExecution: true }`) through `executeUntilSettled$(query, { args })` - cold, its
202
+ snapshot is frozen to that execution, and unsubscribing aborts the request. Keep the Promise
203
+ `executeUntilSettled` for an `async` signal-forms `submit()`. Never set a signal and return the shared `response`
204
+ stream: the retained previous response can be the first non-null emission. See
205
+ {%docsBaseUrl%}/query/queries#the-query-object.
106
206
 
107
207
  ## Gotchas
108
208
 
109
- - Signals-first: read `query.response()` in templates/computeds; it's **nullable**
110
- (`?? []` / `filter(Boolean)` as needed).
111
- - Don't reach for the legacy client for new code.
209
+ - `query.response()` is **nullable** (`?? []` / `filter(Boolean)` as needed).
112
210
  - `.execute()` defaults `args` to the current `args()` when omitted.
113
211
  - Anything under a query's `subtle` namespace is an unsupported escape hatch - never
114
212
  treat it as public API.
213
+ - Use `withArgs` for mutations too. Never reach for `execute({ args })` plus
214
+ `silenceMissingWithArgsFeatureError` unless the args really exist only at call time - the
215
+ flag is an escape hatch, not the mutation pattern.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: rxjs-signals
3
- description: How to choose between signals and RxJS, and use each correctly - synchronous state vs asynchronous work, unsubscribing, and avoiding RxJS inside effects/computeds. Read when adding reactive state, wiring up an observable, or deciding whether something should be a signal or a stream. Part of the Ethlete styleguide (judgment beyond what lint enforces).
3
+ description: How to choose between signals and RxJS, and use each correctly - synchronous state vs asynchronous work, unsubscribing, and avoiding RxJS inside effects/computeds. Read when adding reactive state, wiring up an observable, or deciding whether something should be a signal or a stream.
4
4
  kind: skill
5
5
  scope: both
6
6
  requires: ['@ethlete/core']
@@ -34,9 +34,18 @@ const data = toSignal(obs$);
34
34
 
35
35
  ## Using RxJS correctly
36
36
 
37
- - **Always unsubscribe.** Prefer `takeUntilDestroyed()` (needs an injection
38
- context); otherwise `take` / `takeUntil` / `takeWhile`, or store and call
39
- `.unsubscribe()`. Place the limiting operator **last** in the pipe.
37
+ - **Tear down long-lived or manual subscriptions.** Finite streams that complete on their
38
+ own need no artificial lifecycle operator. For Angular lifecycle cleanup, prefer
39
+ `takeUntilDestroyed()` (it needs an injection context) or explicitly unsubscribe.
40
+ Do not use `takeWhile` as destruction cleanup: without another emission it stays
41
+ subscribed. Use `take(1)` or `first()` only when one emission is the operation's
42
+ intended semantics.
43
+ - **`takeUntilDestroyed()` goes last in its pipe** (`ethlete/take-until-destroyed-last`), so
44
+ higher-order operators such as `switchMap` and their inner subscriptions are covered.
45
+ Completion-only operators (`finalize`, `defaultIfEmpty`, `toArray`, …) may follow it, and
46
+ chained `.pipe(...).pipe(...)` calls count as one pipe.
47
+ Other limiting and finalization operators do not have a universal “last” position;
48
+ place them where their semantics belong.
40
49
  - **Side effects go in `tap()`**, never in the `subscribe()` callback - keep
41
50
  `subscribe()` empty.
42
51
  - **Don't reach for RxJS inside `effect()`/`computed()`.** Subscribing per run
@@ -56,6 +65,26 @@ toObservable(page)
56
65
  .subscribe();
57
66
  ```
58
67
 
68
+ ## An effect that only writes a signal is a derivation
69
+
70
+ If an `effect()` does nothing but `.set()` a signal, even behind an `if`, the value is derived
71
+ state: use `computed()` when it is read-only, `linkedSignal()` when the user can still
72
+ overwrite it. `ethlete/prefer-linked-signal` flags the plain cases. Keep `effect()` for work
73
+ that leaves the signal graph: DOM, storage, logging, third-party APIs.
74
+
75
+ ```ts
76
+ // ❌
77
+ effect(() => {
78
+ if (this.items().length) this.selected.set(this.items()[0]);
79
+ });
80
+
81
+ // ✅ the previous value stays when the list is empty
82
+ selected = linkedSignal<Item[], Item | null>({
83
+ source: this.items,
84
+ computation: (items, previous) => items[0] ?? previous?.value ?? null,
85
+ });
86
+ ```
87
+
59
88
  ## Prefer `@ethlete/core` helpers
60
89
 
61
90
  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:
@@ -28,23 +35,51 @@ Page URLs follow `{%docsBaseUrl%}/<lib>/<topic>`. The library sections:
28
35
  | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
29
36
  | `/components/` | The active UI library - one guide per domain (see the list below) |
30
37
  | `/core/` | Framework primitives: `theming`, `overlay-runtime`, `signal-utils`, `element-signals`, `animations`, `scrolling`, `drag-resize`, `directives-pipes`, `providers`, `seo`, `utilities` |
31
- | `/query/` | Data fetching - see the dedicated {%skill:query%} guide first |
38
+ | `/query/` | Data fetching; load the dedicated query guide first when this package emitted one |
32
39
  | `/cdk/` | The predecessor UI toolkit, maintenance mode. Only for code that still uses it |
33
40
  | `/contentful/`, `/cli/`, `/eslint/`, `/types/` | The remaining packages |
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` `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
+ `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
- `{%docsBaseUrl%}/components/menu`, and so on. When a name isn't in that list, start at
47
- `{%docsBaseUrl%}/components/` and follow the sidebar rather than guessing a URL.
74
+ `{%docsBaseUrl%}/components/menu`, and so on.
75
+
76
+ That list is a snapshot. The site itself is machine-readable, so fetch rather than guess:
77
+
78
+ - `{%docsBaseUrl%}/llms.txt` - every page's title and path as of the last deploy. Use it
79
+ when a name isn't in the list above, or to check the list hasn't drifted.
80
+ - Append `.md` to any page URL - `{%docsBaseUrl%}/components/button.md` - to get raw
81
+ markdown instead of the rendered page. Prefer this over `/llms-full.txt`, which is the
82
+ entire site in one ~1 MB file.
48
83
 
49
84
  ## How to use them
50
85
 
@@ -59,13 +94,14 @@ So the table guide is `{%docsBaseUrl%}/components/table`, the menu guide
59
94
  `-next` prereleases should read `{%docsBaseUrl%}` and `{%sdkStorybookUrl%}` only if
60
95
  they are the matching prerelease deployments, otherwise expect drift and verify
61
96
  against the installed `.d.ts` in `node_modules/@ethlete/<lib>`.
62
- - **`node_modules` is the tiebreaker.** If the docs and the installed package disagree,
63
- the installed type definitions win - report the drift rather than working around it.
97
+ - **Use the SDK skills instead of searching `node_modules` ad hoc.** The installed `.d.ts`
98
+ files are authoritative for the public type surface this consumer can compile against.
99
+ Source matching that installed build is authoritative for runtime implementation details.
100
+ A dirty or ahead `next` checkout is not automatically source for the installed package.
101
+ If docs and installed types disagree, report the drift.
64
102
  - **Never treat a `subtle` namespace as public API.** Anything exposed under `subtle` is
65
103
  an unsupported escape hatch that can change without a major version.
66
104
 
67
105
  ## Related
68
106
 
69
- - Data fetching has its own guide: {%skill:query%}
70
- - Theming tokens and how to register themes: {%skill:theming%}
71
107
  - When the docs cannot answer it, read the SDK source: {%skill:sdk-source%}