@panyam/tsappkit 0.2.0 → 0.6.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/dist/index.d.mts CHANGED
@@ -760,6 +760,47 @@ declare class MobileBottomDrawer extends BaseComponent implements LCMComponent {
760
760
  getContentContainer(): HTMLElement;
761
761
  }
762
762
 
763
+ /**
764
+ * When an island mounts: its `load` in the page spec (goapplib's
765
+ * page.Island.Load), read and waited for. Every strategy mounts once; an
766
+ * island isn't unmounted when its media query stops matching.
767
+ */
768
+ /** A parsed `load`. An empty or missing one is eager. */
769
+ type LoadStrategy = {
770
+ kind: "eager";
771
+ } | {
772
+ kind: "idle";
773
+ } | {
774
+ kind: "visible";
775
+ } | {
776
+ kind: "media";
777
+ query: string;
778
+ };
779
+ /**
780
+ * The strategy `load` names, or null when it isn't one of the forms Go's
781
+ * Validate accepts (`eager`, `idle`, `visible`, `media:<query>`), compared
782
+ * exactly.
783
+ */
784
+ declare function parseLoad(load: string | undefined): LoadStrategy | null;
785
+ /**
786
+ * The browser APIs scheduleMount waits on, so tests can stand them in.
787
+ * `requestIdleCallback` is optional because Safari doesn't have it.
788
+ */
789
+ interface LoadEnv {
790
+ requestIdleCallback?: (cb: () => void) => unknown;
791
+ setTimeout: (cb: () => void, ms?: number) => unknown;
792
+ IntersectionObserver: typeof IntersectionObserver;
793
+ matchMedia: (query: string) => MediaQueryList;
794
+ }
795
+ /**
796
+ * Calls `mount` once, when `strategy` says the island in `el` should mount:
797
+ * at once for eager; when the browser is idle (or on the next task, without
798
+ * requestIdleCallback) for idle; the first time any of `el` enters the
799
+ * viewport for visible; and at once if the query matches, or else on the
800
+ * first change that makes it match, for media.
801
+ */
802
+ declare function scheduleMount(strategy: LoadStrategy, el: Element, mount: () => void, env?: LoadEnv): void;
803
+
763
804
  /**
764
805
  * The page spec: which islands a page mounts, where, and with what config.
765
806
  * The server writes it into the page as JSON (goapplib's page.Spec, through
@@ -775,6 +816,12 @@ interface IslandSpec {
775
816
  presentation?: string;
776
817
  /** Handed to the factory as is. Always an object. */
777
818
  config: Record<string, unknown>;
819
+ /**
820
+ * When it mounts: `eager` (also when absent), `idle`, `visible` or
821
+ * `media:<query>` (see parseLoad). IslandPage waits for it; mountIslands
822
+ * does when given a `defer`.
823
+ */
824
+ load?: string;
778
825
  }
