@ethlete/agent-rules 0.1.0-next.0

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 (68) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +106 -0
  3. package/content/defaults.json +10 -0
  4. package/content/rules/comments.md +31 -0
  5. package/content/rules/lint-and-format.md +26 -0
  6. package/content/rules/reactive-state.md +15 -0
  7. package/content/rules/styling.md +35 -0
  8. package/content/skills/angular-patterns/SKILL.md +60 -0
  9. package/content/skills/git-commit/SKILL.md +29 -0
  10. package/content/skills/handoff/SKILL.md +98 -0
  11. package/content/skills/query/SKILL.md +114 -0
  12. package/content/skills/rxjs-signals/SKILL.md +65 -0
  13. package/content/skills/sdk-docs/SKILL.md +70 -0
  14. package/content/skills/story-styling/SKILL.md +83 -0
  15. package/content/skills/styleguide/SKILL.md +66 -0
  16. package/content/skills/styleguide/STYLEGUIDE.md +520 -0
  17. package/content/skills/theming/SKILL.md +110 -0
  18. package/content/skills/verify-in-storybook/SKILL.md +78 -0
  19. package/content/skills/verify-in-storybook/verify-template.mjs +42 -0
  20. package/package.json +15 -0
  21. package/src/index.d.ts +2 -0
  22. package/src/index.js +79 -0
  23. package/src/index.js.map +1 -0
  24. package/src/lib/config.d.ts +21 -0
  25. package/src/lib/config.js +54 -0
  26. package/src/lib/config.js.map +1 -0
  27. package/src/lib/filter.d.ts +16 -0
  28. package/src/lib/filter.js +49 -0
  29. package/src/lib/filter.js.map +1 -0
  30. package/src/lib/frontmatter.d.ts +23 -0
  31. package/src/lib/frontmatter.js +117 -0
  32. package/src/lib/frontmatter.js.map +1 -0
  33. package/src/lib/index.d.ts +9 -0
  34. package/src/lib/index.js +13 -0
  35. package/src/lib/index.js.map +1 -0
  36. package/src/lib/load-content.d.ts +19 -0
  37. package/src/lib/load-content.js +79 -0
  38. package/src/lib/load-content.js.map +1 -0
  39. package/src/lib/owned-paths.d.ts +7 -0
  40. package/src/lib/owned-paths.js +47 -0
  41. package/src/lib/owned-paths.js.map +1 -0
  42. package/src/lib/plan.d.ts +20 -0
  43. package/src/lib/plan.js +57 -0
  44. package/src/lib/plan.js.map +1 -0
  45. package/src/lib/render.d.ts +39 -0
  46. package/src/lib/render.js +73 -0
  47. package/src/lib/render.js.map +1 -0
  48. package/src/lib/sync.d.ts +9 -0
  49. package/src/lib/sync.js +90 -0
  50. package/src/lib/sync.js.map +1 -0
  51. package/src/lib/targets/claude.d.ts +7 -0
  52. package/src/lib/targets/claude.js +46 -0
  53. package/src/lib/targets/claude.js.map +1 -0
  54. package/src/lib/targets/codex.d.ts +11 -0
  55. package/src/lib/targets/codex.js +26 -0
  56. package/src/lib/targets/codex.js.map +1 -0
  57. package/src/lib/targets/copilot.d.ts +11 -0
  58. package/src/lib/targets/copilot.js +43 -0
  59. package/src/lib/targets/copilot.js.map +1 -0
  60. package/src/lib/targets/cursor.d.ts +7 -0
  61. package/src/lib/targets/cursor.js +35 -0
  62. package/src/lib/targets/cursor.js.map +1 -0
  63. package/src/lib/targets/neutral.d.ts +8 -0
  64. package/src/lib/targets/neutral.js +29 -0
  65. package/src/lib/targets/neutral.js.map +1 -0
  66. package/src/lib/targets/shared.d.ts +60 -0
  67. package/src/lib/targets/shared.js +50 -0
  68. package/src/lib/targets/shared.js.map +1 -0
