@techninja/clearstack 0.3.13 → 0.3.14

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/docs/BUILD_LOG.md CHANGED
@@ -168,6 +168,42 @@ These are the significant corrections:
168
168
  - **Actual:** Coordinates calculated from SVG rect, not accounting for pan
169
169
  - **Fix:** Shared `canvasPos()` utility subtracts pan offset from all tools
170
170
 
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
+
171
207
  ---
172
208
 
173
209
  ## Metrics
@@ -212,6 +212,29 @@ Pages → Templates → Organisms → Molecules → Atoms
212
212
  Store / Utils
213
213
  ```
214
214
 
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
+
215
238
  - Atoms import **nothing** from other component tiers.
216
239
  - Molecules import only from **atoms**.
217
240
  - Organisms import from **molecules** and **atoms**.
@@ -76,6 +76,73 @@ function handleClick(host, event) {
76
76
  }
77
77
  ```
78
78
 
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
+
79
146
  ## Rules
80
147
 
81
148
  - Every exported component gets a `@typedef` for its host interface.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {
@@ -168,6 +168,42 @@ These are the significant corrections:
168
168
  - **Actual:** Coordinates calculated from SVG rect, not accounting for pan
169
169
  - **Fix:** Shared `canvasPos()` utility subtracts pan offset from all tools
170
170
 
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
+
171
207
  ---
172
208
 
173
209
  ## Metrics
@@ -212,6 +212,29 @@ Pages → Templates → Organisms → Molecules → Atoms
212
212
  Store / Utils
213
213
  ```
214
214
 
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
+
215
238
  - Atoms import **nothing** from other component tiers.
216
239
  - Molecules import only from **atoms**.
217
240
  - Organisms import from **molecules** and **atoms**.
@@ -76,6 +76,73 @@ function handleClick(host, event) {
76
76
  }
77
77
  ```
78
78
 
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
+
79
146
  ## Rules
80
147
 
81
148
  - Every exported component gets a `@typedef` for its host interface.