@async/framework 0.11.13 → 0.11.15

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/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.15 - 2026-06-19
4
+
5
+ - Made the source package private and kept its public surface to the minimal
6
+ export spec for root, `/browser`, `/server`, and `/package.json` only.
7
+ - Moved publish staging to generated `dist/package.json` so npm and release
8
+ automation publish from `dist/` while package consumers still receive
9
+ root-level artifacts without `dist/` paths.
10
+ - Removed legacy direct artifact subpath exports plus top-level
11
+ `main`/`module`/`browser`/`types` and generated file lists from the source
12
+ manifest.
13
+ - Updated pack, size, pipeline, and installed-package coverage to verify the
14
+ browser and server entrypoints remain split after packing.
15
+ - Bundle size from bundled TypeScript source: `browser.ts` 197,173 B raw /
16
+ 37,198 B gzip -> `browser.min.js` 84,013 B raw / 24,894 B gzip
17
+ (-113,160 B raw, -12,304 B gzip).
18
+
19
+ ## 0.11.14 - 2026-06-18
20
+
21
+ - Added condition-specific root declaration targets so browser-conditioned root
22
+ imports resolve to browser declarations while Node/default root imports keep
23
+ server-capable declarations.
24
+ - Preserved the root browser runtime condition and documented that server-only
25
+ APIs remain on the Node/server entrypoints.
26
+ - Added packed-artifact export-map, declaration/runtime parity, and static
27
+ import checks for root browser, root Node, explicit `/browser`, and explicit
28
+ `/server` entrypoints.
29
+ - Added component-scoped continuous intersection helpers with
30
+ `this.intersect(...)` and `this.on("intersect", options?, fn)`, preserving
31
+ `on:visible` as a one-shot visibility lifecycle hook.
32
+ - Added declarative `on:intersect` with `intersect:threshold`,
33
+ `intersect:root-margin`, and `intersect:once` pseudo-event options, including
34
+ custom `intersect` attribute prefix support.
35
+ - Added observer cleanup coverage for component teardown, boundary swaps,
36
+ fallback scheduling, repeated entries, and existing visible compatibility.
37
+ - Moved root release artifacts to an ignored generated-output workflow:
38
+ tests, bundle checks, pack checks, and generated CI tasks materialize the
39
+ current package surface before verification or publish.
40
+ - Bundle size from bundled TypeScript source: `browser.ts` 197,173 B raw /
41
+ 37,198 B gzip -> `browser.min.js` 84,013 B raw / 24,894 B gzip
42
+ (-113,160 B raw, -12,304 B gzip).
43
+
3
44
  ## 0.11.13 - 2026-06-18
4
45
 
5
46
  - Validated server proxy arguments, default input payloads, and selected signal
package/README.md CHANGED
@@ -306,6 +306,9 @@ patches, and browser-cache patches. Async does not ship a component resume graph
306
306
  For npm consumers, `@async/framework` uses conditional exports: browser-aware
307
307
  tooling receives the browser entry, while Node receives the server-capable
308
308
  entry. Use explicit subpaths when the target matters.
309
+ The root export also uses condition-specific declarations, so browser-conditioned
310
+ root imports expose the same API as `@async/framework/browser`; server-only APIs
311
+ remain declared on the Node/server entrypoints.
309
312
 
310
313
  ```js
311
314
  import {
@@ -607,6 +610,10 @@ Loader scans regular HTML attributes:
607
610
  | `on:click="server.cart.add(productId)"` | Server command with signal args |
608
611
  | `on:attach="setup"` | Component root attach lifecycle pseudo-event |
609
612
  | `on:visible="trackView"` | Component root visible lifecycle pseudo-event |
613
+ | `on:intersect="trackSection"` | Continuous intersection lifecycle pseudo-event |
614
+ | `intersect:threshold="0,0.5,1"` | Intersection threshold option for `on:intersect` |
615
+ | `intersect:root-margin="-20% 0px -55% 0px"` | Intersection root margin option for `on:intersect` |
616
+ | `intersect:once="true"` | Disconnect `on:intersect` after the first intersecting entry |
610
617
  | `signal:text="product.title"` | Text binding |
611
618
  | `signal:value="productId"` | Form value binding with writeback |
612
619
  | `signal:attr:disabled="product.$loading"` | Attribute binding |
@@ -641,6 +648,7 @@ Async.start({
641
648
  attributes: {
642
649
  async: "data-async-",
643
650
  class: "data-class-",
651
+ intersect: "data-intersect-",
644
652
  signal: "data-signal-",
645
653
  on: "data-on-"
646
654
  }
@@ -648,7 +656,8 @@ Async.start({
648
656
  ```
649
657
 
650
658
  That maps to `data-async-container`, `data-on-click="save"`,
651
- `data-signal-text="product.title"`, and `data-class-selected="selected"`.
659
+ `data-signal-text="product.title"`, `data-class-selected="selected"`, and
660
+ `data-intersect-threshold="0.5"`.
652
661
 
