@panyam/tsappkit 0.0.5 → 0.1.1

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,93 @@ declare class MobileBottomDrawer extends BaseComponent implements LCMComponent {
760
760
  getContentContainer(): HTMLElement;
761
761
  }
762
762
 
763
+ /**
764
+ * The page spec: which islands a page mounts, where, and with what config.
765
+ * The server writes it into the page as JSON (goapplib's page.Spec, through
766
+ * templates/page/Islands.html); this reads it back.
767
+ */
768
+ /** The id of the script element the server writes the spec into. */
769
+ declare const SPEC_ELEMENT_ID = "page-spec";
770
+ /** One island: `name` picks its factory, `slot` the element it mounts in (`[data-slot]`). */
771
+ interface IslandSpec {
772
+ name: string;
773
+ slot: string;
774
+ /** How the layout shows it (the app's own words: "page", "panel", "drawer"), when it draws differently in each. */
775
+ presentation?: string;
776
+ /** Handed to the factory as is. Always an object. */
777
+ config: Record<string, unknown>;
778
+ }
779
+ interface PageSpec {
780
+ /** The arrangement of slots, for state kept per layout. */
781
+ layout: string;
782
+ islands: IslandSpec[];
783
+ }
784
+ /**
785
+ * Reads an app's own fields from the parsed spec, the ones its Go type adds
786
+ * by embedding page.Spec. Gets the whole object; returns what the app wants
787
+ * kept, already checked.
788
+ */
789
+ type SpecExtension<Ext> = (raw: Record<string, unknown>) => Ext;
790
+ /**
791
+ * The spec in `text`, or null when there is none or it isn't one. Islands
792
+ * without a name or with a slot name that isn't plain are dropped, and a
793
+ * config that isn't an object becomes {}, so what comes back can be mounted.
794
+ *
795
+ * `extend`, when given, reads the app's own fields; they're merged into the
796
+ * result, but `layout` and `islands` always come from this reading. If it
797
+ * throws, the spec is unreadable and the result is null.
798
+ */
799
+ declare function readSpec(text: string | null | undefined): PageSpec | null;
800
+ declare function readSpec<Ext extends object>(text: string | null | undefined, extend: SpecExtension<Ext>): (PageSpec & Ext) | null;
801
+
802
+ /**
803
+ * Builds one island in `el`. `ctx` is the page's shared services, whatever
804
+ * the app says they are; `bus` is the page's event bus.
805
+ */
806
+ 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>>;
809
+ /**
810
+ * 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.
818
+ *
819
+ * Plain types throughout (no DOM), so it runs under node in tests and on a
820
+ * bare page without BasePage.
821
+ */
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[];
823
+
824
+ /**
825
+ * A page whose islands come from the page spec. A subclass says which
826
+ * islands it can mount (`registry`) and builds the services they share
827
+ * (`makeContext`); the spec says which islands this page gets and where.
828
+ *
829
+ * `makeContext` is called once with the spec, before the first island
830
+ * mounts, and not at all on a page with no islands to mount. An app whose
831
+ * spec carries more than islands (its Go type embeds page.Spec) reads those
832
+ * fields in `readExtension`, and they arrive typed as `Ext` on the spec
833
+ * `makeContext` gets.
834
+ *
835
+ * A page with no readable `#page-spec` mounts nothing and warns. Subclasses
836
+ * that override initializeSpecificComponents call super and add to what it
837
+ * returns.
838
+ */
839
+ declare abstract class IslandPage<Ctx, Ext extends object = {}> extends BasePage {
840
+ protected abstract registry(): Registry<Ctx, HTMLElement, LCMComponent, EventBus>;
841
+ protected abstract makeContext(spec: PageSpec & Ext): Ctx;
842
+ /**
843
+ * The app's own fields from the parsed spec. The default reads none.
844
+ * Throwing makes the spec unreadable, so nothing mounts.
845
+ */
846
+ protected readExtension(raw: Record<string, unknown>): Ext;
847
+ protected initializeSpecificComponents(): LCMComponent[];
848
+ }
849
+
763
850
  /**
764
851
  * Common DOM utility functions for consistent behavior across components
765
852
  */
@@ -863,4 +950,4 @@ declare class KeyboardShortcutManager {
863
950
  getCurrentArgs(): string;
864
951
  }
865
952
 
866
- export { BaseComponent, BasePage, type Component, type ComponentEventType, ComponentEventTypes, type ComponentLifecycleEvent, EventBus, type EventHandler, type EventSubscriber, KeyboardShortcutManager, KeyboardState, type LCMComponent, type LCMComponentConfig, type LCMComponentEvent, LifecycleController, type LifecycleEventType, LifecycleEventTypes, MobileBottomDrawer, Modal, type ShortcutConfig, type ShortcutManagerConfig, SplashScreen, TemplateLoader, ThemeManager, ToastManager, hasModifierKeys, isInInputContext, shouldIgnoreShortcut };
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 };
package/dist/index.d.ts CHANGED
@@ -760,6 +760,93 @@ declare class MobileBottomDrawer extends BaseComponent implements LCMComponent {
760
760
  getContentContainer(): HTMLElement;
761
761
  }
