@async/framework 0.3.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@async/framework",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "No-build AsyncLoader app runtime with signals, command events, server calls, route partials, cache split, SSR activation, and streaming boundaries.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.1.0",
@@ -28,19 +28,21 @@
28
28
  "web-framework"
29
29
  ],
30
30
  "license": "MIT",
31
- "unpkg": "./src/index.js",
31
+ "unpkg": "./framework.js",
32
32
  "exports": {
33
33
  ".": {
34
- "unpkg": "./src/index.js",
35
- "browser": "./src/index.js",
34
+ "unpkg": "./framework.js",
35
+ "browser": "./framework.js",
36
36
  "import": "./src/index.js",
37
37
  "default": "./src/index.js"
38
38
  },
39
+ "./framework.js": "./framework.js",
39
40
  "./package.json": "./package.json"
40
41
  },
41
42
  "files": [
42
43
  "CHANGELOG.md",
43
44
  "README.md",
45
+ "framework.js",
44
46
  "src",
45
47
  "examples",
46
48
  "LICENSE",
@@ -48,10 +50,12 @@
48
50
  ],
49
51
  "scripts": {
50
52
  "async-pipeline": "async-pipeline",
53
+ "bundle": "node scripts/build-framework-bundle.js",
54
+ "bundle:check": "node scripts/build-framework-bundle.js --check",
51
55
  "docs:build": "node scripts/build-pages.js",
52
56
  "examples": "node --test tests/examples.test.js",
53
57
  "examples:check": "node --test tests/examples.test.js",
54
- "pack:check": "npm pack --dry-run --ignore-scripts",
58
+ "pack:check": "pnpm run bundle:check && npm pack --dry-run --ignore-scripts",
55
59
  "pipeline:github:check": "async-pipeline github check",
56
60
  "pipeline:github:generate": "async-pipeline github generate",
57
61
  "pipeline:pages": "async-pipeline run pages",
package/src/attributes.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const defaultPrefixes = Object.freeze({
2
2
  async: ["async:"],
3
+ class: ["class:"],
3
4
  signal: ["signal:"],
4
5
  on: ["on:"]
5
6
  });
@@ -11,6 +12,7 @@ export function defineAttributeConfig(config = {}) {
11
12
  export function normalizeAttributeConfig(config = {}) {
12
13
  return {
13
14
  async: normalizePrefixes(config.async, defaultPrefixes.async),
15
+ class: normalizePrefixes(config.class, defaultPrefixes.class),
14
16
  signal: normalizePrefixes(config.signal, defaultPrefixes.signal),
15
17
  on: normalizePrefixes(config.on, defaultPrefixes.on)
16
18
  };
package/src/component.js CHANGED
@@ -71,29 +71,46 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
71
71
 
72
72
  const scope = `${parentScope}.${componentName(Component)}.${++componentCounter}`;
73
73
  const cleanups = [];
74
- const mountHooks = [];
74
+ const attachHooks = [];
75
75
  const visibleHooks = [];
76
+ const destroyHooks = [];
77
+ const bindingIds = [];
76
78
  const context = createComponentContext({
77
79
  runtime,
78
80
  scope,
79
81
  cleanups,
80
- mountHooks,
81
- visibleHooks
82
+ attachHooks,
83
+ visibleHooks,
84
+ destroyHooks
82
85
  });
83
86
 
84
87
  const output = Component.call(context, props);
85
- const html = renderTemplate(output);
88
+ const html = renderTemplate(output, {
89
+ attributes: runtime.attributes,
90
+ signals: runtime.signals,
91
+ bind(value) {
92
+ const id = runtime.loader?._registerBinding?.(value);
93
+ if (!id) {
94
+ throw new Error("Inline template bindings require an AsyncLoader.");
95
+ }
96
+ bindingIds.push(id);
97
+ return id;
98
+ }
99
+ });
86
100
 
87
101
  return {
88
102
  html,
89
- mount(target) {
90
- for (const hook of mountHooks) {
103
+ attach(target) {
104
+ for (const hook of attachHooks) {
91
105
  const cleanup = hook(target);
92
106
  if (typeof cleanup === "function") {
93
107
  cleanups.push(cleanup);
94
108
  }
95
109
  }
96
110
  },
111
+ mount(target) {
112
+ this.attach(target);
113
+ },
97
114
  visible(target, observeVisible) {
98
115
  for (const hook of visibleHooks) {
99
116
  const cleanup = observeVisible(target, hook);
@@ -103,15 +120,24 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
103
120
  }
104
121
  },
105
122
  cleanup() {
123
+ while (destroyHooks.length > 0) {
124
+ destroyHooks.pop()?.();
125
+ }
106
126
  while (cleanups.length > 0) {
107
127
  cleanups.pop()?.();
108
128
  }
129
+ while (bindingIds.length > 0) {
130
+ runtime.loader?._releaseBinding?.(bindingIds.pop());
131
+ }
109
132
  }
110
133
  };
111
134
  }
112
135
 
113
- function createComponentContext({ runtime, scope, cleanups, mountHooks, visibleHooks }) {
136
+ function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks }) {
114
137
  const { signals, handlers, loader, server, router, cache } = runtime;
138
+ const generatedHandlers = new WeakMap();
139
+ let generatedHandlerCounter = 0;
140
+ let generatedSignalCounter = 0;
115
141
  const context = {
116
142
  scope,
117
143
  signals,
@@ -122,6 +148,9 @@ function createComponentContext({ runtime, scope, cleanups, mountHooks, visibleH
122
148
  cache,
123
149
 
124
150
  signal(name, initial) {
151
+ if (arguments.length === 1) {
152
+ return signals.ensure(scoped(scope, `signal.${++generatedSignalCounter}`), name);
153
+ }
125
154
  return signals.ensure(scoped(scope, name), initial);
126
155
  },
127
156
 
@@ -150,31 +179,70 @@ function createComponentContext({ runtime, scope, cleanups, mountHooks, visibleH
150
179
  },
151
180
 
152
181
  handler(name, fn) {
153
- const id = scoped(scope, name);
154
- handlers.register(id, function runComponentHandler(handlerContext) {
155
- return fn.call({ ...context, ...handlerContext }, handlerContext);
156
- });
157
- return id;
182
+ if (typeof name === "function" && fn === undefined) {
183
+ const inlineFn = name;
184
+ if (generatedHandlers.has(inlineFn)) {
185
+ return generatedHandlers.get(inlineFn);
186
+ }
187
+ const id = registerScopedHandler(`handler.${++generatedHandlerCounter}`, inlineFn);
188
+ generatedHandlers.set(inlineFn, id);
189
+ return id;
190
+ }
191
+ if (typeof fn !== "function") {
192
+ throw new TypeError("this.handler(name, fn) or this.handler(fn) requires a function.");
193
+ }
194
+ return registerScopedHandler(name, fn);
158
195
  },
159
196
 
160
197
  render(Child, childProps = {}) {
161
198
  const child = renderComponent(Child, childProps, runtime, scope);
162
199
  cleanups.push(child.cleanup);
163
- mountHooks.push((target) => child.mount(target));
200
+ attachHooks.push((target) => child.attach(target));
164
201
  visibleHooks.push((target) => child.visible(target, loader._observeVisible));
165
202
  return rawHtml(child.html);
166
203
  },
167
204
 
205
+ on(eventName, fn) {
206
+ if (typeof eventName !== "string" || eventName.length === 0) {
207
+ throw new TypeError("Component lifecycle event must be a non-empty string.");
208
+ }
209
+ if (typeof fn !== "function") {
210
+ throw new TypeError(`Component lifecycle "${eventName}" requires a function.`);
211
+ }
212
+ const event = eventName === "mount" ? "attach" : eventName;
213
+ if (event === "attach") {
214
+ attachHooks.push((target) => fn.call(context, target));
215
+ return;
216
+ }
217
+ if (event === "visible") {
218
+ visibleHooks.push((target) => fn.call(context, target));
219
+ return;
220
+ }
221
+ if (event === "destroy") {
222
+ destroyHooks.push(() => fn.call(context));
223
+ return;
224
+ }
225
+ throw new Error(`Unsupported component lifecycle event "${eventName}".`);
226
+ },
227
+
168
228
  onMount(fn) {
169
- mountHooks.push((target) => fn.call(context, target));
229
+ context.on("attach", fn);
170
230
  },
171
231
 
172
232
  onVisible(fn) {
173
- visibleHooks.push((target) => fn.call(context, target));
233
+ context.on("visible", fn);
174
234
  }
175
235
  };
176
236
 
177
237
  return context;
238
+
239
+ function registerScopedHandler(name, fn) {
240
+ const id = scoped(scope, name);
241
+ handlers.register(id, function runComponentHandler(handlerContext) {
242
+ return fn.call({ ...context, ...handlerContext }, handlerContext);
243
+ });
244
+ return id;
245
+ }
178
246
  }
179
247
 
180
248
  function scoped(scope, name) {
package/src/html.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isSignalRef } from "./signals.js";
2
+ import { attributeName, matchAttribute, normalizeAttributeConfig } from "./attributes.js";
2
3
 
3
4
  const templateKind = Symbol.for("@async/framework.template");
4
5
  const rawKind = Symbol.for("@async/framework.rawHtml");
@@ -22,29 +23,36 @@ export function rawHtml(value) {
22
23
  };
23
24
  }
24
25
 
25
- export function renderTemplate(value) {
26
+ export function renderTemplate(value, options = {}) {
26
27
  if (isTemplateResult(value)) {
28
+ const context = createRenderContext(options);
27
29
  let output = "";
28
30
  for (let index = 0; index < value.strings.length; index += 1) {
29
31
  output += value.strings[index];
30
32
  if (index < value.values.length) {
31
- output += renderValue(value.values[index]);
33
+ output += renderValue(value.values[index], {
34
+ ...context,
35
+ attribute: readAttributeContext(value.strings[index])
36
+ });
32
37
  }
33
38
  }
34
39
  return output;
35
40
  }
36
- return renderValue(value);
41
+ return renderValue(value, createRenderContext(options));
37
42
  }
38
43
 
39
- function renderValue(value) {
44
+ function renderValue(value, context = createRenderContext()) {
40
45
  if (value?.[rawKind]) {
41
46
  return value.html;
42
47
  }
43
48
  if (isTemplateResult(value)) {
44
- return renderTemplate(value);
49
+ return renderTemplate(value, context);
50
+ }
51
+ if (context.attribute) {
52
+ return renderAttributeValue(value, context);
45
53
  }
46
54
  if (Array.isArray(value)) {
47
- return value.map(renderValue).join("");
55
+ return value.map((item) => renderValue(item, context)).join("");
48
56
  }
49
57
  if (isSignalRef(value)) {
50
58
  return escapeHtml(value.value);
@@ -55,6 +63,91 @@ function renderValue(value) {
55
63
  return escapeHtml(value);
56
64
  }
57
65
 
66
+ function renderAttributeValue(value, context) {
67
+ const signalName = matchAttribute(context.attribute.name, context.attributes, "signal");
68
+ const className = matchAttribute(context.attribute.name, context.attributes, "class");
69
+ const signalPath = signalPathFor(value, context);
70
+
71
+ if (context.attribute.name === "value" && signalPath) {
72
+ const currentValue = readSignalValue(value, context);
73
+ const signalValueAttribute = attributeName(context.attributes, "signal", "value");
74
+ return `${escapeHtml(currentValue)}${context.attribute.quote} ${signalValueAttribute}=${context.attribute.quote}${escapeHtml(signalPath)}`;
75
+ }
76
+
77
+ if (signalName != null || className != null) {
78
+ if (signalPath) {
79
+ return escapeHtml(signalPath);
80
+ }
81
+ if (isInlineBindingValue(value)) {
82
+ return escapeHtml(registerInlineBinding(value, context));
83
+ }
84
+ }
85
+
86
+ return renderValueAsAttributeLiteral(value, context);
87
+ }
88
+
89
+ function renderValueAsAttributeLiteral(value, context) {
90
+ if (Array.isArray(value)) {
91
+ return value.map((item) => renderValueAsAttributeLiteral(item, context)).join("");
92
+ }
93
+ if (isSignalRef(value)) {
94
+ return escapeHtml(value.value);
95
+ }
96
+ if (value == null || value === false) {
97
+ return "";
98
+ }
99
+ return escapeHtml(value);
100
+ }
101
+
102
+ function createRenderContext(options = {}) {
103
+ return {
104
+ ...options,
105
+ attributes: normalizeAttributeConfig(options.attributes)
106
+ };
107
+ }
108
+
109
+ function readAttributeContext(source) {
110
+ const match = source.match(/(?:^|[\s<])([^\s"'=<>`]+)\s*=\s*(["'])$/);
111
+ if (!match) {
112
+ return null;
113
+ }
114
+ return {
115
+ name: match[1],
116
+ quote: match[2]
117
+ };
118
+ }
119
+
120
+ function signalPathFor(value, context) {
121
+ if (isSignalRef(value)) {
122
+ return value.id;
123
+ }
124
+ if (typeof value === "string" && context.signals?.has?.(value)) {
125
+ return value;
126
+ }
127
+ return null;
128
+ }
129
+
130
+ function readSignalValue(value, context) {
131
+ if (isSignalRef(value)) {
132
+ return value.value;
133
+ }
134
+ if (typeof value === "string" && context.signals?.has?.(value)) {
135
+ return context.signals.get(value);
136
+ }
137
+ return value;
138
+ }
139
+
140
+ function isInlineBindingValue(value) {
141
+ return Boolean(value && typeof value === "object");
142
+ }
143
+
144
+ function registerInlineBinding(value, context) {
145
+ if (typeof context.bind !== "function") {
146
+ return value;
147
+ }
148
+ return context.bind(value);
149
+ }
150
+
58
151
  export function escapeHtml(value) {
59
152
  return String(value)
60
153
  .replaceAll("&", "&amp;")
package/src/loader.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { renderComponent } from "./component.js";
2
2
  import { createHandlerRegistry } from "./handlers.js";
3
- import { createSignalRegistry } from "./signals.js";
3
+ import { createSignalRegistry, isSignalRef } from "./signals.js";
4
4
  import { matchAttribute, normalizeAttributeConfig, readAttribute } from "./attributes.js";
5
5
 
6
+ const inlineBindingPrefix = "__async:inline:";
7
+
6
8
  export function AsyncLoader({ root, signals, handlers, server, router, cache, attributes } = {}) {
7
9
  const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
8
10
  const rootNode = root ?? documentRef;
@@ -16,6 +18,8 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
16
18
  const visibleElements = new WeakSet();
17
19
  const boundaryState = new WeakMap();
18
20
  const renderingBoundaries = new WeakSet();
21
+ const inlineBindings = new Map();
22
+ let inlineBindingCounter = 0;
19
23
  let destroyed = false;
20
24
 
21
25
  const api = {
@@ -36,6 +40,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
36
40
  scan(rootOrFragment = rootNode) {
37
41
  assertActive();
38
42
  bindSignalAttributes(rootOrFragment);
43
+ bindClassAttributes(rootOrFragment);
39
44
  bindEventAttributes(rootOrFragment);
40
45
  bindBoundaries(rootOrFragment);
41
46
  runPseudoEvents(rootOrFragment);
@@ -85,6 +90,16 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
85
90
 
86
91
  _observeVisible(target, fn) {
87
92
  return observeVisible(target, fn);
93
+ },
94
+
95
+ _registerBinding(value) {
96
+ const id = `${inlineBindingPrefix}${++inlineBindingCounter}`;
97
+ inlineBindings.set(id, value);
98
+ return id;
99
+ },
100
+
101
+ _releaseBinding(id) {
102
+ inlineBindings.delete(id);
88
103
  }
89
104
  };
90
105
 
@@ -100,7 +115,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
100
115
  if (!eventName) {
101
116
  continue;
102
117
  }
103
- if (eventName === "mount" || eventName === "visible") {
118
+ if (eventName === "attach" || eventName === "mount" || eventName === "visible") {
104
119
  continue;
105
120
  }
106
121
  bindEvent(element, eventName, element.getAttribute(name));
@@ -175,15 +190,62 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
175
190
  if (signalName.startsWith("class:")) {
176
191
  const className = signalName.slice("class:".length);
177
192
  const path = element.getAttribute(name);
178
- bindSignal(element, `class:${className}:${path}`, path, (value) => {
179
- element.classList.toggle(className, Boolean(value));
180
- });
193
+ if (className === "" || className === "{}") {
194
+ bindClass(element, className, path);
195
+ } else {
196
+ bindSignal(element, `class:${className}:${path}`, path, (value) => {
197
+ element.classList.toggle(className, Boolean(value));
198
+ });
199
+ }
200
+ continue;
201
+ }
202
+ if (signalName === "class") {
203
+ const path = element.getAttribute(name);
204
+ bindClass(element, "{}", path);
181
205
  }
182
206
  }
183
207
  }
184
208
  }
185
209
 
186
- function bindSignal(element, key, path, apply) {
210
+ function bindClassAttributes(scope) {
211
+ for (const element of elementsIn(scope)) {
212
+ for (const name of element.getAttributeNames?.() ?? []) {
213
+ const className = matchAttribute(name, attributeConfig, "class");
214
+ if (className == null) {
215
+ continue;
216
+ }
217
+ bindClass(element, className, element.getAttribute(name));
218
+ }
219
+ }
220
+ }
221
+
222
+ function bindClass(element, className, path) {
223
+ if (className === "" || className === "{}") {
224
+ const staticClasses = readClassTokens(element);
225
+ let previous = new Set();
226
+ bindSignal(element, `class:{}:${path}`, path, (value) => {
227
+ const next = normalizeClassTokens(value);
228
+ const current = readClassTokens(element);
229
+ for (const token of previous) {
230
+ if (!next.has(token) && !staticClasses.has(token)) {
231
+ current.delete(token);
232
+ }
233
+ }
234
+ for (const token of next) {
235
+ current.add(token);
236
+ }
237
+ writeClassTokens(element, current);
238
+ previous = next;
239
+ }, { rawInline: true });
240
+ return;
241
+ }
242
+
243
+ bindSignal(element, `class:${className}:${path}`, path, (value) => {
244
+ updateClassToken(element, className, Boolean(value));
245
+ });
246
+ }
247
+
248
+ function bindSignal(element, key, path, apply, options = {}) {
187
249
  const bound = signalBindings.get(element) ?? new Set();
188
250
  if (bound.has(key)) {
189
251
  return;
@@ -191,8 +253,9 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
191
253
  bound.add(key);
192
254
  signalBindings.set(element, bound);
193
255
 
194
- apply(signalRegistry.get(path));
195
- cleanups.add(signalRegistry.subscribe(path, apply));
256
+ const read = () => readBinding(path, options);
257
+ apply(read());
258
+ cleanups.add(subscribeBinding(path, () => apply(read())));
196
259
  }
197
260
 
198
261
  function bindValueWriter(element, path) {
@@ -200,11 +263,42 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
200
263
  bindEvent(element, "change", `__async:set:${path}`);
201
264
  if (!handlerRegistry.resolve(`__async:set:${path}`)) {
202
265
  handlerRegistry.register(`__async:set:${path}`, function writeValue({ element }) {
203
- signalRegistry.set(path, element.value);
266
+ writeBinding(path, element.value);
204
267
  });
205
268
  }
206
269
  }
207
270
 
271
+ function readBinding(path, options = {}) {
272
+ if (isInlineBinding(path)) {
273
+ const value = inlineBindings.get(path);
274
+ return options.rawInline ? value : resolveInlineValue(value);
275
+ }
276
+ return signalRegistry.get(path);
277
+ }
278
+
279
+ function writeBinding(path, value) {
280
+ if (!isInlineBinding(path)) {
281
+ return signalRegistry.set(path, value);
282
+ }
283
+ const binding = inlineBindings.get(path);
284
+ if (isSignalRef(binding)) {
285
+ return binding.set(value);
286
+ }
287
+ throw new Error(`Inline binding "${path}" is not writable.`);
288
+ }
289
+
290
+ function subscribeBinding(path, fn) {
291
+ if (!isInlineBinding(path)) {
292
+ return signalRegistry.subscribe(path, fn);
293
+ }
294
+ const cleanups = collectSignalRefs(inlineBindings.get(path)).map((ref) => ref.subscribe(fn));
295
+ return () => {
296
+ for (const cleanup of cleanups) {
297
+ cleanup();
298
+ }
299
+ };
300
+ }
301
+
208
302
  function bindBoundaries(scope) {
209
303
  for (const boundary of elementsIn(scope)) {
210
304
  if (renderingBoundaries.has(boundary)) {
@@ -252,15 +346,17 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
252
346
 
253
347
  function runPseudoEvents(scope) {
254
348
  for (const element of elementsIn(scope)) {
255
- const ref = readAttribute(element, attributeConfig, "on", "mount");
256
- if (ref == null) {
349
+ const refs = readPseudoRefs(element, ["attach", "mount"]);
350
+ if (refs.length === 0) {
257
351
  continue;
258
352
  }
259
353
  if (mountedElements.has(element)) {
260
354
  continue;
261
355
  }
262
356
  mountedElements.add(element);
263
- runPseudo(element, ref);
357
+ for (const ref of refs) {
358
+ runPseudo(element, ref);
359
+ }
264
360
  }
265
361
 
266
362
  for (const element of elementsIn(scope)) {
@@ -276,6 +372,17 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
276
372
  }
277
373
  }
278
374
 
375
+ function readPseudoRefs(element, names) {
376
+ const refs = [];
377
+ for (const name of names) {
378
+ const ref = readAttribute(element, attributeConfig, "on", name);
379
+ if (ref != null) {
380
+ refs.push(ref);
381
+ }
382
+ }
383
+ return refs;
384
+ }
385
+
279
386
  async function runPseudo(element, ref) {
280
387
  try {
281
388
  const results = await handlerRegistry.run(ref, {
@@ -330,6 +437,110 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
330
437
  return api;
331
438
  }
332
439
 
440
+ function normalizeClassTokens(value, tokens = new Set()) {
441
+ if (value == null || value === false) {
442
+ return tokens;
443
+ }
444
+ if (isSignalRef(value)) {
445
+ const signalValue = value.value;
446
+ if (signalValue === true) {
447
+ tokens.add(signalClassName(value.id));
448
+ return tokens;
449
+ }
450
+ return normalizeClassTokens(signalValue, tokens);
451
+ }
452
+ if (typeof value === "string") {
453
+ for (const token of value.split(/\s+/).filter(Boolean)) {
454
+ tokens.add(token);
455
+ }
456
+ return tokens;
457
+ }
458
+ if (Array.isArray(value)) {
459
+ for (const item of value) {
460
+ normalizeClassTokens(item, tokens);
461
+ }
462
+ return tokens;
463
+ }
464
+ if (typeof value === "object") {
465
+ for (const [token, enabled] of Object.entries(value)) {
466
+ const value = isSignalRef(enabled) ? enabled.value : enabled;
467
+ if (value) {
468
+ normalizeClassTokens(token, tokens);
469
+ }
470
+ }
471
+ return tokens;
472
+ }
473
+ if (value !== true) {
474
+ tokens.add(String(value));
475
+ }
476
+ return tokens;
477
+ }
478
+
479
+ function resolveInlineValue(value) {
480
+ if (isSignalRef(value)) {
481
+ return value.value;
482
+ }
483
+ if (Array.isArray(value)) {
484
+ return value.map(resolveInlineValue);
485
+ }
486
+ if (value && typeof value === "object") {
487
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolveInlineValue(entry)]));
488
+ }
489
+ return value;
490
+ }
491
+
492
+ function collectSignalRefs(value, refs = new Map()) {
493
+ if (isSignalRef(value)) {
494
+ refs.set(value.id, value);
495
+ return [...refs.values()];
496
+ }
497
+ if (Array.isArray(value)) {
498
+ for (const item of value) {
499
+ collectSignalRefs(item, refs);
500
+ }
501
+ return [...refs.values()];
502
+ }
503
+ if (value && typeof value === "object") {
504
+ for (const item of Object.values(value)) {
505
+ collectSignalRefs(item, refs);
506
+ }
507
+ }
508
+ return [...refs.values()];
509
+ }
510
+
511
+ function isInlineBinding(value) {
512
+ return typeof value === "string" && value.startsWith(inlineBindingPrefix);
513
+ }
514
+
515
+ function signalClassName(id) {
516
+ return id.split(".").at(-1);
517
+ }
518
+
519
+ function updateClassToken(element, className, enabled) {
520
+ const tokens = readClassTokens(element);
521
+ for (const token of normalizeClassTokens(className)) {
522
+ if (enabled) {
523
+ tokens.add(token);
524
+ } else {
525
+ tokens.delete(token);
526
+ }
527
+ }
528
+ writeClassTokens(element, tokens);
529
+ }
530
+
531
+ function readClassTokens(element) {
532
+ return normalizeClassTokens(element.getAttribute("class") ?? "");
533
+ }
534
+
535
+ function writeClassTokens(element, tokens) {
536
+ const value = [...tokens].join(" ");
537
+ if (value.length === 0) {
538
+ element.removeAttribute("class");
539
+ return;
540
+ }
541
+ element.setAttribute("class", value);
542
+ }
543
+
333
544
  function collectBoundaryTemplates(boundary, id, attributeConfig) {
334
545
  const templates = {};
335
546
  for (const template of [...boundary.children].filter((child) => child.tagName === "TEMPLATE")) {