Skip to content

Commit cea053f

Browse files
committed
Add findings from StatiCart
# Conflicts: # CHANGELOG.md
1 parent 2d86f46 commit cea053f

8 files changed

Lines changed: 257 additions & 2 deletions

File tree

‎.vscode/settings.json‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
"activityBar.foreground": "#c6f2f1",
55
"activityBar.inactiveForeground": "#4a8ab5",
66
"activityBarBadge.background": "#00c0e0",
7-
"activityBarBadge.foreground": "#002040"
7+
"activityBarBadge.foreground": "#002040",
8+
"titleBar.activeBackground": "#063466",
9+
"titleBar.activeForeground": "#c6f2f1",
10+
"titleBar.inactiveBackground": "#042a52",
11+
"titleBar.inactiveForeground": "#4a8ab5"
812
}
913
}

‎CHANGELOG.md‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1111
- Spec linting
1212
- Allow passing --static for serverless builds
1313

14-
1514
## [0.3.12] - 2026-04-07
1615

1716
- Fix ordering of spec check to account for auto lint fixes Also add convention around splitting vs compressing for big files

‎docs/BUILD_LOG.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,42 @@ These are the significant corrections:
168168
- **Actual:** Coordinates calculated from SVG rect, not accounting for pan
169169
- **Fix:** Shared `canvasPos()` utility subtracts pan offset from all tools
170170

171+
### Enumerable models (`id: true`) break tsc
172+
173+
- **Expected:** `@type {import('hybrids').Model<Product>}` works with `id: true`
174+
- **Actual:** tsc rejects `id: true` — it's not in the Model type definition
175+
- **Fix:** Type the model as `@type {any}` with a comment explaining the cast
176+
- **Documented in:** JSDOC_TYPING.md → Enumerable Models Need `@type {any}`
177+
178+
### Empty arrays in store models throw at runtime
179+
180+
- **Expected:** `items: []` on a singleton model works as an empty default
181+
- **Actual:** Hybrids throws `The first item of the 'items' array must be defined`
182+
- **Why:** Hybrids infers array item type from the first element at model setup
183+
- **Fix:** Provide a prototype item: `items: [{ sku: '', quantity: 0 }]`
184+
- **Documented in:** STATE_AND_ROUTING.md → Store Array Properties
185+
186+
### List store descriptors don't match array types in tsc
187+
188+
- **Expected:** `store([Model])` assignable to an array-typed host property
189+
- **Actual:** tsc sees `EnumerableInstance`, not `any[]` — three separate casts needed
190+
- **Fix:** Cast the descriptor, `store.ready()` call, and the array before `.map()`
191+
- **Documented in:** JSDOC_TYPING.md → List Store Properties Need Casts
192+
193+
### Organisms importing pages creates circular dependencies
194+
195+
- **Expected:** `router.url(PageView)` works from an organism for link generation
196+
- **Actual:** Page imports organism, organism imports page → circular
197+
- **Fix:** Use string URLs (`/product/${sku}`) in organisms instead of `router.url()`
198+
- **Documented in:** FRONTEND_IMPLEMENTATION_RULES.md → Organisms Must Not Import Pages
199+
200+
### `list` connector params need cast for custom filter properties
201+
202+
- **Expected:** `list: async ({ category }) => ...` destructures cleanly
203+
- **Actual:** Param is `ModelIdentifier`, tsc rejects `.category` access
204+
- **Fix:** Cast params to `any` before accessing custom properties
205+
- **Documented in:** JSDOC_TYPING.md → `list` Connector Params
206+
171207
---
172208

173209
## Metrics

‎docs/FRONTEND_IMPLEMENTATION_RULES.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,29 @@ Pages → Templates → Organisms → Molecules → Atoms
212212
Store / Utils
213213
```
214214

215+
#### Organisms Must Not Import Pages
216+
217+
Organisms sit below pages in the import hierarchy. If an organism needs
218+
to generate a URL to a page (e.g. a product grid linking to a product
219+
detail page), **use a string URL, not `router.url(PageView)`**.
220+
221+
`router.url()` requires importing the page component, which creates a
222+
circular dependency: page → organism → page. Even if the bundler or
223+
browser resolves it, it makes the dependency graph untraceable.
224+
225+
```javascript
226+
// ❌ BAD — organism imports a page, circular dependency
227+
import ProductDetailView from '#pages/product-detail/product-detail-view.js';
228+
html`<a href="${router.url(ProductDetailView, { sku })}">View</a>`;
229+
230+
// ✅ GOOD — string URL, no import needed
231+
html`<a href="${`/product/${sku}`}">View</a>`;
232+
```
233+
234+
If the URL pattern changes, update it in one place. For complex URL
235+
generation, extract a `buildUrl(view, params)` utility in `src/utils/`
236+
that returns strings — no component imports.
237+
215238
- Atoms import **nothing** from other component tiers.
216239
- Molecules import only from **atoms**.
217240
- Organisms import from **molecules** and **atoms**.

‎docs/JSDOC_TYPING.md‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,73 @@ function handleClick(host, event) {
7676
}
7777
```
7878

