@techninja/clearstack 0.2.16 → 0.2.18
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/BACKEND_API_SPEC.md +85 -48
- package/docs/BUILD_LOG.md +42 -19
- package/docs/COMPONENT_PATTERNS.md +57 -51
- package/docs/CONVENTIONS.md +43 -31
- package/docs/FRONTEND_IMPLEMENTATION_RULES.md +57 -58
- package/docs/JSDOC_TYPING.md +1 -0
- package/docs/QUICKSTART.md +20 -18
- package/docs/SERVER_AND_DEPS.md +28 -29
- package/docs/STATE_AND_ROUTING.md +53 -52
- package/docs/TESTING.md +38 -37
- package/docs/app-spec/ENTITIES.md +16 -16
- package/docs/app-spec/README.md +4 -4
- package/lib/check.js +3 -1
- package/lib/package-gen.js +3 -0
- package/package.json +5 -2
- package/templates/fullstack/data/seed.json +1 -1
- package/templates/shared/.configs/.markdownlint.jsonc +9 -0
- package/templates/shared/.configs/.stylelintrc.json +16 -0
- package/templates/shared/.configs/jsconfig.json +2 -9
- package/templates/shared/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
- package/templates/shared/.github/ISSUE_TEMPLATE/feature_request.md +1 -1
- package/templates/shared/.github/ISSUE_TEMPLATE/spec_correction.md +1 -1
- package/templates/shared/.github/pull_request_template.md +3 -0
- package/templates/shared/.github/workflows/spec.yml +3 -3
- package/templates/shared/docs/app-spec/README.md +8 -8
- package/templates/shared/docs/clearstack/BACKEND_API_SPEC.md +85 -48
- package/templates/shared/docs/clearstack/BUILD_LOG.md +42 -19
- package/templates/shared/docs/clearstack/COMPONENT_PATTERNS.md +57 -51
- package/templates/shared/docs/clearstack/CONVENTIONS.md +43 -31
- package/templates/shared/docs/clearstack/FRONTEND_IMPLEMENTATION_RULES.md +57 -58
- package/templates/shared/docs/clearstack/JSDOC_TYPING.md +1 -0
- package/templates/shared/docs/clearstack/QUICKSTART.md +20 -18
- package/templates/shared/docs/clearstack/SERVER_AND_DEPS.md +28 -29
- package/templates/shared/docs/clearstack/STATE_AND_ROUTING.md +53 -52
- package/templates/shared/docs/clearstack/TESTING.md +38 -37
- package/templates/shared/src/public/index.html +23 -23
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# State & Routing
|
|
2
|
+
|
|
2
3
|
## Store, Routing, Unified App State & Realtime Sync
|
|
3
4
|
|
|
4
5
|
> How data flows through the application.
|
|
@@ -19,7 +20,11 @@ export default define({
|
|
|
19
20
|
tag: 'app-toggle',
|
|
20
21
|
open: false,
|
|
21
22
|
render: ({ open }) => html`
|
|
22
|
-
<button
|
|
23
|
+
<button
|
|
24
|
+
onclick="${(host) => {
|
|
25
|
+
host.open = !host.open;
|
|
26
|
+
}}"
|
|
27
|
+
>
|
|
23
28
|
${open ? 'Close' : 'Open'}
|
|
24
29
|
</button>
|
|
25
30
|
`,
|
|
@@ -85,9 +90,11 @@ export default define({
|
|
|
85
90
|
tag: 'theme-toggle',
|
|
86
91
|
state: store(AppState),
|
|
87
92
|
render: ({ state }) => html`
|
|
88
|
-
<button
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
<button
|
|
94
|
+
onclick="${(host) => {
|
|
95
|
+
store.set(host.state, { theme: host.state.theme === 'light' ? 'dark' : 'light' });
|
|
96
|
+
}}"
|
|
97
|
+
>
|
|
91
98
|
Theme: ${state.theme}
|
|
92
99
|
</button>
|
|
93
100
|
`,
|
|
@@ -114,13 +121,14 @@ const UserModel = {
|
|
|
114
121
|
lastName: '',
|
|
115
122
|
email: '',
|
|
116
123
|
[store.connect]: {
|
|
117
|
-
get: (id) => fetch(`/api/users/${id}`).then(r => r.json()),
|
|
118
|
-
set: (id, values) =>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
get: (id) => fetch(`/api/users/${id}`).then((r) => r.json()),
|
|
125
|
+
set: (id, values) =>
|
|
126
|
+
fetch(`/api/users/${id}`, {
|
|
127
|
+
method: 'PUT',
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
body: JSON.stringify(values),
|
|
130
|
+
}).then((r) => r.json()),
|
|
131
|
+
list: (id) => fetch(`/api/users?${new URLSearchParams(id)}`).then((r) => r.json()),
|
|
124
132
|
},
|
|
125
133
|
};
|
|
126
134
|
|
|
@@ -129,19 +137,19 @@ export default UserModel;
|
|
|
129
137
|
|
|
130
138
|
#### Store API Quick Reference
|
|
131
139
|
|
|
132
|
-
| Method
|
|
133
|
-
|
|
134
|
-
| `store(Model)`
|
|
135
|
-
| `store.get(Model, id)`
|
|
136
|
-
| `store.set(model, values)`
|
|
137
|
-
| `store.sync(model, values)` | Update (sync, immediate)
|
|
138
|
-
| `store.pending(model)`
|
|
139
|
-
| `store.ready(model)`
|
|
140
|
-
| `store.error(model)`
|
|
141
|
-
| `store.clear(Model)`
|
|
142
|
-
| `store.clear([Model])`
|
|
143
|
-
| `store.submit(draft)`
|
|
144
|
-
| `store.resolve(Model, id)`
|
|
140
|
+
| Method | Purpose |
|
|
141
|
+
| --------------------------- | ---------------------------------------------------- |
|
|
142
|
+
| `store(Model)` | Descriptor — binds model to a component property |
|
|
143
|
+
| `store.get(Model, id)` | Get a cached instance (triggers fetch if needed) |
|
|
144
|
+
| `store.set(model, values)` | Update (async, returns Promise) |
|
|
145
|
+
| `store.sync(model, values)` | Update (sync, immediate) |
|
|
146
|
+
| `store.pending(model)` | `false` or `Promise` while loading |
|
|
147
|
+
| `store.ready(model)` | `true` when loaded and valid |
|
|
148
|
+
| `store.error(model)` | `false` or `Error` |
|
|
149
|
+
| `store.clear(Model)` | Invalidate singular model cache |
|
|
150
|
+
| `store.clear([Model])` | Invalidate list cache — **required for list stores** |
|
|
151
|
+
| `store.submit(draft)` | Submit draft mode changes |
|
|
152
|
+
| `store.resolve(Model, id)` | Returns Promise that resolves when ready |
|
|
145
153
|
|
|
146
154
|
#### Decision Tree: Local vs Shared State
|
|
147
155
|
|
|
@@ -161,13 +169,12 @@ clear triggers re-fetch). Always guard property access on list items:
|
|
|
161
169
|
|
|
162
170
|
```javascript
|
|
163
171
|
// ❌ BAD — task may be pending, accessing .title throws
|
|
164
|
-
tasks.map((task) => html`<span>${task.title}</span>`)
|
|
172
|
+
tasks.map((task) => html`<span>${task.title}</span>`);
|
|
165
173
|
|
|
166
174
|
// ✅ GOOD — guard each item, show fallback for pending items
|
|
167
|
-
tasks.map((task) =>
|
|
168
|
-
? html`<span>${task.title}</span>`
|
|
169
|
-
|
|
170
|
-
)
|
|
175
|
+
tasks.map((task) =>
|
|
176
|
+
store.ready(task) ? html`<span>${task.title}</span>` : html`<span class="spinner"></span>`,
|
|
177
|
+
);
|
|
171
178
|
```
|
|
172
179
|
|
|
173
180
|
This is especially important after batch operations (e.g. drag reorder)
|
|
@@ -189,11 +196,7 @@ import HomeView from '../pages/home/index.js';
|
|
|
189
196
|
export default define({
|
|
190
197
|
tag: 'app-router',
|
|
191
198
|
stack: router(HomeView, { url: '/' }),
|
|
192
|
-
render: ({ stack }) => html`
|
|
193
|
-
<template layout="column height::100vh">
|
|
194
|
-
${stack}
|
|
195
|
-
</template>
|
|
196
|
-
`,
|
|
199
|
+
render: ({ stack }) => html` <template layout="column height::100vh"> ${stack} </template> `,
|
|
197
200
|
});
|
|
198
201
|
```
|
|
199
202
|
|
|
@@ -222,14 +225,14 @@ export default define({
|
|
|
222
225
|
|
|
223
226
|
### Routing Patterns
|
|
224
227
|
|
|
225
|
-
| Pattern
|
|
226
|
-
|
|
227
|
-
| Navigate to view
|
|
228
|
-
| Navigate with params | `router.url(View, { id: '42' })`
|
|
229
|
-
| Back button
|
|
230
|
-
| Check active view
|
|
231
|
-
| Guarded route
|
|
232
|
-
| Dialog overlay
|
|
228
|
+
| Pattern | Code |
|
|
229
|
+
| -------------------- | ---------------------------------------- |
|
|
230
|
+
| Navigate to view | `<a href="${router.url(View)}">` |
|
|
231
|
+
| Navigate with params | `router.url(View, { id: '42' })` |
|
|
232
|
+
| Back button | `<a href="${router.backUrl()}">Back</a>` |
|
|
233
|
+
| Check active view | `router.active(View)` |
|
|
234
|
+
| Guarded route | `guard: () => isAuthenticated()` |
|
|
235
|
+
| Dialog overlay | `dialog: true` on the view config |
|
|
233
236
|
|
|
234
237
|
---
|
|
235
238
|
|
|
@@ -253,12 +256,12 @@ DOM
|
|
|
253
256
|
|
|
254
257
|
### AppState vs Entity Models
|
|
255
258
|
|
|
256
|
-
| Concern
|
|
257
|
-
|
|
258
|
-
| Theme, sidebar, UI flags
|
|
259
|
+
| Concern | Where |
|
|
260
|
+
| ------------------------- | ------------------------------------- |
|
|
261
|
+
| Theme, sidebar, UI flags | `AppState` (singleton) |
|
|
259
262
|
| User records, posts, etc. | `UserModel`, `PostModel` (enumerable) |
|
|
260
|
-
| Form draft state
|
|
261
|
-
| Route state
|
|
263
|
+
| Form draft state | `store(Model, { draft: true })` |
|
|
264
|
+
| Route state | `router()` — managed by hybrids |
|
|
262
265
|
|
|
263
266
|
---
|
|
264
267
|
|
|
@@ -280,7 +283,7 @@ export function connectRealtime(url, modelMap) {
|
|
|
280
283
|
source.addEventListener('update', (event) => {
|
|
281
284
|
const { type } = JSON.parse(event.data);
|
|
282
285
|
const Model = modelMap[type];
|
|
283
|
-
if (Model) store.clear(Model);
|
|
286
|
+
if (Model) store.clear(Model); // full clear triggers re-fetch
|
|
284
287
|
});
|
|
285
288
|
|
|
286
289
|
source.addEventListener('error', () => {
|
|
@@ -326,9 +329,7 @@ export default define({
|
|
|
326
329
|
return disconnect;
|
|
327
330
|
},
|
|
328
331
|
},
|
|
329
|
-
render: ({ stack }) => html`
|
|
330
|
-
<template layout="column height::100vh">${stack}</template>
|
|
331
|
-
`,
|
|
332
|
+
render: ({ stack }) => html` <template layout="column height::100vh">${stack}</template> `,
|
|
332
333
|
});
|
|
333
334
|
```
|
|
334
335
|
|
|
@@ -354,7 +355,7 @@ source.addEventListener('update', (event) => {
|
|
|
354
355
|
const { type } = JSON.parse(event.data);
|
|
355
356
|
clearTimeout(timers[type]);
|
|
356
357
|
timers[type] = setTimeout(() => {
|
|
357
|
-
store.clear([Model]);
|
|
358
|
+
store.clear([Model]); // one clear after the batch settles
|
|
358
359
|
}, 300);
|
|
359
360
|
});
|
|
360
361
|
```
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Testing
|
|
2
|
+
|
|
2
3
|
## Philosophy, Tools & Patterns
|
|
3
4
|
|
|
4
5
|
> How we test in a no-build web component project.
|
|
@@ -17,15 +18,15 @@ bugs at the boundary where they're introduced, not 5 layers up.
|
|
|
17
18
|
|
|
18
19
|
### Core Principles
|
|
19
20
|
|
|
20
|
-
| Principle
|
|
21
|
-
|
|
22
|
-
| Test at the right level
|
|
23
|
-
| No mocking the framework
|
|
24
|
-
| Real browser for components
|
|
25
|
-
| Zero build for tests
|
|
26
|
-
| Small test files
|
|
27
|
-
| Test behavior, not implementation | Assert what the user sees, not internal state shape.
|
|
28
|
-
| Co-locate tests
|
|
21
|
+
| Principle | Rule |
|
|
22
|
+
| --------------------------------- | ----------------------------------------------------------- |
|
|
23
|
+
| Test at the right level | Pure logic → unit. Components → browser. API → integration. |
|
|
24
|
+
| No mocking the framework | Don't mock `html`, `store`, or `define`. Test through them. |
|
|
25
|
+
| Real browser for components | Web components need a real DOM. No jsdom, no happy-dom. |
|
|
26
|
+
| Zero build for tests | Test files are ES modules, same as app code. |
|
|
27
|
+
| Small test files | Same 150-line rule applies to test files. |
|
|
28
|
+
| Test behavior, not implementation | Assert what the user sees, not internal state shape. |
|
|
29
|
+
| Co-locate tests | Tests live next to the code they test. |
|
|
29
30
|
|
|
30
31
|
---
|
|
31
32
|
|
|
@@ -33,10 +34,10 @@ bugs at the boundary where they're introduced, not 5 layers up.
|
|
|
33
34
|
|
|
34
35
|
### Two Test Runners, Clear Boundaries
|
|
35
36
|
|
|
36
|
-
| Tool
|
|
37
|
-
|
|
38
|
-
| `node:test` (built-in) | Server, utils, store model shapes
|
|
39
|
-
| `@web/test-runner`
|
|
37
|
+
| Tool | Tests | Runs in |
|
|
38
|
+
| ---------------------- | -------------------------------------- | ------------- |
|
|
39
|
+
| `node:test` (built-in) | Server, utils, store model shapes | Node.js |
|
|
40
|
+
| `@web/test-runner` | Components, pages, browser integration | Real Chromium |
|
|
40
41
|
|
|
41
42
|
No other test frameworks. No Jest, no Mocha, no Jasmine.
|
|
42
43
|
|
|
@@ -77,24 +78,24 @@ server.test.js ← node:test
|
|
|
77
78
|
|
|
78
79
|
### Test File Naming
|
|
79
80
|
|
|
80
|
-
| Code file
|
|
81
|
-
|
|
81
|
+
| Code file | Test file |
|
|
82
|
+
| --------------- | -------------------- |
|
|
82
83
|
| `app-button.js` | `app-button.test.js` |
|
|
83
84
|
| `formatDate.js` | `formatDate.test.js` |
|
|
84
|
-
| `UserModel.js`
|
|
85
|
-
| `src/server.js` | `server.test.js`
|
|
85
|
+
| `UserModel.js` | `UserModel.test.js` |
|
|
86
|
+
| `src/server.js` | `server.test.js` |
|
|
86
87
|
|
|
87
88
|
### What Gets Tested
|
|
88
89
|
|
|
89
|
-
| Layer
|
|
90
|
-
|
|
91
|
-
| **Utils**
|
|
90
|
+
| Layer | What to assert |
|
|
91
|
+
| ---------------- | ------------------------------------------------------------------------- |
|
|
92
|
+
| **Utils** | Input → output. Edge cases. |
|
|
92
93
|
| **Store models** | Shape is correct. Computed fields work. Storage connector URLs are right. |
|
|
93
|
-
| **Atoms**
|
|
94
|
-
| **Molecules**
|
|
95
|
-
| **Organisms**
|
|
96
|
-
| **Server API**
|
|
97
|
-
| **Pages**
|
|
94
|
+
| **Atoms** | Renders correct HTML. Props reflect to attributes. Events fire. |
|
|
95
|
+
| **Molecules** | Child atoms are present. Composed behavior works. |
|
|
96
|
+
| **Organisms** | Store integration. Data flows to children. |
|
|
97
|
+
| **Server API** | CRUD responses. Schema endpoint. Status codes. |
|
|
98
|
+
| **Pages** | Don't unit-test pages. Test via manual or E2E if needed. |
|
|
98
99
|
|
|
99
100
|
---
|
|
100
101
|
|
|
@@ -191,7 +192,9 @@ describe('app-button', () => {
|
|
|
191
192
|
it('dispatches click event', async () => {
|
|
192
193
|
const el = await fixture(html`<app-button label="Go"></app-button>`);
|
|
193
194
|
let clicked = false;
|
|
194
|
-
el.addEventListener('click', () => {
|
|
195
|
+
el.addEventListener('click', () => {
|
|
196
|
+
clicked = true;
|
|
197
|
+
});
|
|
195
198
|
el.shadowRoot.querySelector('button').click();
|
|
196
199
|
expect(clicked).to.be.true;
|
|
197
200
|
});
|
|
@@ -230,9 +233,7 @@ import { playwrightLauncher } from '@web/test-runner-playwright';
|
|
|
230
233
|
export default {
|
|
231
234
|
files: 'src/components/**/*.test.js',
|
|
232
235
|
nodeResolve: true,
|
|
233
|
-
browsers: [
|
|
234
|
-
playwrightLauncher({ product: 'chromium' }),
|
|
235
|
-
],
|
|
236
|
+
browsers: [playwrightLauncher({ product: 'chromium' })],
|
|
236
237
|
};
|
|
237
238
|
```
|
|
238
239
|
|
|
@@ -255,14 +256,14 @@ export default {
|
|
|
255
256
|
|
|
256
257
|
Each implementation phase must pass its tests before proceeding:
|
|
257
258
|
|
|
258
|
-
| Phase
|
|
259
|
-
|
|
260
|
-
| 1. Infrastructure | server, vendor script, index.html | Server starts, routes respond, vendor files exist
|
|
261
|
-
| 2. Store + Utils
|
|
262
|
-
| 3. Atoms
|
|
263
|
-
| 4. Molecules
|
|
264
|
-
| 5. Organisms
|
|
265
|
-
| 6. Pages + Router | views, app-router
|
|
259
|
+
| Phase | Implement | Then test |
|
|
260
|
+
| ----------------- | --------------------------------- | --------------------------------------------------- |
|
|
261
|
+
| 1. Infrastructure | server, vendor script, index.html | Server starts, routes respond, vendor files exist |
|
|
262
|
+
| 2. Store + Utils | models, formatDate, realtimeSync | Model shapes, util outputs, localStorage round-trip |
|
|
263
|
+
| 3. Atoms | app-button, app-badge, app-icon | Render, props, events |
|
|
264
|
+
| 4. Molecules | task-card, project-card | Composition, slot content |
|
|
265
|
+
| 5. Organisms | task-list, project-header | Store binding, data rendering |
|
|
266
|
+
| 6. Pages + Router | views, app-router | Navigation, full page render |
|
|
266
267
|
|
|
267
268
|
**Rule: never skip a checkpoint.** If phase 3 tests fail, fix before
|
|
268
269
|
starting phase 4. Bugs compound; catch them at the boundary.
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{name}}</title>
|
|
7
|
+
<link rel="stylesheet" href="/styles/reset.css" />
|
|
8
|
+
<link rel="stylesheet" href="/styles/tokens.css" />
|
|
9
|
+
<link rel="stylesheet" href="/styles/shared.css" />
|
|
10
|
+
<link rel="stylesheet" href="/styles/buttons.css" />
|
|
11
|
+
<link rel="stylesheet" href="/styles/forms.css" />
|
|
12
|
+
<link rel="stylesheet" href="/styles/components.css" />
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
</head>
|
|
22
|
-
<body>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
</body>
|
|
14
|
+
<script type="importmap">
|
|
15
|
+
{
|
|
16
|
+
"imports": {
|
|
17
|
+
"hybrids": "/public/vendor/hybrids/index.js"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<app-router></app-router>
|
|
24
|
+
<script type="module" src="/router/index.js"></script>
|
|
25
|
+
</body>
|
|
26
26
|
</html>
|