779
826
  interface PageSpec {
780
827
  /** The arrangement of slots, for state kept per layout. */
@@ -804,22 +851,65 @@ declare function readSpec<Ext extends object>(text: string | null | undefined, e
804
851
  * the app says they are; `bus` is the page's event bus.
805
852
  */
806
853
  type IslandFactory<Ctx, El, C, B> = (el: El, island: IslandSpec, ctx: Ctx, bus: B) => C;
807
- /** The islands an entry can mount, by name. An entry bundles only what its registry names. */
808
- type Registry<Ctx, El, C, B> = Record<string, IslandFactory<Ctx, El, C, B>>;
854
+ declare const LAZY: unique symbol;
855
+ /** A registry entry whose factory is loaded when the island mounts. Made by lazy. */
856
+ interface LazyIsland<Ctx, El, C, B> {
857
+ readonly [LAZY]: () => Promise<IslandFactory<Ctx, El, C, B> | {
858
+ default: IslandFactory<Ctx, El, C, B>;
859
+ }>;
860
+ }
861
+ /**
862
+ * A registry entry that loads its island's module only when the island
863
+ * mounts: `hero: lazy(() => import("./islands/hero"))`. With esbuild's
864
+ * --splitting each such module is its own chunk, so a page downloads only the
865
+ * islands its spec names, each when its `load` says. `load` resolves to the
866
+ * factory or to a module whose default export is the factory.
867
+ *
868
+ * A lazy island always mounts late, even an eager one, since its chunk
869
+ * arrives after the page has started; goapplib's page.Assets writes
870
+ * modulepreload links for the eager ones so that wait is short.
871
+ */
872
+ declare function lazy<Ctx, El, C, B>(load: () => Promise<IslandFactory<Ctx, El, C, B> | {
873
+ default: IslandFactory<Ctx, El, C, B>;
874
+ }>): LazyIsland<Ctx, El, C, B>;
875
+ /**
876
+ * The islands an entry can mount, by name: a factory, bundled with the entry,
877
+ * or a lazy entry, loaded as its own chunk when the island mounts.
878
+ */
879
+ type Registry<Ctx, El, C, B> = Record<string, IslandFactory<Ctx, El, C, B> | LazyIsland<Ctx, El, C, B>>;
880
+ /** How mountIslands handles islands that shouldn't mount at once. */
881
+ interface MountOptions<El, C> {
882
+ /**
883
+ * Gets each island whose `load` isn't eager, with the slot it will mount
884
+ * in; call `mount` when it's time (IslandPage passes scheduleMount). An
885
+ * island with a `load` parseLoad doesn't know is logged and mounted at once
886
+ * instead, so it still shows up.
887
+ */
888
+ defer?: (island: IslandSpec, el: El, mount: () => void) => void;
889
+ /** Gets what the factory built for each island mounted later: through `defer`, or from a lazy entry. */
890
+ onLateMount?: (component: C, island: IslandSpec) => void;
891
+ }
809
892
  /**
810
893
  * Mounts every island in `spec` into the element `findSlot` gives for its
811
- * slot, and returns what the factories built, in spec order. `context` builds
812
- * the page's shared services; it's called once, before the first island
813
- * mounts, and not at all on a page with nothing to mount, so a page without
814
- * islands doesn't start what they'd share. An island the registry doesn't
815
- * know, a slot that isn't on the page, or a factory that throws is reported
816
- * through `log` and skipped, so one bad entry doesn't take the rest of the
817
- * page down with it.
894
+ * slot, and returns what the factories built at once, in spec order. With
895
+ * `options.defer`, an island whose `load` isn't eager is handed to it instead
896
+ * and reported through `options.onLateMount` when it mounts; without it,
897
+ * every island mounts now whatever its `load` says. A lazy entry is loaded
898
+ * when its island would mount and is reported through `onLateMount` too, so
899
+ * it's never in the returned list.
900
+ *
901
+ * `context` builds the page's shared services; it's called once, before the
902
+ * first island mounts (eager or late), and not at all on a page with nothing
903
+ * to mount, so a page without islands doesn't start what they'd share. An
904
+ * island the registry doesn't know, a slot that isn't on the page, or a
905
+ * factory that throws (now or later), or a lazy entry that fails to load, is
906
+ * reported through `log` and skipped,
907
+ * so one bad entry doesn't take the rest of the page down with it.
818
908
  *
819
909
  * Plain types throughout (no DOM), so it runs under node in tests and on a
820
910
  * bare page without BasePage.
821
911
  */
822
- declare function mountIslands<Ctx, El, C, B>(spec: PageSpec, registry: Registry<Ctx, El, C, B>, findSlot: (slot: string) => El | null, context: () => Ctx, bus: B, log: (message: string) => void): C[];
912
+ declare function mountIslands<Ctx, El, C, B>(spec: PageSpec, registry: Registry<Ctx, El, C, B>, findSlot: (slot: string) => El | null, context: () => Ctx, bus: B, log: (message: string) => void, options?: MountOptions<El, C>): C[];
823
913
 
824
914
  /**
825
915
  * A page whose islands come from the page spec. A subclass says which
@@ -832,6 +922,13 @@ declare function mountIslands<Ctx, El, C, B>(spec: PageSpec, registry: Registry<
832
922
  * fields in `readExtension`, and they arrive typed as `Ext` on the spec
833
923
  * `makeContext` gets.
834
924
  *
925
+ * Each island mounts when its `load` says (`eager`, `idle`, `visible`,
926
+ * `media:<query>`; see scheduleMount), and a `lazy` registry entry loads its
927
+ * chunk then. One that mounts later, a lazy eager one included, goes through
928
+ * its own LifecycleController, so it still gets performLocalInit,
929
+ * setupDependencies and activate. A deferred island mustn't be something
930
+ * another island or the page needs at startup: nothing waits for it.
931
+ *
835
932
  * A page with no readable `#page-spec` mounts nothing and warns. Subclasses
836
933
  * that override initializeSpecificComponents call super and add to what it
837
934
  * returns.
@@ -845,6 +942,11 @@ declare abstract class IslandPage<Ctx, Ext extends object = {}> extends BasePage
845
942
  */
846
943
  protected readExtension(raw: Record<string, unknown>): Ext;
847
944
  protected initializeSpecificComponents(): LCMComponent[];
945
+ /**
946
+ * Waits for `strategy` and then calls `mount`. Defaults to scheduleMount
947
+ * against the window; a subclass or test can replace how the waiting is done.
948
+ */
949
+ protected scheduleLoad(strategy: LoadStrategy, el: HTMLElement, mount: () => void): void;
848
950
  }
849
951
 
850
952
  /**
@@ -950,4 +1052,4 @@ declare class KeyboardShortcutManager {
950
1052
  getCurrentArgs(): string;
951
1053
  }
952
1054
 
953
- export { BaseComponent, BasePage, type Component, type ComponentEventType, ComponentEventTypes, type ComponentLifecycleEvent, EventBus, type EventHandler, type EventSubscriber, type IslandFactory, IslandPage, type IslandSpec, KeyboardShortcutManager, KeyboardState, type LCMComponent, type LCMComponentConfig, type LCMComponentEvent, LifecycleController, type LifecycleEventType, LifecycleEventTypes, MobileBottomDrawer, Modal, type PageSpec, type Registry, SPEC_ELEMENT_ID, type ShortcutConfig, type ShortcutManagerConfig, type SpecExtension, SplashScreen, TemplateLoader, ThemeManager, ToastManager, hasModifierKeys, isInInputContext, mountIslands, readSpec, shouldIgnoreShortcut };
1055
+ export { BaseComponent, BasePage, type Component, type ComponentEventType, ComponentEventTypes, type ComponentLifecycleEvent, EventBus, type EventHandler, type EventSubscriber, type IslandFactory, IslandPage, type IslandSpec, KeyboardShortcutManager, KeyboardState, type LCMComponent, type LCMComponentConfig, type LCMComponentEvent, type LazyIsland, LifecycleController, type LifecycleEventType, LifecycleEventTypes, type LoadEnv, type LoadStrategy, MobileBottomDrawer, Modal, type MountOptions, type PageSpec, type Registry, SPEC_ELEMENT_ID, type ShortcutConfig, type ShortcutManagerConfig, type SpecExtension, SplashScreen, TemplateLoader, ThemeManager, ToastManager, hasModifierKeys, isInInputContext, lazy, mountIslands, parseLoad, readSpec, scheduleMount, shouldIgnoreShortcut };
package/dist/index.d.ts CHANGED
@@ -760,6 +760,47 @@ declare class MobileBottomDrawer extends BaseComponent implements LCMComponent {
760
760
  getContentContainer(): HTMLElement;
761
761
  }
762
762
 
763
+ /**
764
+ * When an island mounts: its `load` in the page spec (goapplib's
765
+ * page.Island.Load), read and waited for. Every strategy mounts once; an
766
+ * island isn't unmounted when its media query stops matching.
767
+ */
768
+ /** A parsed `load`. An empty or missing one is eager. */
769
+ type LoadStrategy = {
770
+ kind: "eager";
771
+ } | {
772
+ kind: "idle";
773
+ } | {
774
+ kind: "visible";
775
+ } | {
776
+ kind: "media";
777
+ query: string;
778
+ };
779
+ /**
780
+ * The strategy `load` names, or null when it isn't one of the forms Go's
781
+ * Validate accepts (`eager`, `idle`, `visible`, `media:<query>`), compared
782
+ * exactly.
783
+ */
784
+ declare function parseLoad(load: string | undefined): LoadStrategy | null;
785
+ /**
786
+ * The browser APIs scheduleMount waits on, so tests can stand them in.
787
+ * `requestIdleCallback` is optional because Safari doesn't have it.
788
+ */
789
+ interface LoadEnv {
790
+ requestIdleCallback?: (cb: () => void) => unknown;
791
+ setTimeout: (cb: () => void, ms?: number) => unknown;
792
+ IntersectionObserver: typeof IntersectionObserver;
793
+ matchMedia: (query: string) => MediaQueryList;
794
+ }
795
+ /**
796
+ * Calls `mount` once, when `strategy` says the island in `el` should mount:
797
+ * at once for eager; when the browser is idle (or on the next task, without
798
+ * requestIdleCallback) for idle; the first time any of `el` enters the
799
+ * viewport for visible; and at once if the query matches, or else on the
800
+ * first change that makes it match, for media.
801
+ */
802
+ declare function scheduleMount(strategy: LoadStrategy, el: Element, mount: () => void, env?: LoadEnv): void;
803
+
763
804
  /**
764
805
  * The page spec: which islands a page mounts, where, and with what config.
765
806
  * The server writes it into the page as JSON (goapplib's page.Spec, through
@@ -775,6 +816,12 @@ interface IslandSpec {
775
816
  presentation?: string;
776
817
  /** Handed to the factory as is. Always an object. */
777
818
  config: Record<string, unknown>;
819
+ /**
820
+ * When it mounts: `eager` (also when absent), `idle`, `visible` or
821
+ * `media:<query>` (see parseLoad). IslandPage waits for it; mountIslands
822
+ * does when given a `defer`.
823
+ */
824
+ load?: string;
778
825
  }