79+
## Enumerable Models Need `@type {any}`
80+
81+
Hybrids' TypeScript definitions don't support `id: true` on `Model<T>` —
82+
`tsc` rejects it because `id` isn't in the typedef. The fix is to type
83+
the entire model as `any` with a comment explaining why:
84+
85+
```javascript
86+
/** @type {any} — hybrids Model with id:true; cast to bypass tsc limitations */
87+
const Product = {
88+
id: true,
89+
name: '',
90+
// ...
91+
[store.connect]: { get: ..., list: ... },
92+
};
93+
```
94+
95+
Do **not** add `id` to the `@typedef` — it's a hybrids directive, not a
96+
data field. The `@typedef` should describe the shape of the data.
97+
98+
## List Store Properties Need Casts
99+
100+
`store([Model])` returns a descriptor that `tsc` can't reconcile with
101+
array-typed host properties. Both the descriptor and `store.ready()` calls
102+
need `any` casts:
103+
104+
```javascript
105+
/**
106+
* @typedef {Object} MyGridHost
107+
* @property {any} items — list store, cast for tsc
108+
*/
109+
110+
/** @type {import('hybrids').Component<MyGridHost>} */
111+
export default define({
112+
tag: 'my-grid',
113+
items: /** @type {any} */ (store([Product], { id: () => ({}) })),
114+
render: {
115+
value: ({ items }) => html`
116+
${
117+
/** @type {any} */ (store).ready(items)
118+
? /** @type {any[]} */ (items).map((i) => html`<span>${i.name}</span>`)
119+
: html`<p>Loading…</p>`
120+
}
121+
`,
122+
shadow: false,
123+
},
124+
});
125+
```
126+
127+
Three casts are needed:
128+
129+
1. `/** @type {any} */` on the `store([Model])` descriptor assignment
130+
2. `/** @type {any} */ (store).ready(items)` — `store.ready()` overloads
131+
reject array-typed arguments
132+
3. `/** @type {any[]} */ (items)` before calling `.map()` / `.filter()`
133+
134+
## `list` Connector Params Are `ModelIdentifier`
135+
136+
The `list` connector receives a `ModelIdentifier`, not a plain object.
137+
Accessing custom filter properties (like `.category`) requires a cast:
138+
139+
```javascript
140+
list: async (params) => {
141+
const category = /** @type {any} */ (params)?.category;
142+
// ...
143+
},
144+
```
145+
79146
## Rules
80147
81148
- Every exported component gets a `@typedef` for its host interface.

‎templates/shared/docs/clearstack/BUILD_LOG.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,42 @@ These are the significant corrections:
168168
- **Actual:** Coordinates calculated from SVG rect, not accounting for pan
169169
- **Fix:** Shared `canvasPos()` utility subtracts pan offset from all tools
170170

171+
### Enumerable models (`id: true`) break tsc
172+
173+
- **Expected:** `@type {import('hybrids').Model<Product>}` works with `id: true`
174+
- **Actual:** tsc rejects `id: true` — it's not in the Model type definition
175+
- **Fix:** Type the model as `@type {any}` with a comment explaining the cast
176+
- **Documented in:** JSDOC_TYPING.md → Enumerable Models Need `@type {any}`
177+
178+
### Empty arrays in store models throw at runtime
179+
180+
- **Expected:** `items: []` on a singleton model works as an empty default
181+
- **Actual:** Hybrids throws `The first item of the 'items' array must be defined`
182+
- **Why:** Hybrids infers array item type from the first element at model setup
183+
- **Fix:** Provide a prototype item: `items: [{ sku: '', quantity: 0 }]`
184+
- **Documented in:** STATE_AND_ROUTING.md → Store Array Properties
185+
186+
### List store descriptors don't match array types in tsc
187+
188+
- **Expected:** `store([Model])` assignable to an array-typed host property
189+
- **Actual:** tsc sees `EnumerableInstance`, not `any[]` — three separate casts needed
190+
- **Fix:** Cast the descriptor, `store.ready()` call, and the array before `.map()`
191+
- **Documented in:** JSDOC_TYPING.md → List Store Properties Need Casts
192+
193+
### Organisms importing pages creates circular dependencies
194+
195+
- **Expected:** `router.url(PageView)` works from an organism for link generation
196+
- **Actual:** Page imports organism, organism imports page → circular
197+
- **Fix:** Use string URLs (`/product/${sku}`) in organisms instead of `router.url()`
198+
- **Documented in:** FRONTEND_IMPLEMENTATION_RULES.md → Organisms Must Not Import Pages
199+
200+
### `list` connector params need cast for custom filter properties
201+
202+
- **Expected:** `list: async ({ category }) => ...` destructures cleanly
203+
- **Actual:** Param is `ModelIdentifier`, tsc rejects `.category` access
204+
- **Fix:** Cast params to `any` before accessing custom properties
205+
- **Documented in:** JSDOC_TYPING.md → `list` Connector Params
206+
171207
---
172208

