@techninja/clearstack 0.2.8 → 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
  # Backend API Specification
2
+
2
3
  ## REST Endpoints, JSON Schema Discovery & Realtime Sync
3
4
 
4
5
  > Defines the server-side data contract. The frontend consumes this API via
@@ -70,25 +71,25 @@ The `Allow` header is also set for HTTP compliance.
70
71
  The frontend can fetch the schema at runtime and generate form fields
71
72
  automatically. The schema provides:
72
73
 
73
- | JSON Schema keyword | Form behavior |
74
- |---|---|
75
- | `type: "string"` | `<input type="text">` |
76
- | `format: "email"` | `<input type="email">` |
77
- | `format: "date-time"` | `<input type="datetime-local">` |
78
- | `enum: [...]` | `<select>` with options |
79
- | `minLength` / `maxLength` | Validation constraints |
80
- | `readOnly: true` | Field displayed but not editable |
81
- | `required: [...]` | Native `required` attribute + visual indicator |
74
+ | JSON Schema keyword | Form behavior |
75
+ | ------------------------- | ---------------------------------------------- |
76
+ | `type: "string"` | `<input type="text">` |
77
+ | `format: "email"` | `<input type="email">` |
78
+ | `format: "date-time"` | `<input type="datetime-local">` |
79
+ | `enum: [...]` | `<select>` with options |
80
+ | `minLength` / `maxLength` | Validation constraints |
81
+ | `readOnly: true` | Field displayed but not editable |
82
+ | `required: [...]` | Native `required` attribute + visual indicator |
82
83
 
83
84
  JSON Schema constraints map directly to HTML validation attributes:
84
85
 
85
86
  | JSON Schema | HTML attribute |
86
- |---|---|
87
- | `minLength` | `minlength` |
88
- | `maxLength` | `maxlength` |
89
- | `minimum` | `min` |
90
- | `maximum` | `max` |
91
- | `pattern` | `pattern` |
87
+ | ----------- | -------------- |
88
+ | `minLength` | `minlength` |
89
+ | `maxLength` | `maxlength` |
90
+ | `minimum` | `min` |
91
+ | `maximum` | `max` |
92
+ | `pattern` | `pattern` |
92
93
 
93
94
  The browser's native constraint validation API enforces these — no custom
94
95
  JS validation needed for client-side checks.
@@ -115,13 +116,13 @@ The `schema-form` component maps `fields` entries to the corresponding
115
116
 
116
117
  ### Error Response Contract
117
118
 
118
- | Status | Body | Meaning |
119
- |---|---|---|
120
- | `422` | `{ error, fields }` | Validation failed — `fields` maps names to messages |
121
- | `404` | `{ error }` | Entity or collection not found |
122
- | `201` | Entity JSON | Created successfully |
123
- | `200` | Entity JSON | Updated successfully |
124
- | `204` | Empty | Deleted successfully |
119
+ | Status | Body | Meaning |
120
+ | ------ | ------------------- | --------------------------------------------------- |
121
+ | `422` | `{ error, fields }` | Validation failed — `fields` maps names to messages |
122
+ | `404` | `{ error }` | Entity or collection not found |
123
+ | `201` | Entity JSON | Created successfully |
124
+ | `200` | Entity JSON | Updated successfully |
125
+ | `204` | Empty | Deleted successfully |
125
126
 
126
127
  This allows a single generic `schema-form` component to render any entity's
127
128
  create/edit form without entity-specific template code.
@@ -134,12 +135,12 @@ create/edit form without entity-specific template code.
134
135
 
135
136
  Query params for filtering, sorting, pagination:
136
137
 
137
- | Param | Example | Purpose |
138
- |---|---|---|
139
- | `limit` | `?limit=20` | Page size (default: 20) |
140
- | `offset` | `?offset=40` | Skip N records |
141
- | `sort` | `?sort=-createdAt` | Sort field, `-` prefix for desc |
142
- | `filter` | `?role=admin` | Field equality filter |
138
+ | Param | Example | Purpose |
139
+ | -------- | ------------------ | ------------------------------- |
140
+ | `limit` | `?limit=20` | Page size (default: 20) |
141
+ | `offset` | `?offset=40` | Skip N records |
142
+ | `sort` | `?sort=-createdAt` | Sort field, `-` prefix for desc |
143
+ | `filter` | `?role=admin` | Field equality filter |
143
144
 