762
762
 
763
+ /**
764
+ * The page spec: which islands a page mounts, where, and with what config.
765
+ * The server writes it into the page as JSON (goapplib's page.Spec, through
766
+ * templates/page/Islands.html); this reads it back.
767
+ */
768
+ /** The id of the script element the server writes the spec into. */
769
+ declare const SPEC_ELEMENT_ID = "page-spec";
770
+ /** One island: `name` picks its factory, `slot` the element it mounts in (`[data-slot]`). */
771
+ interface IslandSpec {
772
+ name: string;
773
+ slot: string;
774
+ /** How the layout shows it (the app's own words: "page", "panel", "drawer"), when it draws differently in each. */
775
+ presentation?: string;
776
+ /** Handed to the factory as is. Always an object. */
777
+ config: Record<string, unknown>;
778
+ }
779
+ interface PageSpec {
780
+ /** The arrangement of slots, for state kept per layout. */
781
+ layout: string;
782
+ islands: IslandSpec[];
783
+ }
784
+ /**
785
+ * Reads an app's own fields from the parsed spec, the ones its Go type adds
786
+ * by embedding page.Spec. Gets the whole object; returns what the app wants
787
+ * kept, already checked.
788
+ */
789
+ type SpecExtension<Ext> = (raw: Record<string, unknown>) => Ext;
790
+ /**
791
+ * The spec in `text`, or null when there is none or it isn't one. Islands
792
+ * without a name or with a slot name that isn't plain are dropped, and a
793
+ * config that isn't an object becomes {}, so what comes back can be mounted.
794
+ *
795
+ * `extend`, when given, reads the app's own fields; they're merged into the
796
+ * result, but `layout` and `islands` always come from this reading. If it
797
+ * throws, the spec is unreadable and the result is null.
798
+ */
799
+ declare function readSpec(text: string | null | undefined): PageSpec | null;
800
+ declare function readSpec<Ext extends object>(text: string | null | undefined, extend: SpecExtension<Ext>): (PageSpec & Ext) | null;
801
+
802
+ /**
803
+ * Builds one island in `el`. `ctx` is the page's shared services, whatever
804
+ * the app says they are; `bus` is the page's event bus.
805
+ */
806
+ 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>>;
809
+ /**
810
+ * 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.
818
+ *
819
+ * Plain types throughout (no DOM), so it runs under node in tests and on a
820
+ * bare page without BasePage.
821
+ */
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[];
823
+
824
+ /**
825
+ * A page whose islands come from the page spec. A subclass says which
826
+ * islands it can mount (`registry`) and builds the services they share
827
+ * (`makeContext`); the spec says which islands this page gets and where.
828
+ *
829
+ * `makeContext` is called once with the spec, before the first island
830
+ * mounts, and not at all on a page with no islands to mount. An app whose
831
+ * spec carries more than islands (its Go type embeds page.Spec) reads those
832
+ * fields in `readExtension`, and they arrive typed as `Ext` on the spec
833
+ * `makeContext` gets.
834
+ *
835
+ * A page with no readable `#page-spec` mounts nothing and warns. Subclasses
836
+ * that override initializeSpecificComponents call super and add to what it
837
+ * returns.
838
+ */
839
+ declare abstract class IslandPage<Ctx, Ext extends object = {}> extends BasePage {
840
+ protected abstract registry(): Registry<Ctx, HTMLElement, LCMComponent, EventBus>;
841
+ protected abstract makeContext(spec: PageSpec & Ext): Ctx;
842
+ /**
843
+ * The app's own fields from the parsed spec. The default reads none.
844
+ * Throwing makes the spec unreadable, so nothing mounts.
845
+ */
846
+ protected readExtension(raw: Record<string, unknown>): Ext;
847
+ protected initializeSpecificComponents(): LCMComponent[];
848
+ }
849
+
763
850
  /**
764
851
  * Common DOM utility functions for consistent behavior across components
765
852
  */
@@ -863,4 +950,4 @@ declare class KeyboardShortcutManager {
863
950
  getCurrentArgs(): string;
864
951
  }
865
952
 
866
- export { BaseComponent, BasePage, type Component, type ComponentEventType, ComponentEventTypes, type ComponentLifecycleEvent, EventBus, type EventHandler, type EventSubscriber, KeyboardShortcutManager, KeyboardState, type LCMComponent, type LCMComponentConfig, type LCMComponentEvent, LifecycleController, type LifecycleEventType, LifecycleEventTypes, MobileBottomDrawer, Modal, type ShortcutConfig, type ShortcutManagerConfig, SplashScreen, TemplateLoader, ThemeManager, ToastManager, hasModifierKeys, isInInputContext, shouldIgnoreShortcut };
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 };
package/dist/index.js CHANGED
@@ -1373,6 +1373,93 @@ var MobileBottomDrawer = class extends BaseComponent {
1373
1373
  }
1374
1374
  };
1375
1375
 