653
662
  Inside `html` templates, signal refs can be passed directly to binding
654
663
  attributes:
@@ -1099,6 +1108,8 @@ Component helpers:
1099
1108
  | `this.on(event, fn)` | Fragment lifecycle fallback for `attach`, `visible`, and `destroy` |
1100
1109
  | `this.onMount(fn)` | Compatibility alias for `this.on("attach", fn)` |
1101
1110
  | `this.onVisible(fn)` | Compatibility alias for `this.on("visible", fn)` |
1111
+ | `this.on("intersect", options?, fn)` | Continuous intersection lifecycle for the mounted component scope |
1112
+ | `this.intersect(element, options?, fn)` | Component-owned continuous intersection observer for a direct element |
1102
1113
 
1103
1114
  `this.suspense(...)` is sugar for Loader boundaries:
1104
1115
  `asyncSignal + async:boundary + async:* templates`. It emits only templates. The
@@ -1173,6 +1184,73 @@ this.on("destroy", () => {
1173
1184
  the component root first becomes visible. Lifecycle events do not drive
1174
1185
  component rerenders.
1175
1186
 
1187
+ Use `on:intersect` when markup should receive continuous intersection updates
1188
+ through a registered handler:
1189
+
1190
+ ```html
1191
+ <section
1192
+ on:intersect="trackSection"
1193
+ intersect:threshold="0,0.25,0.5,0.75,1"
1194
+ intersect:root-margin="-20% 0px -55% 0px"
1195
+ >
1196
+ ...
1197
+ </section>
1198
+ ```
1199
+
1200
+ The handler receives `element`, `entry`, `entries`, `observer`,
1201
+ `isIntersecting`, `intersectionRatio`, and `unsupported`. Custom roots are not
1202
+ selector-based; use `this.intersect(...)` with a direct root element when a
1203
+ custom observer root is needed.
1204
+
1205
+ Use `this.on("intersect", ...)` when a component needs continuous visibility
1206
+ state:
1207
+
1208
+ ```js
1209
+ const Card = defineComponent(function Card() {
1210
+ const visible = this.signal(false);
1211
+
1212
+ this.on("intersect", { threshold: 0.5 }, ({ isIntersecting }) => {
1213
+ visible.set(isIntersecting);
1214
+ });
1215
+
1216
+ return html`<article class:visible="${visible}">...</article>`;
1217
+ });
1218
+ ```
1219
+
1220
+ Use `this.intersect(...)` with a direct element when a parent owns scroll-spy or
1221
+ active-section state:
1222
+
1223
+ ```js
1224
+ const Section = defineComponent(function Section({ id, observeSection }) {
1225
+ const attach = this.handler("attach", function ({ element }) {
1226
+ return observeSection(id, element);
1227
+ });
1228
+
1229
+ return html`<section on:attach="${attach}"><h2>${id}</h2></section>`;
1230
+ });
1231
+
1232
+ const Page = defineComponent(function Page() {
1233
+ const active = this.signal("intro");
1234
+ const ratios = new Map();
1235
+ const options = {
1236
+ rootMargin: "-20% 0px -55% 0px",
1237
+ threshold: [0, 0.25, 0.5, 0.75, 1]
1238
+ };
1239
+
1240
+ const observeSection = (id, element) => this.intersect(element, options, ({ entry }) => {
1241
+ ratios.set(id, entry.isIntersecting ? entry.intersectionRatio : 0);
1242
+ const best = [...ratios.entries()].sort((a, b) => b[1] - a[1])[0];
1243
+ active.set(best?.[0] ?? id);
1244
+ });
1245
+
1246
+ return html`
1247
+ <nav signal:text="${active}"></nav>
1248
+ ${this.render(Section, { id: "intro", observeSection })}
1249
+ ${this.render(Section, { id: "runtime", observeSection })}
1250
+ `;
1251
+ });
1252
+ ```
1253
+
1176
1254
  ## Streaming
1177
1255
 
1178
1256
  Out-of-order HTML can target a boundary and keep delegated handlers working:
@@ -1261,6 +1339,8 @@ pnpm run pipeline:github:check
1261
1339
  Useful commands:
1262
1340
 
1263
1341
  ```bash
1342
+ pnpm run bundle
1343
+ pnpm run bundle:clean
1264
1344
  pnpm run pipeline:verify
1265
1345
  pnpm run pipeline:pages
1266
1346
  pnpm run registry:lint
@@ -1268,6 +1348,20 @@ pnpm run pipeline:release:doctor
1268
1348
  pnpm run release:check
1269
1349
  ```
1270
1350
 
1351
+ Release artifacts such as `browser.js`, `browser.min.js`,
1352
+ `browser.umd.min.js`, `browser.ts`, `browser.d.ts`, `framework.ts`,
1353
+ `framework.d.ts`, and `server.js` are generated into `dist/`. The generated
1354
+ `dist/` directory is the package root for `npm pack` and release publishing, so
1355
+ the published package and CDN surface still expose those files at package root
1356
+ rather than under `dist/`. The source `package.json` stays private and owns the
1357
+ minimal public export spec, while omitting legacy `main`/`module`/`browser`
1358
+ entry fields and generated package file lists. `scripts/build-framework-bundle.js`
1359
+ derives the generated `dist/package.json` and staged artifact names from that
1360
+ spec. Feature branches should edit source files and let `pnpm run bundle`,
1361
+ `pnpm test`, `pnpm run pack:check`, or the generated release workflow
1362
+ materialize the publish tree. Use `pnpm run bundle:clean` to remove local
1363
+ generated artifacts after inspection.
1364
+
1271
1365
  `registry:lint` scans package source and examples for declared registry ids
1272
1366
  such as signals, handlers, server functions, partials, routes, and components.
1273
1367
  It writes `.async/registry-manifest.json` plus a per-file cache at
package/browser.d.ts CHANGED
@@ -23,6 +23,7 @@ export interface AttributeConfig {
23
23
  async?: string | string[];
24
24
  class?: string | string[];
25
25
  signal?: string | string[];
26
+ intersect?: string | string[];
26
27
  on?: string | string[];
27
28
  }
28
29
 
@@ -30,6 +31,7 @@ export interface NormalizedAttributeConfig {
30
31
  async: string[];
31
32
  class: string[];
32
33
  signal: string[];
34
+ intersect: string[];
33
35
  on: string[];
34
36
  }
35
37
 
@@ -403,7 +405,41 @@ export interface Router {
403
405
  destroy(): void;
404
406
  }
405
407
 
406
- export type LifecycleEventName = "attach" | "mount" | "visible" | "destroy";
408
+ export type LifecycleEventName = "attach" | "mount" | "visible" | "intersect" | "destroy";
409
+
410
+ export interface IntersectionFallbackEntry {
411
+ target: Element;
412
+ isIntersecting: boolean;
413
+ intersectionRatio: number;
414
+ time: number;
415
+ rootBounds: DOMRectReadOnly | null;
416
+ boundingClientRect: DOMRect | DOMRectReadOnly | null;
417
+ intersectionRect: DOMRect | DOMRectReadOnly | null;
418
+ }
419
+
420
+ export interface IntersectionEvent {
421
+ target: Element;
422
+ element: Element;
423
+ el: Element;
424
+ root: Document | Element | DocumentFragment;
425
+ entry: IntersectionObserverEntry | IntersectionFallbackEntry;
426
+ entries: Array<IntersectionObserverEntry | IntersectionFallbackEntry>;
427
+ observer: IntersectionObserver | null;
428
+ isIntersecting: boolean;
429
+ intersectionRatio: number;
430
+ unsupported: boolean;
431
+ }
432
+
433
+ export interface IntersectionOptions {
434
+ root?: Element | Document | null;
435
+ rootMargin?: string;
436
+ threshold?: number | number[];
437
+ once?: boolean;
438
+ schedule?: "lifecycle" | "sync";
439
+ key?: string;
440
+ }
441
+
442
+ export type IntersectionCallback = (this: ComponentContext, event: IntersectionEvent) => unknown;
407
443
 
408
444
  export interface ComponentContext {
409
445
  scope: string;
@@ -423,9 +459,13 @@ export interface ComponentContext {
423
459
  handler(name: string, fn: HandlerFunction): string;
424
460
  render<TProps extends Record<string, unknown> = Record<string, unknown>>(Child: ComponentFunction<TProps>, props?: TProps): TemplateLike;
425
461
  suspense(signalRef: Pick<SignalRef, "id">, views: SuspenseViews | SuspenseReadyView): TemplateLike;
426
- on(eventName: LifecycleEventName, fn: (this: ComponentContext, target?: Element) => unknown): void;
462
+ on(eventName: "intersect", fn: IntersectionCallback): void;
463
+ on(eventName: "intersect", options: IntersectionOptions | undefined | null, fn: IntersectionCallback): void;
464
+ on(eventName: Exclude<LifecycleEventName, "intersect">, fn: (this: ComponentContext, target?: Element) => unknown): void;
427
465
  onMount(fn: (this: ComponentContext, target?: Element) => unknown): void;
428
466
  onVisible(fn: (this: ComponentContext, target?: Element) => unknown): void;
467
+ intersect(target: Element, fn: IntersectionCallback): Cleanup;
468
+ intersect(target: Element, options: IntersectionOptions | undefined | null, fn: IntersectionCallback): Cleanup;
429
469
  }
430
470
 
431
471
  export type ComponentFunction<TProps extends Record<string, unknown> = Record<string, unknown>> = (this: ComponentContext, props: TProps) => TemplateLike;