@@ -0,0 +1,520 @@
1
+ # Style Guide v0.21.0
2
+
3
+ This document outlines the coding style guide for Angular applications at Braune Digital.
4
+
5
+ **This guide is a work in progress and will be updated regularly.**
6
+
7
+ > **Enforcement:** most rules below are enforced automatically by
8
+ > `@ethlete/eslint-plugin` - run your lint task with `--fix` and fix what it reports,
9
+ > rather than hand-checking. The `styleguide` guide that ships alongside this file
10
+ > distills the judgment calls lint can't check.
11
+
12
+ ## TL;DR
13
+
14
+ Key standards at a glance. **Most are enforced by lint** (see [Enforced by lint](#enforced-by-lint)); the rest are judgment calls documented in the sections below. Not exhaustive.
15
+
16
+ - **Types**: `unknown` not `any`; `type` not `interface`; `as const` objects not `enum`; `T`-prefixed descriptive generics; regular value imports (no `import type`); narrow with type guards.
17
+ - **Code**: `const` by default (`let` only to reassign, never `var`); one declaration per statement; `===` / `!==`; arrow fns standalone, methods in classes; max two params (object param beyond that).
18
+ - **State**: signals for synchronous state, RxJS for async - always unsubscribe; effects for signal-driven side effects.
19
+ - **Angular**: `ViewEncapsulation.None`; `inject()` not constructor injection; no legacy lifecycle hooks - prefer `constructor` + `afterNextRender` + `DestroyRef.onDestroy`; no function calls in templates except signal reads.
20
+ - **Naming & structure**: name things after what they do; routing components end in `-view`; mirror routes in folders; keep related files together.
21
+ - **Changesets**: one focused, imperative-mood entry per change (see the `changeset` skill).
22
+
23
+ ---
24
+
25
+ ## Enforced by lint
26
+
27
+ Run your lint task with `--fix` - the rules below are enforced (and mostly auto-fixed) by `@ethlete/eslint-plugin`. This table is a lookup for _why_ a fix was applied; don't hand-check these.
28
+
29
+ | Rule | Enforced by |
30
+ | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
31
+ | No `any` / `$any()`; use `unknown` + type guards | `@typescript-eslint/no-explicit-any`, `@angular-eslint/template/no-any` |
32
+ | No property bindings for static strings/booleans (`[etIcon]="'foo'"`, `[isReadonly]="true"`) - use static attributes | `@angular-eslint/template/prefer-static-string-properties`, `ethlete/prefer-static-boolean-properties` |
33
+ | No `interface` - use `type` | `@typescript-eslint/consistent-type-definitions` |
34
+ | No `enum` - use an `as const` object + derived union | `no-restricted-syntax` |
35
+ | No `var`; prefer `const`; one declaration per statement | `no-var`, `prefer-const`, `one-var` |
36
+ | `===` / `!==` only | `eqeqeq` |
37
+ | Max two function parameters | `max-params` |
38
+ | No `import type` / inline `type` specifiers | `ethlete/no-type-only-import` |
39
+ | Generic params `T`-prefixed (`TValue`), never bare `T` | `@typescript-eslint/naming-convention` |
40
+ | No `async`/`await` - use RxJS | `no-restricted-syntax` |
41
+ | Arrow fns standalone; methods in classes; no arrow-fn class props; no `function` keyword | `no-restricted-syntax` |
42
+ | Blank line before `return` in multi-line guard clauses | `ethlete/guard-return-newline` |
43
+ | No trivially-inferable explicit return types | `ethlete/no-trivial-return-type` |
44
+ | camelCase / PascalCase / UPPER*CASE; no `#` or `*` member prefixes | `@typescript-eslint/naming-convention`, `ethlete/no-leading-underscore-class-member` |
45
+ | No SCREAMING_CASE locals; class constants `readonly` + SCREAMING_CASE | `ethlete/no-screaming-case-local`, `ethlete/class-constant-property` |
46
+ | No `readonly` on reactive members (signals, inputs, computed, inject) | `ethlete/no-readonly-signal` |
47
+ | No `static` members (except `ngTemplateContextGuard`, which Angular requires to be static) | `no-restricted-syntax` |
48
+ | Injected providers `private` by default, `protected` only when template/host-visible; explicit accessibility on reachable members | `ethlete/inject-member-accessibility`, `ethlete/template-member-accessibility` |
49
+ | No `inject(X).member` chaining; no pure member aliases | `ethlete/no-inject-chain`, `ethlete/no-member-alias` |
50
+ | No module-scope destructuring of a factory call; `@__PURE__` on module-scope calls in library source | `ethlete/no-impure-top-level-provider` |
51
+ | No redundant `@internal` on `private`/`protected` members | `ethlete/no-redundant-internal` |
52
+ | Observable vars/props end with `$` | `ethlete/require-dollar-suffix` |
53
+ | No body in `subscribe()`; no `subscribe` in `pipe()`; no RxJS in `effect()`/`computed()` | `ethlete/no-subscribe-with-body`, `ethlete/no-subscribe-in-pipe`, `ethlete/no-rxjs-in-effect` |
54
+ | Effect teardown goes through `onCleanup` (or `DestroyRef`), never a returned cleanup function | `ethlete/no-effect-cleanup-return` |
55
+ | `ViewEncapsulation.None` | `ethlete/require-view-encapsulation-none` |
56
+ | No legacy lifecycle hooks; no legacy Angular decorators (`@HostBinding`, `@Input`, …) | `no-restricted-syntax`, `ethlete/no-legacy-angular-decorators` |
57
+ | No `@Injectable` / `@Service`; no route guards; no resolvers | `no-restricted-syntax` |
58
+ | Outputs: no `on` prefix, no native event names, present-tense naming (`playerSelect`, not `playerSelected`) | `@angular-eslint/no-output-on-prefix`, `@angular-eslint/no-output-native`, `ethlete/prefer-present-tense-output` |
59
+ | Inputs/models not named after a global HTML attribute (`title`, `id`, `hidden`, `role`, …) - collides with the host element | `ethlete/no-native-html-input-name` |
60
+ | No logic in pipe `transform` | `ethlete/no-pipe-logic` |
61
+ | Consistent class-member + decorator-metadata order; concise host-directive / style metadata | `ethlete/class-member-order`, `ethlete/angular-decorator-property-order`, `ethlete/prefer-concise-angular-host-directives`, `ethlete/prefer-concise-angular-style-metadata` |
62
+ | No interpolated template literal above an inline `template:` - it kills Angular language service completions in that file | `ethlete/no-template-literal-before-inline-template` |
63
+ | Routing components: `-view` path + `ViewComponent` class name | `ethlete/enforce-routing-view-naming` |
64
+ | No direct `document` / `window` / DOM query / observers / cookies / `window.location` | `no-restricted-globals`, `ethlete/no-direct-dom-manipulation`, `ethlete/no-dom-query`, `ethlete/no-native-observers`, `ethlete/no-document-cookie`, `ethlete/no-window-location` |
65
+ | No barrel (index) imports - import from the source file | `no-restricted-syntax` |
66
+ | Prefer `@ethlete/core` utils over raw APIs (clone/equal, rxjs timers, media query, viewport size, SEO, locale, router state) | `ethlete/prefer-clone-equal`, `ethlete/prefer-rxjs-timer`, `ethlete/prefer-match-media`, `ethlete/prefer-viewport-size`, `ethlete/no-angular-seo-services`, `ethlete/no-locale-id`, `ethlete/no-angular-router-api` |
67
+
68
+ ## Accessibility & visibility
69
+
70
+ Lint auto-fixes injected providers to `private` and flags template/host-visible members, but the _intent_ is yours:
71
+
72
+ - Injected provider β†’ `private` by default; `protected` **only** when referenced from the HTML template or a `host:` binding expression; **drop the modifier entirely** if keeping it `private` would force a member alias (a property whose sole purpose is re-exposing a nested member - expose the injected symbol directly instead).
73
+ - Never add a member that only **aliases** another member's nested property (`foo = this.thing.foo`) - widen the source member's visibility and use it directly.
74
+ - For a member that must stay technically public purely for cross-class/DI use (e.g. a self-registration method called by a sub-directive), keep it `public` and tag `/** @internal */` so build tooling strips it from the published `.d.ts`. Never put `@internal` on `private`/`protected` members.
75
+
76
+ ## Naming & functions with intent
77
+
78
+ Case conventions, the `T` generic prefix, arrow-vs-method, and the two-param limit are all lint-enforced. What lint can't judge:
79
+
80
+ - **Name things after what they do**, not after the mechanism. `onChange` that posts a form β†’ `sendFormValueToApi`. Descriptive generics (`TValue`, `TResult`), descriptive constant/variable names.
81
+ - When a function needs more than two parameters, take a single **object parameter** with a named `type` rather than widening the signature.
82
+
83
+ ```ts
84
+ // ❌ name describes the trigger, not the behaviour
85
+ const onChange = () => sendToApi(myForm.getRawValue());
86
+
87
+ // βœ… name describes the behaviour
88
+ const sendFormValueToApi = () => sendToApi(myForm.getRawValue());
89
+
90
+ // βœ… object param instead of a third positional arg
91
+ type LogMessageConfig = { scope: string; logLevel: string };
92
+ const logMessage = (message: string, config: LogMessageConfig) => {};
93
+ ```
94
+
95
+ ## TypeScript Config
96
+
97
+ - Ensure `strict` is set to `true`.
98
+ - Ensure `noUncheckedIndexedAccess` is set to `true`.
99
+ - Keep the remaining defaults provided by NX
100
+ - Set `resolveJsonModule` to `true` only if necessary. JSON files should be fetched via HTTP requests.
101
+ - Set `esModuleInterop` to `true` only if necessary.
102
+
103
+ ## Signals vs RxJS
104
+
105
+ - **Synchronous state β†’ signals. Asynchronous work β†’ RxJS.** Never model sync state with a `BehaviorSubject`; never use RxJS just to read a value back synchronously. Bridge with `toSignal()` / `toObservable()` instead of copying values across with `.subscribe()`.
106
+ - **Always unsubscribe.** Prefer `takeUntilDestroyed()`; otherwise `take` / `takeUntil` / `takeWhile`, and place the limiting operator **last** in the pipe. Side effects go in `tap()`, not the `subscribe()` callback. (Lint blocks bodies in `subscribe()` but cannot prove you unsubscribe.)
107
+ - Don't reach for RxJS inside `effect()` / `computed()` - model the stream with `toObservable(signal).pipe(switchMap(...))` instead of subscribing per run.
108
+
109
+ ```ts
110
+ // ❌ sync state as a subject // βœ… signal
111
+ const count$ = new BehaviorSubject(0);
112
+ const count = signal(0);
113
+
114
+ // βœ… react to a signal without subscribing inside an effect
115
+ toObservable(page)
116
+ .pipe(
117
+ switchMap((p) => fetchPage(p)),
118
+ tap(handle),
119
+ takeUntilDestroyed(),
120
+ )
121
+ .subscribe();
122
+ ```
123
+
124
+ ## Angular patterns
125
+
126
+ Lint covers the mechanical Angular rules (`ViewEncapsulation.None`, no legacy hooks/decorators, no native DOM/`window`, output naming, class-member + decorator-metadata order, no `@Injectable` / `@Service` / guards / resolvers). The judgment calls:
127
+
128
+ - **No function calls in template value bindings except signal reads** - a method in a binding re-runs every change-detection cycle. Move it into a `computed()` and bind that. Event bindings (`(click)="save()"`) are fine.
129
+ - **Prefer the `constructor`** (runs in the injection context) over `ngOnInit` / `ngOnDestroy`: `afterNextRender()` for first-render work, `inject(DestroyRef).onDestroy(...)` for cleanup.
130
+ - **Prefer utility functions + provider factories over services** - `createProvider` / `createRootProvider` and the `injectX()` helper pattern from `@ethlete/core`, not an `@Injectable` (or its Angular 22 `@Service` shorthand).
131
+ - **Most directives can be plain functions.** Move the logic into a function so it's reusable without applying a directive; keep a directive only when a host element genuinely needs it. Avoid common input/output names that clash with the host component.
132
+ - **Pipes carry no logic** - put it in a utility function called from a `computed()`; most pipes can be replaced by a `computed` outright.
133
+ - **Components**: inline template/styles for small components, external `.html` / `.css` files for complex ones.
134
+
135
+ ```html
136
+ <!-- ❌ runs every CD cycle -->
137
+ <button [disabled]="isDisabled()">
138
+ <!-- βœ… computed signal -->
139
+ <button [disabled]="disabled()"></button>
140
+ </button>
141
+ ```
142
+
143
+ ## General File Structure
144
+
145
+ Given are the following routes:
146
+
147
+ ```ts
148
+ import { Routes } from '@angular/router';
149
+
150
+ export const SHOP_ROUTES: Routes = [
151
+ {
152
+ path: 'items',
153
+ loadComponent: () => import('./items-list-view/items-list-view.component').then((m) => m.ItemsListViewComponent),
154
+ },
155
+ {
156
+ path: 'items/:id',
157
+ loadComponent: () => import('./item-detail-host-view/item-detail-host-view.component').then((m) => m.ItemDetailHostViewComponent),
158
+ children: [
159
+ {
160
+ path: '',
161
+ loadComponent: () => import('./item-detail-host-view/item-detail-view/item-detail-view.component').then((m) => m.ItemDetailViewComponent),
162
+ }
163
+ {
164
+ path: 'reviews',
165
+ loadComponent: () => import('./item-detail-host-view/item-reviews-view/item-reviews-view.component').then((m) => m.ItemReviewsViewComponent),
166
+ }
167
+ ]
168
+ }
169
+ ];
170
+ ```
171
+
172
+ - Organize folder and file structure to mirror Angular routes.
173
+ - Name routing components with the suffix `view` (e.g., `settings-view.component.ts`).
174
+ - Place reusable components in a `components` directory.
175
+ - Always include an `index.ts` file to export components.
176
+ - For components with inline templates, place them directly in the `components` directory without nesting.
177
+ - Place child components specific to a parent component in a `partials` directory.
178
+ - Restrict the usage of partial components to their parent component only. For example, the `item-image` and `item-price` components should only be used within the `item-card` component.
179
+ - Similarly, components like `item-card` should only be used within their parent view (`items-list-view`). If needed elsewhere, move them higher in the directory structure.
180
+ - Create and place directives, pipes, and utilities in the same directory as the component that uses them. For example, place `item-status.pipe.ts` in the `item-card` directory.
181
+ - Position services and providers in the directory of the view component that includes them in its `providers` array. For example, place `item-detail-api.service.ts` in the `item-detail-host-view` directory.
182
+ - Use simple file names for utility files without the `.utils.ts` suffix. For example, use `items-list-filter-form.ts` for files containing form configurations.
183
+ - Always put storybook files in a `storybook` directory within the component directory. This directory should contain the storybook component and any dummy data needed for the storybook.
184
+
185
+ ```plaintext
186
+ shop/
187
+ β”œβ”€β”€ items-list-view/
188
+ β”‚ β”œβ”€β”€ components/
189
+ β”‚ β”‚ β”œβ”€β”€ item-card/
190
+ β”‚ β”‚ β”‚ β”œβ”€β”€ partials/
191
+ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ item-image/
192
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ item-image.component.ts
193
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ item-image.component.html
194
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ index.ts βœ… (exports the item image component)
195
+ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ item-price/
196
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ item-price.component.ts
197
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ item-price.component.html
198
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ index.ts βœ… (exports the item price component)
199
+ β”‚ β”‚ β”‚ β”œβ”€β”€ storybook/
200
+ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ item-card.component.stories.ts
201
+ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ item-card-storybook-data.ts
202
+ β”‚ β”‚ β”‚ β”œβ”€β”€ item-card.component.ts
203
+ β”‚ β”‚ β”‚ β”œβ”€β”€ item-card.component.html
204
+ β”‚ β”‚ β”‚ β”œβ”€β”€ item-status.pipe.ts
205
+ β”‚ β”‚ β”‚ β”œβ”€β”€ index.ts βœ… (exports the item card component)
206
+ β”‚ β”œβ”€β”€ items-list-view.component.ts
207
+ β”‚ β”œβ”€β”€ items-list-view.component.html
208
+ β”‚ β”œβ”€β”€ items-list-filter-form.ts
209
+ β”œβ”€β”€ items-detail-host-view/
210
+ β”‚ β”œβ”€β”€ item-detail-host-view.component.ts
211
+ β”‚ β”œβ”€β”€ item-detail-host-view.component.html
212
+ β”‚ β”œβ”€β”€ item-detail-api.service.ts
213
+ β”‚ β”œβ”€β”€ item-data.provider.ts
214
+ β”‚ β”œβ”€β”€ item-detail-view/
215
+ β”‚ β”‚ β”œβ”€β”€ item-detail-view.component.ts
216
+ β”‚ β”‚ β”œβ”€β”€ item-detail-view.component.html
217
+ β”‚ β”œβ”€β”€ item-reviews-view/
218
+ β”‚ β”‚ β”œβ”€β”€ item-reviews-view.component.ts
219
+ β”‚ β”‚ β”œβ”€β”€ item-reviews-view.component.html
220
+ ```
221
+
222
+ #### Miscellaneous
223
+
224
+ - Super generic components and other logic (e.g., buttons, inputs, etc.) should be placed in a uikit directory.
225
+ - Things placed in the uikit directory should be as generic as possible and should not contain any business logic (dumb components).
226
+ - Components and logic needed for the app shell (e.g., header, footer, etc.) should be placed in a shell directory.
227
+ - To reduce the risk of circular dependencies, avoid importing from the parent directory in a subdirectory.
228
+
229
+ ### NX Workspace
230
+
231
+ - Apps should be placed in the `apps` directory. They should contain the main application logic and should be as slim as possible. No business logic should be placed in the app directory besides the app component.
232
+ - Libraries should be placed in the `libs` directory.
233
+ - They should be `buildable`.
234
+ - They should have a clear import path (e.g., `@org/domain/my-app` or `@org/uikit`). The import path can be found in the project.json file and should be checked after generation.
235
+ - They should have a clear name (e.g. `domain-my-app` or `uikit`). The name can be found in the project.json file and should also be checked after generation.
236
+
237
+ The following abstract example shows a correct file structure:
238
+
239
+ ```plaintext
240
+ apps/
241
+ β”‚ β”œβ”€β”€ my-app/
242
+ β”‚ β”‚ β”œβ”€β”€ src/...
243
+ β”‚ β”œβ”€β”€ other-app/
244
+ β”‚ β”‚ β”œβ”€β”€ src/...
245
+ libs/
246
+ β”‚ β”œβ”€β”€ assets/
247
+ β”‚ β”‚ β”œβ”€β”€ src/...
248
+ β”‚ β”œβ”€β”€ domain/
249
+ β”‚ β”‚ β”œβ”€β”€ my-app/
250
+ β”‚ β”‚ β”‚ β”œβ”€β”€ src/...
251
+ β”‚ β”‚ β”œβ”€β”€ other-app/
252
+ β”‚ β”‚ β”‚ β”œβ”€β”€ src/...
253
+ β”‚ β”œβ”€β”€ env/
254
+ | β”‚ β”œβ”€β”€ src/...
255
+ β”‚ β”œβ”€β”€ queries/
256
+ β”‚ β”‚ β”œβ”€β”€ src/...
257
+ β”‚ β”œβ”€β”€ types/
258
+ β”‚ β”‚ β”œβ”€β”€ src/...
259
+ β”‚ β”œβ”€β”€ uikit/
260
+ β”‚ β”‚ β”œβ”€β”€ src/...
261
+ ```
262
+
263
+ - The `assets` library should contain all assets used across applications.
264
+ - The `domain` library should contain all domain-specific logic. Each domain should have its own library.
265
+ - The `env` library should contain the `environment` files. This way they can be shared across applications and libraries.
266
+ - The `queries` library should contain all queries used across applications.
267
+ - The `types` library should contain common types used across applications and libraries (e.g. API types).
268
+ - The `uikit` library should contain all shared components and logic.
269
+
270
+ ### Assets
271
+
272
+ - Place all assets in the `assets` library.
273
+ - Given the apps `my-app` and `other-app`, the assets library should be structured as follows:
274
+
275
+ ```plaintext
276
+ assets/
277
+ β”‚ β”œβ”€β”€ my-app/
278
+ β”‚ β”‚ β”œβ”€β”€ build/
279
+ β”‚ β”‚ β”œβ”€β”€ serve/
280
+ β”‚ β”‚ β”œβ”€β”€ storybook/
281
+ β”‚ β”œβ”€β”€ other-app/
282
+ β”‚ β”‚ β”œβ”€β”€ build/
283
+ β”‚ β”‚ β”œβ”€β”€ serve/
284
+ β”‚ β”‚ β”œβ”€β”€ storybook/
285
+ β”‚ β”œβ”€β”€ shared/
286
+ β”‚ β”‚ β”œβ”€β”€ build/
287
+ β”‚ β”‚ β”œβ”€β”€ serve/
288
+ β”‚ β”‚ β”œβ”€β”€ storybook/
289
+ ```
290
+
291
+ - The storybook directory is optional and can be used for assets that are only needed in Storybook (e.g., dummy data, storybook-specific assets).
292
+ - The serve directory is optional and can be used for assets that are only needed during development (e.g., placeholder assets). These assets are not included in the build process.
293
+ - The build directory should contain all assets that are needed in production.
294
+ - Adjust each app's `project.json` file to include the assets as follows:
295
+
296
+ ```json
297
+ {
298
+ "build": {
299
+ "executor": "@angular/build:application",
300
+ "outputs": ["{options.outputPath}"],
301
+ "options": {
302
+ // ...
303
+ "assets": [
304
+ {
305
+ "input": "libs/assets/src/APP_NAME_HERE/build",
306
+ "glob": "**/*",
307
+ "output": "assets" // There should be no separate build directory for build assets.
308
+ },
309
+ {
310
+ "input": "libs/assets/src/APP_NAME_HERE/serve",
311
+ "glob": "**/*",
312
+ "output": "assets/serve"
313
+ },
314
+ // If shared assets are needed, include them as well
315
+ {
316
+ "input": "libs/assets/src/shared/build",
317
+ "glob": "**/*",
318
+ "output": "assets/shared"
319
+ },
320
+ {
321
+ "input": "libs/assets/src/shared/serve",
322
+ "glob": "**/*",
323
+ "output": "assets/shared/serve"
324
+ }
325
+ ]
326
+ // ...
327
+ },
328
+ "configurations": {
329
+ "production": {
330
+ "assets": [
331
+ {
332
+ "input": "libs/assets/src/APP_NAME_HERE/build",
333
+ "glob": "**/*",
334
+ "output": "assets"
335
+ },
336
+ // If shared assets are needed, include them as well
337
+ {
338
+ "input": "libs/assets/src/shared/build",
339
+ "glob": "**/*",
340
+ "output": "assets/shared"
341
+ }
342
+ ]
343
+ },
344
+ // If you have a Storybook setup, you can add configurations for development and production environments.
345
+ // Make sure to add zone.js as a polyfill. This is currently still required for Storybook to work properly.
346
+ "storybook-development": {
347
+ "polyfills": ["zone.js"],
348
+ "assets": [
349
+ {
350
+ "input": "libs/assets/src/APP_NAME_HERE/serve",
351
+ "glob": "**/*",
352
+ "output": "assets/serve"
353
+ },
354
+ {
355
+ "input": "libs/assets/src/APP_NAME_HERE/storybook",
356
+ "glob": "**/*",
357
+ "output": "assets"
358
+ },
359
+ {
360
+ "input": "libs/assets/src/APP_NAME_HERE/build",
361
+ "glob": "**/*",
362
+ "output": "assets"
363
+ },
364
+ // If shared assets are needed, include them as well
365
+ {
366
+ "input": "libs/assets/src/shared/build",
367
+ "glob": "**/*",
368
+ "output": "assets/shared"
369
+ },
370
+ {
371
+ "input": "libs/assets/src/shared/serve",
372
+ "glob": "**/*",
373
+ "output": "assets/shared/serve"
374
+ },
375
+ {
376
+ "input": "libs/assets/src/shared/storybook",
377
+ "glob": "**/*",
378
+ "output": "assets/shared/storybook"
379
+ }
380
+ ]
381
+ },
382
+ "storybook-production": {
383
+ "polyfills": ["zone.js"],
384
+ "assets": [
385
+ {
386
+ "input": "libs/assets/src/APP_NAME_HERE/storybook",
387
+ "glob": "**/*",
388
+ "output": "assets"
389
+ },
390
+ {
391
+ "input": "libs/assets/src/APP_NAME_HERE/build",
392
+ "glob": "**/*",
393
+ "output": "assets"
394
+ },
395
+ // If shared assets are needed, include them as well
396
+ {
397
+ "input": "libs/assets/src/shared/build",
398
+ "glob": "**/*",
399
+ "output": "assets/shared"
400
+ },
401
+ {
402
+ "input": "libs/assets/src/shared/storybook",
403
+ "glob": "**/*",
404
+ "output": "assets/shared/storybook"
405
+ }
406
+ ]
407
+ }
408
+ }
409
+ }
410
+ }
411
+ ```
412
+
413
+ #### Asset examples
414
+
415
+ - Given the `cat.jpg` image used in the `my-app` application, the asset should be placed in the `libs/assets/src/my-app/build` directory.
416
+ - Subdirectories can be used to organize assets further, such as `libs/assets/src/my-app/build/images/cat.jpg`.
417
+
418
+ ```plaintext
419
+ assets/
420
+ β”‚ β”œβ”€β”€ my-app/
421
+ β”‚ β”‚ β”œβ”€β”€ build/
422
+ β”‚ β”‚ β”‚ β”œβ”€β”€ images/
423
+ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ cat.jpg
424
+ ```
425
+
426
+ This way, the asset can be accessed in the application as follows:
427
+
428
+ ```html
429
+ <img src="assets/images/cat.jpg" alt="Cat Image" />
430
+ ```
431
+
432
+ - If the asset is only needed during development (e.g., as a mock placeholder image)
433
+ - Place it in the `libs/assets/src/my-app/serve/images/cat.jpg`.
434
+ - The asset can then be accessed in the application as follows:
435
+
436
+ ```html
437
+ <img src="assets/serve/images/cat.jpg" alt="Cat Image" />
438
+ ```
439
+
440
+ **Keep in mind** that the `serve` directory is optional and can be used for assets that are only needed during development. These assets are not included in the build process.
441
+
442
+ ### Storybook
443
+
444
+ - If extra components are needed to render a component in Storybook, they should be placed in a `storybook` directory within the component directory.
445
+ - **Never** export storybook specific logic from the component directory.
446
+
447
+ ```plaintext
448
+ settings-form/
449
+ β”‚ β”œβ”€β”€ storybook/
450
+ β”‚ β”‚ β”œβ”€β”€ settings-form.storybook.component.ts
451
+ β”‚ β”‚ β”œβ”€β”€ settings-form.storybook.component.html
452
+ β”‚ β”‚ β”œβ”€β”€ settings-form-dummy-data.ts
453
+ β”‚ β”‚ β”œβ”€β”€ index.ts βœ… (exports the storybook component for use in the .stories.ts file + dummy data if needed)
454
+ β”‚ β”œβ”€β”€ settings-form.component.ts
455
+ β”‚ β”œβ”€β”€ settings-form.component.stories.ts
456
+ β”‚ β”œβ”€β”€ settings-form.component.html
457
+ β”‚ β”œβ”€β”€ index.ts βœ… (exports the form component)
458
+ ```
459
+
460
+ ## Changesets
461
+
462
+ - Use `@changesets` to manage changelogs.
463
+ - **Do not** create a changeset for irrelevant changes (e.g., formatting, comments, internal refactoring).
464
+ - **Do not** create a changeset for fixes to features that have not yet been released.
465
+ - **Do not** include multiple changes in a single changeset. Each changeset should contain only one change.
466
+ - **Always** start a changeset with at least one sentence describing the change. Optional follow-up markdown can be added after the initial sentence.
467
+ - Create changesets for dependency updates if they are relevant to the project (e.g., major version updates).
468
+ - Write changesets in the imperative mood. For example:
469
+ - Add button component
470
+ - Fix spacing issues inside buttons
471
+ - Update to Angular 20
472
+
473
+ ### Examples
474
+
475
+ Use the following legend to determine the type of changeset you should create.
476
+ **Do not** include the emoji in your changeset message; it is only used here for clarity.
477
+
478
+ - ✨ **Major Change**: For breaking changes that require updates or modifications by consumers of the project.
479
+ - Example: Remove settings view from the app.
480
+
481
+ - πŸš€ **Minor Change**: For adding new features or functionality in a backward-compatible way.
482
+ - Example: Add support for dark mode in components.
483
+
484
+ - πŸ› **Patch Change**: For bug fixes or small adjustments that do not introduce breaking changes.
485
+ - Example: Fix button alignment issue.
486
+
487
+ #### Valid Changesets
488
+
489
+ The following changesets are valid and should be created:
490
+
491
+ - ✨ Migrate to NX 20
492
+ - πŸš€ Add button component
493
+ - πŸš€ Add text input component
494
+ - πŸš€ Add settings view
495
+ - πŸš€ Add uikit library
496
+ - πŸš€ Add login app
497
+ - πŸš€ Update TypeScript configurations to allow usage of ES2027
498
+ - πŸš€ Make CI pipelines faster by caching `node_modules`
499
+
500
+ #### Special Cases
501
+
502
+ For these types of changesets, ensure that the feature you are working on has already been released (and can be found in the changelog). If the feature is not yet released, **do not** create a changeset for it.
503
+
504
+ - ✨ Change route of settings view from `/settings` to `/user/settings`
505
+ - ✨ Rename `MatchComponent` to `MatchupComponent`
506
+ - πŸš€ Add general tab to settings view
507
+ - πŸ› Fix issue with settings view not loading on mobile devices
508
+ - πŸ› Enhance button component rendering to improve performance
509
+ - πŸ› Fix typo in settings view headline
510
+ - πŸ› Fix linting issues inside progress bar component
511
+
512
+ #### Invalid Changesets
513
+
514
+ The following changesets are generally invalid and should **not** be created:
515
+
516
+ - Cleanup code inside button component (no changeset needed).
517
+ - Update Angular to 19.1.1 from 19.1.0 (it's a patch update and does not require a changeset).
518
+ - Move button component to a new directory (if it remains in the same NX library, no changeset is needed. Otherwise, it's a ✨).
519
+ - Run Prettier on all files (no changeset needed).
520
+ - Fix button style on hover **and** update slider component bar thickness (two changes should not be combined into one changeset).
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: theming
3
+ description: The two runtime theming systems in @ethlete/core - surface theming (elevation-aware neutrals) and color theming (semantic accent palettes) - how to register them in an app and how components must consume them. Read BEFORE writing or reviewing CSS that involves any color, background, border, or interaction state, and when wiring theme context across overlay/portal boundaries.
4
+ kind: skill
5
+ scope: consumer
6
+ requires: ['@ethlete/core']
7
+ paths: ['**/*.css']
8
+ vars: [docsBaseUrl]
9
+ ---
10
+
11
+ # Surface & color theming
12
+
13
+ Two **independent** systems in `@ethlete/core`; most components consume both:
14
+
15
+ - **Surface theming** - the neutral, elevation-aware colors of the box a component
16
+ sits on: background, text, muted/subtle text, borders, and a neutral
17
+ "interaction" color for hover/active tints. Set via `[etProvideSurface]`.
18
+ - **Color theming** - semantic accent palettes (brand/primary, success, warning,
19
+ error): a primary color, an on-primary contrast color, and an "ink" color for
20
+ text/borders on transparent fills. Set via `[etProvideColor]`.
21
+
22
+ Full reference: {%docsBaseUrl%}/core/theming.
23
+
24
+ ## Registering themes (once, in the app)
25
+
26
+ Your app owns the themes; the SDK ships none. Register them at bootstrap with
27
+ `provideSurfaceThemesWithTailwind4(SURFACE_THEMES)` and
28
+ `provideColorThemesWithTailwind4(THEMES)`, and generate the raw CSS variables with
29
+ the Nx generators shipped in `@ethlete/core`:
30
+
31
+ ```bash
32
+ npx nx g @ethlete/core:tailwind-4-surface-theme
33
+ npx nx g @ethlete/core:tailwind-4-color-theme
34
+ ```
35
+
36
+ The generated CSS defines the raw vars on `.et-surface--<name>` / `.et-color--<name>`
37
+ classes (plus `:root` defaults) and derives the public tokens below on `:root` **and**
38
+ every scope class - so the tokens are always resolvable and components just read them.
39
+
40
+ **Theme names are yours.** `brand`, `danger`, `neutral`, `dark-elevated` are only
41
+ examples. Never hardcode a theme-name union in a component type, a doc or an example.
42
+ The portable handle is the theme **`type`**: `injectErrorTheme()` finds whichever theme
43
+ the app registered with `type: 'error'`.
44
+
45
+ ## Tokens components may consume
46
+
47
+ Use these **derived** tokens - never the raw `--et-surface-background` /
48
+ `--et-color-primary` channel triplets, and never a hardcoded color (a static fallback
49
+ _after_ the token is fine).
50
+
51
+ Surface (each exists as `-solid` = usable color, `-rgb` = `R G B` channels):
52
+
53
+ | Token | Use for |
54
+ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
55
+ | `--et-surface-background-solid` | component/panel background |
56
+ | `--et-surface-color-solid` | text |
57
+ | `--et-surface-color-muted-solid` | secondary text, placeholders, group labels |
58
+ | `--et-surface-color-subtle-solid` | tertiary text |
59
+ | `--et-surface-border-solid` | borders, separators |
60
+ | `--et-surface-interaction-solid` | neutral hover/active tint source - mix it: `color-mix(in srgb, var(--et-surface-interaction-solid) 12%, transparent)` (12/16/20% for hover/focus/active, 6–8% for subtle fills) |
61
+
62
+ Color (from the nearest `[etProvideColor]` scope):
63
+
64
+ | Token | Use for |
65
+ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
66
+ | `--et-theme-color-primary` | filled backgrounds; **opacity-aware** - set `--et-theme-color-primary-opacity: 0.16` etc. per state to tint without changing the color |
67
+ | `--et-theme-color-primary-solid` | accents at full strength: focus borders, selected marks, spinners |
68
+ | `--et-theme-color-on-primary` | text/icons on a primary-filled background |
69
+ | `--et-theme-color-ink-solid` | primary-tinted text/border on transparent/tonal fills |
70
+
71
+ Interaction-state variants (`--et-surface-interaction-{hover,focus,active,disabled}-solid`)
72
+ resolve automatically per CSS state when the element has `[etSurfaceInteractive]`;
73
+ similarly `[etColorInteractive]` re-resolves the color tokens per state. Put either on
74
+ the interactive element itself, never on a wrapper.
75
+
76
+ ## Providing context
77
+
78
+ - `[etProvideSurface]="'dark-elevated'"` / `[etAutoSurface]` (auto-picks the next
79
+ elevation) set the surface scope; `[etProvideColor]="'brand'"` sets the color scope.
80
+ A component that owns a themed region usually adds `ProvideColorDirective` /
81
+ `ProvideSurfaceDirective` as `hostDirectives`.
82
+ - **Detached overlay panes (menu, dialog, tooltip…) do not inherit DOM context.**
83
+ Re-apply it: inject `COLOR_PROVIDER` / `SURFACE_PROVIDER` with
84
+ `{ optional: true, skipSelf: true }`, then `ownProvider.syncWithProvider(ctx)` for
85
+ color and `resolveSurfaceByElevation(themes, type, elevation + 1)` for the surface.
86
+ - **Semantic colors come through DI, not CSS.** There is no global "error color"
87
+ variable - error/warning/success are just color themes. To render something in the
88
+ error color, `injectErrorTheme()` (it throws if the app registered no `type: 'error'`
89
+ theme) and either bind it (`[etProvideColor]="errorColorTheme"`) or force it
90
+ programmatically (`provideColor.forceColor(theme)` / `clearForcedColor()`). Inside
91
+ that scope, `--et-theme-color-primary-*` _is_ the error color.
92
+
93
+ ## Pitfalls
94
+
95
+ - `@property` `initial-value` cannot contain `var()`, so a public `--et-*` token
96
+ declared via `@property` can never default to a theme token. If the default should
97
+ come from the theme, don't declare an `@property` - consume the theme token directly
98
+ (optionally behind a `--_et-*` indirection var).
99
+ - `--et-theme-color-primary-*` always resolves to the **nearest color scope**. A
100
+ hardcoded semantic color in CSS can't be replaced by it unless the right theme is
101
+ provided on that element.
102
+ - Keep a static fallback (`var(--et-surface-border-solid, rgb(255 255 255 / 0.1))`) so
103
+ components degrade in theme-less setups. `injectErrorTheme()` is the exception: it is
104
+ a hard requirement wherever it is used.
105
+ - **Cascade layers, not `:where()`, are what let a Tailwind utility override component
106
+ CSS.** Wrap every component CSS file in `@layer components { … }`; `:where()` only
107
+ flattens a component's own modifiers to single-class weight. See the `styling` rule.
108
+
109
+ Styling a **story** file rather than a component? The colour rule is the same, but the
110
+ Tailwind side has its own traps - {%skill:story-styling%}.