@panyam/tsappkit 0.0.5 → 0.1.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 +88 -1
- package/dist/index.d.ts +88 -1
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +88 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -3
- package/src/index.ts +7 -0
- package/src/page/IslandPage.test.ts +83 -0
- package/src/page/IslandPage.ts +49 -0
- package/src/page/mount.test.ts +73 -0
- package/src/page/mount.ts +54 -0
- package/src/page/spec.test.ts +65 -0
- package/src/page/spec.ts +79 -0
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
|