@webjsdev/mcp 0.1.7 → 0.1.8

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/README.md +1 -1
  2. package/package.json +1 -1
  3. package/resources/AGENTS.md +83 -72
  4. package/resources/SKILL.md +226 -0
  5. package/resources/references/auth-and-sessions.md +220 -0
  6. package/resources/references/built-ins.md +200 -0
  7. package/resources/references/client-router-and-streaming.md +204 -0
  8. package/resources/references/components.md +167 -0
  9. package/resources/references/data-and-actions.md +187 -0
  10. package/resources/references/muscle-memory-gotchas.md +170 -0
  11. package/resources/references/optimistic-ui.md +128 -0
  12. package/resources/references/routing-and-pages.md +158 -0
  13. package/resources/references/runtime.md +80 -0
  14. package/resources/references/service-worker.md +78 -0
  15. package/resources/references/styling.md +123 -0
  16. package/resources/references/testing.md +125 -0
  17. package/resources/references/typescript.md +148 -0
  18. package/src/mcp-docs.js +27 -14
  19. package/src/mcp-source.js +1 -1
  20. package/src/mcp.js +4 -3
  21. package/resources/agent-docs/advanced.md +0 -1334
  22. package/resources/agent-docs/built-ins.md +0 -389
  23. package/resources/agent-docs/components-shadowing.md +0 -87
  24. package/resources/agent-docs/components.md +0 -734
  25. package/resources/agent-docs/configuration.md +0 -284
  26. package/resources/agent-docs/framework-dev.md +0 -73
  27. package/resources/agent-docs/lit-muscle-memory-gotchas.md +0 -470
  28. package/resources/agent-docs/metadata.md +0 -334
  29. package/resources/agent-docs/orm.md +0 -198
  30. package/resources/agent-docs/recipes.md +0 -664
  31. package/resources/agent-docs/runtime.md +0 -65
  32. package/resources/agent-docs/service-worker.md +0 -104
  33. package/resources/agent-docs/styling.md +0 -279
  34. package/resources/agent-docs/testing.md +0 -479
  35. package/resources/agent-docs/types-and-mutations.md +0 -171
  36. package/resources/agent-docs/typescript.md +0 -402
