@techninja/clearstack 0.2.0

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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/bin/cli.js +62 -0
  4. package/docs/BACKEND_API_SPEC.md +281 -0
  5. package/docs/BUILD_LOG.md +193 -0
  6. package/docs/COMPONENT_PATTERNS.md +481 -0
  7. package/docs/CONVENTIONS.md +226 -0
  8. package/docs/FRONTEND_IMPLEMENTATION_RULES.md +239 -0
  9. package/docs/JSDOC_TYPING.md +86 -0
  10. package/docs/QUICKSTART.md +190 -0
  11. package/docs/SERVER_AND_DEPS.md +163 -0
  12. package/docs/STATE_AND_ROUTING.md +363 -0
  13. package/docs/TESTING.md +268 -0
  14. package/docs/app-spec/ENTITIES.md +37 -0
  15. package/docs/app-spec/README.md +19 -0
  16. package/lib/check.js +115 -0
  17. package/lib/copy.js +43 -0
  18. package/lib/init.js +73 -0
  19. package/lib/package-gen.js +83 -0
  20. package/lib/update.js +73 -0
  21. package/package.json +69 -0
  22. package/templates/fullstack/data/seed.json +1 -0
  23. package/templates/fullstack/src/api/db.js +75 -0
  24. package/templates/fullstack/src/api/entities.js +114 -0
  25. package/templates/fullstack/src/api/events.js +35 -0
  26. package/templates/fullstack/src/api/schemas.js +104 -0
  27. package/templates/fullstack/src/api/validate.js +52 -0
  28. package/templates/fullstack/src/pages/home/home-view.js +19 -0
  29. package/templates/fullstack/src/router/index.js +16 -0
  30. package/templates/fullstack/src/server.js +46 -0
  31. package/templates/fullstack/src/store/AppState.js +33 -0
  32. package/templates/fullstack/src/store/UserPrefs.js +31 -0
  33. package/templates/fullstack/src/store/realtimeSync.js +54 -0
  34. package/templates/shared/.configs/.prettierrc +8 -0
  35. package/templates/shared/.configs/eslint.config.js +64 -0
  36. package/templates/shared/.configs/jsconfig.json +24 -0
  37. package/templates/shared/.configs/web-test-runner.config.js +8 -0
  38. package/templates/shared/.env +9 -0
  39. package/templates/shared/.github/ISSUE_TEMPLATE/bug_report.md +42 -0
  40. package/templates/shared/.github/ISSUE_TEMPLATE/feature_request.md +30 -0
  41. package/templates/shared/.github/ISSUE_TEMPLATE/spec_correction.md +26 -0
  42. package/templates/shared/.github/pull_request_template.md +51 -0
  43. package/templates/shared/.github/workflows/spec.yml +46 -0
  44. package/templates/shared/README.md +22 -0
  45. package/templates/shared/docs/app-spec/README.md +40 -0
  46. package/templates/shared/docs/clearstack/BACKEND_API_SPEC.md +281 -0
  47. package/templates/shared/docs/clearstack/BUILD_LOG.md +193 -0
  48. package/templates/shared/docs/clearstack/COMPONENT_PATTERNS.md +481 -0
  49. package/templates/shared/docs/clearstack/CONVENTIONS.md +226 -0
  50. package/templates/shared/docs/clearstack/FRONTEND_IMPLEMENTATION_RULES.md +239 -0
  51. package/templates/shared/docs/clearstack/JSDOC_TYPING.md +86 -0
  52. package/templates/shared/docs/clearstack/QUICKSTART.md +190 -0
  53. package/templates/shared/docs/clearstack/SERVER_AND_DEPS.md +163 -0
  54. package/templates/shared/docs/clearstack/STATE_AND_ROUTING.md +363 -0
  55. package/templates/shared/docs/clearstack/TESTING.md +268 -0
  56. package/templates/shared/public/index.html +26 -0
  57. package/templates/shared/scripts/build-icons.js +86 -0
  58. package/templates/shared/scripts/vendor-deps.js +25 -0
  59. package/templates/shared/src/components/atoms/app-badge/app-badge.css +4 -0
  60. package/templates/shared/src/components/atoms/app-badge/app-badge.js +23 -0
  61. package/templates/shared/src/components/atoms/app-badge/app-badge.test.js +26 -0
  62. package/templates/shared/src/components/atoms/app-badge/index.js +1 -0
  63. package/templates/shared/src/components/atoms/app-button/app-button.css +3 -0
  64. package/templates/shared/src/components/atoms/app-button/app-button.js +41 -0
  65. package/templates/shared/src/components/atoms/app-button/app-button.test.js +43 -0
  66. package/templates/shared/src/components/atoms/app-button/index.js +1 -0
  67. package/templates/shared/src/components/atoms/app-icon/app-icon.css +4 -0
  68. package/templates/shared/src/components/atoms/app-icon/app-icon.js +57 -0
  69. package/templates/shared/src/components/atoms/app-icon/app-icon.test.js +30 -0
  70. package/templates/shared/src/components/atoms/app-icon/index.js +1 -0
  71. package/templates/shared/src/components/atoms/theme-toggle/index.js +1 -0
  72. package/templates/shared/src/components/atoms/theme-toggle/theme-toggle.css +10 -0
  73. package/templates/shared/src/components/atoms/theme-toggle/theme-toggle.js +42 -0
  74. package/templates/shared/src/styles/buttons.css +79 -0
  75. package/templates/shared/src/styles/components.css +31 -0
  76. package/templates/shared/src/styles/forms.css +20 -0
  77. package/templates/shared/src/styles/reset.css +32 -0
  78. package/templates/shared/src/styles/shared.css +135 -0
  79. package/templates/shared/src/styles/tokens.css +65 -0
  80. package/templates/shared/src/utils/formatDate.js +41 -0
  81. package/templates/shared/src/utils/statusColors.js +60 -0
  82. package/templates/static/src/pages/home/home-view.js +38 -0
  83. package/templates/static/src/router/index.js +16 -0
  84. package/templates/static/src/store/AppState.js +26 -0