173209
## Metrics

‎templates/shared/docs/clearstack/FRONTEND_IMPLEMENTATION_RULES.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,29 @@ Pages → Templates → Organisms → Molecules → Atoms
212212
Store / Utils
213213
```
214214

215+
#### Organisms Must Not Import Pages
216+
217+
Organisms sit below pages in the import hierarchy. If an organism needs
218+
to generate a URL to a page (e.g. a product grid linking to a product
219+
detail page), **use a string URL, not `router.url(PageView)`**.
220+
221+
`router.url()` requires importing the page component, which creates a
222+
circular dependency: page → organism → page. Even if the bundler or
223+
browser resolves it, it makes the dependency graph untraceable.
224+
225+
```javascript
226+
// ❌ BAD — organism imports a page, circular dependency
227+
import ProductDetailView from '#pages/product-detail/product-detail-view.js';
228+
html`<a href="${router.url(ProductDetailView, { sku })}">View</a>`;
229+
230+
// ✅ GOOD — string URL, no import needed
231+
html`<a href="${`/product/${sku}`}">View</a>`;
232+
```
233+
234+
If the URL pattern changes, update it in one place. For complex URL
235+
generation, extract a `buildUrl(view, params)` utility in `src/utils/`
236+
that returns strings — no component imports.
237+
215238
- Atoms import **nothing** from other component tiers.
216239
- Molecules import only from **atoms**.
217240
- Organisms import from **molecules** and **atoms**.

‎templates/shared/docs/clearstack/JSDOC_TYPING.md‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,73 @@ function handleClick(host, event) {
7676
}
7777
```
7878

79+
## Enumerable Models Need `@type {any}`
80+
81+
Hybrids' TypeScript definitions don't support `id: true` on `Model<T>` —
82+
`tsc` rejects it because `id` isn't in the typedef. The fix is to type
83+
the entire model as `any` with a comment explaining why:
84+
85+
```javascript
86+
/** @type {any} — hybrids Model with id:true; cast to bypass tsc limitations */
87+
const Product = {
88+
id: true,
89+
name: '',
90+
// ...
91+
[store.connect]: { get: ..., list: ... },
92+
};
93+
```
94+
95+
Do **not** add `id` to the `@typedef` — it's a hybrids directive, not a
96+
data field. The `@typedef` should describe the shape of the data.
97+
98+
## List Store Properties Need Casts
99+
100+
`store([Model])` returns a descriptor that `tsc` can't reconcile with
101+
array-typed host properties. Both the descriptor and `store.ready()` calls
102+
need `any` casts:
103+
104+
```javascript
105+
/**
106+
* @typedef {Object} MyGridHost
107+
* @property {any} items — list store, cast for tsc
108+
*/
109+
110+
/** @type {import('hybrids').Component<MyGridHost>} */
111+
export default define({
112+
tag: 'my-grid',
113+
items: /** @type {any} */ (store([Product], { id: () => ({}) })),
114+
render: {
115+
value: ({ items }) => html`
116+
${
117+
/** @type {any} */ (store).ready(items)
118+
? /** @type {any[]} */ (items).map((i) => html`<span>${i.name}</span>`)
119+
: html`<p>Loading…</p>`
120+
}
121+
`,
122+
shadow: false,
123+
},
124+
});
125+
```
126+
127+
Three casts are needed:
128+
129+
1. `/** @type {any} */` on the `store([Model])` descriptor assignment
130+
2. `/** @type {any} */ (store).ready(items)` — `store.ready()` overloads
131+
reject array-typed arguments
132+
3. `/** @type {any[]} */ (items)` before calling `.map()` / `.filter()`
133+
134+
## `list` Connector Params Are `ModelIdentifier`
135+
136+
The `list` connector receives a `ModelIdentifier`, not a plain object.
137+
Accessing custom filter properties (like `.category`) requires a cast:
138+
139+
```javascript
140+
list: async (params) => {
141+
const category = /** @type {any} */ (params)?.category;
142+
// ...
143+
},
144+
```
145+
79146
## Rules
80147
81148
- Every exported component gets a `@typedef` for its host interface.

0 commit comments

Comments
 (0)