@gratiaos/pad-core 1.0.4 β†’ 1.0.5

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/README.md CHANGED
@@ -10,6 +10,20 @@ Now extended with **Realtime Presence**, **Scene Events**, and **P2P awareness**
10
10
  It’s framework-agnostic, DOM-optional, and designed to let Pads bloom in any app (Playground, M3 UI, future mirrors).
11
11
  The package is side-effect free (`"sideEffects": false`) so bundlers can tree-shake unused helpers.
12
12
 
13
+ ### πŸ”© Signals Interop
14
+
15
+ Pad Core itself is not opinionated about reactivity; for local observable state (Pad mood, ephemeral counters, lab toggles) prefer the tiny `@gratiaos/signal` package:
16
+
17
+ ```ts
18
+ import { createSignal } from '@gratiaos/signal';
19
+ const mood$ = createSignal<'idle' | 'focused'>('idle');
20
+ const stop = mood$.subscribe((m) => console.log('mood', m));
21
+ mood$.set('focused');
22
+ stop();
23
+ ```
24
+
25
+ Use `@gratiaos/presence-kernel` for shared cross-pad presence/phase signals; use `@gratiaos/signal` when you just need a local synchronous observable.
26
+
13
27
  ---
14
28
 
15
29
  ## 🌠 Vision
package/dist/catalog.js CHANGED
@@ -83,12 +83,16 @@ export function buildCatalogFromMany(registries, opts) {
83
83
  register: () => {
84
84
  throw new Error('immutable view');
85
85
  },
86
+ unregister: () => {
87
+ throw new Error('immutable view');
88
+ },
86
89
  get: (id) => merged.find((m) => m.id === id),
87
90
  list: () => [...merged],
88
91
  has: (id) => merged.some((m) => m.id === id),
89
92
  clear: () => {
90
93
  throw new Error('immutable view');
91
94
  },
95
+ subscribe: () => () => { },
92
96
  };
93
97
  return buildCatalog(fauxRegistry, opts);
94
98
  }
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * Keep this file small and stable: consumers import from here.
12
12
  */
13
13
  export type * from './types.js';
14
- export { createRegistry, sortPads, registerAll, globalRegistry } from './registry.js';
14
+ export { createRegistry, sortPads, registerAll, globalRegistry, getPadManifest, listPadManifests, type PadRegistryChange, } from './registry.js';
15
15
  export { DEFAULT_HASH_KEY, getActivePadId, setActivePadId, clearActivePadId, hrefForPad, onPadRouteChange } from './route.js';
16
16
  export { padEvents, dispatchPadOpen, dispatchPadClose, dispatchPadBulletinUpdated, onPadOpen, onPadClose, onPadBulletinUpdated } from './events.js';
17
17
  export { SCENE_ENTER, SCENE_COMPLETE, dispatchSceneEnter, dispatchSceneComplete, onSceneEnter, onSceneComplete, type SceneEnterDetail, type SceneCompleteDetail, type SceneVia, } from './scene-events.js';
@@ -19,3 +19,5 @@ export { uid, slug } from './id.js';
19
19
  export { type CatalogEntry, toCatalogEntry, buildCatalog, mergeRegistries, buildCatalogFromMany, filterCatalog, groupCatalog, pathForPad, findPad, ensurePad, } from './catalog.js';
20
20
  export * from './hooks/usePadMood';
21
21
  export { setRealtimePort, getRealtimePort, getRealtimeCircleId, onRealtimePortChange } from './realtime/registry.js';
22
+ export { createSignal, type Signal } from './signal.js';
23
+ export { padRegistry$, activePadId$, activeManifest$, scene$, flow$, announceSceneEnter, announceSceneLeave, getActivePadManifest, type FlowSnapshot, } from './state.js';
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * Keep this file small and stable: consumers import from here.
12
12
  */
