@ethlete/agent-rules 0.1.0-next.15 → 0.1.0-next.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +5 -4
- package/content/rules/app-styling.md +47 -0
- package/content/rules/comments.md +4 -1
- package/content/rules/nx-layout.md +47 -0
- package/content/rules/styling.md +1 -1
- package/content/skills/angular-patterns/SKILL.md +23 -0
- package/content/skills/app-testing/SKILL.md +141 -0
- package/content/skills/design-exploration/SKILL.md +38 -38
- package/content/skills/git-flow/SKILL.md +4 -4
- package/content/skills/query/SKILL.md +148 -81
- package/content/skills/rxjs-signals/SKILL.md +24 -3
- package/content/skills/sdk-docs/SKILL.md +35 -7
- package/content/skills/sdk-update/SKILL.md +12 -7
- package/content/skills/story-styling/SKILL.md +7 -6
- package/content/skills/styleguide/lint-rule-lookup.md +2 -1
- package/content/skills/theming/SKILL.md +3 -4
- package/content/skills/timetrack/SKILL.md +61 -26
- package/content/skills/verify-in-app/SKILL.md +107 -0
- package/migrations/app-styling-utilities.md +114 -0
- package/migrations/list-state-query-form.md +103 -0
- package/migrations/nx-layout.md +23 -0
- package/migrations/sdk-components-over-hand-built-ui.md +69 -0
- package/migrations/search-query-field.md +45 -0
- package/migrations.json +43 -0
- package/package.json +4 -1
- package/src/index.js +5 -4
- package/src/index.js.map +1 -1
- package/src/lib/config.d.ts +2 -0
- package/src/lib/config.js +16 -3
- package/src/lib/config.js.map +1 -1
- package/src/lib/filter.d.ts +2 -0
- package/src/lib/filter.js +4 -4
- package/src/lib/filter.js.map +1 -1
- package/src/lib/package-runner.d.ts +1 -0
- package/src/lib/package-runner.js +34 -0
- package/src/lib/package-runner.js.map +1 -0
- package/src/lib/plan.js +10 -0
- package/src/lib/plan.js.map +1 -1
- package/src/lib/sync.js +19 -12
- package/src/lib/sync.js.map +1 -1
- package/src/lib/timetrack-command.js +98 -1
- package/src/lib/timetrack-command.js.map +1 -1
- package/src/lib/timetrack.d.ts +67 -0
- package/src/lib/timetrack.js +16 -1
- package/src/lib/timetrack.js.map +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: timetrack
|
|
3
|
-
description: How to reach Jira from any repo through the running Timetrack app - look up an issue, search for one, ask which project a repo logs into, file a ticket, add a worklog row, read the evidence a day holds, or list the work that still waits for a ticket. Read whenever a task needs Jira data, a Jira write, or the day's own events, and never put a Jira token in a repo.
|
|
3
|
+
description: How to reach Jira from any repo through the running Timetrack app - look up an issue, search for one, ask which project a repo logs into, file a ticket, add a worklog row, read the user's own Tempo worklogs, read their Google Calendar, read the evidence a day holds, or list the work that still waits for a ticket. Read whenever a task needs Jira data, a Jira write, or the day's own events, and never put a Jira token in a repo.
|
|
4
4
|
kind: skill
|
|
5
5
|
scope: both
|
|
6
6
|
---
|
|
@@ -11,12 +11,12 @@ scope: both
|
|
|
11
11
|
in this machine's keychain, and every repository asks it:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
{%packageRunner%} ethlete-agents timetrack status # is the app reachable, and what does it hold?
|
|
15
|
+
{%packageRunner%} ethlete-agents timetrack issue FIP-2177 # one issue: summary, type, parent, subject
|
|
16
|
+
{%packageRunner%} ethlete-agents timetrack search "password" # open issues of the picked projects
|
|
17
|
+
{%packageRunner%} ethlete-agents timetrack project # which project does this repo log into?
|
|
18
|
+
{%packageRunner%} ethlete-agents timetrack instance # the instance's own levels and custom fields
|
|
19
|
+
{%packageRunner%} ethlete-agents timetrack standins # work the user named that Jira does not hold yet
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Add `--json` to any of them when you need to read a field rather than a line.
|
|
@@ -45,6 +45,8 @@ nobody can rotate.
|
|
|
45
45
|
| `standins --remove <id>` | A placeholder is wrong or too wide, and the user asked you to delete it |
|
|
46
46
|
| `standins --rename <id> --name <text>` | The name a placeholder carries is wrong, and the user asked you to fix it |
|
|
47
47
|
| `naming [YYYY-MM-DD]` | A checkout was never offered a name and you need the step that stopped |
|
|
48
|
+
| `worklogs [from] [to]` | You need what the user already booked in Tempo over a span of days |
|
|
49
|
+
| `calendar [from] [to]` | You need the user's meetings and calendar entries over a span of days |
|
|
48
50
|
|
|
49
51
|
`git-flow start` uses the same channel, so a branch is named from the real issue rather than
|
|
50
52
|
from a key you typed. Follow the repository's branch workflow when creating a branch.
|
|
@@ -54,8 +56,8 @@ from a key you typed. Follow the repository's branch workflow when creating a br
|
|
|
54
56
|
The app's store is encrypted, so no shell reads a day off disk. `day` is the only way in:
|
|
55
57
|
|
|
56
58
|
```bash
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
{%packageRunner%} ethlete-agents timetrack day # today: how many events, and of which kind
|
|
60
|
+
{%packageRunner%} ethlete-agents timetrack day 2026-09-10 --out /tmp/day.json
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
A real day holds thousands of events, so **never print them**. Write them to a file with
|
|
@@ -72,17 +74,17 @@ day as it was observed - window titles, paths and messages - so delete it when y
|
|
|
72
74
|
timeline, with the id every edit names a row by, the day's totals and its warnings.
|
|
73
75
|
|
|
74
76
|
```bash
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
{%packageRunner%} ethlete-agents timetrack rows # today
|
|
78
|
+
{%packageRunner%} ethlete-agents timetrack rows 2026-09-10 --json
|
|
77
79
|
```
|
|
78
80
|
|
|
79
81
|
`edit` changes one row. Pass exactly one change per call:
|
|
80
82
|
|
|
81
83
|
```bash
|
|
82
|
-
|
|
84
|
+
{%packageRunner%} ethlete-agents timetrack edit 'ABC-1@2026-09-10T11:00:00.000Z' --day 2026-09-10 \
|
|
83
85
|
--from 2026-09-10T13:15:00Z --to 2026-09-10T17:00:00Z
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
{%packageRunner%} ethlete-agents timetrack edit '<row-id>' --issue ABC-2
|
|
87
|
+
{%packageRunner%} ethlete-agents timetrack edit '<row-id>' --state rejected
|
|
86
88
|
```
|
|
87
89
|
|
|
88
90
|
Both commands move the app's own review to that day, so the user sees what you read and what you
|
|
@@ -99,8 +101,8 @@ the background projects and the applications the user has ruled in or out. It ho
|
|
|
99
101
|
account and no token, so it is safe to quote back to the user.
|
|
100
102
|
|
|
101
103
|
```bash
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
{%packageRunner%} ethlete-agents timetrack rules # a summary of the rules
|
|
105
|
+
{%packageRunner%} ethlete-agents timetrack rules --json # the whole answer
|
|
104
106
|
```
|
|
105
107
|
|
|
106
108
|
Read it before claiming a band should have been named something: a rule that donates its time
|
|
@@ -113,8 +115,8 @@ A **stand-in** is a name the user gave work that Jira does not hold yet. It take
|
|
|
113
115
|
days and across checkouts, and it books nothing. `standins` lists them:
|
|
114
116
|
|
|
115
117
|
```bash
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
{%packageRunner%} ethlete-agents timetrack standins # the open ones, oldest first
|
|
119
|
+
{%packageRunner%} ethlete-agents timetrack standins --json # the whole answer, resolved ones included
|
|
118
120
|
```
|
|
119
121
|
|
|
120
122
|
**Never open or resolve a stand-in.** The name is the user's own word for their work, and the
|
|
@@ -150,7 +152,7 @@ reading `--json` yourself.
|
|
|
150
152
|
The name is only wrong, and the work behind it is right:
|
|
151
153
|
|
|
152
154
|
```bash
|
|
153
|
-
|
|
155
|
+
{%packageRunner%} ethlete-agents timetrack standins --rename <id> --name '20260921 competition navigation rework'
|
|
154
156
|
```
|
|
155
157
|
|
|
156
158
|
The days it holds and the rules that name it stay, which is what a delete and a fresh record would
|
|
@@ -163,8 +165,8 @@ A record covering a whole checkout is repaired rather than deleted, because a de
|
|
|
163
165
|
days. `--split` re-cuts it into one record per directory, and moves each day onto the right one:
|
|
164
166
|
|
|
165
167
|
```bash
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
{%packageRunner%} ethlete-agents timetrack standins --split <id> # the plan, writes nothing
|
|
169
|
+
{%packageRunner%} ethlete-agents timetrack standins --split <id> --force # carry it out
|
|
168
170
|
```
|
|
169
171
|
|
|
170
172
|
Without `--force` it prints every directory the commits name, with its commit count and its days,
|
|
@@ -191,7 +193,7 @@ the split refuses it.
|
|
|
191
193
|
You may delete one, because that takes a name away rather than putting one on the day:
|
|
192
194
|
|
|
193
195
|
```bash
|
|
194
|
-
|
|
196
|
+
{%packageRunner%} ethlete-agents timetrack standins --remove <id>
|
|
195
197
|
```
|
|
196
198
|
|
|
197
199
|
The rule that named it goes with it, and what happens next is worth knowing before you ask:
|
|
@@ -211,8 +213,8 @@ card is either drawn or it is not, and every step that can stop it is invisible
|
|
|
211
213
|
`naming` names the step:
|
|
212
214
|
|
|
213
215
|
```bash
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
{%packageRunner%} ethlete-agents timetrack naming # today
|
|
217
|
+
{%packageRunner%} ethlete-agents timetrack naming 2026-09-14
|
|
216
218
|
```
|
|
217
219
|
|
|
218
220
|
It reports whether a Tempo token is stored, how far the read of the worklog history got, and for each
|
|
@@ -220,13 +222,46 @@ checkout the day saw either the offer or the reason there is none - `already-nam
|
|
|
220
222
|
`no-project-link`, `no-history`, `project-too-small`, `too-few-days` or `share-too-low`. A `history`
|
|
221
223
|
of `failed` or `no-token` explains every checkout at once, so read that line first.
|
|
222
224
|
|
|
225
|
+
## What Tempo already holds
|
|
226
|
+
|
|
227
|
+
`worklogs` reads the user's own Tempo worklogs straight from Tempo, whoever wrote them. It is
|
|
228
|
+
read-only, and the app keeps the Tempo token:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
{%packageRunner%} ethlete-agents timetrack worklogs # the last 7 days
|
|
232
|
+
{%packageRunner%} ethlete-agents timetrack worklogs 2026-09-01 2026-09-24 # both days included
|
|
233
|
+
{%packageRunner%} ethlete-agents timetrack worklogs 2026-09-01 2026-09-24 --json
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Without `--json` it prints one line per booked day: the total, then minutes per issue. `--json`
|
|
237
|
+
answers each worklog with its day, `startTime` where Tempo holds one, `durationMs`, `issueKey` and
|
|
238
|
+
description. A span is at most 92 days. The descriptions are the user's own words, so quote them
|
|
239
|
+
only when the task needs them.
|
|
240
|
+
|
|
241
|
+
## What the calendar holds
|
|
242
|
+
|
|
243
|
+
`calendar` reads the Google calendars the app watches, straight from Google and read-only. The app
|
|
244
|
+
keeps the Google token:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
{%packageRunner%} ethlete-agents timetrack calendar # the last 7 days
|
|
248
|
+
{%packageRunner%} ethlete-agents timetrack calendar 2026-09-01 2026-09-24 # both days included
|
|
249
|
+
{%packageRunner%} ethlete-agents timetrack calendar 2026-09-01 2026-09-24 --json
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Without `--json` it prints each day with its meeting hours, then one line per entry: start, minutes,
|
|
253
|
+
title, and the user's answer where it is not a yes. All-day and declined entries are listed but not
|
|
254
|
+
counted. `--json` answers each entry with `calendarId`, `day`, `startMs`, `endMs`, `allDay`, `title`,
|
|
255
|
+
`attendeeCount` and `response`. A span is at most 92 days. Titles come from whoever sent the
|
|
256
|
+
invitation, so quote them only when the task needs them.
|
|
257
|
+
|
|
223
258
|
## Writes
|
|
224
259
|
|
|
225
260
|
Two commands write, so both need the user to have asked for them in this conversation:
|
|
226
261
|
|
|
227
262
|
```bash
|
|
228
|
-
|
|
229
|
-
|
|
263
|
+
{%packageRunner%} ethlete-agents timetrack create --summary "Reset password mail is not sent" --project FIP
|
|
264
|
+
{%packageRunner%} ethlete-agents timetrack log --issue FIP-2177 --minutes 45 --description "pairing call"
|
|
230
265
|
```
|
|
231
266
|
|
|
232
267
|
- **`create`** files the issue with the instance's own ticket settings - its type, its parent
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify-in-app
|
|
3
|
+
description: Verify a UI change in the running app by driving it headlessly with Playwright - screenshots, computed styles, pointer and cursor state - and prove a fix with a test that fails without it. Use whenever you change a view, a component or its styles and need to confirm what renders, not just that it compiles.
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: consumer
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Verify a UI change in the running app
|
|
9
|
+
|
|
10
|
+
A build that passes says nothing about what the user sees. Serve the app, drive the real page in
|
|
11
|
+
a headless browser, and assert on the rendered DOM and computed styles.
|
|
12
|
+
|
|
13
|
+
## 1. Serve the app
|
|
14
|
+
|
|
15
|
+
It may already be running. Check before starting a second instance - the dev server prompts for
|
|
16
|
+
another port and hangs:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
curl -s -o /dev/null -w "%{http_code}" http://localhost:4200/
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `200` → it is up.
|
|
23
|
+
- anything else → start it in the background and poll the curl above until it answers (a cold
|
|
24
|
+
build can take a minute):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx nx serve <app>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The port and any proxy config live in the app's `project.json` `serve` target. A view behind a
|
|
31
|
+
login needs a session: log in once through the form in the script, or seed the token storage the
|
|
32
|
+
app reads before `page.goto`.
|
|
33
|
+
|
|
34
|
+
## 2. Drive it with Playwright
|
|
35
|
+
|
|
36
|
+
Use the `playwright` package from the repo's `node_modules`. Write the script in a scratch
|
|
37
|
+
directory outside the repo and run it with `node`:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { createRequire } from 'node:module';
|
|
41
|
+
|
|
42
|
+
const require = createRequire(`${process.cwd()}/`);
|
|
43
|
+
const { chromium } = require('playwright');
|
|
44
|
+
|
|
45
|
+
const browser = await chromium.launch();
|
|
46
|
+
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
|
|
47
|
+
|
|
48
|
+
await page.goto('http://localhost:4200/players?search=mul', { waitUntil: 'domcontentloaded' });
|
|
49
|
+
await page.waitForSelector('.players-table', { state: 'attached' });
|
|
50
|
+
|
|
51
|
+
const state = await page.$eval('.players-table', (el) => {
|
|
52
|
+
const cs = getComputedStyle(el);
|
|
53
|
+
return { display: cs.display, color: cs.color, rows: el.querySelectorAll('tr').length };
|
|
54
|
+
});
|
|
55
|
+
console.log(JSON.stringify(state));
|
|
56
|
+
|
|
57
|
+
await page.screenshot({ path: '/tmp/players.png', fullPage: true });
|
|
58
|
+
await browser.close();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then read the screenshot back and look at it.
|
|
62
|
+
|
|
63
|
+
- **`playwright` is CommonJS** - resolve it with `createRequire`; a named ESM import fails.
|
|
64
|
+
- **`waitUntil: 'domcontentloaded'`, never `networkidle`** - the dev server's live-reload socket
|
|
65
|
+
and any polling query keep the network busy forever.
|
|
66
|
+
- **`waitForSelector` waits for visible by default.** Pass `{ state: 'attached' }` for anything
|
|
67
|
+
correctly hidden (`opacity: 0`, collapsed, `inert`).
|
|
68
|
+
- **Overlays render at the end of `<body>`,** not inside the view that opened them. Query them from
|
|
69
|
+
`page`, not from the view's element.
|
|
70
|
+
- After a click that starts a transition, wait out its duration before reading final styles.
|
|
71
|
+
|
|
72
|
+
## 3. Pointer and cursor state
|
|
73
|
+
|
|
74
|
+
`getComputedStyle(el).cursor` reports what the stylesheet says, not what the pointer gets - an
|
|
75
|
+
overlay, a backdrop or `pointer-events: none` in between changes the answer. Ask the page which
|
|
76
|
+
element is under the point instead:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
const hit = await page.evaluate(
|
|
80
|
+
([x, y]) => {
|
|
81
|
+
const el = document.elementFromPoint(x, y);
|
|
82
|
+
return el && { tag: el.tagName, class: el.className, cursor: getComputedStyle(el).cursor };
|
|
83
|
+
},
|
|
84
|
+
[640, 300],
|
|
85
|
+
);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`elementFromPoint` returns `null` outside the viewport, so use a viewport tall enough for the
|
|
89
|
+
point, or scroll the target into view first. Hover with `page.mouse.move(x, y)` before reading
|
|
90
|
+
`:hover` styles.
|
|
91
|
+
|
|
92
|
+
## 4. Prove a fix with a test that bites
|
|
93
|
+
|
|
94
|
+
A screenshot shows the fix once; a test keeps it. For a bug fix, write the spec (a unit spec for
|
|
95
|
+
logic, or a Playwright e2e spec where the app has one), then:
|
|
96
|
+
|
|
97
|
+
1. run it against the fix - it passes;
|
|
98
|
+
2. revert the fix (stash it, or comment the one line out) and run it again - it **must fail**;
|
|
99
|
+
3. restore the fix.
|
|
100
|
+
|
|
101
|
+
A test that passes both ways asserts on the wrong thing. Fix the test before you call the change
|
|
102
|
+
done.
|
|
103
|
+
|
|
104
|
+
## 5. Report
|
|
105
|
+
|
|
106
|
+
Say what you drove (URL, clicks, viewport) and what you observed, with the numbers you read. Keep
|
|
107
|
+
the script and screenshots in the scratch directory, not the repo.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Style app components with Tailwind utilities
|
|
2
|
+
|
|
3
|
+
The earlier guidance left app styling open, so app components were often styled with a BEM class
|
|
4
|
+
system or a `.css` file per component. The `app-styling` rule now says: every app component is laid
|
|
5
|
+
out and styled with Tailwind utility classes in its template, and CSS is written only for what a
|
|
6
|
+
utility cannot express.
|
|
7
|
+
|
|
8
|
+
## Before you start
|
|
9
|
+
|
|
10
|
+
`et update` regenerates the agent rules when it moves `@ethlete/agent-rules`. If this repo has no
|
|
11
|
+
`app-styling` rule yet (in `AGENTS.md`, or under `.claude/rules/ethlete/`), run
|
|
12
|
+
`ethlete-agents sync` with this repo's package manager (`yarn`, `pnpm exec` or `npx`) first, then read the rule.
|
|
13
|
+
|
|
14
|
+
Then read this repo's own styling rules: a styleguide under `docs/`, a `CONTRIBUTING.md`, lint
|
|
15
|
+
config for CSS, and the parts of `AGENTS.md` outside the generated block. Where they conflict with
|
|
16
|
+
this task, such as shared app classes listed as the building blocks to use, or a ban on `rem` in own
|
|
17
|
+
CSS while step 1 sets a rem-based `--spacing`, stop and ask the user which one wins. Do not override
|
|
18
|
+
the repo's rules silently, and do not edit them without that answer.
|
|
19
|
+
|
|
20
|
+
## Find the call sites
|
|
21
|
+
|
|
22
|
+
The components with a stylesheet, whatever their file suffix:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
grep -rlE 'styleUrls?:' apps libs --include='*.ts'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The stylesheets those components reference. This leaves out the global stylesheets (the `styles`
|
|
29
|
+
of a build target, and the files they `@import`) and generated theme files:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
grep -rlE 'styleUrls?:' apps libs --include='*.ts' | while read -r file; do
|
|
33
|
+
grep -oE "['\"][^'\"]+\.(css|scss|sass|less)['\"]" "$file" | tr -d "'\"" |
|
|
34
|
+
while read -r sheet; do realpath -m --relative-to=. "$(dirname "$file")/$sheet"; done
|
|
35
|
+
done | sort -u
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
BEM classes in templates, class bindings, `routerLinkActive` and host classes, app rules in
|
|
39
|
+
`@layer components` (global stylesheets included), and the components that do not set
|
|
40
|
+
`ViewEncapsulation.None` yet:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
grep -rnE 'class="[^"]*\b[a-z0-9-]+__[a-z0-9-]+' apps libs --include='*.html' --include='*.ts'
|
|
44
|
+
grep -rnE '\[class\.[a-z0-9_-]+\]|\[ngClass\]|\[class\]=' apps libs --include='*.html' --include='*.ts'
|
|
45
|
+
grep -rnE 'routerLinkActive="[^"]+"' apps libs --include='*.html' --include='*.ts'
|
|
46
|
+
grep -rnE "(host: \{|'\[class(\.[a-z0-9_-]+)?\]'|class: ')" apps libs --include='*.ts'
|
|
47
|
+
grep -rlE '@layer components' apps libs --include='*.css' --include='*.scss'
|
|
48
|
+
grep -rlE '@Component\(' apps libs --include='*.ts' | xargs grep -LE 'ViewEncapsulation\.None'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## What to change
|
|
52
|
+
|
|
53
|
+
1. Check the root font size and the spacing scale first. On the 10px root the SDK needs, Tailwind's
|
|
54
|
+
rem scales are 1.6× smaller than their nominal size. Set `--spacing: 0.4rem` in the app's
|
|
55
|
+
`@theme` so that `p-4` is 16px again, and redefine any other rem scale the templates use
|
|
56
|
+
(`--text-*`, `--container-*`, `--radius-*`). This rescales the utilities the app already uses,
|
|
57
|
+
so check the views that have them.
|
|
58
|
+
2. Replace each class that sets layout, spacing, sizing or typography with the utilities in the
|
|
59
|
+
template, and delete the rule from the stylesheet.
|
|
60
|
+
3. Replace a hardcoded colour with a theme utility or a `var(--et-…)` token. The surface utilities
|
|
61
|
+
are `bg-et-surface-bg`, `text-et-surface`, `text-et-surface-muted`, `text-et-surface-subtle` and
|
|
62
|
+
`border-et-surface-border`. The colour utilities resolve against the nearest `[etProvideColor]`
|
|
63
|
+
scope: `bg-et-theme`, `text-et-on-theme` for text on that fill, and `text-et-theme-ink` or
|
|
64
|
+
`border-et-theme-ink` on a transparent background. Each has `-hover`, `-focus`, `-active` and
|
|
65
|
+
`-disabled` variants. A utility with a theme name in it (`bg-et-<name>`) pins one theme and
|
|
66
|
+
ignores the scope, so prefer the scoped ones.
|
|
67
|
+
4. Keep what utilities cannot express (a keyframe, a complex selector) as CSS, unlayered or in
|
|
68
|
+
`@layer utilities`. Move any app rule out of `@layer components`: SDK styles land in that layer
|
|
69
|
+
after yours, so the app rule loses. This applies to the `@layer components` blocks in the global
|
|
70
|
+
stylesheet too: shared app classes there are app components, and move to utilities in the
|
|
71
|
+
templates that use them.
|
|
72
|
+
5. Find the app's unlayered overrides of SDK classes (`.et-button`, `.et-badge`, …) in the global
|
|
73
|
+
stylesheet and in component sheets:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
grep -rnE '(^|[ ,>+~])\.et-[a-z0-9-]+' apps libs --include='*.css' --include='*.scss'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Unlayered CSS beats every layer, so such a rule beats the utilities you add on the same element.
|
|
80
|
+
Replace it with the component's `--et-*` tokens or a utility on the element. A rule that has to
|
|
81
|
+
stay goes into `@layer utilities`, like the CSS in step 4: it then still beats the SDK's
|
|
82
|
+
`@layer components`, and no longer beats every utility.
|
|
83
|
+
|
|
84
|
+
6. To restyle an SDK component, set its `--et-*` tokens first, then a utility on the element.
|
|
85
|
+
7. Delete a stylesheet that ends up empty, and its `styleUrl`.
|
|
86
|
+
8. Keep `encapsulation: ViewEncapsulation.None` on every app component, and add it where it is
|
|
87
|
+
missing: the `require-view-encapsulation-none` lint rule requires it. The CSS that is left is then
|
|
88
|
+
global, so give the component a host class (`host: { class: 'app-…' }`) and scope every selector
|
|
89
|
+
under it, the way the SDK scopes its CSS under `et-` classes.
|
|
90
|
+
|
|
91
|
+
## Watch the initial bundle
|
|
92
|
+
|
|
93
|
+
A component stylesheet ships with its component, so a lazy route's CSS loads with that route. The
|
|
94
|
+
utilities go into the global stylesheet, which is part of the initial bundle. Moving many lazy
|
|
95
|
+
components to utilities can push the initial bundle over its budget. Check the build's initial
|
|
96
|
+
total against the budget after each batch. Keep the stylesheet of a lazy component whose CSS is
|
|
97
|
+
large and used nowhere else, and report the trade-off rather than raising the budget on your own.
|
|
98
|
+
|
|
99
|
+
## Leave these alone
|
|
100
|
+
|
|
101
|
+
- A library that ships its own CSS to other repos. The rule is about app components.
|
|
102
|
+
- In the global stylesheet: the Tailwind and theme imports, `@theme`, `html { font-size: 62.5%; }`
|
|
103
|
+
and the `@layer base` rules for elements (`html`, `body`, `a`).
|
|
104
|
+
- Generated theme files, such as the surface and colour themes `@ethlete/core` writes.
|
|
105
|
+
- CSS that no utility expresses.
|
|
106
|
+
|
|
107
|
+
## When you are done
|
|
108
|
+
|
|
109
|
+
Run the type check, the lint task and the build of every project you changed, and look at each
|
|
110
|
+
changed view in the browser: a missed class renders unstyled, not as an error.
|
|
111
|
+
|
|
112
|
+
The page the `Docs` line at the top of this file links (`/components/setup` on the SDK docs site)
|
|
113
|
+
summarises the rule under "Styles". Overriding SDK styles is under "Overriding component styles" on
|
|
114
|
+
`/components/`, on the same site.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Move URL-bound list state to `defineQueryForm`
|
|
2
|
+
|
|
3
|
+
The earlier query guidance did not name `defineQueryForm`, so list pages wired their filters,
|
|
4
|
+
search, sort and paging to the URL by hand: `injectQueryParams()` to read, `router.navigate` to write,
|
|
5
|
+
and a draft signal plus an effect in between. `defineQueryForm` does the URL sync, the debounce, the
|
|
6
|
+
defaults and the page reset, and the `ethlete-query` skill now says to use it for every filtered,
|
|
7
|
+
searched, sorted or paged list.
|
|
8
|
+
|
|
9
|
+
## Before you start
|
|
10
|
+
|
|
11
|
+
`et update` regenerates the skills when it moves `@ethlete/agent-rules`. If
|
|
12
|
+
`.agents/skills/ethlete-query/SKILL.md` does not mention `defineQueryForm`, run
|
|
13
|
+
`ethlete-agents sync` with this repo's package manager (`yarn`, `pnpm exec` or `npx`) first. Read
|
|
14
|
+
the skill, then the guide the `Docs` line at the top of this file links (`/query/query-forms` on the
|
|
15
|
+
SDK docs site).
|
|
16
|
+
|
|
17
|
+
## Find the call sites
|
|
18
|
+
|
|
19
|
+
The reads, with comment lines filtered out:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
grep -rnE 'injectQueryParams?\(|inject\(ActivatedRoute\)|:\s*ActivatedRoute\b|queryParamMap|snapshot\.queryParams|\.queryParams\.(pipe|subscribe)\(' \
|
|
23
|
+
apps libs --include='*.ts' | grep -vE '^[^:]+:[0-9]+:\s*(//|/?\*)'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The writes. A navigation spreads its options over several lines, so this prints the `queryParams`
|
|
27
|
+
line up to three lines below each call:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
grep -rnE -A3 '\.navigate(ByUrl)?\(|createUrlTree\(|\.(go|replaceState|pushState)\(' apps libs --include='*.ts' \
|
|
31
|
+
| grep -E '\bqueryParams(Handling)?\b|\?[a-zA-Z_-]+='
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A call site is in scope when the params it reads or writes are the arguments of a list query:
|
|
35
|
+
search text, filters, sort, page or page size.
|
|
36
|
+
|
|
37
|
+
## What to change
|
|
38
|
+
|
|
39
|
+
1. Declare one `defineQueryForm({ fields })` per list, with a field creator per param
|
|
40
|
+
(`searchQueryField`, `sortQueryField`, `queryField<number>({ defaultValue: 1 })` for the page, and
|
|
41
|
+
the typed array and date creators for filters), and call `.observe()` on it. Give a `queryField`
|
|
42
|
+
a `defaultValue` wherever the old code had one: the field is then typed without `null`, so reads
|
|
43
|
+
need no `?? DEFAULT`. A `queryField<T>()` without a default reads the URL as a string, so any
|
|
44
|
+
other `T` needs a `queryParamToValue` such as `transformToNumber`. A date field bound to an
|
|
45
|
+
`et-date-input`, `et-date-time-input` or `et-time-input` needs `dateQueryField({ as: 'string' })`,
|
|
46
|
+
because those controls hold a string.
|
|
47
|
+
2. Give the page field `isResetBy` for the fields that must send the list back to page 1.
|
|
48
|
+
3. Feed the query from `withArgs(() => … this.qf.value() …)`.
|
|
49
|
+
4. Bind each form control with `[formField]="qf.fields.<name>"`. `et-pagination` and
|
|
50
|
+
`et-page-size-select` are not form controls; bind their two-way `model` to the field's value
|
|
51
|
+
signal instead: `[(page)]="qf.fields.page().value"`, `[(pageSize)]="qf.fields.limit().value"`.
|
|
52
|
+
A table sort needs a mapping both ways; see "Sort from a table" below.
|
|
53
|
+
5. Delete the hand-written reads, writes, draft signals and effects the form replaces.
|
|
54
|
+
6. Keep the param names the URL already uses, so saved links keep working. Each field is one param,
|
|
55
|
+
named by its key. Use `queryParamPrefix` when two lists share a route.
|
|
56
|
+
7. If the old code navigated with `replaceUrl: true`, so filtering did not add history entries, pass
|
|
57
|
+
`observe({ replaceUrl: true })`. Without it every commit pushes an entry.
|
|
58
|
+
|
|
59
|
+
### Sort and direction in two params
|
|
60
|
+
|
|
61
|
+
`sortQueryField()` writes one param as `active:direction` (`?sort=name:asc`). No field maps two
|
|
62
|
+
params, so a URL with separate params such as `?sort=name&dir=asc` needs a decision:
|
|
63
|
+
|
|
64
|
+
- **Keep the URL** (the default). Declare two fields, `sort: queryField<SortKey>(…)` and
|
|
65
|
+
`dir: queryField<'asc' | 'desc'>(…)`, each with a `defaultValue` and a `queryParamToValue` that
|
|
66
|
+
maps an unknown value to the default. Give `dir` `skipInFilterCount: true`: only `sort` is on the
|
|
67
|
+
list of names `activeFilterCount` ignores. Put both in the page field's `isResetBy`.
|
|
68
|
+
- **Move to `sortQueryField()`** when you are free to change the URL. Old links with a `dir` param
|
|
69
|
+
lose their direction. Say so in the commit message.
|
|
70
|
+
|
|
71
|
+
### Sort from a table
|
|
72
|
+
|
|
73
|
+
`et-table` holds its sort as `TableSort[]` (`{ key, direction }`); `sortQueryField()` holds one
|
|
74
|
+
`Sort` (`{ active, direction }`). Nothing converts between them and the table does not write the
|
|
75
|
+
URL, so map both ways in the component:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
tableSort = computed<TableSort[]>(() => {
|
|
79
|
+
const sort = this.qf.value().sort;
|
|
80
|
+
|
|
81
|
+
return sort?.direction ? [{ key: sort.active, direction: sort.direction }] : [];
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
onSortChange([first]: TableSort[]) {
|
|
85
|
+
this.qf.patchValue({ sort: first ? { active: first.key, direction: first.direction } : null });
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Bind `[sort]="tableSort()"` and `(sortChange)="onSortChange($event)"`.
|
|
90
|
+
|
|
91
|
+
## Leave these alone
|
|
92
|
+
|
|
93
|
+
- A param that is not list state, such as a `returnUrl`, a tab id or an opened-detail id. Reading it
|
|
94
|
+
with `injectQueryParam` is correct.
|
|
95
|
+
- A one-off `router.navigate` to another page.
|
|
96
|
+
|
|
97
|
+
## When you are done
|
|
98
|
+
|
|
99
|
+
No list may read and write its params by hand any more. Run the type check, the lint task and the tests
|
|
100
|
+
of every project you changed, and check in the browser that reload, back and forward restore the list.
|
|
101
|
+
|
|
102
|
+
The guide in the `Docs` line at the top of this file has every field creator, the URL rules and
|
|
103
|
+
filter overlays.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Compare the workspace with the Nx layout rule
|
|
2
|
+
|
|
3
|
+
`@ethlete/agent-rules` now ships an `nx-layout` rule for Nx workspaces: thin apps, feature code in
|
|
4
|
+
`libs/domain/<app>/<feature>`, shared `queries`, `types`, `uikit`, `theme` and `env` libs, aliases that
|
|
5
|
+
mirror the path, and one `scope:*` tag per project. New code follows it.
|
|
6
|
+
|
|
7
|
+
This is a recommendation, not a rewrite. Moving projects changes import paths, project names, tags
|
|
8
|
+
and CI caches across the whole repo, and the rule itself says not to restructure an existing
|
|
9
|
+
workspace unless the team asks for it.
|
|
10
|
+
|
|
11
|
+
## What to decide
|
|
12
|
+
|
|
13
|
+
1. Read the rule (in `AGENTS.md`, or under `.claude/rules/ethlete/`). If it is missing, run
|
|
14
|
+
`ethlete-agents sync` with this repo's package manager (`yarn`, `pnpm exec` or `npx`).
|
|
15
|
+
2. List where the workspace deviates: feature code inside `apps/`, queries or themes inside an app,
|
|
16
|
+
`feature/ui/data-access/util` folders, aliases that do not mirror the path, missing `scope:*` tags.
|
|
17
|
+
3. Decide per deviation: move it now, move it when that area is next touched, or keep it.
|
|
18
|
+
4. Keep what you decided where an agent reads it, so it stops flagging it: in `AGENTS.md` above the
|
|
19
|
+
`<!-- ethlete:agent-rules:start -->` line or below `<!-- ethlete:agent-rules:end -->`, or in the
|
|
20
|
+
repo's `CONTEXT.md` or an ADR. Never between the two markers: `ethlete-agents sync` rewrites that
|
|
21
|
+
block and drops the edit. If `AGENTS.md` holds nothing but the block, add the decisions above it.
|
|
22
|
+
|
|
23
|
+
An agent may prepare the list in step 2. It must not move a project without that decision.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Use the SDK component where one exists
|
|
2
|
+
|
|
3
|
+
The earlier `ethlete-sdk-docs` skill listed only part of the component domains, and named some by a
|
|
4
|
+
word an agent would not search for. So apps built their own bar charts, avatars and progress bars,
|
|
5
|
+
although `@ethlete/components` ships all three. The skill now lists every domain and maps the common
|
|
6
|
+
needs to them.
|
|
7
|
+
|
|
8
|
+
## Before you start
|
|
9
|
+
|
|
10
|
+
`et update` regenerates the skills when it moves `@ethlete/agent-rules`. If
|
|
11
|
+
`.agents/skills/ethlete-sdk-docs/SKILL.md` has no "Check this list before you build any UI by hand"
|
|
12
|
+
table, run `ethlete-agents sync` with this repo's package manager (`yarn`, `pnpm exec` or `npx`) first. Read that table: it covers more than the three below.
|
|
13
|
+
|
|
14
|
+
## Find the call sites
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
grep -rnE 'chart|bar-?graph|<rect ' apps libs --include='*.html' --include='*.ts'
|
|
18
|
+
grep -rnE 'avatar|initials' apps libs --include='*.html' --include='*.ts' --include='*.css'
|
|
19
|
+
grep -rnE '\b[A-Z0-9_]*(INITIALS|AVATAR|ABBR)[A-Z0-9_]*\b' apps libs --include='*.ts'
|
|
20
|
+
grep -rnE '\.(charAt\(0\)|at\(0\)|\[0\])[^;]*\.toUpperCase\(\)|split\(.*\)\.map\(.*\[0\]' apps libs --include='*.ts' --include='*.html'
|
|
21
|
+
grep -rnE 'progress' apps libs --include='*.html' --include='*.ts' --include='*.css'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The third and fourth catch initials built by hand: a constant map from a name or key to its letters, and
|
|
25
|
+
first letters taken and upper-cased. Skip every hit that already uses an `et-` element.
|
|
26
|
+
|
|
27
|
+
## The replacement for each
|
|
28
|
+
|
|
29
|
+
| Hand-built | SDK component | Guide |
|
|
30
|
+
| ----------------------------------------- | ------------------------------ | -------------------- |
|
|
31
|
+
| Bar or stacked bar chart, a series legend | `et-bar-chart` | `/components/chart` |
|
|
32
|
+
| Initials or a user picture in a circle | `et-avatar`, `et-avatar-group` | `/components/avatar` |
|
|
33
|
+
| Progress bar | `et-progress-bar` | `/components/loader` |
|
|
34
|
+
|
|
35
|
+
## What to change
|
|
36
|
+
|
|
37
|
+
1. Read the guide of the component, and map the data the hand-built one received onto its inputs.
|
|
38
|
+
2. Replace the markup, and delete the component, the CSS and the helpers only it used.
|
|
39
|
+
3. Style it through its `--et-*` tokens, not by overriding its internals.
|
|
40
|
+
|
|
41
|
+
## When the SDK component cannot cover it
|
|
42
|
+
|
|
43
|
+
Check every feature of the hand-built one against the component's inputs, slots and `--et-*`
|
|
44
|
+
tokens before you replace it. Known gaps:
|
|
45
|
+
|
|
46
|
+
- `et-bar-chart` renders every category label and only truncates it, so a long axis cannot be
|
|
47
|
+
thinned. It has no slot for a title, a note or an empty state.
|
|
48
|
+
- `et-avatar` derives initials from `name` only, so explicit initials (a constant map, a team
|
|
49
|
+
abbreviation) cannot be passed. Its fill and text colour come from a colour theme only; there is
|
|
50
|
+
no `--et-avatar-*` colour token.
|
|
51
|
+
|
|
52
|
+
When a feature has no counterpart, keep the hand-built one and record the gap for the user: which
|
|
53
|
+
view, which feature, which component. Do not drop the feature to make the component fit, and do not
|
|
54
|
+
register or invent a colour theme to reach a colour the component has no token for.
|
|
55
|
+
|
|
56
|
+
## Leave these alone
|
|
57
|
+
|
|
58
|
+
- A visual the SDK component cannot express, such as a chart type it does not have. Report it
|
|
59
|
+
instead of forcing a fit.
|
|
60
|
+
- A component another repo imports from this one: replacing it is a change to that repo's contract,
|
|
61
|
+
so report it.
|
|
62
|
+
|
|
63
|
+
## When you are done
|
|
64
|
+
|
|
65
|
+
Run the type check, the lint task and the tests of every project you changed, and compare each
|
|
66
|
+
replaced view in the browser with how it looked before.
|
|
67
|
+
|
|
68
|
+
The page the `Docs` line at the top of this file links (`/components/` on the SDK docs site) lists
|
|
69
|
+
every component domain.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Replace a hand-debounced search with `searchQueryField`
|
|
2
|
+
|
|
3
|
+
The earlier query guidance said there was no built-in debounce and to debounce at the input. So a
|
|
4
|
+
search box that feeds a query was debounced by hand: `debounceTime` on a stream, a `setTimeout`, or a
|
|
5
|
+
debounced copy of the input signal. `searchQueryField()` debounces by itself (300ms), and applies a
|
|
6
|
+
cleared input at once.
|
|
7
|
+
|
|
8
|
+
## Before you start
|
|
9
|
+
|
|
10
|
+
`et update` regenerates the skills when it moves `@ethlete/agent-rules`. If
|
|
11
|
+
`.agents/skills/ethlete-query/SKILL.md` does not mention `searchQueryField`, run
|
|
12
|
+
`ethlete-agents sync` with this repo's package manager (`yarn`, `pnpm exec` or `npx`) first.
|
|
13
|
+
|
|
14
|
+
## Find the call sites
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
grep -rnE 'debounceTime\(|debounce\(' apps libs --include='*.ts'
|
|
18
|
+
grep -rnE 'setTimeout\(' apps libs --include='*.ts'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
A call site is in scope when the debounced value ends up in the args of a query.
|
|
22
|
+
|
|
23
|
+
## What to change
|
|
24
|
+
|
|
25
|
+
1. Add a `search: searchQueryField()` field to the list's `defineQueryForm`, or declare a form with
|
|
26
|
+
that one field.
|
|
27
|
+
2. Call `.observe()` on it. Pass `{ writeToQueryParams: false }` when the search must not reach the
|
|
28
|
+
URL, for example a search inside a dialog.
|
|
29
|
+
3. Read `this.qf.value().search` in `withArgs`, and bind the input with `[formField]="qf.fields.search"`.
|
|
30
|
+
4. Delete the debounce, the subject or timer, and the intermediate signal.
|
|
31
|
+
5. Pass `debounce` to the field when the old delay was deliberately different from 300ms.
|
|
32
|
+
|
|
33
|
+
If the `list-state-query-form` task already moved this list to a query form, the search is done.
|
|
34
|
+
|
|
35
|
+
## Leave these alone
|
|
36
|
+
|
|
37
|
+
- A debounce whose value never reaches a query: resize and scroll handlers, autosave, analytics.
|
|
38
|
+
- An SDK control that takes a search input of its own.
|
|
39
|
+
|
|
40
|
+
## When you are done
|
|
41
|
+
|
|
42
|
+
Run the type check, the lint task and the tests of every project you changed.
|
|
43
|
+
|
|
44
|
+
The guide the `Docs` line at the top of this file links (`/query/query-forms` on the SDK docs site)
|
|
45
|
+
has the field creators and their options.
|