1376
+ // src/page/mount.ts
1377
+ function mountIslands(spec, registry, findSlot, context, bus, log) {
1378
+ const out = [];
1379
+ let ctx;
1380
+ for (const island of spec.islands) {
1381
+ const factory = Object.prototype.hasOwnProperty.call(registry, island.name) ? registry[island.name] : void 0;
1382
+ if (!factory) {
1383
+ log(`page spec: no island called "${island.name}" in this page's registry`);
1384
+ continue;
1385
+ }
1386
+ const el = findSlot(island.slot);
1387
+ if (el === null) {
1388
+ log(`page spec: island "${island.name}" wants slot "${island.slot}", which isn't on the page`);
1389
+ continue;
1390
+ }
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)}`);
1396
+ }
1397
+ }
1398
+ return out;
1399
+ }
1400
+
1401
+ // src/page/spec.ts
1402
+ var SPEC_ELEMENT_ID = "page-spec";
1403
+ var SLOT = /^[a-z][a-z0-9-]*$/;
1404
+ function readSpec(text, extend) {
1405
+ if (!text) return null;
1406
+ let raw;
1407
+ try {
1408
+ raw = JSON.parse(text);
1409
+ } catch {
1410
+ return null;
1411
+ }
1412
+ if (!isObject(raw) || typeof raw.layout !== "string" || !Array.isArray(raw.islands)) return null;
1413
+ const islands = [];
1414
+ for (const is of raw.islands) {
1415
+ if (!isObject(is) || typeof is.name !== "string" || !is.name || typeof is.slot !== "string" || !SLOT.test(is.slot)) continue;
1416
+ islands.push({
1417
+ name: is.name,
1418
+ slot: is.slot,
1419
+ ...typeof is.presentation === "string" && { presentation: is.presentation },
1420
+ config: isObject(is.config) ? is.config : {}
1421
+ });
1422
+ }
1423
+ const spec = { layout: raw.layout, islands };
1424
+ if (!extend) return spec;
1425
+ let ext;
1426
+ try {
1427
+ ext = extend(raw);
1428
+ } catch {
1429
+ return null;
1430
+ }
1431
+ return { ...ext, ...spec };
1432
+ }
1433
+ function isObject(v) {
1434
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1435
+ }
1436
+
1437
+ // src/page/IslandPage.ts
1438
+ var IslandPage = class extends BasePage {
1439
+ /**
1440
+ * The app's own fields from the parsed spec. The default reads none.
1441
+ * Throwing makes the spec unreadable, so nothing mounts.
1442
+ */
1443
+ readExtension(raw) {
1444
+ return {};
1445
+ }
1446
+ initializeSpecificComponents() {
1447
+ const spec = readSpec(document.getElementById(SPEC_ELEMENT_ID)?.textContent, (raw) => this.readExtension(raw));
1448
+ if (!spec) {
1449
+ console.warn(`page spec: no readable #${SPEC_ELEMENT_ID} on this page, so nothing is mounted`);
1450
+ return [];
1451
+ }
1452
+ return mountIslands(
1453
+ spec,
1454
+ this.registry(),
1455
+ (slot) => document.querySelector(`[data-slot="${slot}"]`),
1456
+ () => this.makeContext(spec),
1457
+ this.eventBus,
1458
+ (message) => console.warn(message)
1459
+ );
1460
+ }
1461
+ };
1462
+
1376
1463
  // src/DOMUtils.ts
1377
1464
  function isInInputContext(element) {
1378
1465
  if (!element) return false;
@@ -1719,18 +1806,22 @@ exports.BaseComponent = BaseComponent;
1719
1806
  exports.BasePage = BasePage;
1720
1807
  exports.ComponentEventTypes = ComponentEventTypes;
1721
1808
  exports.EventBus = EventBus;
1809
+ exports.IslandPage = IslandPage;
1722
1810
  exports.KeyboardShortcutManager = KeyboardShortcutManager;
1723
1811
  exports.KeyboardState = KeyboardState;
1724
1812
  exports.LifecycleController = LifecycleController;
1725
1813
  exports.LifecycleEventTypes = LifecycleEventTypes;
1726
1814
  exports.MobileBottomDrawer = MobileBottomDrawer;
1727
1815
  exports.Modal = Modal;
1816
+ exports.SPEC_ELEMENT_ID = SPEC_ELEMENT_ID;
1728
1817
  exports.SplashScreen = SplashScreen;
1729
1818
  exports.TemplateLoader = TemplateLoader;
1730
1819
  exports.ThemeManager = ThemeManager;
1731
1820
  exports.ToastManager = ToastManager;
1732
1821
  exports.hasModifierKeys = hasModifierKeys;
1733
1822
  exports.isInInputContext = isInInputContext;
1823
+ exports.mountIslands = mountIslands;
1824
+ exports.readSpec = readSpec;
1734
1825
  exports.shouldIgnoreShortcut = shouldIgnoreShortcut;
1735
1826
  //# sourceMappingURL=index.js.map
1736
1827
  //# sourceMappingURL=index.js.map