@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.
Files changed (36) hide show
  1. package/docs/BACKEND_API_SPEC.md +85 -48
  2. package/docs/BUILD_LOG.md +42 -19
  3. package/docs/COMPONENT_PATTERNS.md +57 -51
  4. package/docs/CONVENTIONS.md +43 -31
  5. package/docs/FRONTEND_IMPLEMENTATION_RULES.md +57 -58
  6. package/docs/JSDOC_TYPING.md +1 -0
  7. package/docs/QUICKSTART.md +20 -18
  8. package/docs/SERVER_AND_DEPS.md +28 -29
  9. package/docs/STATE_AND_ROUTING.md +53 -52
  10. package/docs/TESTING.md +38 -37
  11. package/docs/app-spec/ENTITIES.md +16 -16
  12. package/docs/app-spec/README.md +4 -4
  13. package/lib/check.js +3 -1
  14. package/lib/package-gen.js +3 -0
  15. package/package.json +5 -2
  16. package/templates/fullstack/data/seed.json +1 -1
  17. package/templates/shared/.configs/.markdownlint.jsonc +9 -0
  18. package/templates/shared/.configs/.stylelintrc.json +16 -0
  19. package/templates/shared/.configs/jsconfig.json +2 -9
  20. package/templates/shared/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  21. package/templates/shared/.github/ISSUE_TEMPLATE/feature_request.md +1 -1
  22. package/templates/shared/.github/ISSUE_TEMPLATE/spec_correction.md +1 -1
  23. package/templates/shared/.github/pull_request_template.md +3 -0
  24. package/templates/shared/.github/workflows/spec.yml +3 -3
  25. package/templates/shared/docs/app-spec/README.md +8 -8
  26. package/templates/shared/docs/clearstack/BACKEND_API_SPEC.md +85 -48
  27. package/templates/shared/docs/clearstack/BUILD_LOG.md +42 -19
  28. package/templates/shared/docs/clearstack/COMPONENT_PATTERNS.md +57 -51
  29. package/templates/shared/docs/clearstack/CONVENTIONS.md +43 -31
  30. package/templates/shared/docs/clearstack/FRONTEND_IMPLEMENTATION_RULES.md +57 -58
  31. package/templates/shared/docs/clearstack/JSDOC_TYPING.md +1 -0
  32. package/templates/shared/docs/clearstack/QUICKSTART.md +20 -18
  33. package/templates/shared/docs/clearstack/SERVER_AND_DEPS.md +28 -29
  34. package/templates/shared/docs/clearstack/STATE_AND_ROUTING.md +53 -52
  35. package/templates/shared/docs/clearstack/TESTING.md +38 -37
  36. 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 onclick="${host => { host.open = !host.open; }}">
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 onclick="${host => {
89
- store.set(host.state, { theme: host.state.theme === 'light' ? 'dark' : 'light' });
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) => fetch(`/api/users/${id}`, {
119
- method: 'PUT',
120
- headers: { 'Content-Type': 'application/json' },
121
- body: JSON.stringify(values),
122
- }).then(r => r.json()),
123
- list: (id) => fetch(`/api/users?${new URLSearchParams(id)}`).then(r => r.json()),
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 | Purpose |
133
- |---|---|
134
- | `store(Model)` | Descriptor — binds model to a component property |
135
- | `store.get(Model, id)` | Get a cached instance (triggers fetch if needed) |
136
- | `store.set(model, values)` | Update (async, returns Promise) |
137
- | `store.sync(model, values)` | Update (sync, immediate) |
138
- | `store.pending(model)` | `false` or `Promise` while loading |
139
- | `store.ready(model)` | `true` when loaded and valid |
140
- | `store.error(model)` | `false` or `Error` |
141
- | `store.clear(Model)` | Invalidate singular model cache |
142
- | `store.clear([Model])` | Invalidate list cache — **required for list stores** |
143
- | `store.submit(draft)` | Submit draft mode changes |
144
- | `store.resolve(Model, id)` | Returns Promise that resolves when ready |
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) => store.ready(task)
168
- ? html`<span>${task.title}</span>`
169
- : html`<span class="spinner"></span>`
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 | Code |
226
- |---|---|
227
- | Navigate to view | `<a href="${router.url(View)}">` |
228
- | Navigate with params | `router.url(View, { id: '42' })` |
229
- | Back button | `<a href="${router.backUrl()}">Back</a>` |
230
- | Check active view | `router.active(View)` |
231
- | Guarded route | `guard: () => isAuthenticated()` |
232
- | Dialog overlay | `dialog: true` on the view config |
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 | Where |
257
- |---|---|
258
- | Theme, sidebar, UI flags | `AppState` (singleton) |
259
+ | Concern | Where |
260
+ | ------------------------- | ------------------------------------- |
261
+ | Theme, sidebar, UI flags | `AppState` (singleton) |
259
262
  | User records, posts, etc. | `UserModel`, `PostModel` (enumerable) |
260
- | Form draft state | `store(Model, { draft: true })` |
261
- | Route state | `router()` — managed by hybrids |
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); // full clear triggers re-fetch
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]); // one clear after the batch settles
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 | Rule |
21
- |---|---|
22
- | Test at the right level | Pure logic → unit. Components → browser. API → integration. |
23
- | No mocking the framework | Don't mock `html`, `store`, or `define`. Test through them. |
24
- | Real browser for components | Web components need a real DOM. No jsdom, no happy-dom. |
25
- | Zero build for tests | Test files are ES modules, same as app code. |
26
- | Small test files | Same 150-line rule applies to test files. |
27
- | Test behavior, not implementation | Assert what the user sees, not internal state shape. |
28
- | Co-locate tests | Tests live next to the code they test. |
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 | Tests | Runs in |
37
- |---|---|---|
38
- | `node:test` (built-in) | Server, utils, store model shapes | Node.js |
39
- | `@web/test-runner` | Components, pages, browser integration | Real Chromium |
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 | Test 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` | `UserModel.test.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 | What to assert |
90
- |---|---|
91
- | **Utils** | Input → output. Edge cases. |
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** | Renders correct HTML. Props reflect to attributes. Events fire. |
94
- | **Molecules** | Child atoms are present. Composed behavior works. |
95
- | **Organisms** | Store integration. Data flows to children. |
96
- | **Server API** | CRUD responses. Schema endpoint. Status codes. |
97
- | **Pages** | Don't unit-test pages. Test via manual or E2E if needed. |
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', () => { clicked = true; });
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 | Implement | Then test |
259
- |---|---|---|
260
- | 1. Infrastructure | server, vendor script, index.html | Server starts, routes respond, vendor files exist |
261
- | 2. Store + Utils | models, formatDate, realtimeSync | Model shapes, util outputs, localStorage round-trip |
262
- | 3. Atoms | app-button, app-badge, app-icon | Render, props, events |
263
- | 4. Molecules | task-card, project-card | Composition, slot content |
264
- | 5. Organisms | task-list, project-header | Store binding, data rendering |
265
- | 6. Pages + Router | views, app-router | Navigation, full page render |
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
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
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">
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
- <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>
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>