13
- export { createRegistry, sortPads, registerAll, globalRegistry } from './registry.js';
13
+ export { createRegistry, sortPads, registerAll, globalRegistry, getPadManifest, listPadManifests, } from './registry.js';
14
14
  export { DEFAULT_HASH_KEY, getActivePadId, setActivePadId, clearActivePadId, hrefForPad, onPadRouteChange } from './route.js';
15
15
  export { padEvents, dispatchPadOpen, dispatchPadClose, dispatchPadBulletinUpdated, onPadOpen, onPadClose, onPadBulletinUpdated } from './events.js';
16
16
  // Scene events (Pad & Scene coordination)
@@ -21,3 +21,5 @@ export { toCatalogEntry, buildCatalog, mergeRegistries, buildCatalogFromMany, fi
21
21
  export * from './hooks/usePadMood';
22
22
  // Realtime registry (optional integration used by scene-events)
23
23
  export { setRealtimePort, getRealtimePort, getRealtimeCircleId, onRealtimePortChange } from './realtime/registry.js';
24
+ export { createSignal } from './signal.js';
25
+ export { padRegistry$, activePadId$, activeManifest$, scene$, flow$, announceSceneEnter, announceSceneLeave, getActivePadManifest, } from './state.js';
@@ -19,9 +19,23 @@
19
19
  * ```
20
20
  */
21
21
  import type { PadId, PadManifest } from './types.js';
22
+ export type PadRegistryChange = {
23
+ type: 'pad:register';
24
+ manifest: PadManifest;
25
+ } | {
26
+ type: 'pad:update';
27
+ manifest: PadManifest;
28
+ } | {
29
+ type: 'pad:unregister';
30
+ id: PadId;
31
+ } | {
32
+ type: 'pad:clear';
33
+ };
22
34
  export interface PadRegistry {
23
35
  /** Insert or replace a pad manifest by id. */
24
36
  register(manifest: PadManifest): void;
37
+ /** Remove a manifest by id (no-op if absent). */
38
+ unregister(id: PadId): void;
25
39
  /** Fetch a manifest by id (undefined if absent). */
26
40
  get(id: PadId): PadManifest | undefined;
27
41
  /** Return all manifests (unsorted). */
@@ -30,6 +44,8 @@ export interface PadRegistry {
30
44
  has(id: PadId): boolean;
31
45
  /** Remove all manifests. */
32
46
  clear(): void;
47
+ /** Listen for registry mutations. */
48
+ subscribe(listener: (change: PadRegistryChange) => void): () => void;
33
49
  }
34
50
  /**
35
51
  * Create a fresh registry with optional initial manifests.
@@ -46,3 +62,7 @@ export declare function registerAll(registry: PadRegistry, manifests: ReadonlyAr
46
62
  * Tests can import and call `globalRegistry.clear()` to isolate cases.
47
63
  */
48
64
  export declare const globalRegistry: PadRegistry;
65
+ /** Helper: fetch a manifest by id from the shared registry. */
66
+ export declare function getPadManifest(id: PadId | string): PadManifest | null;
67
+ /** Helper: list all manifests from the shared registry. */
68
+ export declare function listPadManifests(): PadManifest[];
package/dist/registry.js CHANGED
@@ -4,11 +4,31 @@
4
4
  */
5
5
  export function createRegistry(initial) {
6
6
  const map = new Map();
7
+ const listeners = new Set();
8
+ const notify = (change) => {
9
+ listeners.forEach((fn) => {
10
+ try {
11
+ fn(change);
12
+ }
13
+ catch {
14
+ // ignore listener errors to keep registry resilient
15
+ }
16
+ });
17
+ };
7
18
  for (const m of initial ?? [])
8
19
  map.set(m.id, m);
9
20
  return {
10
21
  register(m) {
22
+ // JS registries run on a single thread; capturing `existed` before `set`
23
+ // is safe and keeps notify semantics stable for listeners.
24
+ const existed = map.has(m.id);
11
25
  map.set(m.id, m);
26
+ notify({ type: existed ? 'pad:update' : 'pad:register', manifest: m });
27
+ },
28
+ unregister(id) {
29
+ const existed = map.delete(id);
30
+ if (existed)
31
+ notify({ type: 'pad:unregister', id });
12
32
  },
13
33
  get(id) {
14
34
  return map.get(id);
@@ -20,7 +40,17 @@ export function createRegistry(initial) {
20
40
  return map.has(id);
21
41
  },
22
42
  clear() {
23
- map.clear();
43
+ // No-op if already empty. Only emit a 'pad:clear' when at least one manifest was removed.
44
+ if (map.size > 0) {
45
+ map.clear();
46
+ notify({ type: 'pad:clear' });
47
+ }
48
+ },
49
+ subscribe(listener) {
50
+ listeners.add(listener);
51
+ return () => {
52
+ listeners.delete(listener);
53
+ };
24
54
  },
25
55
  };
26
56
  }
@@ -39,3 +69,11 @@ export function registerAll(registry, manifests) {
39
69
  * Tests can import and call `globalRegistry.clear()` to isolate cases.
40
70
  */
41
71
  export const globalRegistry = createRegistry();
72
+ /** Helper: fetch a manifest by id from the shared registry. */
73
+ export function getPadManifest(id) {
74
+ return globalRegistry.get(id) ?? null;
75
+ }
76
+ /** Helper: list all manifests from the shared registry. */
77
+ export function listPadManifests() {
78
+ return globalRegistry.list();
79
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Re-export signal primitive from presence-kernel to avoid duplicate implementations.
3
+ * Consolidation note: previously this file owned its own `createSignal`. Keeping this shim
4
+ * preserves import paths (`pad-core/src/signal`) while centralizing behavior in
5
+ * `@gratiaos/presence-kernel` (which itself delegates to `@gratiaos/signal`).
6
+ */
7
+ export type { SignalListener, Signal } from '@gratiaos/presence-kernel';
8
+ export { createSignal } from '@gratiaos/presence-kernel';
package/dist/signal.js ADDED
@@ -0,0 +1 @@
1
+ export { createSignal } from '@gratiaos/presence-kernel';
@@ -0,0 +1,29 @@
1
+ import type { PadId, PadManifest, SceneId } from './types.js';
2
+ import { type Signal } from './signal.js';
3
+ import { type Phase } from '@gratiaos/presence-kernel';
4
+ /**
5
+ * Phase coupling note:
6
+ * pad-core consumes the Presence Kernel's `Phase` union directly so pad scene logic
7
+ * (e.g. flow snapshots, focus handoff + announcements) stays aligned with global
8
+ * presence semantics. If either package evolves its phase states, update *both*.
9
+ *
10
+ * Rationale: avoiding a second alias layer prevents silent divergence ("archive" vs
11
+ * "archived" etc.). If pad-core ever needs extra pad-only phases, extend here via:
12
+ * export type PadPhase = Phase | 'pad-extra';
13
+ * and adjust downstream snapshots & UI affordances accordingly.
14
+ */
15
+ export type PadPhase = Phase;
16
+ export declare const padRegistry$: Signal<PadManifest[]>;
17
+ export declare const activePadId$: Signal<PadId | null>;
18
+ export declare const activeManifest$: Signal<PadManifest | null>;
19
+ export declare const scene$: Signal<SceneId | null>;
20
+ export type FlowSnapshot = {
21
+ pad: PadManifest | null;
22
+ scene: SceneId | null;
23
+ phase: Phase;
24
+ t: number;
25
+ };
26
+ export declare const flow$: Signal<FlowSnapshot>;
27
+ export declare function getActivePadManifest(): PadManifest | null;
28
+ export declare function announceSceneEnter(sceneId: SceneId | null): void;
29
+ export declare function announceSceneLeave(sceneId?: SceneId | null): void;
package/dist/state.js ADDED
@@ -0,0 +1,73 @@
1
+ import { createSignal } from './signal.js';
2
+ import { getActivePadId, onPadRouteChange } from './route.js';
3
+ import { getPadManifest, listPadManifests, globalRegistry } from './registry.js';
4
+ import { onSceneEnter } from './scene-events.js';
5
+ import { onPadClose } from './events.js';
6
+ import { phase$, pulse$ } from '@gratiaos/presence-kernel';
7
+ export const padRegistry$ = createSignal(listPadManifests());
8
+ // Global registry lives for the lifetime of the app/test process, so we keep this
9
+ // subscription active; tests rely on globalRegistry.clear() to reset state.
10
+ globalRegistry.subscribe(() => {
11
+ padRegistry$.set(listPadManifests());
12
+ });
13
+ export const activePadId$ = createSignal(getActivePadId());
14
+ onPadRouteChange((id) => {
15
+ activePadId$.set(id);
16
+ });
17
+ export const activeManifest$ = createSignal(activePadId$.value ? getPadManifest(activePadId$.value) : null);
18
+ activePadId$.subscribe((id) => {
19
+ activeManifest$.set(id ? getPadManifest(id) : null);
20
+ });
21
+ padRegistry$.subscribe(() => {
22
+ const id = activePadId$.value;
23
+ activeManifest$.set(id ? getPadManifest(id) : null);
24
+ });
25
+ export const scene$ = createSignal(null);
26
+ onSceneEnter((event) => {
27
+ const detail = event.detail;
28
+ if (!detail)
29
+ return;
30
+ scene$.set(detail.sceneId ?? null);
31
+ });
32
+ onPadClose((detail) => {
33
+ if (!detail || !detail.id || detail.id === activePadId$.value) {
34
+ scene$.set(null);
35
+ }
36
+ });
37
+ activePadId$.subscribe(() => {
38
+ scene$.set(null);
39
+ });
40
+ const flow$Signal = createSignal({
41
+ pad: activeManifest$.value,
42
+ scene: scene$.value,
43
+ phase: phase$.value,
44
+ t: pulse$.value,
45
+ });
46
+ const flowEquals = (a, b) => a.pad === b.pad && a.scene === b.scene && a.phase === b.phase && a.t === b.t;
47
+ const refreshFlow = () => {
48
+ const next = {
49
+ pad: activeManifest$.value,
50
+ scene: scene$.value,
51
+ phase: phase$.value,
52
+ t: pulse$.value,
53
+ };
54
+ if (flowEquals(flow$Signal.value, next))
55
+ return;
56
+ flow$Signal.set(next);
57
+ };
58
+ activeManifest$.subscribe(refreshFlow);
59
+ scene$.subscribe(refreshFlow);
60
+ phase$.subscribe(refreshFlow);
61
+ pulse$.subscribe(refreshFlow);
62
+ export const flow$ = flow$Signal;
63
+ export function getActivePadManifest() {
64
+ return activeManifest$.value;
65
+ }
66
+ export function announceSceneEnter(sceneId) {
67
+ scene$.set(sceneId ?? null);
68
+ }
69
+ export function announceSceneLeave(sceneId) {
70
+ if (!sceneId || scene$.value === sceneId) {
71
+ scene$.set(null);
72
+ }
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gratiaos/pad-core",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Shared pad contract, registry, routing, and realtime events for Garden apps.",
5
5
  "funding": "https://github.com/sponsors/GratiaOS",
6
6
  "type": "module",
@@ -10,6 +10,9 @@
10
10
  "registry": "https://registry.npmjs.org/",
11
11
  "provenance": true
12
12
  },
13
+ "dependencies": {
14
+ "@gratiaos/presence-kernel": "workspace:*"
15
+ },
13
16
  "repository": {
14
17
  "type": "git",
15
18
  "url": "git+https://github.com/GratiaOS/garden-core.git",