@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.
- 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
package/docs/BACKEND_API_SPEC.md
CHANGED
|
@@ -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
|
|
74
|
-
|
|
75
|
-
| `type: "string"`
|
|
76
|
-
| `format: "email"`
|
|
77
|
-
| `format: "date-time"`
|
|
78
|
-
| `enum: [...]`
|
|
79
|
-
| `minLength` / `maxLength` | Validation constraints
|
|
80
|
-
| `readOnly: true`
|
|
81
|
-
| `required: [...]`
|
|
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`
|
|
90
|
-
| `maximum`
|
|
91
|
-
| `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
|
|
119
|
-
|
|
120
|
-
| `422`
|
|
121
|
-
| `404`
|
|
122
|
-
| `201`
|
|
123
|
-
| `200`
|
|
124
|
-
| `204`
|
|
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
|
|
138
|
-
|
|
139
|
-
| `limit`
|
|
140
|
-
| `offset` | `?offset=40`
|
|
141
|
-
| `sort`
|
|
142
|
-
| `filter` | `?role=admin`
|
|
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
|
|
204
|
-
|
|
205
|
-
| `type`
|
|
206
|
-
| `id`
|
|
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(
|
|
230
|
-
|
|
231
|
-
[
|
|
232
|
-
|
|
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: {
|
|
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)
|
|
266
|
-
|
|
267
|
-
| `[store.connect].get(id)`
|
|
268
|
-
| `[store.connect].set(id, values)` | `PUT /api/:entity/:id`
|
|
269
|
-
| `[store.connect].list(params)`
|
|
270
|
-
| `store.set(model, null)` (delete) | `DELETE /api/:entity/:id`
|
|
271
|
-
| Schema fetch for forms
|
|
272
|
-
| Schema fetch for item
|
|
273
|
-
| Realtime invalidation
|
|
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
|
|
153
|
-
|
|
154
|
-
| Total source files
|
|
155
|
-
| Utility modules
|
|
156
|
-
| Component files
|
|
157
|
-
| Style sheets
|
|
158
|
-
| API modules
|
|
159
|
-
| Page views
|
|
160
|
-
| Test files
|
|
161
|
-
| Node tests
|
|
162
|
-
| Browser tests
|
|
163
|
-
| Spec documents
|
|
164
|
-
| Max lines per file
|
|
165
|
-
| Max lines per doc
|
|
166
|
-
| Automated checks
|
|
167
|
-
| Build phases
|
|
168
|
-
| Bugs found & fixed
|
|
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
|
|
171
|
-
| Build tools
|
|
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
|
|
37
|
-
|
|
38
|
-
| `count: 0`
|
|
39
|
-
| `label: ''`
|
|
40
|
-
| `active: false`
|
|
41
|
-
| `items: []`
|
|
42
|
-
| `onClick: () => {}` | Function
|
|
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(() => {
|
|
55
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
| `value`
|
|
68
|
-
| `connect(host, key, invalidate)`
|
|
69
|
-
| `observe(host, value, lastValue)` | Runs when value changes
|
|
70
|
-
| `reflect`
|
|
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
|
|
152
|
-
|
|
153
|
-
| Inside a component's own template
|
|
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
|
|
156
|
-
| Page-level actions
|
|
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
|
|
217
|
-
|
|
218
|
-
| Wrapping a third-party widget
|
|
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
|
|
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
|
|
306
|
-
|
|
307
|
-
| **Tokens**
|
|
308
|
-
| **Shared**
|
|
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 {
|
|
330
|
-
|
|
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=
|
|
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=
|
|
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
|
|
417
|
-
|
|
418
|
-
| `layout="row"`
|
|
419
|
-
| `layout="column"`
|
|
420
|
-
| `layout="grid:1\|max"`
|
|
421
|
-
| `layout="grow"`
|
|
422
|
-
| `layout="center"`
|
|
423
|
-
| `layout="gap:2"`
|
|
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
|
|
459
|
-
|
|
460
|
-
| Component logic exceeds 150 lines | Extract helpers to `src/utils/`
|
|
461
|
-
| Template is too complex
|
|
462
|
-
| CSS exceeds 150 lines
|
|
463
|
-
| Store model has many relations
|
|
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
|
|