144
145
  Response:
145
146
 
@@ -200,11 +201,11 @@ event: update
200
201
  data: {"type":"user","id":"2","action":"deleted"}
201
202
  ```
202
203
 
203
- | Field | Value |
204
- |---|---|
205
- | `type` | Entity name (lowercase, singular): `user`, `post` |
206
- | `id` | Entity ID that changed |
207
- | `action` | `created`, `updated`, or `deleted` |
204
+ | Field | Value |
205
+ | -------- | ------------------------------------------------- |
206
+ | `type` | Entity name (lowercase, singular): `user`, `post` |
207
+ | `id` | Entity ID that changed |
208
+ | `action` | `created`, `updated`, or `deleted` |
208
209
 
209
210
  ### Frontend Integration
210
211
 
@@ -226,11 +227,44 @@ required.
226
227
  const db = new Map();
227
228
 
228
229
  // Seed with dummy users
229
- db.set('users', new Map([
230
- ['1', { id: '1', firstName: 'Jane', lastName: 'Doe', email: 'jane@example.com', role: 'admin', createdAt: '2024-01-15T09:00:00Z' }],
231
- ['2', { id: '2', firstName: 'John', lastName: 'Smith', email: 'john@example.com', role: 'editor', createdAt: '2024-02-20T14:30:00Z' }],
232
- ['3', { id: '3', firstName: 'Alex', lastName: 'Chen', email: 'alex@example.com', role: 'viewer', createdAt: '2024-03-10T11:15:00Z' }],
233
- ]));
230
+ db.set(
231
+ 'users',
232
+ new Map([
233
+ [
234
+ '1',
235
+ {
236
+ id: '1',
237
+ firstName: 'Jane',
238
+ lastName: 'Doe',
239
+ email: 'jane@example.com',
240
+ role: 'admin',
241
+ createdAt: '2024-01-15T09:00:00Z',
242
+ },
243
+ ],
244
+ [
245
+ '2',
246
+ {
247
+ id: '2',
248
+ firstName: 'John',
249
+ lastName: 'Smith',
250
+ email: 'john@example.com',
251
+ role: 'editor',
252
+ createdAt: '2024-02-20T14:30:00Z',
253
+ },
254
+ ],
255
+ [
256
+ '3',
257
+ {
258
+ id: '3',
259
+ firstName: 'Alex',
260
+ lastName: 'Chen',
261
+ email: 'alex@example.com',
262
+ role: 'viewer',
263
+ createdAt: '2024-03-10T11:15:00Z',
264
+ },
265
+ ],
266
+ ]),
267
+ );
234
268
  ```
235
269
 
236
270
  ### Schema Registry (server-side)
@@ -244,7 +278,9 @@ schemas.set('users', {
244
278
  title: 'User',
245
279
  type: 'object',
246
280
  required: ['firstName', 'lastName', 'email'],
247
- properties: { /* ... as above ... */ },
281
+ properties: {
282
+ /* ... as above ... */
283
+ },
248
284
  });
