@adobe/data-ai 0.9.84 → 0.9.87
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/cohesion.md +2 -2
- package/.claude/rules/data-modelling.md +2 -0
- package/.claude/rules/ecs.md +275 -0
- package/.claude/rules/element.md +3 -1
- package/.claude/rules/features/data/state.md +2 -0
- package/.claude/rules/features/ecs/conformance.md +5 -3
- package/.claude/rules/features/ecs/systems.md +61 -5
- package/.claude/rules/features/ui/binding-element.md +226 -0
- package/.claude/rules/features/ui/lazy-wrapper.md +27 -0
- package/.claude/rules/features/ui/presentation-test.md +38 -0
- package/.claude/rules/{presentation.md → features/ui/presentation.md} +2 -32
- package/.claude/rules/function-references.md +2 -2
- package/.claude/rules/hooks.md +57 -0
- package/.claude/rules/layout.md +132 -0
- package/.claude/rules/lazy-element.md +1 -1
- package/.claude/rules/lit.md +115 -0
- package/.claude/rules/namespace.md +93 -135
- package/.claude/rules/observe.md +183 -0
- package/.claude/rules/react.md +102 -0
- package/.claude/rules/service.md +164 -0
- package/.claude/rules/type-casts.md +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +24 -4
- package/package.json +3 -3
- package/skills/build-actions/SKILL.md +3 -1
- package/skills/build-application/SKILL.md +5 -3
- package/skills/build-computed/SKILL.md +2 -0
- package/skills/build-data/SKILL.md +4 -0
- package/skills/build-feature/SKILL.md +12 -15
- package/skills/build-game/SKILL.md +4 -2
- package/skills/build-indexes/SKILL.md +2 -0
- package/skills/build-services/SKILL.md +2 -0
- package/skills/build-systems/SKILL.md +3 -3
- package/skills/graph-execute/SKILL.md +13 -0
- package/.claude/rules/archetypes.md +0 -60
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/*presentation.test.ts'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Testing
|
|
7
|
+
|
|
8
|
+
A pure `render(props)` is trivial to test — no DOM, no rendering, no
|
|
9
|
+
component lifecycle. Pass stub props (no-op callbacks) and assert on the
|
|
10
|
+
**declaration** it returns. Co-locate tests as `<name>-presentation.test.ts`.
|
|
11
|
+
|
|
12
|
+
For Lit, wrap the result with `Template.from` (from `@adobe/data-lit`) and
|
|
13
|
+
assert on the query, not on positional `.values` indices:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { describe, it, expect } from "vitest";
|
|
17
|
+
import { Template } from "@adobe/data-lit";
|
|
18
|
+
import { render } from "./todo-toolbar-presentation.js";
|
|
19
|
+
|
|
20
|
+
it("wires the Add button and shows the stats", () => {
|
|
21
|
+
const addTodo = () => {};
|
|
22
|
+
const t = Template.from(render(props({ addTodo, totalCount: 3, completedCount: 1 })));
|
|
23
|
+
expect(t.has("<sp-textfield")).toBe(true); // presence
|
|
24
|
+
expect(t.text).toContain("1 / 3 completed"); // static + primitive text
|
|
25
|
+
expect(t.values).toContain(addTodo); // callback wired (identity, not values[3])
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`Template.Query` exposes `text`, `values`, `children`, `has(fragment)`, and
|
|
30
|
+
`find(fragment)`. Prefer **identity** checks (`values` `toContain` a
|
|
31
|
+
callback / `nothing` / a bound value) over positions. To exercise a
|
|
32
|
+
callback, pull it from `values` and call it with a plain synthetic event —
|
|
33
|
+
no mocks, no DOM.
|
|
34
|
+
|
|
35
|
+
A presentation that only composes lazy child-element wrappers (or imports
|
|
36
|
+
Spectrum components) needs a `// @vitest-environment jsdom` docblock so the
|
|
37
|
+
module's custom-element side effects can load — the assertions still never
|
|
38
|
+
render or touch the DOM. Presentations of native elements run under Node.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
paths:
|
|
3
|
-
- '
|
|
3
|
+
- '**/*presentation.ts'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Presentation authoring
|
|
@@ -42,37 +42,7 @@ export function render({ onDismiss, onLayoutChange, onCreate }: { ... }) {}
|
|
|
42
42
|
|
|
43
43
|
## Testing
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
component lifecycle. Pass stub props (no-op callbacks) and assert on the
|
|
47
|
-
**declaration** it returns. Co-locate tests as `<name>-presentation.test.ts`.
|
|
48
|
-
|
|
49
|
-
For Lit, wrap the result with `Template.from` (from `@adobe/data-lit`) and
|
|
50
|
-
assert on the query, not on positional `.values` indices:
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
import { describe, it, expect } from "vitest";
|
|
54
|
-
import { Template } from "@adobe/data-lit";
|
|
55
|
-
import { render } from "./todo-toolbar-presentation.js";
|
|
56
|
-
|
|
57
|
-
it("wires the Add button and shows the stats", () => {
|
|
58
|
-
const addTodo = () => {};
|
|
59
|
-
const t = Template.from(render(props({ addTodo, totalCount: 3, completedCount: 1 })));
|
|
60
|
-
expect(t.has("<sp-textfield")).toBe(true); // presence
|
|
61
|
-
expect(t.text).toContain("1 / 3 completed"); // static + primitive text
|
|
62
|
-
expect(t.values).toContain(addTodo); // callback wired (identity, not values[3])
|
|
63
|
-
});
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
`Template.Query` exposes `text`, `values`, `children`, `has(fragment)`, and
|
|
67
|
-
`find(fragment)`. Prefer **identity** checks (`values` `toContain` a
|
|
68
|
-
callback / `nothing` / a bound value) over positions. To exercise a
|
|
69
|
-
callback, pull it from `values` and call it with a plain synthetic event —
|
|
70
|
-
no mocks, no DOM.
|
|
71
|
-
|
|
72
|
-
A presentation that only composes lazy child-element wrappers (or imports
|
|
73
|
-
Spectrum components) needs a `// @vitest-environment jsdom` docblock so the
|
|
74
|
-
module's custom-element side effects can load — the assertions still never
|
|
75
|
-
render or touch the DOM. Presentations of native elements run under Node.
|
|
45
|
+
@see ./presentation-test.md
|
|
76
46
|
|
|
77
47
|
## Framework bindings
|
|
78
48
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/use-*.ts'
|
|
4
|
+
- '**/hooks/**/*.ts'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Element hooks (Lit / `@adobe/data-lit`)
|
|
8
|
+
|
|
9
|
+
Hooks tie **element lifecycle** and **DOM** concerns to reactive updates. This rule covers **where** hooks live, **how** to name and split
|
|
10
|
+
them, and **what** to reuse before writing something new.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Where hooks belong
|
|
15
|
+
|
|
16
|
+
| Location | Role |
|
|
17
|
+
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| **Binding element** (`ApplicationElement` / app `*Element` / `DatabaseElement`) | **Default home** for hooks: `useObservableValues`, refs, resize, visibility, keyboard, etc. Compute values here and pass **plain data + action callbacks** into `presentation.render(...)`. |
|
|
19
|
+
| **Presentation** (`*-presentation.ts`) | **Hook-free** so `render` stays a pure `props → TemplateResult` function. **Exception:** only when unavoidable for performance or platform constraints — document **why** a binding-level hook could not work. |
|
|
20
|
+
| **Shared UI primitives** (plain `LitElement`, not a binding element) | Hooks allowed when the primitive owns DOM behavior; still follow **one hook per file** and the reuse order below. |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Reuse before inventing
|
|
25
|
+
|
|
26
|
+
Search in this order before adding a new hook module:
|
|
27
|
+
|
|
28
|
+
1. **`src/elements/hooks/`** — shared hooks for Studio / platform elements.
|
|
29
|
+
2. **`@adobe/data-lit`** — framework-provided hooks and primitives.
|
|
30
|
+
3. **Application-specific** — under `src/applications/<app>/…` when behavior is owned by that app.
|
|
31
|
+
4. **Element-local** — `use-<concern>.ts` next to the element when behavior is specific to one component.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## One hook, one file
|
|
36
|
+
|
|
37
|
+
- Each custom hook lives in **its own module** with a clear name (`use-element-size.ts`, `use-my-panel-keyboard.ts`).
|
|
38
|
+
- Do **not** bundle unrelated hooks in one file.
|
|
39
|
+
- Export the hook (and any minimal types its signature needs). Avoid dumping unrelated utilities into the same file.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Encapsulation
|
|
44
|
+
|
|
45
|
+
- A hook should own a **single concern**: e.g. "sync width to state", "subscribe to window resize", "focus prompt on open".
|
|
46
|
+
- Prefer **narrow return values** (values + small set of callbacks) over mutating arbitrary element fields.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Execute
|
|
51
|
+
|
|
52
|
+
When adding or changing hook code:
|
|
53
|
+
|
|
54
|
+
1. Confirm the hook cannot live on a **binding element** (or shared hook package) before putting it in a **presentation** file.
|
|
55
|
+
2. Run the **reuse** search order above; extend an existing hook if the behavior is the same concern.
|
|
56
|
+
3. Add or edit **one file per hook**; name with the `use-` prefix consistent with the rest of the repo.
|
|
57
|
+
4. Keep **binding** `render()` thin — call hooks, then pass results into `presentation.render`.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/*.css.ts'
|
|
4
|
+
- '**/*.css'
|
|
5
|
+
- '**/elements/**/*.ts'
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Layout and component types
|
|
9
|
+
|
|
10
|
+
UI components are one of two types with no overlap. Every component is either terminal or layout.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Terminal components
|
|
15
|
+
|
|
16
|
+
- Render their own UI (buttons, inputs, cards, text, etc.)
|
|
17
|
+
- Contain CSS for their appearance
|
|
18
|
+
- **Never** contain any external margin
|
|
19
|
+
|
|
20
|
+
Terminal components are the leaves of the component tree. They own their visual styling and content.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Layout components
|
|
25
|
+
|
|
26
|
+
- Do **not** render any UI themselves
|
|
27
|
+
- Composed only of other layout or terminal components
|
|
28
|
+
- Responsible for interior gaps between their children
|
|
29
|
+
- **Never** contain any external margin
|
|
30
|
+
- Should not need CSS 90% of the time or more — use the standard layout utility classes below.
|
|
31
|
+
- Generally have no business logic and should never re-render — keeps re-renders confined to lower terminal levels
|
|
32
|
+
|
|
33
|
+
**Exception:** Some layout components explicitly manage state (animating, showing/hiding tabs, accordions). Those will have logic and may
|
|
34
|
+
re-render.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Layout utility classes
|
|
39
|
+
|
|
40
|
+
Define `--layout-gap`, `--layout-gutter`, and `--layout-padding` on `:root` (or the root element) with your spacing scale. In Lit, import
|
|
41
|
+
the styles and include them in `export const styles` of the root element or shared base class.
|
|
42
|
+
|
|
43
|
+
```css
|
|
44
|
+
/* -- Direction --------------------------------------------------------- */
|
|
45
|
+
.stack {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
gap: var(--layout-gap);
|
|
49
|
+
}
|
|
50
|
+
.row {
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: row;
|
|
53
|
+
gap: var(--layout-gap);
|
|
54
|
+
}
|
|
55
|
+
.cluster {
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: row;
|
|
58
|
+
flex-wrap: wrap;
|
|
59
|
+
gap: var(--layout-gap);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* -- Gap overrides ----------------------------------------------------- */
|
|
63
|
+
.gap-gutter {
|
|
64
|
+
gap: var(--layout-gutter);
|
|
65
|
+
}
|
|
66
|
+
.gap-padding {
|
|
67
|
+
gap: var(--layout-padding);
|
|
68
|
+
}
|
|
69
|
+
.gap-none {
|
|
70
|
+
gap: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* -- Inset (padding) --------------------------------------------------- */
|
|
74
|
+
.inset {
|
|
75
|
+
padding: var(--layout-padding);
|
|
76
|
+
}
|
|
77
|
+
.inset-gutter {
|
|
78
|
+
padding: var(--layout-gutter);
|
|
79
|
+
}
|
|
80
|
+
.inset-gap {
|
|
81
|
+
padding: var(--layout-gap);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* -- Alignment --------------------------------------------------------- */
|
|
85
|
+
.center {
|
|
86
|
+
justify-content: center;
|
|
87
|
+
align-items: center;
|
|
88
|
+
}
|
|
89
|
+
.spread {
|
|
90
|
+
justify-content: space-between;
|
|
91
|
+
}
|
|
92
|
+
.align-center {
|
|
93
|
+
align-items: center;
|
|
94
|
+
}
|
|
95
|
+
.align-start {
|
|
96
|
+
align-items: flex-start;
|
|
97
|
+
}
|
|
98
|
+
.align-end {
|
|
99
|
+
align-items: flex-end;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* -- Sizing (applied to children) -------------------------------------- */
|
|
103
|
+
.fill {
|
|
104
|
+
flex: 1;
|
|
105
|
+
}
|
|
106
|
+
.fit {
|
|
107
|
+
flex: 0 0 auto;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For custom spacing in terminal components, use `var(--layout-gap)`, `var(--layout-gutter)`, or `var(--layout-padding)` rather than hardcoded
|
|
112
|
+
lengths.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Why this split matters
|
|
117
|
+
|
|
118
|
+
Layout components that rarely re-render are efficient. State changes and user interactions trigger updates at terminal levels; layout
|
|
119
|
+
structure above stays stable. Avoid putting reactive logic in layout components unless the layout itself is dynamic (tabs, accordions,
|
|
120
|
+
animations).
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Execute
|
|
125
|
+
|
|
126
|
+
When creating or modifying UI components:
|
|
127
|
+
|
|
128
|
+
1. Classify as terminal or layout. No overlap.
|
|
129
|
+
2. Terminal: owns UI and CSS; never external margin.
|
|
130
|
+
3. Layout: no own UI; only layout/terminal children; owns interior gaps; never external margin.
|
|
131
|
+
4. Prefer standard layout tokens for layout components; avoid custom CSS when possible.
|
|
132
|
+
5. Keep layout components free of business logic unless they explicitly manage layout state.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/ui/**/*.ts'
|
|
4
|
+
- '**/elements/**/*.ts'
|
|
5
|
+
- '**/*-element.ts'
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Lit element authoring
|
|
9
|
+
|
|
10
|
+
Lit elements live in the `components/` layer per the structure rule. Consume Observe and void actions from plugins per the service rule.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Extending DatabaseElement
|
|
15
|
+
|
|
16
|
+
Binding elements extend `DatabaseElement<typeof myPlugin>` — either directly or via an intermediate base class. The required plugin must be
|
|
17
|
+
specified in the generic.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// Direct
|
|
21
|
+
export class HelloWorldElement extends DatabaseElement<typeof helloWorldPlugin> {
|
|
22
|
+
get plugin() {
|
|
23
|
+
return helloWorldPlugin;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Indirect — base extends DatabaseElement, leaf specifies plugin
|
|
28
|
+
export class LayoutElement<T extends MyApplicationPlugin = MyApplicationPlugin> extends CoreApplicationElement<T> {}
|
|
29
|
+
export class ToolbarElement extends LayoutElement<typeof toolbarPlugin> {}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Binding element vs presentation
|
|
35
|
+
|
|
36
|
+
**Binding element** — the Lit custom element that:
|
|
37
|
+
|
|
38
|
+
- Injects observed values via `useObservableValues`
|
|
39
|
+
- Triggers re-render when those values change
|
|
40
|
+
- Binds action callbacks to the presentation
|
|
41
|
+
|
|
42
|
+
**Presentation** — a pure function (no hooks by default) that receives data and action callbacks as props and returns a `TemplateResult`.
|
|
43
|
+
Keep reactive logic in the binding element; presentation stays pure.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## useObservableValues
|
|
48
|
+
|
|
49
|
+
_Most_ binding elements use a **single** `useObservableValues` call. Collect all observed values in one object.
|
|
50
|
+
|
|
51
|
+
**Observe only what you need** — the minimal values required for rendering. For values that may resolve slowly, wrap with
|
|
52
|
+
`Observe.withDefault`.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
render() {
|
|
56
|
+
const values = useObservableValues(() => ({
|
|
57
|
+
visible: this.service.computed.isViewVisible(name),
|
|
58
|
+
userProfile: this.service.services.authentication.userProfile,
|
|
59
|
+
}));
|
|
60
|
+
if (!values) return;
|
|
61
|
+
return presentation.render({ ...values, toggleView: () => this.service.transactions.setViewVisible({ name, visible: true }) });
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Presentation exports
|
|
68
|
+
|
|
69
|
+
Presentation files ONLY export `render` (and `unlocalized` bundles where appropriate). Nothing else. If you need render args type
|
|
70
|
+
externally, use `Parameters<typeof render>[0]`.
|
|
71
|
+
|
|
72
|
+
**Styles:** Do not import or re-export Lit `CSSResult` from presentations. The binding element imports `*.css.ts` and sets `static styles`
|
|
73
|
+
there.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Action callbacks (not events)
|
|
78
|
+
|
|
79
|
+
Use **verb** or **verbNoun** names — never `on*` prefix. See `.claude/rules/presentation.md` for full examples and rationale.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Lit properties
|
|
84
|
+
|
|
85
|
+
**Almost never** use `@property` on binding elements.
|
|
86
|
+
|
|
87
|
+
**Exception:** Use properties ONLY when needed to bind to the correct entity in the database — e.g. `entity` for table rows, `layer` for
|
|
88
|
+
view hosts. Multiple instances of the same element need a property to identify which entity they represent.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Hooks
|
|
93
|
+
|
|
94
|
+
Canonical rules (reuse order, one hook per file, binding vs presentation): `.claude/rules/hooks.md`.
|
|
95
|
+
|
|
96
|
+
Use hooks from `src/elements/hooks/` and `@adobe/data-lit` instead of `connectedCallback`/`disconnectedCallback`. Use `useEffect` for
|
|
97
|
+
setup/teardown, `useKeyboardEvent` for single key events.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Execute
|
|
102
|
+
|
|
103
|
+
When creating or modifying a Lit element:
|
|
104
|
+
|
|
105
|
+
1. Extend `DatabaseElement<typeof myPlugin>` (directly or indirectly) with the required plugin specified.
|
|
106
|
+
2. Split into binding element (reactive) and presentation (pure).
|
|
107
|
+
3. Use a single `useObservableValues` in the binding element. Observe only minimal values; use `Observe.withDefault` for slow-resolving
|
|
108
|
+
values.
|
|
109
|
+
4. Pass observed values and action callbacks to presentation.
|
|
110
|
+
5. Keep presentation pure — no hooks (see `.claude/rules/hooks.md` for exceptions).
|
|
111
|
+
6. Add `@property` only when entity binding requires it (multiple instances).
|
|
112
|
+
7. Presentation exports only `render` (and localization bundles where appropriate). Binding element sets `static styles` from `*.css.ts`.
|
|
113
|
+
8. Add `*-presentation.test.ts` for presentation when appropriate; do not unit test binding elements.
|
|
114
|
+
9. Never include business logic within binding elements. Move into computed values or action handlers.
|
|
115
|
+
10. Good binding elements should be extremely small.
|
|
@@ -1,160 +1,118 @@
|
|
|
1
1
|
---
|
|
2
2
|
paths:
|
|
3
|
-
- '
|
|
3
|
+
- '**/*.ts'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Type namespace pattern
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
Single import surface per type: `<type-name>/<type-name>.ts`. Schema in `-schema.ts`, namespace re-export from `public.js`. Types live in
|
|
9
|
+
the `types/` layer.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
src/types/<type-name>/
|
|
13
|
-
<type-name>.ts # type alias + `export * as <Type> from "./public.js"`
|
|
14
|
-
schema.ts # `export const schema = { ... } as const satisfies Schema`
|
|
15
|
-
public.ts # re-exports every public helper
|
|
16
|
-
<helper>.ts # one declaration per file, camelCase
|
|
17
|
-
```
|
|
11
|
+
## Authority
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
This rule defines the canonical pattern. **Follow this standard for all new and refactored type folders.** The codebase may contain legacy
|
|
14
|
+
patterns (e.g., inline `export namespace` in shared files) that do not conform — do not copy those; follow this rule instead.
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
// types/log-level/schema.ts
|
|
23
|
-
export const schema = { type: "string", enum: ["debug", "info", "warn", "error"] }
|
|
24
|
-
as const satisfies Schema;
|
|
25
|
-
|
|
26
|
-
// types/log-level/log-level.ts
|
|
27
|
-
import { Schema } from "@adobe/data/schema";
|
|
28
|
-
import { schema } from "./schema.js";
|
|
29
|
-
export type LogLevel = Schema.ToType<typeof schema>;
|
|
30
|
-
export * as LogLevel from "./public.js";
|
|
31
|
-
|
|
32
|
-
// types/log-level/public.ts
|
|
33
|
-
export { schema } from "./schema.js";
|
|
34
|
-
export { is } from "./is.js"; // when external code narrows broader input
|
|
35
|
-
export { values } from "./values.js"; // when external code iterates the set
|
|
36
|
-
```
|
|
16
|
+
## Naming conventions
|
|
37
17
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
## Rules
|
|
42
|
-
|
|
43
|
-
- Never import `schema.ts` or `public.ts` from outside the folder.
|
|
44
|
-
- Consumers use one import: `import { LogLevel } from "./log-level.js"`.
|
|
45
|
-
The same identifier serves as type *and* namespace — a separate
|
|
46
|
-
`import type` is unnecessary and produces a duplicate-identifier error.
|
|
47
|
-
- Helper files use `import type` from the sibling `<type-name>.ts` to
|
|
48
|
-
avoid cycling through `public.ts`.
|
|
49
|
-
- Add `is` / `values` / per-member descriptors only when a consumer
|
|
50
|
-
actually needs them — not preemptively. A consumer in a not-yet-built
|
|
51
|
-
higher layer counts: a presentational descriptor the `ui/` will need
|
|
52
|
-
(per-member glyphs, a keypad `values` list) may be authored with the
|
|
53
|
-
data type, but give it a one-line doc naming the intended consumer (as
|
|
54
|
-
`player-mark/mark-color.ts` does) so the need is recorded, not guessed.
|
|
55
|
-
|
|
56
|
-
## Strict single public export per file
|
|
57
|
-
|
|
58
|
-
**One public export per file** — this is strict, not a preference. A file
|
|
59
|
-
that *declares* exports exactly one public symbol (private declarations
|
|
60
|
-
inside it are fine and are tested through the public export). The reason is
|
|
61
|
-
cohesion: when several declarations share one file, each pulls its own
|
|
62
|
-
external imports to the top and the file becomes a jumble that belongs to no
|
|
63
|
-
single concept. One declaration → one file → imports that are about that one
|
|
64
|
-
thing.
|
|
65
|
-
|
|
66
|
-
More than one public export in a file is allowed **only when explicitly
|
|
67
|
-
approved on a per-file or per-file-type basis** — the deliberate, named
|
|
68
|
-
patterns below, nothing else. There is no "shared imports" or "cohesive set"
|
|
69
|
-
escape hatch: a folder of sibling pure functions over one type (e.g. the
|
|
70
|
-
`vec3`/`vec4`/`mat4x4` math) is one function per file, collected by the
|
|
71
|
-
folder's `public.ts` barrel — dozens of one-line files is the intended shape,
|
|
72
|
-
not a reason to lump.
|
|
73
|
-
|
|
74
|
-
- **Barrels** — `index.ts` and a namespace's `public.ts` *only* re-export
|
|
75
|
-
(`export … from "./…"`); they never declare. Many re-exports is their job.
|
|
76
|
-
- **The eponymous namespace file** (`<type>.ts`) — the type alias plus
|
|
77
|
-
`export * as <Type> from "./public.js"` (a type and its namespace value).
|
|
78
|
-
- **The single-file pattern** (below) — one value export plus type-only
|
|
79
|
-
exports, for a trivial namespace not worth a folder.
|
|
80
|
-
- **`<format>-schema.ts`** — a borrowed data-format projection (plain
|
|
81
|
-
`interface`s), next to its parser/emitter.
|
|
82
|
-
- **Explicit authoring-cohesion files** — where two exports are genuinely one
|
|
83
|
-
unit to author, e.g. a presentation's `render` plus a co-located fixture
|
|
84
|
-
bundle (`unlocalized`). Rare; the pair must truly belong together.
|
|
85
|
-
|
|
86
|
-
Everything else is one-per-file. In particular, **ECS facet folders
|
|
87
|
-
(`components/`, `resources/`, `archetypes/`, `indexes/`, …) are not exempt**:
|
|
88
|
-
one component / resource / archetype / index per file, collected by the
|
|
89
|
-
folder's `index.ts` barrel. Collapsing several into one `index.ts` is exactly
|
|
90
|
-
the anti-pattern this rule exists to prevent.
|
|
91
|
-
|
|
92
|
-
Never `*-types.ts`. "Types" is a meta-word the `.ts` extension already
|
|
93
|
-
implies; the name has to predict the contents. If you can't beat
|
|
94
|
-
`-types`, the file isn't a real lump — split per type, or inline at use
|
|
95
|
-
site.
|
|
96
|
-
|
|
97
|
-
## Casing: `PascalCase` for types, `camelCase` for everything else
|
|
98
|
-
|
|
99
|
-
Capitalize a name **only** when it is a type, or a value that *represents a
|
|
100
|
-
type*. Everything else — functions, values, methods — is `camelCase`.
|
|
101
|
-
|
|
102
|
-
A `Schema` is a runtime type, so this extends to schema helpers:
|
|
103
|
-
|
|
104
|
-
- A **schema value that is a type** is `PascalCase`: the namespace schemas
|
|
105
|
-
`F32`, `Boolean`, `Guid`, `True` (each *is* a type's schema).
|
|
106
|
-
- A **higher-order schema function that returns a new type-representing
|
|
107
|
-
schema** is a runtime **type constructor** — the value-level analogue of
|
|
108
|
-
`Readonly<T>` / `Partial<T>` — and is `PascalCase`: `Nullable(schema)`,
|
|
109
|
-
`Tuple(item, n)`, `SuccessResultSchema(v)`, `ProgressiveResultSchema(v, e)`.
|
|
110
|
-
- Everything else stays `camelCase`, even when it touches schemas: functions
|
|
111
|
-
that **annotate the same type** (`withValidation`), **build from a spec**
|
|
112
|
-
(`fromArchetype`, `fromObjectProperties`, `fromStructProperties`),
|
|
113
|
-
**resolve/transform** (`getDynamicSchema`, `toVertexBufferLayout`), or hang
|
|
114
|
-
off a namespace (`Guid.create`, `FractionalIndex.between`).
|
|
115
|
-
|
|
116
|
-
The test: *does calling it yield a new runtime type?* Yes → `PascalCase`
|
|
117
|
-
constructor. No → `camelCase`.
|
|
118
|
-
|
|
119
|
-
## Domain namespaces (for types you don't own)
|
|
120
|
-
|
|
121
|
-
When utility functions operate on a platform or third-party type (e.g.
|
|
122
|
-
`GPUDevice`, `GPUTexture`), use a **domain namespace** instead: a folder
|
|
123
|
-
named after the concept, with a namespace export but *no type re-export*.
|
|
18
|
+
- Constants are **camelCase** (`flicksPerSecond`, not `FLICKS_PER_SECOND`) — strong team preference
|
|
19
|
+
- Each function/type gets its own file matching its name
|
|
124
20
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
21
|
+
## Constant types
|
|
22
|
+
|
|
23
|
+
| Visibility | Definition |
|
|
24
|
+
| ---------- | ------------------------------- |
|
|
25
|
+
| private | Single-file only, not exported |
|
|
26
|
+
| exported | Sole export from eponymous file |
|
|
27
|
+
| public | Re-exported from public.ts |
|
|
28
|
+
| internal | Exported, not in public.ts |
|
|
29
|
+
|
|
30
|
+
## Constraints
|
|
31
|
+
|
|
32
|
+
- `<type-name>/<type-name>.ts` is the only public import surface
|
|
33
|
+
- Export a single type alias: `export type <type-name> = <type>` (or `export type * from "./<type-name>-types.js"` when the type lives in a
|
|
34
|
+
separate file)
|
|
35
|
+
- Schema-based types: schema in `<type-name>-schema.ts`, named `schema` (not `<type-name>Schema`), derive with
|
|
36
|
+
`Schema.ToType<typeof schema>`
|
|
37
|
+
- Export namespace: `export * as <type-name> from "./public.js"`
|
|
38
|
+
- In public.ts, re-export every public constant file
|
|
39
|
+
- Filenames map deterministically to single export:
|
|
40
|
+
- `foo.ts` → `export type Foo = ...`
|
|
41
|
+
- `do-bar.ts` → `export function doBar() { }`
|
|
42
|
+
- DO NOT append other suffixes like `-type` etc.
|
|
43
|
+
- Function files: name by purpose only (e.g., `add.ts`, `sub.ts`). **Do not prefix** with the type name — the folder provides the namespace
|
|
44
|
+
context.
|
|
45
|
+
|
|
46
|
+
## Anti-patterns (do not copy)
|
|
47
|
+
|
|
48
|
+
- **`export namespace <TypeName> { ... }`** — Use `export * as <TypeName> from "./public.js"` instead.
|
|
49
|
+
- **Prefixing function files with the type name** (e.g., `vec2-add.ts`) — Use `add.ts`; the folder provides context.
|
|
50
|
+
- **Importing from `public.js` or `-schema.js`** outside the type folder — Consumers must import only from `<type-name>/<type-name>.ts`.
|
|
51
|
+
|
|
52
|
+
## Consumers
|
|
53
|
+
|
|
54
|
+
**Never:**
|
|
55
|
+
|
|
56
|
+
- Import from `public.js` outside the type namespace folder
|
|
57
|
+
- Import from `<type-name>-schema.js` outside the type namespace folder
|
|
58
|
+
|
|
59
|
+
**Do:**
|
|
60
|
+
|
|
61
|
+
- `import { Volume } from "../../types/volume/volume.js"`
|
|
62
|
+
- For schema: `import { PlayerMark } from "../../types/player-mark/player-mark.js"`, then `PlayerMark.schema`
|
|
63
|
+
|
|
64
|
+
## Schema pattern example
|
|
131
65
|
|
|
132
66
|
```ts
|
|
133
|
-
//
|
|
134
|
-
export
|
|
67
|
+
// src/types/player-mark/player-mark-schema.ts
|
|
68
|
+
export const schema = { enum: ['X', 'O'] } as const;
|
|
69
|
+
|
|
70
|
+
// src/types/player-mark/public.ts
|
|
71
|
+
export { schema } from './player-mark-schema.js';
|
|
135
72
|
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
73
|
+
// src/types/player-mark/player-mark.ts
|
|
74
|
+
import { Schema } from '@adobe/data/schema';
|
|
75
|
+
import { schema } from './player-mark-schema.js';
|
|
76
|
+
export type PlayerMark = Schema.ToType<typeof schema>;
|
|
77
|
+
export * as PlayerMark from './public.js';
|
|
139
78
|
```
|
|
140
79
|
|
|
141
|
-
|
|
142
|
-
|
|
80
|
+
## Example: `Point` type refactor
|
|
81
|
+
|
|
82
|
+
Starting file `src/types/point.ts`:
|
|
143
83
|
|
|
144
84
|
```ts
|
|
145
|
-
|
|
146
|
-
|
|
85
|
+
export type Point = { x: number; y: number };
|
|
86
|
+
export const length = ({ x, y }: Point) => Math.hypot(x, y);
|
|
87
|
+
export const add = ({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point): Point => ({ x: x1 + x2, y: y1 + y2 });
|
|
147
88
|
```
|
|
148
89
|
|
|
149
|
-
|
|
90
|
+
After applying the namespace pattern:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// src/types/point/point.ts
|
|
94
|
+
export type Point = { x: number; y: number };
|
|
95
|
+
export * as Point from './public.js';
|
|
96
|
+
|
|
97
|
+
// src/types/point/public.ts
|
|
98
|
+
export * from './length.js';
|
|
99
|
+
export * from './add.js';
|
|
100
|
+
|
|
101
|
+
// src/types/point/length.ts
|
|
102
|
+
export const length = ({ x, y }: Point) => Math.hypot(x, y);
|
|
103
|
+
|
|
104
|
+
// src/types/point/add.ts
|
|
105
|
+
export const add = ({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point): Point => ({ x: x1 + x2, y: y1 + y2 });
|
|
106
|
+
```
|
|
150
107
|
|
|
151
|
-
|
|
152
|
-
property accesses (`GPU.createCubemap`) and exclude unused helpers.
|
|
153
|
-
Dynamic access (`GPU[name]`) defeats this — avoid it.
|
|
108
|
+
## Execute
|
|
154
109
|
|
|
155
|
-
**
|
|
156
|
-
|
|
157
|
-
|
|
110
|
+
1. **Follow this rule** — Do not copy patterns from existing code; apply this standard.
|
|
111
|
+
2. Identify constants in the current file.
|
|
112
|
+
3. Extract them into the standard pattern.
|
|
113
|
+
4. Identify all consumers of public namespaces.
|
|
114
|
+
5. Check each consumer: ensure imports use `<type-name>.ts` only, never public.js or -schema.js.
|
|
115
|
+
6. Ensure unit tests exist per function file, splitting as needed.
|
|
158
116
|
|
|
159
117
|
## Single file pattern
|
|
160
118
|
|