@adobe/data-ai 0.10.22 → 0.10.24
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/.claude/rules/README.md +8 -3
- package/.claude/rules/ecs.md +1 -1
- package/.claude/rules/element.md +9 -0
- package/.claude/rules/features/data/components.md +6 -3
- package/.claude/rules/features/data/index.md +4 -1
- package/.claude/rules/features/data/state.md +211 -0
- package/.claude/rules/features/ecs/conformance.md +12 -6
- package/.claude/rules/features/index.md +4 -0
- package/.claude/rules/features/services/index.md +2 -0
- package/.claude/rules/features/services/main-service/actions.md +72 -0
- package/.claude/rules/features/services/main-service/archetypes.md +51 -0
- package/.claude/rules/features/services/main-service/components.md +43 -0
- package/.claude/rules/features/services/main-service/computed.md +84 -0
- package/.claude/rules/features/services/main-service/conformance.md +97 -0
- package/.claude/rules/features/services/main-service/core-database.md +50 -0
- package/.claude/rules/features/services/main-service/index.md +189 -0
- package/.claude/rules/features/services/main-service/indexes.md +65 -0
- package/.claude/rules/features/services/main-service/resources.md +30 -0
- package/.claude/rules/features/services/main-service/services.md +27 -0
- package/.claude/rules/features/services/main-service/systems.md +145 -0
- package/.claude/rules/features/services/main-service/transactions.md +61 -0
- package/.claude/rules/features/ui/index.md +2 -0
- package/.claude/rules/service.md +25 -75
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +8 -0
- package/bin/cli.mjs +24 -8
- package/package.json +3 -3
- package/skills/build-actions/SKILL.md +2 -0
- package/skills/build-app-entry/SKILL.md +2 -0
- package/skills/build-computed/SKILL.md +2 -0
- package/skills/build-core-database/SKILL.md +2 -0
- package/skills/build-data/SKILL.md +2 -0
- package/skills/build-feature/SKILL.md +3 -0
- package/skills/build-indexes/SKILL.md +2 -0
- package/skills/build-service-database/SKILL.md +2 -0
- package/skills/build-services/SKILL.md +2 -0
- package/skills/build-spec/SKILL.md +2 -0
- package/skills/build-systems/SKILL.md +2 -0
- package/skills/build-transactions/SKILL.md +2 -0
- package/skills/build-ui/SKILL.md +2 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/structure/SKILL.md +2 -0
- package/.claude/rules/features/ui/binding-element.md +0 -226
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
paths:
|
|
3
|
-
- '**/src/**/*element*.ts'
|
|
4
|
-
- '**/src/**/elements/**/*.ts'
|
|
5
|
-
- '**/src/**/ui/**/*-element.ts'
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Binding elements
|
|
9
|
-
|
|
10
|
-
Binding elements are thin wires between application state and the UI — not designed for unit-testability. Non-trivial logic doesn't belong
|
|
11
|
-
here, it must be pushed into either the action/transaction layer or into presentations.
|
|
12
|
-
|
|
13
|
-
## Scope
|
|
14
|
-
|
|
15
|
-
This rule applies to **binding elements** — Lit elements that read state from a service via `useObservableValues`. If you are not using Lit then still try to adhere to a similar pattern to this with the same focus on separation of concerns and functional presentation if possible.
|
|
16
|
-
The discriminator is the base class:
|
|
17
|
-
|
|
18
|
-
- `ApplicationElement<T>` — from either `@adobe/data-lit` or `src/applications/application-element.ts`. Every app-level binding element
|
|
19
|
-
extends this (directly or transitively). Concrete app bases like `FooElement`, `VideoGenerationElement`, `ImageEditorElement`,
|
|
20
|
-
`StoryGenerationElement`, `ImageGenerationElement` are thin subclasses that bind a specific `MainService` type and inherit this contract.
|
|
21
|
-
- `DatabaseElement<T>` — the lower-level `@adobe/data-lit` base used by apps with no intermediate `*Element` layer (e.g.
|
|
22
|
-
`HelloWorldElement`).
|
|
23
|
-
|
|
24
|
-
Plain `LitElement` subclasses that expose their API through `@property` alone (shared UI primitives like `media-timeline`, `model-picker`,
|
|
25
|
-
`toast-with-image`) are **not** binding elements. They don't subscribe to a service; their props are the entire contract. Out of scope for
|
|
26
|
-
this rule.
|
|
27
|
-
|
|
28
|
-
Presentation files (`*-presentation.ts`), CSS files (`*.css.ts`), tests (`*.test.ts`, `*.spec.ts`), and stories are also out of scope.
|
|
29
|
-
|
|
30
|
-
## The shape
|
|
31
|
-
|
|
32
|
-
A binding element is a thin wire between a service and a presentation. The class body contains:
|
|
33
|
-
|
|
34
|
-
1. Optional static `styles` property definition
|
|
35
|
-
2. Zero or more `@property` declarations (allowed below).
|
|
36
|
-
3. One `render()` method whose body is: one `useObservableValues(...)` call, another lifecycle hook call (in rare cases), a null-guard, and
|
|
37
|
-
`return presentation.render({ ...values, ...callbacks })`.
|
|
38
|
-
|
|
39
|
-
No other members. No `@state`. No private handler fields. No lifecycle methods (`connectedCallback`, `updated`, `firstUpdated`,
|
|
40
|
-
`disconnectedCallback`) except on app-entrypoint classes where the lifecycle concern is app-boot shaped but cannot be expressed as a hook
|
|
41
|
-
(see `hooks.md` for hook-first lifecycle).
|
|
42
|
-
|
|
43
|
-
## `@property` allowlist
|
|
44
|
-
|
|
45
|
-
The allowed set depends on **class role**:
|
|
46
|
-
|
|
47
|
-
- **App entrypoint classes** — the (typically empty-body) subclass of `ApplicationElement<T>` that binds a specific `MainService` type.
|
|
48
|
-
Examples: `VideoGenerationElement`, `HelloWorldElement`, `TodoElement`. These accept injected services and top-level inputs from the host
|
|
49
|
-
shell.
|
|
50
|
-
- **Concrete binding elements** — classes that extend an app entrypoint (or another concrete binding) and render UI. Examples: `Hud`,
|
|
51
|
-
`FooRotationControlElement`, `CommunitySubmissionConnector`. These accept only:
|
|
52
|
-
- Entity-id-shaped locator props (`entityId`, `entityIds`, `trackId`) that identify _where_ to load this element's data from.
|
|
53
|
-
- Rare parent-forwarded flags passed straight to the presentation without branching on them (e.g. `highVolumeFlag` in `audio-view.ts`).
|
|
54
|
-
Prefer promoting the flag into a service computed.
|
|
55
|
-
|
|
56
|
-
All other values come from the service.
|
|
57
|
-
|
|
58
|
-
## Observed values must be raw reads
|
|
59
|
-
|
|
60
|
-
Every expression inside `useObservableValues` is either a direct read of a service observable (`this.service.observe.*`) or a direct read of
|
|
61
|
-
a computed (`this.service.computed.*`). No `.map`, `.filter`, `.reduce`, arithmetic, conditionals, or `Observe.withMap`-inline composition.
|
|
62
|
-
|
|
63
|
-
If derivation is needed: ECS Database apps place it in a database computed (`plugins/computed`, read back as `this.service.computed.*`);
|
|
64
|
-
non-ECS apps place it in a dependent-state service function (`dependent-state-service`). If it is used only by this element, define it
|
|
65
|
-
locally in a sibling file and import it. Computed and dependent-state must use utilities from `Obsereve.*` rather than trying to call
|
|
66
|
-
observable functions.
|
|
67
|
-
|
|
68
|
-
**Bad - doesn't use utilities from `Obsereve.*`**
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
return notify => {
|
|
72
|
-
const unobserve = oValue(value => {
|
|
73
|
-
notify(value ** 2);
|
|
74
|
-
});
|
|
75
|
-
return unobserve;
|
|
76
|
-
};
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
**Good - uses utilities from `Obsereve.*`**
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
return Observe.withMap(oValue => value ** 2);
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## Callbacks are one-liners
|
|
86
|
-
|
|
87
|
-
- Each callback passed to the presentation invokes a UI action with optional arguments or a transaction with optional arguments.
|
|
88
|
-
- Callbacks must not encode domain knowledge, branch on state, or build complex shapes. Logic belongs in the UI action layer or transactions
|
|
89
|
-
layer.
|
|
90
|
-
- UI action functions and transactions must be called only via `this.service`, they must never be imported directly.
|
|
91
|
-
- Never consume the return value of a UI action or transaction call.
|
|
92
|
-
- Simple argument transformations are allowed — arithmetic, negation, fixed arguments, simple ternaries that select an argument value.
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
render() {
|
|
96
|
-
return presentation.render({
|
|
97
|
-
// UI action call without parameters — good
|
|
98
|
-
saveProgress: this.service.actions.saveProgress,
|
|
99
|
-
// UI action call with fixed argument — good
|
|
100
|
-
setLandscape: () => this.service.actions.setOrientation('landscape'),
|
|
101
|
-
// UI action call with simple ternary — good
|
|
102
|
-
jump: (heroMode: boolean) => this.service.actions.move(heroMode ? 100 : 10),
|
|
103
|
-
// transaction call - good
|
|
104
|
-
play: this.service.actions.playTrack
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## No derivation math in `render()`
|
|
110
|
-
|
|
111
|
-
**Derivation math** is forbidden in `render()`. Simple argument assembly in callbacks is the only exception — see **Action callbacks are
|
|
112
|
-
one-liners** above.
|
|
113
|
-
|
|
114
|
-
## Lifecycle goes through hooks
|
|
115
|
-
|
|
116
|
-
Any need for mount/unmount/update behavior routes through a hook. **Reuse order, one hook per file, presentation vs binding placement, and
|
|
117
|
-
testing notes:** `hooks.md`.
|
|
118
|
-
|
|
119
|
-
Never inline lifecycle logic in the binding. App-entrypoint classes may override lifecycle methods when the concern is app-boot shaped
|
|
120
|
-
(service wiring, ancestor lookup); even then, prefer a hook.
|
|
121
|
-
|
|
122
|
-
Rare cases when an additional hook can be used in `render()`:
|
|
123
|
-
|
|
124
|
-
- focus on element
|
|
125
|
-
- scroll to element
|
|
126
|
-
- clean up on un-mount
|
|
127
|
-
- and other lifecycle-bound behaviour
|
|
128
|
-
|
|
129
|
-
Use `useEffect` for cleanup on unmount rather than `disconnectedCallback`:
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
render() {
|
|
133
|
-
useEffect(() => {
|
|
134
|
-
return () => this.service.actions.someCleanup();
|
|
135
|
-
}, []); // empty array → runs cleanup on unmount only
|
|
136
|
-
|
|
137
|
-
const values = useObservableValues(() => ({ ... }));
|
|
138
|
-
if (!values) return nothing;
|
|
139
|
-
return presentation.render(values);
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
In binding-elements `render()` doesn't need to be explicitly decorated with `withHooks()` because it's done implicitly in the
|
|
144
|
-
`ApplicationElement` base class.
|
|
145
|
-
|
|
146
|
-
## No timers, emitters, or broadcast channels
|
|
147
|
-
|
|
148
|
-
- Never `setInterval`, `setTimeout`, or `requestAnimationFrame`.
|
|
149
|
-
- Never `new EventEmitter`. Cross-element state flows through service observables.
|
|
150
|
-
- Never `new BroadcastChannel`. Cross-tab concerns go through a dedicated service.
|
|
151
|
-
|
|
152
|
-
## No direct database reads
|
|
153
|
-
|
|
154
|
-
Never call `this.service.get()`, `this.service.read()`, `this.service.select()`, or any synchronous accessor. The service interface does not
|
|
155
|
-
expose them by design; reaching for them means a missing computed.
|
|
156
|
-
|
|
157
|
-
## Presentation contract
|
|
158
|
-
|
|
159
|
-
A binding element imports a sibling `*-presentation.ts` file that exports exactly `render` (required) and optionally `unlocalized`. Nothing
|
|
160
|
-
else — no types, no styles, no helpers. Consumers derive the props type via `Parameters<typeof render>[0]`. Presentation stays hook-free by
|
|
161
|
-
default; element hooks live here or in shared modules per `hooks.md` and `features/ui/presentation.md`.
|
|
162
|
-
|
|
163
|
-
**For refactors whose stated scope does not include presentations**, the matching `*-presentation.ts` file must not change. Verify with
|
|
164
|
-
`git diff --name-only | grep presentation`. If a scope-limited refactor requires a presentation signature change, stop and reconsider the
|
|
165
|
-
binding-side helper — the scope fence exists because presentations are the public contract between binding and render output.
|
|
166
|
-
|
|
167
|
-
Callback prop names use **verb** or **verbNoun** form. Never `on*` prefix. The binding-side variable matches the presentation prop name 1:1.
|
|
168
|
-
See `features/ui/presentation.md` for full examples.
|
|
169
|
-
|
|
170
|
-
## Size budget
|
|
171
|
-
|
|
172
|
-
A compliant binding element file is small enough that the `render()` body fits on screen without scrolling. If it doesn't, a concern has not
|
|
173
|
-
been extracted yet — find it.
|
|
174
|
-
|
|
175
|
-
## Deletion test
|
|
176
|
-
|
|
177
|
-
Delete everything except the class body. What remains should be: static styles assignment, property declarations, one
|
|
178
|
-
`useObservableValues(...)` call, another lifecycle hook call (in rare cases), a null-guard, and one render call. Anything else is a
|
|
179
|
-
violation.
|
|
180
|
-
|
|
181
|
-
## Good examples (in-tree)
|
|
182
|
-
|
|
183
|
-
- `src/applications/image-generation/elements/sub-navigation-panel/sub-navigation-panel.ts` — concrete binding element; one-liner action
|
|
184
|
-
callback
|
|
185
|
-
- `src/applications/foo/elements/properties-panel/views/audio-view.ts` — entity-id-only + parent-forwarded flag
|
|
186
|
-
- `src/applications/image-editor/elements/hud/hud.ts` — concrete binding element (extends `ImageEditorElement`); `useObservableValues` reads
|
|
187
|
-
a dependentState observable; forwards to presentation with spread.
|
|
188
|
-
- `src/applications/video-generation/elements/commerce-coachmark/commerce-coachmark.ts` — localization-only binding; trivially small and
|
|
189
|
-
compliant.
|
|
190
|
-
|
|
191
|
-
## Counter-example (do not land)
|
|
192
|
-
|
|
193
|
-
```ts
|
|
194
|
-
@customElement(tagName)
|
|
195
|
-
export class BadControlElement extends FooElement {
|
|
196
|
-
@property({ attribute: false }) entityIds: readonly Entity[] = [];
|
|
197
|
-
@state() private _cachedSum = 0; // violation: @state
|
|
198
|
-
private _handleChange?: (e: Event) => void; // violation: private handler
|
|
199
|
-
|
|
200
|
-
connectedCallback() {
|
|
201
|
-
// violation: lifecycle override
|
|
202
|
-
super.connectedCallback();
|
|
203
|
-
this._handleChange = e => (this._cachedSum += Number((e.target as HTMLInputElement).value));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
@withHooks // Redundant
|
|
207
|
-
render() {
|
|
208
|
-
const values = useObservableValues(() => ({
|
|
209
|
-
items: this.service.observe.select(/* ... */),
|
|
210
|
-
total: Observe.withMap(
|
|
211
|
-
// violation: logic in observed values
|
|
212
|
-
this.service.observe.select(/* ... */),
|
|
213
|
-
rows => rows.reduce((a, r) => a + r.duration, 0)
|
|
214
|
-
),
|
|
215
|
-
}));
|
|
216
|
-
const clamped = Math.max(0, Math.min(100, values.total)); // violation: derivation in render
|
|
217
|
-
const onSave = (v: number) => {
|
|
218
|
-
// violation: logic in callback + onX name
|
|
219
|
-
const normalized = Math.round(v * 100) / 100;
|
|
220
|
-
if (!Number.isFinite(normalized)) return;
|
|
221
|
-
this.service.actions.updateThing({ value: normalized });
|
|
222
|
-
};
|
|
223
|
-
return presentation.render({ total: clamped, onSave });
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
```
|