779
826
  interface PageSpec {
780
827
  /** The arrangement of slots, for state kept per layout. */
@@ -804,22 +851,65 @@ declare function readSpec<Ext extends object>(text: string | null | undefined, e
804
851
  * the app says they are; `bus` is the page's event bus.
805
852
  */
806
853
  type IslandFactory<Ctx, El, C, B> = (el: El, island: IslandSpec, ctx: Ctx, bus: B) => C;
807
- /** The islands an entry can mount, by name. An entry bundles only what its registry names. */
808
- type Registry<Ctx, El, C, B> = Record<string, IslandFactory<Ctx, El, C, B>>;
854
+ declare const LAZY: unique symbol;
855
+ /** A registry entry whose factory is loaded when the island mounts. Made by lazy. */
856
+ interface LazyIsland<Ctx, El, C, B> {
857
+ readonly [LAZY]: () => Promise<IslandFactory<Ctx, El, C, B> | {
858
+ default: IslandFactory<Ctx, El, C, B>;
859
+ }>;
860
+ }
861
+ /**
862
+ * A registry entry that loads its island's module only when the island
863
+ * mounts: `hero: lazy(() => import("./islands/hero"))`. With esbuild's
864
+ * --splitting each such module is its own chunk, so a page downloads only the
865
+ * islands its spec names, each when its `load` says. `load` resolves to the
866
+ * factory or to a module whose default export is the factory.
867
+ *
868
+ * A lazy island always mounts late, even an eager one, since its chunk
869
+ * arrives after the page has started; goapplib's page.Assets writes
870
+ * modulepreload links for the eager ones so that wait is short.
871
+ */
872
+ declare function lazy<Ctx, El, C, B>(load: () => Promise<IslandFactory<Ctx, El, C, B> | {
873
+ default: IslandFactory<Ctx, El, C, B>;
874
+ }>): LazyIsland<Ctx, El, C, B>;
875
+ /**
876
+ * The islands an entry can mount, by name: a factory, bundled with the entry,
877
+ * or a lazy entry, loaded as its own chunk when the island mounts.
878
+ */
879
+ type Registry<Ctx, El, C, B> = Record<string, IslandFactory<Ctx, El, C, B> | LazyIsland<Ctx, El, C, B>>;
880
+ /** How mountIslands handles islands that shouldn't mount at once. */
881
+ interface MountOptions<El, C> {
882
+ /**
883
+ * Gets each island whose `load` isn't eager, with the slot it will mount
884
+ * in; call `mount` when it's time (IslandPage passes scheduleMount). An
885
+ * island with a `load` parseLoad doesn't know is logged and mounted at once
886
+ * instead, so it still shows up.
887
+ */
888
+ defer?: (island: IslandSpec, el: El, mount: () => void) => void;
889
+ /** Gets what the factory built for each island mounted later: through `defer`, or from a lazy entry. */
890
+ onLateMount?: (component: C, island: IslandSpec) => void;
891
+ }
809
892
  /**
810
893
  * Mounts every island in `spec` into the element `findSlot` gives for its
811
- * slot, and returns what the factories built, in spec order. `context` builds
812
- * the page's shared services; it's called once, before the first island
813
- * mounts, and not at all on a page with nothing to mount, so a page without
814
- * islands doesn't start what they'd share. An island the registry doesn't
815
- * know, a slot that isn't on the page, or a factory that throws is reported
816
- * through `log` and skipped, so one bad entry doesn't take the rest of the
817
- * page down with it.
894
+ * slot, and returns what the factories built at once, in spec order. With
895
+ * `options.defer`, an island whose `load` isn't eager is handed to it instead
896
+ * and reported through `options.onLateMount` when it mounts; without it,
897
+ * every island mounts now whatever its `load` says. A lazy entry is loaded
898
+ * when its island would mount and is reported through `onLateMount` too, so
899
+ * it's never in the returned list.
900
+ *
901
+ * `context` builds the page's shared services; it's called once, before the
902
+ * first island mounts (eager or late), and not at all on a page with nothing
903
+ * to mount, so a page without islands doesn't start what they'd share. An
904
+ * island the registry doesn't know, a slot that isn't on the page, or a
905
+ * factory that throws (now or later), or a lazy entry that fails to load, is
906
+ * reported through `log` and skipped,
907
+ * so one bad entry doesn't take the rest of the page down with it.
818
908
  *
819
909
  * Plain types throughout (no DOM), so it runs under node in tests and on a
820
910
  * bare page without BasePage.
821
911
  */
822
- declare function mountIslands<Ctx, El, C, B>(spec: PageSpec, registry: Registry<Ctx, El, C, B>, findSlot: (slot: string) => El | null, context: () => Ctx, bus: B, log: (message: string) => void): C[];
912
+ declare function mountIslands<Ctx, El, C, B>(spec: PageSpec, registry: Registry<Ctx, El, C, B>, findSlot: (slot: string) => El | null, context: () => Ctx, bus: B, log: (message: string) => void, options?: MountOptions<El, C>): C[];
823
913
 
824
914
  /**
825
915
  * A page whose islands come from the page spec. A subclass says which
@@ -832,6 +922,13 @@ declare function mountIslands<Ctx, El, C, B>(spec: PageSpec, registry: Registry<
832
922
  * fields in `readExtension`, and they arrive typed as `Ext` on the spec
833
923
  * `makeContext` gets.
834
924
  *
925
+ * Each island mounts when its `load` says (`eager`, `idle`, `visible`,
926
+ * `media:<query>`; see scheduleMount), and a `lazy` registry entry loads its
927
+ * chunk then. One that mounts later, a lazy eager one included, goes through
928
+ * its own LifecycleController, so it still gets performLocalInit,
929
+ * setupDependencies and activate. A deferred island mustn't be something
930
+ * another island or the page needs at startup: nothing waits for it.
931
+ *
835
932
  * A page with no readable `#page-spec` mounts nothing and warns. Subclasses
836
933
  * that override initializeSpecificComponents call super and add to what it
837
934
  * returns.
@@ -845,6 +942,11 @@ declare abstract class IslandPage<Ctx, Ext extends object = {}> extends BasePage
845
942
  */
846
943
  protected readExtension(raw: Record<string, unknown>): Ext;
847
944
  protected initializeSpecificComponents(): LCMComponent[];
945
+ /**
946
+ * Waits for `strategy` and then calls `mount`. Defaults to scheduleMount
947
+ * against the window; a subclass or test can replace how the waiting is done.
948
+ */
949
+ protected scheduleLoad(strategy: LoadStrategy, el: HTMLElement, mount: () => void): void;
848
950
  }
849
951
 
850
952
  /**
@@ -950,4 +1052,4 @@ declare class KeyboardShortcutManager {
950
1052
  getCurrentArgs(): string;
951
1053
  }
952
1054
 
953
- export { BaseComponent, BasePage, type Component, type ComponentEventType, ComponentEventTypes, type ComponentLifecycleEvent, EventBus, type EventHandler, type EventSubscriber, type IslandFactory, IslandPage, type IslandSpec, KeyboardShortcutManager, KeyboardState, type LCMComponent, type LCMComponentConfig, type LCMComponentEvent, LifecycleController, type LifecycleEventType, LifecycleEventTypes, MobileBottomDrawer, Modal, type PageSpec, type Registry, SPEC_ELEMENT_ID, type ShortcutConfig, type ShortcutManagerConfig, type SpecExtension, SplashScreen, TemplateLoader, ThemeManager, ToastManager, hasModifierKeys, isInInputContext, mountIslands, readSpec, shouldIgnoreShortcut };
1055
+ export { BaseComponent, BasePage, type Component, type ComponentEventType, ComponentEventTypes, type ComponentLifecycleEvent, EventBus, type EventHandler, type EventSubscriber, type IslandFactory, IslandPage, type IslandSpec, KeyboardShortcutManager, KeyboardState, type LCMComponent, type LCMComponentConfig, type LCMComponentEvent, type LazyIsland, LifecycleController, type LifecycleEventType, LifecycleEventTypes, type LoadEnv, type LoadStrategy, MobileBottomDrawer, Modal, type MountOptions, type PageSpec, type Registry, SPEC_ELEMENT_ID, type ShortcutConfig, type ShortcutManagerConfig, type SpecExtension, SplashScreen, TemplateLoader, ThemeManager, ToastManager, hasModifierKeys, isInInputContext, lazy, mountIslands, parseLoad, readSpec, scheduleMount, shouldIgnoreShortcut };
package/dist/index.js CHANGED
@@ -257,9 +257,9 @@ var BaseComponent = class {
257
257
  /**
258
258
  * Log messages with component identification
259
259
  */
260
- log(message, data = null) {
260
+ log(message2, data = null) {
261
261
  if (this.debugMode) {
262
- console.log(`[${this.componentId}] ${message}`, data);
262
+ console.log(`[${this.componentId}] ${message2}`, data);
263
263
  }
264
264
  }
265
265
  /**
@@ -437,16 +437,16 @@ var LifecycleController = class {
437
437
  /**
438
438
  * Log message if debug logging is enabled
439
439
  */
440
- log(message, ...args) {
440
+ log(message2, ...args) {
441
441
  if (this.config.enableDebugLogging) {
442
- console.log(`[LifecycleController] ${message}`, ...args);
442
+ console.log(`[LifecycleController] ${message2}`, ...args);
443
443
  }
444
444
  }
445
445
  /**
446
446
  * Log error message
447
447
  */
448
- logError(message, error) {
449
- console.error(`[LifecycleController] ${message}`, error);
448
+ logError(message2, error) {
449
+ console.error(`[LifecycleController] ${message2}`, error);
450
450
  }
451
451
  /**
452
452
  * Get current status of all components
@@ -845,7 +845,7 @@ var _ToastManager = class _ToastManager {
845
845
  * @param type Toast type for styling
846
846
  * @param duration Duration in ms (default: 4000)
847
847
  */
848
- showToast(title, message, type = "info", duration = 4e3) {
848
+ showToast(title, message2, type = "info", duration = 4e3) {
849
849
  if (!this.container || !this.template) return "";
850
850
  const id = `toast-${Date.now()}-${this.counter++}`;
851
851
  const toast = this.template.cloneNode(true);
@@ -854,7 +854,7 @@ var _ToastManager = class _ToastManager {
854
854
  const titleElement = toast.querySelector(".toast-title");
855
855
  const messageElement = toast.querySelector(".toast-message");
856
856
  if (titleElement) titleElement.textContent = title;
857
- if (messageElement) messageElement.textContent = message;
857
+ if (messageElement) messageElement.textContent = message2;
858
858
  const iconContainer = toast.querySelector(".flex-shrink-0");
859
859
  if (iconContainer) {
860
860
  iconContainer.innerHTML = "";
@@ -959,7 +959,7 @@ var SplashScreen = class {
959
959
  /**
960
960
  * Update the splash screen message (if it hasn't been dismissed yet)
961
961
  */
962
- static updateMessage(title, message) {
962
+ static updateMessage(title, message2) {
963
963
  if (this.dismissed) {
964
964
  return;
965
965
  }
@@ -973,10 +973,10 @@ var SplashScreen = class {
973
973
  titleEl.textContent = title;
974
974
  }
975
975
  }
976
- if (message) {
976
+ if (message2) {
977
977
  const messageEl = splash.querySelector("[data-splash-message]");
978
978
  if (messageEl) {
979
- messageEl.textContent = message;
979
+ messageEl.textContent = message2;
980
980
  }
981
981
  }
982
982
  }
@@ -1091,8 +1091,8 @@ var BasePage = class extends BaseComponent {
1091
1091
  /**
1092
1092
  * Show a toast notification
1093
1093
  */
1094
- showToast(title, message, type = "info", duration) {
1095
- this.toastManager?.showToast(title, message, type, duration);
1094
+ showToast(title, message2, type = "info", duration) {
1095
+ this.toastManager?.showToast(title, message2, type, duration);
1096
1096
  }
1097
1097
  /**
1098
1098
  * Show a modal dialog
@@ -1373,13 +1373,68 @@ var MobileBottomDrawer = class extends BaseComponent {
1373
1373
  }
1374
1374
  };
1375
1375
 
1376
+ // src/page/load.ts
1377
+ function parseLoad(load) {
1378
+ switch (load ?? "") {
1379
+ case "":
1380
+ case "eager":
1381
+ return { kind: "eager" };
1382
+ case "idle":
1383
+ return { kind: "idle" };
1384
+ case "visible":
1385
+ return { kind: "visible" };
1386
+ }
1387
+ const query = load.startsWith("media:") ? load.slice("media:".length) : "";
1388
+ return query.trim() ? { kind: "media", query } : null;
1389
+ }
1390
+ function scheduleMount(strategy, el, mount, env = window) {
1391
+ switch (strategy.kind) {
1392
+ case "eager":
1393
+ mount();
1394
+ return;
1395
+ case "idle":
1396
+ if (env.requestIdleCallback) env.requestIdleCallback(mount);
1397
+ else env.setTimeout(mount, 1);
1398
+ return;
1399
+ case "visible": {
1400
+ let done = false;
1401
+ const observer = new env.IntersectionObserver((entries) => {
1402
+ if (done || !entries.some((e) => e.isIntersecting)) return;
1403
+ done = true;
1404
+ observer.disconnect();
1405
+ mount();
1406
+ });
1407
+ observer.observe(el);
1408
+ return;
1409
+ }
1410
+ case "media": {
1411
+ const mq = env.matchMedia(strategy.query);
1412
+ if (mq.matches) {
1413
+ mount();
1414
+ return;
1415
+ }
1416
+ const onChange = (e) => {
1417
+ if (!e.matches) return;
1418
+ mq.removeEventListener("change", onChange);
1419
+ mount();
1420
+ };
1421
+ mq.addEventListener("change", onChange);
1422
+ return;
1423
+ }
1424
+ }
1425
+ }
1426
+
1376
1427
  // src/page/mount.ts
1377
- function mountIslands(spec, registry, findSlot, context, bus, log) {
1428
+ var LAZY = /* @__PURE__ */ Symbol("lazy island");
1429
+ function lazy(load) {
1430
+ return { [LAZY]: load };
1431
+ }
1432
+ function mountIslands(spec, registry, findSlot, context, bus, log, options = {}) {
1378
1433
  const out = [];
1379
1434
  let ctx;
1380
1435
  for (const island of spec.islands) {
1381
- const factory = Object.prototype.hasOwnProperty.call(registry, island.name) ? registry[island.name] : void 0;
1382
- if (!factory) {
1436
+ const entry = Object.prototype.hasOwnProperty.call(registry, island.name) ? registry[island.name] : void 0;
1437
+ if (!entry) {
1383
1438
  log(`page spec: no island called "${island.name}" in this page's registry`);
1384
1439
  continue;
1385
1440
  }
@@ -1388,15 +1443,53 @@ function mountIslands(spec, registry, findSlot, context, bus, log) {
1388
1443
  log(`page spec: island "${island.name}" wants slot "${island.slot}", which isn't on the page`);
1389
1444
  continue;
1390
1445
  }
1391
- try {
1392
- ctx ?? (ctx = context());
1393
- out.push(factory(el, island, ctx, bus));
1394
- } catch (err) {
1395
- log(`page spec: island "${island.name}" failed to mount: ${err instanceof Error ? err.message : String(err)}`);
1446
+ const build = (factory) => {
1447
+ try {
1448
+ ctx ?? (ctx = context());
1449
+ return factory(el, island, ctx, bus);
1450
+ } catch (err) {
1451
+ log(`page spec: island "${island.name}" failed to mount: ${message(err)}`);
1452
+ return void 0;
1453
+ }
1454
+ };
1455
+ const mountLate = () => {
1456
+ const late = (factory) => {
1457
+ const c2 = build(factory);
1458
+ if (c2 !== void 0) options.onLateMount?.(c2, island);
1459
+ };
1460
+ if (typeof entry === "function") {
1461
+ late(entry);
1462
+ return;
1463
+ }
1464
+ new Promise((resolve) => resolve(entry[LAZY]())).then(
1465
+ (m) => {
1466
+ const factory = typeof m === "function" ? m : m?.default;
1467
+ if (typeof factory === "function") late(factory);
1468
+ else log(`page spec: island "${island.name}" loaded, but its module has no factory (a default export or the function itself)`);
1469
+ },
1470
+ (err) => log(`page spec: island "${island.name}" failed to load: ${message(err)}`)
1471
+ );
1472
+ };
1473
+ const strategy = parseLoad(island.load);
1474
+ if (strategy === null) {
1475
+ log(`page spec: island "${island.name}" has load "${island.load}", which isn't eager, idle, visible or media:<query>; mounting it now`);
1476
+ }
1477
+ if (options.defer && strategy !== null && strategy.kind !== "eager") {
1478
+ options.defer(island, el, mountLate);
1479
+ continue;
1396
1480
  }
1481
+ if (typeof entry !== "function") {
1482
+ mountLate();
1483
+ continue;
1484
+ }
1485
+ const c = build(entry);
1486
+ if (c !== void 0) out.push(c);
1397
1487
  }
1398
1488
  return out;
1399
1489
  }
1490
+ function message(err) {
1491
+ return err instanceof Error ? err.message : String(err);
1492
+ }
1400
1493
 
1401
1494
  // src/page/spec.ts
1402
1495
  var SPEC_ELEMENT_ID = "page-spec";
@@ -1417,7 +1510,8 @@ function readSpec(text, extend) {
1417
1510
  name: is.name,
1418
1511
  slot: is.slot,
1419
1512
  ...typeof is.presentation === "string" && { presentation: is.presentation },
1420
- config: isObject(is.config) ? is.config : {}
1513
+ config: isObject(is.config) ? is.config : {},
1514
+ ...typeof is.load === "string" && { load: is.load }
1421
1515
  });
1422
1516
  }
1423
1517
  const spec = { layout: raw.layout, islands };
@@ -1455,9 +1549,22 @@ var IslandPage = class extends BasePage {
1455
1549
  (slot) => document.querySelector(`[data-slot="${slot}"]`),
1456
1550
  () => this.makeContext(spec),
1457
1551
  this.eventBus,
1458
- (message) => console.warn(message)
1552
+ (message2) => console.warn(message2),
1553
+ {
1554
+ defer: (island, el, mount) => this.scheduleLoad(parseLoad(island.load) ?? { kind: "eager" }, el, mount),
1555
+ onLateMount: (component, island) => {
1556
+ new LifecycleController(this.eventBus, LifecycleController.DefaultConfig).initializeFromRoot(component).catch((err) => console.warn(`page spec: island "${island.name}" failed to start: ${err instanceof Error ? err.message : String(err)}`));
1557
+ }
1558
+ }
1459
1559
  );
1460
1560
  }
1561
+ /**
1562
+ * Waits for `strategy` and then calls `mount`. Defaults to scheduleMount
1563
+ * against the window; a subclass or test can replace how the waiting is done.
1564
+ */
1565
+ scheduleLoad(strategy, el, mount) {
1566
+ scheduleMount(strategy, el, mount);
1567
+ }
1461
1568
  };
1462
1569
 
1463
1570
  // src/DOMUtils.ts
@@ -1820,8 +1927,11 @@ exports.ThemeManager = ThemeManager;
1820
1927
  exports.ToastManager = ToastManager;
1821
1928
  exports.hasModifierKeys = hasModifierKeys;
1822
1929
  exports.isInInputContext = isInInputContext;
1930
+ exports.lazy = lazy;
1823
1931
  exports.mountIslands = mountIslands;
1932
+ exports.parseLoad = parseLoad;
1824
1933
  exports.readSpec = readSpec;
1934
+ exports.scheduleMount = scheduleMount;
1825
1935
  exports.shouldIgnoreShortcut = shouldIgnoreShortcut;
1826
1936
  //# sourceMappingURL=index.js.map
1827
1937
  //# sourceMappingURL=index.js.map