@@ -0,0 +1,481 @@
1
+ # Component Patterns
2
+ ## Authoring, Styling, Templates & JSDoc Typing
3
+
4
+ > How to write, style, and type components in this framework.
5
+ > See [FRONTEND_IMPLEMENTATION_RULES.md](./FRONTEND_IMPLEMENTATION_RULES.md) for
6
+ > project structure and atomic design hierarchy.
7
+
8
+ ---
9
+
10
+ ## Component Authoring
11
+
12
+ Every component is a plain object passed to `define()`. No classes.
13
+
14
+ ### Minimal Component
15
+
16
+ ```javascript
17
+ import { html, define } from 'hybrids';
18
+
19
+ export default define({
20
+ tag: 'app-button',
21
+ label: '',
22
+ render: {
23
+ value: ({ label }) => html`<button>${label}</button>`,
24
+ shadow: false,
25
+ },
26
+ });
27
+ ```
28
+
29
+ Note: `shadow: false` is required on every component in this framework.
30
+ See the Light DOM section below for why.
31
+
32
+ ### Property Types
33
+
34
+ Properties are declared as default values. Hybrids infers the type:
35
+
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 |
43
+
44
+ ### Property Descriptors
45
+
46
+ For advanced behavior, use the descriptor form:
47
+
48
+ ```javascript
49
+ export default define({
50
+ tag: 'app-timer',
51
+ elapsed: {
52
+ value: 0,
53
+ connect(host, key, invalidate) {
54
+ const id = setInterval(() => { host.elapsed++; }, 1000);
55
+ return () => clearInterval(id); // cleanup on disconnect
56
+ },
57
+ observe(host, value) {
58
+ if (value >= 60) dispatch(host, 'timeout');
59
+ },
60
+ },
61
+ render: ({ elapsed }) => html`<span>${elapsed}s</span>`,
62
+ });
63
+ ```
64
+
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 |
71
+
72
+ ### Event Handling
73
+
74
+ Always bind events in templates. Never use `addEventListener`.
75
+
76
+ ```javascript
77
+ function handleClick(host, event) {
78
+ host.count++;
79
+ }
80
+
81
+ export default define({
82
+ tag: 'app-counter',
83
+ count: 0,
84
+ render: ({ count }) => html`
85
+ <button onclick="${handleClick}">Count: ${count}</button>
86
+ `,
87
+ });
88
+ ```
89
+
90
+ For input binding, use `html.set()`:
91
+
92
+ ```javascript
93
+ render: ({ query }) => html`
94
+ <input value="${query}" oninput="${html.set('query')}" />
95
+ `,
96
+ ```
97
+
98
+ ### Event Handler Host Context
99
+
100
+ In hybrids, the `host` parameter in an event handler resolves to the
101
+ **nearest hybrids component ancestor** in the DOM, not necessarily the
102
+ component that defined the handler.
103
+
104
+ This matters when passing templates as properties to other components
105
+ (e.g. `page-layout`'s `content` property). Inline arrow functions will
106
+ receive the **wrong host** — the template component, not your page.
107
+
108
+ ```javascript
109
+ // ❌ BAD — host is page-layout, not my-page
110
+ render: {
111
+ value: ({ showForm }) => html`
112
+ <page-layout content="${html`
113
+ <app-button onpress="${(host) => { host.showForm = true; }}"></app-button>
114
+ `}"></page-layout>
115
+ `,
116
+ },
117
+
118
+ // ✅ GOOD — named function, hybrids resolves host to the defining component
119
+ function toggleForm(host) {
120
+ host.showForm = !host.showForm;
121
+ }
122
+ // ... in render:
123
+ <app-button onpress="${toggleForm}"></app-button>
124
+ ```
125
+
126
+ **Rule: always use named functions for event handlers.** This ensures
127
+ hybrids resolves `host` to the component that owns the property, not
128
+ the nearest ancestor in the DOM tree. Inline arrows in nested templates
129
+ are the #1 source of "nothing happens when I click" bugs.
130
+
131
+ ### Custom Events Must Bubble
132
+
133
+ In light DOM, custom events dispatched from child components must set
134
+ `bubbles: true` to reach parent listeners — especially when content is
135
+ passed as a template property through intermediate components.
136
+
137
+ ```javascript
138
+ // ❌ BAD — event won't reach listeners above the dispatching component
139
+ dispatch(host, 'press');
140
+
141
+ // ✅ GOOD — event bubbles through the DOM tree
142
+ dispatch(host, 'press', { bubbles: true });
143
+ ```
144
+
145
+ ### When to Use `app-button` vs Plain `<button>`
146
+
147
+ Custom events from atoms can be unreliable when templates are passed as
148
+ properties through intermediate components (e.g. `page-layout`'s `content`).
149
+ The host context and event bubbling path may not resolve as expected.
150
+
151
+ | Context | Use | Why |
152
+ |---|---|---|
153
+ | Inside a component's own template | `app-button` | Host context is correct, events bubble normally |
154
+ | 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 |
157
+
158
+ The `.btn` classes are global (defined in `buttons.css`), so plain buttons
159
+ look identical to `app-button`. Use the atom when you need its component
160
+ API; use a plain button when you need reliable click handling in nested
161
+ templates.
162
+
163
+ ### SVG Content via innerHTML
164
+
165
+ Hybrids' `html` template doesn't support dynamic SVG content well. For
166
+ canvas/whiteboard components that build SVG from data, use `innerHTML`
167
+ on a wrapper div:
168
+
169
+ ```javascript
170
+ render: {
171
+ value: (host) => html`
172
+ <div class="canvas" innerHTML="${buildSvgString(host.objects)}"></div>
173
+ `,
174
+ }
175
+ ```
176
+
177
+ Event listeners must be re-attached in `observe` since `innerHTML`
178
+ replaces the DOM. Use a flag to avoid duplicate binding, and attach
179
+ persistent listeners (like `keydown`) to the host element instead.
180
+
181
+ ### Coordinate Transforms for Rotated Objects
182
+
183
+ When objects have rotation via an outer `<g transform="rotate(...)">`:
184
+
185
+ - **Move:** Shift both the inner translate AND the rotation center by the
186
+ same screen-space delta. No trigonometry needed.
187
+ - **Resize:** Unrotate the drag delta to align with the object's local axes.
188
+ - **Rotation center:** Store `rotationCx`/`rotationCy` on the object data
189
+ so it persists across renders and reloads.
190
+
191
+ ### Light DOM (Default)
192
+
193
+ All components in this framework use **light DOM** (`shadow: false`). This
194
+ means global stylesheets, shared CSS classes, and design tokens all apply
195
+ automatically — no style injection boilerplate.
196
+
197
+ ```javascript
198
+ export default define({
199
+ tag: 'app-button',
200
+ label: '',
201
+ render: {
202
+ value: ({ label }) => html`<button>${label}</button>`,
203
+ shadow: false,
204
+ },
205
+ });
206
+ ```
207
+
208
+ With light DOM, the component's rendered HTML lives in the main document
209
+ tree. CSS scoping is achieved through **native CSS nesting** on the tag name
210
+ (see Styling section below), not through shadow boundary.
211
+
212
+ ### When to Use Shadow DOM
213
+
214
+ Shadow DOM is the exception, not the rule. Enable it only when:
215
+
216
+ | Situation | Why shadow DOM |
217
+ |---|---|
218
+ | Wrapping a third-party widget | Prevent its styles from leaking out |
219
+ | Distributing a standalone component | Consumer's styles must not break it |
220
+ | Embedding untrusted content | Hard style boundary needed |
221
+
222
+ For an internal application where you control all the CSS, shadow DOM
223
+ creates more problems than it solves — you end up fighting it to inject
224
+ shared styles into every component.
225
+
226
+ ### Inline Styles for Small Additions
227
+
228
+ When a component needs a few custom styles that don't warrant a `.css` file,
229
+ use `.css()` directly on the template:
230
+
231
+ ```javascript
232
+ render: {
233
+ value: ({ active }) => html`
234
+ <span class="${active ? 'active' : ''}">${label}</span>
235
+ `.css`
236
+ :host { display: inline-block; }
237
+ .active { font-weight: bold; color: var(--color-primary); }
238
+ `,
239
+ shadow: false,
240
+ },
241
+ ```
242
+
243
+ This keeps small style additions co-located with the template. When inline
244
+ styles grow past ~10 rules, move them to the component's `.css` file.
245
+
246
+ ### Content Composition (No Slots)
247
+
248
+ **`<slot>` is a shadow DOM feature.** It does not work with `shadow: false`.
249
+ Hybrids will throw if it finds a `<slot>` in a light DOM template.
250
+
251
+ **Template components break host context.** If you pass content as a property
252
+ to another component (e.g. `<page-layout content="${html`...`}">`), event
253
+ handlers inside that content will resolve `host` to the template component,
254
+ not the page that defined the handler. This makes buttons and forms silently
255
+ fail.
256
+
257
+ The solution: **use template functions, not template components**, for
258
+ page-level layout:
259
+
260
+ ```javascript
261
+ // Template as a function — no component boundary, host context preserved
262
+ export function pageLayout(title, content) {
263
+ return html`
264
+ <div class="page-layout">
265
+ <header>${title}</header>
266
+ <main>${content}</main>
267
+ </div>
268
+ `;
269
+ }
270
+
271
+ // Page calls the function directly in its render
272
+ render: {
273
+ value: ({ items }) => pageLayout('Home', html`
274
+ <button onclick="${handleClick}">Works!</button>
275
+ <ul>${items.map(...)}</ul>
276
+ `),
277
+ shadow: false,
278
+ },
279
+ ```
280
+
281
+ Because `pageLayout` is a plain function (not a `define()`'d component),
282
+ there is no intermediate hybrids element in the DOM tree. The `onclick`
283
+ handler resolves `host` to the page component as expected.
284
+
285
+ ---
286
+
287
+ ## Styling
288
+
289
+ ### Style Inheritance Model
290
+
291
+ Because components use light DOM, styles flow naturally:
292
+
293
+ ```
294
+ public/index.html
295
+ ├── <link> src/styles/reset.css ← base reset
296
+ ├── <link> src/styles/tokens.css ← :root custom properties
297
+ ├── <link> src/styles/shared.css ← error states, icons, utilities
298
+ └── <link> src/styles/components.css ← all component styles (loaded once)
299
+ ```
300
+
301
+ Every component automatically inherits all shared styles. No injection needed.
302
+
303
+ ### Three Layers of CSS
304
+
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 |
310
+
311
+ ### Per-Component CSS with Native Nesting
312
+
313
+ Each component has a `.css` file that scopes styles using the tag name
314
+ as the top-level selector, with native CSS nesting inside:
315
+
316
+ ```css
317
+ /* app-button/app-button.css */
318
+ app-button {
319
+ display: inline-block;
320
+
321
+ & button {
322
+ padding: var(--space-sm) var(--space-md);
323
+ border: none;
324
+ border-radius: var(--radius-md);
325
+ background: var(--color-primary);
326
+ color: white;
327
+ font: inherit;
328
+
329
+ &:hover { background: var(--color-primary-hover); }
330
+ &:disabled { opacity: 0.5; cursor: not-allowed; }
331
+ }
332
+
333
+ &[variant="secondary"] button {
334
+ background: var(--color-surface);
335
+ color: var(--color-text);
336
+ border: 1px solid var(--color-border);
337
+ }
338
+
339
+ &[variant="ghost"] button {
340
+ background: transparent;
341
+ color: var(--color-primary);
342
+ }
343
+ }
344
+ ```
345
+
346
+ This gives you **scoping by convention** — styles only apply inside the
347
+ component's tag. No shadow DOM needed, no style collisions.
348
+
349
+ ### Loading Component Styles
350
+
351
+ Component `.css` files are imported into a single `src/styles/components.css`
352
+ that aggregates them:
353
+
354
+ ```css
355
+ /* src/styles/components.css */
356
+ @import '../components/atoms/app-button/app-button.css';
357
+ @import '../components/atoms/app-badge/app-badge.css';
358
+ /* ... add as components are created ... */
359
+ ```
360
+
361
+ This file is loaded once in `index.html`. Adding a new component means
362
+ adding one `@import` line.
363
+
364
+ ### Design Tokens
365
+
366
+ Shared tokens live in `src/styles/tokens.css` as custom properties on `:root`.
367
+ These pierce all boundaries (even shadow DOM if ever used):
368
+
369
+ ```css
370
+ :root {
371
+ --color-primary: #2563eb;
372
+ --space-md: 1rem;
373
+ --radius-md: 0.375rem;
374
+ }
375
+ ```
376
+
377
+ Components reference tokens, never hardcode values.
378
+
379
+ ### Rules
380
+
381
+ - No CSS-in-JS libraries. No preprocessors. Native CSS only.
382
+ - One `.css` file per component, scoped via tag-name nesting.
383
+ - Shared patterns (error, loading, icons) go in `shared.css`.
384
+ - Use custom properties for all colors, spacing, and typography.
385
+ - Small inline styles via `.css()` are fine — move to file at ~10 rules.
386
+
387
+ ---
388
+
389
+ ## Templates & Layout Engine
390
+
391
+ ### Tagged Template Literals
392
+
393
+ All templates use `html` from hybrids:
394
+
395
+ ```javascript
396
+ html`<div>${expression}</div>`
397
+ ```
398
+
399
+ Expressions can be: strings, numbers, booleans, other templates, arrays of
400
+ templates, Promises (via `html.resolve()`), or event handlers.
401
+
402
+ ### Layout Attributes
403
+
404
+ Hybrids' layout engine lets you declare CSS layout inline:
405
+
406
+ ```javascript
407
+ render: () => html`
408
+ <template layout="column gap:2">
409
+ <header layout="row center gap">...</header>
410
+ <main layout="grow">...</main>
411
+ <footer layout@768px="hidden">...</footer>
412
+ </template>
413
+ `,
414
+ ```
415
+
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 |
424
+ | `layout@768px="hidden"` | Responsive — applies at ≥768px |
425
+
426
+ ### Keyed Lists
427
+
428
+ For efficient list rendering, use `.key()`:
429
+
430
+ ```javascript
431
+ render: ({ items }) => html`
432
+ <ul>
433
+ ${items.map(item => html`
434
+ <li>${item.name}</li>
435
+ `.key(item.id))}
436
+ </ul>
437
+ `,
438
+ ```
439
+
440
+ ### Async Content
441
+
442
+ ```javascript
443
+ render: ({ dataPromise }) => html`
444
+ <div>
445
+ ${html.resolve(dataPromise, html`<span>Loading...</span>`)}
446
+ </div>
447
+ `,
448
+ ```
449
+
450
+ ---
451
+
452
+ ## File Size & Decomposition
453
+
454
+ **Hard limit: 150 lines per file.** This applies to `.js` and `.css` alike.
455
+
456
+ ### When a file grows too large
457
+
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
+
465
+ ### What counts toward the limit
466
+
467
+ - All lines including imports, blank lines, and comments.
468
+ - JSDoc blocks count. Keep them concise.
469
+
470
+ ### What does NOT split
471
+
472
+ - The 3-file component structure (`component.js`, `component.css`, `index.js`)
473
+ stays together in one directory. Don't add more files to a component dir.
474
+
475
+ ---
476
+
477
+ ## JSDoc Typing Strategy
478
+
479
+ See [JSDOC_TYPING.md](./JSDOC_TYPING.md) for the full JSDoc typing
480
+ specification, including component properties, store models, event handlers,
481
+ and validation rules.
@@ -0,0 +1,226 @@
1
+ # Conventions
2
+ ## Naming Rules & Anti-Patterns
3
+
4
+ > Quick reference for naming and what to avoid.
5
+ > See [FRONTEND_IMPLEMENTATION_RULES.md](./FRONTEND_IMPLEMENTATION_RULES.md) for
6
+ > the full specification index.
7
+
8
+ ---
9
+
10
+ ## Naming Conventions
11
+
12
+ ### Component Tags
13
+
14
+ All custom element tags use **kebab-case** with an `app-` prefix:
15
+
16
+ | Thing | Name | Tag |
17
+ |---|---|---|
18
+ | Button atom | `app-button` | `<app-button>` |
19
+ | Header organism | `app-header` | `<app-header>` |
20
+ | Page layout template | `page-layout` | `<page-layout>` |
21
+ | Home page view | `home-view` | `<home-view>` |
22
+
23
+ The `app-` prefix prevents collisions with native elements and third-party
24
+ components. Page views and templates may drop the prefix when unambiguous.
25
+
26
+ ### Files & Directories
27
+
28
+ | Type | Convention | Example |
29
+ |---|---|---|
30
+ | Component file | Match tag name | `app-button.js` |
31
+ | Component CSS | Match tag name | `app-button.css` |
32
+ | Component dir | Match tag name | `app-button/` |
33
+ | Re-export | Always `index.js` | `index.js` |
34
+ | Store model | PascalCase | `UserModel.js` |
35
+ | Utility function | camelCase | `formatDate.js` |
36
+ | Shared CSS | Descriptive kebab | `tokens.css`, `reset.css` |
37
+
38
+ ### JavaScript
39
+
40
+ | Type | Convention | Example |
41
+ |---|---|---|
42
+ | Event handlers | `handle` + action | `handleClick`, `handleSubmit` |
43
+ | Store models | PascalCase noun | `UserModel`, `AppState` |
44
+ | Utility functions | camelCase verb | `formatDate`, `parseQuery` |
45
+ | Constants | UPPER_SNAKE | `MAX_RETRIES`, `API_BASE` |
46
+ | JSDoc typedefs | PascalCase | `@typedef {Object} User` |
47
+
48
+ ---
49
+
50
+ ## Anti-Patterns
51
+
52
+ ### ❌ Never Do This
53
+
54
+ **DOM queries inside components:**
55
+ ```javascript
56
+ // BAD — breaks encapsulation, ignores shadow DOM
57
+ const el = document.querySelector('.my-thing');
58
+ ```
59
+
60
+ **Manual event listeners:**
61
+ ```javascript
62
+ // BAD — leaks memory, bypasses hybrids lifecycle
63
+ connectedCallback() {
64
+ this.addEventListener('click', this.onClick);
65
+ }
66
+ ```
67
+
68
+ **Global mutable state:**
69
+ ```javascript
70
+ // BAD — invisible dependencies, untraceable bugs
71
+ window.appState = { user: null };
72
+ ```
73
+
74
+ **Imperative DOM manipulation:**
75
+ ```javascript
76
+ // BAD — fights the reactive render cycle
77
+ host.shadowRoot.querySelector('span').textContent = 'updated';
78
+ ```
79
+
80
+ **Business logic in render:**
81
+ ```javascript
82
+ // BAD — render should be pure projection of state
83
+ render: ({ items }) => html`
84
+ <ul>${items.filter(i => i.active).sort((a,b) => a.name.localeCompare(b.name)).map(...)}</ul>
85
+ `,
86
+ ```
87
+
88
+ **Files over 150 lines:**
89
+ ```
90
+ // BAD — extract to utils/ or split into sub-components
91
+ ```
92
+
93
+ **Deep nesting (>3 component levels):**
94
+ ```
95
+ // BAD — flatten by composing at the page level
96
+ <app-layout>
97
+ <app-sidebar>
98
+ <nav-section>
99
+ <nav-group> ← too deep
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Error Handling
105
+
106
+ Errors are handled **at the boundary where they are actionable.** Each layer
107
+ has a single responsibility — catch only what you can meaningfully respond to,
108
+ let everything else propagate.
109
+
110
+ ### Boundary Rules
111
+
112
+ | Layer | Responsibility | Example |
113
+ |---|---|---|
114
+ | **Utils** | Never catch. Return error values or throw. Caller decides. | `formatDate(null)` returns `''` |
115
+ | **Store connectors** | Let fetch failures propagate. Hybrids' `store.error()` catches them. | Don't wrap fetch in try/catch |
116
+ | **Components** | Display `store.pending()` / `store.error()` states. Never try/catch in render. | `${store.error(model) && html`<div class="error-message">...</div>`}` |
117
+ | **Event handlers** | Guard with `store.ready()` before accessing store properties. | `if (!store.ready(host.state)) return;` |
118
+ | **Server routes** | Return HTTP status + JSON error body. Never crash the process. | `res.status(404).json({ error: 'Not found' })` |
119
+ | **Server infra** | Handle process-level errors with clear messages and exit codes. | Port conflict → log message → `process.exit(1)` |
120
+
121
+ ### Why This Matters
122
+
123
+ If a store connector catches its own fetch error, `store.error()` never fires
124
+ and the component can't show an error state. If a utility swallows an
125
+ exception, the caller can't decide how to handle it. Each layer trusts the
126
+ next layer up to handle what it can't.
127
+
128
+ ### ❌ Don't
129
+
130
+ ```javascript
131
+ // BAD — swallows the error, store.error() never fires
132
+ [store.connect]: {
133
+ get: async (id) => {
134
+ try { return await fetch(`/api/items/${id}`).then(r => r.json()); }
135
+ catch { return null; } // silent failure
136
+ },
137
+ }
138
+ ```
139
+
140
+ ```javascript
141
+ // BAD — accesses store properties without ready guard
142
+ function toggle(host) {
143
+ const next = host.state.theme === 'light' ? 'dark' : 'light';
144
+ store.set(host.state, { theme: next }); // crashes if model is in error state
145
+ }
146
+ ```
147
+
148
+ ### ✅ Do
149
+
150
+ ```javascript
151
+ // GOOD — let it throw, component handles via store.error()
152
+ [store.connect]: {
153
+ get: (id) => fetch(`/api/items/${id}`).then(r => r.json()),
154
+ }
155
+ ```
156
+
157
+ ```javascript
158
+ // GOOD — guard before accessing properties
159
+ function toggle(host) {
160
+ if (!store.ready(host.state)) return;
161
+ const next = host.state.theme === 'light' ? 'dark' : 'light';
162
+ store.set(host.state, { theme: next });
163
+ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ### ✅ Always Do This
169
+
170
+ **Declarative event binding:**
171
+ ```javascript
172
+ html`<button onclick="${handleClick}">Go</button>`
173
+ ```
174
+
175
+ **Store for shared state:**
176
+ ```javascript
177
+ state: store(AppState),
178
+ ```
179
+
180
+ **Pure functions for logic:**
181
+ ```javascript
182
+ // In src/utils/filterActive.js
183
+ export const filterActive = (items) => items.filter(i => i.active);
184
+
185
+ // In component
186
+ import { filterActive } from '../../utils/filterActive.js';
187
+ render: ({ items }) => html`<ul>${filterActive(items).map(...)}</ul>`,
188
+ ```
189
+
190
+ **JSDoc on all exports:**
191
+ ```javascript
192
+ /** @param {User} user */
193
+ export const fullName = (user) => `${user.firstName} ${user.lastName}`;
194
+ ```
195
+
196
+ ---
197
+
198
+ ## File Size: Soft Warnings Before Hard Limits
199
+
200
+ The 150-line limit is a hard gate in CI, but treat **~120 lines as a yellow
201
+ light.** When a file passes 120 lines:
202
+
203
+ 1. Add a `// SPLIT CANDIDATE:` comment noting where a logical split could happen
204
+ 2. Continue working — don't split mid-feature
205
+ 3. Split when the file hits 150 or when the feature is complete
206
+
207
+ This prevents premature extraction while keeping the eventual split obvious.
208
+
209
+ ```javascript
210
+ // SPLIT CANDIDATE: moveObj/resizeObj could extract to canvasTransform.js
211
+ function moveObj(o, dx, dy) { ... }
212
+ ```
213
+
214
+ ---
215
+
216
+ ## Session Retrospective
217
+
218
+ At the end of each implementation session, ask:
219
+
220
+ 1. **What patterns did we discover?** Document in the relevant spec doc.
221
+ 2. **What broke that we didn't expect?** Add to BUILD_LOG discoveries.
222
+ 3. **What tests would catch the bugs we found?** Write them before committing.
223
+ 4. **Did any files grow past the yellow light?** Split or add split markers.
224
+ 5. **Did the spec need correction?** Update it — the spec improves through use.
225
+
226
+ This practice is what keeps the spec alive and accurate.