@@ -1,470 +0,0 @@
1
- # Lit muscle-memory gotchas
2
-
3
- AI agents trained on lit will reach for patterns that look correct but
4
- break webjs's SSR contract, reactivity model, or styling defaults. This
5
- file catalogs those failures with the webjs-shaped fix for each.
6
-
7
- The architectural disagreement underneath all of these. Lit is JS-first
8
- (hydration is the API). Webjs is HTML-first (first paint is real HTML,
9
- JS is opt-in per interactive behavior). Every gotcha below is
10
- downstream of that one disagreement.
11
-
12
- ## Mental model. Progressive enhancement, JS opt-in per behavior not per component
13
-
14
- Webjs is a progressive enhancement framework. Pages render as real HTML
15
- on the server, and every web component renders to real HTML on the
16
- server. With JavaScript disabled in the browser, the page is still
17
- readable, `<a>` links still navigate, and `<form action method>`
18
- submissions still hit server actions. Display-only custom elements
19
- still render their server-produced HTML.
20
-
21
- JavaScript is opt-in **per interactive behavior, not per component**.
22
- This is the distinction most lit-shaped intuitions miss.
23
-
24
- In lit and most modern frameworks, hydration is a per-component
25
- decision. You decide whether a given component is interactive (and
26
- therefore needs JS shipped and run) at the component boundary.
27
- "Hydrate this island, skip that one."
28
-
29
- In webjs, the granularity is different. Every component is server
30
- rendered. JavaScript is requested **by the specific interactive holes
31
- you write in the template**. A `@click=${...}` binding requests JS for
32
- click handling. A `signal.set(...)` call (instance or module-scope)
33
- requests JS for reactive updates. A property binding
34
- `.data=${richObject}` requests JS for property hydration. A controller
35
- like `Task` requests JS for that async behavior. A plain `<a href>`
36
- does not request JS. A `<form action="...">` does not request JS. A
37
- purely display-time component (no event listeners, no signal
38
- mutations, no property bindings to hydrate) does not request JS.
39
-
40
- A single component can mix both. A product card that shows
41
- server-rendered title, price, image, and a "View" link
42
- (no JS needed) plus an "Add to cart" button with a `@click`
43
- (JS needed for that one behavior). The framework loads JS for the
44
- component because of the `@click`, runs it, and the rest of the card
45
- stays exactly as the server painted it. You do not pick a hydration
46
- mode for the component. You write the markup, and the JS budget
47
- follows from which interactive behaviors that markup uses.
48
-
49
- Practical consequences for agents writing webjs code.
50
-
51
- 1. Never reach for `fetch()` plus a JS click handler when a `<form>`
52
- plus a server action would do. The form is free (no JS), the
53
- server action is typed and CSRF-protected, the result reaches the
54
- page through normal navigation.
55
- 2. Never make first paint depend on hydration. If the user sees a
56
- blank skeleton until JS runs, you wrote the feature wrong.
57
- 3. Never assume "this component needs JS" or "this component is
58
- static" as a binary. Pick interactive primitives per behavior. A
59
- shopping cart page can have ten components, eight of them adding
60
- zero JS bytes, two of them adding the handlers they need.
61
- 4. When choosing between a server action invoked from `<form>` and a
62
- client-side action invoked from `@click`, default to the form
63
- unless the interaction genuinely needs client-only state
64
- (optimistic UI, in-flight indicators tied to client state,
65
- keyboard shortcuts).
66
-
67
- ## Use lit idioms, not vanilla DOM (the whole point of lit-style components)
68
-
69
- webjs components are lit-shaped on purpose: the value is the declarative
70
- DX (typed reactive props, signals, `html` templates, declarative
71
- bindings), not raw DOM scripting. Reaching for vanilla web-component
72
- muscle memory (`this.getAttribute`, `this.setAttribute`, `this.classList`,
73
- `this.addEventListener`, `this.innerHTML`, `document.createElement`,
74
- manual `observedAttributes` / `attributeChangedCallback`, manual
75
- `customElements.define`) inside a component is the anti-pattern. Use the
76
- lit form unless the vanilla API is genuinely unavoidable.
77
-
78
- | Vanilla muscle memory | Lit-style webjs form |
79
- |---|---|
80
- | `this.getAttribute('x')` / `this.hasAttribute('x')` for own config | a reactive prop declared in the factory: `extends WebComponent({ x: String })`, read `this.x` (the prop rides the `x` attribute) |
81
- | `this.setAttribute('x', v)` / `removeAttribute` to reflect own state | a reactive prop with `reflect: true`, or for non-attribute state a `signal` |
82
- | `state: true` reactive prop for internal state | a `signal` (instance signal in the constructor, or module-scope) |
83
- | `this.classList.add/toggle(...)` on self | a `class=${...}` binding in `render()` |
84
- | `this.innerHTML = ...` / `appendChild` / `document.createElement` | return the markup from `render()` as `` html`...` `` |
85
- | `this.addEventListener('click', ...)` on own/child elements | a `@click=${...}` binding in the template |
86
- | `this.querySelector(...)` to reach own rendered DOM | the `ref()` directive + `createRef()`, or read a `<form>` with `new FormData(form)` |
87
- | manual `observedAttributes` + `attributeChangedCallback` | a factory-declared reactive prop `WebComponent({ ... })` (the framework derives both) |
88
- | manual `customElements.define('x', C)` | `C.register('x')` |
89
-
90
- Emitting an event with `this.dispatchEvent(new CustomEvent(...))` is the
91
- correct lit form, not a vanilla smell. Reading form values with
92
- `new FormData(e.currentTarget)` inside a `@submit` handler is also fine.
93
-
94
- **When vanilla DOM is genuinely needed (these stay):**
95
-
96
- - **Ancestor lookup in a compound component.** `this.closest('ui-tabs')`
97
- to read a parent's state. There is no declarative lit equivalent. This
98
- resolves at SSR too (tag-name selectors, against the SSR ancestor
99
- chain), so a compound child's active/pressed state is correct in the
100
- first server paint, not just after hydration. Host attributes the
101
- child sets in `render()` (`this.dataset.* =`, `this.className =`,
102
- `this.ariaPressed =`) reflect onto the SSR'd host tag. A class or
103
- attribute selector still resolves to null server-side.
104
- - **Slotted / projected content.** `this.querySelector(...)` reaching a
105
- `<slot>`-projected child or a sibling sub-component the template does
106
- not own. `ref()` only binds elements this component's own `render()`
107
- creates, so it cannot reach slotted content.
108
- - **Host attributes in light DOM.** A light-DOM `render()` template
109
- cannot bind attributes or listeners on the host element itself, so a
110
- component that must style or listen on its own host writes
111
- `this.dataset.* =` / `this.className =` / `this.addEventListener` on
112
- `this` in a lifecycle hook. Shadow-DOM components avoid this.
113
- - **Global listeners.** `document` / `window` `addEventListener` for
114
- click-away, global keys, resize, or reposition.
115
- - **Reading another element's attribute.** `contentHost.getAttribute('side')`
116
- reads a different element's config, not `this`.
117
- - **Browser-only globals.** `localStorage`, `matchMedia`, `navigator`,
118
- `document.documentElement` mutations (a theme toggle setting `<html>`),
119
- clipboard. These belong in `connectedCallback` or an event handler.
120
- - **Imperative focus.** `el.focus()` has no declarative form.
121
-
122
- The rule of thumb: if a reactive prop, a signal, an `html` binding, or
123
- `ref()` expresses it, use that. Reach for vanilla DOM only for the cases
124
- above, where the platform offers nothing declarative.
125
-
126
- This is a **convention, not a lint rule.** `webjs check` is reserved for
127
- general correctness (SSR safety, server-only imports, erasable TS, and
128
- so on), not for policing every vanilla call, which would be noisy and
129
- poor DX. Use your judgment: prefer the lit form by default, and when a
130
- vanilla API genuinely has no declarative equivalent (the cases above),
131
- just use it.
132
-
133
- ## The SSR contract: the pre-render lifecycle plus `render()`
134
-
135
- By design, the webjs SSR pipeline constructs the instance, applies
136
- attributes, runs the **pre-render value-deriving hooks** (`willUpdate`,
137
- then controllers' `hostUpdate`), reflects `reflect: true` properties,
138
- and calls `instance.render()`. Nothing past render fires server-side.
139
- Not `connectedCallback`, not `shouldUpdate`, not the `update` DOM
140
- commit, not `firstUpdated`, not `updated`, not controllers'
141
- `hostConnected` / `hostUpdated`. See
142
- `packages/core/src/render-server.js` around line 357.
143
-
144
- The mental model is one sentence. Code in the constructor, `willUpdate`,
145
- and `render()` must avoid the genuinely browser-only surface (`document`,
146
- `window`, `localStorage`, `navigator`, `querySelector`, layout reads),
147
- though the attribute, event, and `attachInternals` methods are backed by
148
- a server shim and are safe. Code in every other hook is client-only and
149
- can freely use any browser API without an `isServer` guard.
150
-
151
- The gotchas below are all violations of that rule.
152
-
153
- ## Patterns that produce visibly broken SSR
154
-
155
- ### 1. Fetching data in `connectedCallback` or `firstUpdated`
156
-
157
- The lit pattern is to subscribe or fetch on connect, then update
158
- state when the data arrives. In webjs the first paint is empty because
159
- neither hook runs server-side. Content pops in after hydration, often
160
- with a layout shift.
161
-
162
- Fix. Fetch in the page function and pass the data as props or
163
- attributes.
164
-
165
- ```ts
166
- // app/users/[id]/page.ts (correct)
167
- import { fetchUser } from '../../modules/users/queries/fetch-user.server.ts';
168
- export default async function User({ params }) {
169
- const user = await fetchUser(params.id);
170
- return html`<user-card .user=${user}></user-card>`;
171
- }
172
- ```
173
-
174
- ### 2. Using `Task` for initial-paint data
175
-
176
- Lit's canonical async pattern. The `Task` controller wires up a fetcher
177
- that runs on host update. Controllers' `hostUpdate` does fire at SSR, but
178
- `Task` deliberately does not auto-run server-side: it keeps its `INITIAL`
179
- state and runs only on hydration, so no request fires during SSR. The
180
- client then renders the resolved state, causing a flash.
181
-
182
- `Task` is still useful for client-time async (interaction-triggered
183
- mutations, polling, websocket reactions). For initial-paint data, fetch
184
- in the page function OR in the component itself with an `async render()`
185
- (see the next entry), which Lit does not have.
186
-
187
- ### 2b. Lit has no async render; webjs does (#469)
188
-
189
- Lit keeps `render()` synchronous (its async-SSR work signals a promise the
190
- renderer awaits BEFORE a still-sync render). webjs lets `render()` itself be
191
- `async`, so you write `const u = await getUser(this.id)` directly in the
192
- component and SSR bakes the resolved data into the first paint. Three things
193
- trip up Lit muscle memory:
194
-
195
- - **SSR blocks by default. Streaming is NOT automatic.** A bare `async
196
- render()` (no wrapper) renders real data in the first paint with no
197
- fallback. There is no skeleton flash. To STREAM slow data (fallback on
198
- first byte), wrap the region in `<webjs-suspense .fallback=${html`…`}>`.
199
- Do not reach for a fallback expecting it to show on first load; the
200
- unwrapped default is block-real-data, and streaming is the deliberate
201
- opt-in for slow regions.
202
- - **`renderFallback()` is NOT a first-paint concern.** It is the OPTIONAL
203
- client re-fetch loading UI, shown only when a prop / dependency change
204
- re-runs `async render()`, never on the first paint. The first-paint and
205
- re-fetch defaults are both no-flash: SSR has the data, and a re-fetch keeps
206
- the stale content (stale-while-revalidate) until the new render resolves.
207
- - **You do not need an error boundary.** A thrown `await getData()` is
208
- isolated to that component automatically (siblings render, the page does
209
- not blank). Add `renderError()` only to customize the error UI.
210
-
211
- ### 3. Browser-only APIs in the constructor or `render()`
212
-
213
- Calls like `window.matchMedia(...)`, `localStorage.getItem(...)`,
214
- `navigator.userAgent`, `document.querySelector(...)` in the constructor
215
- or render path crash SSR. The constructor is for pure-JS init
216
- (defaults, method binding, instance fields). Browser APIs belong in
217
- `connectedCallback` or later hooks (which are client-only by
218
- construction).
219
-
220
- ```ts
221
- // wrong
222
- constructor() {
223
- super();
224
- this.dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
225
- }
226
-
227
- // right
228
- constructor() {
229
- super();
230
- this.dark = false;
231
- }
232
- connectedCallback() {
233
- super.connectedCallback();
234
- this.dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
235
- }
236
- ```
237
-
238
- This applies only to the genuinely browser-only HTMLElement members on
239
- `this` (`this.classList`, `this.querySelector(...)`,
240
- `this.attachShadow(...)`, `this.getBoundingClientRect(...)`, `this.focus()`):
241
- the SSR-time instance has no DOM, so they throw. The attribute methods
242
- (`this.getAttribute` / `setAttribute` / `hasAttribute` / `toggleAttribute`),
243
- the event methods (`addEventListener` / `removeEventListener` /
244
- `dispatchEvent`), and `this.attachInternals()` ARE backed by a server shim,
245
- so reading an attribute in `render()`, wiring a delegated listener in the
246
- constructor, or reflecting a property during the SSR update cycle all work.
247
- Reading attributes that drive render through a factory-declared reactive
248
- property (`WebComponent({ ... })`) is still the idiomatic path, but
249
- `this.hasAttribute(...)` no longer crashes.
250
-
251
- Two guards catch the browser-only cases. `webjs check` flags browser
252
- globals and the still-unsupported HTMLElement members used in a constructor
253
- or render body (the `no-browser-globals-in-render` rule). And if one slips
254
- through, the SSR crash is actionable: the log names the offending member and
255
- tells you to move it to `connectedCallback` or a lifecycle hook, instead of
256
- a raw `document is not defined`.
257
-
258
- ### 4. Top-level imports of browser-only libraries
259
-
260
- `import * as d3 from 'd3'`, `import Chart from 'chart.js'`, or any
261
- library that touches `window` at import time. The page module loads on
262
- the server during SSR, so the offending top-level access crashes.
263
-
264
- Two fixes. Use a dynamic `import()` inside `connectedCallback` for
265
- client-only behavior. Or wrap server-side work in a `.server.ts` file
266
- if the library has both server and client uses.
267
-
268
- ```ts
269
- connectedCallback() {
270
- super.connectedCallback();
271
- import('chart.js').then(({ Chart }) => {
272
- this.chart = new Chart(this.canvas, this.config);
273
- });
274
- }
275
- ```
276
-
277
- ## Patterns that compile but silently break reactivity
278
-
279
- ### 5. Class-field initializers for reactive properties
280
-
281
- In Lit, class-field initializers (like `student: Student = { name: '', email: '' }`) are commonly used. In webjs, the base class installs reactive accessors on `this` inside the constructor via `Object.defineProperty` (to support SSR property hydration and signals). Under modern V8 class-field semantics, a class-field initializer compiles to an assignment after `super()`, which uses `[[Define]]` and overwrites the accessor, silently breaking reactivity. The footgun is still live with factory-declared props, so it stays a gotcha.
282
-
283
- The fix is to declare the prop in the base-class factory `WebComponent({ ... })` (which types it automatically, no `declare` line) and set its default in the constructor after `super()`, never as a class-field initializer:
284
-
285
- ```ts
286
- // set the default in the constructor (fully typed, no declare needed)
287
- class StudentCard extends WebComponent({
288
- student: prop<Student>(Object),
289
- }) {
290
- constructor() {
291
- super();
292
- this.student = { name: '', email: '' };
293
- }
294
- }
295
- ```
296
-
297
- `webjs check` flags a class-field initializer on a factory-declared prop via the `reactive-props-no-class-field` rule.
298
-
299
- ### 6. The `@property()` decorator and a direct `static properties` block
300
-
301
- The `@property()` decorator is banned by framework invariant 10 (erasable TS): decorators are non-erasable, so they would force the framework to depend on a build step. A direct `static properties = { ... }` block is also gone, and webjs THROWS at runtime if a class body declares one (flagged by the `no-static-properties` rule). The single replacement for both is the declare-free base-class factory `WebComponent({ ... })` (the `prop()` helper carries options), as shown above.
302
-
303
- ## Patterns that produce different visual output
304
-
305
- ### 7. Expecting shadow DOM by default (and reaching for scoped CSS instead of Tailwind)
306
-
307
- Lit components default to shadow DOM. `static styles = css` scoping
308
- works automatically. Webjs defaults to light DOM. A `static styles`
309
- block without `static shadow = true` does nothing useful (the framework
310
- warns at runtime), and styles authored for the component bleed into
311
- the global namespace.
312
-
313
- This is also the **styling reflex** to unlearn, not just a config
314
- default. Because lit scopes, the lit habit is to author scoped CSS
315
- (`static styles = css\`\``) or an inline `<style>` with semantic class
316
- names (`.hero`, `.feature`, `.card`) for every component. In a webjs
317
- light-DOM component that CSS either does nothing (the scoped block) or
318
- leaks globally (the inline `<style>` with bare class names). **The
319
- webjs-shaped fix is Tailwind utilities, which apply directly in light
320
- DOM and are webjs's strong styling default.** Reach for raw CSS only for
321
- the short allowlist (design tokens, `@property` + `@keyframes`,
322
- `::-webkit-scrollbar`, `prefers-reduced-motion`, complex `color-mix()` /
323
- gradients); see `agent-docs/styling.md` for the full Tailwind-first rule
324
- and that allowlist.
325
-
326
- Three correct paths. Use Tailwind utilities in light DOM (the default,
327
- and the answer for the vast majority of components). Or add
328
- `static shadow = true` to the class and keep `static styles` (scoped CSS
329
- genuinely belongs in a shadow root). Or, if authoring vanilla CSS in
330
- light-DOM mode anyway, prefix every selector with the component tag, per
331
- the styling invariant. When a utility bundle repeats across light-DOM
332
- components, extract it into a `lib/utils/ui.ts` helper returning an
333
- `` html`...` `` fragment, never a shared CSS class.
334
-
335
- ### 8. `<slot>` timing differs across DOM modes
336
-
337
- Both modes accept `<slot>` syntax in templates and provide
338
- `assignedNodes`, `assignedElements`, `slotchange`, named slots,
339
- fallback content, and first-wins resolution. The public surface
340
- aligns.
341
-
342
- What differs is timing. In shadow DOM, slot projection is a browser
343
- primitive that fires synchronously on parse. In light DOM, projection
344
- is framework-driven (`packages/core/src/slot.js`) and observes
345
- mutations via `MutationObserver`. Code that reads
346
- `slot.assignedNodes()` synchronously in `connectedCallback` may see an
347
- empty list in light DOM and a populated list in shadow DOM. Use
348
- `slotchange` to react instead of reading synchronously.
349
-
350
- ## Lifecycle subtleties at SSR
351
-
352
- ### 9. `willUpdate` computing state for SSR (this now works)
353
-
354
- This used to be a gotcha. The SSR pipeline now runs `willUpdate` (and
355
- controllers' `hostUpdate`) before `render()`, so deriving render state
356
- there is correct in the first paint:
357
-
358
- ```ts
359
- willUpdate(changedProperties) {
360
- this.fullName = `${this.first} ${this.last}`;
361
- }
362
- render() {
363
- return html`<p>${this.fullName}</p>`;
364
- }
365
- ```
366
-
367
- The SSR HTML now shows `<p>Ada Lovelace</p>`. The value must still be a
368
- pure function of constructor state plus applied attributes (no
369
- browser-only APIs), since SSR has no DOM. What still does NOT run
370
- server-side is the post-render and connection hooks (`update` commit,
371
- `firstUpdated`, `updated`, `connectedCallback`, controllers'
372
- `hostConnected` / `hostUpdated`), so state those compute is absent from
373
- the first paint.
374
-
375
- One tradeoff to know: overriding `willUpdate` is an interactivity
376
- signal for the elision analyser, so a component that uses it (even
377
- purely to derive SSR state) ships its JS to the browser and is never
378
- elided. For a truly display-only component, prefer computing the value
379
- inline in `render()` so the module can still be elided; reach for
380
- `willUpdate` when the component is interactive anyway, or when the
381
- derivation is shared across `render()` and a client hook.
382
-
383
- ### 10. `ContextProvider` for server-known data
384
-
385
- Context providers in lit publish on connect via `hostConnected`. In
386
- webjs SSR, `connectedCallback` does not run, so descendants that read
387
- context during SSR see the default value (or undefined). On hydration
388
- the provider connects and consumers re-render, causing a content
389
- shift.
390
-
391
- Rule of thumb. For data known on the server (session, user, theme,
392
- locale, feature flags, A/B variants), pass it through props from the
393
- page function rather than through context. Reserve `ContextProvider`
394
- for client-time concerns (interaction state, focus management,
395
- transient UI state).
396
-
397
- ## List rendering
398
-
399
- ### 11. Reordering a `.map()` list needs a keyed `repeat()`
400
-
401
- A plain `.map()` list reconciles in place, matching lit-html's non-keyed
402
- child-part behaviour. When one item's binding changes (a card flips its
403
- `dragging` class on `@dragstart`, a row's input is edited), the framework
404
- patches that item's existing nodes instead of rebuilding the whole list,
405
- so DOM node identity survives. That is what makes native drag-and-drop,
406
- focus, caret, text selection, scroll position, and uncontrolled input
407
- value all survive an item-level update, no `repeat()` required. (This
408
- used to be a real gotcha. Before the fix, any change to a `.map()`'s
409
- output tore down and replaced every node, which silently aborted a
410
- drag-in-progress and lost focus and input state.)
411
-
412
- ```ts
413
- // Item updates preserve node identity. Drag-and-drop, focus, and
414
- // input state all survive. No repeat() needed.
415
- render() {
416
- return html`<ul>${this.cards.map((c) => html`
417
- <li class=${c.id === this.draggingId ? 'dragging' : 'idle'}
418
- draggable="true"
419
- @dragstart=${() => (this.draggingId = c.id)}>${c.text}</li>`)}</ul>`;
420
- }
421
- ```
422
-
423
- What plain `.map()` still does NOT do is **keyed reordering**.
424
- Reconciliation is positional (by index): if the array is reordered or an
425
- item is inserted/removed in the MIDDLE, index *i* is patched from the new
426
- item at *i*, so the nodes stay put and their contents are rewritten
427
- rather than the nodes themselves moving. For an item carrying live state
428
- (a focused input, a playing media element, an in-flight CSS transition)
429
- across a reorder, that state stays with the old position. When a list
430
- **reorders** or splices in the middle and node identity must follow the
431
- item, reach for the keyed directive, exactly as in lit:
432
-
433
- ```ts
434
- import { repeat } from '@webjsdev/core/directives';
435
- render() {
436
- return html`<ul>${repeat(this.cards, (c) => c.id, (c) => html`
437
- <li>${c.text}</li>`)}</ul>`;
438
- }
439
- ```
440
-
441
- Rule of thumb. Append-only or update-in-place list, where items keep
442
- their position, plain `.map()` is fine and preserves identity. List that
443
- **reorders or splices in the middle** and each item owns DOM state that
444
- must move with it, use `repeat()` with a stable key.
445
-
446
- ## Quick reference
447
-
448
- | Lit pattern | Webjs equivalent |
449
- |---|---|
450
- | Fetch in `connectedCallback` or `firstUpdated` | Fetch in the page function, pass as props |
451
- | `Task` for initial-paint data | Page function fetch and pass as props |
452
- | `Task` for client-time async | `Task` (no change, that's its job) |
453
- | `window.X` or `document.X` in constructor or `render()` | Move to `connectedCallback` |
454
- | Top-level `import` of browser-only library | Dynamic `import()` inside `connectedCallback` |
455
- | `student: Student = { ... }` field initializer | Base-class factory `WebComponent({ student: Object })`, default in the constructor after `super()` |
456
- | `@property()` decorator / `static properties` block | Base-class factory `WebComponent({ ... })` (the `prop()` helper carries options) |
457
- | `static styles = css` / inline `<style>` with semantic class names in a light-DOM component | Tailwind utilities (the default); or `static shadow = true` for genuinely scoped CSS |
458
- | Plain `.map()` for an interactive/stateful list | Works (reconciles in place, keeps node identity); use `repeat(items, key, t)` only when the list **reorders** |
459
- | `willUpdate` for SSR-visible derived state | Works (runs at SSR); keep it a pure derivation |
460
- | `this.hasAttribute` / `getAttribute` in `render()` | Works (server attribute shim) |
461
- | `ContextProvider` for server-known data | Pass via props from the page function |
462
-
463
- ## When in doubt
464
-
465
- If a pattern needs to influence the first paint, it has to be in the
466
- constructor, the page function, or `render()`. If a pattern needs the
467
- DOM, the event loop, or a browser API, it has to be in
468
- `connectedCallback` or later. There is no third category. Anything
469
- that violates this split either crashes SSR or produces a hydration
470
- flash.