@madojs/mado 0.6.1 → 0.7.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.
package/AGENTS.md CHANGED
@@ -91,6 +91,36 @@ component("x-timer", (ctx) => {
91
91
 
92
92
  **`resource()`, `effect()`, and subscriptions inside `setup()` hook into the lifecycle automatically** — no need to write onDispose for them.
93
93
 
94
+ ### 4b. Reactive attributes — `ctx.attr()`
95
+
96
+ ```ts
97
+ // ❌ NO (reading once, never reactive)
98
+ component("x-badge", ({ host }) => () => {
99
+ const variant = host.getAttribute("variant") ?? "default";
100
+ return html`<span class=${variant}>...</span>`;
101
+ });
102
+
103
+ // ❌ NO (MutationObserver boilerplate)
104
+ component("x-badge", ({ host, onDispose }) => {
105
+ const variant = signal(host.getAttribute("variant") ?? "default");
106
+ const obs = new MutationObserver(() =>
107
+ variant.set(host.getAttribute("variant") ?? "default"),
108
+ );
109
+ obs.observe(host, { attributes: true, attributeFilter: ["variant"] });
110
+ onDispose(() => obs.disconnect());
111
+ return () => html`<span class=${variant}>...</span>`;
112
+ });
113
+
114
+ // ✅ YES — one line, reactive, no cleanup needed
115
+ component("x-badge", ({ attr }) => {
116
+ const variant = attr("variant", "default");
117
+ return () => html`<span class=${() => `badge-${variant()}`}>...</span>`;
118
+ });
119
+ ```
120
+
121
+ `ctx.attr(name, defaultValue?)` returns a `Signal<string>` that auto-updates.
122
+ The attribute is automatically added to `observedAttributes`.
123
+
94
124
  ### 5. Reactive value in template child position = function
95
125
 
96
126
  The most common AI mistake:
@@ -99,13 +129,13 @@ The most common AI mistake:
99
129
  const count = signal(0);
100
130
 
101
131
  // ❌ NOT REACTIVE — count() is read once
102
- html`<div>${count() * 2}</div>`
132
+ html`<div>${count() * 2}</div>`;
103
133
 
104
134
  // ✅ REACTIVE — the function will be called when count changes
105
- html`<div>${() => count() * 2}</div>`
135
+ html`<div>${() => count() * 2}</div>`;
106
136
 
107
137
  // ✅ ALSO OK — the signal itself is a function, Mado recognizes it
108
- html`<div>${count}</div>`
138
+ html`<div>${count}</div>`;
109
139
  ```
110
140
 
111
141
  **Rule of thumb:** if there is a signal call (with parentheses) inside `${...}`, wrap it in `() => ...`.
@@ -114,17 +144,17 @@ html`<div>${count}</div>`
114
144
 
115
145
  ```ts
116
146
  // string/number → attribute
117
- html`<a href=${url}>...</a>`
147
+ html`<a href=${url}>...</a>`;
118
148
 
119
149
  // DOM property (objects, numbers without serialization, .value for input)
120
- html`<input .value=${user.name}>`
121
- html`<my-list .items=${arr}>`
150
+ html`<input .value=${user.name} />`;
151
+ html`<my-list .items=${arr}></my-list>`;
122
152
 
123
153
  // boolean attribute (toggle)
124
- html`<button ?disabled=${isLoading}>...</button>`
154
+ html`<button ?disabled=${isLoading}>...</button>`;
125
155
 
126
156
  // event
127
- html`<button @click=${fn}>...</button>`
157
+ html`<button @click=${fn}>...</button>`;
128
158
  ```
129
159
 
130
160
  Common mistake: `disabled=${loading()}` — this attempts to set a **string** attribute `disabled="true"` or `disabled="false"`, which does not work correctly. **Use `?disabled=`.**
@@ -135,10 +165,19 @@ Common mistake: `disabled=${loading()}` — this attempts to set a **string** at
135
165
  import { each } from "@madojs/mado";
136
166
 
137
167
  // ❌ Works, but no keyed reconciliation → loses focus on reorder
138
- html`<ul>${() => items().map(t => html`<li>${t.name}</li>`)}</ul>`
168
+ html`<ul>
169
+ ${() => items().map((t) => html`<li>${t.name}</li>`)}
170
+ </ul>`;
139
171
 
140
172
  // ✅ Correct: keyed, reuses DOM nodes
141
- html`<ul>${() => each(items(), t => t.id, t => html`<li>${t.name}</li>`)}</ul>`
173
+ html`<ul>
174
+ ${() =>
175
+ each(
176
+ items(),
177
+ (t) => t.id,
178
+ (t) => html`<li>${t.name}</li>`,
179
+ )}
180
+ </ul>`;
142
181
  ```
143
182
 
144
183
  ### 8. Routing — `routes()` + `page()`
@@ -198,14 +237,20 @@ import { useForm } from "@madojs/mado";
198
237
 
199
238
  const f = useForm({
200
239
  email: { required: true, type: "email" },
201
- age: { required: true, type: "number", min: 18 },
240
+ age: { required: true, type: "number", min: 18 },
202
241
  });
203
242
 
204
243
  html`
205
- <form @submit=${f.onSubmit(async v => { await api.save(v); })}>
206
- <input name="email" @input=${f.onInput} @blur=${f.onBlur}>
207
- ${() => f.touched().email && f.errors().email
208
- ? html`<small>${f.errors().email}</small>` : null}
244
+ <form
245
+ @submit=${f.onSubmit(async (v) => {
246
+ await api.save(v);
247
+ })}
248
+ >
249
+ <input name="email" @input=${f.onInput} @blur=${f.onBlur} />
250
+ ${() =>
251
+ f.touched().email && f.errors().email
252
+ ? html`<small>${f.errors().email}</small>`
253
+ : null}
209
254
  <button ?disabled=${() => !f.isValid() || f.submitting()}>Save</button>
210
255
  </form>
211
256
  `;
@@ -220,16 +265,23 @@ import { component, css, html } from "@madojs/mado";
220
265
 
221
266
  component("x-card", () => () => html`<div><slot></slot></div>`, {
222
267
  styles: css`
223
- :host { display: block; padding: 1rem; }
224
- div { background: var(--bg); }
225
- ::slotted(h2) { margin: 0; }
268
+ :host {
269
+ display: block;
270
+ padding: 1rem;
271
+ }
272
+ div {
273
+ background: var(--bg);
274
+ }
275
+ ::slotted(h2) {
276
+ margin: 0;
277
+ }
226
278
  `,
227
279
  });
228
280
 
229
281
  // Light DOM (without Shadow), global styles:
230
282
  component("x-shell", () => () => html`...`, {
231
- shadow: false, // disables Shadow DOM
232
- styles: css`x-shell header { ... }`, // selectors are written as usual
283
+ shadow: false, // disables Shadow DOM
284
+ styles: css`x-shell header { ... }`, // selectors are written as usual
233
285
  });
234
286
  ```
235
287
 
@@ -314,17 +366,17 @@ When generating an app, prefer the blessed production shape from
314
366
 
315
367
  ## Where to find specific answers
316
368
 
317
- | Question | File |
318
- |---|---|
319
- | How does reactivity work? | `src/signal.ts` (283 lines) |
320
- | How are templates parsed? | `src/html.ts` (1013 lines) |
321
- | How does the router work? | `src/router.ts` (~530 lines) |
322
- | How does resource + cache work? | `src/resource.ts` (297 lines) |
323
- | How do forms work? | `src/forms.ts` (212 lines) |
369
+ | Question | File |
370
+ | -------------------------------- | -------------------------------- |
371
+ | How does reactivity work? | `src/signal.ts` (283 lines) |
372
+ | How are templates parsed? | `src/html.ts` (1013 lines) |
373
+ | How does the router work? | `src/router.ts` (~530 lines) |
374
+ | How does resource + cache work? | `src/resource.ts` (297 lines) |
375
+ | How do forms work? | `src/forms.ts` (212 lines) |
324
376
  | How should an app be structured? | `docs/en/10-app-architecture.md` |
325
- | How should errors be handled? | `docs/en/15-error-handling.md` |
326
- | How should bake be used? | `docs/en/16-bake-cookbook.md` |
327
- | When something goes wrong | `docs/en/07-llm-pitfalls.md` |
377
+ | How should errors be handled? | `docs/en/15-error-handling.md` |
378
+ | How should bake be used? | `docs/en/16-bake-cookbook.md` |
379
+ | When something goes wrong | `docs/en/07-llm-pitfalls.md` |
328
380
 
329
381
  ## Before committing
330
382
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,88 @@
4
4
 
5
5
  Nothing yet.
6
6
 
7
+ ## 0.7.0
8
+
9
+ Reactive component props, Shadow DOM + Forms fixes, deterministic releases,
10
+ and `mado serve` unification. Motivated by stress-test findings in a real-world
11
+ admin panel (see `MADO_TEST_REPORT.md`).
12
+
13
+ ### Breaking Changes
14
+
15
+ - **`mado serve` in app-mode** no longer uses the legacy `serveStaticProject()`
16
+ fallback. It now always goes through `server/serve.mjs`, which means
17
+ `--host`, `--port`, `mado.config.json` dev.proxy, and HMR all work for
18
+ generated apps. If you relied on the old no-HMR behaviour, pass
19
+ `NO_HMR=1 mado serve`.
20
+
21
+ ### Added — Framework
22
+
23
+ - **`ctx.attr(name, defaultValue?)`** — reactive attribute accessor for
24
+ components. Returns a `Signal<string>` that auto-updates when the attribute
25
+ changes on the host element via `attributeChangedCallback`. No more
26
+ `MutationObserver` boilerplate in every component.
27
+
28
+ ```ts
29
+ component("x-badge", ({ attr }) => {
30
+ const variant = attr("variant", "default");
31
+ return () =>
32
+ html`<span class=${() => `badge-${variant()}`}><slot></slot></span>`;
33
+ });
34
+ ```
35
+
36
+ Attributes used with `ctx.attr()` are automatically added to
37
+ `observedAttributes`.
38
+
39
+ ### Added — Starters
40
+
41
+ - **`apiFetcher<T>()`** in `starters/admin/src/lib/api.ts` — a fetcher for
42
+ `resource()` that attaches the Bearer token from memory. Use for protected
43
+ endpoints instead of the plain `jsonFetcher()`.
44
+ - **`x-button`**: now bridges Shadow DOM → Light DOM form submit via
45
+ `form.requestSubmit()`. Buttons inside Shadow DOM cannot natively trigger
46
+ `<form>` submit in Light DOM — this is now handled automatically.
47
+ - **`x-button`**: uses `ctx.attr("disabled")` for reactive disabled state.
48
+ External `?disabled=${() => !form.isValid()}` now correctly enables/disables
49
+ the inner button.
50
+ - **`x-input`**: proxies `.name` and `.value` DOM properties on the host
51
+ element so that `useForm().onInput` works after Shadow DOM event retargeting.
52
+
53
+ ### Added — CLI / Build
54
+
55
+ - **`mado release --no-clean`**: release now cleans the entire `out/` directory
56
+ before building (deterministic artifacts). Pass `--no-clean` to opt out.
57
+ Previously stale assets, removed bake routes, and deleted public files could
58
+ linger in the deploy artifact.
59
+ - **`scripts/bake.mjs`**: `<title>` now falls back to `page.title` if
60
+ `head().title` is not explicitly set. Previously baked HTML kept the template
61
+ `<title>` from `index.html` — a critical SEO gap.
62
+
63
+ ### Added — Documentation
64
+
65
+ - **`docs/en/17-shadow-dom-forms.md`** — full recipe for using `useForm()` with
66
+ Shadow DOM components (proxy properties, form submit bridge, ctx.attr()).
67
+ - **`llms.txt`**: added `ctx.attr()` section, `apiFetcher` recipe, and Shadow
68
+ DOM + Forms guidance.
69
+
70
+ ### Fixed
71
+
72
+ - **`x-button` in starters**: the disabled state was read once from
73
+ `host.hasAttribute("disabled")` in the render function — never updating when
74
+ the attribute changed externally. Every form using `?disabled` on `x-button`
75
+ was broken from the start.
76
+ - **`x-input` in starters**: `useForm().onInput` received `undefined` for
77
+ `name` and `value` because Shadow DOM retargets `e.target` from the inner
78
+ `<input>` to `<x-input>`, which had no DOM properties.
79
+ - **`jsonFetcher()`**: the admin starter relied on `jsonFetcher()` for protected
80
+ endpoints but it sends no Authorization header. Documented the pattern and
81
+ added `apiFetcher()`.
82
+ - **`mado serve`**: app-mode did not respect `--host`, `--port`, or config
83
+ settings. All flag pass-through now goes through `server/serve.mjs`.
84
+ - **`mado release`**: stale files from deleted bake routes or removed public
85
+ assets could remain in `out/`. Now cleans `out/` fully before building.
86
+ - **`mado bake`**: `<title>` was not set in baked HTML if only `page.title`
87
+ was defined (without `head().title`).
88
+
7
89
  ## 0.6.1
8
90
 
9
91
  Starter & release-pipeline hardening pass. No public API breaks.
@@ -12,6 +94,7 @@ starter / bundle / bake / dev-server contour. All fixes verified by
12
94
  regression tests added in this release.
13
95
 
14
96
  ### Fixed
97
+
15
98
  - **Starters**: every `index.html` in `starters/{admin,crud,minimal}/` now
16
99
  uses root-absolute paths in the importmap and entry `<script>` tag
17
100
  (`/node_modules/@madojs/mado/...`, `/dist/main.js`). Relative paths
@@ -20,7 +103,7 @@ regression tests added in this release.
20
103
  `/admin/orders/dist/main.js` → 404 → blank page). Inline comments in each
21
104
  file explain the trap so it does not get reverted.
22
105
  - **Starters/admin**: `pages/admin/order-detail.ts` now uses `each(items,
23
- key, render)` instead of `o.items.map(...)`, matching `llms.txt` rule #3
106
+ key, render)` instead of `o.items.map(...)`, matching `llms.txt` rule #3
24
107
  and the framework's own pitfalls documentation.
25
108
  - **`scripts/bundle.mjs`**: cleans stale hashed assets before every build.
26
109
  Previously each `mado bundle` / `mado release` left old `main-<hash>.js`
@@ -57,6 +140,7 @@ regression tests added in this release.
57
140
  had succeeded.
58
141
 
59
142
  ### Added
143
+
60
144
  - **`mado dev` / `mado serve` flag pass-through**: `cli.mjs` now splits
61
145
  positional arguments from flags via `splitDevArgs()`, so calls like
62
146
  `mado dev --host 127.0.0.1`, `mado dev showcase --port 6000` and
@@ -71,7 +155,7 @@ regression tests added in this release.
71
155
  - **`scripts/bake.mjs`**: fails loudly when the manifest exists but no
72
156
  page declares `bake: { paths, data }`. The previous behaviour produced
73
157
  `0 pages + sitemap.xml` silently with exit code 0, making `mado
74
- release` look successful while shipping only the SPA shell with no
158
+ release` look successful while shipping only the SPA shell with no
75
159
  SEO-friendly HTML. The new warning prints the skipped routes, a
76
160
  worked example bake snippet, and exits non-zero. Override with
77
161
  `MADO_BAKE_ALLOW_EMPTY=1` for intentional SPA-only deploys.
@@ -99,12 +183,14 @@ regression tests added in this release.
99
183
  response.
100
184
 
101
185
  ### Changed
186
+
102
187
  - **`server/serve.mjs`** default host is now `localhost` (was implicitly
103
188
  `0.0.0.0`). LAN exposure is opt-in via `mado dev --host 0.0.0.0` or
104
189
  `HOST=0.0.0.0`. The startup banner shows both the bound host and a
105
190
  click-friendly URL (`localhost` substituted when bound to `0.0.0.0`).
106
191
 
107
192
  ### Notes
193
+
108
194
  - No public API changes; no migrations required. Apps that previously
109
195
  worked on a fresh-out-of-the-box `mado init` did so only because
110
196
  someone manually fixed the starter's relative paths and dev deps —
@@ -124,12 +210,13 @@ pipeline, core hardening and v1 recipe docs.
124
210
  Phase 1 — Repo-vs-app split:
125
211
 
126
212
  ### Added
213
+
127
214
  - `MADO_V1_PLAN.md` — executable tracker for the v1 push.
128
215
  - `scripts/_config.mjs` — single configuration loader (defaults < `mado.config.json`
129
216
  < CLI flags). Exports `loadConfig`, `detectContext`, `parseFlags`,
130
217
  `resolveProjectPath`. [v1 F1.1]
131
218
  - `mado release` command: one-shot `typecheck + build + bundle + bake + copy
132
- public/ → out/` pipeline so apps have exactly one command to ship. [v1 F1.3]
219
+ public/ → out/` pipeline so apps have exactly one command to ship. [v1 F1.3]
133
220
  - `mado.config.json` shipped in the `minimal` and `crud` starters with the
134
221
  default app-mode layout (`src/routes.ts`, `index.html`, `out/`). [v1 F1.4]
135
222
  - Tests: `test/config-loader.test.mjs`, `test/bake-cli.test.mjs` (11 + 3
@@ -137,6 +224,7 @@ Phase 1 — Repo-vs-app split:
137
224
  flags, and the no-more-silent-`[object Object]` contract). [v1 F1.6]
138
225
 
139
226
  ### Changed
227
+
140
228
  - `scripts/bake.mjs` now reads configuration from `mado.config.json` and
141
229
  accepts `--entry`, `--template`, `--out`, `--base-url` flags. In app-mode
142
230
  defaults are `src/routes.ts` + `index.html` + `out/baked/`; the
@@ -158,6 +246,7 @@ Phase 1 — Repo-vs-app split:
158
246
  Phase 2 — One blessed way:
159
247
 
160
248
  ### Added
249
+
161
250
  - `layout()` factory in `src/page.ts` (alias of `nested()`) plus `Guard` and
162
251
  `GuardResult` types. Exported from the public API. [v1 F2.1 / F2.3]
163
252
  - Route guards: nested groups and individual pages accept `guard: Guard | Guard[]`.
@@ -182,6 +271,7 @@ Phase 2 — One blessed way:
182
271
  Phase 3 — Bake first-class + Release pipeline:
183
272
 
184
273
  ### Added
274
+
185
275
  - `mado release` writes `_redirects` (`/* /index.html 200`) and `_headers`
186
276
  (immutable for `/assets/*`, no-cache for HTML) into `out/` when they do not
187
277
  exist, so Cloudflare Pages / Netlify deploys "just work". [v1 F3.7]
@@ -195,6 +285,7 @@ Phase 3 — Bake first-class + Release pipeline:
195
285
  and copied `public/` assets are all present. [v1 F3.10]
196
286
 
197
287
  ### Changed
288
+
198
289
  - `scripts/preview.mjs` now reads `mado.config.json` (`build.out`, `dev.port`),
199
290
  refuses to auto-build by default in app-mode, and asks the user to run
200
291
  `mado release` first. Legacy auto-build is opt-in via `PREVIEW_AUTOBUILD=1`
@@ -209,6 +300,7 @@ Phase 3 — Bake first-class + Release pipeline:
209
300
  dependencies. The startup banner prints the active proxy table. [v1 F3.6]
210
301
 
211
302
  ### Deferred to v0.7
303
+
212
304
  - `mado dev` does not yet serve baked routes inline. Workaround: run
213
305
  `mado release && mado preview`. [v1 F3.2]
214
306
  - `mado check` (bake-safety scan over `bake:` routes) is not exposed yet.
@@ -218,6 +310,7 @@ Phase 3 — Bake first-class + Release pipeline:
218
310
  Phase 4 — Core hardening:
219
311
 
220
312
  ### Added
313
+
221
314
  - `computed(fn, { equals })` option to suppress subscriber reruns when an
222
315
  observed computed recomputes to an equal value. [v1 F4.3]
223
316
  - HTML directives: `unsafeHTML()`, `ref()`, `classMap()` and `styleMap()` are
@@ -239,6 +332,7 @@ Phase 4 — Core hardening:
239
332
  its regression tests. [v1 F4.9]
240
333
 
241
334
  ### Changed
335
+
242
336
  - `computed()` now releases dependency subscriptions after unobserved reads and
243
337
  after the last subscriber is disposed, avoiding long-lived stale subscriptions
244
338
  in the signal graph. [v1 F4.1]
@@ -257,6 +351,7 @@ Phase 4 — Core hardening:
257
351
  Phase 5 — Documentation:
258
352
 
259
353
  ### Added
354
+
260
355
  - `docs/en/10-app-architecture.md`, `14-testing.md`, `15-error-handling.md`
261
356
  and `16-bake-cookbook.md` complete the v1 English recipe set. [v1 F5.1-F5.4]
262
357
  - `AGENTS.md` now includes an "App architecture for LLM" section and `llms.txt`
@@ -15,13 +15,25 @@
15
15
  * Shadow DOM (open) is used by default. It can be disabled, and
16
16
  * styles will be scoped via @scope (or a tag-prefix fallback).
17
17
  */
18
- import { type Disposer } from "./signal.js";
18
+ import { type Signal, type Disposer } from "./signal.js";
19
19
  import { html, type TemplateResult } from "./html.js";
20
20
  import { type CSSResult } from "./css.js";
21
21
  export interface ComponentContext {
22
22
  host: HTMLElement;
23
23
  /** Run cleanup when the component is removed. */
24
24
  onDispose(fn: Disposer): void;
25
+ /**
26
+ * Reactive attribute accessor. Returns a Signal<string> that updates
27
+ * automatically whenever the attribute changes on the host element.
28
+ *
29
+ * const variant = ctx.attr("variant", "primary");
30
+ * return () => html`<div class=${variant()}>…</div>`;
31
+ *
32
+ * No MutationObserver boilerplate needed — the signal updates via
33
+ * attributeChangedCallback. The attribute name is automatically added to
34
+ * observedAttributes if not already listed.
35
+ */
36
+ attr(name: string, defaultValue?: string): Signal<string>;
25
37
  }
26
38
  export type SetupFn = (ctx: ComponentContext) => () => TemplateResult;
27
39
  export type StyleInput = string | CSSResult | Array<string | CSSResult>;
@@ -38,9 +50,10 @@ export interface ComponentOptions {
38
50
  /**
39
51
  * List of observed attributes.
40
52
  *
41
- * v0.3: this is plain reflection into host[attr], without a reactive props API.
42
- * If you need to re-render the component when an attribute changes, create a signal()
43
- * inside setup() and update it manually from your own wrapper/event.
53
+ * Attributes listed here are reflected to host[attr] via
54
+ * attributeChangedCallback and also power ctx.attr() reactive signals.
55
+ * You only need to list them here if you use the legacy property reflection
56
+ * pattern. ctx.attr() automatically registers any attribute it tracks.
44
57
  */
45
58
  observedAttributes?: readonly string[];
46
59
  }
@@ -15,10 +15,10 @@
15
15
  * Shadow DOM (open) is used by default. It can be disabled, and
16
16
  * styles will be scoped via @scope (or a tag-prefix fallback).
17
17
  */
18
- import { effect } from "./signal.js";
18
+ import { signal, effect } from "./signal.js";
19
19
  import { html, render } from "./html.js";
20
20
  import { adopt, scopeStyles } from "./css.js";
21
- import { createLifecycle, runInLifecycle } from "./lifecycle.js";
21
+ import { createLifecycle, runInLifecycle, } from "./lifecycle.js";
22
22
  import { warnOnce } from "./diagnostics.js";
23
23
  export function component(tagName, setup, options = {}) {
24
24
  if (!tagName.includes("-")) {
@@ -36,18 +36,22 @@ export function component(tagName, setup, options = {}) {
36
36
  }
37
37
  const useShadow = options.shadow !== false;
38
38
  const observed = options.observedAttributes ?? [];
39
+ // Collect attribute names that ctx.attr() will track. These are merged with
40
+ // options.observedAttributes to form the final static observedAttributes.
41
+ const attrSignalNames = new Set(observed);
39
42
  // Normalize styles to an array of CSSStyleSheet once.
40
43
  // Sheets are shared across all instances — memory is not duplicated.
41
44
  const stylesheets = normalizeStyles(options.styles, tagName, useShadow);
42
45
  class MadoElement extends HTMLElement {
43
46
  static get observedAttributes() {
44
- return [...observed];
47
+ return [...attrSignalNames];
45
48
  }
46
49
  #root;
47
50
  #renderer = null;
48
51
  #effectDispose = null;
49
52
  #lifecycle = null;
50
53
  #connected = false;
54
+ #attrSignals = new Map();
51
55
  constructor() {
52
56
  super();
53
57
  this.#root = useShadow ? this.attachShadow({ mode: "open" }) : this;
@@ -69,12 +73,25 @@ export function component(tagName, setup, options = {}) {
69
73
  // getCurrentLifecycle() and register its own cleanup.
70
74
  const lifecycle = createLifecycle();
71
75
  this.#lifecycle = lifecycle;
76
+ const host = this;
72
77
  const ctx = {
73
78
  host: this,
74
79
  // ctx.onDispose proxies to lifecycle — the single source of truth
75
80
  // for component cleanups (including auto-cleanup from
76
81
  // resource(), navigator listeners, etc.).
77
82
  onDispose: (fn) => lifecycle.onDispose(fn),
83
+ attr(name, defaultValue = "") {
84
+ let s = host.#attrSignals.get(name);
85
+ if (!s) {
86
+ s = signal(host.getAttribute(name) ?? defaultValue);
87
+ host.#attrSignals.set(name, s);
88
+ // Ensure the attribute is observed. For the current instance
89
+ // we already have the signal; for future registrations of
90
+ // the same tag (HMR) the set persists.
91
+ attrSignalNames.add(name);
92
+ }
93
+ return s;
94
+ },
78
95
  };
79
96
  this.#renderer = runInLifecycle(lifecycle, () => setup(ctx));
80
97
  this.#effectDispose = effect(() => {
@@ -89,7 +106,12 @@ export function component(tagName, setup, options = {}) {
89
106
  this.#connected = false;
90
107
  }
91
108
  attributeChangedCallback(name, _old, value) {
92
- // reflect attribute to property user can bind a signal to it
109
+ // Update ctx.attr() signal if it exists for this attribute.
110
+ const s = this.#attrSignals.get(name);
111
+ if (s) {
112
+ s.set(value ?? "");
113
+ }
114
+ // Legacy reflection: reflect attribute to property.
93
115
  this[name] = value;
94
116
  }
95
117
  }
@@ -1 +1 @@
1
- {"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/component.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/component.ts"],"names":[],"mappings":"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"}