249
285
  ```
250
286
 
@@ -262,17 +298,18 @@ router that works for any entity registered in the schema map.
262
298
 
263
299
  ## Frontend ↔ Backend Contract
264
300
 
265
- | Frontend (Hybrids Store) | Backend (Express) |
266
- |---|---|
267
- | `[store.connect].get(id)` | `GET /api/:entity/:id` |
268
- | `[store.connect].set(id, values)` | `PUT /api/:entity/:id` |
269
- | `[store.connect].list(params)` | `GET /api/:entity?...` |
270
- | `store.set(model, null)` (delete) | `DELETE /api/:entity/:id` |
271
- | Schema fetch for forms | `OPTIONS /api/:entity` |
272
- | Schema fetch for item | `OPTIONS /api/:entity/:id` |
273
- | Realtime invalidation | `GET /api/events` (SSE) |
301
+ | Frontend (Hybrids Store) | Backend (Express) |
302
+ | --------------------------------- | -------------------------- |
303
+ | `[store.connect].get(id)` | `GET /api/:entity/:id` |
304
+ | `[store.connect].set(id, values)` | `PUT /api/:entity/:id` |
305
+ | `[store.connect].list(params)` | `GET /api/:entity?...` |
306
+ | `store.set(model, null)` (delete) | `DELETE /api/:entity/:id` |
307
+ | Schema fetch for forms | `OPTIONS /api/:entity` |
308
+ | Schema fetch for item | `OPTIONS /api/:entity/:id` |
309
+ | Realtime invalidation | `GET /api/events` (SSE) |
274
310
 
275
311
  This contract means adding a new entity type requires:
312
+
276
313
  - One JSON Schema definition (server)
277
314
  - One store model file (frontend)
278
315
  - Dummy seed data (server, for dev)
package/docs/BUILD_LOG.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # Build Log
2
+
2
3
  ## How This Project Was Built
3
4
 
4
5
  > This entire repository — specification, implementation, tests, and this
@@ -13,33 +14,39 @@ The project was built over ~1.5 days in a single LLM context window.
13
14
  Each phase was implemented, tested, and verified before proceeding.
14
15
 
15
16
  ### Phase 1: Specification
17
+
16
18
  - Wrote the initial spec as one file, hit ~500 lines
17
19
  - Split into 7 topic-specific documents (the spec eating its own dogfood)
18
20
  - Chose Hybrids.js after reading its source and type definitions from node_modules
19
21
 
20
22
  ### Phase 2: Infrastructure
23
+
21
24
  - Express server, vendor-deps script, import maps, HTML shell
22
25
  - JSON Schema registry with seed data
23
26
  - Generic CRUD router — one router handles all entity types
24
27
  - 15 server tests passing before writing any frontend code
25
28
 
26
29
  ### Phase 3: Store Models & Utils
30
+
27
31
  - Singleton models (AppState, UserPrefs) with localStorage connectors
28
32
  - Enumerable models (ProjectModel, TaskModel) with API connectors
29
33
  - Utility functions (formatDate, timeAgo, statusColors)
30
34
  - 35 tests passing
31
35
 
32
36
  ### Phase 4: Components (Atoms → Molecules → Organisms)
37
+
33
38
  - Built bottom-up: app-button, app-badge, app-icon → task-card, project-card → task-list, project-header
34
39
  - Browser tests via @web/test-runner + Playwright
35
40
  - 63 tests passing
36
41
 
37
42
  ### Phase 5: Pages, Router & Integration
43
+
38
44
  - page-layout template, home-view, project-view, app-router
39
45
  - SSE realtime sync wired at the router level
40
46
  - SPA fallback for direct URL navigation
41
47
 
42
48
  ### Phase 6: Schema-Driven Forms
49
+
43
50
  - OPTIONS endpoint returns JSON Schema + form layout
44
51
  - schema-form organism auto-generates fields from schema
45
52
  - Native HTML validation from schema constraints
@@ -47,6 +54,7 @@ Each phase was implemented, tested, and verified before proceeding.
47
54
  - Form layout system with column grouping and action alignment
48
55
 
49
56
  ### Phase 7: Polish & Tooling
57
+
50
58
  - Dark mode via CSS custom property overrides
51
59
  - Spec checker CLI with interactive menu
52
60
  - ESLint + Prettier + tsc --checkJs for JSDoc type validation
@@ -54,6 +62,7 @@ Each phase was implemented, tested, and verified before proceeding.
54
62
  - File reorganization (docs/, .configs/, tests/)
55
63
 
56
64
  ### Phase 8: Drag Reorder & Whiteboard
65
+
57
66
  - Drag-to-reorder with visual gap indicator and backend persistence
58
67
  - WebSocket server for real-time canvas collaboration
59
68
  - SVG whiteboard with drawing tools (in progress)
@@ -66,81 +75,95 @@ The spec was written first, then corrected as implementation revealed gaps.
66
75
  These are the significant corrections:
67
76
 
68
77
  ### Light DOM breaks slots
78
+
69
79
  - **Expected:** `<slot>` for content composition
70
80
  - **Actual:** Hybrids throws on `<slot>` in light DOM
71
81
  - **Fix:** Template functions instead of template components
72
82
  - **Documented in:** COMPONENT_PATTERNS.md → Content Composition
73
83
 
74
84
  ### Template components break host context
85
+
75
86
  - **Expected:** Event handlers in content templates resolve to the page
76
87
  - **Actual:** `host` resolves to the nearest hybrids component (the template)
77
88
  - **Fix:** Use plain functions that return `html`, not `define()`'d components
78
89
  - **Documented in:** COMPONENT_PATTERNS.md → Event Handler Host Context
79
90
 
80
91
  ### Custom events don't bubble by default
92
+
81
93
  - **Expected:** `dispatch(host, 'press')` reaches parent listeners
82
94
  - **Actual:** Without `bubbles: true`, events stop at the dispatching element
83
95
  - **Fix:** Always `dispatch(host, 'event', { bubbles: true })`
84
96
  - **Documented in:** COMPONENT_PATTERNS.md → Custom Events Must Bubble
85
97
 
86
98
  ### store.clear(Model) vs store.clear([Model])
99
+
87
100
  - **Expected:** `store.clear(TaskModel)` refreshes task lists
88
101
  - **Actual:** Only clears singular cache, not list cache
89
102
  - **Fix:** Use `store.clear([TaskModel])` for list stores
90
103
  - **Documented in:** STATE_AND_ROUTING.md → Store API Quick Reference
91
104
 
92
105
  ### store.ready(list) doesn't guarantee item readiness
106
+
93
107
  - **Expected:** If the list is ready, items are ready
94
108
  - **Actual:** Individual items can be pending after cache clear
95
109
  - **Fix:** Guard each item with `store.ready(task)` in map()
96
110
  - **Documented in:** STATE_AND_ROUTING.md → Guarding List Item Access
97
111
 
98
112
  ### localStorage connector must return {}, not undefined
113
+
99
114
  - **Expected:** Returning undefined uses model defaults
100
115
  - **Actual:** Hybrids treats undefined as a failed get → error state
101
116
  - **Fix:** Return `{}` so hybrids merges with defaults
102
117
  - **Documented in:** STATE_AND_ROUTING.md → localStorage Connector
103
118
 
104
119
  ### Batch operations cause SSE storms
120
+
105
121
  - **Expected:** Reordering N tasks sends N updates, UI handles it
106
122
  - **Actual:** N SSE events → N store.clear() calls → cascading errors
107
123
  - **Fix:** Debounce SSE handler per entity type (300ms)
108
124
  - **Documented in:** STATE_AND_ROUTING.md → Debouncing Batch Operations
109
125
 
110
126
  ### Server sort used string comparison for numbers
127
+
111
128
  - **Expected:** sortOrder field sorts numerically
112
129
  - **Actual:** `localeCompare` on numbers gives wrong order
113
130
  - **Fix:** Detect numeric values and use `a - b` comparison
114
131
 
115
132
  ### SVG transforms: two-group rotation approach
133
+
116
134
  - **Expected:** Embed rotation in shapeTransform string
117
135
  - **Actual:** Rotation center in local coords doesn't match screen position
118
136
  - **Fix:** Outer `<g>` for rotation (screen-space center), inner `<g>` for translate+scale
119
137
  - **Documented in:** COMPONENT_PATTERNS.md → Coordinate Transforms
120
138
 
121
139
  ### Move after rotation: unrotate is wrong for translate
140
+
122
141
  - **Expected:** Unrotate screen delta for the inner translate
123
142
  - **Actual:** Causes magnified/skewed movement
124
143
  - **Fix:** Move both rotation center AND inner translate by raw screen delta
125
144
  - **Key insight:** `rotate(deg, cx, cy)` = `translate(cx,cy) rotate(deg) translate(-cx,-cy)`. Shifting cx,cy and translate by the same delta cancels out.
126
145
 
127
146
  ### Resize after rotation: unrotate IS needed
147
+
128
148
  - **Expected:** Same approach as move
129
149
  - **Actual:** Handles are visually rotated, so screen drag doesn't align with object axes
130
150
  - **Fix:** Unrotate resize deltas only, not move deltas
131
151
 
132
152
  ### SVG innerHTML destroys event listeners
153
+
133
154
  - **Expected:** Event listeners persist across renders
134
155
  - **Actual:** `innerHTML` replaces the DOM, losing all listeners
135
156
  - **Fix:** Re-bind mouse listeners in `observe`, attach keyboard to host element
136
157
  - **Documented in:** COMPONENT_PATTERNS.md → SVG Content via innerHTML
137
158
 
138
159
  ### Path d-string manipulation breaks arc commands
160
+
139
161
  - **Expected:** Regex replace on coordinate pairs works for all paths
140
162
  - **Actual:** Arc commands have flags (0/1) that get mangled
141
163
  - **Fix:** Use `shapeTransform` for complex shapes, only rewrite d for M/L paths
142
164
 
143
165
  ### Canvas pan offset not applied to drawing coordinates
166
+
144
167
  - **Expected:** Drawing at the visual position works after panning
145
168
  - **Actual:** Coordinates calculated from SVG rect, not accounting for pan
146
169
  - **Fix:** Shared `canvasPos()` utility subtracts pan offset from all tools
@@ -149,26 +172,26 @@ These are the significant corrections:
149
172
 
150
173
  ## Metrics
151
174
 
152
- | Metric | Value |
153
- |---|---|
154
- | Total source files | 108 |
155
- | Utility modules | 25 |
156
- | Component files | 13 |
157
- | Style sheets | 6 |
158
- | API modules | 6 |
159
- | Page views | 2 |
160
- | Test files | 14 |
161
- | Node tests | 65 |
162
- | Browser tests | 41 |
163
- | Spec documents | 10 |
164
- | Max lines per file | 150 (enforced) |
165
- | Max lines per doc | 500 (enforced) |
166
- | Automated checks | 7 (line counts, lint, format, types, tests) |
167
- | Build phases | 8 + whiteboard |
168
- | Bugs found & fixed | ~25 significant |
175
+ | Metric | Value |
176
+ | ---------------------------- | ------------------------------------------------ |
177
+ | Total source files | 108 |
178
+ | Utility modules | 25 |
179
+ | Component files | 13 |
180
+ | Style sheets | 6 |
181
+ | API modules | 6 |
182
+ | Page views | 2 |
183
+ | Test files | 14 |
184
+ | Node tests | 65 |
185
+ | Browser tests | 41 |
186
+ | Spec documents | 10 |
187
+ | Max lines per file | 150 (enforced) |
188
+ | Max lines per doc | 500 (enforced) |
189
+ | Automated checks | 7 (line counts, lint, format, types, tests) |
190
+ | Build phases | 8 + whiteboard |
191
+ | Bugs found & fixed | ~25 significant |
169
192
  | Bugs requiring >4 iterations | 3 (drag reorder, event bubbling, SVG transforms) |
170
- | External dependencies | 4 runtime (hybrids, express, ws, lucide-static) |
171
- | Build tools | 0 |
193
+ | External dependencies | 4 runtime (hybrids, express, ws, lucide-static) |
194
+ | Build tools | 0 |
172
195
 
173
196
  ---
174
197
 
@@ -1,4 +1,5 @@
1
1
  # Component Patterns
2
+
2
3
  ## Authoring, Styling, Templates & JSDoc Typing
3
4
 
4
5
  > How to write, style, and type components in this framework.
@@ -33,13 +34,13 @@ See the Light DOM section below for why.
33
34
 
34
35
  Properties are declared as default values. Hybrids infers the type:
35
36
 
36
- | Declaration | Type | Reflected to attribute |
37
- |---|---|---|
38
- | `count: 0` | Number | Yes |
39
- | `label: ''` | String | Yes |
40
- | `active: false` | Boolean | Yes |
41
- | `items: []` | Array/Object | No |
42
- | `onClick: () => {}` | Function | No |
37
+ | Declaration | Type | Reflected to attribute |
38
+ | ------------------- | ------------ | ---------------------- |
39
+ | `count: 0` | Number | Yes |
40
+ | `label: ''` | String | Yes |
41
+ | `active: false` | Boolean | Yes |
42
+ | `items: []` | Array/Object | No |
43
+ | `onClick: () => {}` | Function | No |
43
44
 
44
45
  ### Property Descriptors
45
46
 
@@ -51,8 +52,10 @@ export default define({
51
52
  elapsed: {
52
53
  value: 0,
53
54
  connect(host, key, invalidate) {
54
- const id = setInterval(() => { host.elapsed++; }, 1000);
55
- return () => clearInterval(id); // cleanup on disconnect
55
+ const id = setInterval(() => {
56
+ host.elapsed++;
57
+ }, 1000);
58
+ return () => clearInterval(id); // cleanup on disconnect
56
59
  },
57
60
  observe(host, value) {
58
61
  if (value >= 60) dispatch(host, 'timeout');
@@ -62,12 +65,12 @@ export default define({
62
65
  });
63
66
  ```
64
67
 
65
- | Descriptor field | Purpose |
66
- |---|---|
67
- | `value` | Default value or factory function |
68
- | `connect(host, key, invalidate)` | Runs on DOM connect. Return cleanup fn. |
69
- | `observe(host, value, lastValue)` | Runs when value changes |
70
- | `reflect` | `true` or `(value) => string` — sync to attribute |
68
+ | Descriptor field | Purpose |
69
+ | --------------------------------- | ------------------------------------------------- |
70
+ | `value` | Default value or factory function |
71
+ | `connect(host, key, invalidate)` | Runs on DOM connect. Return cleanup fn. |
72
+ | `observe(host, value, lastValue)` | Runs when value changes |
73
+ | `reflect` | `true` or `(value) => string` — sync to attribute |
71
74
 
72
75
  ### Event Handling
73
76
 
@@ -81,9 +84,7 @@ function handleClick(host, event) {
81
84
  export default define({
82
85
  tag: 'app-counter',
83
86
  count: 0,
84
- render: ({ count }) => html`
85
- <button onclick="${handleClick}">Count: ${count}</button>
86
- `,
87
+ render: ({ count }) => html` <button onclick="${handleClick}">Count: ${count}</button> `,
87
88
  });
88
89
  ```
89
90
 
@@ -148,12 +149,12 @@ Custom events from atoms can be unreliable when templates are passed as
148
149
  properties through intermediate components (e.g. `page-layout`'s `content`).
149
150
  The host context and event bubbling path may not resolve as expected.
150
151
 
151
- | Context | Use | Why |
152
- |---|---|---|
153
- | Inside a component's own template | `app-button` | Host context is correct, events bubble normally |
152
+ | Context | Use | Why |
153
+ | ------------------------------------ | ---------------------------- | ------------------------------------------------ |
154
+ | Inside a component's own template | `app-button` | Host context is correct, events bubble normally |
154
155
  | Inside a `content` template property | Plain `<button class="btn">` | Direct `onclick` handler, no custom event needed |
155
- | Reusable molecule/organism | `app-button` | Encapsulated, predictable host |
156
- | Page-level actions | Plain `<button class="btn">` | Simplest, most reliable |
156
+ | Reusable molecule/organism | `app-button` | Encapsulated, predictable host |
157
+ | Page-level actions | Plain `<button class="btn">` | Simplest, most reliable |
157
158
 
158
159
  The `.btn` classes are global (defined in `buttons.css`), so plain buttons
159
160
  look identical to `app-button`. Use the atom when you need its component
@@ -213,11 +214,11 @@ tree. CSS scoping is achieved through **native CSS nesting** on the tag name
213
214
 
214
215
  Shadow DOM is the exception, not the rule. Enable it only when:
215
216
 
216
- | Situation | Why shadow DOM |
217
- |---|---|
218
- | Wrapping a third-party widget | Prevent its styles from leaking out |
217
+ | Situation | Why shadow DOM |
218
+ | ----------------------------------- | ----------------------------------- |
219
+ | Wrapping a third-party widget | Prevent its styles from leaking out |
219
220
  | Distributing a standalone component | Consumer's styles must not break it |
220
- | Embedding untrusted content | Hard style boundary needed |
221
+ | Embedding untrusted content | Hard style boundary needed |
221
222
 
222
223
  For an internal application where you control all the CSS, shadow DOM
223
224
  creates more problems than it solves — you end up fighting it to inject
@@ -302,11 +303,11 @@ Every component automatically inherits all shared styles. No injection needed.
302
303
 
303
304
  ### Three Layers of CSS
304
305
 
305
- | Layer | File(s) | What goes here |
306
- |---|---|---|
307
- | **Tokens** | `tokens.css` | `:root` custom properties — colors, spacing, type, radii, shadows |
308
- | **Shared** | `shared.css` | Error states, loading states, icon base, screen-reader utils, transitions |
309
- | **Components** | `components.css` + per-component `.css` | Styles scoped to tag names via native CSS nesting |
306
+ | Layer | File(s) | What goes here |
307
+ | -------------- | --------------------------------------- | ------------------------------------------------------------------------- |
308
+ | **Tokens** | `tokens.css` | `:root` custom properties — colors, spacing, type, radii, shadows |
309
+ | **Shared** | `shared.css` | Error states, loading states, icon base, screen-reader utils, transitions |
310
+ | **Components** | `components.css` + per-component `.css` | Styles scoped to tag names via native CSS nesting |
310
311
 
311
312
  ### Per-Component CSS with Native Nesting
312
313
 
@@ -326,17 +327,22 @@ app-button {
326
327
  color: white;
327
328
  font: inherit;
328
329
 
329
- &:hover { background: var(--color-primary-hover); }
330
- &:disabled { opacity: 0.5; cursor: not-allowed; }
330
+ &:hover {
331
+ background: var(--color-primary-hover);
332
+ }
333
+ &:disabled {
334
+ opacity: 0.5;
335
+ cursor: not-allowed;
336
+ }
331
337
  }
332
338
 
333
- &[variant="secondary"] button {
339
+ &[variant='secondary'] button {
334
340
  background: var(--color-surface);
335
341
  color: var(--color-text);
336
342
  border: 1px solid var(--color-border);
337
343
  }
338
344
 
339
- &[variant="ghost"] button {
345
+ &[variant='ghost'] button {
340
346
  background: transparent;
341
347
  color: var(--color-primary);
342
348
  }
@@ -393,7 +399,7 @@ Components reference tokens, never hardcode values.
393
399
  All templates use `html` from hybrids:
394
400
 
395
401
  ```javascript
396
- html`<div>${expression}</div>`
402
+ html`<div>${expression}</div>`;
397
403
  ```
398
404
 
399
405
  Expressions can be: strings, numbers, booleans, other templates, arrays of
@@ -413,14 +419,14 @@ render: () => html`
413
419
  `,
414
420
  ```
415
421
 
416
- | Attribute | Effect |
417
- |---|---|
418
- | `layout="row"` | Flexbox row |
419
- | `layout="column"` | Flexbox column |
420
- | `layout="grid:1\|max"` | CSS grid with defined tracks |
421
- | `layout="grow"` | `flex-grow: 1` |
422
- | `layout="center"` | Center content |
423
- | `layout="gap:2"` | Gap using spacing scale |
422
+ | Attribute | Effect |
423
+ | ----------------------- | ------------------------------ |
424
+ | `layout="row"` | Flexbox row |
425
+ | `layout="column"` | Flexbox column |
426
+ | `layout="grid:1\|max"` | CSS grid with defined tracks |
427
+ | `layout="grow"` | `flex-grow: 1` |
428
+ | `layout="center"` | Center content |
429
+ | `layout="gap:2"` | Gap using spacing scale |
424
430
  | `layout@768px="hidden"` | Responsive — applies at ≥768px |
425
431
 
426
432
  ### Keyed Lists
@@ -455,12 +461,12 @@ render: ({ dataPromise }) => html`
455
461
 
456
462
  ### When a file grows too large
457
463
 
458
- | Situation | Action |
459
- |---|---|
460
- | Component logic exceeds 150 lines | Extract helpers to `src/utils/` |
461
- | Template is too complex | Split into child sub-components |
462
- | CSS exceeds 150 lines | Extract shared patterns to `src/styles/` |
463
- | Store model has many relations | Split related models into own files |
464
+ | Situation | Action |
465
+ | --------------------------------- | ---------------------------------------- |
466
+ | Component logic exceeds 150 lines | Extract helpers to `src/utils/` |
467
+ | Template is too complex | Split into child sub-components |
468
+ | CSS exceeds 150 lines | Extract shared patterns to `src/styles/` |
469
+ | Store model has many relations | Split related models into own files |
464
470
 
465
471
  ### What counts toward the limit
466
472